diff --git a/.github/workflows/apidocs.yml b/.github/workflows/apidocs.yml index 4f8b6731a..87da78b75 100644 --- a/.github/workflows/apidocs.yml +++ b/.github/workflows/apidocs.yml @@ -30,11 +30,6 @@ jobs: source .venv/bin/activate python3 resources/scripts/apidocs.py - - name: Run graphdocs script - run: | - source .venv/bin/activate - python3 resources/scripts/graphdocs.py - - uses: stefanzweifel/git-auto-commit-action@v5 with: - commit_message: Update apidocs and/or graphdocs + commit_message: Update apidocs on ${{ github.head_ref }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 021b70d66..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: deploy -on: - workflow_run: - workflows: ["test"] - types: - - completed - -jobs: - deploy-to-dockerhub: - runs-on: ubuntu-latest - if: github.event.workflow_run.conclusion == 'success' - steps: - - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_RPC_REPO }} - tags: | - type=ref,event=tag - type=ref,event=pr - type=raw,value=latest,enable={{is_default_branch}} - labels: | - maintainer=bitcoindevproject - org.opencontainers.image.title=warnet-rpc - org.opencontainers.image.description=Warnet RPC server - - name: Login to Docker Hub - if: github.ref == 'refs/heads/main' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build production RPC image - uses: docker/build-push-action@v5 - with: - file: resources/images/rpc/Dockerfile_prod - platforms: linux/amd64,linux/arm64 - context: . - push: ${{ github.ref == 'refs/heads/main' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Build dev RPC image - uses: docker/build-push-action@v5 - with: - file: resources/images/rpc/Dockerfile_dev - platforms: linux/amd64,linux/arm64 - context: resources/images/rpc - push: ${{ github.ref == 'refs/heads/main' }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_RPC_REPO }}:dev - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index d228464e0..b0dad4cf7 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distribution 📦 to PyPI +name: Publish 🐍 📦 to PyPI on: push @@ -6,13 +6,12 @@ jobs: build: name: Build distribution 📦 runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.x" + python-version: '3.12' - name: Install pypa/build run: >- python3 -m @@ -22,7 +21,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ @@ -42,7 +41,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ @@ -63,12 +62,12 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: >- ./dist/*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0524bed9c..e8a5a5573 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,108 +4,98 @@ on: pull_request: push: branches: - - main + - '**' + +env: + PYTHON_VERSION: "3.12" + STERN_VERSION: "1.30.0" jobs: - # ruff: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: chartboost/ruff-action@v1 - # with: - # args: 'check .' - ruff-format: + + ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - args: 'format --check .' - build-image: - # needs: [ruff, ruff-format] - needs: [ruff-format] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and export - uses: docker/build-push-action@v5 + version: "0.6.11" + enable-cache: true + - name: Lint + uses: astral-sh/ruff-action@v3 with: - file: resources/images/rpc/Dockerfile_prod - context: . - tags: warnet/dev - cache-from: type=gha - cache-to: type=gha,mode=max - outputs: type=docker,dest=/tmp/warnet.tar - - - name: Upload artifact - uses: actions/upload-artifact@v4 + args: "check --fix" + - name: Format + uses: astral-sh/ruff-action@v3 with: - name: warnet - path: /tmp/warnet.tar + args: "format --check --diff" + test: - needs: [build-image] + needs: [ruff] runs-on: ubuntu-latest strategy: matrix: - test: [scenarios_test.py, rpc_test.py, graph_test.py, ln_test.py, dag_connection_test.py, logging_test.py] + test: + - bitcoin_rpc_args_test.py + - conf_test.py + - dag_connection_test.py + - graph_test.py + - logging_test.py + - ln_basic_test.py + - ln_test.py + - onion_test.py + - plugin_test.py + - rpc_test.py + - services_test.py + - signet_test.py + - scenarios_test.py + - namespace_admin_test.py + - wargames_test.py steps: - uses: actions/checkout@v4 - - uses: hynek/setup-cached-uv@v1 - uses: azure/setup-helm@v4.2.0 - - uses: medyagh/setup-minikube@master + - name: start minikube + uses: medyagh/setup-minikube@v0.0.20 + id: minikube with: - mount-path: ${{ github.workspace }}:/mnt/src - - uses: actions/download-artifact@v4 + minikube-version: 1.37.0 + kubernetes-version: v1.34.0 + cpus: max + memory: 4000m + - name: Start minikube's loadbalancer tunnel + run: minikube tunnel &> /dev/null & + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 with: - name: warnet - path: /tmp - - run: | - echo loading the image directly into minikube docker - eval $(minikube -p minikube docker-env) - docker load --input /tmp/warnet.tar - docker image ls -a - - echo Installing warnet python package for cli - uv venv - uv pip install -e . - - echo "Contents of warnet-rpc-statefulset-dev.yaml being used:" - cat resources/manifests/warnet-rpc-statefulset-dev.yaml - - echo Setting up k8s - kubectl apply -f resources/manifests/namespace.yaml - kubectl apply -f resources/manifests/rbac-config.yaml - kubectl apply -f resources/manifests/warnet-rpc-service.yaml - kubectl apply -f resources/manifests/warnet-rpc-statefulset-dev.yaml - kubectl config set-context --current --namespace=warnet - - echo sleeping for 30s to give k8s time to boot - sleep 30 - kubectl describe pod rpc-0 - kubectl logs rpc-0 - - echo Waiting for rpc-0 to come online - until kubectl get pod rpc-0 --namespace=warnet; do - echo "Waiting for server to find pod rpc-0..." - sleep 4 - done - kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - shell: bash - - run: | - kubectl port-forward svc/rpc 9276:9276 & + version: "latest" + enable-cache: true + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + - name: Install project + run: uv sync --all-extras --dev - name: Run tests run: | source .venv/bin/activate ./test/${{matrix.test}} - # build-test: - # needs: [build-image] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: ./.github/actions/compose - # - run: ./test/build_branch_test.py compose + - name: Collect Kubernetes logs + if: always() + run: | + echo "Installing stern..." + curl -Lo stern.tar.gz https://github.com/stern/stern/releases/download/v${STERN_VERSION}/stern_${STERN_VERSION}_linux_amd64.tar.gz + tar zxvf stern.tar.gz + chmod +x stern + sudo mv stern /usr/local/bin/ + + # Run script + curl -O https://raw.githubusercontent.com/bitcoin-dev-project/warnet/main/resources/scripts/k8s-log-collector.sh + chmod +x k8s-log-collector.sh + ./k8s-log-collector.sh default + - name: Upload log artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: kubernetes-logs-${{ matrix.test }} + path: ./k8s-logs + retention-days: 5 diff --git a/.gitignore b/.gitignore index c42f6ba7f..35e704c67 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ warnet.egg-info .env dist/ build/ +**/kubeconfigs/ +src/warnet/_version.py diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..e06b03bfd --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,176 @@ +# Overview + +Warnet is designed for Bitcoin developers, researchers, and enthusiasts who want to safely experiment with and test ideas related to Bitcoin networks. By leveraging Kubernetes, Warnet enables users to deploy and simulate large-scale Bitcoin networks in a controlled and reproducible environment. + +## Table of Contents + +1. [Overview](#overview) + - [Key Features](#key-features) + - [Target Audience](#target-audience) + - [Benefits](#benefits) + +2. [Philosophy](#philosophy) + +3. [Kubernetes and Helm in Warnet](#kubernetes-and-helm-in-warnet) + - [Kubernetes](#kubernetes) + - [Helm](#helm) + - [Why Helm is Preferred in Warnet](#why-helm-is-preferred-in-warnet) + +4. [Project Structure](#project-structure) + - [Overview of Resources](#overview-of-resources) + - [Overview of src/warnet](#overview-of-srcwarnet) + - [Overview of Test](#overview-of-test) + +5. [Operating in the Network with "Scenarios"/Commanders](#operating-in-the-network-with-scenarioscommanders) + +6. [The Resources Configuration Pipeline - An Example](#the-resources-configuration-pipeline---an-example) + +## Key Features: +- Safe simulation of Bitcoin networks +- Kubernetes-based deployment for scalability and ease of management +- User interaction capabilities through custom scenarios +- Infrastructure as Code (IaC) approach for reproducibility + +## Target Audience: +- Bitcoin core developers +- Bitcoin researchers +- Network security specialists +- Bitcoin protocol testers +- Bitcoin application developers +- Lightning developers +- Students of bitcoin +- And more! + +## Benefits: +1. Scalability: Simulate networks of various sizes to understand behaviour at scale +2. Reproducibility: Easily recreate test environments using IaC principles +3. Flexibility: Customize network configurations and scenarios to test specific hypotheses +4. Risk-free experimentation: Test new ideas without affecting real Bitcoin networks +5. Educational: Learn about Bitcoin network behaviour in a controlled setting + +## Philosophy + +The implementation should follow a native Kubernetes (via Helm) approach wherever possible (see [Kubernetes and Helm in Warnet](#kubernetes-and-helm-in-warnet) for more information on these applications). This simplifies development in this project, and offloads responsibility over (often) non-trivial components to the Kubernetes development team. Ideologically this means that all Warnet components should be thought of as "Kubernetes resources which are to be controlled (installed/uninstalled) using Helm", wherever possible. + +This philosophy applies to the implementation in this codebase: there are often multiple possible ways of implementing a new feature and developers should first seek out the way this is usually achieved on Kubernetes (natively) and encode this in a Helm chart, before falling back to a custom solution if this is not viable. + +For example, a new logging component could be added in multiple possible ways to Warnet: + +1. Run a process on the local host which connects in to the cluster via a forwarded port and performs customised logging. + +2. Launch a standardised logging application (e.g. Grafana) via executing a `kubectl` command on a *.yaml* file. + + ```python + subprocess.run("kubectl apply -f grafana.yaml --namespace=grafana") + ``` + +3. Create a helm chart (or use an [already-available](https://github.com/grafana/helm-charts) one) for the Grafana component and install using helm. + + ```python + subprocess.run("helm upgrade --install --namespace logging promtail grafana/promtail") + ``` + +Out of these three options, the third should be preferred where possible, followed by the second, with the first only being used in extreme cases. + +## Kubernetes and Helm in Warnet + +
You can access the following services:
+0: + self._on_data() + # Socket read methods def data_received(self, t): """asyncio callback when data is read from the socket.""" if len(t) > 0: self.recvbuf += t - self._on_data() + if self.supports_v2_p2p and not self.v2_state.tried_v2_handshake: + self._on_data_v2_handshake() + else: + self._on_data() def _on_data(self): """Try to read P2P messages from the recv buffer. @@ -236,23 +319,48 @@ def _on_data(self): the on_message callback for processing.""" try: while True: - if len(self.recvbuf) < 4: - return - if self.recvbuf[:4] != self.magic_bytes: - raise ValueError("magic bytes mismatch: {} != {}".format(repr(self.magic_bytes), repr(self.recvbuf))) - if len(self.recvbuf) < 4 + 12 + 4 + 4: - return - msgtype = self.recvbuf[4:4+12].split(b"\x00", 1)[0] - msglen = struct.unpack("= MIN_P2P_VERSION_SUPPORTED, "Version {} received. Test framework only supports versions greater than {}".format(message.nVersion, MIN_P2P_VERSION_SUPPORTED) + # for inbound connections, reply to version with own version message + # (could be due to v1 reconnect after a failed v2 handshake) + if not self.p2p_connected_to_node: + self.send_version() + self.reconnect = False if message.nVersion >= 70016 and self.wtxidrelay: - self.send_message(msg_wtxidrelay()) + self.send_without_ping(msg_wtxidrelay()) if self.support_addrv2: - self.send_message(msg_sendaddrv2()) - self.send_message(msg_verack()) + self.send_without_ping(msg_sendaddrv2()) + self.send_without_ping(msg_verack()) self.nServices = message.nServices self.relay = message.relay - self.send_message(msg_getaddr()) + if self.p2p_connected_to_node: + self.send_without_ping(msg_getaddr()) # Connection helper methods - def wait_until(self, test_function_in, *, timeout=60, check_connected=True): + def wait_until(self, test_function_in, *, timeout=60, check_connected=True, check_interval=0.05): def test_function(): if check_connected: assert self.is_connected return test_function_in() - wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor) + wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor, check_interval=check_interval) - def wait_for_connect(self, timeout=60): + def wait_for_connect(self, *, timeout=60): test_function = lambda: self.is_connected self.wait_until(test_function, timeout=timeout, check_connected=False) - def wait_for_disconnect(self, timeout=60): + def wait_for_disconnect(self, *, timeout=60): test_function = lambda: not self.is_connected self.wait_until(test_function, timeout=timeout, check_connected=False) + def wait_for_reconnect(self, *, timeout=60): + def test_function(): + return self.is_connected and self.last_message.get('version') and not self.supports_v2_p2p + self.wait_until(test_function, timeout=timeout, check_connected=False) + # Message receiving helper methods - def wait_for_tx(self, txid, timeout=60): + def wait_for_tx(self, txid, *, timeout=60): def test_function(): if not self.last_message.get('tx'): return False - return self.last_message['tx'].tx.rehash() == txid + return self.last_message['tx'].tx.txid_hex == txid self.wait_until(test_function, timeout=timeout) - def wait_for_block(self, blockhash, timeout=60): + def wait_for_block(self, blockhash, *, timeout=60): def test_function(): - return self.last_message.get("block") and self.last_message["block"].block.rehash() == blockhash + return self.last_message.get("block") and self.last_message["block"].block.hash_int == blockhash self.wait_until(test_function, timeout=timeout) - def wait_for_header(self, blockhash, timeout=60): + def wait_for_header(self, blockhash, *, timeout=60): def test_function(): last_headers = self.last_message.get('headers') if not last_headers: return False - return last_headers.headers[0].rehash() == int(blockhash, 16) + return last_headers.headers[0].hash_int == int(blockhash, 16) self.wait_until(test_function, timeout=timeout) - def wait_for_merkleblock(self, blockhash, timeout=60): + def wait_for_merkleblock(self, blockhash, *, timeout=60): def test_function(): last_filtered_block = self.last_message.get('merkleblock') if not last_filtered_block: return False - return last_filtered_block.merkleblock.header.rehash() == int(blockhash, 16) + return last_filtered_block.merkleblock.header.hash_int == int(blockhash, 16) self.wait_until(test_function, timeout=timeout) - def wait_for_getdata(self, hash_list, timeout=60): + def wait_for_getdata(self, hash_list, *, timeout=60): """Waits for a getdata message. The object hashes in the inventory vector must match the provided hash_list.""" @@ -522,19 +659,21 @@ def test_function(): self.wait_until(test_function, timeout=timeout) - def wait_for_getheaders(self, timeout=60): - """Waits for a getheaders message. + def wait_for_getheaders(self, block_hash=None, *, timeout=60): + """Waits for a getheaders message containing a specific block hash. - Receiving any getheaders message will satisfy the predicate. the last_message["getheaders"] - value must be explicitly cleared before calling this method, or this will return - immediately with success. TODO: change this method to take a hash value and only - return true if the correct block header has been requested.""" + If no block hash is provided, checks whether any getheaders message has been received by the node.""" def test_function(): - return self.last_message.get("getheaders") + last_getheaders = self.last_message.pop("getheaders", None) + if block_hash is None: + return last_getheaders + if last_getheaders is None: + return False + return block_hash == last_getheaders.locator.vHave[0] self.wait_until(test_function, timeout=timeout) - def wait_for_inv(self, expected_inv, timeout=60): + def wait_for_inv(self, expected_inv, *, timeout=60): """Waits for an INV message and checks that the first inv object in the message was as expected.""" if len(expected_inv) > 1: raise NotImplementedError("wait_for_inv() will only verify the first inv object") @@ -546,7 +685,7 @@ def test_function(): self.wait_until(test_function, timeout=timeout) - def wait_for_verack(self, timeout=60): + def wait_for_verack(self, *, timeout=60): def test_function(): return "verack" in self.last_message @@ -554,17 +693,22 @@ def test_function(): # Message sending helper functions - def send_and_ping(self, message, timeout=60): - self.send_message(message) + def send_version(self): + if self.on_connection_send_msg: + self.send_without_ping(self.on_connection_send_msg) + self.on_connection_send_msg = None # Never used again + + def send_and_ping(self, message, *, timeout=60): + self.send_without_ping(message) self.sync_with_ping(timeout=timeout) - def sync_with_ping(self, timeout=60): + def sync_with_ping(self, *, timeout=60): """Ensure ProcessMessages and SendMessages is called on this connection""" # Sending two pings back-to-back, requires that the node calls # `ProcessMessage` twice, and thus ensures `SendMessages` must have # been called at least once - self.send_message(msg_ping(nonce=0)) - self.send_message(msg_ping(nonce=self.ping_counter)) + self.send_without_ping(msg_ping(nonce=0)) + self.send_without_ping(msg_ping(nonce=self.ping_counter)) def test_function(): return self.last_message.get("pong") and self.last_message["pong"].nonce == self.ping_counter @@ -591,7 +735,7 @@ def __init__(self): NetworkThread.listeners = {} NetworkThread.protos = {} - if sys.platform == 'win32': + if platform.system() == 'Windows': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) NetworkThread.network_event_loop = asyncio.new_event_loop() @@ -599,7 +743,7 @@ def run(self): """Start the network thread.""" self.network_event_loop.run_forever() - def close(self, timeout=10): + def close(self, *, timeout=10): """Close the connections and network event loop.""" self.network_event_loop.call_soon_threadsafe(self.network_event_loop.stop) wait_until_helper_internal(lambda: not self.network_event_loop.is_running(), timeout=timeout) @@ -620,6 +764,11 @@ def listen(cls, p2p, callback, port=None, addr=None, idx=1): if addr is None: addr = '127.0.0.1' + def exception_handler(loop, context): + if not p2p.reconnect: + loop.default_exception_handler(context) + + cls.network_event_loop.set_exception_handler(exception_handler) coroutine = cls.create_listen_server(addr, port, callback, p2p) cls.network_event_loop.call_soon_threadsafe(cls.network_event_loop.create_task, coroutine) @@ -633,7 +782,9 @@ def peer_protocol(): protocol function from that dict, and returns it so the event loop can start executing it.""" response = cls.protos.get((addr, port)) - cls.protos[(addr, port)] = None + # remove protocol function from dict only when reconnection doesn't need to happen/already happened + if not proto.reconnect: + cls.protos[(addr, port)] = None return response if (addr, port) not in cls.listeners: @@ -665,13 +816,14 @@ def __init__(self): self.getdata_requests = [] def on_getdata(self, message): - """Check for the tx/block in our stores and if found, reply with an inv message.""" + """Check for the tx/block in our stores and if found, reply with MSG_TX or MSG_BLOCK.""" for inv in message.inv: self.getdata_requests.append(inv.hash) - if (inv.type & MSG_TYPE_MASK) == MSG_TX and inv.hash in self.tx_store.keys(): - self.send_message(msg_tx(self.tx_store[inv.hash])) - elif (inv.type & MSG_TYPE_MASK) == MSG_BLOCK and inv.hash in self.block_store.keys(): - self.send_message(msg_block(self.block_store[inv.hash])) + invtype = inv.type & MSG_TYPE_MASK + if (invtype == MSG_TX or invtype == MSG_WTX) and inv.hash in self.tx_store.keys(): + self.send_without_ping(msg_tx(self.tx_store[inv.hash])) + elif invtype == MSG_BLOCK and inv.hash in self.block_store.keys(): + self.send_without_ping(msg_block(self.block_store[inv.hash])) else: logger.debug('getdata message type {} received.'.format(hex(inv.type))) @@ -685,14 +837,14 @@ def on_getheaders(self, message): return headers_list = [self.block_store[self.last_block_hash]] - while headers_list[-1].sha256 not in locator.vHave: + while headers_list[-1].hash_int not in locator.vHave: # Walk back through the block store, adding headers to headers_list # as we go. prev_block_hash = headers_list[-1].hashPrevBlock if prev_block_hash in self.block_store: prev_block_header = CBlockHeader(self.block_store[prev_block_hash]) headers_list.append(prev_block_header) - if prev_block_header.sha256 == hash_stop: + if prev_block_header.hash_int == hash_stop: # if this is the hashstop header, stop here break else: @@ -704,9 +856,9 @@ def on_getheaders(self, message): response = msg_headers(headers_list) if response is not None: - self.send_message(response) + self.send_without_ping(response) - def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, reject_reason=None, expect_disconnect=False, timeout=60): + def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, reject_reason=None, expect_disconnect=False, timeout=60, is_decoy=False): """Send blocks to test node and test whether the tip advances. - add all blocks to our block_store @@ -720,18 +872,20 @@ def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, with p2p_lock: for block in blocks: - self.block_store[block.sha256] = block - self.last_block_hash = block.sha256 + self.block_store[block.hash_int] = block + self.last_block_hash = block.hash_int reject_reason = [reject_reason] if reject_reason else [] with node.assert_debug_log(expected_msgs=reject_reason): + if is_decoy: # since decoy messages are ignored by the recipient - no need to wait for response + force_send = True if force_send: for b in blocks: - self.send_message(msg_block(block=b)) + self.send_without_ping(msg_block(block=b), is_decoy) else: - self.send_message(msg_headers([CBlockHeader(block) for block in blocks])) + self.send_without_ping(msg_headers([CBlockHeader(block) for block in blocks])) self.wait_until( - lambda: blocks[-1].sha256 in self.getdata_requests, + lambda: blocks[-1].hash_int in self.getdata_requests, timeout=timeout, check_connected=success, ) @@ -742,47 +896,43 @@ def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, self.sync_with_ping(timeout=timeout) if success: - self.wait_until(lambda: node.getbestblockhash() == blocks[-1].hash, timeout=timeout) + self.wait_until(lambda: node.getbestblockhash() == blocks[-1].hash_hex, timeout=timeout) else: - assert node.getbestblockhash() != blocks[-1].hash + assert_not_equal(node.getbestblockhash(), blocks[-1].hash_hex) - def send_txs_and_test(self, txs, node, *, success=True, expect_disconnect=False, reject_reason=None): + def send_txs_and_test(self, txs, node, *, success=True, reject_reason=None): """Send txs to test node and test whether they're accepted to the mempool. - add all txs to our tx_store - send tx messages for all txs - if success is True/False: assert that the txs are/are not accepted to the mempool - - if expect_disconnect is True: Skip the sync with ping - if reject_reason is set: assert that the correct reject message is logged.""" with p2p_lock: for tx in txs: - self.tx_store[tx.sha256] = tx + self.tx_store[tx.txid_int] = tx reject_reason = [reject_reason] if reject_reason else [] with node.assert_debug_log(expected_msgs=reject_reason): for tx in txs: - self.send_message(msg_tx(tx)) + self.send_without_ping(msg_tx(tx)) - if expect_disconnect: - self.wait_for_disconnect() - else: - self.sync_with_ping() + self.sync_with_ping() raw_mempool = node.getrawmempool() if success: # Check that all txs are now in the mempool for tx in txs: - assert tx.hash in raw_mempool, "{} not found in mempool".format(tx.hash) + assert tx.txid_hex in raw_mempool, "{} not found in mempool".format(tx.txid_hex) else: # Check that none of the txs are now in the mempool for tx in txs: - assert tx.hash not in raw_mempool, "{} tx found in mempool".format(tx.hash) + assert tx.txid_hex not in raw_mempool, "{} tx found in mempool".format(tx.txid_hex) class P2PTxInvStore(P2PInterface): """A P2PInterface which stores a count of how many times each txid has been announced.""" - def __init__(self): - super().__init__() + def __init__(self, **kwargs): + super().__init__(**kwargs) self.tx_invs_received = defaultdict(int) def on_inv(self, message): @@ -797,7 +947,7 @@ def get_invs(self): with p2p_lock: return list(self.tx_invs_received.keys()) - def wait_for_broadcast(self, txns, timeout=60): + def wait_for_broadcast(self, txns, *, timeout=60): """Waits for the txns (list of txids) to complete initial broadcast. The mempool should mark unbroadcast=False for these transactions. """ diff --git a/src/test_framework/psbt.py b/resources/scenarios/test_framework/psbt.py similarity index 92% rename from src/test_framework/psbt.py rename to resources/scenarios/test_framework/psbt.py index 1eff4a250..4fe688ec0 100644 --- a/src/test_framework/psbt.py +++ b/resources/scenarios/test_framework/psbt.py @@ -50,6 +50,9 @@ PSBT_IN_TAP_BIP32_DERIVATION = 0x16 PSBT_IN_TAP_INTERNAL_KEY = 0x17 PSBT_IN_TAP_MERKLE_ROOT = 0x18 +PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a +PSBT_IN_MUSIG2_PUB_NONCE = 0x1b +PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c PSBT_IN_PROPRIETARY = 0xfc # per-output types @@ -61,6 +64,7 @@ PSBT_OUT_TAP_INTERNAL_KEY = 0x05 PSBT_OUT_TAP_TREE = 0x06 PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 +PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08 PSBT_OUT_PROPRIETARY = 0xfc @@ -88,6 +92,9 @@ def serialize(self): for k,v in self.map.items(): if isinstance(k, int) and 0 <= k and k <= 255: k = bytes([k]) + if isinstance(v, list): + assert all(type(elem) is bytes for elem in v) + v = b"".join(v) # simply concatenate the byte-strings w/o size prefixes m += ser_compact_size(len(k)) + k m += ser_compact_size(len(v)) + v m += b"\x00" diff --git a/src/test_framework/script.py b/resources/scenarios/test_framework/script.py similarity index 91% rename from src/test_framework/script.py rename to resources/scenarios/test_framework/script.py index 17a954cb2..f3cee7c66 100644 --- a/src/test_framework/script.py +++ b/resources/scenarios/test_framework/script.py @@ -8,9 +8,7 @@ """ from collections import namedtuple -import struct import unittest -from typing import List, Dict from .key import TaggedHash, tweak_add_pubkey, compute_xonly_pubkey @@ -19,14 +17,13 @@ CTxOut, hash256, ser_string, - ser_uint256, sha256, - uint256_from_str, ) -from .ripemd160 import ripemd160 +from .crypto.ripemd160 import ripemd160 MAX_SCRIPT_ELEMENT_SIZE = 520 +MAX_SCRIPT_SIZE = 10000 MAX_PUBKEYS_PER_MULTI_A = 999 LOCKTIME_THRESHOLD = 500000000 ANNEX_TAG = 0x50 @@ -59,9 +56,9 @@ def encode_op_pushdata(d): elif len(d) <= 0xff: return b'\x4c' + bytes([len(d)]) + d # OP_PUSHDATA1 elif len(d) <= 0xffff: - return b'\x4d' + struct.pack(b'OP_PUSHDATA4: @@ -591,7 +588,7 @@ def GetSigOpCount(self, fAccurate): n += 1 elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY): if fAccurate and (OP_1 <= lastOpcode <= OP_16): - n += opcode.decode_op_n() + n += lastOpcode.decode_op_n() else: n += 20 lastOpcode = opcode @@ -671,7 +668,7 @@ def LegacySignatureMsg(script, txTo, inIdx, hashtype): txtmp.vin.append(tmp) s = txtmp.serialize_without_witness() - s += struct.pack(b" 0 or len(program) in [20, 32] + return CScript([version, program]) + + +def script_to_p2wsh_script(script): + script = check_script(script) + return program_to_witness_script(0, sha256(script)) + + +def key_to_p2wpkh_script(key): + key = check_key(key) + return program_to_witness_script(0, hash160(key)) + + +def script_to_p2sh_p2wsh_script(script): + script = check_script(script) + p2shscript = CScript([OP_0, sha256(script)]) + return script_to_p2sh_script(p2shscript) + +def bulk_vout(tx, target_vsize): + if target_vsize < tx.get_vsize(): + raise RuntimeError(f"target_vsize {target_vsize} is less than transaction virtual size {tx.get_vsize()}") + # determine number of needed padding bytes + dummy_vbytes = target_vsize - tx.get_vsize() + # compensate for the increase of the compact-size encoded script length + # (note that the length encoding of the unpadded output script needs one byte) + dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1 + tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes) + assert_equal(tx.get_vsize(), target_vsize) + +def output_key_to_p2tr_script(key): + assert len(key) == 32 + return program_to_witness_script(1, key) + + +def check_key(key): + if isinstance(key, str): + key = bytes.fromhex(key) # Assuming this is hex string + if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): + return key + assert False + + +def check_script(script): + if isinstance(script, str): + script = bytes.fromhex(script) # Assuming this is hex string + if isinstance(script, bytes) or isinstance(script, CScript): + return script + assert False + + +class ValidWitnessMalleatedTx: + """ + Creates a valid witness malleation transaction test case: + - Parent transaction with a script supporting 2 branches + - 2 child transactions with the same txid but different wtxids + """ + def __init__(self): + hashlock = hash160(b'Preimage') + self.witness_script = CScript([OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF]) + + def build_parent_tx(self, funding_txid, amount): + # Create an unsigned parent transaction paying to the witness script. + witness_program = sha256(self.witness_script) + script_pubkey = CScript([OP_0, witness_program]) + + parent = CTransaction() + parent.vin.append(CTxIn(COutPoint(int(funding_txid, 16), 0), b"")) + parent.vout.append(CTxOut(int(amount), script_pubkey)) + return parent + + def build_malleated_children(self, signed_parent_txid, amount): + # Create 2 valid children that differ only in witness data. + # 1. Create a new transaction with witness solving first branch + child_witness_script = CScript([OP_TRUE]) + child_witness_program = sha256(child_witness_script) + child_script_pubkey = CScript([OP_0, child_witness_program]) + + child_one = CTransaction() + child_one.vin.append(CTxIn(COutPoint(int(signed_parent_txid, 16), 0), b"")) + child_one.vout.append(CTxOut(int(amount), child_script_pubkey)) + child_one.wit.vtxinwit.append(CTxInWitness()) + child_one.wit.vtxinwit[0].scriptWitness.stack = [b'Preimage', b'\x01', self.witness_script] + + # 2. Create another identical transaction with witness solving second branch + child_two = deepcopy(child_one) + child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', self.witness_script] + return child_one, child_two + + +class TestFrameworkScriptUtil(unittest.TestCase): + def test_multisig(self): + fake_pubkey = bytes([0]*33) + # check correct encoding of P2MS script with n,k <= 16 + normal_ms_script = keys_to_multisig_script([fake_pubkey]*16, k=15) + self.assertEqual(len(normal_ms_script), 1 + 16*34 + 1 + 1) + self.assertTrue(normal_ms_script.startswith(bytes([OP_15]))) + self.assertTrue(normal_ms_script.endswith(bytes([OP_16, OP_CHECKMULTISIG]))) + + # check correct encoding of P2MS script with n,k > 16 + max_ms_script = keys_to_multisig_script([fake_pubkey]*20, k=19) + self.assertEqual(len(max_ms_script), 2 + 20*34 + 2 + 1) + self.assertTrue(max_ms_script.startswith(bytes([1, 19]))) # using OP_PUSH1 + self.assertTrue(max_ms_script.endswith(bytes([1, 20, OP_CHECKMULTISIG]))) diff --git a/src/test_framework/segwit_addr.py b/resources/scenarios/test_framework/segwit_addr.py similarity index 100% rename from src/test_framework/segwit_addr.py rename to resources/scenarios/test_framework/segwit_addr.py diff --git a/src/test_framework/socks5.py b/resources/scenarios/test_framework/socks5.py similarity index 64% rename from src/test_framework/socks5.py rename to resources/scenarios/test_framework/socks5.py index 0ca06a739..0cd16a3ff 100644 --- a/src/test_framework/socks5.py +++ b/resources/scenarios/test_framework/socks5.py @@ -4,11 +4,16 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Dummy Socks5 server for testing.""" +import select import socket import threading import queue import logging +from .netutil import ( + format_addr_port +) + logger = logging.getLogger("TestFramework.socks5") # Protocol constants @@ -32,6 +37,42 @@ def recvall(s, n): n -= len(d) return rv +def sendall(s, data): + """Send all data to a socket, or fail.""" + sent = 0 + while sent < len(data): + _, wlist, _ = select.select([], [s], []) + if len(wlist) > 0: + n = s.send(data[sent:]) + if n == 0: + raise IOError('send() on socket returned 0') + sent += n + +def forward_sockets(a, b): + """Forward data received on socket a to socket b and vice versa, until EOF is received on one of the sockets.""" + # Mark as non-blocking so that we do not end up in a deadlock-like situation + # where we block and wait on data from `a` while there is data ready to be + # received on `b` and forwarded to `a`. And at the same time the application + # at `a` is not sending anything because it waits for the data from `b` to + # respond. + a.setblocking(False) + b.setblocking(False) + sockets = [a, b] + done = False + while not done: + rlist, _, xlist = select.select(sockets, [], sockets) + if len(xlist) > 0: + raise IOError('Exceptional condition on socket') + for s in rlist: + data = s.recv(4096) + if data is None or len(data) == 0: + done = True + break + if s == a: + sendall(b, data) + else: + sendall(a, data) + # Implementation classes class Socks5Configuration(): """Proxy configuration.""" @@ -41,6 +82,19 @@ def __init__(self): self.unauth = False # Support unauthenticated self.auth = False # Support authentication self.keep_alive = False # Do not automatically close connections + # This function is called whenever a new connection arrives to the proxy + # and it decides where the connection is redirected to. It is passed: + # - the address the client requested to connect to + # - the port the client requested to connect to + # It is supposed to return an object like: + # { + # "actual_to_addr": "127.0.0.1" + # "actual_to_port": 28276 + # } + # or None. + # If it returns an object then the connection is redirected to actual_to_addr:actual_to_port. + # If it returns None, or destinations_factory itself is None then the connection is closed. + self.destinations_factory = None class Socks5Command(): """Information about an incoming socks5 command.""" @@ -60,7 +114,7 @@ def __init__(self, serv, conn): self.conn = conn def handle(self): - """Handle socks5 request according to RFC192.""" + """Handle socks5 request according to RFC1928.""" try: # Verify socks version ver = recvall(self.conn, 1)[0] @@ -117,6 +171,22 @@ def handle(self): cmdin = Socks5Command(cmd, atyp, addr, port, username, password) self.serv.queue.put(cmdin) logger.debug('Proxy: %s', cmdin) + + requested_to_addr = addr.decode("utf-8") + requested_to = format_addr_port(requested_to_addr, port) + + if self.serv.conf.destinations_factory is not None: + dest = self.serv.conf.destinations_factory(requested_to_addr, port) + if dest is not None: + logger.debug(f"Serving connection to {requested_to}, will redirect it to " + f"{dest['actual_to_addr']}:{dest['actual_to_port']} instead") + with socket.create_connection((dest["actual_to_addr"], dest["actual_to_port"])) as conn_to: + forward_sockets(self.conn, conn_to) + else: + logger.debug(f"Can't serve the connection to {requested_to}: the destinations factory returned None") + else: + logger.debug(f"Can't serve the connection to {requested_to}: no destinations factory") + # Fall through to disconnect except Exception as e: logger.exception("socks5 request handling failed.") @@ -124,6 +194,8 @@ def handle(self): finally: if not self.serv.keep_alive: self.conn.close() + else: + logger.debug("Keeping client connection alive") class Socks5Server(): def __init__(self, conf): diff --git a/src/test_framework/test_framework.py b/resources/scenarios/test_framework/test_framework.py similarity index 71% rename from src/test_framework/test_framework.py rename to resources/scenarios/test_framework/test_framework.py index 4e6d245b5..0b9295cef 100755 --- a/src/test_framework/test_framework.py +++ b/resources/scenarios/test_framework/test_framework.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2022 The Bitcoin Core developers +# Copyright (c) 2014-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Base class for RPC testing.""" @@ -7,19 +7,22 @@ import configparser from enum import Enum import argparse +from datetime import datetime, timezone +import json import logging import os import platform import pdb import random import re +import shlex import shutil import subprocess import sys import tempfile import time +import types -from typing import List from .address import create_deterministic_address_bcrt1_p2tr_op_true from .authproxy import JSONRPCException from . import coverage @@ -30,10 +33,12 @@ PortSeed, assert_equal, check_json_precision, + find_vout_for_address, get_datadir_path, initialize_datadir, p2p_port, wait_until_helper_internal, + wallet_importprivkey, ) @@ -56,6 +61,63 @@ def __init__(self, message): self.message = message +class Binaries: + """Helper class to provide information about bitcoin binaries + + Attributes: + paths: Object returned from get_binary_paths() containing information + which binaries and command lines to use from environment variables and + the config file. + bin_dir: An optional string containing a directory path to look for + binaries, which takes precedence over the paths above, if specified. + This is used by tests calling binaries from previous releases. + """ + def __init__(self, paths, bin_dir): + self.paths = paths + self.bin_dir = bin_dir + + def node_argv(self, **kwargs): + "Return argv array that should be used to invoke bitcoind" + return self._argv("node", self.paths.bitcoind, **kwargs) + + def rpc_argv(self): + "Return argv array that should be used to invoke bitcoin-cli" + # Add -nonamed because "bitcoin rpc" enables -named by default, but bitcoin-cli doesn't + return self._argv("rpc", self.paths.bitcoincli) + ["-nonamed"] + + def tx_argv(self): + "Return argv array that should be used to invoke bitcoin-tx" + return self._argv("tx", self.paths.bitcointx) + + def util_argv(self): + "Return argv array that should be used to invoke bitcoin-util" + return self._argv("util", self.paths.bitcoinutil) + + def wallet_argv(self): + "Return argv array that should be used to invoke bitcoin-wallet" + return self._argv("wallet", self.paths.bitcoinwallet) + + def chainstate_argv(self): + "Return argv array that should be used to invoke bitcoin-chainstate" + return self._argv("chainstate", self.paths.bitcoinchainstate) + + def _argv(self, command, bin_path, need_ipc=False): + """Return argv array that should be used to invoke the command. It + either uses the bitcoin wrapper executable (if BITCOIN_CMD is set or + need_ipc is True), or the direct binary path (bitcoind, etc). When + bin_dir is set (by tests calling binaries from previous releases) it + always uses the direct path.""" + if self.bin_dir is not None: + return [os.path.join(self.bin_dir, os.path.basename(bin_path))] + elif self.paths.bitcoin_cmd is not None or need_ipc: + # If the current test needs IPC functionality, use the bitcoin + # wrapper binary and append -m so it calls multiprocess binaries. + bitcoin_cmd = self.paths.bitcoin_cmd or [self.paths.bitcoin_bin] + return bitcoin_cmd + (["-m"] if need_ipc else []) + [command] + else: + return [bin_path] + + class BitcoinTestMetaClass(type): """Metaclass for BitcoinTestFramework. @@ -92,18 +154,20 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): This class also contains various public and private helper methods.""" - def __init__(self) -> None: + def __init__(self, test_file) -> None: """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method""" self.chain: str = 'regtest' self.setup_clean_chain: bool = False - self.nodes: List[TestNode] = [] + self.noban_tx_relay: bool = False + self.nodes: list[TestNode] = [] self.extra_args = None + self.extra_init = None self.network_thread = None self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond self.supports_cli = True self.bind_to_localhost_only = True - self.parse_args() - self.default_wallet_name = "default_wallet" if self.options.descriptors else "" + self.parse_args(test_file) + self.default_wallet_name = "default_wallet" self.wallet_data_filename = "wallet.dat" # Optional list of wallet names that can be set in set_test_params to # create and import keys to. If unset, default is len(nodes) * @@ -112,8 +176,9 @@ def __init__(self) -> None: # are not imported. self.wallet_names = None # By default the wallet is not required. Set to true by skip_if_no_wallet(). - # When False, we ignore wallet_names regardless of what it is. - self._requires_wallet = False + # Can also be set to None to indicate that the wallet will be used if available. + # When False or None, we ignore wallet_names in setup_nodes(). + self.uses_wallet = False # Disable ThreadOpenConnections by default, so that adding entries to # addrman will not result in automatic connections to them. self.disable_autoconnect = True @@ -128,42 +193,39 @@ def main(self): try: self.setup() - self.run_test() - except JSONRPCException: - self.log.exception("JSONRPC error") - self.success = TestStatus.FAILED + if self.options.test_methods: + self.run_test_methods() + else: + self.run_test() + except SkipTest as e: self.log.warning("Test Skipped: %s" % e.message) self.success = TestStatus.SKIPPED - except AssertionError: - self.log.exception("Assertion failed") - self.success = TestStatus.FAILED - except KeyError: - self.log.exception("Key error") - self.success = TestStatus.FAILED except subprocess.CalledProcessError as e: - self.log.exception("Called Process failed with '{}'".format(e.output)) + self.log.exception(f"Called Process failed with stdout='{e.stdout}'; stderr='{e.stderr}';") self.success = TestStatus.FAILED - except Exception: - self.log.exception("Unexpected exception caught during testing") - self.success = TestStatus.FAILED - except KeyboardInterrupt: - self.log.warning("Exiting after keyboard interrupt") + except BaseException: + self.log.exception("Unexpected exception") self.success = TestStatus.FAILED finally: exit_code = self.shutdown() sys.exit(exit_code) - def parse_args(self): + def run_test_methods(self): + for method_name in self.options.test_methods: + self.log.info(f"Attempting to execute method: {method_name}") + method = getattr(self, method_name) + method() + self.log.info(f"Method '{method_name}' executed successfully.") + + def parse_args(self, test_file): previous_releases_path = os.getenv("PREVIOUS_RELEASES_DIR") or os.getcwd() + "/releases" parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") - parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", - help="Don't stop bitcoinds after the test execution") - parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), + parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(test_file) + "/../cache"), help="Directory for caching pregenerated datadirs (default: %(default)s)") - parser.add_argument("--tmpdir", dest="tmpdir", help="Root directory for datadirs") + parser.add_argument("--tmpdir", dest="tmpdir", help="Root directory for datadirs (must not exist)") parser.add_argument("-l", "--loglevel", dest="loglevel", default="INFO", help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.") parser.add_argument("--tracerpc", dest="trace_rpc", default=False, action="store_true", @@ -176,7 +238,7 @@ def parse_args(self): parser.add_argument("--coveragedir", dest="coveragedir", help="Write tested RPC commands into this directory") parser.add_argument("--configfile", dest="configfile", - default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../config.ini"), + default=os.path.abspath(os.path.dirname(test_file) + "/../config.ini"), help="Location of the test framework config file (default: %(default)s)") parser.add_argument("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true", help="Attach a python debugger if test fails") @@ -191,6 +253,10 @@ def parse_args(self): parser.add_argument("--timeout-factor", dest="timeout_factor", type=float, help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts") parser.add_argument("--v2transport", dest="v2transport", default=False, action="store_true", help="use BIP324 v2 connections between all nodes by default") + parser.add_argument("--v1transport", dest="v1transport", default=False, action="store_true", + help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)") + parser.add_argument("--test_methods", dest="test_methods", nargs='*', + help="Run specified test methods sequentially instead of the full test. Use only for methods that do not depend on any context set up in run_test or other methods.") self.add_options(parser) # Running TestShell in a Jupyter notebook causes an additional -f argument @@ -199,50 +265,47 @@ def parse_args(self): parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") self.options = parser.parse_args() if self.options.timeout_factor == 0: - self.options.timeout_factor = 99999 + self.options.timeout_factor = 999 self.options.timeout_factor = self.options.timeout_factor or (4 if self.options.valgrind else 1) self.options.previous_releases_path = previous_releases_path - config = configparser.ConfigParser() - config.read_file(open(self.options.configfile)) - self.config = config - - if "descriptors" not in self.options: - # Wallet is not required by the test at all and the value of self.options.descriptors won't matter. - # It still needs to exist and be None in order for tests to work however. - # So set it to None to force -disablewallet, because the wallet is not needed. - self.options.descriptors = None - elif self.options.descriptors is None: - # Some wallet is either required or optionally used by the test. - # Prefer SQLite unless it isn't available - if self.is_sqlite_compiled(): - self.options.descriptors = True - elif self.is_bdb_compiled(): - self.options.descriptors = False - else: - # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter - # It still needs to exist and be None in order for tests to work however. - # So set it to None, which will also set -disablewallet. - self.options.descriptors = None + self.config = configparser.ConfigParser() + self.config.read_file(open(self.options.configfile)) + self.binary_paths = self.get_binary_paths() + if self.options.v1transport: + self.options.v2transport=False PortSeed.n = self.options.port_seed - def set_binary_paths(self): - """Update self.options with the paths of all binaries from environment variables or their default values""" + def get_binary_paths(self): + """Get paths of all binaries from environment variables or their default values""" + paths = types.SimpleNamespace() binaries = { - "bitcoind": ("bitcoind", "BITCOIND"), - "bitcoin-cli": ("bitcoincli", "BITCOINCLI"), - "bitcoin-util": ("bitcoinutil", "BITCOINUTIL"), - "bitcoin-wallet": ("bitcoinwallet", "BITCOINWALLET"), + "bitcoin": "BITCOIN_BIN", + "bitcoind": "BITCOIND", + "bitcoin-cli": "BITCOINCLI", + "bitcoin-util": "BITCOINUTIL", + "bitcoin-tx": "BITCOINTX", + "bitcoin-chainstate": "BITCOINCHAINSTATE", + "bitcoin-wallet": "BITCOINWALLET", } - for binary, [attribute_name, env_variable_name] in binaries.items(): + # Set paths to bitcoin core binaries allowing overrides with environment + # variables. + for binary, env_variable_name in binaries.items(): default_filename = os.path.join( self.config["environment"]["BUILDDIR"], - "src", + "bin", binary + self.config["environment"]["EXEEXT"], ) - setattr(self.options, attribute_name, os.getenv(env_variable_name, default=default_filename)) + setattr(paths, env_variable_name.lower(), os.getenv(env_variable_name, default=default_filename)) + # BITCOIN_CMD environment variable can be specified to invoke bitcoin + # wrapper binary instead of other executables. + paths.bitcoin_cmd = shlex.split(os.getenv("BITCOIN_CMD", "")) or None + return paths + + def get_binaries(self, bin_dir=None): + return Binaries(self.binary_paths, bin_dir) def setup(self): """Call this method to start up the test framework object with options set.""" @@ -251,13 +314,9 @@ def setup(self): self.options.cachedir = os.path.abspath(self.options.cachedir) - config = self.config - - self.set_binary_paths() - os.environ['PATH'] = os.pathsep.join([ - os.path.join(config['environment']['BUILDDIR'], 'src'), - os.path.join(config['environment']['BUILDDIR'], 'src', 'qt'), os.environ['PATH'] + os.path.join(self.config["environment"]["BUILDDIR"], "bin"), + os.environ['PATH'] ]) # Set up temp directory and start logging @@ -307,18 +366,15 @@ def shutdown(self): self.log.debug('Closing down network thread') self.network_thread.close() - if not self.options.noshutdown: + if self.success == TestStatus.FAILED: + self.log.info("Not stopping nodes as test failed. The dangling processes will be cleaned up later.") + else: self.log.info("Stopping nodes") if self.nodes: self.stop_nodes() - else: - for node in self.nodes: - node.cleanup_on_exit = False - self.log.info("Note: bitcoinds were not stopped and may still be running") should_clean_up = ( not self.options.nocleanup and - not self.options.noshutdown and self.success != TestStatus.FAILED and not self.options.perf ) @@ -344,7 +400,7 @@ def shutdown(self): self.log.error("Hint: Call {} '{}' to consolidate all logs".format(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../combine_logs.py"), self.options.tmpdir)) self.log.error("") self.log.error("If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.") - self.log.error(self.config['environment']['PACKAGE_BUGREPORT']) + self.log.error(self.config['environment']['CLIENT_BUGREPORT']) self.log.error("") exit_code = TEST_EXIT_FAILED # Logging.shutdown will not remove stream- and filehandlers, so we must @@ -411,7 +467,7 @@ def setup_nodes(self): """Override this method to customize test node setup""" self.add_nodes(self.num_nodes, self.extra_args) self.start_nodes() - if self._requires_wallet: + if self.uses_wallet: self.import_deterministic_coinbase_privkeys() if not self.setup_clean_chain: for n in self.nodes: @@ -436,8 +492,8 @@ def init_wallet(self, *, node): if wallet_name is not False: n = self.nodes[node] if wallet_name is not None: - n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True) - n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase', rescan=True) + n.createwallet(wallet_name=wallet_name, load_on_startup=True) + wallet_importprivkey(n.get_wallet_rpc(wallet_name), n.get_deterministic_priv_key().key, 0, label="coinbase") def run_test(self): """Tests must override this method to define test logic""" @@ -445,29 +501,14 @@ def run_test(self): # Public helper methods. These can be accessed by the subclass test scripts. - def add_wallet_options(self, parser, *, descriptors=True, legacy=True): - kwargs = {} - if descriptors + legacy == 1: - # If only one type can be chosen, set it as default - kwargs["default"] = descriptors - group = parser.add_mutually_exclusive_group( - # If only one type is allowed, require it to be set in test_runner.py - required=os.getenv("REQUIRE_WALLET_TYPE_SET") == "1" and "default" in kwargs) - if descriptors: - group.add_argument("--descriptors", action='store_const', const=True, **kwargs, - help="Run test using a descriptor wallet", dest='descriptors') - if legacy: - group.add_argument("--legacy-wallet", action='store_const', const=False, **kwargs, - help="Run test using legacy wallets", dest='descriptors') - - def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=None, binary_cli=None, versions=None): + def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, versions=None): """Instantiate TestNode objects. Should only be called once after the nodes have been specified in set_test_params().""" - def get_bin_from_version(version, bin_name, bin_default): + def bin_dir_from_version(version): if not version: - return bin_default + return None if version > 219999: # Starting at client version 220000 the first two digits represent # the major version, e.g. v22.0 instead of v0.22.0. @@ -485,7 +526,6 @@ def get_bin_from_version(version, bin_name, bin_default): ), ), 'bin', - bin_name, ) if self.bind_to_localhost_only: @@ -494,30 +534,43 @@ def get_bin_from_version(version, bin_name, bin_default): extra_confs = [[]] * num_nodes if extra_args is None: extra_args = [[]] * num_nodes + # Whitelist peers to speed up tx relay / mempool sync. Don't use it if testing tx relay or timing. + if self.noban_tx_relay: + for i in range(len(extra_args)): + extra_args[i] = extra_args[i] + ["-whitelist=noban,in,out@127.0.0.1"] if versions is None: versions = [None] * num_nodes - if binary is None: - binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions] - if binary_cli is None: - binary_cli = [get_bin_from_version(v, 'bitcoin-cli', self.options.bitcoincli) for v in versions] + bin_dirs = [] + for v in versions: + bin_dir = bin_dir_from_version(v) + + # Fail test if any of the needed release binaries is missing + for bin_path in (argv[0] for binaries in (self.get_binaries(bin_dir),) + for argv in (binaries.node_argv(), binaries.rpc_argv())): + + if shutil.which(bin_path) is None: + self.log.error(f"Binary not found: {bin_path}") + if v is None: + raise AssertionError("At least one binary is missing, did you compile?") + raise AssertionError("At least one release binary is missing. " + "Previous releases binaries can be downloaded via `test/get_previous_releases.py`.") + + bin_dirs.append(bin_dir) + + extra_init = [{}] * num_nodes if self.extra_init is None else self.extra_init # type: ignore[var-annotated] + assert_equal(len(extra_init), num_nodes) assert_equal(len(extra_confs), num_nodes) assert_equal(len(extra_args), num_nodes) assert_equal(len(versions), num_nodes) - assert_equal(len(binary), num_nodes) - assert_equal(len(binary_cli), num_nodes) + assert_equal(len(bin_dirs), num_nodes) for i in range(num_nodes): args = list(extra_args[i]) - if self.options.v2transport and ("-v2transport=0" not in args): - args.append("-v2transport=1") - test_node_i = TestNode( - i, - get_datadir_path(self.options.tmpdir, i), + init = dict( chain=self.chain, rpchost=rpchost, timewait=self.rpc_timeout, timeout_factor=self.options.timeout_factor, - bitcoind=binary[i], - bitcoin_cli=binary_cli[i], + binaries=self.get_binaries(bin_dirs[i]), version=versions[i], coverage_dir=self.options.coveragedir, cwd=self.options.tmpdir, @@ -526,8 +579,14 @@ def get_bin_from_version(version, bin_name, bin_default): use_cli=self.options.usecli, start_perf=self.options.perf, use_valgrind=self.options.valgrind, - descriptors=self.options.descriptors, + v2transport=self.options.v2transport, + uses_wallet=self.uses_wallet, ) + init.update(extra_init[i]) + test_node_i = TestNode( + i, + get_datadir_path(self.options.tmpdir, i), + **init) self.nodes.append(test_node_i) if not test_node_i.version_is_at_least(170000): # adjust conf for pre 17 @@ -542,7 +601,7 @@ def start_node(self, i, *args, **kwargs): node.wait_for_rpc_connection() if self.options.coveragedir is not None: - coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) + coverage.write_all_rpc_commands(self.options.coveragedir, node._rpc) def start_nodes(self, extra_args=None, *args, **kwargs): """Start multiple bitcoinds""" @@ -550,19 +609,14 @@ def start_nodes(self, extra_args=None, *args, **kwargs): if extra_args is None: extra_args = [None] * self.num_nodes assert_equal(len(extra_args), self.num_nodes) - try: - for i, node in enumerate(self.nodes): - node.start(extra_args[i], *args, **kwargs) - for node in self.nodes: - node.wait_for_rpc_connection() - except Exception: - # If one node failed to start, stop the others - self.stop_nodes() - raise + for i, node in enumerate(self.nodes): + node.start(extra_args[i], *args, **kwargs) + for node in self.nodes: + node.wait_for_rpc_connection() if self.options.coveragedir is not None: for node in self.nodes: - coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) + coverage.write_all_rpc_commands(self.options.coveragedir, node._rpc) def stop_node(self, i, expected_stderr='', wait=0): """Stop a bitcoind test node""" @@ -578,10 +632,16 @@ def stop_nodes(self, wait=0): # Wait for nodes to stop node.wait_until_stopped() - def restart_node(self, i, extra_args=None): + def restart_node(self, i, extra_args=None, clear_addrman=False, *, expected_stderr=''): """Stop and start a test node""" - self.stop_node(i) - self.start_node(i, extra_args) + self.stop_node(i, expected_stderr=expected_stderr) + if clear_addrman: + peers_dat = self.nodes[i].chain_path / "peers.dat" + os.remove(peers_dat) + with self.nodes[i].assert_debug_log(expected_msgs=[f'Creating peers.dat because the file was not found ("{peers_dat}")']): + self.start_node(i, extra_args) + else: + self.start_node(i, extra_args) def wait_for_node_exit(self, i, timeout): self.nodes[i].process.wait(timeout) @@ -596,36 +656,43 @@ def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool """ from_connection = self.nodes[a] to_connection = self.nodes[b] - from_num_peers = 1 + len(from_connection.getpeerinfo()) - to_num_peers = 1 + len(to_connection.getpeerinfo()) ip_port = "127.0.0.1:" + str(p2p_port(b)) if peer_advertises_v2 is None: - peer_advertises_v2 = self.options.v2transport + peer_advertises_v2 = from_connection.use_v2transport - if peer_advertises_v2: - from_connection.addnode(node=ip_port, command="onetry", v2transport=True) + if peer_advertises_v2 != from_connection.use_v2transport: + from_connection.addnode(node=ip_port, command="onetry", v2transport=peer_advertises_v2) else: - # skip the optional third argument (default false) for + # skip the optional third argument if it matches the default, for # compatibility with older clients from_connection.addnode(ip_port, "onetry") if not wait_for_connect: return - # poll until version handshake complete to avoid race conditions - # with transaction relaying - # See comments in net_processing: - # * Must have a version message before anything else - # * Must have a verack message before anything else - self.wait_until(lambda: sum(peer['version'] != 0 for peer in from_connection.getpeerinfo()) == from_num_peers) - self.wait_until(lambda: sum(peer['version'] != 0 for peer in to_connection.getpeerinfo()) == to_num_peers) - self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) >= 21 for peer in from_connection.getpeerinfo()) == from_num_peers) - self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) >= 21 for peer in to_connection.getpeerinfo()) == to_num_peers) - # The message bytes are counted before processing the message, so make - # sure it was fully processed by waiting for a ping. - self.wait_until(lambda: sum(peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 for peer in from_connection.getpeerinfo()) == from_num_peers) - self.wait_until(lambda: sum(peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 for peer in to_connection.getpeerinfo()) == to_num_peers) + # Use subversion as peer id. Test nodes have their node number appended to the user agent string + from_connection_subver = from_connection.getnetworkinfo()['subversion'] + to_connection_subver = to_connection.getnetworkinfo()['subversion'] + + def find_conn(node, peer_subversion, inbound): + return next(filter(lambda peer: peer['subver'] == peer_subversion and peer['inbound'] == inbound, node.getpeerinfo()), None) + + self.wait_until(lambda: find_conn(from_connection, to_connection_subver, inbound=False) is not None) + self.wait_until(lambda: find_conn(to_connection, from_connection_subver, inbound=True) is not None) + + def check_bytesrecv(peer, msg_type, min_bytes_recv): + assert peer is not None, "Error: peer disconnected" + return peer['bytesrecv_per_msg'].pop(msg_type, 0) >= min_bytes_recv + + # Poll until version handshake (fSuccessfullyConnected) is complete to + # avoid race conditions, because some message types are blocked from + # being sent or received before fSuccessfullyConnected. + # + # As the flag fSuccessfullyConnected is not exposed, check it by + # waiting for a pong, which can only happen after the flag was set. + self.wait_until(lambda: check_bytesrecv(find_conn(from_connection, to_connection_subver, inbound=False), 'pong', 29)) + self.wait_until(lambda: check_bytesrecv(find_conn(to_connection, from_connection_subver, inbound=True), 'pong', 29)) def disconnect_nodes(self, a, b): def disconnect_nodes_helper(node_a, node_b): @@ -678,25 +745,41 @@ def no_op(self): pass def generate(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generate(*args, invalid_call=False, **kwargs) + blocks = generator.generate(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks def generateblock(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generateblock(*args, invalid_call=False, **kwargs) + blocks = generator.generateblock(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks def generatetoaddress(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generatetoaddress(*args, invalid_call=False, **kwargs) + blocks = generator.generatetoaddress(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks def generatetodescriptor(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generatetodescriptor(*args, invalid_call=False, **kwargs) + blocks = generator.generatetodescriptor(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks + def create_outpoints(self, node, *, outputs): + """Send funds to a given list of `{address: amount}` targets using the bitcoind + wallet and return the corresponding outpoints as a list of dictionaries + `[{"txid": txid, "vout": vout1}, {"txid": txid, "vout": vout2}, ...]`. + The result can be used to specify inputs for RPCs like `createrawtransaction`, + `createpsbt`, `lockunspent` etc.""" + assert all(len(output.keys()) == 1 for output in outputs) + send_res = node.send(outputs) + assert send_res["complete"] + utxos = [] + for output in outputs: + address = list(output.keys())[0] + vout = find_vout_for_address(node, send_res["txid"], address) + utxos.append({"txid": send_res["txid"], "vout": vout}) + return utxos + def sync_blocks(self, nodes=None, wait=1, timeout=60): """ Wait until everybody has the same tip. @@ -746,8 +829,8 @@ def sync_all(self, nodes=None): self.sync_blocks(nodes) self.sync_mempools(nodes) - def wait_until(self, test_function, timeout=60): - return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor) + def wait_until(self, test_function, timeout=60, check_interval=0.05): + return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor, check_interval=check_interval) # Private helper methods. These should not be accessed by the subclass test scripts. @@ -763,9 +846,16 @@ def _start_logging(self): # User can provide log level as a number or string (eg DEBUG). loglevel was caught as a string, so try to convert it to an int ll = int(self.options.loglevel) if self.options.loglevel.isdigit() else self.options.loglevel.upper() ch.setLevel(ll) + # Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) - formatter = logging.Formatter(fmt='%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%dT%H:%M:%S') - formatter.converter = time.gmtime + class MicrosecondFormatter(logging.Formatter): + def formatTime(self, record, _=None): + dt = datetime.fromtimestamp(record.created, timezone.utc) + return dt.strftime('%Y-%m-%dT%H:%M:%S.%f') + + formatter = MicrosecondFormatter( + fmt='%(asctime)sZ %(name)s (%(levelname)s): %(message)s', + ) fh.setFormatter(formatter) ch.setFormatter(formatter) # add the handlers to the logger @@ -799,15 +889,14 @@ def _initialize_chain(self): cache_node_dir, chain=self.chain, extra_conf=["bind=127.0.0.1"], - extra_args=['-disablewallet'], + extra_args=[], rpchost=None, timewait=self.rpc_timeout, timeout_factor=self.options.timeout_factor, - bitcoind=self.options.bitcoind, - bitcoin_cli=self.options.bitcoincli, + binaries=self.get_binaries(), coverage_dir=None, cwd=self.options.tmpdir, - descriptors=self.options.descriptors, + uses_wallet=self.uses_wallet, )) self.start_node(CACHE_NODE_ID) cache_node = self.nodes[CACHE_NODE_ID] @@ -875,6 +964,13 @@ def skip_if_no_py_sqlite3(self): except ImportError: raise SkipTest("sqlite3 module not available.") + def skip_if_no_py_capnp(self): + """Attempt to import the capnp package and skip the test if the import fails.""" + try: + import capnp # type: ignore[import] # noqa: F401 + except ImportError: + raise SkipTest("capnp module not available.") + def skip_if_no_python_bcc(self): """Attempt to import the bcc package and skip the tests if the import fails.""" try: @@ -910,39 +1006,40 @@ def skip_if_no_bitcoind_zmq(self): def skip_if_no_wallet(self): """Skip the running test if wallet has not been compiled.""" - self._requires_wallet = True + self.uses_wallet = True if not self.is_wallet_compiled(): raise SkipTest("wallet has not been compiled.") - if self.options.descriptors: - self.skip_if_no_sqlite() - else: - self.skip_if_no_bdb() - - def skip_if_no_sqlite(self): - """Skip the running test if sqlite has not been compiled.""" - if not self.is_sqlite_compiled(): - raise SkipTest("sqlite has not been compiled.") - - def skip_if_no_bdb(self): - """Skip the running test if BDB has not been compiled.""" - if not self.is_bdb_compiled(): - raise SkipTest("BDB has not been compiled.") def skip_if_no_wallet_tool(self): """Skip the running test if bitcoin-wallet has not been compiled.""" if not self.is_wallet_tool_compiled(): raise SkipTest("bitcoin-wallet has not been compiled") + def skip_if_no_bitcoin_tx(self): + """Skip the running test if bitcoin-tx has not been compiled.""" + if not self.is_bitcoin_tx_compiled(): + raise SkipTest("bitcoin-tx has not been compiled") + def skip_if_no_bitcoin_util(self): """Skip the running test if bitcoin-util has not been compiled.""" if not self.is_bitcoin_util_compiled(): raise SkipTest("bitcoin-util has not been compiled") + def skip_if_no_bitcoin_chainstate(self): + """Skip the running test if bitcoin-chainstate has not been compiled.""" + if not self.is_bitcoin_chainstate_compiled(): + raise SkipTest("bitcoin-chainstate has not been compiled") + def skip_if_no_cli(self): """Skip the running test if bitcoin-cli has not been compiled.""" if not self.is_cli_compiled(): raise SkipTest("bitcoin-cli has not been compiled.") + def skip_if_no_ipc(self): + """Skip the running test if ipc is not compiled.""" + if not self.is_ipc_compiled(): + raise SkipTest("ipc has not been compiled.") + def skip_if_no_previous_releases(self): """Skip the running test if previous releases are not available.""" if not self.has_previous_releases(): @@ -961,6 +1058,11 @@ def skip_if_no_external_signer(self): if not self.is_external_signer_compiled(): raise SkipTest("external signer support has not been compiled.") + def skip_if_running_under_valgrind(self): + """Skip the running test if Valgrind is being used.""" + if self.options.valgrind: + raise SkipTest("This test is not compatible with Valgrind.") + def is_cli_compiled(self): """Checks whether bitcoin-cli was compiled.""" return self.config["components"].getboolean("ENABLE_CLI") @@ -973,22 +1075,22 @@ def is_wallet_compiled(self): """Checks whether the wallet module was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET") - def is_specified_wallet_compiled(self): - """Checks whether wallet support for the specified type - (legacy or descriptor wallet) was compiled.""" - if self.options.descriptors: - return self.is_sqlite_compiled() - else: - return self.is_bdb_compiled() - def is_wallet_tool_compiled(self): """Checks whether bitcoin-wallet was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET_TOOL") + def is_bitcoin_tx_compiled(self): + """Checks whether bitcoin-tx was compiled.""" + return self.config["components"].getboolean("BUILD_BITCOIN_TX") + def is_bitcoin_util_compiled(self): """Checks whether bitcoin-util was compiled.""" return self.config["components"].getboolean("ENABLE_BITCOIN_UTIL") + def is_bitcoin_chainstate_compiled(self): + """Checks whether bitcoin-chainstate was compiled.""" + return self.config["components"].getboolean("ENABLE_BITCOIN_CHAINSTATE") + def is_zmq_compiled(self): """Checks whether the zmq module was compiled.""" return self.config["components"].getboolean("ENABLE_ZMQ") @@ -997,14 +1099,14 @@ def is_usdt_compiled(self): """Checks whether the USDT tracepoints were compiled.""" return self.config["components"].getboolean("ENABLE_USDT_TRACEPOINTS") - def is_sqlite_compiled(self): - """Checks whether the wallet module was compiled with Sqlite support.""" - return self.config["components"].getboolean("USE_SQLITE") - - def is_bdb_compiled(self): - """Checks whether the wallet module was compiled with BDB support.""" - return self.config["components"].getboolean("USE_BDB") + def is_ipc_compiled(self): + """Checks whether ipc was compiled.""" + return self.config["components"].getboolean("ENABLE_IPC") def has_blockfile(self, node, filenum: str): - blocksdir = node.datadir_path / self.chain / 'blocks' - return (blocksdir / f"blk{filenum}.dat").is_file() + return (node.blocks_path/ f"blk{filenum}.dat").is_file() + + def convert_to_json_for_cli(self, text): + if self.options.usecli: + return json.dumps(text) + return text diff --git a/src/test_framework/test_node.py b/resources/scenarios/test_framework/test_node.py similarity index 66% rename from src/test_framework/test_node.py rename to resources/scenarios/test_framework/test_node.py index efbb9001d..1ec2fe8a6 100755 --- a/src/test_framework/test_node.py +++ b/resources/scenarios/test_framework/test_node.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2022 The Bitcoin Core developers +# Copyright (c) 2017-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Class for bitcoind node under test""" @@ -8,10 +8,11 @@ import decimal import errno from enum import Enum -import http.client import json import logging import os +import pathlib +import platform import re import subprocess import tempfile @@ -19,6 +20,7 @@ import urllib.parse import collections import shlex +import shutil import sys from pathlib import Path @@ -26,11 +28,12 @@ JSONRPCException, serialization_fallback, ) -from .descriptors import descsum_create -from .p2p import P2P_SUBVERSION +from .messages import NODE_P2P_V2 +from .p2p import P2P_SERVICES, P2P_SUBVERSION from .util import ( MAX_NODES, assert_equal, + assert_not_equal, append_config, delete_cookie_file, get_auth_cookie, @@ -38,9 +41,30 @@ rpc_url, wait_until_helper_internal, p2p_port, + tor_port, ) BITCOIND_PROC_WAIT_TIMEOUT = 60 +# The size of the blocks xor key +# from InitBlocksdirXorKey::xor_key.size() +NUM_XOR_BYTES = 8 +# Many systems have a 128kB limit for a command size. Depending on the +# platform, this limit may be larger or smaller. Moreover, when using the +# 'bitcoin' command, it may internally insert more args, which must be +# accounted for. There is no need to pick the largest possible value here +# anyway and it should be fine to set it to 1kB in tests. +TEST_CLI_MAX_ARG_SIZE = 1024 + +# The null blocks key (all 0s) +NULL_BLK_XOR_KEY = bytes([0] * NUM_XOR_BYTES) +BITCOIN_PID_FILENAME_DEFAULT = "bitcoind.pid" + +if sys.platform.startswith("linux"): + UNIX_PATH_MAX = 108 # includes the trailing NUL +elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd")): + UNIX_PATH_MAX = 104 +else: # safest portable value + UNIX_PATH_MAX = 92 class FailedToStartError(Exception): @@ -67,7 +91,7 @@ class TestNode(): To make things easier for the test writer, any unrecognised messages will be dispatched to the RPC connection.""" - def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False): + def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, binaries, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, v2transport=False, uses_wallet=False, ipcbind=False): """ Kwargs: start_perf (bool): If True, begin profiling the node with `perf` as soon as @@ -83,12 +107,14 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, self.chain = chain self.rpchost = rpchost self.rpc_timeout = timewait - self.binary = bitcoind + self.binaries = binaries self.coverage_dir = coverage_dir self.cwd = cwd - self.descriptors = descriptors + self.has_explicit_bind = False if extra_conf is not None: append_config(self.datadir_path, extra_conf) + # Remember if there is bind=... in the config file. + self.has_explicit_bind = any(e.startswith("bind=") for e in extra_conf) # Most callers will just need to add extra args to the standard list below. # For those callers that need more flexibility, they can just set the args property directly. # Note that common args are set in the config file (see initialize_datadir) @@ -97,19 +123,29 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, # Configuration for logging is set as command-line args rather than in the bitcoin.conf file. # This means that starting a bitcoind using the temp dir to debug a failed test won't # spam debug.log. - self.args = [ - self.binary, + self.args = self.binaries.node_argv(need_ipc=ipcbind) + [ f"-datadir={self.datadir_path}", "-logtimemicros", "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-debugexclude=rand", - "-uacomment=testnode%d" % i, + "-uacomment=testnode%d" % i, # required for subversion uniqueness across peers ] - if self.descriptors is None: + if uses_wallet is not None and not uses_wallet: self.args.append("-disablewallet") + self.ipc_tmp_dir = None + if ipcbind: + self.ipc_socket_path = self.chain_path / "node.sock" + if len(os.fsencode(self.ipc_socket_path)) < UNIX_PATH_MAX: + self.args.append("-ipcbind=unix") + else: + # Work around default CI path exceeding maximum socket path length. + self.ipc_tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="test-ipc-")) + self.ipc_socket_path = self.ipc_tmp_dir / "node.sock" + self.args.append(f"-ipcbind=unix:{self.ipc_socket_path}") + # Use valgrind, expect for previous release binaries if use_valgrind and version is None: default_suppressions_file = Path(__file__).parents[3] / "contrib" / "valgrind.supp" @@ -125,19 +161,31 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, self.args.append("-logsourcelocations") if self.version_is_at_least(239000): self.args.append("-loglevel=trace") + if self.version_is_at_least(299900): + self.args.append("-nologratelimit") + + # Default behavior from global -v2transport flag is added to args to persist it over restarts. + # May be overwritten in individual tests, using extra_args. + self.default_to_v2 = v2transport + if self.version_is_at_least(260000): + # 26.0 and later support v2transport + if v2transport: + self.args.append("-v2transport=1") + else: + self.args.append("-v2transport=0") + # if v2transport is requested via global flag but not supported for node version, ignore it - self.cli = TestNodeCLI(bitcoin_cli, self.datadir_path) + self.cli = TestNodeCLI(binaries, self.datadir_path) self.use_cli = use_cli self.start_perf = start_perf self.running = False self.process = None self.rpc_connected = False - self.rpc = None - self.miniwallet = None + self._rpc = None # Should usually not be accessed directly in tests to allow for --usecli mode + self.reuse_http_connections = True # Must be set before calling get_rpc_proxy() i.e. before restarting node self.url = None self.log = logging.getLogger('TestFramework.node%d' % i) - self.cleanup_on_exit = True # Whether to kill the node when this object goes away # Cache perf subprocesses here by their data output filename. self.perf_subprocesses = {} @@ -179,26 +227,41 @@ def _raise_assertion_error(self, msg: str): def __del__(self): # Ensure that we don't leave any bitcoind processes lying around after # the test ends - if self.process and self.cleanup_on_exit: + if self.process: # Should only happen on test failure # Avoid using logger, as that may have already been shutdown when # this destructor is called. - print(self._node_msg("Cleaning up leftover process")) + print(self._node_msg("Cleaning up leftover process"), file=sys.stderr) self.process.kill() + if self.ipc_tmp_dir: + print(self._node_msg(f"Cleaning up ipc directory {str(self.ipc_tmp_dir)!r}")) + shutil.rmtree(self.ipc_tmp_dir) def __getattr__(self, name): """Dispatches any unrecognised messages to the RPC connection or a CLI instance.""" if self.use_cli: - return getattr(RPCOverloadWrapper(self.cli, True, self.descriptors), name) + return getattr(self.cli, name) else: - assert self.rpc_connected and self.rpc is not None, self._node_msg("Error: no RPC connection") - return getattr(RPCOverloadWrapper(self.rpc, descriptors=self.descriptors), name) + assert self.rpc_connected and self._rpc is not None, self._node_msg("Error: no RPC connection") + return getattr(self._rpc, name) def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, env=None, **kwargs): """Start the node.""" if extra_args is None: extra_args = self.extra_args + # If listening and no -bind is given, then bitcoind would bind P2P ports on + # 0.0.0.0:P and 127.0.0.1:P+1 (for incoming Tor connections), where P is + # a unique port chosen by the test framework and configured as port=P in + # bitcoin.conf. To avoid collisions, change it to 127.0.0.1:tor_port(). + will_listen = all(e != "-nolisten" and e != "-listen=0" for e in extra_args) + has_explicit_bind = self.has_explicit_bind or any(e.startswith("-bind=") for e in extra_args) + if will_listen and not has_explicit_bind: + extra_args.append(f"-bind=0.0.0.0:{p2p_port(self.index)}") + extra_args.append(f"-bind=127.0.0.1:{tor_port(self.index)}=onion") + + self.use_v2transport = "-v2transport=1" in extra_args or (self.default_to_v2 and "-v2transport=0" not in extra_args) + # Add a new stdout and stderr file each time bitcoind is started if stderr is None: stderr = tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False) @@ -228,10 +291,17 @@ def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, env=None if self.start_perf: self._start_perf() - def wait_for_rpc_connection(self): + def wait_for_rpc_connection(self, *, wait_for_import=True): """Sets up an RPC connection to the bitcoind process. Returns False if unable to connect.""" # Poll at a rate of four times per second poll_per_s = 4 + + suppressed_errors = collections.defaultdict(int) + latest_error = None + def suppress_error(category: str, e: Exception): + suppressed_errors[category] += 1 + return (category, repr(e)) + for _ in range(poll_per_s * self.rpc_timeout): if self.process.poll() is not None: # Attach abrupt shutdown error/s to the exception message @@ -248,12 +318,13 @@ def wait_for_rpc_connection(self): timeout=self.rpc_timeout // 2, # Shorter timeout to allow for one retry in case of ETIMEDOUT coveragedir=self.coverage_dir, ) + rpc.auth_service_proxy_instance.reuse_http_connections = self.reuse_http_connections rpc.getblockcount() # If the call to getblockcount() succeeds then the RPC connection is up - if self.version_is_at_least(190000): + if self.version_is_at_least(190000) and wait_for_import: # getmempoolinfo.loaded is available since commit # bb8ae2c (version 0.19.0) - wait_until_helper_internal(lambda: rpc.getmempoolinfo()['loaded'], timeout_factor=self.timeout_factor) + self.wait_until(lambda: rpc.getmempoolinfo()['loaded']) # Wait for the node to finish reindex, block import, and # loading the mempool. Usually importing happens fast or # even "immediate" when the node is started. However, there @@ -272,33 +343,43 @@ def wait_for_rpc_connection(self): # overhead is trivial, and the added guarantees are worth # the minimal performance cost. self.log.debug("RPC successfully started") + # Set rpc_connected even if we are in use_cli mode so that we know we can call self.stop() if needed. + self.rpc_connected = True if self.use_cli: return - self.rpc = rpc - self.rpc_connected = True - self.url = self.rpc.rpc_url + self._rpc = rpc + self.url = self._rpc.rpc_url return - except JSONRPCException as e: # Initialization phase + except JSONRPCException as e: + # Suppress these as they are expected during initialization. # -28 RPC in warmup - # -342 Service unavailable, RPC server started but is shutting down due to error - if e.error['code'] != -28 and e.error['code'] != -342: + # -342 Service unavailable, could be starting up or shutting down + if e.error['code'] not in [-28, -342]: raise # unknown JSON RPC exception - except ConnectionResetError: - # This might happen when the RPC server is in warmup, but shut down before the call to getblockcount - # succeeds. Try again to properly raise the FailedToStartError - pass + latest_error = suppress_error(f"JSONRPCException {e.error['code']}", e) except OSError as e: - if e.errno == errno.ETIMEDOUT: - pass # Treat identical to ConnectionResetError - elif e.errno == errno.ECONNREFUSED: - pass # Port not yet open? - else: + error_num = e.errno + # Work around issue where socket timeouts don't have errno set. + # https://github.com/python/cpython/issues/109601 + if error_num is None and isinstance(e, TimeoutError): + error_num = errno.ETIMEDOUT + + # Suppress similarly to the above JSONRPCException errors. + if error_num not in [ + errno.ECONNRESET, # This might happen when the RPC server is in warmup, + # but shut down before the call to getblockcount succeeds. + errno.ETIMEDOUT, # Treat identical to ECONNRESET + errno.ECONNREFUSED # Port not yet open? + ]: raise # unknown OS error - except ValueError as e: # cookie file not found and no rpcuser or rpcpassword; bitcoind is still starting + latest_error = suppress_error(f"OSError {errno.errorcode[error_num]}", e) + except ValueError as e: + # Suppress if cookie file isn't generated yet and no rpcuser or rpcpassword; bitcoind may be starting. if "No RPC credentials" not in str(e): raise + latest_error = suppress_error("missing_credentials", e) time.sleep(1.0 / poll_per_s) - self._raise_assertion_error("Unable to connect to bitcoind after {}s".format(self.rpc_timeout)) + self._raise_assertion_error(f"Unable to connect to bitcoind after {self.rpc_timeout}s (ignored errors: {dict(suppressed_errors)!s}{'' if latest_error is None else f', latest: {latest_error[0]!r}/{latest_error[1]}'})") def wait_for_cookie_credentials(self): """Ensures auth cookie credentials can be read, e.g. for testing CLI with -rpcwait before RPC connection is up.""" @@ -319,16 +400,16 @@ def generate(self, nblocks, maxtries=1000000, **kwargs): self.log.debug("TestNode.generate() dispatches `generate` call to `generatetoaddress`") return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries, **kwargs) - def generateblock(self, *args, invalid_call, **kwargs): - assert not invalid_call + def generateblock(self, *args, called_by_framework, **kwargs): + assert called_by_framework, "Direct call of this mining RPC is discouraged. Please use one of the self.generate* methods on the test framework, which sync the nodes to avoid intermittent test issues. You may use sync_fun=self.no_op to disable the sync explicitly." return self.__getattr__('generateblock')(*args, **kwargs) - def generatetoaddress(self, *args, invalid_call, **kwargs): - assert not invalid_call + def generatetoaddress(self, *args, called_by_framework, **kwargs): + assert called_by_framework, "Direct call of this mining RPC is discouraged. Please use one of the self.generate* methods on the test framework, which sync the nodes to avoid intermittent test issues. You may use sync_fun=self.no_op to disable the sync explicitly." return self.__getattr__('generatetoaddress')(*args, **kwargs) - def generatetodescriptor(self, *args, invalid_call, **kwargs): - assert not invalid_call + def generatetodescriptor(self, *args, called_by_framework, **kwargs): + assert called_by_framework, "Direct call of this mining RPC is discouraged. Please use one of the self.generate* methods on the test framework, which sync the nodes to avoid intermittent test issues. You may use sync_fun=self.no_op to disable the sync explicitly." return self.__getattr__('generatetodescriptor')(*args, **kwargs) def setmocktime(self, timestamp): @@ -342,11 +423,11 @@ def setmocktime(self, timestamp): def get_wallet_rpc(self, wallet_name): if self.use_cli: - return RPCOverloadWrapper(self.cli("-rpcwallet={}".format(wallet_name)), True, self.descriptors) + return self.cli("-rpcwallet={}".format(wallet_name)) else: - assert self.rpc_connected and self.rpc, self._node_msg("RPC not connected") + assert self.rpc_connected and self._rpc, self._node_msg("RPC not connected") wallet_path = "wallet/{}".format(urllib.parse.quote(wallet_name)) - return RPCOverloadWrapper(self.rpc / wallet_path, descriptors=self.descriptors) + return self._rpc / wallet_path def version_is_at_least(self, ver): return self.version is None or self.version >= ver @@ -355,15 +436,15 @@ def stop_node(self, expected_stderr='', *, wait=0, wait_until_stopped=True): """Stop the node.""" if not self.running: return + assert self.rpc_connected, self._node_msg( + "Should only call stop_node() on a running node after wait_for_rpc_connection() succeeded. " + f"Did you forget to call the latter after start()? Not connected to process: {self.process.pid}") self.log.debug("Stopping node") - try: - # Do not use wait argument when testing older nodes, e.g. in wallet_backwards_compatibility.py - if self.version_is_at_least(180000): - self.stop(wait=wait) - else: - self.stop() - except http.client.CannotSendRequest: - self.log.exception("Unable to stop node.") + # Do not use wait argument when testing older nodes, e.g. in wallet_backwards_compatibility.py + if self.version_is_at_least(180000): + self.stop(wait=wait) + else: + self.stop() # If there are any running perf processes, stop them. for profile_name in tuple(self.perf_subprocesses.keys()): @@ -401,13 +482,19 @@ def is_node_stopped(self, *, expected_stderr="", expected_ret_code=0): self.running = False self.process = None self.rpc_connected = False - self.rpc = None + self._rpc = None self.log.debug("Node stopped") return True def wait_until_stopped(self, *, timeout=BITCOIND_PROC_WAIT_TIMEOUT, expect_error=False, **kwargs): - expected_ret_code = 1 if expect_error else 0 # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS - wait_until_helper_internal(lambda: self.is_node_stopped(expected_ret_code=expected_ret_code, **kwargs), timeout=timeout, timeout_factor=self.timeout_factor) + if "expected_ret_code" not in kwargs: + kwargs["expected_ret_code"] = 1 if expect_error else 0 # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS + self.wait_until(lambda: self.is_node_stopped(**kwargs), timeout=timeout) + + def kill_process(self): + self.process.kill() + self.wait_until_stopped(expected_ret_code=1 if platform.system() == "Windows" else -9) + assert self.is_node_stopped() def replace_in_config(self, replacements): """ @@ -436,6 +523,14 @@ def debug_log_path(self) -> Path: def blocks_path(self) -> Path: return self.chain_path / "blocks" + @property + def blocks_key_path(self) -> Path: + return self.blocks_path / "xor.dat" + + def read_xor_key(self) -> bytes: + with open(self.blocks_key_path, "rb") as xor_f: + return xor_f.read(NUM_XOR_BYTES) + @property def wallets_path(self) -> Path: return self.chain_path / "wallets" @@ -477,7 +572,7 @@ def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2): self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log)) @contextlib.contextmanager - def wait_for_debug_log(self, expected_msgs, timeout=60): + def busy_wait_for_debug_log(self, expected_msgs, timeout=60): """ Block until we see a particular debug log message fragment or until we exceed the timeout. Return: @@ -512,6 +607,23 @@ def wait_for_debug_log(self, expected_msgs, timeout=60): 'Expected messages "{}" does not partially match log:\n\n{}\n\n'.format( str(expected_msgs), print_log)) + @contextlib.contextmanager + def wait_for_new_peer(self, timeout=5): + """ + Wait until the node is connected to at least one new peer. We detect this + by watching for an increased highest peer id, using the `getpeerinfo` RPC call. + Note that the simpler approach of only accounting for the number of peers + suffers from race conditions, as disconnects from unrelated previous peers + could happen anytime in-between. + """ + def get_highest_peer_id(): + peer_info = self.getpeerinfo() + return peer_info[-1]["id"] if peer_info else -1 + + initial_peer_id = get_highest_peer_id() + yield + self.wait_until(lambda: get_highest_peer_id() > initial_peer_id, timeout=timeout) + @contextlib.contextmanager def profile_with_perf(self, profile_name: str): """ @@ -542,7 +654,7 @@ def test_success(cmd): cmd, shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0 - if not sys.platform.startswith('linux'): + if platform.system() != 'Linux': self.log.warning("Can't profile with perf; only available on Linux platforms") return None @@ -605,7 +717,7 @@ def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, mat self.start(extra_args, stdout=log_stdout, stderr=log_stderr, *args, **kwargs) ret = self.process.wait(timeout=self.rpc_timeout) self.log.debug(self._node_msg(f'bitcoind exited with status {ret} during initialization')) - assert ret != 0 # Exit code must indicate failure + assert_not_equal(ret, 0) # Exit code must indicate failure self.running = False self.process = None # Check stderr for expected message @@ -635,19 +747,39 @@ def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, mat assert_msg += "with expected error " + expected_msg self._raise_assertion_error(assert_msg) - def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs): + def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, expect_success=True, **kwargs): """Add an inbound p2p connection to the node. This method adds the p2p connection to the self.p2ps list and also - returns the connection to the caller.""" + returns the connection to the caller. + + When self.use_v2transport is True, TestNode advertises NODE_P2P_V2 service flag + + An inbound connection is made from TestNode <------ P2PConnection + - if TestNode doesn't advertise NODE_P2P_V2 service, P2PConnection sends version message and v1 P2P is followed + - if TestNode advertises NODE_P2P_V2 service, (and if P2PConnections supports v2 P2P) + P2PConnection sends ellswift bytes and v2 P2P is followed + """ if 'dstport' not in kwargs: kwargs['dstport'] = p2p_port(self.index) if 'dstaddr' not in kwargs: kwargs['dstaddr'] = '127.0.0.1' + if supports_v2_p2p is None: + supports_v2_p2p = self.use_v2transport + + if self.use_v2transport: + kwargs['services'] = kwargs.get('services', P2P_SERVICES) | NODE_P2P_V2 + supports_v2_p2p = self.use_v2transport and supports_v2_p2p + p2p_conn.peer_connect(**kwargs, send_version=send_version, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p)() - p2p_conn.peer_connect(**kwargs, net=self.chain, timeout_factor=self.timeout_factor)() self.p2ps.append(p2p_conn) + if not expect_success: + return p2p_conn p2p_conn.wait_until(lambda: p2p_conn.is_connected, check_connected=False) + if supports_v2_p2p and wait_for_v2_handshake: + p2p_conn.wait_until(lambda: p2p_conn.v2_state.tried_v2_handshake) + if send_version: + p2p_conn.wait_until(lambda: not p2p_conn.on_connection_send_msg) if wait_for_verack: # Wait for the node to send us the version and verack p2p_conn.wait_for_verack() @@ -674,7 +806,7 @@ def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs): return p2p_conn - def add_outbound_p2p_connection(self, p2p_conn, *, wait_for_verack=True, p2p_idx, connection_type="outbound-full-relay", **kwargs): + def add_outbound_p2p_connection(self, p2p_conn, *, wait_for_verack=True, wait_for_disconnect=False, p2p_idx, connection_type="outbound-full-relay", supports_v2_p2p=None, advertise_v2_p2p=None, **kwargs): """Add an outbound p2p connection from node. Must be an "outbound-full-relay", "block-relay-only", "addr-fetch" or "feeler" connection. @@ -684,15 +816,43 @@ def add_outbound_p2p_connection(self, p2p_conn, *, wait_for_verack=True, p2p_idx p2p_idx must be different for simultaneously connected peers. When reusing it for the next peer after disconnecting the previous one, it is necessary to wait for the disconnect to finish to avoid a race condition. + + Parameters: + supports_v2_p2p: whether p2p_conn supports v2 P2P or not + advertise_v2_p2p: whether p2p_conn is advertised to support v2 P2P or not + + An outbound connection is made from TestNode -------> P2PConnection + - if P2PConnection doesn't advertise_v2_p2p, TestNode sends version message and v1 P2P is followed + - if P2PConnection both supports_v2_p2p and advertise_v2_p2p, TestNode sends ellswift bytes and v2 P2P is followed + - if P2PConnection doesn't supports_v2_p2p but advertise_v2_p2p, + TestNode sends ellswift bytes and P2PConnection disconnects, + TestNode reconnects by sending version message and v1 P2P is followed """ def addconnection_callback(address, port): self.log.debug("Connecting to %s:%d %s" % (address, port, connection_type)) - self.addconnection('%s:%d' % (address, port), connection_type) + self.addconnection('%s:%d' % (address, port), connection_type, advertise_v2_p2p) + + if supports_v2_p2p is None: + supports_v2_p2p = self.use_v2transport + if advertise_v2_p2p is None: + advertise_v2_p2p = self.use_v2transport - p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, **kwargs)() + if advertise_v2_p2p: + kwargs['services'] = kwargs.get('services', P2P_SERVICES) | NODE_P2P_V2 + assert self.use_v2transport # only a v2 TestNode could make a v2 outbound connection - if connection_type == "feeler": + # if P2PConnection is advertised to support v2 P2P when it doesn't actually support v2 P2P, + # reconnection needs to be attempted using v1 P2P by sending version message + reconnect = advertise_v2_p2p and not supports_v2_p2p + # P2PConnection needs to be advertised to support v2 P2P so that ellswift bytes are sent instead of msg_version + supports_v2_p2p = supports_v2_p2p and advertise_v2_p2p + p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p, reconnect=reconnect, **kwargs)() + + if reconnect: + p2p_conn.wait_for_reconnect() + + if connection_type == "feeler" or wait_for_disconnect: # feeler connections are closed as soon as the node receives a `version` message p2p_conn.wait_until(lambda: p2p_conn.message_count["version"] == 1, check_connected=False) p2p_conn.wait_until(lambda: not p2p_conn.is_connected, check_connected=False) @@ -700,6 +860,9 @@ def addconnection_callback(address, port): p2p_conn.wait_for_connect() self.p2ps.append(p2p_conn) + if supports_v2_p2p: + p2p_conn.wait_until(lambda: p2p_conn.v2_state.tried_v2_handshake) + p2p_conn.wait_until(lambda: not p2p_conn.on_connection_send_msg) if wait_for_verack: p2p_conn.wait_for_verack() p2p_conn.sync_with_ping() @@ -712,12 +875,13 @@ def num_test_p2p_connections(self): def disconnect_p2ps(self): """Close all p2p connections to the node. - Use only after each p2p has sent a version message to ensure the wait works.""" + The state of the peers (such as txrequests) may not be fully cleared + yet, even after this method returns.""" for p in self.p2ps: p.peer_disconnect() del self.p2ps[:] - wait_until_helper_internal(lambda: self.num_test_p2p_connections() == 0, timeout_factor=self.timeout_factor) + self.wait_until(lambda: self.num_test_p2p_connections() == 0) def bumpmocktime(self, seconds): """Fast forward using setmocktime to self.mocktime + seconds. Requires setmocktime to have @@ -726,6 +890,9 @@ def bumpmocktime(self, seconds): self.mocktime += seconds self.setmocktime(self.mocktime) + def wait_until(self, test_function, timeout=60, check_interval=0.05): + return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.timeout_factor, check_interval=check_interval) + class TestNodeCLIAttr: def __init__(self, cli, command): @@ -744,7 +911,7 @@ def arg_to_cli(arg): return str(arg).lower() elif arg is None: return 'null' - elif isinstance(arg, dict) or isinstance(arg, list): + elif isinstance(arg, dict) or isinstance(arg, list) or isinstance(arg, tuple): return json.dumps(arg, default=serialization_fallback) else: return str(arg) @@ -752,16 +919,16 @@ def arg_to_cli(arg): class TestNodeCLI(): """Interface to bitcoin-cli for an individual node""" - def __init__(self, binary, datadir): + def __init__(self, binaries, datadir): self.options = [] - self.binary = binary + self.binaries = binaries self.datadir = datadir self.input = None self.log = logging.getLogger('TestFramework.bitcoincli') def __call__(self, *options, input=None): # TestNodeCLI is callable with bitcoin-cli command-line options - cli = TestNodeCLI(self.binary, self.datadir) + cli = TestNodeCLI(self.binaries, self.datadir) cli.options = [str(o) for o in options] cli.input = input return cli @@ -778,19 +945,35 @@ def batch(self, requests): results.append(dict(error=e)) return results - def send_cli(self, command=None, *args, **kwargs): + def send_cli(self, clicommand=None, *args, **kwargs): """Run bitcoin-cli command. Deserializes returned string as python object.""" pos_args = [arg_to_cli(arg) for arg in args] - named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()] - p_args = [self.binary, f"-datadir={self.datadir}"] + self.options + named_args = [key + "=" + arg_to_cli(value) for (key, value) in kwargs.items() if value is not None] + p_args = self.binaries.rpc_argv() + [f"-datadir={self.datadir}"] + self.options if named_args: p_args += ["-named"] - if command is not None: - p_args += [command] + base_arg_pos = len(p_args) + if clicommand is not None: + p_args += [clicommand] p_args += pos_args + named_args + + # TEST_CLI_MAX_ARG_SIZE is set low enough that checking the string + # length is enough and encoding to bytes is not needed before + # calculating the sum. + sum_arg_size = sum(len(arg) for arg in p_args) + stdin_data = self.input + if sum_arg_size >= TEST_CLI_MAX_ARG_SIZE: + self.log.debug(f"Cli: Command size {sum_arg_size} too large, using stdin") + rpc_args = "\n".join([arg for arg in p_args[base_arg_pos:]]) + if stdin_data is not None: + stdin_data += "\n" + rpc_args + else: + stdin_data = rpc_args + p_args = p_args[:base_arg_pos] + ['-stdin'] + self.log.debug("Running bitcoin-cli {}".format(p_args[2:])) process = subprocess.Popen(p_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - cli_stdout, cli_stderr = process.communicate(input=self.input) + cli_stdout, cli_stderr = process.communicate(input=stdin_data) returncode = process.poll() if returncode: match = re.match(r'error code: ([-0-9]+)\nerror message:\n(.*)', cli_stderr) @@ -798,95 +981,10 @@ def send_cli(self, command=None, *args, **kwargs): code, message = match.groups() raise JSONRPCException(dict(code=int(code), message=message)) # Ignore cli_stdout, raise with cli_stderr - raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr) + raise subprocess.CalledProcessError(returncode, p_args, output=cli_stderr) try: + if not cli_stdout.strip(): + return None return json.loads(cli_stdout, parse_float=decimal.Decimal) except (json.JSONDecodeError, decimal.InvalidOperation): return cli_stdout.rstrip("\n") - -class RPCOverloadWrapper(): - def __init__(self, rpc, cli=False, descriptors=False): - self.rpc = rpc - self.is_cli = cli - self.descriptors = descriptors - - def __getattr__(self, name): - return getattr(self.rpc, name) - - def createwallet_passthrough(self, *args, **kwargs): - return self.__getattr__("createwallet")(*args, **kwargs) - - def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None, external_signer=None): - if descriptors is None: - descriptors = self.descriptors - return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup, external_signer) - - def importprivkey(self, privkey, label=None, rescan=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('importprivkey')(privkey, label, rescan) - desc = descsum_create('combo(' + privkey + ')') - req = [{ - 'desc': desc, - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }] - import_res = self.importdescriptors(req) - if not import_res[0]['success']: - raise JSONRPCException(import_res[0]['error']) - - def addmultisigaddress(self, nrequired, keys, label=None, address_type=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('addmultisigaddress')(nrequired, keys, label, address_type) - cms = self.createmultisig(nrequired, keys, address_type) - req = [{ - 'desc': cms['descriptor'], - 'timestamp': 0, - 'label': label if label else '' - }] - import_res = self.importdescriptors(req) - if not import_res[0]['success']: - raise JSONRPCException(import_res[0]['error']) - return cms - - def importpubkey(self, pubkey, label=None, rescan=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('importpubkey')(pubkey, label, rescan) - desc = descsum_create('combo(' + pubkey + ')') - req = [{ - 'desc': desc, - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }] - import_res = self.importdescriptors(req) - if not import_res[0]['success']: - raise JSONRPCException(import_res[0]['error']) - - def importaddress(self, address, label=None, rescan=None, p2sh=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('importaddress')(address, label, rescan, p2sh) - is_hex = False - try: - int(address ,16) - is_hex = True - desc = descsum_create('raw(' + address + ')') - except Exception: - desc = descsum_create('addr(' + address + ')') - reqs = [{ - 'desc': desc, - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }] - if is_hex and p2sh: - reqs.append({ - 'desc': descsum_create('p2sh(raw(' + address + '))'), - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }) - import_res = self.importdescriptors(reqs) - for res in import_res: - if not res['success']: - raise JSONRPCException(res['error']) diff --git a/src/test_framework/test_shell.py b/resources/scenarios/test_framework/test_shell.py similarity index 73% rename from src/test_framework/test_shell.py rename to resources/scenarios/test_framework/test_shell.py index 09ccec28a..9820a222b 100644 --- a/src/test_framework/test_shell.py +++ b/resources/scenarios/test_framework/test_shell.py @@ -2,9 +2,11 @@ # Copyright (c) 2019-2022 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +import pathlib from test_framework.test_framework import BitcoinTestFramework + class TestShell: """Wrapper Class for BitcoinTestFramework. @@ -16,11 +18,8 @@ class TestShell: start a single TestShell at a time.""" class __TestShell(BitcoinTestFramework): - def add_options(self, parser): - self.add_wallet_options(parser) - def set_test_params(self): - pass + self.uses_wallet = None def run_test(self): pass @@ -59,7 +58,8 @@ def reset(self): print("Shutdown TestShell before resetting!") else: self.num_nodes = None - super().__init__() + dummy_testshell_file = pathlib.Path(__file__).absolute().parent.parent / "testshell_dummy.py" + super().__init__(dummy_testshell_file) instance = None @@ -67,7 +67,13 @@ def __new__(cls): # This implementation enforces singleton pattern, and will return the # previously initialized instance if available if not TestShell.instance: - TestShell.instance = TestShell.__TestShell() + # BitcoinTestFramework instances are supposed to be constructed with the path + # of the calling test in order to find shared data like configuration and the + # cache. Since TestShell is meant for interactive use, there is no concrete + # test; passing a dummy name is fine though, as only the containing directory + # is relevant for successful initialization. + dummy_testshell_file = pathlib.Path(__file__).absolute().parent.parent / "testshell_dummy.py" + TestShell.instance = TestShell.__TestShell(dummy_testshell_file) TestShell.instance.running = False return TestShell.instance diff --git a/src/test_framework/util.py b/resources/scenarios/test_framework/util.py similarity index 78% rename from src/test_framework/util.py rename to resources/scenarios/test_framework/util.py index 61346e9d1..e5a5938f0 100644 --- a/src/test_framework/util.py +++ b/resources/scenarios/test_framework/util.py @@ -5,7 +5,7 @@ """Helpful routines for regression testing.""" from base64 import b64encode -from decimal import Decimal, ROUND_DOWN +from decimal import Decimal from subprocess import CalledProcessError import hashlib import inspect @@ -13,14 +13,18 @@ import logging import os import pathlib +import platform import random import re -import sys import time from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException -from typing import Callable, Optional, Tuple +from .descriptors import descsum_create +from collections.abc import Callable +from typing import Optional, Union + +SATOSHI_PRECISION = Decimal('0.00000001') logger = logging.getLogger("TestFramework.utils") @@ -52,10 +56,31 @@ def assert_fee_amount(fee, tx_size, feerate_BTC_kvB): raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) +def summarise_dict_differences(thing1, thing2): + if not isinstance(thing1, dict) or not isinstance(thing2, dict): + return thing1, thing2 + d1, d2 = {}, {} + for k in sorted(thing1.keys()): + if k not in thing2: + d1[k] = thing1[k] + elif thing1[k] != thing2[k]: + d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k]) + for k in sorted(thing2.keys()): + if k not in thing1: + d2[k] = thing2[k] + return d1, d2 + def assert_equal(thing1, thing2, *args): + if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict): + d1,d2 = summarise_dict_differences(thing1, thing2) + raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2)) if thing1 != thing2 or any(thing1 != arg for arg in args): raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args)) +def assert_not_equal(thing1, thing2, *, error_message=""): + if thing1 == thing2: + raise AssertionError(f"Both values are {thing1}{f', {error_message}' if error_message else ''}") + def assert_greater_than(thing1, thing2): if thing1 <= thing2: @@ -77,10 +102,9 @@ def assert_raises_message(exc, message, fun, *args, **kwds): except JSONRPCException: raise AssertionError("Use assert_raises_rpc_error() to test RPC failures") except exc as e: - if message is not None and message not in e.error['message']: - raise AssertionError( - "Expected substring not found in error message:\nsubstring: '{}'\nerror message: '{}'.".format( - message, e.error['message'])) + if message is not None and message not in str(e): + raise AssertionError("Expected substring not found in exception:\n" + f"substring: '{message}'\nexception: {e!r}.") except Exception as e: raise AssertionError("Unexpected exception raised: " + type(e).__name__) else: @@ -230,6 +254,12 @@ def ceildiv(a, b): return -(-a // b) +def random_bitflip(data): + data = list(data) + data[random.randrange(len(data))] ^= (1 << (random.randrange(8))) + return bytes(data) + + def get_fee(tx_size, feerate_btc_kvb): """Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee""" feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors @@ -237,11 +267,33 @@ def get_fee(tx_size, feerate_btc_kvb): return target_fee_sat / Decimal(1e8) # Return result in BTC -def satoshi_round(amount): - return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) +def satoshi_round(amount: Union[int, float, str], *, rounding: str) -> Decimal: + """Rounds a Decimal amount to the nearest satoshi using the specified rounding mode.""" + return Decimal(amount).quantize(SATOSHI_PRECISION, rounding=rounding) + +def ensure_for(*, duration, f, check_interval=0.2): + """Check if the predicate keeps returning True for duration. -def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0): + check_interval can be used to configure the wait time between checks. + Setting check_interval to 0 will allow to have two checks: one in the + beginning and one after duration. + """ + # If check_interval is 0 or negative or larger than duration, we fall back + # to checking once in the beginning and once at the end of duration + if check_interval <= 0 or check_interval > duration: + check_interval = duration + time_end = time.time() + duration + predicate_source = "''''\n" + inspect.getsource(f) + "'''" + while True: + if not f(): + raise AssertionError(f"Predicate {predicate_source} became false within {duration} seconds") + if time.time() > time_end: + return + time.sleep(check_interval) + + +def wait_until_helper_internal(predicate, *, timeout=60, lock=None, timeout_factor=1.0, check_interval=0.05): """Sleep until the predicate resolves to be True. Warning: Note that this method is not recommended to be used in tests as it is @@ -250,13 +302,10 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in `p2p.py` has a preset lock. """ - if attempts == float('inf') and timeout == float('inf'): - timeout = 60 timeout = timeout * timeout_factor - attempt = 0 time_end = time.time() + timeout - while attempt < attempts and time.time() < time_end: + while time.time() < time_end: if lock: with lock: if predicate(): @@ -264,17 +313,19 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa else: if predicate(): return - attempt += 1 - time.sleep(0.05) + time.sleep(check_interval) # Print the cause of the timeout predicate_source = "''''\n" + inspect.getsource(predicate) + "'''" logger.error("wait_until() failed. Predicate: {}".format(predicate_source)) - if attempt >= attempts: - raise AssertionError("Predicate {} not true after {} attempts".format(predicate_source, attempts)) - elif time.time() >= time_end: - raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout)) - raise RuntimeError('Unreachable') + raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout)) + + +def bpf_cflags(): + return [ + "-Wno-error=implicit-function-declaration", + "-Wno-duplicate-decl-specifier", # https://github.com/bitcoin/bitcoin/issues/32322 + ] def sha256sum_file(filename): @@ -287,10 +338,11 @@ def sha256sum_file(filename): return h.digest() -# TODO: Remove and use random.randbytes(n) instead, available in Python 3.9 -def random_bytes(n): - """Return a random bytes object of length n.""" - return bytes(random.getrandbits(8) for i in range(n)) +def util_xor(data, key, *, offset): + data = bytearray(data) + for i in range(len(data)): + data[i] ^= key[(i + offset) % len(key)] + return bytes(data) # RPC/P2P connection constants and functions @@ -298,9 +350,9 @@ def random_bytes(n): # The maximum number of nodes a single test can spawn MAX_NODES = 12 -# Don't assign rpc or p2p ports lower than this +# Don't assign p2p, rpc or tor ports lower than this PORT_MIN = int(os.getenv('TEST_RUNNER_PORT_MIN', default=11000)) -# The number of ports to "reserve" for p2p and rpc, each +# The number of ports to "reserve" for p2p, rpc and tor, each PORT_RANGE = 5000 @@ -340,7 +392,11 @@ def p2p_port(n): def rpc_port(n): - return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + return p2p_port(n) + PORT_RANGE + + +def tor_port(n): + return p2p_port(n) + PORT_RANGE * 2 def rpc_url(datadir, i, chain, rpchost): @@ -401,14 +457,23 @@ def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect= # in tests. f.write("peertimeout=999999999\n") f.write("printtoconsole=0\n") - f.write("upnp=0\n") f.write("natpmp=0\n") f.write("shrinkdebugfile=0\n") - f.write("deprecatedrpc=create_bdb\n") # Required to run the tests # To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync f.write("unsafesqlitesync=1\n") if disable_autoconnect: f.write("connect=0\n") + # Limit max connections to mitigate test failures on some systems caused by the warning: + # "Warning: Reducing -maxconnections from <...> to <...> due to system limitations". + # The value is calculated as follows: + # available_fds = 256 // Same as FD_SETSIZE on NetBSD. + # MIN_CORE_FDS = 151 // Number of file descriptors required for core functionality. + # MAX_ADDNODE_CONNECTIONS = 8 // Maximum number of -addnode outgoing nodes. + # nBind == 3 // Maximum number of bound interfaces used in a test. + # + # min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind = 151 + 8 + 3 = 162; + # nMaxConnections = available_fds - min_required_fds = 256 - 161 = 94; + f.write("maxconnections=94\n") f.write(extra_config) @@ -416,16 +481,16 @@ def get_datadir_path(dirname, n): return pathlib.Path(dirname) / f"node{n}" -def get_temp_default_datadir(temp_dir: pathlib.Path) -> Tuple[dict, pathlib.Path]: +def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]: """Return os-specific environment variables that can be set to make the GetDefaultDataDir() function return a datadir path under the provided temp_dir, as well as the complete path it would return.""" - if sys.platform == "win32": + if platform.system() == "Windows": env = dict(APPDATA=str(temp_dir)) datadir = temp_dir / "Bitcoin" else: env = dict(HOME=str(temp_dir)) - if sys.platform == "darwin": + if platform.system() == "Darwin": datadir = temp_dir / "Library/Application Support/Bitcoin" else: datadir = temp_dir / ".bitcoin" @@ -490,18 +555,6 @@ def check_node_connections(*, node, num_in, num_out): ############################# -def find_output(node, txid, amount, *, blockhash=None): - """ - Return index to output of txid with value amount - Raises exception if there is none. - """ - txdata = node.getrawtransaction(txid, 1, blockhash) - for i in range(len(txdata["vout"])): - if txdata["vout"][i]["value"] == amount: - return i - raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount))) - - # Create large OP_RETURN txouts that can be appended to a transaction # to make it large (helper for constructing large transactions). The # total serialized size of the txouts is about 66k vbytes. @@ -549,3 +602,20 @@ def find_vout_for_address(node, txid, addr): if addr == tx["vout"][i]["scriptPubKey"]["address"]: return i raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr)) + + +def sync_txindex(test_framework, node): + test_framework.log.debug("Waiting for node txindex to sync") + sync_start = int(time.time()) + test_framework.wait_until(lambda: node.getindexinfo("txindex")["txindex"]["synced"]) + test_framework.log.debug(f"Synced in {time.time() - sync_start} seconds") + +def wallet_importprivkey(wallet_rpc, privkey, timestamp, *, label=""): + desc = descsum_create("combo(" + privkey + ")") + req = [{ + "desc": desc, + "timestamp": timestamp, + "label": label, + }] + import_res = wallet_rpc.importdescriptors(req) + assert_equal(import_res[0]["success"], True) diff --git a/resources/scenarios/test_framework/v2_p2p.py b/resources/scenarios/test_framework/v2_p2p.py new file mode 100644 index 000000000..87600c36d --- /dev/null +++ b/resources/scenarios/test_framework/v2_p2p.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Class for v2 P2P protocol (see BIP 324)""" + +import random + +from .crypto.bip324_cipher import FSChaCha20Poly1305 +from .crypto.chacha20 import FSChaCha20 +from .crypto.ellswift import ellswift_create, ellswift_ecdh_xonly +from .crypto.hkdf import hkdf_sha256 +from .key import TaggedHash +from .messages import MAGIC_BYTES + + +CHACHA20POLY1305_EXPANSION = 16 +HEADER_LEN = 1 +IGNORE_BIT_POS = 7 +LENGTH_FIELD_LEN = 3 +MAX_GARBAGE_LEN = 4095 + +SHORTID = { + 1: b"addr", + 2: b"block", + 3: b"blocktxn", + 4: b"cmpctblock", + 5: b"feefilter", + 6: b"filteradd", + 7: b"filterclear", + 8: b"filterload", + 9: b"getblocks", + 10: b"getblocktxn", + 11: b"getdata", + 12: b"getheaders", + 13: b"headers", + 14: b"inv", + 15: b"mempool", + 16: b"merkleblock", + 17: b"notfound", + 18: b"ping", + 19: b"pong", + 20: b"sendcmpct", + 21: b"tx", + 22: b"getcfilters", + 23: b"cfilter", + 24: b"getcfheaders", + 25: b"cfheaders", + 26: b"getcfcheckpt", + 27: b"cfcheckpt", + 28: b"addrv2", +} + +# Dictionary which contains short message type ID for the P2P message +MSGTYPE_TO_SHORTID = {msgtype: shortid for shortid, msgtype in SHORTID.items()} + + +class EncryptedP2PState: + """A class for managing the state when v2 P2P protocol is used. Performs initial v2 handshake and encrypts/decrypts + P2P messages. P2PConnection uses an object of this class. + + + Args: + initiating (bool): defines whether the P2PConnection is an initiator or responder. + - initiating = True for inbound connections in the test framework [TestNode <------- P2PConnection] + - initiating = False for outbound connections in the test framework [TestNode -------> P2PConnection] + + net (string): chain used (regtest, signet etc..) + + Methods: + perform an advanced form of diffie-hellman handshake to instantiate the encrypted transport. before exchanging + any P2P messages, 2 nodes perform this handshake in order to determine a shared secret that is unique to both + of them and use it to derive keys to encrypt/decrypt P2P messages. + - initial v2 handshakes is performed by: (see BIP324 section #overall-handshake-pseudocode) + 1. initiator using initiate_v2_handshake(), complete_handshake() and authenticate_handshake() + 2. responder using respond_v2_handshake(), complete_handshake() and authenticate_handshake() + - initialize_v2_transport() sets various BIP324 derived keys and ciphers. + + encrypt/decrypt v2 P2P messages using v2_enc_packet() and v2_receive_packet(). + """ + def __init__(self, *, initiating, net): + self.initiating = initiating # True if initiator + self.net = net + self.peer = {} # object with various BIP324 derived keys and ciphers + self.privkey_ours = None + self.ellswift_ours = None + self.sent_garbage = b"" + self.received_garbage = b"" + self.received_prefix = b"" # received ellswift bytes till the first mismatch from 16 bytes v1_prefix + self.tried_v2_handshake = False # True when the initial handshake is over + # stores length of packet contents to detect whether first 3 bytes (which contains length of packet contents) + # has been decrypted. set to -1 if decryption hasn't been done yet. + self.contents_len = -1 + self.found_garbage_terminator = False + self.transport_version = b'' + + @staticmethod + def v2_ecdh(priv, ellswift_theirs, ellswift_ours, initiating): + """Compute BIP324 shared secret. + + Returns: + bytes - BIP324 shared secret + """ + ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv) + if initiating: + # Initiating, place our public key encoding first. + return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_ours + ellswift_theirs + ecdh_point_x32) + else: + # Responding, place their public key encoding first. + return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_theirs + ellswift_ours + ecdh_point_x32) + + def generate_keypair_and_garbage(self, garbage_len=None): + """Generates ellswift keypair and 4095 bytes garbage at max""" + self.privkey_ours, self.ellswift_ours = ellswift_create() + if garbage_len is None: + garbage_len = random.randrange(MAX_GARBAGE_LEN + 1) + self.sent_garbage = random.randbytes(garbage_len) + return self.ellswift_ours + self.sent_garbage + + def initiate_v2_handshake(self): + """Initiator begins the v2 handshake by sending its ellswift bytes and garbage + + Returns: + bytes - bytes to be sent to the peer when starting the v2 handshake as an initiator + """ + return self.generate_keypair_and_garbage() + + def respond_v2_handshake(self, response): + """Responder begins the v2 handshake by sending its ellswift bytes and garbage. However, the responder + sends this after having received at least one byte that mismatches 16-byte v1_prefix. + + Returns: + 1. int - length of bytes that were consumed so that recvbuf can be updated + 2. bytes - bytes to be sent to the peer when starting the v2 handshake as a responder. + - returns b"" if more bytes need to be received before we can respond and start the v2 handshake. + - returns -1 to downgrade the connection to v1 P2P. + """ + v1_prefix = MAGIC_BYTES[self.net] + b'version\x00\x00\x00\x00\x00' + while len(self.received_prefix) < 16: + byte = response.read(1) + # return b"" if we need to receive more bytes + if not byte: + return len(self.received_prefix), b"" + self.received_prefix += byte + if self.received_prefix[-1] != v1_prefix[len(self.received_prefix) - 1]: + return len(self.received_prefix), self.generate_keypair_and_garbage() + # return -1 to decide v1 only after all 16 bytes processed + return len(self.received_prefix), -1 + + def complete_handshake(self, response): + """ Instantiates the encrypted transport and + sends garbage terminator + optional decoy packets + transport version packet. + Done by both initiator and responder. + + Returns: + 1. int - length of bytes that were consumed. returns 0 if all 64 bytes from ellswift haven't been received yet. + 2. bytes - bytes to be sent to the peer when completing the v2 handshake + """ + ellswift_theirs = self.received_prefix + response.read(64 - len(self.received_prefix)) + # return b"" if we need to receive more bytes + if len(ellswift_theirs) != 64: + return 0, b"" + ecdh_secret = self.v2_ecdh(self.privkey_ours, ellswift_theirs, self.ellswift_ours, self.initiating) + self.initialize_v2_transport(ecdh_secret) + # Send garbage terminator + msg_to_send = self.peer['send_garbage_terminator'] + # Optionally send decoy packets after garbage terminator. + aad = self.sent_garbage + for decoy_content_len in [random.randint(1, 100) for _ in range(random.randint(0, 10))]: + msg_to_send += self.v2_enc_packet(decoy_content_len * b'\x00', aad=aad, ignore=True) + aad = b'' + # Send version packet. + msg_to_send += self.v2_enc_packet(self.transport_version, aad=aad) + return 64 - len(self.received_prefix), msg_to_send + + def authenticate_handshake(self, response): + """ Ensures that the received optional decoy packets and transport version packet are authenticated. + Marks the v2 handshake as complete. Done by both initiator and responder. + + Returns: + 1. int - length of bytes that were processed so that recvbuf can be updated + 2. bool - True if the authentication was successful/more bytes need to be received and False otherwise + """ + processed_length = 0 + + # Detect garbage terminator in the received bytes + if not self.found_garbage_terminator: + received_garbage = response[:16] + response = response[16:] + processed_length = len(received_garbage) + for i in range(MAX_GARBAGE_LEN + 1): + if received_garbage[-16:] == self.peer['recv_garbage_terminator']: + # Receive, decode, and ignore version packet. + # This includes skipping decoys and authenticating the received garbage. + self.found_garbage_terminator = True + self.received_garbage = received_garbage[:-16] + break + else: + # don't update recvbuf since more bytes need to be received + if len(response) == 0: + return 0, True + received_garbage += response[:1] + processed_length += 1 + response = response[1:] + else: + # disconnect since garbage terminator was not seen after 4 KiB of garbage. + return processed_length, False + + # Process optional decoy packets and transport version packet + while not self.tried_v2_handshake: + length, contents = self.v2_receive_packet(response, aad=self.received_garbage) + if length == -1: + return processed_length, False + elif length == 0: + return processed_length, True + processed_length += length + self.received_garbage = b"" + # decoy packets have contents = None. v2 handshake is complete only when version packet + # (can be empty with contents = b"") with contents != None is received. + if contents is not None: + assert contents == b"" # currently TestNode sends an empty version packet + self.tried_v2_handshake = True + return processed_length, True + response = response[length:] + + def initialize_v2_transport(self, ecdh_secret): + """Sets the peer object with various BIP324 derived keys and ciphers.""" + peer = {} + salt = b'bitcoin_v2_shared_secret' + MAGIC_BYTES[self.net] + for name in ('initiator_L', 'initiator_P', 'responder_L', 'responder_P', 'garbage_terminators', 'session_id'): + peer[name] = hkdf_sha256(salt=salt, ikm=ecdh_secret, info=name.encode('utf-8'), length=32) + if self.initiating: + self.peer['send_L'] = FSChaCha20(peer['initiator_L']) + self.peer['send_P'] = FSChaCha20Poly1305(peer['initiator_P']) + self.peer['send_garbage_terminator'] = peer['garbage_terminators'][:16] + self.peer['recv_L'] = FSChaCha20(peer['responder_L']) + self.peer['recv_P'] = FSChaCha20Poly1305(peer['responder_P']) + self.peer['recv_garbage_terminator'] = peer['garbage_terminators'][16:] + else: + self.peer['send_L'] = FSChaCha20(peer['responder_L']) + self.peer['send_P'] = FSChaCha20Poly1305(peer['responder_P']) + self.peer['send_garbage_terminator'] = peer['garbage_terminators'][16:] + self.peer['recv_L'] = FSChaCha20(peer['initiator_L']) + self.peer['recv_P'] = FSChaCha20Poly1305(peer['initiator_P']) + self.peer['recv_garbage_terminator'] = peer['garbage_terminators'][:16] + self.peer['session_id'] = peer['session_id'] + + def v2_enc_packet(self, contents, aad=b'', ignore=False): + """Encrypt a BIP324 packet. + + Returns: + bytes - encrypted packet contents + """ + assert len(contents) <= 2**24 - 1 + header = (ignore << IGNORE_BIT_POS).to_bytes(HEADER_LEN, 'little') + plaintext = header + contents + aead_ciphertext = self.peer['send_P'].encrypt(aad, plaintext) + enc_plaintext_len = self.peer['send_L'].crypt(len(contents).to_bytes(LENGTH_FIELD_LEN, 'little')) + return enc_plaintext_len + aead_ciphertext + + def v2_receive_packet(self, response, aad=b''): + """Decrypt a BIP324 packet + + Returns: + 1. int - number of bytes consumed (or -1 if error) + 2. bytes - contents of decrypted non-decoy packet if any (or None otherwise) + """ + if self.contents_len == -1: + if len(response) < LENGTH_FIELD_LEN: + return 0, None + enc_contents_len = response[:LENGTH_FIELD_LEN] + self.contents_len = int.from_bytes(self.peer['recv_L'].crypt(enc_contents_len), 'little') + response = response[LENGTH_FIELD_LEN:] + if len(response) < HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION: + return 0, None + aead_ciphertext = response[:HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION] + plaintext = self.peer['recv_P'].decrypt(aad, aead_ciphertext) + if plaintext is None: + return -1, None # disconnect + header = plaintext[:HEADER_LEN] + length = LENGTH_FIELD_LEN + HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION + self.contents_len = -1 + return length, None if (header[0] & (1 << IGNORE_BIT_POS)) else plaintext[HEADER_LEN:] diff --git a/src/test_framework/wallet.py b/resources/scenarios/test_framework/wallet.py similarity index 88% rename from src/test_framework/wallet.py rename to resources/scenarios/test_framework/wallet.py index 035a482f4..a47ccab01 100644 --- a/src/test_framework/wallet.py +++ b/resources/scenarios/test_framework/wallet.py @@ -9,7 +9,6 @@ from enum import Enum from typing import ( Any, - List, Optional, ) from test_framework.address import ( @@ -33,10 +32,10 @@ CTxIn, CTxInWitness, CTxOut, + hash256, ) from test_framework.script import ( CScript, - LEAF_VERSION_TAPSCRIPT, OP_NOP, OP_RETURN, OP_TRUE, @@ -44,6 +43,7 @@ taproot_construct, ) from test_framework.script_util import ( + bulk_vout, key_to_p2pk_script, key_to_p2pkh_script, key_to_p2sh_p2wpkh_script, @@ -52,6 +52,7 @@ from test_framework.util import ( assert_equal, assert_greater_than_or_equal, + get_fee, ) from test_framework.wallet_util import generate_keypair @@ -66,7 +67,10 @@ class MiniWalletMode(Enum): However, if the transactions need to be modified by the user (e.g. prepending scriptSig for testing opcodes that are activated by a soft-fork), or the txs should contain an actual signature, the raw modes RAW_OP_TRUE and RAW_P2PK - can be useful. Summary of modes: + can be useful. In order to avoid mixing of UTXOs between different MiniWallet + instances, a tag name can be passed to the default mode, to create different + output scripts. Note that the UTXOs from the pre-generated test chain can + only be spent if no tag is passed. Summary of modes: | output | | tx is | can modify | needs mode | description | address | standard | scriptSig | signing @@ -81,22 +85,25 @@ class MiniWalletMode(Enum): class MiniWallet: - def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE): + def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE, tag_name=None): self._test_node = test_node self._utxos = [] self._mode = mode assert isinstance(mode, MiniWalletMode) if mode == MiniWalletMode.RAW_OP_TRUE: + assert tag_name is None self._scriptPubKey = bytes(CScript([OP_TRUE])) elif mode == MiniWalletMode.RAW_P2PK: # use simple deterministic private key (k=1) + assert tag_name is None self._priv_key = ECKey() self._priv_key.set((1).to_bytes(32, 'big'), True) pub_key = self._priv_key.get_pubkey() self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes()) elif mode == MiniWalletMode.ADDRESS_OP_TRUE: - self._address, self._internal_key = create_deterministic_address_bcrt1_p2tr_op_true() + internal_key = None if tag_name is None else compute_xonly_pubkey(hash256(tag_name.encode()))[0] + self._address, self._taproot_info = create_deterministic_address_bcrt1_p2tr_op_true(internal_key) self._scriptPubKey = address_to_scriptpubkey(self._address) # When the pre-mined test framework chain is used, it contains coinbase @@ -109,17 +116,13 @@ def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE): def _create_utxo(self, *, txid, vout, value, height, coinbase, confirmations): return {"txid": txid, "vout": vout, "value": value, "height": height, "coinbase": coinbase, "confirmations": confirmations} - def _bulk_tx(self, tx, target_weight): - """Pad a transaction with extra outputs until it reaches a target weight (or higher). + def _bulk_tx(self, tx, target_vsize): + """Pad a transaction with extra outputs until it reaches a target vsize. returns the tx """ - tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'a']))) - dummy_vbytes = (target_weight - tx.get_weight() + 3) // 4 - tx.vout[-1].scriptPubKey = CScript([OP_RETURN, b'a' * dummy_vbytes]) - # Lower bound should always be off by at most 3 - assert_greater_than_or_equal(tx.get_weight(), target_weight) - # Higher bound should always be off by at most 3 + 12 weight (for encoding the length) - assert_greater_than_or_equal(target_weight + 15, tx.get_weight()) + tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN]))) + bulk_vout(tx, target_vsize) + def get_balance(self): return sum(u['value'] for u in self._utxos) @@ -181,7 +184,12 @@ def sign_tx(self, tx, fixed_length=True): elif self._mode == MiniWalletMode.ADDRESS_OP_TRUE: tx.wit.vtxinwit = [CTxInWitness()] * len(tx.vin) for i in tx.wit.vtxinwit: - i.scriptWitness.stack = [CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key] + assert_equal(len(self._taproot_info.leaves), 1) + leaf_info = list(self._taproot_info.leaves.values())[0] + i.scriptWitness.stack = [ + leaf_info.script, + bytes([leaf_info.version | self._taproot_info.negflag]) + self._taproot_info.internal_pubkey, + ] else: assert False @@ -198,7 +206,7 @@ def generate(self, num_blocks, **kwargs): self.rescan_utxos() return blocks - def get_scriptPubKey(self): + def get_output_script(self): return self._scriptPubKey def get_descriptor(self): @@ -270,7 +278,7 @@ def send_to(self, *, from_node, scriptPubKey, amount, fee=1000): return { "sent_vout": 1, "txid": txid, - "wtxid": tx.getwtxid(), + "wtxid": tx.wtxid_hex, "hex": tx.serialize().hex(), "tx": tx, } @@ -284,14 +292,15 @@ def send_self_transfer_multi(self, *, from_node, **kwargs): def create_self_transfer_multi( self, *, - utxos_to_spend: Optional[List[dict]] = None, + utxos_to_spend: Optional[list[dict]] = None, num_outputs=1, amount_per_output=0, + version=2, locktime=0, sequence=0, fee_per_output=1000, - target_weight=0, - confirmed_only=False + target_vsize=0, + confirmed_only=False, ): """ Create and return a transaction that spends the given UTXOs and creates a @@ -314,14 +323,15 @@ def create_self_transfer_multi( tx = CTransaction() tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend, seq in zip(utxos_to_spend, sequence)] tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)] + tx.version = version tx.nLockTime = locktime self.sign_tx(tx) - if target_weight: - self._bulk_tx(tx, target_weight) + if target_vsize: + self._bulk_tx(tx, target_vsize) - txid = tx.rehash() + txid = tx.txid_hex return { "new_utxos": [self._create_utxo( txid=txid, @@ -333,19 +343,20 @@ def create_self_transfer_multi( ) for i in range(len(tx.vout))], "fee": fee, "txid": txid, - "wtxid": tx.getwtxid(), + "wtxid": tx.wtxid_hex, "hex": tx.serialize().hex(), "tx": tx, } - def create_self_transfer(self, *, + def create_self_transfer( + self, + *, fee_rate=Decimal("0.003"), fee=Decimal("0"), utxo_to_spend=None, - locktime=0, - sequence=0, - target_weight=0, - confirmed_only=False + target_vsize=0, + confirmed_only=False, + **kwargs, ): """Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed.""" utxo_to_spend = utxo_to_spend or self.get_utxo(confirmed_only=confirmed_only) @@ -358,11 +369,19 @@ def create_self_transfer(self, *, vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) else: assert False + if target_vsize and not fee: # respect fee_rate if target vsize is passed + fee = get_fee(target_vsize, fee_rate) send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000)) - + if send_value <= 0: + raise RuntimeError(f"UTXO value {utxo_to_spend['value']} is too small to cover fees {(fee or (fee_rate * vsize / 1000))}") # create tx - tx = self.create_self_transfer_multi(utxos_to_spend=[utxo_to_spend], locktime=locktime, sequence=sequence, amount_per_output=int(COIN * send_value), target_weight=target_weight) - if not target_weight: + tx = self.create_self_transfer_multi( + utxos_to_spend=[utxo_to_spend], + amount_per_output=int(COIN * send_value), + target_vsize=target_vsize, + **kwargs, + ) + if not target_vsize: assert_equal(tx["tx"].get_vsize(), vsize) tx["new_utxo"] = tx.pop("new_utxos")[0] diff --git a/src/test_framework/wallet_util.py b/resources/scenarios/test_framework/wallet_util.py similarity index 62% rename from src/test_framework/wallet_util.py rename to resources/scenarios/test_framework/wallet_util.py index 44811918b..2168e607b 100755 --- a/src/test_framework/wallet_util.py +++ b/resources/scenarios/test_framework/wallet_util.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Useful util functions for testing the wallet""" from collections import namedtuple +import unittest from test_framework.address import ( byte_to_base58, @@ -15,6 +16,11 @@ script_to_p2wsh, ) from test_framework.key import ECKey +from test_framework.messages import ( + CTxIn, + CTxInWitness, + WITNESS_SCALE_FACTOR, +) from test_framework.script_util import ( key_to_p2pkh_script, key_to_p2wpkh_script, @@ -123,6 +129,19 @@ def generate_keypair(compressed=True, wif=False): privkey = bytes_to_wif(privkey.get_bytes(), compressed) return privkey, pubkey +def calculate_input_weight(scriptsig_hex, witness_stack_hex=None): + """Given a scriptSig and a list of witness stack items for an input in hex format, + calculate the total input weight. If the input has no witness data, + `witness_stack_hex` can be set to None.""" + tx_in = CTxIn(scriptSig=bytes.fromhex(scriptsig_hex)) + witness_size = 0 + if witness_stack_hex is not None: + tx_inwit = CTxInWitness() + for witness_item_hex in witness_stack_hex: + tx_inwit.scriptWitness.stack.append(bytes.fromhex(witness_item_hex)) + witness_size = len(tx_inwit.serialize()) + return len(tx_in.serialize()) * WITNESS_SCALE_FACTOR + witness_size + class WalletUnlock(): """ A context manager for unlocking a wallet with a passphrase and automatically locking it afterward. @@ -141,3 +160,42 @@ def __enter__(self): def __exit__(self, *args): _ = args self.wallet.walletlock() + + +class TestFrameworkWalletUtil(unittest.TestCase): + def test_calculate_input_weight(self): + SKELETON_BYTES = 32 + 4 + 4 # prevout-txid, prevout-index, sequence + SMALL_LEN_BYTES = 1 # bytes needed for encoding scriptSig / witness item lengths < 253 + LARGE_LEN_BYTES = 3 # bytes needed for encoding scriptSig / witness item lengths >= 253 + + # empty scriptSig, no witness + self.assertEqual(calculate_input_weight(""), + (SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + self.assertEqual(calculate_input_weight("", None), + (SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + # small scriptSig, no witness + scriptSig_small = "00"*252 + self.assertEqual(calculate_input_weight(scriptSig_small, None), + (SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR) + # small scriptSig, empty witness stack + self.assertEqual(calculate_input_weight(scriptSig_small, []), + (SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR + SMALL_LEN_BYTES) + # large scriptSig, no witness + scriptSig_large = "00"*253 + self.assertEqual(calculate_input_weight(scriptSig_large, None), + (SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR) + # large scriptSig, empty witness stack + self.assertEqual(calculate_input_weight(scriptSig_large, []), + (SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR + SMALL_LEN_BYTES) + # empty scriptSig, 5 small witness stack items + self.assertEqual(calculate_input_weight("", ["00", "11", "22", "33", "44"]), + ((SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 5 * SMALL_LEN_BYTES + 5) + # empty scriptSig, 253 small witness stack items + self.assertEqual(calculate_input_weight("", ["00"]*253), + ((SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + LARGE_LEN_BYTES + 253 * SMALL_LEN_BYTES + 253) + # small scriptSig, 3 large witness stack items + self.assertEqual(calculate_input_weight(scriptSig_small, ["00"*253]*3), + ((SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 3 * LARGE_LEN_BYTES + 3*253) + # large scriptSig, 3 large witness stack items + self.assertEqual(calculate_input_weight(scriptSig_large, ["00"*253]*3), + ((SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 3 * LARGE_LEN_BYTES + 3*253) diff --git a/resources/scenarios/test_scenarios/__init__.py b/resources/scenarios/test_scenarios/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/scenarios/test_scenarios/buggy_failure.py b/resources/scenarios/test_scenarios/buggy_failure.py new file mode 100644 index 000000000..fbda306d7 --- /dev/null +++ b/resources/scenarios/test_scenarios/buggy_failure.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + + +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander + + +class Failure(Commander): + def set_test_params(self): + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = "This test will fail and exit with code 222" + parser.usage = "warnet run /path/to/scenario_buggy_failure.py" + + def run_test(self): + raise Exception("Failed execution!") + + +def main(): + Failure("").main() + + +if __name__ == "__main__": + main() diff --git a/test/data/scenario_connect_dag.py b/resources/scenarios/test_scenarios/connect_dag.py similarity index 60% rename from test/data/scenario_connect_dag.py rename to resources/scenarios/test_scenarios/connect_dag.py index f0565f9b7..c45caef6f 100644 --- a/test/data/scenario_connect_dag.py +++ b/resources/scenarios/test_scenarios/connect_dag.py @@ -2,13 +2,9 @@ import os from enum import Enum, auto, unique -from time import sleep -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Connect a complete DAG from a set of unconnected nodes" +# The base class exists inside the commander container +from commander import Commander @unique @@ -17,23 +13,16 @@ class ConnectionType(Enum): DNS = auto() -class ConnectDag(WarnetTestFramework): +class ConnectDag(Commander): def set_test_params(self): # This is just a minimum self.num_nodes = 10 def add_options(self, parser): - parser.add_argument( - "--network_name", - dest="network_name", - default="warnet", - help="", - ) + parser.description = "Connect a complete DAG from a set of unconnected nodes" + parser.usage = "warnet run /path/to/scenario_connect_dag.py" def run_test(self): - while not self.warnet.network_connected(): - sleep(1) - # All permutations of a directed acyclic graph with zero, one, or two inputs/outputs # # │ Node │ In │ Out │ Con In │ Con Out │ @@ -65,53 +54,48 @@ def run_test(self): self.connect_nodes(5, 4) self.connect_nodes(5, 6) self.connect_nodes(6, 7) - - # Nodes 8 & 9 shall come pre-connected. Attempt to connect them anyway to test the handling - # of dns node addresses - self.connect_nodes(8, 9) - self.connect_nodes(9, 8) - self.sync_all() - zero_peers = self.nodes[0].getpeerinfo() - one_peers = self.nodes[1].getpeerinfo() - two_peers = self.nodes[2].getpeerinfo() - three_peers = self.nodes[3].getpeerinfo() - four_peers = self.nodes[4].getpeerinfo() - five_peers = self.nodes[5].getpeerinfo() - six_peers = self.nodes[6].getpeerinfo() - seven_peers = self.nodes[7].getpeerinfo() - eight_peers = self.nodes[8].getpeerinfo() - nine_peers = self.nodes[9].getpeerinfo() - - for tank in self.warnet.tanks: - self.log.info( - f"Tank {tank.index}: {tank.warnet.tanks[tank.index].get_dns_addr()} pod:" - f" {tank.warnet.tanks[tank.index].get_ip_addr()}" - ) - - self.assert_connection(zero_peers, 2, ConnectionType.DNS) - self.assert_connection(one_peers, 2, ConnectionType.DNS) - self.assert_connection(one_peers, 3, ConnectionType.DNS) + zero_peers = self.tanks["tank-0000"].getpeerinfo() + one_peers = self.tanks["tank-0001"].getpeerinfo() + two_peers = self.tanks["tank-0002"].getpeerinfo() + three_peers = self.tanks["tank-0003"].getpeerinfo() + four_peers = self.tanks["tank-0004"].getpeerinfo() + five_peers = self.tanks["tank-0005"].getpeerinfo() + six_peers = self.tanks["tank-0006"].getpeerinfo() + seven_peers = self.tanks["tank-0007"].getpeerinfo() + eight_peers = self.tanks["tank-0008"].getpeerinfo() + nine_peers = self.tanks["tank-0009"].getpeerinfo() + + for node in self.nodes: + self.log.info(f"Node {node.index}: tank={node.tank} ip={node.rpchost}") + + self.assert_connection(zero_peers, 2, ConnectionType.IP) + self.assert_connection(one_peers, 2, ConnectionType.IP) + self.assert_connection(one_peers, 3, ConnectionType.IP) self.assert_connection(two_peers, 0, ConnectionType.IP) self.assert_connection(two_peers, 1, ConnectionType.IP) - self.assert_connection(two_peers, 3, ConnectionType.DNS) - self.assert_connection(two_peers, 4, ConnectionType.DNS) + self.assert_connection(two_peers, 3, ConnectionType.IP) + self.assert_connection(two_peers, 4, ConnectionType.IP) self.assert_connection(three_peers, 1, ConnectionType.IP) self.assert_connection(three_peers, 2, ConnectionType.IP) - self.assert_connection(three_peers, 5, ConnectionType.DNS) + self.assert_connection(three_peers, 5, ConnectionType.IP) self.assert_connection(four_peers, 2, ConnectionType.IP) self.assert_connection(four_peers, 5, ConnectionType.IP) self.assert_connection(five_peers, 3, ConnectionType.IP) - self.assert_connection(five_peers, 4, ConnectionType.DNS) - self.assert_connection(five_peers, 6, ConnectionType.DNS) + self.assert_connection(five_peers, 4, ConnectionType.IP) + self.assert_connection(five_peers, 6, ConnectionType.IP) self.assert_connection(six_peers, 5, ConnectionType.IP) - self.assert_connection(six_peers, 7, ConnectionType.DNS) + self.assert_connection(six_peers, 7, ConnectionType.IP) self.assert_connection(seven_peers, 6, ConnectionType.IP) # Check the pre-connected nodes + # The only connection made by DNS name would be from the initial graph edges self.assert_connection(eight_peers, 9, ConnectionType.DNS) self.assert_connection(nine_peers, 8, ConnectionType.IP) + # TODO: This needs to cause the test to fail + # assert False + self.log.info( f"Successfully ran the connect_dag.py scenario using a temporary file: " f"{os.path.basename(__file__)} " @@ -120,17 +104,22 @@ def run_test(self): def assert_connection(self, connector, connectee_index, connection_type: ConnectionType): if connection_type == ConnectionType.DNS: assert any( - d.get("addr") == self.warnet.tanks[connectee_index].get_dns_addr() + # ignore the ...-service suffix + self.nodes[connectee_index].tank in d.get("addr") for d in connector - ), f"Could not find {self.options.network_name}-tank-00000{connectee_index}-service" + ), "Could not find conectee hostname" elif connection_type == ConnectionType.IP: assert any( - d.get("addr").split(":")[0] == self.warnet.tanks[connectee_index].get_ip_addr() + d.get("addr").split(":")[0] == self.nodes[connectee_index].rpchost for d in connector - ), f"Could not find Tank {connectee_index}'s ip addr" + ), "Could not find connectee ip addr" else: raise ValueError("ConnectionType must be of type DNS or IP") +def main(): + ConnectDag("").main() + + if __name__ == "__main__": - ConnectDag().main() + main() diff --git a/resources/scenarios/test_scenarios/generate_one_allnodes.py b/resources/scenarios/test_scenarios/generate_one_allnodes.py new file mode 100644 index 000000000..6d70f2bd8 --- /dev/null +++ b/resources/scenarios/test_scenarios/generate_one_allnodes.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander + + +class GenOneAllNodes(Commander): + def set_test_params(self): + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = ( + "Attempt to generate one block on every node the scenario has access to" + ) + parser.usage = "warnet run /path/to/generate_one_allnodes.py" + + def run_test(self): + for node in self.nodes: + wallet = self.ensure_miner(node) + addr = wallet.getnewaddress("bech32") + self.log.info(f"node: {node.tank}") + self.log.info(self.generatetoaddress(node, 1, addr)) + + +def main(): + GenOneAllNodes("").main() + + +if __name__ == "__main__": + main() diff --git a/resources/scenarios/test_scenarios/nothing.py b/resources/scenarios/test_scenarios/nothing.py new file mode 100644 index 000000000..35609e0d0 --- /dev/null +++ b/resources/scenarios/test_scenarios/nothing.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +from time import sleep + +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander + + +class Nothing(Commander): + def set_test_params(self): + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = "This test will do nothing, forever" + parser.usage = "warnet run /path/to/nothing.py" + + def run_test(self): + while True: + sleep(60) + + +def main(): + Nothing("").main() + + +if __name__ == "__main__": + main() diff --git a/test/data/scenario_p2p_interface.py b/resources/scenarios/test_scenarios/p2p_interface.py similarity index 68% rename from test/data/scenario_p2p_interface.py rename to resources/scenarios/test_scenarios/p2p_interface.py index 440fb86f7..61efa1012 100644 --- a/test/data/scenario_p2p_interface.py +++ b/resources/scenarios/test_scenarios/p2p_interface.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 from collections import defaultdict -from time import sleep -from test_framework.messages import CInv, msg_getdata -from test_framework.p2p import P2PInterface -from warnet.test_framework_bridge import WarnetTestFramework +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander -def cli_help(): - return "Run P2P GETDATA test" +from test_framework.messages import CInv, msg_getdata +from test_framework.p2p import P2PInterface class P2PStoreBlock(P2PInterface): @@ -17,24 +18,22 @@ def __init__(self): self.blocks = defaultdict(int) def on_block(self, message): - message.block.calc_sha256() - self.blocks[message.block.sha256] += 1 + self.blocks[message.block.hash_int] += 1 -class GetdataTest(WarnetTestFramework): +class GetdataTest(Commander): def set_test_params(self): self.num_nodes = 1 - def run_test(self): - while not self.warnet.network_connected(): - self.log.info("Waiting for complete network connection...") - sleep(5) - self.log.info("Network connected") + def add_options(self, parser): + parser.description = "Run P2P GETDATA test" + parser.usage = "warnet run /path/to/scenario_p2p_interface.py" + def run_test(self): self.log.info("Adding the p2p connection") p2p_block_store = self.nodes[0].add_p2p_connection( - P2PStoreBlock(), dstaddr=self.warnet.tanks[0].ipv4, dstport=18444 + P2PStoreBlock(), dstaddr=self.tanks["tank-0000"].rpchost, dstport=18444 ) self.log.info("test that an invalid GETDATA doesn't prevent processing of future messages") @@ -49,8 +48,12 @@ def run_test(self): good_getdata = msg_getdata() good_getdata.inv.append(CInv(t=2, h=best_block)) p2p_block_store.send_and_ping(good_getdata) - p2p_block_store.wait_until(lambda: p2p_block_store.blocks[best_block] == 1) + p2p_block_store.wait_until(lambda: p2p_block_store.blocks[best_block] >= 1) + + +def main(): + GetdataTest("").main() if __name__ == "__main__": - GetdataTest().main() + main() diff --git a/resources/scenarios/test_scenarios/pyln_connect.py b/resources/scenarios/test_scenarios/pyln_connect.py new file mode 100644 index 000000000..40e68d37d --- /dev/null +++ b/resources/scenarios/test_scenarios/pyln_connect.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +from io import BytesIO + +from commander import Commander +from pyln.proto.message import Message, MessageNamespace +from pyln.proto.wire import PrivateKey, PublicKey, connect + + +class PyLNConnect(Commander): + def set_test_params(self): + self.num_nodes = 0 + + def run_test(self): + ln = self.lns["tank-0000-ln"] + ln.createrune() + uri = ln.uri() + pk, host = uri.split("@") + host, port = host.split(":") + + ls_privkey = PrivateKey( + bytes.fromhex("1111111111111111111111111111111111111111111111111111111111111111") + ) + remote_pubkey = PublicKey(bytes.fromhex(pk)) + + lc = connect(ls_privkey, remote_pubkey, host, port) + + # Send an init message, with no global features, and 0b10101010 as local + # features. + # From BOLT1: + # type: 16 (init) + # data: + # [u16:gflen] + # [gflen*byte:globalfeatures] + # [u16:flen] + # [flen*byte:features] + # [init_tlvs:tlvs] + lc.send_message(b"\x00\x10\x00\x00\x00\x01\xaa") + + # Import the BOLT#1 init message namesapce + ns = MessageNamespace( + [ + "msgtype,init,16", + "msgdata,init,gflen,u16,", + "msgdata,init,globalfeatures,byte,gflen", + "msgdata,init,flen,u16,", + "msgdata,init,features,byte,flen", + ] + ) + # read reply from peer + msg = lc.read_message() + self.log.info(f"Got message bytes: {msg.hex()}") + # interpret reply from peer + stream = BytesIO(msg) + msg = Message.read(ns, stream) + self.log.info(f"Decoded message type: {msg.messagetype} content: {msg.to_py()}") + + +def main(): + PyLNConnect("").main() + + +if __name__ == "__main__": + main() diff --git a/resources/scenarios/test_scenarios/signet_grinder.py b/resources/scenarios/test_scenarios/signet_grinder.py new file mode 100644 index 000000000..9830abea8 --- /dev/null +++ b/resources/scenarios/test_scenarios/signet_grinder.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +from commander import Commander + + +class SignetGrinder(Commander): + def set_test_params(self): + self.num_nodes = 0 + + def run_test(self): + self.generatetoaddress(self.tanks["miner"], 1, "tb1qjfplwf7a2dpjj04cx96rysqeastvycc0j50cch") + + +def main(): + SignetGrinder("").main() + + +if __name__ == "__main__": + main() diff --git a/src/warnet/scenarios/tx_flood.py b/resources/scenarios/tx_flood.py similarity index 86% rename from src/warnet/scenarios/tx_flood.py rename to resources/scenarios/tx_flood.py index e4fe3ce2f..7d46a6baa 100755 --- a/src/warnet/scenarios/tx_flood.py +++ b/resources/scenarios/tx_flood.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 + import threading from random import choice, randrange from time import sleep -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Make a big transaction mess. Options: [--interval= ]" +from commander import Commander -class TXFlood(WarnetTestFramework): +class TXFlood(Commander): def set_test_params(self): self.num_nodes = 1 self.addrs = [] self.threads = [] def add_options(self, parser): + parser.description = ( + "Sends random transactions between all nodes with available balance in their wallet" + ) + parser.usage = "warnet run /path/to/tx_flood.py [options]" parser.add_argument( "--interval", dest="interval", @@ -27,7 +27,7 @@ def add_options(self, parser): ) def orders(self, node): - wallet = ensure_miner(node) + wallet = self.ensure_miner(node) for address_type in ["legacy", "p2sh-segwit", "bech32", "bech32m"]: self.addrs.append(wallet.getnewaddress(address_type=address_type)) while True: @@ -67,5 +67,9 @@ def run_test(self): sleep(30) +def main(): + TXFlood("").main() + + if __name__ == "__main__": - TXFlood().main() + main() diff --git a/resources/scripts/apidocs.py b/resources/scripts/apidocs.py index 3815126a2..cca6fdce7 100755 --- a/resources/scripts/apidocs.py +++ b/resources/scripts/apidocs.py @@ -6,16 +6,17 @@ from click import Context from tabulate import tabulate -from warnet.cli.main import cli -file_path = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / ".." / "docs" / "warcli.md" +from warnet.main import cli + +file_path = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / ".." / "docs" / "warnet.md" doc = "" def print_cmd(cmd, super=""): global doc - doc += f"### `warcli{super} {cmd['name']}`" + "\n" + doc += f"### `warnet{super} {cmd['name']}`" + "\n" doc += cmd["help"].strip().replace("<", "\\<") + "\n" if len(cmd["params"]) > 1: doc += "\noptions:\n" @@ -25,19 +26,25 @@ def print_cmd(cmd, super=""): p["name"], p["type"]["param_type"] if p["type"]["param_type"] != "Unprocessed" else "String", "yes" if p["required"] else "", - '"' + p["default"] + '"' - if p["default"] and p["type"]["param_type"] == "String" - else Path(p["default"]).relative_to(Path.cwd()) - if p["default"] and p["type"]["param_type"] == "Path" - else p["default"], + format_default_value(p["default"], p["type"]["param_type"]), ] for p in cmd["params"] - if p["name"] != "help" + if p["name"] != "help" and p["name"] != "unknown_args" ] doc += tabulate(data, headers=headers, tablefmt="github") doc += "\n\n" +def format_default_value(default, param_type): + if default is None: + return "" + if param_type == "String": + return f'"{default}"' + if param_type == "Path": + return str(default) + return default + + with Context(cli) as ctx: info = ctx.to_info_dict() # root-level commands first diff --git a/resources/scripts/build-k8s-rpc.sh b/resources/scripts/build-k8s-rpc.sh deleted file mode 100755 index a61291291..000000000 --- a/resources/scripts/build-k8s-rpc.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -# Run with e.g.: -# $ DOCKER_REGISTRY=bitcoindevproject/warnet-rpc TAG=0.1 LATEST=1 ./scripts/build-k8s-rpc.sh Dockerfile_prod - -# Fail on any step -set -ex - -# Create a new builder to enable building multi-platform images -BUILDER_NAME="warnet-rpc-builder" -docker buildx create --name "$BUILDER_NAME" --use - -# Read DOCKER_REGISTRY and TAG from the environment -: "${DOCKER_REGISTRY?Need to set DOCKER_REGISTRY}" -: "${TAG?Need to set TAG}" -: "${LATEST:=0}" - -# Architectures for building -ARCHS="linux/amd64,linux/arm64" - -# Read Dockerfile from the first argument -DOCKERFILE_PATH=$1 -if [[ ! -f "$DOCKERFILE_PATH" ]]; then - echo "Dockerfile does not exist at the specified path: $DOCKERFILE_PATH" - exit 1 -fi - -# Determine the image tags -IMAGE_FULL_NAME="$DOCKER_REGISTRY:$TAG" -TAGS="--tag $IMAGE_FULL_NAME" - -# If LATEST=1, add the latest tag -if [[ "$LATEST" -eq 1 ]]; then - LATEST_TAG_IMAGE="$DOCKER_REGISTRY:latest" - TAGS="$TAGS --tag $LATEST_TAG_IMAGE" -fi - -# Use Buildx to build the image for the specified architectures and tag it accordingly -docker buildx build --platform "$ARCHS" \ - --file "$DOCKERFILE_PATH" \ - --progress=plain \ - $TAGS \ - . --push - -docker buildx rm "$BUILDER_NAME" diff --git a/resources/scripts/connect_logging.sh b/resources/scripts/connect_logging.sh deleted file mode 100755 index 20ea4a63e..000000000 --- a/resources/scripts/connect_logging.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# NO `set -e` here so an error does not exit the script - -POD_NAME=$(kubectl get pods --namespace warnet-logging -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=loki-grafana" -o jsonpath="{.items[0].metadata.name}") - -echo "Go to http://localhost:3000" -echo "Grafana pod name: ${POD_NAME}" - -while true; do - echo "Attempting to start Grafana port forwarding" - echo "Please wait... it might take a few minutes for all logging pods to start" - kubectl --namespace warnet-logging port-forward "${POD_NAME}" 3000 2>&1 - echo "Grafana port forwarding exited with status: $?" - sleep 5 -done; - -echo "warnet-logging port-forward exited" diff --git a/resources/scripts/deploy.sh b/resources/scripts/deploy.sh deleted file mode 100755 index f6d74da28..000000000 --- a/resources/scripts/deploy.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - - -if [ -z "${WAR_MANIFESTS+x}" ]; then - echo "WAR_MANIFESTS is unset. Please provide a path to warnet manifests." - exit 1 -fi - -# Function to check if warnet-rpc container is already running -check_warnet_rpc() { - if kubectl get pods --all-namespaces | grep -q "bitcoindevproject/warnet-rpc"; then - echo "warnet-rpc pod found" - exit 1 - fi -} - -# Deploy base configurations -kubectl apply -f "$WAR_MANIFESTS/namespace.yaml" -kubectl apply -f "$WAR_MANIFESTS/rbac-config.yaml" -kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-service.yaml" - -# Deploy rpc server -if [ -n "${WAR_DEV+x}" ]; then # Dev mode selector - # Build image in local registry - docker build -t warnet/dev -f "$WAR_RPC/Dockerfile_dev" "$WAR_RPC" --load - if [ "$(kubectl config current-context)" = "docker-desktop" ]; then - sed "s?/mnt/src?$(pwd)?g" "$WAR_MANIFESTS/warnet-rpc-statefulset-dev.yaml" | kubectl apply -f - - else # assuming minikube - minikube image load warnet/dev - kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-statefulset-dev.yaml" - fi -else - kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-statefulset.yaml" -fi - -kubectl config set-context --current --namespace=warnet - -# Check for warnet-rpc container -check_warnet_rpc - -until kubectl get pod rpc-0 --namespace=warnet; do - echo "Waiting for server to find pod rpc-0..." - sleep 4 -done - -echo "⏲️ This could take a minute or so." -kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - -echo Done... diff --git a/resources/scripts/graphdocs.py b/resources/scripts/graphdocs.py deleted file mode 100755 index 8f3f80d81..000000000 --- a/resources/scripts/graphdocs.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 - -import os -import re -from pathlib import Path - -from tabulate import tabulate -from warnet.utils import load_schema - -graph_schema = load_schema() - -file_path = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / ".." / "docs" / "graph.md" - -doc = "" - -doc += "### GraphML file format and headers\n" -doc += "```xml\n" -doc += ' \n' - -sections = ["graph", "node", "edge"] - -for section in sections: - for name, details in graph_schema[section]["properties"].items(): - if "comment" not in details: - continue - vname = f'"{name}"' - vtype = f'"{details["type"]}"' - doc += f' \n' -doc += "```\n\n" - -headers = ["key", "for", "type", "default", "explanation"] -data = [] -for section in sections: - data += [ - [name, section, p["type"], p.get("default", ""), p["comment"]] - for name, p in graph_schema[section]["properties"].items() - if "comment" in p - ] - -doc += tabulate(data, headers=headers, tablefmt="github") - - -with open(file_path) as file: - text = file.read() - -pattern = r"(## GraphML file specification\n)(.*\n)*?\Z" -updated_text = re.sub(pattern, rf"\1\n{doc}\n", text) - -with open(file_path, "w") as file: - file.write(updated_text) diff --git a/resources/scripts/install_logging.sh b/resources/scripts/install_logging.sh deleted file mode 100755 index 15fd1ab1e..000000000 --- a/resources/scripts/install_logging.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e - -THIS_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -MANIFESTS_DIR=$( cd -- "$THIS_DIR/../manifests" &> /dev/null && pwd ) - -helm repo add grafana https://grafana.github.io/helm-charts -helm repo add prometheus-community https://prometheus-community.github.io/helm-charts -helm repo update - -helm upgrade --install --namespace warnet-logging --create-namespace --values "${MANIFESTS_DIR}/loki_values.yaml" loki grafana/loki --version 5.47.2 -helm upgrade --install --namespace warnet-logging promtail grafana/promtail -helm upgrade --install --namespace warnet-logging prometheus prometheus-community/kube-prometheus-stack --namespace warnet-logging --set grafana.enabled=false -helm upgrade --install --namespace warnet-logging loki-grafana grafana/grafana --values "${MANIFESTS_DIR}/grafana_values.yaml" \ No newline at end of file diff --git a/resources/scripts/k8s-log-collector.sh b/resources/scripts/k8s-log-collector.sh new file mode 100755 index 000000000..d98c5a38c --- /dev/null +++ b/resources/scripts/k8s-log-collector.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Set variables +NAMESPACE=${1:-default} +LOG_DIR="./k8s-logs" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + +# Ensure log directory exists +mkdir -p "$LOG_DIR" + +# Collect logs using stern (includes logs from terminated pods) +echo "Collecting stern logs..." +stern "(tank|commander).*" --namespace="$NAMESPACE" --output default --since 1h --no-follow > "$LOG_DIR/${TIMESTAMP}_stern_logs" + +# Collect descriptions of all resources +echo "Collecting resource descriptions..." +kubectl describe all --namespace="$NAMESPACE" > "$LOG_DIR/${TIMESTAMP}_resource_descriptions.txt" + +# Collect events +echo "Collecting events..." +kubectl get events --namespace="$NAMESPACE" --sort-by='.metadata.creationTimestamp' > "$LOG_DIR/${TIMESTAMP}_events.txt" + +echo "Log collection complete. Logs saved in $LOG_DIR" diff --git a/resources/scripts/quick_start.sh b/resources/scripts/quick_start.sh deleted file mode 100755 index 7d473848c..000000000 --- a/resources/scripts/quick_start.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash -set -euo pipefail - - -is_cygwin_etal() { - uname -s | grep -qE "CYGWIN|MINGW|MSYS" -} -is_wsl() { - grep -qEi "(Microsoft|WSL)" /proc/version &> /dev/null -} -if is_cygwin_etal || is_wsl; then - echo "Quick start does not support Windows" - exit 1 -fi - - -# Colors and styles -RESET='\033[0m' -BOLD='\033[1m' - -# Use colors if we can and have the color space -if command -v tput &> /dev/null; then - ncolors=$(tput colors) - if [ -n "$ncolors" ] && [ "$ncolors" -ge 8 ]; then - RESET=$(tput sgr0) - BOLD=$(tput bold) - fi -fi - -print_message() { - local color="$1" - local message="$2" - local format="${3:-}" - echo -e "${format}${color}${message}${RESET}" -} - -print_partial_message() { - local pre_message="$1" - local formatted_part="$2" - local post_message="$3" - local format="${4:-}" # Default to empty string if not provided - local color="${5:-$RESET}" - - echo -e "${color}${pre_message}${format}${formatted_part}${RESET}${color}${post_message}${RESET}" -} - -print_message "" "" "" -print_message "" " ╭───────────────────────────────────╮" "" -print_message "" " │ Welcome to the Warnet Quickstart │" "" -print_message "" " ╰───────────────────────────────────╯" "" -print_message "" "" "" -print_message "" " Let's find out if your system has what it takes to run Warnet..." "" -print_message "" "" "" - -minikube_path=$(command -v minikube || true) -if [ -n "$minikube_path" ]; then - print_partial_message " ⭐️ Found " "minikube" ": $minikube_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "minikube" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://minikube.sigs.k8s.io/docs/start/" "$BOLD" - exit 127 -fi - -kubectl_path=$(command -v kubectl || true) -if [ -n "$kubectl_path" ]; then - print_partial_message " ⭐️ Found " "kubectl" ": $kubectl_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "kubectl" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://kubernetes.io/docs/tasks/tools/" "$BOLD" - exit 127 -fi - -docker_path=$(command -v docker || true) -if [ -n "$docker_path" ]; then - print_partial_message " ⭐️ Found " "docker" ": $docker_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "docker" ". Please follow this link to install Docker Engine..." "$BOLD" - print_message "" " https://docs.docker.com/engine/install/" "$BOLD" - exit 127 -fi - -current_user=$(whoami) -if id -nG "$current_user" | grep -qw "docker"; then - print_partial_message " ⭐️ Found " "$current_user" " in the docker group" "$BOLD" -else - print_partial_message " 💥 Could not find " "$current_user" " in the docker group. Please add it like this..." "$BOLD" - print_message "" " sudo usermod -aG docker $current_user && newgrp docker" "$BOLD" - exit 1 -fi - -helm_path=$(command -v helm || true) -if [ -n "$helm_path" ]; then - print_partial_message " ⭐️ Found " "helm" ": $helm_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "helm" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://helm.sh/docs/intro/install/" "$BOLD" - exit 127 -fi - -just_path=$(command -v just || true) -if [ -n "$just_path" ]; then - print_partial_message " ⭐️ Found " "just" ": $just_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "just" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries" "$BOLD" -fi - -python_path=$(command -v python3 || true) -if [ -n "$python_path" ]; then - print_partial_message " ⭐️ Found " "python3" ": $python_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "python3" ". Please follow this link to install it (or use your package manager)..." "$BOLD" - print_message "" " https://www.python.org/downloads/" "$BOLD" - exit 127 -fi - -if [ -n "$VIRTUAL_ENV" ]; then - print_partial_message " ⭐️ Running in virtual environment: " "$VIRTUAL_ENV" "$BOLD" -else - print_partial_message " 💥 Not running in a virtual environment. " "Please activate a venv before proceeding." "$BOLD" - exit 127 -fi - -bpf_status=$(grep CONFIG_BPF /boot/config-"$(uname -r)" || true) -if [ -n "$bpf_status" ]; then - config_bpf=$(echo "$bpf_status" | grep CONFIG_BPF=y) - if [ "$config_bpf" = "CONFIG_BPF=y" ]; then - print_partial_message " ⭐️ Found " "BPF" ": Berkeley Packet Filters appear enabled" "$BOLD" - else - print_partial_message " 💥 Could not find " "BPF" ". Please figure out how to enable Berkeley Packet Filters in your kernel." "$BOLD" - exit 1 - fi -else - print_partial_message " 💥 Could not find " "BPF" ". Please figure out how to enable Berkeley Packet Filters in your kernel." "$BOLD" - exit 1 -fi diff --git a/resources/scripts/setup_minikube.sh b/resources/scripts/setup_minikube.sh deleted file mode 100755 index 7f1cd234e..000000000 --- a/resources/scripts/setup_minikube.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -set +x -set +v - -if [ -z "${WAR_RPC+x}" ]; then - echo "WAR_RPC is unset. Please provide a path to warnet RPC images." - exit 1 -fi - -ERROR_CODE=0 - -# Colors and styles -RESET='\033[0m' -BOLD='\033[1m' - -# Use colors if we can and have the color space -if command -v tput &> /dev/null; then - ncolors=$(tput colors) - if [ -n "$ncolors" ] && [ "$ncolors" -ge 8 ]; then - RESET=$(tput sgr0) - BOLD=$(tput bold) - fi -fi - -print_message() { - local color="$1" - local message="$2" - local format="${3:-}" - echo -e "${format}${color}${message}${RESET}" -} - -print_partial_message() { - local pre_message="$1" - local formatted_part="$2" - local post_message="$3" - local format="${4:-}" # Default to empty string if not provided - local color="${5:-$RESET}" - - echo -e "${color}${pre_message}${format}${formatted_part}${RESET}${color}${post_message}${RESET}" -} - -docker_path=$(command -v docker || true) -if [ -n "$docker_path" ]; then - print_partial_message " ⭐️ Found " "docker" ": $docker_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "docker" ". Please follow this link to install Docker Engine..." "$BOLD" - print_message "" " https://docs.docker.com/engine/install/" "$BOLD" - ERROR_CODE=127 -fi - -current_user=$(whoami) -current_context=$(docker context show) -if id -nG "$current_user" | grep -qw "docker"; then - print_partial_message " ⭐️ Found " "$current_user" " in the docker group" "$BOLD" -elif [ "$current_context" == "rootless" ]; then - print_message " " "⭐️ Running Docker as rootless" "$BOLD" -elif [[ "$(uname)" == "Darwin" ]]; then - print_message " " "⭐️ Running Docker on Darwin" "$BOLD" -else - print_partial_message " 💥 Could not find " "$current_user" " in the docker group. Please add it like this..." "$BOLD" - print_message "" " sudo usermod -aG docker $current_user && newgrp docker" "$BOLD" - ERROR_CODE=1 -fi - -minikube_path=$(command -v minikube || true) -if [ -n "$minikube_path" ]; then - print_partial_message " ⭐️ Found " "minikube" ": $minikube_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "minikube" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://minikube.sigs.k8s.io/docs/start/" "$BOLD" - ERROR_CODE=127 -fi - -kubectl_path=$(command -v kubectl || true) -if [ -n "$kubectl_path" ]; then - print_partial_message " ⭐️ Found " "kubectl" ": $kubectl_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "kubectl" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://kubernetes.io/docs/tasks/tools/" "$BOLD" - ERROR_CODE=127 -fi - -helm_path=$(command -v helm || true) -if [ -n "$helm_path" ]; then - print_partial_message " ⭐️ Found " "helm" ": $helm_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "helm" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://helm.sh/docs/intro/install/" "$BOLD" - ERROR_CODE=127 -fi - -if [ $ERROR_CODE -ne 0 ]; then - print_message "" "There were errors in the setup process. Please fix them and try again." "$BOLD" - exit $ERROR_CODE -fi - -# Check minikube status -minikube delete - -# Prepare minikube start command -MINIKUBE_CMD="minikube start --driver=docker --container-runtime=containerd --mount --mount-string=\"$PWD:/mnt/src\"" - -# Check for WAR_CPU and add to command if set -if [ -n "${WAR_CPU:-}" ]; then - MINIKUBE_CMD="$MINIKUBE_CMD --cpus=$WAR_CPU" -fi - -# Check for WAR_MEM and add to command if set -if [ -n "${WAR_MEM:-}" ]; then - MINIKUBE_CMD="$MINIKUBE_CMD --memory=${WAR_MEM}m" -fi - -# Start minikube with the constructed command -eval "$MINIKUBE_CMD" - -echo -print_message "" "Next, run the following command to deploy warnet" "" -print_message "" " warcli cluster deploy" "$BOLD" -print_partial_message " After that, run " "warcli network start" " to start the network." "$BOLD" - diff --git a/resources/scripts/ssl/cert-gen.sh b/resources/scripts/ssl/cert-gen.sh new file mode 100755 index 000000000..c1370f884 --- /dev/null +++ b/resources/scripts/ssl/cert-gen.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generate the private key using the P-256 curve +openssl ecparam -name prime256v1 -genkey -noout -out tls.key + +# Generate the self-signed certificate using the configuration file +# Expires in ten years, 2034 +openssl req -x509 -new -nodes -key tls.key -days 3650 -out tls.cert -config openssl-config.cnf diff --git a/resources/scripts/ssl/openssl-config.cnf b/resources/scripts/ssl/openssl-config.cnf new file mode 100644 index 000000000..2a7539ecf --- /dev/null +++ b/resources/scripts/ssl/openssl-config.cnf @@ -0,0 +1,28 @@ +[ req ] +distinguished_name = req_distinguished_name +req_extensions = req_ext +x509_extensions = v3_ca +prompt = no + +[ req_distinguished_name ] +O = lnd autogenerated cert +CN = warnet + +[ req_ext ] +keyUsage = critical, digitalSignature, keyEncipherment, keyCertSign +extendedKeyUsage = serverAuth +basicConstraints = critical, CA:true +subjectKeyIdentifier = hash + +[ v3_ca ] +keyUsage = critical, digitalSignature, keyEncipherment, keyCertSign +extendedKeyUsage = serverAuth +basicConstraints = critical, CA:true +subjectKeyIdentifier = hash +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = localhost +DNS.2 = *.default +IP.1 = 127.0.0.1 +IP.2 = ::1 diff --git a/resources/scripts/ssl/tls.cert b/resources/scripts/ssl/tls.cert new file mode 100644 index 000000000..4dfc97a8a --- /dev/null +++ b/resources/scripts/ssl/tls.cert @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB+DCCAZ6gAwIBAgIUSbyK/9viFWS3cLoPkmxZsW8fcH8wCgYIKoZIzj0EAwIw +MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy +bmV0MB4XDTI1MDkwMzE1NDgzNFoXDTM1MDkwMTE1NDgzNFowMjEfMB0GA1UECgwW +bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFg +PyZTvxWUJy7AI3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sqOBkTCBjjAOBgNVHQ8B +Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUNhDWW7rajlA9sNGI/1Q5BDLH/rMwNwYDVR0RBDAwLoIJbG9jYWxo +b3N0ggkqLmRlZmF1bHSHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0E +AwIDSAAwRQIhAOFm85wvwPZMJg0+16Sh0FkKqAuGVmllHnriWHQJ1NhuAiAfoxzE +9ooZuDwKy0Y3dP4DfJCrOlFNTHfp3abG7VQ+VQ== +-----END CERTIFICATE----- diff --git a/resources/scripts/ssl/tls.key b/resources/scripts/ssl/tls.key new file mode 100644 index 000000000..1f27900aa --- /dev/null +++ b/resources/scripts/ssl/tls.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEKlsxGkakClpHqXbr6tqEey634Xc364DgGMJxLdiLHIoAoGCCqGSM49 +AwEHoUQDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFgPyZTvxWUJy7A +I3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sg== +-----END EC PRIVATE KEY----- diff --git a/resources/scripts/startd.sh b/resources/scripts/startd.sh deleted file mode 100755 index 84343510b..000000000 --- a/resources/scripts/startd.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - -if [ $# -eq 0 ]; then - echo "Please provide a path as an argument." - exit 1 -fi -RPC_PATH="$1" - -docker build -t warnet/dev -f "$RPC_PATH/Dockerfile_rpc_dev src/warnet/templates/rpc" --load -kubectl apply -f "$RPC_PATH/namespace.yaml" -kubectl apply -f "$RPC_PATH/rbac-config.yaml" -kubectl apply -f "$RPC_PATH/warnet-rpc-service.yaml" -sed "s?/mnt/src?$(pwd)?g" "$RPC_PATH/warnet-rpc-statefulset-dev.yaml" | kubectl apply -f - -kubectl config set-context --current --namespace=warnet - -echo waiting for rpc to come online -kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - -echo Done... diff --git a/resources/scripts/stop.sh b/resources/scripts/stop.sh deleted file mode 100755 index 7ea07cd1e..000000000 --- a/resources/scripts/stop.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - -# Delete namespaces -kubectl delete namespace warnet --ignore-not-found -kubectl delete namespace warnet-logging --ignore-not-found - -# Set the context to default namespace -kubectl config set-context --current --namespace=default - -# Delete minikube, if it exists -if command -v minikube &> /dev/null; then - minikube delete || true -fi diff --git a/ruff.toml b/ruff.toml index b940c5694..9f24bb64c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,8 @@ +required-version = ">=0.11" extend-exclude = [ - "src/test_framework/*.py", + "resources/scenarios/test_framework", "resources/images/exporter/authproxy.py", + "src/test_framework/*", ] line-length = 100 indent-width = 4 diff --git a/src/test_framework/bdb.py b/src/test_framework/bdb.py deleted file mode 100644 index 41886c09f..000000000 --- a/src/test_framework/bdb.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2020-2021 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -""" -Utilities for working directly with the wallet's BDB database file - -This is specific to the configuration of BDB used in this project: - - pagesize: 4096 bytes - - Outer database contains single subdatabase named 'main' - - btree - - btree leaf pages - -Each key-value pair is two entries in a btree leaf. The first is the key, the one that follows -is the value. And so on. Note that the entry data is itself not in the correct order. Instead -entry offsets are stored in the correct order and those offsets are needed to then retrieve -the data itself. - -Page format can be found in BDB source code dbinc/db_page.h -This only implements the deserialization of btree metadata pages and normal btree pages. Overflow -pages are not implemented but may be needed in the future if dealing with wallets with large -transactions. - -`db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file -""" - -import struct - -# Important constants -PAGESIZE = 4096 -OUTER_META_PAGE = 0 -INNER_META_PAGE = 2 - -# Page type values -BTREE_INTERNAL = 3 -BTREE_LEAF = 5 -BTREE_META = 9 - -# Some magic numbers for sanity checking -BTREE_MAGIC = 0x053162 -DB_VERSION = 9 - -# Deserializes a leaf page into a dict. -# Btree internal pages have the same header, for those, return None. -# For the btree leaf pages, deserialize them and put all the data into a dict -def dump_leaf_page(data): - page_info = {} - page_header = data[0:26] - _, pgno, prev_pgno, next_pgno, entries, hf_offset, level, pg_type = struct.unpack('QIIIHHBB', page_header) - page_info['pgno'] = pgno - page_info['prev_pgno'] = prev_pgno - page_info['next_pgno'] = next_pgno - page_info['hf_offset'] = hf_offset - page_info['level'] = level - page_info['pg_type'] = pg_type - page_info['entry_offsets'] = struct.unpack('{}H'.format(entries), data[26:26 + entries * 2]) - page_info['entries'] = [] - - if pg_type == BTREE_INTERNAL: - # Skip internal pages. These are the internal nodes of the btree and don't contain anything relevant to us - return None - - assert pg_type == BTREE_LEAF, 'A non-btree leaf page has been encountered while dumping leaves' - - for i in range(0, entries): - offset = page_info['entry_offsets'][i] - entry = {'offset': offset} - page_data_header = data[offset:offset + 3] - e_len, pg_type = struct.unpack('HB', page_data_header) - entry['len'] = e_len - entry['pg_type'] = pg_type - entry['data'] = data[offset + 3:offset + 3 + e_len] - page_info['entries'].append(entry) - - return page_info - -# Deserializes a btree metadata page into a dict. -# Does a simple sanity check on the magic value, type, and version -def dump_meta_page(page): - # metadata page - # general metadata - metadata = {} - meta_page = page[0:72] - _, pgno, magic, version, pagesize, encrypt_alg, pg_type, metaflags, _, free, last_pgno, nparts, key_count, record_count, flags, uid = struct.unpack('QIIIIBBBBIIIIII20s', meta_page) - metadata['pgno'] = pgno - metadata['magic'] = magic - metadata['version'] = version - metadata['pagesize'] = pagesize - metadata['encrypt_alg'] = encrypt_alg - metadata['pg_type'] = pg_type - metadata['metaflags'] = metaflags - metadata['free'] = free - metadata['last_pgno'] = last_pgno - metadata['nparts'] = nparts - metadata['key_count'] = key_count - metadata['record_count'] = record_count - metadata['flags'] = flags - metadata['uid'] = uid.hex().encode() - - assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic' - assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page' - assert version == DB_VERSION, 'Database too new' - - # btree metadata - btree_meta_page = page[72:512] - _, minkey, re_len, re_pad, root, _, crypto_magic, _, iv, chksum = struct.unpack('IIIII368sI12s16s20s', btree_meta_page) - metadata['minkey'] = minkey - metadata['re_len'] = re_len - metadata['re_pad'] = re_pad - metadata['root'] = root - metadata['crypto_magic'] = crypto_magic - metadata['iv'] = iv.hex().encode() - metadata['chksum'] = chksum.hex().encode() - - return metadata - -# Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict -def extract_kv_pairs(page_data): - out = {} - last_key = None - for i, entry in enumerate(page_data['entries']): - # By virtue of these all being pairs, even number entries are keys, and odd are values - if i % 2 == 0: - out[entry['data']] = b'' - last_key = entry['data'] - else: - out[last_key] = entry['data'] - return out - -# Extract the key-value pairs of the BDB file given in filename -def dump_bdb_kv(filename): - # Read in the BDB file and start deserializing it - pages = [] - with open(filename, 'rb') as f: - data = f.read(PAGESIZE) - while len(data) > 0: - pages.append(data) - data = f.read(PAGESIZE) - - # Sanity check the meta pages - dump_meta_page(pages[OUTER_META_PAGE]) - dump_meta_page(pages[INNER_META_PAGE]) - - # Fetch the kv pairs from the leaf pages - kv = {} - for i in range(3, len(pages)): - info = dump_leaf_page(pages[i]) - if info is not None: - info_kv = extract_kv_pairs(info) - kv = {**kv, **info_kv} - return kv diff --git a/src/test_framework/muhash.py b/src/test_framework/muhash.py deleted file mode 100644 index 0d96114e3..000000000 --- a/src/test_framework/muhash.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) 2020 Pieter Wuille -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Native Python MuHash3072 implementation.""" - -import hashlib -import unittest - -def rot32(v, bits): - """Rotate the 32-bit value v left by bits bits.""" - bits %= 32 # Make sure the term below does not throw an exception - return ((v << bits) & 0xffffffff) | (v >> (32 - bits)) - -def chacha20_doubleround(s): - """Apply a ChaCha20 double round to 16-element state array s. - - See https://cr.yp.to/chacha/chacha-20080128.pdf and https://tools.ietf.org/html/rfc8439 - """ - QUARTER_ROUNDS = [(0, 4, 8, 12), - (1, 5, 9, 13), - (2, 6, 10, 14), - (3, 7, 11, 15), - (0, 5, 10, 15), - (1, 6, 11, 12), - (2, 7, 8, 13), - (3, 4, 9, 14)] - - for a, b, c, d in QUARTER_ROUNDS: - s[a] = (s[a] + s[b]) & 0xffffffff - s[d] = rot32(s[d] ^ s[a], 16) - s[c] = (s[c] + s[d]) & 0xffffffff - s[b] = rot32(s[b] ^ s[c], 12) - s[a] = (s[a] + s[b]) & 0xffffffff - s[d] = rot32(s[d] ^ s[a], 8) - s[c] = (s[c] + s[d]) & 0xffffffff - s[b] = rot32(s[b] ^ s[c], 7) - -def chacha20_32_to_384(key32): - """Specialized ChaCha20 implementation with 32-byte key, 0 IV, 384-byte output.""" - # See RFC 8439 section 2.3 for chacha20 parameters - CONSTANTS = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574] - - key_bytes = [0]*8 - for i in range(8): - key_bytes[i] = int.from_bytes(key32[(4 * i):(4 * (i+1))], 'little') - - INITIALIZATION_VECTOR = [0] * 4 - init = CONSTANTS + key_bytes + INITIALIZATION_VECTOR - out = bytearray() - for counter in range(6): - init[12] = counter - s = init.copy() - for _ in range(10): - chacha20_doubleround(s) - for i in range(16): - out.extend(((s[i] + init[i]) & 0xffffffff).to_bytes(4, 'little')) - return bytes(out) - -def data_to_num3072(data): - """Hash a 32-byte array data to a 3072-bit number using 6 Chacha20 operations.""" - bytes384 = chacha20_32_to_384(data) - return int.from_bytes(bytes384, 'little') - -class MuHash3072: - """Class representing the MuHash3072 computation of a set. - - See https://cseweb.ucsd.edu/~mihir/papers/inchash.pdf and https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014337.html - """ - - MODULUS = 2**3072 - 1103717 - - def __init__(self): - """Initialize for an empty set.""" - self.numerator = 1 - self.denominator = 1 - - def insert(self, data): - """Insert a byte array data in the set.""" - data_hash = hashlib.sha256(data).digest() - self.numerator = (self.numerator * data_to_num3072(data_hash)) % self.MODULUS - - def remove(self, data): - """Remove a byte array from the set.""" - data_hash = hashlib.sha256(data).digest() - self.denominator = (self.denominator * data_to_num3072(data_hash)) % self.MODULUS - - def digest(self): - """Extract the final hash. Does not modify this object.""" - val = (self.numerator * pow(self.denominator, -1, self.MODULUS)) % self.MODULUS - bytes384 = val.to_bytes(384, 'little') - return hashlib.sha256(bytes384).digest() - -class TestFrameworkMuhash(unittest.TestCase): - def test_muhash(self): - muhash = MuHash3072() - muhash.insert(b'\x00' * 32) - muhash.insert((b'\x01' + b'\x00' * 31)) - muhash.remove((b'\x02' + b'\x00' * 31)) - finalized = muhash.digest() - # This mirrors the result in the C++ MuHash3072 unit test - self.assertEqual(finalized[::-1].hex(), "10d312b100cbd32ada024a6646e40d3482fcff103668d2625f10002a607d5863") - - def test_chacha20(self): - def chacha_check(key, result): - self.assertEqual(chacha20_32_to_384(key)[:64].hex(), result) - - # Test vectors from https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04#section-7 - # Since the nonce is hardcoded to 0 in our function we only use those vectors. - chacha_check([0]*32, "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586") - chacha_check([0]*31 + [1], "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963") diff --git a/src/test_framework/script_util.py b/src/test_framework/script_util.py deleted file mode 100755 index 62894cc0f..000000000 --- a/src/test_framework/script_util.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2019-2022 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Useful Script constants and utils.""" -from test_framework.script import ( - CScript, - CScriptOp, - OP_0, - OP_CHECKMULTISIG, - OP_CHECKSIG, - OP_DUP, - OP_EQUAL, - OP_EQUALVERIFY, - OP_HASH160, - OP_RETURN, - hash160, - sha256, -) - -# To prevent a "tx-size-small" policy rule error, a transaction has to have a -# non-witness size of at least 65 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in -# src/policy/policy.h). Considering a Tx with the smallest possible single -# input (blank, empty scriptSig), and with an output omitting the scriptPubKey, -# we get to a minimum size of 60 bytes: -# -# Tx Skeleton: 4 [Version] + 1 [InCount] + 1 [OutCount] + 4 [LockTime] = 10 bytes -# Blank Input: 32 [PrevTxHash] + 4 [Index] + 1 [scriptSigLen] + 4 [SeqNo] = 41 bytes -# Output: 8 [Amount] + 1 [scriptPubKeyLen] = 9 bytes -# -# Hence, the scriptPubKey of the single output has to have a size of at -# least 5 bytes. -MIN_STANDARD_TX_NONWITNESS_SIZE = 65 -MIN_PADDING = MIN_STANDARD_TX_NONWITNESS_SIZE - 10 - 41 - 9 -assert MIN_PADDING == 5 - -# This script cannot be spent, allowing dust output values under -# standardness checks -DUMMY_MIN_OP_RETURN_SCRIPT = CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 1))) -assert len(DUMMY_MIN_OP_RETURN_SCRIPT) == MIN_PADDING - -def key_to_p2pk_script(key): - key = check_key(key) - return CScript([key, OP_CHECKSIG]) - - -def keys_to_multisig_script(keys, *, k=None): - n = len(keys) - if k is None: # n-of-n multisig by default - k = n - assert k <= n - op_k = CScriptOp.encode_op_n(k) - op_n = CScriptOp.encode_op_n(n) - checked_keys = [check_key(key) for key in keys] - return CScript([op_k] + checked_keys + [op_n, OP_CHECKMULTISIG]) - - -def keyhash_to_p2pkh_script(hash): - assert len(hash) == 20 - return CScript([OP_DUP, OP_HASH160, hash, OP_EQUALVERIFY, OP_CHECKSIG]) - - -def scripthash_to_p2sh_script(hash): - assert len(hash) == 20 - return CScript([OP_HASH160, hash, OP_EQUAL]) - - -def key_to_p2pkh_script(key): - key = check_key(key) - return keyhash_to_p2pkh_script(hash160(key)) - - -def script_to_p2sh_script(script): - script = check_script(script) - return scripthash_to_p2sh_script(hash160(script)) - - -def key_to_p2sh_p2wpkh_script(key): - key = check_key(key) - p2shscript = CScript([OP_0, hash160(key)]) - return script_to_p2sh_script(p2shscript) - - -def program_to_witness_script(version, program): - if isinstance(program, str): - program = bytes.fromhex(program) - assert 0 <= version <= 16 - assert 2 <= len(program) <= 40 - assert version > 0 or len(program) in [20, 32] - return CScript([version, program]) - - -def script_to_p2wsh_script(script): - script = check_script(script) - return program_to_witness_script(0, sha256(script)) - - -def key_to_p2wpkh_script(key): - key = check_key(key) - return program_to_witness_script(0, hash160(key)) - - -def script_to_p2sh_p2wsh_script(script): - script = check_script(script) - p2shscript = CScript([OP_0, sha256(script)]) - return script_to_p2sh_script(p2shscript) - - -def output_key_to_p2tr_script(key): - assert len(key) == 32 - return program_to_witness_script(1, key) - - -def check_key(key): - if isinstance(key, str): - key = bytes.fromhex(key) # Assuming this is hex string - if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): - return key - assert False - - -def check_script(script): - if isinstance(script, str): - script = bytes.fromhex(script) # Assuming this is hex string - if isinstance(script, bytes) or isinstance(script, CScript): - return script - assert False diff --git a/src/warnet/admin.py b/src/warnet/admin.py new file mode 100644 index 000000000..0aa117c00 --- /dev/null +++ b/src/warnet/admin.py @@ -0,0 +1,140 @@ +import os +import sys +from pathlib import Path + +import click +import yaml +from rich import print as richprint + +from .constants import KUBECONFIG, NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX +from .k8s import ( + K8sError, + get_cluster_of_current_context, + get_namespaces_by_type, + get_token_for_service_acount, + get_warnet_user_service_accounts_in_namespace, + open_kubeconfig, +) +from .namespaces import copy_namespaces_defaults, namespaces +from .network import copy_network_defaults + + +@click.group(name="admin", hidden=True) +def admin(): + """Admin commands for warnet project management""" + pass + + +admin.add_command(namespaces) + + +@admin.command() +def init(): + """Initialize a warnet project in the current directory""" + current_dir = os.getcwd() + if os.listdir(current_dir): + richprint("[yellow]Warning: Current directory is not empty[/yellow]") + if not click.confirm("Do you want to continue?", default=True): + return + + copy_network_defaults(Path(current_dir)) + copy_namespaces_defaults(Path(current_dir)) + richprint( + f"[green]Copied network and namespace example files to {Path(current_dir) / NETWORK_DIR.name}[/green]" + ) + richprint(f"[green]Created warnet project structure in {current_dir}[/green]") + + +@admin.command() +@click.option( + "--kubeconfig-dir", + default="kubeconfigs", + help="Directory to store kubeconfig files (default: kubeconfigs)", +) +@click.option( + "--token-duration", + default=172800, + type=int, + help="Duration of the token in seconds (default: 48 hours)", +) +def create_kubeconfigs(kubeconfig_dir, token_duration): + """Create kubeconfig files for ServiceAccounts""" + kubeconfig_dir = os.path.expanduser(kubeconfig_dir) + + try: + kubeconfig_data = open_kubeconfig(KUBECONFIG) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open auth_config: {KUBECONFIG}", fg="red") + sys.exit(1) + + cluster = get_cluster_of_current_context(kubeconfig_data) + + os.makedirs(kubeconfig_dir, exist_ok=True) + + # Get all namespaces that start with prefix + # This assumes when deploying multiple namespaces for the purpose of team games, all namespaces start with a prefix, + # e.g., tabconf-wargames-*. Currently, this is a bit brittle, but we can improve on this in the future + # by automatically applying a TEAM_PREFIX when creating the get_warnet_namespaces + # TODO: choose a prefix convention and have it managed by the helm charts instead of requiring the + # admin user to pipe through the correct string in multiple places. Another would be to use + # labels instead of namespace naming conventions + warnet_namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) + + for v1namespace in warnet_namespaces: + namespace = v1namespace.metadata.name + click.echo(f"Processing namespace: {namespace}") + service_accounts = get_warnet_user_service_accounts_in_namespace(namespace) + + for sa in service_accounts: + name = sa.metadata.name + # Create a token for the ServiceAccount with specified duration + try: + token = get_token_for_service_acount(sa, token_duration) + except Exception as e: + click.echo( + f"Failed to create token for ServiceAccount {name} in namespace {namespace}. Error: {str(e)}. Skipping..." + ) + continue + + # Create a kubeconfig file for the user + kubeconfig_file = os.path.join(kubeconfig_dir, f"{name}-{namespace}-kubeconfig") + + # TODO: move yaml out of python code to resources/manifests/ + # + # might not be worth it since we are just reading the yaml to then create a bunch of values and its not + # actually used to deploy anything into the cluster + # Then benefit would be making this code a bit cleaner and easy to follow, fwiw + kubeconfig_dict = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [cluster], + "users": [{"name": name, "user": {"token": token}}], + "contexts": [ + { + "name": f"{name}-{namespace}", + "context": { + "cluster": cluster["name"], + "namespace": namespace, + "user": name, + }, + } + ], + "current-context": f"{name}-{namespace}", + } + + # Write to a YAML file + with open(kubeconfig_file, "w") as f: + yaml.dump(kubeconfig_dict, f, default_flow_style=False) + + click.echo(f" Created kubeconfig file for {name}: {kubeconfig_file}") + + click.echo("---") + click.echo( + f"All kubeconfig files have been created in the '{kubeconfig_dir}' directory with a duration of {token_duration} seconds." + ) + click.echo("Distribute these files to the respective users.") + click.echo( + "Users can then use by running `warnet auth\n' -doc += ' \n \n \n \n` or with kubectl by specifying the --kubeconfig flag or by setting the KUBECONFIG environment variable." + ) + click.echo(f"Note: The tokens will expire after {token_duration} seconds.") diff --git a/src/warnet/backend/kubernetes_backend.py b/src/warnet/backend/kubernetes_backend.py deleted file mode 100644 index f8821888d..000000000 --- a/src/warnet/backend/kubernetes_backend.py +++ /dev/null @@ -1,885 +0,0 @@ -import base64 -import logging -import re -import subprocess -import time -from pathlib import Path -from typing import cast - -import yaml -from kubernetes import client, config -from kubernetes.client.exceptions import ApiValueError -from kubernetes.client.models.v1_pod import V1Pod -from kubernetes.client.models.v1_service import V1Service -from kubernetes.client.rest import ApiException -from kubernetes.dynamic import DynamicClient -from kubernetes.dynamic.exceptions import NotFoundError, ResourceNotFoundError -from kubernetes.stream import stream -from warnet.cli.image import build_image -from warnet.services import SERVICES, ServiceType -from warnet.status import RunningStatus -from warnet.tank import Tank -from warnet.utils import parse_raw_messages - -DOCKER_REGISTRY_CORE = "bitcoindevproject/bitcoin" -LOCAL_REGISTRY = "warnet/bitcoin-core" - -POD_PREFIX = "tank" -BITCOIN_CONTAINER_NAME = "bitcoin" -LN_CONTAINER_NAME = "ln" -LN_CB_CONTAINER_NAME = "ln-cb" -MAIN_NAMESPACE = "warnet" -PROMETHEUS_METRICS_PORT = 9332 -LND_MOUNT_PATH = "/root/.lnd" - - -logger = logging.getLogger("k8s") - - -class KubernetesBackend: - def __init__(self, config_dir: Path, network_name: str, logs_pod="fluentd") -> None: - # assumes the warnet rpc server is always - # running inside a k8s cluster as a statefulset - config.load_incluster_config() - self.client = client.CoreV1Api() - self.dynamic_client = DynamicClient(client.ApiClient()) - self.namespace = "warnet" - self.logs_pod = logs_pod - self.network_name = network_name - self.log = logger - - def build(self) -> bool: - # TODO: just return true for now, this is so we can be running either docker or k8s as a backend - # on the same branch - return True - - def up(self, warnet) -> bool: - self.deploy_pods(warnet) - return True - - def down(self, warnet) -> bool: - """ - Bring an existing network down. - e.g. `k delete -f warnet-tanks.yaml` - """ - - for tank in warnet.tanks: - self.client.delete_namespaced_pod( - self.get_pod_name(tank.index, ServiceType.BITCOIN), self.namespace - ) - self.client.delete_namespaced_service( - self.get_service_name(tank.index, ServiceType.BITCOIN), self.namespace - ) - if tank.lnnode: - self.client.delete_namespaced_pod( - self.get_pod_name(tank.index, ServiceType.LIGHTNING), self.namespace - ) - self.client.delete_namespaced_service( - self.get_service_name(tank.index, ServiceType.LIGHTNING), self.namespace - ) - - self.remove_prometheus_service_monitors(warnet.tanks) - - for service_name in warnet.services: - try: - self.client.delete_namespaced_pod( - self.get_service_pod_name(SERVICES[service_name]["container_name_suffix"]), - self.namespace, - ) - self.client.delete_namespaced_service( - self.get_service_service_name(SERVICES[service_name]["container_name_suffix"]), - self.namespace, - ) - except Exception as e: - self.log.error(f"Could not delete service: {service_name}:\n{e}") - - return True - - def get_file(self, tank_index: int, service: ServiceType, file_path: str): - """ - Read a file from inside a container - """ - pod_name = self.get_pod_name(tank_index, service) - exec_command = ["sh", "-c", f'cat "{file_path}" | base64'] - - resp = stream( - self.client.connect_get_namespaced_pod_exec, - pod_name, - self.namespace, - command=exec_command, - stderr=True, - stdin=True, - stdout=True, - tty=False, - _preload_content=False, - container=BITCOIN_CONTAINER_NAME - if service == ServiceType.BITCOIN - else LN_CONTAINER_NAME, - ) - - base64_encoded_data = "" - while resp.is_open(): - resp.update(timeout=1) - if resp.peek_stdout(): - base64_encoded_data += resp.read_stdout() - if resp.peek_stderr(): - stderr_output = resp.read_stderr() - logger.error(f"STDERR: {stderr_output}") - raise Exception(f"Problem copying file from pod: {stderr_output}") - - decoded_bytes = base64.b64decode(base64_encoded_data) - return decoded_bytes - - def get_service_pod_name(self, suffix: str) -> str: - return f"{self.network_name}-{suffix}" - - def get_service_service_name(self, suffix: str) -> str: - return f"{self.network_name}-{suffix}-service" - - def get_pod_name(self, tank_index: int, type: ServiceType) -> str: - if type == ServiceType.LIGHTNING or type == ServiceType.CIRCUITBREAKER: - return f"{self.network_name}-{POD_PREFIX}-ln-{tank_index:06d}" - return f"{self.network_name}-{POD_PREFIX}-{tank_index:06d}" - - def get_service_name(self, tank_index: int, type: ServiceType) -> str: - return f"{self.get_pod_name(tank_index, type)}-service" - - def get_pod(self, pod_name: str) -> V1Pod | None: - try: - return cast( - V1Pod, self.client.read_namespaced_pod(name=pod_name, namespace=self.namespace) - ) - except ApiException as e: - if e.status == 404: - return None - - def get_service(self, service_name: str) -> V1Service | None: - try: - return cast( - V1Service, - self.client.read_namespaced_service(name=service_name, namespace=self.namespace), - ) - except ApiException as e: - if e.status == 404: - return None - - # We could enhance this by checking the pod status as well - # The following pod phases are available: Pending, Running, Succeeded, Failed, Unknown - # For example not able to pull image will be a phase of Pending, but the container status will be ErrImagePull - def get_status(self, tank_index: int, service: ServiceType) -> RunningStatus: - pod_name = self.get_pod_name(tank_index, service) - pod = self.get_pod(pod_name) - # Possible states: - # 1. pod not found? - # -> STOPPED - # 2. pod phase Succeeded? - # -> STOPPED - # 3. pod phase Failed? - # -> FAILED - # 4. pod phase Unknown? - # -> UNKNOWN - # Pod phase is now "Running" or "Pending" - # -> otherwise we need a bug fix, return UNKNOWN - # - # The pod is ready if all containers are ready. - # 5. Pod not ready? - # -> PENDING - # 6. Pod ready? - # -> RUNNING - # - # Note: we don't know anything about deleted pods so we can't return a status for them. - # TODO: we could use a kubernetes job to keep the result 🤔 - - if pod is None: - return RunningStatus.STOPPED - - assert pod.status, "Could not get pod status" - assert pod.status.phase, "Could not get pod status.phase" - if pod.status.phase == "Succeeded": - return RunningStatus.STOPPED - if pod.status.phase == "Failed": - return RunningStatus.FAILED - if pod.status.phase == "Unknown": - return RunningStatus.UNKNOWN - if pod.status.phase == "Pending": - return RunningStatus.PENDING - - assert pod.status.phase in ("Running", "Pending"), f"Unknown pod phase {pod.status.phase}" - - # a pod is ready if all containers are ready - ready = True - for container in pod.status.container_statuses: - if container.ready is not True: - ready = False - break - return RunningStatus.RUNNING if ready else RunningStatus.PENDING - - def exec_run(self, tank_index: int, service: ServiceType, cmd: str): - pod_name = self.get_pod_name(tank_index, service) - exec_cmd = ["/bin/sh", "-c", f"{cmd}"] - self.log.debug(f"Running {exec_cmd=:} on {tank_index=:}") - if service == ServiceType.BITCOIN: - container = BITCOIN_CONTAINER_NAME - if service == ServiceType.LIGHTNING: - container = LN_CONTAINER_NAME - if service == ServiceType.CIRCUITBREAKER: - container = LN_CB_CONTAINER_NAME - result = stream( - self.client.connect_get_namespaced_pod_exec, - pod_name, - self.namespace, - container=container, - command=exec_cmd, - stderr=True, - stdin=False, - stdout=True, - tty=False, - # Avoid preloading the content to keep JSON intact - _preload_content=False, - ) - # TODO: stream result is just a string, so there is no error code to check - # ideally, we use a method where we can check for an error code, otherwise we will - # need to check for errors in the string (meh) - # - # if result.exit_code != 0: - # raise Exception( - # f"Command failed with exit code {result.exit_code}: {result.output.decode('utf-8')}" - # ) - result.run_forever() - result = result.read_all() - return result - - def get_bitcoin_debug_log(self, tank_index: int): - pod_name = self.get_pod_name(tank_index, ServiceType.BITCOIN) - logs = self.client.read_namespaced_pod_log( - name=pod_name, - namespace=self.namespace, - container=BITCOIN_CONTAINER_NAME, - ) - return logs - - def ln_cli(self, tank: Tank, command: list[str]): - if tank.lnnode is None: - raise Exception("No LN node configured for tank") - cmd = tank.lnnode.generate_cli_command(command) - self.log.debug(f"Running lncli {cmd=:} on {tank.index=:}") - return self.exec_run(tank.index, ServiceType.LIGHTNING, cmd) - - def ln_pub_key(self, tank) -> str: - if tank.lnnode is None: - raise Exception("No LN node configured for tank") - self.log.debug(f"Getting pub key for tank {tank.index}") - return tank.lnnode.get_pub_key() - - def get_bitcoin_cli(self, tank: Tank, method: str, params=None): - if params: - cmd = f"bitcoin-cli -regtest -rpcuser={tank.rpc_user} -rpcport={tank.rpc_port} -rpcpassword={tank.rpc_password} {method} {' '.join(map(str, params))}" - else: - cmd = f"bitcoin-cli -regtest -rpcuser={tank.rpc_user} -rpcport={tank.rpc_port} -rpcpassword={tank.rpc_password} {method}" - self.log.debug(f"Running bitcoin-cli {cmd=:} on {tank.index=:}") - return self.exec_run(tank.index, ServiceType.BITCOIN, cmd) - - def get_messages( - self, - a_index: int, - b_index: int, - bitcoin_network: str = "regtest", - ): - b_pod = self.get_pod(self.get_pod_name(b_index, ServiceType.BITCOIN)) - b_service = self.get_service(self.get_service_name(b_index, ServiceType.BITCOIN)) - subdir = "/" if bitcoin_network == "main" else f"{bitcoin_network}/" - base_dir = f"/root/.bitcoin/{subdir}message_capture" - cmd = f"ls {base_dir}" - self.log.debug(f"Running {cmd=:} on {a_index=:}") - dirs = self.exec_run( - a_index, - ServiceType.BITCOIN, - cmd, - ) - dirs = dirs.splitlines() - self.log.debug(f"Got dirs: {dirs}") - messages = [] - - for dir_name in dirs: - if b_pod.status.pod_ip in dir_name or b_service.spec.cluster_ip in dir_name: - for file, outbound in [["msgs_recv.dat", False], ["msgs_sent.dat", True]]: - # Fetch the file contents from the container - file_path = f"{base_dir}/{dir_name}/{file}" - blob = self.get_file(a_index, ServiceType.BITCOIN, f"{file_path}") - # Parse the blob - json = parse_raw_messages(blob, outbound) - messages = messages + json - - messages.sort(key=lambda x: x["time"]) - return messages - - def logs_grep(self, pattern: str, network: str, k8s_timestamps=False, no_sort=False): - compiled_pattern = re.compile(pattern) - matching_logs = [] - pods = self.client.list_namespaced_pod(self.namespace) - relevant_pods = [pod for pod in pods.items if "warnet" in pod.metadata.name] - - for pod in relevant_pods: - try: - log_stream = self.client.read_namespaced_pod_log( - name=pod.metadata.name, - container=BITCOIN_CONTAINER_NAME, - namespace=self.namespace, - timestamps=k8s_timestamps, - _preload_content=False, - ) - for log_entry in log_stream: - log_entry_str = log_entry.decode("utf-8").strip() - if compiled_pattern.search(log_entry_str): - matching_logs.append((log_entry_str, pod.metadata.name)) - except ApiException as e: - print(f"Error fetching logs for pod {pod.metadata.name}: {e}") - - sorted_logs = matching_logs if no_sort else sorted(matching_logs, key=lambda x: x[0]) - # Prepend pod names - formatted_logs = [f"{pod_name}: {log}" for log, pod_name in sorted_logs] - - return "\n".join(formatted_logs) - - def generate_deployment_file(self, warnet): - """ - TODO: implement this - """ - pass - - def create_bitcoind_container(self, tank: Tank) -> client.V1Container: - self.log.debug(f"Creating bitcoind container for tank {tank.index}") - container_name = BITCOIN_CONTAINER_NAME - container_image = None - - # Prebuilt image - if tank.image: - container_image = tank.image - # On-demand built image - elif "/" and "#" in tank.version: - # We don't have docker installed on the RPC server, where this code will be run from, - # and it's currently unclear to me if having the RPC pod build images is a good idea. - # Don't support this for now in CI by disabling in the workflow. - - # This can be re-enabled by enabling in the workflow file and installing docker and - # docker-buildx on the rpc server image. - - # it's a git branch, building step is necessary - repo, branch = tank.version.split("#") - build_image( - repo, - branch, - LOCAL_REGISTRY, - branch, - tank.DEFAULT_BUILD_ARGS + tank.build_args, - arches="amd64", - ) - # Prebuilt major version - else: - container_image = f"{DOCKER_REGISTRY_CORE}:{tank.version}" - - peers = [ - self.get_service_name(dst_index, ServiceType.BITCOIN) for dst_index in tank.init_peers - ] - bitcoind_options = tank.get_bitcoin_conf(peers) - container_env = [client.V1EnvVar(name="BITCOIN_ARGS", value=bitcoind_options)] - - bitcoind_container = client.V1Container( - name=container_name, - image=container_image, - env=container_env, - liveness_probe=client.V1Probe( - failure_threshold=3, - initial_delay_seconds=5, - period_seconds=5, - timeout_seconds=1, - _exec=client.V1ExecAction(command=["pidof", "bitcoind"]), - ), - readiness_probe=client.V1Probe( - failure_threshold=1, - initial_delay_seconds=0, - period_seconds=1, - timeout_seconds=1, - tcp_socket=client.V1TCPSocketAction(port=tank.rpc_port), - ), - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - ) - self.log.debug( - f"Created bitcoind container for tank {tank.index} using {bitcoind_options=:}" - ) - return bitcoind_container - - def create_prometheus_container(self, tank) -> client.V1Container: - env = [ - client.V1EnvVar(name="BITCOIN_RPC_HOST", value="127.0.0.1"), - client.V1EnvVar(name="BITCOIN_RPC_PORT", value=str(tank.rpc_port)), - client.V1EnvVar(name="BITCOIN_RPC_USER", value=tank.rpc_user), - client.V1EnvVar(name="BITCOIN_RPC_PASSWORD", value=tank.rpc_password), - ] - if tank.metrics is not None: - env.append( - client.V1EnvVar(name="METRICS", value=tank.metrics), - ) - return client.V1Container( - name="prometheus", image="bitcoindevproject/bitcoin-exporter:latest", env=env - ) - - def check_logging_crds_installed(self): - logging_crd_name = "servicemonitors.monitoring.coreos.com" - api = client.ApiextensionsV1Api() - crds = api.list_custom_resource_definition() - return bool(any(crd.metadata.name == logging_crd_name for crd in crds.items)) - - def apply_prometheus_service_monitors(self, tanks): - for tank in tanks: - if not tank.exporter: - continue - - tank_name = self.get_pod_name(tank.index, ServiceType.BITCOIN) - - service_monitor = { - "apiVersion": "monitoring.coreos.com/v1", - "kind": "ServiceMonitor", - "metadata": { - "name": tank_name, - "namespace": MAIN_NAMESPACE, - "labels": { - "app.kubernetes.io/name": "bitcoind-metrics", - "release": "prometheus", - }, - }, - "spec": { - "endpoints": [{"port": "prometheus-metrics"}], - "selector": {"matchLabels": {"app": tank_name}}, - }, - } - # Create the custom resource using the dynamic client - sc_crd = self.dynamic_client.resources.get( - api_version="monitoring.coreos.com/v1", kind="ServiceMonitor" - ) - sc_crd.create(body=service_monitor, namespace=MAIN_NAMESPACE) - - # attempts to delete the service monitors whether they exist or not - def remove_prometheus_service_monitors(self, tanks): - for tank in tanks: - try: - self.dynamic_client.resources.get( - api_version="monitoring.coreos.com/v1", kind="ServiceMonitor" - ).delete( - name=f"warnet-tank-{tank.index:06d}", - namespace=MAIN_NAMESPACE, - ) - except (ResourceNotFoundError, NotFoundError): - continue - - def get_lnnode_hostname(self, index: int) -> str: - return f"{self.get_service_name(index, ServiceType.LIGHTNING)}.{self.namespace}" - - def create_ln_container(self, tank, bitcoind_service_name, volume_mounts) -> client.V1Container: - # These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]` - bitcoind_rpc_host = f"{bitcoind_service_name}.{self.namespace}" - lightning_dns = self.get_lnnode_hostname(tank.index) - args = tank.lnnode.get_conf(lightning_dns, bitcoind_rpc_host) - self.log.debug(f"Creating lightning container for tank {tank.index} using {args=:}") - lightning_ready_probe = "" - if tank.lnnode.impl == "lnd": - lightning_ready_probe = "lncli --network=regtest getinfo" - elif tank.lnnode.impl == "cln": - lightning_ready_probe = "lightning-cli --network=regtest getinfo" - else: - raise Exception( - f"Lightning node implementation {tank.lnnode.impl} for tank {tank.index} not supported" - ) - lightning_container = client.V1Container( - name=LN_CONTAINER_NAME, - image=tank.lnnode.image, - args=args.split(" "), - env=[ - client.V1EnvVar(name="LN_IMPL", value=tank.lnnode.impl), - ], - readiness_probe=client.V1Probe( - failure_threshold=1, - success_threshold=3, - initial_delay_seconds=10, - period_seconds=2, - timeout_seconds=2, - _exec=client.V1ExecAction(command=["/bin/sh", "-c", lightning_ready_probe]), - ), - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - volume_mounts=volume_mounts, - ) - self.log.debug(f"Created lightning container for tank {tank.index}") - return lightning_container - - def create_circuitbreaker_container(self, tank, volume_mounts) -> client.V1Container: - self.log.debug(f"Creating circuitbreaker container for tank {tank.index}") - cb_container = client.V1Container( - name=LN_CB_CONTAINER_NAME, - image=tank.lnnode.cb, - args=[ - "--network=regtest", - f"--rpcserver=127.0.0.1:{tank.lnnode.rpc_port}", - f"--tlscertpath={LND_MOUNT_PATH}/tls.cert", - f"--macaroonpath={LND_MOUNT_PATH}/data/chain/bitcoin/regtest/admin.macaroon", - ], - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - volume_mounts=volume_mounts, - ) - self.log.debug(f"Created circuitbreaker container for tank {tank.index}") - return cb_container - - def create_pod_object( - self, - tank: Tank, - containers: list[client.V1Container], - volumes: list[client.V1Volume], - name: str, - ) -> client.V1Pod: - # Create and return a Pod object - # TODO: pass a custom namespace , e.g. different warnet sims can be deployed into diff namespaces - - return client.V1Pod( - api_version="v1", - kind="Pod", - metadata=client.V1ObjectMeta( - name=name, - namespace=self.namespace, - labels={ - "app": name, - "network": tank.warnet.network_name, - }, - ), - spec=client.V1PodSpec( - # Might need some more thinking on the pod restart policy, setting to Never for now - # This means if a node has a problem it dies - restart_policy="OnFailure", - containers=containers, - volumes=volumes, - ), - ) - - def get_tank_ipv4(self, index: int) -> str | None: - pod_name = self.get_pod_name(index, ServiceType.BITCOIN) - pod = self.get_pod(pod_name) - if pod: - return pod.status.pod_ip - else: - return None - - def get_tank_dns_addr(self, index: int) -> str | None: - service_name = self.get_service_name(index, ServiceType.BITCOIN) - try: - self.client.read_namespaced_service(name=service_name, namespace=self.namespace) - except ApiValueError as e: - self.log.info(ApiValueError(f"dns addr request for {service_name} raised {str(e)}")) - return None - return service_name - - def get_tank_ip_addr(self, index: int) -> str | None: - service_name = self.get_service_name(index, ServiceType.BITCOIN) - try: - endpoints = self.client.read_namespaced_endpoints( - name=service_name, namespace=self.namespace - ) - except ApiValueError as e: - self.log.info(f"ip addr request for {service_name} raised {str(e)}") - return None - - if len(endpoints.subsets) == 0: - raise Exception(f"{service_name}'s endpoint does not have an initial subset") - initial_subset = endpoints.subsets[0] - - if len(initial_subset.addresses) == 0: - raise Exception(f"{service_name}'s initial subset does not have an initial address") - initial_address = initial_subset.addresses[0] - - return str(initial_address.ip) - - def create_bitcoind_service(self, tank) -> client.V1Service: - service_name = self.get_service_name(tank.index, ServiceType.BITCOIN) - self.log.debug(f"Creating bitcoind service {service_name} for tank {tank.index}") - service = client.V1Service( - api_version="v1", - kind="Service", - metadata=client.V1ObjectMeta( - name=service_name, - labels={ - "app": self.get_pod_name(tank.index, ServiceType.BITCOIN), - "network": tank.warnet.network_name, - }, - ), - spec=client.V1ServiceSpec( - selector={"app": self.get_pod_name(tank.index, ServiceType.BITCOIN)}, - publish_not_ready_addresses=True, - ports=[ - client.V1ServicePort(port=18444, target_port=18444, name="p2p"), - client.V1ServicePort(port=tank.rpc_port, target_port=tank.rpc_port, name="rpc"), - client.V1ServicePort( - port=tank.zmqblockport, target_port=tank.zmqblockport, name="zmqblock" - ), - client.V1ServicePort( - port=tank.zmqtxport, target_port=tank.zmqtxport, name="zmqtx" - ), - client.V1ServicePort( - port=PROMETHEUS_METRICS_PORT, - target_port=PROMETHEUS_METRICS_PORT, - name="prometheus-metrics", - ), - ], - ), - ) - self.log.debug(f"Created bitcoind service {service_name} for tank {tank.index}") - return service - - def create_lightning_service(self, tank) -> client.V1Service: - service_name = self.get_service_name(tank.index, ServiceType.LIGHTNING) - self.log.debug(f"Creating lightning service {service_name} for tank {tank.index}") - service = client.V1Service( - api_version="v1", - kind="Service", - metadata=client.V1ObjectMeta( - name=service_name, - labels={ - "app": self.get_pod_name(tank.index, ServiceType.LIGHTNING), - "network": tank.warnet.network_name, - }, - ), - spec=client.V1ServiceSpec( - selector={"app": self.get_pod_name(tank.index, ServiceType.LIGHTNING)}, - cluster_ip="None", - ports=[ - client.V1ServicePort( - port=tank.lnnode.rpc_port, target_port=tank.lnnode.rpc_port, name="rpc" - ), - ], - publish_not_ready_addresses=True, - ), - ) - self.log.debug(f"Created lightning service {service_name} for tank {tank.index}") - return service - - def deploy_pods(self, warnet): - # TODO: this is pretty hack right now, ideally it should mirror - # a similar workflow to the docker backend: - # 1. read graph file, turn graph file into k8s resources, deploy the resources - tank_resource_files = [] - self.log.debug("Deploying pods") - for tank in warnet.tanks: - # Create and deploy bitcoind pod and service - bitcoind_container = self.create_bitcoind_container(tank) - bitcoind_pod = self.create_pod_object( - tank, [bitcoind_container], [], self.get_pod_name(tank.index, ServiceType.BITCOIN) - ) - - if tank.exporter and self.check_logging_crds_installed(): - prometheus_container = self.create_prometheus_container(tank) - bitcoind_pod.spec.containers.append(prometheus_container) - - bitcoind_service = self.create_bitcoind_service(tank) - self.client.create_namespaced_pod(namespace=self.namespace, body=bitcoind_pod) - # delete the service if it already exists, ignore 404 - try: - self.client.delete_namespaced_service( - name=bitcoind_service.metadata.name, namespace=self.namespace - ) - except ApiException as e: - if e.status != 404: - raise e - self.client.create_namespaced_service(namespace=self.namespace, body=bitcoind_service) - - # Create and deploy a lightning pod - if tank.lnnode: - conts = [] - vols = [] - volume_mounts = [] - if tank.lnnode.cb: - # Create a shared volume between containers in the pod - volume_name = f"ln-cb-data-{tank.index}" - vols.append( - client.V1Volume(name=volume_name, empty_dir=client.V1EmptyDirVolumeSource()) - ) - volume_mounts.append( - client.V1VolumeMount( - name=volume_name, - mount_path=LND_MOUNT_PATH, - ) - ) - # Add circuit breaker container - conts.append(self.create_circuitbreaker_container(tank, volume_mounts)) - # Add lightning container - conts.append( - self.create_ln_container(tank, bitcoind_service.metadata.name, volume_mounts) - ) - # Put it all together in a pod - lnd_pod = self.create_pod_object( - tank, conts, vols, self.get_pod_name(tank.index, ServiceType.LIGHTNING) - ) - self.client.create_namespaced_pod(namespace=self.namespace, body=lnd_pod) - # Create service for the pod - lightning_service = self.create_lightning_service(tank) - try: - self.client.delete_namespaced_service( - name=lightning_service.metadata.name, namespace=self.namespace - ) - except ApiException as e: - if e.status != 404: - raise e - self.client.create_namespaced_service( - namespace=self.namespace, body=lightning_service - ) - - # add metrics scraping for tanks configured to export metrics - if self.check_logging_crds_installed(): - self.apply_prometheus_service_monitors(warnet.tanks) - - for service_name in warnet.services: - try: - self.service_from_json(SERVICES[service_name]) - except Exception as e: - self.log.error(f"Error starting service: {service_name}\n{e}") - - self.log.debug("Containers and services created. Configuring IP addresses") - # now that the pods have had a second to create, - # get the ips and set them on the tanks - - # TODO: this is really hacky, should probably just update the generate_ipv4 function at some point - # by moving it into the base class - for tank in warnet.tanks: - pod_ip = None - while not pod_ip: - pod_name = self.get_pod_name(tank.index, ServiceType.BITCOIN) - pod = self.get_pod(pod_name) - if pod is None or pod.status is None or getattr(pod.status, "pod_ip", None) is None: - self.log.info("Waiting for pod response or pod IP...") - time.sleep(3) - continue - pod_ip = pod.status.pod_ip - - tank._ipv4 = pod_ip - self.log.debug(f"Tank {tank.index} created") - - with open(warnet.config_dir / "warnet-tanks.yaml", "w") as f: - for pod in tank_resource_files: - yaml.dump(pod.to_dict(), f) - f.write("---\n") # separator for multiple resources - self.log.info("Pod definitions saved to warnet-tanks.yaml") - - def wait_for_healthy_tanks(self, warnet, timeout=30): - """ - Wait for healthy status on all bitcoind nodes - """ - pass - - def service_from_json(self, obj): - env = [] - for pair in obj.get("environment", []): - name, value = pair.split("=") - env.append(client.V1EnvVar(name=name, value=value)) - volume_mounts = [] - volumes = [] - for vol in obj.get("config_files", []): - volume_name, mount_path = vol.split(":") - volume_name = volume_name.replace("/", "") - volume_mounts.append(client.V1VolumeMount(name=volume_name, mount_path=mount_path)) - volumes.append( - client.V1Volume(name=volume_name, empty_dir=client.V1EmptyDirVolumeSource()) - ) - - service_container = client.V1Container( - name=self.get_service_pod_name(obj["container_name_suffix"]), - image=obj["image"], - env=env, - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - volume_mounts=volume_mounts, - ) - sidecar_container = client.V1Container( - name="sidecar", - image="pinheadmz/sidecar:latest", - volume_mounts=volume_mounts, - ports=[client.V1ContainerPort(container_port=22)], - ) - service_pod = client.V1Pod( - api_version="v1", - kind="Pod", - metadata=client.V1ObjectMeta( - name=self.get_service_pod_name(obj["container_name_suffix"]), - namespace=self.namespace, - labels={ - "app": self.get_service_pod_name(obj["container_name_suffix"]), - "network": self.network_name, - }, - ), - spec=client.V1PodSpec( - restart_policy="OnFailure", - containers=[service_container, sidecar_container], - volumes=volumes, - ), - ) - - # Do not ever change this variable name. xoxo, --Zip - service_service = client.V1Service( - api_version="v1", - kind="Service", - metadata=client.V1ObjectMeta( - name=self.get_service_service_name(obj["container_name_suffix"]), - labels={ - "app": self.get_service_pod_name(obj["container_name_suffix"]), - "network": self.network_name, - }, - ), - spec=client.V1ServiceSpec( - selector={"app": self.get_service_pod_name(obj["container_name_suffix"])}, - publish_not_ready_addresses=True, - ports=[ - client.V1ServicePort(name="ssh", port=22, target_port=22), - ], - ), - ) - - self.client.create_namespaced_pod(namespace=self.namespace, body=service_pod) - self.client.create_namespaced_service(namespace=self.namespace, body=service_service) - - def write_service_config(self, source_path: str, service_name: str, destination_path: str): - obj = SERVICES[service_name] - container_name = "sidecar" - # Copy the archive from our local drive (Warnet RPC container/pod) - # to the destination service's sidecar container via ssh - self.log.info( - f"Copying local {source_path} to remote {destination_path} for {service_name}" - ) - subprocess.run( - [ - "scp", - "-o", - "StrictHostKeyChecking=accept-new", - source_path, - f"root@{self.get_service_service_name(obj['container_name_suffix'])}.{self.namespace}:/arbitrary_filename.tar", - ] - ) - self.log.info(f"Finished copying tarball for {service_name}, unpacking...") - # Unpack the archive - stream( - self.client.connect_get_namespaced_pod_exec, - self.get_service_pod_name(obj["container_name_suffix"]), - self.namespace, - container=container_name, - command=["/bin/sh", "-c", f"tar -xf /arbitrary_filename.tar -C {destination_path}"], - stderr=True, - stdin=False, - stdout=True, - tty=False, - _preload_content=False, - ) - self.log.info(f"Finished unpacking config data for {service_name} to {destination_path}") diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py new file mode 100644 index 000000000..5b392e22e --- /dev/null +++ b/src/warnet/bitcoin.py @@ -0,0 +1,373 @@ +import json +import os +import re +import shlex +import sys +from datetime import datetime +from io import BytesIO +from typing import Optional + +import click +from test_framework.messages import ser_uint256 +from test_framework.p2p import MESSAGEMAP +from urllib3.exceptions import MaxRetryError + +from .constants import BITCOINCORE_CONTAINER +from .k8s import get_default_namespace_or, get_mission, pod_log +from .process import run_command + + +@click.group(name="bitcoin") +def bitcoin(): + """Control running bitcoin nodes""" + + +@bitcoin.command(context_settings={"ignore_unknown_options": True}) +@click.argument("tank", type=str) +@click.argument("method", type=str) +@click.argument("params", type=click.UNPROCESSED, nargs=-1) # get raw unprocessed arguments +@click.option("--namespace", default=None, show_default=True) +def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): + """ + Call bitcoin-cli [params] on + """ + try: + result = _rpc(tank, method, params, namespace) + except Exception as e: + print(f"{e}") + sys.exit(1) + print(result) + + +def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + + if params: + # First, try to join all parameters into a single string. + full_param_str = " ".join(params) + + try: + # Heuristic: if the string looks like a JSON object/array, try to parse it. + # This handles the `signet_test` case where one large JSON argument was split + # by the shell into multiple params. + if full_param_str.strip().startswith(("[", "{")): + json.loads(full_param_str) + # SUCCESS: The params form a single, valid JSON object. + # Quote the entire reconstructed string as one argument. + param_str = shlex.quote(full_param_str) + else: + # It's not a JSON object, so it must be multiple distinct arguments. + # Raise an error to fall through to the individual quoting logic. + raise ValueError + except (json.JSONDecodeError, ValueError): + # FAILURE: The params are not one single JSON object. + # This handles the `rpc_test` case with mixed arguments. + # Quote each parameter individually to preserve them as separate arguments. + param_str = " ".join(shlex.quote(p) for p in params) + + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {param_str}" + else: + # Handle commands with no parameters + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" + + return run_command(cmd) + + +@bitcoin.command() +@click.argument("tank", type=str, required=True) +@click.option("--namespace", default=None, show_default=True) +def debug_log(tank: str, namespace: Optional[str]): + """ + Fetch the Bitcoin Core debug log from + """ + namespace = get_default_namespace_or(namespace) + cmd = f"kubectl logs {tank} --namespace {namespace}" + try: + print(run_command(cmd)) + except Exception as e: + print(f"{e}") + + +@bitcoin.command() +@click.argument("pattern", type=str, required=True) +@click.option("--show-k8s-timestamps", is_flag=True, default=False, show_default=True) +@click.option("--no-sort", is_flag=True, default=False, show_default=True) +def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): + """ + Grep combined bitcoind logs using regex + """ + + try: + tanks = get_mission("tank") + except MaxRetryError as e: + print(f"{e}") + sys.exit(1) + + matching_logs = [] + longest_namespace_len = 0 + + for tank in tanks: + if len(tank.metadata.namespace) > longest_namespace_len: + longest_namespace_len = len(tank.metadata.namespace) + + pod_name = tank.metadata.name + logs = pod_log(pod_name, BITCOINCORE_CONTAINER) + + if logs is not False: + try: + for line in logs: + log_entry = line.decode("utf-8").rstrip() + if re.search(pattern, log_entry): + matching_logs.append((log_entry, tank.metadata.namespace, pod_name)) + except Exception as e: + print(e) + except KeyboardInterrupt: + print("Interrupted streaming log!") + + # Sort logs if needed + if not no_sort: + matching_logs.sort(key=lambda x: x[0]) + + # Print matching logs + for log_entry, namespace, pod_name in matching_logs: + try: + # Split the log entry into Kubernetes timestamp, Bitcoin timestamp, and the rest of the log + k8s_timestamp, rest = log_entry.split(" ", 1) + bitcoin_timestamp, log_message = rest.split(" ", 1) + + # Format the output based on the show_k8s_timestamps option + if show_k8s_timestamps: + print( + f"{pod_name} {namespace:<{longest_namespace_len}} {k8s_timestamp} {bitcoin_timestamp} {log_message}" + ) + else: + print( + f"{pod_name} {namespace:<{longest_namespace_len}} {bitcoin_timestamp} {log_message}" + ) + except ValueError: + # If we can't parse the timestamps, just print the original log entry + print(f"{pod_name}: {log_entry}") + + return matching_logs + + +@bitcoin.command() +@click.argument("tank_a", type=str, required=True) +@click.argument("tank_b", type=str, required=True) +@click.option("--chain", default="regtest", show_default=True) +def messages(tank_a: str, tank_b: str, chain: str): + """ + Fetch messages sent between and in [chain] + + Optionally, include a namespace like so: tank-name.namespace + """ + + def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: + tank_split = tank.split(".") + try: + namespace = tank_split[1] + except IndexError: + namespace = None + return tank_split[0], namespace + + tank_a_split = tank_a.split(".") + tank_b_split = tank_b.split(".") + if len(tank_a_split) > 2 or len(tank_b_split) > 2: + click.secho("Accepted formats: tank-name OR tank-name.namespace") + click.secho(f"Foramts found: {tank_a} {tank_b}") + sys.exit(1) + + tank_a, namespace_a = parse_name_and_namespace(tank_a) + tank_b, namespace_b = parse_name_and_namespace(tank_b) + + try: + namespace_a = get_default_namespace_or(namespace_a) + namespace_b = get_default_namespace_or(namespace_b) + + # Get the messages + messages = get_messages( + tank_a, tank_b, chain, namespace_a=namespace_a, namespace_b=namespace_b + ) + + if not messages: + print( + f"No messages found between {tank_a} ({namespace_a}) and {tank_b} ({namespace_b})" + ) + return + + # Process and print messages + for message in messages: + if not (message.get("time") and isinstance(message["time"], (int, float))): + continue + + timestamp = datetime.utcfromtimestamp(message["time"] / 1e6).strftime( + "%Y-%m-%d %H:%M:%S" + ) + direction = ">>>" if message.get("outbound", False) else "<<<" + msgtype = message.get("msgtype", "") + body_dict = message.get("body", {}) + + if not isinstance(body_dict, dict): + continue + + body_str = ", ".join(f"{key}: {value}" for key, value in body_dict.items()) + print(f"{timestamp} {direction} {msgtype} {body_str}") + + except Exception as e: + print(f"Error fetching messages between nodes {tank_a} and {tank_b}: {e}") + + +def get_messages(tank_a: str, tank_b: str, chain: str, namespace_a: str, namespace_b: str): + """ + Fetch messages from the message capture files + """ + subdir = "" if chain == "main" else f"{chain}/" + base_dir = f"/root/.bitcoin/{subdir}message_capture" + + # Get the IP of node_b + cmd = f"kubectl get pod {tank_b} -o jsonpath='{{.status.podIP}}' --namespace {namespace_b}" + tank_b_ip = run_command(cmd).strip() + + # Get the service IP of node_b + cmd = ( + f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}' --namespace {namespace_b}" + ) + tank_b_service_ip = run_command(cmd).strip() + + # List directories in the message capture folder + cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- ls {base_dir}" + + dirs = run_command(cmd).splitlines() + + messages = [] + + for dir_name in dirs: + if tank_b_ip in dir_name or tank_b_service_ip in dir_name: + for file, outbound in [["msgs_recv.dat", False], ["msgs_sent.dat", True]]: + file_path = f"{base_dir}/{dir_name}/{file}" + # Fetch the file contents from the container + cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- cat {file_path}" + import subprocess + + blob = subprocess.run( + cmd, shell=True, capture_output=True, executable="bash" + ).stdout + + # Parse the blob + json = parse_raw_messages(blob, outbound) + messages = messages + json + + messages.sort(key=lambda x: x["time"]) + return messages + + +# This function is a hacked-up copy of process_file() from +# Bitcoin Core contrib/message-capture/message-capture-parser.py +def parse_raw_messages(blob: bytes, outbound: bool): + TIME_SIZE = 8 + LENGTH_SIZE = 4 + MSGTYPE_SIZE = 12 + + messages = [] + offset = 0 + while True: + # Read the Header + header_len = TIME_SIZE + LENGTH_SIZE + MSGTYPE_SIZE + tmp_header_raw = blob[offset : offset + header_len] + + offset = offset + header_len + if not tmp_header_raw: + break + tmp_header = BytesIO(tmp_header_raw) + time = int.from_bytes(tmp_header.read(TIME_SIZE), "little") # type: int + msgtype = tmp_header.read(MSGTYPE_SIZE).split(b"\x00", 1)[0] # type: bytes + length = int.from_bytes(tmp_header.read(LENGTH_SIZE), "little") # type: int + + # Start converting the message to a dictionary + msg_dict = {} + msg_dict["outbound"] = outbound + msg_dict["time"] = time + msg_dict["size"] = length # "size" is less readable here, but more readable in the output + + msg_ser = BytesIO(blob[offset : offset + length]) + offset = offset + length + + # Determine message type + if msgtype not in MESSAGEMAP: + # Unrecognized message type + try: + msgtype_tmp = msgtype.decode() + if not msgtype_tmp.isprintable(): + raise UnicodeDecodeError + msg_dict["msgtype"] = msgtype_tmp + except UnicodeDecodeError: + msg_dict["msgtype"] = "UNREADABLE" + msg_dict["body"] = msg_ser.read().hex() + msg_dict["error"] = "Unrecognized message type." + messages.append(msg_dict) + # print(f"WARNING - Unrecognized message type {msgtype}", file=sys.stderr) + continue + + # Deserialize the message + msg = MESSAGEMAP[msgtype]() + msg_dict["msgtype"] = msgtype.decode() + + try: + msg.deserialize(msg_ser) + except KeyboardInterrupt: + raise + except Exception: + # Unable to deserialize message body + msg_ser.seek(0, os.SEEK_SET) + msg_dict["body"] = msg_ser.read().hex() + msg_dict["error"] = "Unable to deserialize message." + messages.append(msg_dict) + # print("WARNING - Unable to deserialize message", file=sys.stderr) + continue + + # Convert body of message into a jsonable object + if length: + msg_dict["body"] = to_jsonable(msg) + messages.append(msg_dict) + return messages + + +def to_jsonable(obj: str): + HASH_INTS = [ + "blockhash", + "block_hash", + "hash", + "hashMerkleRoot", + "hashPrevBlock", + "hashstop", + "prev_header", + "sha256", + "stop_hash", + ] + + HASH_INT_VECTORS = [ + "hashes", + "headers", + "vHave", + "vHash", + ] + + if hasattr(obj, "__dict__"): + return obj.__dict__ + elif hasattr(obj, "__slots__"): + ret = {} # type: Any + for slot in obj.__slots__: + val = getattr(obj, slot, None) + if slot in HASH_INTS and isinstance(val, int): + ret[slot] = ser_uint256(val).hex() + elif slot in HASH_INT_VECTORS and all(isinstance(a, int) for a in val): + ret[slot] = [ser_uint256(a).hex() for a in val] + else: + ret[slot] = to_jsonable(val) + return ret + elif isinstance(obj, list): + return [to_jsonable(a) for a in obj] + elif isinstance(obj, bytes): + return obj.hex() + else: + return obj diff --git a/src/warnet/cli/bitcoin.py b/src/warnet/cli/bitcoin.py deleted file mode 100644 index 0c01f7d0d..000000000 --- a/src/warnet/cli/bitcoin.py +++ /dev/null @@ -1,67 +0,0 @@ -import click - -from .rpc import rpc_call - - -@click.group(name="bitcoin") -def bitcoin(): - """Control running bitcoin nodes""" - - -@bitcoin.command(context_settings={"ignore_unknown_options": True}) -@click.argument("node", type=int) -@click.argument("method", type=str) -@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments -@click.option("--network", default="warnet", show_default=True) -def rpc(node, method, params, network): - """ - Call bitcoin-cli [params] on in [network] - """ - print( - rpc_call( - "tank_bcli", {"network": network, "node": node, "method": method, "params": params} - ) - ) - - -@bitcoin.command() -@click.argument("node", type=int, required=True) -@click.option("--network", default="warnet", show_default=True) -def debug_log(node, network): - """ - Fetch the Bitcoin Core debug log from in [network] - """ - print(rpc_call("tank_debug_log", {"node": node, "network": network})) - - -@bitcoin.command() -@click.argument("node_a", type=int, required=True) -@click.argument("node_b", type=int, required=True) -@click.option("--network", default="warnet", show_default=True) -def messages(node_a, node_b, network): - """ - Fetch messages sent between and in [network] - """ - print(rpc_call("tank_messages", {"network": network, "node_a": node_a, "node_b": node_b})) - - -@bitcoin.command() -@click.argument("pattern", type=str, required=True) -@click.option("--show-k8s-timestamps", is_flag=True, default=False, show_default=True) -@click.option("--no-sort", is_flag=True, default=False, show_default=True) -@click.option("--network", default="warnet", show_default=True) -def grep_logs(pattern, network, show_k8s_timestamps, no_sort): - """ - Grep combined logs via fluentd using regex - """ - print( - rpc_call( - "logs_grep", - { - "network": network, - "pattern": pattern, - "k8s_timestamps": show_k8s_timestamps, - "no_sort": no_sort, - }, - ) - ) diff --git a/src/warnet/cli/cluster.py b/src/warnet/cli/cluster.py deleted file mode 100644 index f9a26f230..000000000 --- a/src/warnet/cli/cluster.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -import subprocess -import sys -from importlib.resources import files - -import click - -from .util import run_command - -MANIFEST_PATH = files("manifests") -RPC_PATH = files("images").joinpath("rpc") - -SCRIPTS_PATH = files("scripts") -START_SCRIPT = SCRIPTS_PATH / "start.sh" -DEPLOY_SCRIPT = SCRIPTS_PATH / "deploy.sh" -INSTALL_LOGGING_SCRIPT = SCRIPTS_PATH / "install_logging.sh" -CONNECT_LOGGING_SCRIPT = SCRIPTS_PATH / "connect_logging.sh" - - -@click.group(name="cluster", chain=True) -def cluster(): - """Start, configure and stop a warnet k8s cluster\n - \b - Supports chaining, e.g: - warcli cluster deploy-logging connect-logging - """ - pass - - -@cluster.command() -@click.option("--clean", is_flag=True, help="Remove configuration files") -def setup_minikube(clean): - """Configure a local minikube cluster""" - memory = click.prompt( - "How much RAM should we assign to the minikube cluster? (MB)", - type=int, - default=4000, - ) - cpu = click.prompt( - "How many CPUs should we assign to the minikube cluster?", type=int, default=4 - ) - env = {"WAR_MEM": str(memory), "WAR_CPU": str(cpu), "WAR_RPC": RPC_PATH} - run_command(SCRIPTS_PATH / "setup_minikube.sh", stream_output=True, env=env) - - -# TODO: Add a --dev flag to this -@cluster.command() -@click.option("--dev", is_flag=True, help="Remove configuration files") -def deploy(dev: bool): - """Deploy Warnet using the current kubectl-configured cluster""" - env = {"WAR_MANIFESTS": str(MANIFEST_PATH), "WAR_RPC": RPC_PATH} - if dev: - env["WAR_DEV"] = 1 - res = run_command(SCRIPTS_PATH / "deploy.sh", stream_output=True, env=env) - if res: - _port_start_internal() - - -@cluster.command() -def teardown(): - """Stop the warnet server and tear down the cluster""" - run_command(SCRIPTS_PATH / "stop.sh", stream_output=True) - _port_stop_internal() - - -@cluster.command() -def deploy_logging(): - """Deploy logging configurations to the cluster using helm""" - run_command(SCRIPTS_PATH / "install_logging.sh", stream_output=True) - - -@cluster.command() -def connect_logging(): - """Connect kubectl to cluster logging""" - run_command(CONNECT_LOGGING_SCRIPT, stream_output=True) - - -def is_windows(): - return sys.platform.startswith("win") - - -def run_detached_process(command): - if is_windows(): - # For Windows, use CREATE_NEW_PROCESS_GROUP and DETACHED_PROCESS - subprocess.Popen( - command, - shell=True, - stdin=None, - stdout=None, - stderr=None, - close_fds=True, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, - ) - else: - # For Unix-like systems, use nohup and redirect output - command = f"nohup {command} > /dev/null 2>&1 &" - subprocess.Popen(command, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) - - print(f"Started detached process: {command}") - - -def _port_start_internal(): - command = "kubectl port-forward svc/rpc 9276:9276" - run_detached_process(command) - print( - "Port forwarding on port 9276 started in the background. Use 'warcli' (or 'kubectl') to manage the warnet." - ) - - -@cluster.command() -def port_start(): - """Port forward (runs as a detached process)""" - _port_start_internal() - - -def _port_stop_internal(): - if is_windows(): - os.system("taskkill /F /IM kubectl.exe") - else: - os.system("pkill -f 'kubectl port-forward svc/rpc 9276:9276'") - print("Port forwarding stopped.") - - -@cluster.command() -def port_stop(): - """Stop the port forwarding process""" - _port_stop_internal() diff --git a/src/warnet/cli/graph.py b/src/warnet/cli/graph.py deleted file mode 100644 index 45128d603..000000000 --- a/src/warnet/cli/graph.py +++ /dev/null @@ -1,127 +0,0 @@ -import json -from io import BytesIO -from pathlib import Path - -import click -import networkx as nx -from rich import print -from warnet.utils import DEFAULT_TAG, create_cycle_graph, validate_graph_schema - - -@click.group(name="graph") -def graph(): - """Create and validate network graphs""" - - -@graph.command() -@click.argument("number", type=int) -@click.option("--outfile", type=click.Path()) -@click.option("--version", type=str, default=DEFAULT_TAG) -@click.option("--bitcoin_conf", type=click.Path()) -@click.option("--random", is_flag=True) -def create(number: int, outfile: Path, version: str, bitcoin_conf: Path, random: bool = False): - """ - Create a cycle graph with nodes, and include 7 extra random outbounds per node. - Returns XML file as string with or without --outfile option - """ - graph = create_cycle_graph(number, version, bitcoin_conf, random) - - if outfile: - file_path = Path(outfile) - nx.write_graphml(graph, file_path, named_key_ids=True) - bio = BytesIO() - nx.write_graphml(graph, bio, named_key_ids=True) - xml_data = bio.getvalue() - print(xml_data.decode("utf-8")) - - -@graph.command() -@click.argument("infile", type=click.Path()) -@click.option("--outfile", type=click.Path()) -@click.option("--cb", type=str) -@click.option("--ln_image", type=str) -def import_json(infile: Path, outfile: Path, cb: str, ln_image: str): - """ - Create a cycle graph with nodes imported from lnd `describegraph` JSON file, - and additionally include 7 extra random outbounds per node. Include lightning - channels and their policies as well. - Returns XML file as string with or without --outfile option. - """ - with open(infile) as f: - json_graph = json.loads(f.read()) - - # Start with a connected L1 graph with the right amount of tanks - graph = create_cycle_graph( - len(json_graph["nodes"]), version=DEFAULT_TAG, bitcoin_conf=None, random_version=False - ) - - # Initialize all the tanks with basic LN node configurations - for index, n in enumerate(graph.nodes()): - graph.nodes[n]["bitcoin_config"] = f"-uacomment=tank{index:06}" - graph.nodes[n]["ln"] = "lnd" - graph.nodes[n]["ln_config"] = "--protocol.wumbo-channels" - if cb: - graph.nodes[n]["ln_cb_image"] = cb - if ln_image: - graph.nodes[n]["ln_image"] = ln_image - - # Save a map of LN pubkey -> Tank index - ln_ids = {} - for index, node in enumerate(json_graph["nodes"]): - ln_ids[node["pub_key"]] = index - - # Offset for edge IDs - # Note create_cycle_graph() creates L1 edges all with the same id "0" - L1_edges = len(graph.edges) - - # Insert LN channels - # Ensure channels are in order by channel ID like lnd describegraph output - sorted_edges = sorted(json_graph["edges"], key=lambda chan: int(chan["channel_id"])) - for ln_index, channel in enumerate(sorted_edges): - src = ln_ids[channel["node1_pub"]] - tgt = ln_ids[channel["node2_pub"]] - cap = int(channel["capacity"]) - push = cap // 2 - openp = f"--local_amt={cap} --push_amt={push}" - srcp = "" - tgtp = "" - if channel["node1_policy"]: - srcp += f" --base_fee_msat={channel['node1_policy']['fee_base_msat']}" - srcp += f" --fee_rate_ppm={channel['node1_policy']['fee_rate_milli_msat']}" - srcp += f" --time_lock_delta={max(int(channel['node1_policy']['time_lock_delta']), 18)}" - srcp += f" --min_htlc_msat={max(int(channel['node1_policy']['min_htlc']), 1)}" - srcp += f" --max_htlc_msat={push * 1000}" - if channel["node2_policy"]: - tgtp += f" --base_fee_msat={channel['node2_policy']['fee_base_msat']}" - tgtp += f" --fee_rate_ppm={channel['node2_policy']['fee_rate_milli_msat']}" - tgtp += f" --time_lock_delta={max(int(channel['node2_policy']['time_lock_delta']), 18)}" - tgtp += f" --min_htlc_msat={max(int(channel['node2_policy']['min_htlc']), 1)}" - tgtp += f" --max_htlc_msat={push * 1000}" - - graph.add_edge( - src, - tgt, - key=ln_index + L1_edges, - channel_open=openp, - source_policy=srcp, - target_policy=tgtp, - ) - - if outfile: - file_path = Path(outfile) - nx.write_graphml(graph, file_path, named_key_ids=True) - bio = BytesIO() - nx.write_graphml(graph, bio, named_key_ids=True) - xml_data = bio.getvalue() - print(xml_data.decode("utf-8")) - - -@graph.command() -@click.argument("graph", type=click.Path()) -def validate(graph: Path): - """ - Validate a against the schema. - """ - with open(graph) as f: - graph = nx.parse_graphml(f.read(), node_type=int, force_multigraph=True) - return validate_graph_schema(graph) diff --git a/src/warnet/cli/image.py b/src/warnet/cli/image.py deleted file mode 100644 index 6965838b6..000000000 --- a/src/warnet/cli/image.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys - -import click - -from .image_build import build_image - - -@click.group(name="image") -def image(): - """Build a a custom Warnet Bitcoin Core image""" - - -@image.command() -@click.option("--repo", required=True, type=str) -@click.option("--commit-sha", required=True, type=str) -@click.option("--registry", required=True, type=str) -@click.option("--tag", required=True, type=str) -@click.option("--build-args", required=False, type=str) -@click.option("--arches", required=False, type=str) -@click.option("--action", required=False, type=str, default="load") -def build(repo, commit_sha, registry, tag, build_args, arches, action): - """ - Build bitcoind and bitcoin-cli from at as : . - Optionally deploy to remote registry using --action=push, otherwise image is loaded to local registry. - """ - res = build_image(repo, commit_sha, registry, tag, build_args, arches, action) - if not res: - sys.exit(1) diff --git a/src/warnet/cli/ln.py b/src/warnet/cli/ln.py deleted file mode 100644 index ade55759e..000000000 --- a/src/warnet/cli/ln.py +++ /dev/null @@ -1,39 +0,0 @@ -import click - -from .rpc import rpc_call - - -@click.group(name="ln") -def ln(): - """Control running lightning nodes""" - - -@ln.command(context_settings={"ignore_unknown_options": True}) -@click.argument("node", type=int) -@click.argument("command", type=str, required=True, nargs=-1) -@click.option("--network", default="warnet", show_default=True, type=str) -def rpc(node: int, command: tuple, network: str): - """ - Call lightning cli rpc on in [network] - """ - print( - rpc_call( - "tank_lncli", - {"network": network, "node": node, "command": command}, - ) - ) - - -@ln.command(context_settings={"ignore_unknown_options": True}) -@click.argument("node", type=int) -@click.option("--network", default="warnet", show_default=True, type=str) -def pubkey(node: int, network: str): - """ - Get lightning node pub key on in [network] - """ - print( - rpc_call( - "tank_ln_pub_key", - {"network": network, "node": node}, - ) - ) diff --git a/src/warnet/cli/main.py b/src/warnet/cli/main.py deleted file mode 100644 index 3aa7e8dce..000000000 --- a/src/warnet/cli/main.py +++ /dev/null @@ -1,99 +0,0 @@ -import os -import subprocess -from importlib.resources import files - -import click -from rich import print as richprint - -from .bitcoin import bitcoin -from .cluster import cluster -from .graph import graph -from .image import image -from .ln import ln -from .network import network -from .scenarios import scenarios - -QUICK_START_PATH = files("scripts").joinpath("quick_start.sh") - - -@click.group() -def cli(): - pass - - -cli.add_command(bitcoin) -cli.add_command(cluster) -cli.add_command(graph) -cli.add_command(image) -cli.add_command(ln) -cli.add_command(network) -cli.add_command(scenarios) - - -@cli.command(name="help") -@click.argument("commands", required=False, nargs=-1) -@click.pass_context -def help_command(ctx, commands): - """ - Display help information for the given [command] (and sub-command). - If no command is given, display help for the main CLI. - """ - if not commands: - # Display help for the main CLI - richprint(ctx.parent.get_help()) - return - - # Recurse down the subcommands, fetching the command object for each - cmd_obj = cli - for command in commands: - cmd_obj = cmd_obj.get_command(ctx, command) - if cmd_obj is None: - richprint(f'Unknown command "{command}" in {commands}') - return - ctx = click.Context(cmd_obj, info_name=command, parent=ctx) - - if cmd_obj is None: - richprint(f"Unknown command: {commands}") - return - - # Get the help info - help_info = cmd_obj.get_help(ctx).strip() - # Get rid of the duplication - help_info = help_info.replace("Usage: warcli help [COMMANDS]...", "Usage: warcli", 1) - richprint(help_info) - - -cli.add_command(help_command) - - -@cli.command() -def setup(): - """Check Warnet requirements are installed""" - try: - process = subprocess.Popen( - ["/bin/bash", str(QUICK_START_PATH)], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - # This preserves colours from grant's lovely script! - env=dict(os.environ, TERM="xterm-256color"), - ) - - for line in iter(process.stdout.readline, ""): - print(line, end="", flush=True) - - process.stdout.close() - return_code = process.wait() - - if return_code != 0: - print(f"Quick start script failed with return code {return_code}") - return False - return True - - except Exception as e: - print(f"An error occurred while running the quick start script: {e}") - return False - - -if __name__ == "__main__": - cli() diff --git a/src/warnet/cli/network.py b/src/warnet/cli/network.py deleted file mode 100644 index b81cbaec6..000000000 --- a/src/warnet/cli/network.py +++ /dev/null @@ -1,174 +0,0 @@ -import base64 # noqa: I001 -import json -from pathlib import Path -from importlib.resources import files - -import click -from rich import print -from rich.console import Console -from rich.table import Table - -from .rpc import rpc_call # noqa: I001 -from .util import run_command - - -DEFAULT_GRAPH_FILE = files("graphs").joinpath("default.graphml") - - -def print_repr(wn: dict) -> None: - if not isinstance(wn, dict): - print("Error, cannot print_repr of non-dict") - return - console = Console() - - # Warnet table - warnet_table = Table(show_header=True, header_style="bold") - for header in wn["warnet_headers"]: - warnet_table.add_column(header) - for row in wn["warnet"]: - warnet_table.add_row(*[str(cell) for cell in row]) - - # Tank table - tank_table = Table(show_header=True, header_style="bold") - for header in wn["tank_headers"]: - tank_table.add_column(header) - for row in wn["tanks"]: - tank_table.add_row(*[str(cell) for cell in row]) - - console.print("Warnet:") - console.print(warnet_table) - console.print("\nTanks:") - console.print(tank_table) - - -@click.group(name="network") -def network(): - """Network commands""" - - -@network.command() -@click.argument("graph_file", default=DEFAULT_GRAPH_FILE, type=click.Path()) -@click.option("--force", default=False, is_flag=True, type=bool) -@click.option("--network", default="warnet", show_default=True) -def start(graph_file: Path, force: bool, network: str): - """ - Start a warnet with topology loaded from a into [network] - """ - try: - encoded_graph_file = "" - with open(graph_file, "rb") as graph_file_buffer: - encoded_graph_file = base64.b64encode(graph_file_buffer.read()).decode("utf-8") - except Exception as e: - print(f"Error encoding graph file: {e}") - return - - result = rpc_call( - "network_from_file", - {"graph_file": encoded_graph_file, "force": force, "network": network}, - ) - assert isinstance(result, dict) - print_repr(result) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def up(network: str): - """ - Bring up a previously-stopped warnet named [network] - """ - print(rpc_call("network_up", {"network": network})) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def down(network: str): - """ - Bring down a running warnet named [network] - """ - - running_scenarios = rpc_call("scenarios_list_running", {}) - assert isinstance(running_scenarios, list) - if running_scenarios: - for scenario in running_scenarios: - pid = scenario.get("pid") - if pid: - try: - params = {"pid": pid} - rpc_call("scenarios_stop", params) - except Exception as e: - print( - f"Exception when stopping scenario: {scenario} with PID {scenario.pid}: {e}" - ) - print("Continuing with shutdown...") - continue - print(rpc_call("network_down", {"network": network})) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def info(network: str): - """ - Get info about a warnet named [network] - """ - result = rpc_call("network_info", {"network": network}) - assert isinstance(result, dict), "Result is not a dict" # Make mypy happy - print_repr(result) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def status(network: str): - """ - Get status of a warnet named [network] - """ - result = rpc_call("network_status", {"network": network}) - assert isinstance(result, list), "Result is not a list" # Make mypy happy - for tank in result: - lightning_status = "" - circuitbreaker_status = "" - if "lightning_status" in tank: - lightning_status = f"\tLightning: {tank['lightning_status']}" - if "circuitbreaker_status" in tank: - circuitbreaker_status = f"\tCircuit Breaker: {tank['circuitbreaker_status']}" - print( - f"Tank: {tank['tank_index']} \tBitcoin: {tank['bitcoin_status']}{lightning_status}{circuitbreaker_status}" - ) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def connected(network: str): - """ - Indicate whether the all of the edges in the gaph file are connected in [network] - """ - print(rpc_call("network_connected", {"network": network})) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -@click.option("--activity", type=str) -@click.option("--exclude", type=str, default="[]") -def export(network: str, activity: str, exclude: str): - """ - Export all [network] data for a "simln" service running in a container - on the network. Optionally add JSON string [activity] to simln config. - Optionally provide a list of tank indexes to [exclude]. - Returns True on success. - """ - exclude = json.loads(exclude) - print( - rpc_call("network_export", {"network": network, "activity": activity, "exclude": exclude}) - ) - - -@network.command() -@click.option("--follow", "-f", is_flag=True, help="Follow logs") -def logs(follow: bool): - """Get Kubernetes logs from the RPC server""" - command = "kubectl logs rpc-0" - stream_output = False - if follow: - command += " --follow" - stream_output = True - - run_command(command, stream_output=stream_output) diff --git a/src/warnet/cli/rpc.py b/src/warnet/cli/rpc.py deleted file mode 100644 index 9380ede0c..000000000 --- a/src/warnet/cli/rpc.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import sys -from typing import Any - -import requests -from jsonrpcclient.requests import request -from jsonrpcclient.responses import Error, Ok, parse -from warnet.server import WARNET_SERVER_PORT - - -class JSONRPCException(Exception): - def __init__(self, code, message): - try: - errmsg = f"{code} {message}" - except (KeyError, TypeError): - errmsg = "" - super().__init__(errmsg) - - -def rpc_call(rpc_method, params: dict[str, Any] | tuple[Any, ...] | None): - payload = request(rpc_method, params) - url = f"http://127.0.0.1:{WARNET_SERVER_PORT}/api" - try: - response = requests.post(url, json=payload) - except ConnectionRefusedError as e: - print(f"Error connecting to {url}. Is the server running and using matching API URL?") - logging.debug(e) - return - match parse(response.json()): - case Ok(result, _): - return result - case Error(code, message, _, _): - print(f"{code}: {message}") - sys.exit(1) - # raise JSONRPCException(code, message) diff --git a/src/warnet/cli/scenarios.py b/src/warnet/cli/scenarios.py deleted file mode 100644 index fb40be6e7..000000000 --- a/src/warnet/cli/scenarios.py +++ /dev/null @@ -1,110 +0,0 @@ -import base64 -import os -import sys - -import click -from rich import print -from rich.console import Console -from rich.table import Table - -from .rpc import rpc_call - - -@click.group(name="scenarios") -def scenarios(): - """Manage scenarios on a running network""" - - -@scenarios.command() -def available(): - """ - List available scenarios in the Warnet Test Framework - """ - console = Console() - result = rpc_call("scenarios_available", None) - if not isinstance(result, list): # Make mypy happy - print(f"Error. Expected list but got {type(result)}: {result}") - sys.exit(1) - - # Create the table - table = Table(show_header=True, header_style="bold") - table.add_column("Name") - table.add_column("Description") - - for scenario in result: - table.add_row(scenario[0], scenario[1]) - console.print(table) - - -@scenarios.command(context_settings={"ignore_unknown_options": True}) -@click.argument("scenario", type=str) -@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) -@click.option("--network", default="warnet", show_default=True) -def run(scenario, network, additional_args): - """ - Run from the Warnet Test Framework on [network] with optional arguments - """ - params = { - "scenario": scenario, - "additional_args": additional_args, - "network": network, - } - print(rpc_call("scenarios_run", params)) - - -@scenarios.command(context_settings={"ignore_unknown_options": True}) -@click.argument("scenario_path", type=str) -@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) -@click.option("--name", type=str) -@click.option("--network", default="warnet", show_default=True) -def run_file(scenario_path, network, additional_args, name=""): - """ - Run from the Warnet Test Framework on [network] with optional arguments - """ - if not scenario_path.endswith(".py"): - print("Error. Currently only python scenarios are supported") - sys.exit(1) - scenario_name = name if name else os.path.splitext(os.path.basename(scenario_path))[0] - scenario_base64 = "" - with open(scenario_path, "rb") as f: - scenario_base64 = base64.b64encode(f.read()).decode("utf-8") - - params = { - "scenario_base64": scenario_base64, - "scenario_name": scenario_name, - "additional_args": additional_args, - "network": network, - } - print(rpc_call("scenarios_run_file", params)) - - -@scenarios.command() -def active(): - """ - List running scenarios "name": "pid" pairs - """ - console = Console() - result = rpc_call("scenarios_list_running", {}) - if not result: - print("No scenarios running") - return - assert isinstance(result, list) # Make mypy happy - - table = Table(show_header=True, header_style="bold") - for key in result[0].keys(): # noqa: SIM118 - table.add_column(key.capitalize()) - - for scenario in result: - table.add_row(*[str(scenario[key]) for key in scenario]) - - console.print(table) - - -@scenarios.command() -@click.argument("pid", type=int) -def stop(pid: int): - """ - Stop scenario with PID from running - """ - params = {"pid": pid} - print(rpc_call("scenarios_stop", params)) diff --git a/src/warnet/cli/util.py b/src/warnet/cli/util.py deleted file mode 100644 index 80db73cd9..000000000 --- a/src/warnet/cli/util.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import subprocess - - -def run_command(command, stream_output=False, env=None): - # Merge the current environment with the provided env - full_env = os.environ.copy() - if env: - # Convert all env values to strings (only a safeguard) - env = {k: str(v) for k, v in env.items()} - full_env.update(env) - - if stream_output: - process = subprocess.Popen( - ["/bin/bash", "-c", command], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - universal_newlines=True, - env=full_env, - ) - - for line in iter(process.stdout.readline, ""): - print(line, end="") - - process.stdout.close() - return_code = process.wait() - - if return_code != 0: - print(f"Command failed with return code {return_code}") - return False - return True - else: - result = subprocess.run( - command, shell=True, capture_output=True, text=True, executable="/bin/bash" - ) - if result.returncode != 0: - print(f"Error: {result.stderr}") - return False - print(result.stdout) - return True diff --git a/src/warnet/cln.py b/src/warnet/cln.py deleted file mode 100644 index 53ed5ffa1..000000000 --- a/src/warnet/cln.py +++ /dev/null @@ -1,198 +0,0 @@ -import io -import tarfile - -from warnet.backend.kubernetes_backend import KubernetesBackend -from warnet.services import ServiceType -from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json - -from .lnchannel import LNChannel, LNPolicy -from .lnnode import LNNode -from .status import RunningStatus - -CLN_CONFIG_BASE = " ".join( - [ - "--network=regtest", - "--database-upgrade=true", - "--bitcoin-retry-timeout=600", - "--bind-addr=0.0.0.0:9735", - "--developer", - "--dev-fast-gossip", - "--log-level=debug", - ] -) - - -class CLNNode(LNNode): - def __init__(self, warnet, tank, backend: KubernetesBackend, options): - self.warnet = warnet - self.tank = tank - self.backend = backend - self.image = options["ln_image"] - self.cb = options["cb_image"] - self.ln_config = options["ln_config"] - self.ipv4 = generate_ipv4_addr(self.warnet.subnet) - self.rpc_port = 10009 - self.impl = "cln" - - @property - def status(self) -> RunningStatus: - return super().status - - @property - def cb_status(self) -> RunningStatus: - return super().cb_status - - def get_conf(self, ln_container_name, tank_container_name) -> str: - conf = CLN_CONFIG_BASE - conf += f" --alias={self.tank.index}" - conf += f" --grpc-port={self.rpc_port}" - conf += f" --bitcoin-rpcuser={self.tank.rpc_user}" - conf += f" --bitcoin-rpcpassword={self.tank.rpc_password}" - conf += f" --bitcoin-rpcconnect={tank_container_name}" - conf += f" --bitcoin-rpcport={self.tank.rpc_port}" - conf += f" --announce-addr=dns:{ln_container_name}:9735" - return conf - - @exponential_backoff(max_retries=20, max_delay=300) - @handle_json - def lncli(self, cmd) -> dict: - cli = "lightning-cli" - cmd = f"{cli} --network=regtest {cmd}" - return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd) - - def getnewaddress(self): - return self.lncli("newaddr")["bech32"] - - def get_pub_key(self): - res = self.lncli("getinfo") - return res["id"] - - def getURI(self): - res = self.lncli("getinfo") - if len(res["address"]) < 1: - return None - return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}' - - def get_wallet_balance(self) -> int: - res = self.lncli("listfunds") - return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) - - # returns the channel point in the form txid:output_index - def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: - tank = self.warnet.tanks[index] - [pubkey, host] = tank.lnnode.getURI().split("@") - res = self.lncli(f"fundchannel id={pubkey} {channel_open_data}") - if "txid" not in res or "outnum" not in res: - raise ValueError(f"Error opening channel to tank: {res}") - return f"{res['txid']}:{res['outnum']}" - - def update_channel_policy(self, chan_point: str, policy: str) -> str: - return self.lncli(f"setchannel {chan_point} {policy}") - - def get_graph_nodes(self) -> list[str]: - return list(n["nodeid"] for n in self.lncli("listnodes")["nodes"]) - - def get_graph_channels(self) -> list[LNChannel]: - cln_channels = self.lncli("listchannels")["channels"] - # CLN lists channels twice, once for each direction. This finds the unique channel ids. - short_channel_ids = {chan["short_channel_id"]: chan for chan in cln_channels}.keys() - channels = [] - for short_channel_id in short_channel_ids: - nodes = [ - chans for chans in cln_channels if chans["short_channel_id"] == short_channel_id - ] - # CLN has only heard about one side of the channel - if len(nodes) == 1: - channels.append(self.lnchannel_from_json(nodes[0], None)) - continue - channels.append(self.lnchannel_from_json(nodes[0], nodes[1])) - return channels - - @staticmethod - def lnchannel_from_json(node1: object, node2: object) -> LNChannel: - if not node1: - raise ValueError("node1 can't be None") - - node2_policy = ( - LNPolicy( - min_htlc=node2["htlc_minimum_msat"], - max_htlc=node2["htlc_maximum_msat"], - base_fee_msat=node2["base_fee_millisatoshi"], - fee_rate_milli_msat=node2["fee_per_millionth"], - ) - if node2 is not None - else None - ) - - return LNChannel( - node1_pub=node1["source"], - node2_pub=node1["destination"], - capacity_msat=node1["amount_msat"], - short_chan_id=node1["short_channel_id"], - node1_policy=LNPolicy( - min_htlc=node1["htlc_minimum_msat"], - max_htlc=node1["htlc_maximum_msat"], - base_fee_msat=node1["base_fee_millisatoshi"], - fee_rate_milli_msat=node1["fee_per_millionth"], - ), - node2_policy=node2_policy, - ) - - def get_peers(self) -> list[str]: - return list(p["id"] for p in self.lncli("listpeers")["peers"]) - - def connect_to_tank(self, index): - return super().connect_to_tank(index) - - def generate_cli_command(self, command: list[str]): - network = f"--network={self.tank.warnet.bitcoin_network}" - cmd = f"{network} {' '.join(command)}" - cmd = f"lightning-cli {cmd}" - return cmd - - def export(self, config: object, tar_file): - # Retrieve the credentials - ca_cert = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lightning/regtest/ca.pem", - ) - client_cert = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lightning/regtest/client.pem", - ) - client_key = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lightning/regtest/client-key.pem", - ) - name = f"ln-{self.tank.index}" - ca_cert_filename = f"{name}_ca_cert.pem" - client_cert_filename = f"{name}_client_cert.pem" - client_key_filename = f"{name}_client_key.pem" - host = self.backend.get_lnnode_hostname(self.tank.index) - - # Add the files to the in-memory tar archive - tarinfo1 = tarfile.TarInfo(name=ca_cert_filename) - tarinfo1.size = len(ca_cert) - fileobj1 = io.BytesIO(ca_cert) - tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1) - tarinfo2 = tarfile.TarInfo(name=client_cert_filename) - tarinfo2.size = len(client_cert) - fileobj2 = io.BytesIO(client_cert) - tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2) - tarinfo3 = tarfile.TarInfo(name=client_key_filename) - tarinfo3.size = len(client_key) - fileobj3 = io.BytesIO(client_key) - tar_file.addfile(tarinfo=tarinfo3, fileobj=fileobj3) - - config["nodes"].append( - { - "id": name, - "address": f"https://{host}:{self.rpc_port}", - "ca_cert": f"/simln/{ca_cert_filename}", - "client_cert": f"/simln/{client_cert_filename}", - "client_key": f"/simln/{client_key_filename}", - } - ) diff --git a/src/warnet/constants.py b/src/warnet/constants.py new file mode 100644 index 000000000..19c75599c --- /dev/null +++ b/src/warnet/constants.py @@ -0,0 +1,235 @@ +import os +from enum import Enum +from importlib.resources import files +from pathlib import Path + +# Constants used throughout the project +# Storing as constants for now but we might want a more sophisticated config management +# at some point. +SUPPORTED_TAGS = ["29.0", "28.1", "27.0", "26.0", "25.1", "24.2", "23.2", "22.2"] +DEFAULT_TAG = SUPPORTED_TAGS[0] +WEIGHTED_TAGS = [ + tag for index, tag in enumerate(reversed(SUPPORTED_TAGS)) for _ in range(index + 1) +] + +DEFAULT_NAMESPACE = "default" +LOGGING_NAMESPACE = "warnet-logging" +INGRESS_NAMESPACE = "ingress" +WARGAMES_NAMESPACE_PREFIX = "wargames-" +KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] +HELM_COMMAND = "helm upgrade --install" + +TANK_MISSION = "tank" +COMMANDER_MISSION = "commander" +LIGHTNING_MISSION = "lightning" + +BITCOINCORE_CONTAINER = "bitcoincore" +COMMANDER_CONTAINER = "commander" + + +class HookValue(Enum): + PRE_DEPLOY = "preDeploy" + POST_DEPLOY = "postDeploy" + PRE_NODE = "preNode" + POST_NODE = "postNode" + PRE_NETWORK = "preNetwork" + POST_NETWORK = "postNetwork" + + +class WarnetContent(Enum): + HOOK_VALUE = "hook_value" + NAMESPACE = "namespace" + ANNEX = "annex" + + +class AnnexMember(Enum): + NODE_NAME = "node_name" + + +PLUGIN_ANNEX = "annex" + +DEFAULT_IMAGE_REPO = "bitcoindevproject/bitcoin" + +# Bitcoin Core config +FORK_OBSERVER_RPCAUTH = "forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c" +# Fork Observer config +FORK_OBSERVER_RPC_USER = "forkobserver" +FORK_OBSERVER_RPC_PASSWORD = "tabconf2024" + +# Directories and files for non-python assets, e.g., helm charts, example scenarios, default configs +SRC_DIR = files("warnet") +RESOURCES_DIR = files("resources") +NETWORK_DIR = RESOURCES_DIR.joinpath("networks") +NAMESPACES_DIR = RESOURCES_DIR.joinpath("namespaces") +SCENARIOS_DIR = RESOURCES_DIR.joinpath("scenarios") +CHARTS_DIR = RESOURCES_DIR.joinpath("charts") +MANIFESTS_DIR = RESOURCES_DIR.joinpath("manifests") +PLUGINS_DIR = RESOURCES_DIR.joinpath("plugins") +NETWORK_FILE = "network.yaml" +DEFAULTS_FILE = "node-defaults.yaml" +NAMESPACES_FILE = "namespaces.yaml" +DEFAULTS_NAMESPACE_FILE = "namespace-defaults.yaml" + +# Helm charts +BITCOIN_CHART_LOCATION = str(CHARTS_DIR.joinpath("bitcoincore")) +FORK_OBSERVER_CHART = str(CHARTS_DIR.joinpath("fork-observer")) +COMMANDER_CHART = str(CHARTS_DIR.joinpath("commander")) +NAMESPACES_CHART_LOCATION = CHARTS_DIR.joinpath("namespaces") +FORK_OBSERVER_CHART = str(files("resources.charts").joinpath("fork-observer")) +CADDY_CHART = str(files("resources.charts").joinpath("caddy")) +CADDY_INGRESS_NAME = "caddy-ingress" + +DEFAULT_NAMESPACES = Path("two_namespaces_two_users") + +# Kubeconfig related stuffs +KUBECONFIG = os.environ.get("KUBECONFIG", os.path.expanduser("~/.kube/config")) +KUBECONFIG_UNDO = KUBECONFIG + "_warnet_undo" + +# TODO: all of this logging stuff should be a helm chart +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(asctime)s | %(levelname)-7s | %(name)-8s | %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + "detailed": { + "format": "%(asctime)s | %(levelname)-7s | [%(module)21s:%(lineno)4d] | %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + "stderr": { + "class": "logging.StreamHandler", + "level": "WARNING", + "formatter": "simple", + "stream": "ext://sys.stderr", + }, + "file": { + "class": "logging.handlers.RotatingFileHandler", + "level": "DEBUG", + "formatter": "detailed", + "filename": "warnet.log", + "maxBytes": 16000000, + "backupCount": 3, + }, + }, + "loggers": { + "root": {"level": "DEBUG", "handlers": ["stdout", "stderr", "file"]}, + "urllib3.connectionpool": {"level": "WARNING", "propagate": 1}, + "kubernetes.client.rest": {"level": "WARNING", "propagate": 1}, + "werkzeug": {"level": "WARNING", "propagate": 1}, + }, +} + +LOGGING_CRD_COMMANDS = [ + "helm repo add prometheus-community https://prometheus-community.github.io/helm-charts", + "helm repo update", + "helm upgrade --install prometheus-operator-crds prometheus-community/prometheus-operator-crds", +] + +# Helm commands for logging setup +# TODO: also lots of hardcode stuff in these helm commands, will need to fix this when moving to helm charts +LOGGING_HELM_COMMANDS = [ + "helm repo add grafana https://grafana.github.io/helm-charts", + "helm repo add prometheus-community https://prometheus-community.github.io/helm-charts", + "helm repo update", + f"helm upgrade --install --namespace warnet-logging --create-namespace --values {MANIFESTS_DIR}/loki_values.yaml loki grafana/loki --version 5.47.2", + "helm upgrade --install --namespace warnet-logging promtail grafana/promtail --create-namespace", + "helm upgrade --install --namespace warnet-logging prometheus prometheus-community/kube-prometheus-stack --namespace warnet-logging --create-namespace --set grafana.enabled=false --set prometheus.prometheusSpec.maximumStartupDurationSeconds=300", + f"helm upgrade --install grafana-dashboards {CHARTS_DIR}/grafana-dashboards --namespace warnet-logging --create-namespace", + f"helm upgrade --install --namespace warnet-logging --create-namespace loki-grafana grafana/grafana --values {MANIFESTS_DIR}/grafana_values.yaml", +] + + +INGRESS_HELM_COMMANDS = [ + "helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx", + "helm repo update", + f"helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --namespace {INGRESS_NAMESPACE} --create-namespace --set controller.progressDeadlineSeconds=600", +] + +# Helm binary +HELM_DOWNLOAD_URL_STUB = "https://get.helm.sh/" +HELM_BINARY_NAME = "helm" +HELM_BLESSED_VERSION = "v3.16.1" +HELM_BLESSED_NAME_AND_CHECKSUMS = [ + { + "name": "helm-v3.16.1-darwin-amd64.tar.gz", + "checksum": "1b194824e36da3e3889920960a93868b541c7888c905a06757e88666cfb562c9", + }, + { + "name": "helm-v3.16.1-darwin-arm64.tar.gz", + "checksum": "405a3b13f0e194180f7b84010dfe86689d7703e80612729882ad71e2a4ef3504", + }, + { + "name": "helm-v3.16.1-linux-amd64.tar.gz", + "checksum": "e57e826410269d72be3113333dbfaac0d8dfdd1b0cc4e9cb08bdf97722731ca9", + }, + { + "name": "helm-v3.16.1-linux-arm.tar.gz", + "checksum": "a15a8ddfc373628b13cd2a987206756004091a1f6a91c3b9ee8de6f0b1e2ce90", + }, + { + "name": "helm-v3.16.1-linux-arm64.tar.gz", + "checksum": "780b5b86f0db5546769b3e9f0204713bbdd2f6696dfdaac122fbe7f2f31541d2", + }, + { + "name": "helm-v3.16.1-linux-386.tar.gz", + "checksum": "92d7a47a90734b50528ffffc99cd1b2d4b9fc0f4291bac92c87ef03406a5a7b2", + }, + { + "name": "helm-v3.16.1-linux-ppc64le.tar.gz", + "checksum": "9f0178957c94516eff9a3897778edb93d78fab1f76751bd282883f584ea81c23", + }, + { + "name": "helm-v3.16.1-linux-s390x.tar.gz", + "checksum": "357f8b441cc535240f1b0ba30a42b44571d4c303dab004c9e013697b97160360", + }, + { + "name": "helm-v3.16.1-linux-riscv64.tar.gz", + "checksum": "9a2cab45b7d9282e9be7b42f86d8034dcaa2e81ab338642884843676c2f6929f", + }, + { + "name": "helm-v3.16.1-windows-amd64.zip", + "checksum": "89952ea1bace0a9498053606296ea03cf743c48294969dfc731e7f78d1dc809a", + }, + { + "name": "helm-v3.16.1-windows-arm64.zip", + "checksum": "fc370a291ed926da5e77acf42006de48e7fd5ff94d20c3f6aa10c04fea66e53c", + }, +] + + +# Kubectl binary +KUBECTL_BINARY_NAME = "kubectl" +KUBECTL_BLESSED_VERSION = "v1.31.1" +KUBECTL_DOWNLOAD_URL_STUB = f"https://dl.k8s.io/release/{KUBECTL_BLESSED_VERSION}/bin" +KUBECTL_BLESSED_NAME_AND_CHECKSUMS = [ + { + "system": "linux", + "arch": "amd64", + "checksum": "57b514a7facce4ee62c93b8dc21fda8cf62ef3fed22e44ffc9d167eab843b2ae", + }, + { + "system": "linux", + "arch": "arm64", + "checksum": "3af2451191e27ecd4ac46bb7f945f76b71e934d54604ca3ffc7fe6f5dd123edb", + }, + { + "system": "darwin", + "arch": "amd64", + "checksum": "4b86d3fb8dee8dd61f341572f1ba13c1030d493f4dc1b4831476f61f3cbb77d0", + }, + { + "system": "darwin", + "arch": "arm64", + "checksum": "08909b92e62004f4f1222dfd39214085383ea368bdd15c762939469c23484634", + }, +] diff --git a/src/warnet/control.py b/src/warnet/control.py new file mode 100644 index 000000000..d3183c73a --- /dev/null +++ b/src/warnet/control.py @@ -0,0 +1,575 @@ +import io +import json +import os +import subprocess +import sys +import time +import zipapp +from concurrent.futures import ThreadPoolExecutor, as_completed +from multiprocessing import Pool +from pathlib import Path +from typing import Optional + +import click +import inquirer +from inquirer.themes import GreenPassion +from kubernetes.client.models import V1Pod +from rich import print +from rich.console import Console +from rich.prompt import Confirm, Prompt +from rich.table import Table + +from .constants import ( + BITCOINCORE_CONTAINER, + COMMANDER_CHART, + COMMANDER_CONTAINER, + COMMANDER_MISSION, + TANK_MISSION, +) +from .k8s import ( + can_delete_pods, + delete_pod, + get_default_namespace, + get_default_namespace_or, + get_mission, + get_namespaces, + get_pod, + get_pods, + pod_log, + snapshot_bitcoin_datadir, + wait_for_init, + wait_for_pod, + write_file_to_container, +) +from .process import run_command, stream_command + +console = Console() + + +@click.command() +@click.argument("scenario_name", required=False) +def stop(scenario_name): + """Stop a running scenario or all scenarios""" + active_scenarios = [sc.metadata.name for sc in get_mission("commander")] + + if not active_scenarios: + console.print("[bold red]No active scenarios found.[/bold red]") + return + + if not scenario_name: + table = Table(title="Active Scenarios", show_header=True, header_style="bold magenta") + table.add_column("Number", style="cyan", justify="right") + table.add_column("Scenario Name", style="green") + + for idx, name in enumerate(active_scenarios, 1): + table.add_row(str(idx), name) + + console.print(table) + + choices = [str(i) for i in range(1, len(active_scenarios) + 1)] + ["a", "q"] + choice = Prompt.ask( + "[bold yellow]Enter the number of the scenario to stop, 'a' to stop all, or 'q' to quit[/bold yellow]", + choices=choices, + show_choices=False, + ) + + if choice == "q": + console.print("[bold blue]Operation cancelled.[/bold blue]") + return + elif choice == "a": + if Confirm.ask("[bold red]Are you sure you want to stop all scenarios?[/bold red]"): + stop_all_scenarios(active_scenarios) + else: + console.print("[bold blue]Operation cancelled.[/bold blue]") + return + + scenario_name = active_scenarios[int(choice) - 1] + + if scenario_name not in active_scenarios: + console.print(f"[bold red]No active scenario found with name: {scenario_name}[/bold red]") + return + + stop_scenario(scenario_name) + + +def stop_scenario(scenario_name): + """Stop a single scenario using Helm""" + # Stop the pod immediately (faster than uninstalling) + namespace = get_default_namespace() + cmd = f"kubectl --namespace {namespace} delete pod {scenario_name} --grace-period=0 --force" + if stream_command(cmd): + console.print(f"[bold green]Successfully stopped scenario: {scenario_name}[/bold green]") + else: + console.print(f"[bold red]Failed to stop scenario: {scenario_name}[/bold red]") + + # Then uninstall via helm (non-blocking) + command = f"helm uninstall {scenario_name} --namespace {namespace} --wait=false" + + # Run the helm uninstall command in the background + subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + console.print( + f"[bold yellow]Initiated helm uninstall for release: {scenario_name}[/bold yellow]" + ) + + +def _stop_single(scenario: str) -> str: + """ + Stop a single scenario + + Args: + scenario: Name of the scenario to stop + + Returns: + str: Message indicating the scenario has been stopped + """ + stop_scenario(scenario) + return f"Stopped scenario: {scenario}" + + +def stop_all_scenarios(scenarios) -> None: + """ + Stop all active scenarios in parallel using multiprocessing + + Args: + scenarios: List of scenario names to stop + + Returns: + None + """ + + with console.status("[bold yellow]Stopping all scenarios...[/bold yellow]"), Pool() as pool: + results = pool.map(_stop_single, scenarios) + + for result in results: + console.print(f"[bold green]{result}[/bold green]") + + console.print("[bold green]All scenarios have been stopped.[/bold green]") + + +@click.command() +def down(): + """Bring down a running warnet quickly""" + + def uninstall_release(namespace, release_name): + cmd = f"helm uninstall {release_name} --namespace {namespace} --wait" + print(f"Initiating uninstall of {release_name} in namespace {namespace}") + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return f"Uninstalled {release_name} in namespace {namespace}" + + def delete_pod(pod_name, namespace): + cmd = f"kubectl delete pod --ignore-not-found=true {pod_name} -n {namespace} --grace-period=0 --force" + subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return f"Initiated deletion of pod: {pod_name} in namespace {namespace}" + + if not can_delete_pods(): + click.secho("You do not have permission to bring down the network.", fg="red") + return + + namespaces = get_namespaces() + release_list: list[dict[str, str]] = [] + for v1namespace in namespaces: + namespace = v1namespace.metadata.name + command = f"helm list --namespace {namespace} -o json" + result = run_command(command) + if result: + releases = json.loads(result) + for release in releases: + release_list.append({"namespace": namespace, "name": release["name"]}) + + confirmed = "confirmed" + click.secho("Preparing to bring down the running Warnet...", fg="yellow") + + table = Table(title="PODS TO DESTROY", show_header=True, header_style="bold red") + table.add_column("Namespace", style="red") + table.add_column("Name", style="red") + for release in release_list: + table.add_row(release["namespace"], release["name"]) + console.print(table) + click.secho("PODS WILL BE DESTROYED FOREVER IF YOU TYPE 'y'", fg="red", bg="white") + + proj_answers = inquirer.prompt( + [ + inquirer.Confirm( + confirmed, + message=click.style( + "Do you want to bring down the running Warnet?", fg="yellow", bold=False + ), + default=False, + ), + ] + ) + if not proj_answers: + click.secho("Operation cancelled by user.", fg="yellow") + sys.exit(0) + if proj_answers[confirmed]: + click.secho("Bringing down the warnet...", fg="yellow") + else: + click.secho("Operation cancelled by user", fg="yellow") + sys.exit(0) + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = [] + + # Uninstall Helm releases + for release in release_list: + futures.append( + executor.submit(uninstall_release, release["namespace"], release["name"]) + ) + + # Wait for all tasks to complete and print results + for future in as_completed(futures): + console.print(f"[yellow]{future.result()}[/yellow]") + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = [] + + # Delete remaining pods + pods = get_pods() + for pod in pods: + futures.append(executor.submit(delete_pod, pod.metadata.name, pod.metadata.namespace)) + + # Wait for all tasks to complete and print results + for future in as_completed(futures): + console.print(f"[yellow]{future.result()}[/yellow]") + + console.print("[bold yellow]Teardown process initiated for all components.[/bold yellow]") + console.print("[bold yellow]Note: Some processes may continue in the background.[/bold yellow]") + console.print("[bold green]Warnet teardown process completed.[/bold green]") + + +def get_active_network(namespace): + """Get the name of the active network (Helm release) in the given namespace""" + cmd = f"helm list --namespace {namespace} --output json" + result = run_command(cmd) + if result: + import json + + releases = json.loads(result) + if releases: + # Assuming the first release is the active network + return releases[0]["name"] + return None + + +@click.command(context_settings={"ignore_unknown_options": True}) +@click.argument("scenario_file", type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.option( + "--debug", + is_flag=True, + default=False, + help="Stream scenario output and delete container when stopped", +) +@click.option( + "--source_dir", type=click.Path(exists=True, file_okay=False, dir_okay=True), required=False +) +@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) +@click.option("--admin", is_flag=True, default=False, show_default=False) +@click.option("--namespace", default=None, show_default=True) +def run( + scenario_file: str, + debug: bool, + source_dir, + additional_args: tuple[str], + admin: bool, + namespace: Optional[str], +): + """ + Run a scenario from a file. + Pass `-- --help` to get individual scenario help + """ + return _run(scenario_file, debug, source_dir, additional_args, admin, namespace) + + +def _run( + scenario_file: str, + debug: bool, + source_dir, + additional_args: tuple[str], + admin: bool, + namespace: Optional[str], +) -> str: + namespace = get_default_namespace_or(namespace) + + scenario_path = Path(scenario_file).resolve() + scenario_dir = scenario_path.parent if not source_dir else Path(source_dir).resolve() + scenario_name = scenario_path.stem + + if additional_args and ("--help" in additional_args or "-h" in additional_args): + return subprocess.run([sys.executable, scenario_path, "--help"]) + + name = f"commander-{scenario_name.replace('_', '')}-{int(time.time())}" + + # Create in-memory buffer to store python archive instead of writing to disk + archive_buffer = io.BytesIO() + + # No need to copy the entire scenarios/ directory into the archive + def filter(path): + if any(needle in str(path) for needle in [".pyc", ".csv", ".DS_Store"]): + return False + if any( + needle in str(path) + for needle in [ + "__init__.py", + "commander.py", + "test_framework", + "ln_framework", + scenario_path.name, + ] + ): + print(f"Including: {path}") + return True + return False + + # In case the scenario file is not in the root of the archive directory, + # we need to specify its relative path as a submodule + # First get the path of the file relative to the source directory + relative_path = scenario_path.relative_to(scenario_dir) + # Remove the '.py' extension + relative_name = relative_path.with_suffix("") + # Replace path separators with dots and pray the user included __init__.py + module_name = ".".join(relative_name.parts) + # Compile python archive + zipapp.create_archive( + source=scenario_dir, + target=archive_buffer, + main=f"{module_name}:main", + compressed=True, + filter=filter, + ) + + # Encode the binary data as Base64 + archive_buffer.seek(0) + archive_data = archive_buffer.read() + + # Start the commander pod with python and init containers + try: + # Construct Helm command + helm_command = [ + "helm", + "upgrade", + "--install", + "--namespace", + namespace, + "--set", + f"fullnameOverride={name}", + ] + + # Add additional arguments + if admin: + helm_command.extend(["--set", "admin=true"]) + if additional_args: + helm_command.extend(["--set", f"args={' '.join(additional_args)}"]) + + helm_command.extend([name, COMMANDER_CHART]) + + # Execute Helm command + result = subprocess.run(helm_command, check=True, capture_output=True, text=True) + + if result.returncode == 0: + print(f"Successfully deployed scenario commander: {scenario_name}") + print(f"Commander pod name: {name}") + else: + print(f"Failed to deploy scenario commander: {scenario_name}") + print(f"Error: {result.stderr}") + + except subprocess.CalledProcessError as e: + print(f"Failed to deploy scenario commander: {scenario_name}") + print(f"Error: {e.stderr}") + return None + except FileNotFoundError as e: + click.secho(e) + click.secho("Please install Helm, or run `warnet setup`.", fg="red") + return None + + # upload scenario files and network data to the init container + wait_for_init(name, namespace=namespace) + if write_file_to_container( + name, "init", "/shared/archive.pyz", archive_data, namespace=namespace + ): + print(f"Successfully uploaded scenario data to commander: {scenario_name}") + + if debug: + print("Waiting for commander pod to start...") + wait_for_pod(name, namespace=namespace) + _logs(pod_name=name, follow=True, namespace=namespace) + print("Deleting pod...") + delete_pod(name, namespace=namespace) + + return name + + +@click.command() +@click.argument("pod_name", type=str, default="") +@click.option("--follow", "-f", is_flag=True, default=False, help="Follow logs") +@click.option("--namespace", type=str, default="default", show_default=True) +def logs(pod_name: str, follow: bool, namespace: str): + """Show the logs of a pod""" + return _logs(pod_name, follow, namespace) + + +def _logs(pod_name: str, follow: bool, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + + def format_pods(pods: list[V1Pod]) -> list[str]: + sorted_pods = sorted(pods, key=lambda pod: pod.metadata.creation_timestamp, reverse=True) + return [f"{pod.metadata.name}: {pod.metadata.namespace}" for pod in sorted_pods] + + if pod_name == "": + try: + pod_list = [] + formatted_commanders = format_pods(get_mission(COMMANDER_MISSION)) + formatted_tanks = format_pods(get_mission(TANK_MISSION)) + pod_list.extend(formatted_commanders) + pod_list.extend(formatted_tanks) + + except Exception as e: + print(f"Could not fetch any pods in namespace ({namespace}): {e}") + return + + if not pod_list: + print(f"Could not fetch any pods in namespace ({namespace})") + return + + q = [ + inquirer.List( + name="pod", + message="Please choose a pod", + choices=pod_list, + ) + ] + selected = inquirer.prompt(q, theme=GreenPassion()) + if selected: + pod_name, namespace = selected["pod"].split(": ") + else: + return # cancelled by user + + try: + pod = get_pod(pod_name, namespace=namespace) + eligible_container_names = [BITCOINCORE_CONTAINER, COMMANDER_CONTAINER] + available_container_names = [container.name for container in pod.spec.containers] + container_name = next( + ( + container_name + for container_name in available_container_names + if container_name in eligible_container_names + ), + None, + ) + if not container_name: + print("Could not determine primary container.") + return + except Exception as e: + print(f"Error getting pods. Could not determine primary container: {e}") + return + + try: + stream = pod_log( + pod_name, container_name=container_name, namespace=namespace, follow=follow + ) + for line in stream: + click.echo(line.decode("utf-8").rstrip()) + except Exception as e: + print(e) + except KeyboardInterrupt: + print("Interrupted streaming log!") + + +@click.command() +@click.argument("tank_name", required=False) +@click.option("--all", "-a", "snapshot_all", is_flag=True, help="Snapshot all running tanks") +@click.option( + "--output", + "-o", + type=click.Path(), + default="./warnet-snapshots", + help="Output directory for snapshots", +) +@click.option( + "--filter", + "-f", + type=str, + help="Comma-separated list of directories and/or files to include in the snapshot", +) +def snapshot(tank_name, snapshot_all, output, filter): + """Create a snapshot of a tank's Bitcoin data or snapshot all tanks""" + tanks = get_mission("tank") + + if not tanks: + console.print("[bold red]No active tanks found.[/bold red]") + return + + # Create the output directory if it doesn't exist + os.makedirs(output, exist_ok=True) + + filter_list = [f.strip() for f in filter.split(",")] if filter else None + if snapshot_all: + snapshot_all_tanks(tanks, output, filter_list) + elif tank_name: + snapshot_single_tank(tank_name, tanks, output, filter_list) + else: + select_and_snapshot_tank(tanks, output, filter_list) + + +def find_tank_by_name(tanks, tank_name): + for tank in tanks: + if tank.metadata.name == tank_name: + return tank + return None + + +def snapshot_all_tanks(tanks, output_dir, filter_list): + with console.status("[bold yellow]Snapshotting all tanks...[/bold yellow]"): + for tank in tanks: + tank_name = tank.metadata.name + chain = tank.metadata.labels["chain"] + snapshot_tank(tank_name, chain, output_dir, filter_list) + console.print("[bold green]All tank snapshots completed.[/bold green]") + + +def snapshot_single_tank(tank_name, tanks, output_dir, filter_list): + tank = find_tank_by_name(tanks, tank_name) + if tank: + chain = tank.metadata.labels["chain"] + snapshot_tank(tank_name, chain, output_dir, filter_list) + else: + console.print(f"[bold red]No active tank found with name: {tank_name}[/bold red]") + + +def select_and_snapshot_tank(tanks, output_dir, filter_list): + table = Table(title="Active Tanks", show_header=True, header_style="bold magenta") + table.add_column("Number", style="cyan", justify="right") + table.add_column("Tank Name", style="green") + + for idx, tank in enumerate(tanks, 1): + table.add_row(str(idx), tank.metadata.name) + + console.print(table) + + choices = [str(i) for i in range(1, len(tanks) + 1)] + ["q"] + choice = Prompt.ask( + "[bold yellow]Enter the number of the tank to snapshot, or 'q' to quit[/bold yellow]", + choices=choices, + show_choices=False, + ) + + if choice == "q": + console.print("[bold blue]Operation cancelled.[/bold blue]") + return + + selected_tank = tanks[int(choice) - 1] + tank_name = selected_tank.metadata.name + chain = selected_tank.metadata.labels["chain"] + snapshot_tank(tank_name, chain, output_dir, filter_list) + + +def snapshot_tank(tank_name, chain, output_dir, filter_list): + try: + output_path = Path(output_dir).resolve() + snapshot_bitcoin_datadir(tank_name, chain, str(output_path), filter_list) + console.print( + f"[bold green]Successfully created snapshot for tank: {tank_name}[/bold green]" + ) + except Exception as e: + console.print( + f"[bold red]Failed to create snapshot for tank {tank_name}: {str(e)}[/bold red]" + ) diff --git a/src/warnet/dashboard.py b/src/warnet/dashboard.py new file mode 100644 index 000000000..6eb693911 --- /dev/null +++ b/src/warnet/dashboard.py @@ -0,0 +1,25 @@ +import sys + +import click + +from .k8s import get_ingress_ip_or_host, wait_for_ingress_endpoint + + +@click.command() +def dashboard(): + """Open the Warnet dashboard in default browser""" + import webbrowser + + timeout = 300 + click.echo(f"Waiting {timeout} seconds for ingress endpoint ...") + try: + wait_for_ingress_endpoint(timeout=timeout) + except Exception as e: + click.echo(e) + sys.exit(1) + ip = get_ingress_ip_or_host() + + url = f"http://{ip}" + + webbrowser.open(url) + click.echo(f"Warnet dashboard opened in default browser. URL: {url}") diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py new file mode 100644 index 000000000..6819fc306 --- /dev/null +++ b/src/warnet/deploy.py @@ -0,0 +1,550 @@ +import json +import subprocess +import sys +import tempfile +from multiprocessing import Process +from pathlib import Path +from typing import Optional + +import click +import yaml + +from .constants import ( + BITCOIN_CHART_LOCATION, + CADDY_CHART, + DEFAULTS_FILE, + DEFAULTS_NAMESPACE_FILE, + FORK_OBSERVER_CHART, + FORK_OBSERVER_RPC_PASSWORD, + FORK_OBSERVER_RPC_USER, + HELM_COMMAND, + INGRESS_HELM_COMMANDS, + LOGGING_CRD_COMMANDS, + LOGGING_HELM_COMMANDS, + LOGGING_NAMESPACE, + NAMESPACES_CHART_LOCATION, + NAMESPACES_FILE, + NETWORK_FILE, + PLUGIN_ANNEX, + SCENARIOS_DIR, + WARGAMES_NAMESPACE_PREFIX, + AnnexMember, + HookValue, + WarnetContent, +) +from .control import _logs, _run +from .k8s import ( + get_default_namespace, + get_default_namespace_or, + get_mission, + get_namespaces_by_type, + wait_for_ingress_controller, + wait_for_pod_ready, +) +from .process import run_command, stream_command + +HINT = "\nAre you trying to run a scenario? See `warnet run --help`" + + +def validate_directory(ctx, param, value): + directory = Path(value) + if not directory.is_dir(): + raise click.BadParameter(f"'{value}' is not a valid directory.{HINT}") + if not (directory / NETWORK_FILE).exists() and not (directory / NAMESPACES_FILE).exists(): + raise click.BadParameter( + f"'{value}' does not contain a valid network.yaml or namespaces.yaml file.{HINT}" + ) + return directory + + +@click.command(context_settings={"ignore_unknown_options": True}) +@click.argument( + "directory", + type=click.Path(exists=True), + callback=validate_directory, +) +@click.option("--debug", is_flag=True) +@click.option("--namespace", type=str, help="Specify a namespace in which to deploy the network") +@click.option("--to-all-users", is_flag=True, help="Deploy network to all user namespaces") +@click.argument("unknown_args", nargs=-1) +def deploy(directory, debug, namespace, to_all_users, unknown_args): + """Deploy a warnet with topology loaded from """ + if unknown_args: + raise click.BadParameter(f"Unknown args: {unknown_args}{HINT}") + + _deploy(directory, debug, namespace, to_all_users) + + +def _deploy(directory, debug, namespace, to_all_users): + """Deploy a warnet with topology loaded from """ + directory = Path(directory) + + if to_all_users: + namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) + processes = [] + for namespace in namespaces: + p = Process(target=_deploy, args=(directory, debug, namespace.metadata.name, False)) + p.start() + processes.append(p) + for p in processes: + p.join() + return + + if (directory / NETWORK_FILE).exists(): + run_plugins(directory, HookValue.PRE_DEPLOY, namespace) + + processes = [] + # Deploy logging CRD first to avoid synchronisation issues + deploy_logging_crd(directory, debug) + + logging_process = Process(target=deploy_logging_stack, args=(directory, debug)) + logging_process.start() + processes.append(logging_process) + + run_plugins(directory, HookValue.PRE_NETWORK, namespace) + + network_process = Process(target=deploy_network, args=(directory, debug, namespace)) + network_process.start() + + ingress_process = Process(target=deploy_ingress, args=(directory, debug)) + ingress_process.start() + processes.append(ingress_process) + + caddy_process = Process(target=deploy_caddy, args=(directory, debug)) + caddy_process.start() + processes.append(caddy_process) + + # Wait for the network process to complete + network_process.join() + + run_plugins(directory, HookValue.POST_NETWORK, namespace) + + # Start the fork observer process immediately after network process completes + fork_observer_process = Process(target=deploy_fork_observer, args=(directory, debug)) + fork_observer_process.start() + processes.append(fork_observer_process) + + # Wait for all other processes to complete + for p in processes: + p.join() + + run_plugins(directory, HookValue.POST_DEPLOY, namespace) + + elif (directory / NAMESPACES_FILE).exists(): + deploy_namespaces(directory) + else: + click.echo( + "Error: Neither network.yaml nor namespaces.yaml found in the specified directory." + ) + + +def run_plugins(directory, hook_value: HookValue, namespace, annex: Optional[dict] = None): + """Run the plugin commands within a given hook value""" + + network_file_path = directory / NETWORK_FILE + + with network_file_path.open() as f: + network_file = yaml.safe_load(f) or {} + if not isinstance(network_file, dict): + raise ValueError(f"Invalid network file structure: {network_file_path}") + + processes = [] + + plugins_section = network_file.get("plugins", {}) + hook_section = plugins_section.get(hook_value.value, {}) + for plugin_name, plugin_content in hook_section.items(): + match (plugin_name, plugin_content): + case (str(), dict()): + try: + entrypoint_path = Path(plugin_content.get("entrypoint")) + except Exception as err: + raise SyntaxError("Each plugin must have an 'entrypoint'") from err + + warnet_content = { + WarnetContent.HOOK_VALUE.value: hook_value.value, + WarnetContent.NAMESPACE.value: namespace, + PLUGIN_ANNEX: annex, + } + + cmd = ( + f"{sys.executable} {network_file_path.parent / entrypoint_path / Path('plugin.py')} entrypoint " + f"'{json.dumps(plugin_content)}' '{json.dumps(warnet_content)}'" + ) + print( + f"Queuing {hook_value.value} plugin command: {plugin_name} with {plugin_content}" + ) + + process = Process(target=run_command, args=(cmd,)) + processes.append(process) + + case _: + print( + f"The following plugin command does not match known plugin command structures: {plugin_name} {plugin_content}" + ) + sys.exit(1) + + if processes: + print(f"Starting {hook_value.value} plugins") + + for process in processes: + process.start() + + for process in processes: + process.join() + + print(f"Completed {hook_value.value} plugins") + + +def check_logging_required(directory: Path): + # check if node-defaults has logging or metrics enabled + default_file_path = directory / DEFAULTS_FILE + with default_file_path.open() as f: + default_file = yaml.safe_load(f) + if default_file.get("collectLogs", False): + return True + if default_file.get("metricsExport", False): + return True + if default_file.get("lnd", {}).get("metricsExport"): + return True + + # check to see if individual nodes have logging enabled + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + nodes = network_file.get("nodes") or [] + for node in nodes: + if node.get("collectLogs", False): + return True + if node.get("metricsExport", False): + return True + if node.get("lnd", {}).get("metricsExport"): + return True + + return False + + +def deploy_logging_crd(directory: Path, debug: bool) -> bool: + """ + This function exists so we can parallelise the rest of the loggin stack + installation + """ + if not check_logging_required(directory): + return False + + click.echo( + "Found collectLogs or metricsExport in network definition, Deploying logging stack CRD" + ) + + for command in LOGGING_CRD_COMMANDS: + if not stream_command(command): + print(f"Failed to run Helm command: {command}") + return False + return True + + +def deploy_logging_stack(directory: Path, debug: bool) -> bool: + if not check_logging_required(directory): + return False + + click.echo("Deploying logging stack") + + for command in LOGGING_HELM_COMMANDS: + if not stream_command(command): + print(f"Failed to run Helm command: {command}") + return False + return True + + +def deploy_caddy(directory: Path, debug: bool): + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + namespace = LOGGING_NAMESPACE + # TODO: get this from the helm chart + name = "caddy" + + # Only start if configured in the network file + if not network_file.get(name, {}).get("enabled", False): + return + + # configure reverse proxy to webservers in the network + services = [] + # built-in services + if check_logging_required(directory): + services.append( + {"title": "Grafana", "path": "/grafana/", "host": "loki-grafana", "port": 80} + ) + if network_file.get("fork_observer", {}).get("enabled", False): + services.append( + { + "title": "Fork Observer", + "path": "/fork-observer/", + "host": "fork-observer", + "port": 2323, + } + ) + # add any extra services + services += network_file.get("services", {}) + + click.echo(f"Adding services to dashboard: {json.dumps(services, indent=2)}") + + cmd = ( + f"{HELM_COMMAND} {name} {CADDY_CHART} " + f"--namespace {namespace} --create-namespace " + f"--set-json services='{json.dumps(services)}'" + ) + if debug: + cmd += " --debug" + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return + + wait_for_pod_ready(name, namespace) + click.echo("\nTo access the warnet dashboard run:\n warnet dashboard") + + +def deploy_ingress(directory: Path, debug: bool): + # Deploy ingress if either logging or fork observer is enabled + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + # Only start if caddy is enabled in the network file + if not network_file.get("caddy", {}).get("enabled", False): + return + click.echo("Deploying ingress controller") + + for command in INGRESS_HELM_COMMANDS: + if not stream_command(command): + print(f"Failed to run Helm command: {command}") + return False + + wait_for_ingress_controller() + + return True + + +def deploy_fork_observer(directory: Path, debug: bool) -> bool: + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + # Only start if configured in the network file + if not network_file.get("fork_observer", {}).get("enabled", False): + return False + + default_namespace = get_default_namespace() + namespace = LOGGING_NAMESPACE + cmd = f"{HELM_COMMAND} 'fork-observer' {FORK_OBSERVER_CHART} --namespace {namespace} --create-namespace" + if debug: + cmd += " --debug" + + temp_override_file_path = "" + override_string = "" + + # Add an entry for each node in the graph + for i, tank in enumerate(get_mission("tank")): + node_name = tank.metadata.name + for container in tank.spec.containers: + if container.name == "bitcoincore": + for port in container.ports: + if port.name == "rpc": + rpcport = port.container_port + if port.name == "p2p": + p2pport = port.container_port + node_config = f""" +[[networks.nodes]] +id = {i} +name = "{node_name}" +description = "{node_name}.{default_namespace}.svc:{int(p2pport)}" +rpc_host = "{node_name}.{default_namespace}.svc" +rpc_port = {int(rpcport)} +rpc_user = "{FORK_OBSERVER_RPC_USER}" +rpc_password = "{FORK_OBSERVER_RPC_PASSWORD}" +""" + + override_string += node_config + + # Create yaml string using multi-line string format + override_string = override_string.strip() + v = {"config": override_string} + v["configQueryinterval"] = network_file.get("fork_observer", {}).get("configQueryinterval", 20) + yaml_string = yaml.dump(v, default_style="|", default_flow_style=False) + + # Dump to yaml tempfile + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + temp_file.write(yaml_string) + temp_override_file_path = Path(temp_file.name) + + cmd = f"{cmd} -f {temp_override_file_path}" + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return False + return True + + +def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str] = None): + network_file_path = directory / NETWORK_FILE + namespace = get_default_namespace_or(namespace) + + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + needs_ln_init = False + supported_ln_projects = ["lnd", "cln"] + for node in network_file["nodes"]: + ln_config = node.get("ln", {}) + for key in supported_ln_projects: + if ln_config.get(key, False) and key in node and "channels" in node[key]: + needs_ln_init = True + break + if needs_ln_init: + break + + default_file_path = directory / DEFAULTS_FILE + with default_file_path.open() as f: + default_file = yaml.safe_load(f) + if any(default_file.get("ln", {}).get(key, False) for key in supported_ln_projects): + needs_ln_init = True + + processes = [] + for node in network_file["nodes"]: + p = Process(target=deploy_single_node, args=(node, directory, debug, namespace)) + p.start() + processes.append(p) + + for p in processes: + p.join() + + if needs_ln_init: + name = _run( + scenario_file=SCENARIOS_DIR / "ln_init.py", + debug=False, + source_dir=SCENARIOS_DIR, + additional_args=("--timeout-factor=0",), + admin=True, + namespace=namespace, + ) + wait_for_pod_ready(name, namespace=namespace) + _logs(pod_name=name, follow=True, namespace=namespace) + + +def deploy_single_node(node, directory: Path, debug: bool, namespace: str): + defaults_file_path = directory / DEFAULTS_FILE + click.echo(f"Deploying node: {node.get('name')}") + temp_override_file_path = "" + try: + node_name = node.get("name") + node_config_override = {k: v for k, v in node.items() if k != "name"} + + defaults_file_path = directory / DEFAULTS_FILE + cmd = f"{HELM_COMMAND} {node_name} {BITCOIN_CHART_LOCATION} --namespace {namespace} -f {defaults_file_path}" + if debug: + cmd += " --debug" + + if node_config_override: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + yaml.dump(node_config_override, temp_file) + temp_override_file_path = Path(temp_file.name) + cmd = f"{cmd} -f {temp_override_file_path}" + + run_plugins( + directory, HookValue.PRE_NODE, namespace, annex={AnnexMember.NODE_NAME.value: node_name} + ) + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return + + run_plugins( + directory, + HookValue.POST_NODE, + namespace, + annex={AnnexMember.NODE_NAME.value: node_name}, + ) + + except Exception as e: + click.echo(f"Error: {e}") + return + finally: + if temp_override_file_path: + Path(temp_override_file_path).unlink() + + +def deploy_namespaces(directory: Path): + namespaces_file_path = directory / NAMESPACES_FILE + defaults_file_path = directory / DEFAULTS_NAMESPACE_FILE + + with namespaces_file_path.open() as f: + namespaces_file = yaml.safe_load(f) + + names = [n.get("name") for n in namespaces_file["namespaces"]] + for n in names: + if not n.startswith(WARGAMES_NAMESPACE_PREFIX): + click.secho( + f"Failed to create namespace: {n}. Namespaces must start with a '{WARGAMES_NAMESPACE_PREFIX}' prefix.", + fg="red", + ) + return + + processes = [] + for namespace in namespaces_file["namespaces"]: + p = Process(target=deploy_single_namespace, args=(namespace, defaults_file_path)) + p.start() + processes.append(p) + + for p in processes: + p.join() + + +def deploy_single_namespace(namespace, defaults_file_path: Path): + click.echo(f"Deploying namespace: {namespace.get('name')}") + temp_override_file_path = "" + try: + namespace_name = namespace.get("name") + namespace_config_override = {k: v for k, v in namespace.items() if k != "name"} + + cmd = f"{HELM_COMMAND} {namespace_name} {NAMESPACES_CHART_LOCATION} -f {defaults_file_path}" + + if namespace_config_override: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + yaml.dump(namespace_config_override, temp_file) + temp_override_file_path = Path(temp_file.name) + cmd = f"{cmd} -f {temp_override_file_path}" + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return + except Exception as e: + click.echo(f"Error: {e}") + return + finally: + if temp_override_file_path: + Path(temp_override_file_path).unlink() + + +def is_windows(): + return sys.platform.startswith("win") + + +def run_detached_process(command): + if is_windows(): + # For Windows, use CREATE_NEW_PROCESS_GROUP and DETACHED_PROCESS + subprocess.Popen( + command, + shell=True, + stdin=None, + stdout=None, + stderr=None, + close_fds=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, + ) + else: + # For Unix-like systems, use nohup and redirect output + command = f"nohup {command} > /dev/null 2>&1 &" + subprocess.Popen(command, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) + + print(f"Started detached process: {command}") diff --git a/src/warnet/graph.py b/src/warnet/graph.py new file mode 100644 index 000000000..d4ac77a59 --- /dev/null +++ b/src/warnet/graph.py @@ -0,0 +1,386 @@ +import json +import os +import random +import sys +from pathlib import Path + +import click +import inquirer +import yaml +from rich import print +from rich.console import Console +from rich.table import Table + +from resources.scenarios.ln_framework.ln import ( + CHANNEL_OPEN_START_HEIGHT, + CHANNEL_OPENS_PER_BLOCK, + Policy, +) + +from .constants import ( + DEFAULT_IMAGE_REPO, + DEFAULT_TAG, + FORK_OBSERVER_RPCAUTH, + SUPPORTED_TAGS, +) + + +@click.group(name="graph", hidden=True) +def graph(): + """Create and validate network graphs""" + + +def custom_graph( + tanks: list, + datadir: Path, + fork_observer: bool, + fork_obs_query_interval: int, + caddy: bool, + logging: bool, +): + try: + datadir.mkdir(parents=False, exist_ok=False) + except FileExistsError as e: + print(e) + print("Exiting network builder without overwriting") + sys.exit(1) + + # Generate network.yaml + nodes = [] + connections = set() + total_count = sum(int(entry["count"]) for entry in tanks) + index = 0 + + for entry in tanks: + for _ in range(int(entry["count"])): + if ":" in entry["version"] and "/" in entry["version"]: + repo, tag = entry["version"].split(":") + image = {"repository": repo, "tag": tag} + else: + image = {"tag": entry["version"]} + node = {"name": f"tank-{index:04d}", "addnode": [], "image": image} + + # Connect to other nodes + available_nodes = list(range(total_count)) + # Do not connect to self + available_nodes.remove(index) + for count in range(min(int(entry["connections"]), len(available_nodes))): + # Add neighbor connection first to create minimal round-robin + if count == 0: + next_node = (index + 1) % total_count + node["addnode"].append(f"tank-{next_node:04d}") + connections.add((index, next_node)) + if next_node in available_nodes: + available_nodes.remove(next_node) + else: + random_node = random.choice(available_nodes) + # Avoid circular loops of A -> B -> A + if (random_node, index) not in connections: + node["addnode"].append(f"tank-{random_node:04d}") + connections.add((index, random_node)) + available_nodes.remove(random_node) + + nodes.append(node) + index += 1 + + network_yaml_data = {"nodes": nodes} + network_yaml_data["fork_observer"] = { + "enabled": fork_observer, + "configQueryInterval": fork_obs_query_interval, + } + network_yaml_data["caddy"] = { + "enabled": caddy, + } + + with open(os.path.join(datadir, "network.yaml"), "w") as f: + yaml.dump(network_yaml_data, f, default_flow_style=False) + + # Generate node-defaults.yaml + defaults_yaml_content = { + "chain": "regtest", + "image": { + "repository": DEFAULT_IMAGE_REPO, + "pullPolicy": "IfNotPresent", + }, + "defaultConfig": f"rpcauth={FORK_OBSERVER_RPCAUTH}\n" + + "rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo\n" + + "rpcwhitelistdefault=0\n" + + "debug=rpc\n", + } + + # Configure logging + defaults_yaml_content["collectLogs"] = logging + defaults_yaml_content["metricsExport"] = logging + + with open(os.path.join(datadir, "node-defaults.yaml"), "w") as f: + yaml.dump(defaults_yaml_content, f, default_flow_style=False, sort_keys=False) + + click.echo( + f"Project '{datadir}' has been created with 'network.yaml' and 'node-defaults.yaml'." + ) + + +def inquirer_create_network(project_path: Path): + network_name_prompt = inquirer.prompt( + [ + inquirer.Text( + "network_name", + message=click.style("Enter your network name", fg="blue", bold=True), + validate=lambda _, x: len(x) > 0, + ) + ] + ) + if not network_name_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + + tanks = [] + while True: + table = Table(title="Current Network Population", show_header=True, header_style="magenta") + table.add_column("Version", style="cyan") + table.add_column("Count", style="green") + table.add_column("Connections", style="green") + + for entry in tanks: + table.add_row(entry["version"], entry["count"], entry["connections"]) + + Console().print(table) + + add_more_prompt = inquirer.prompt( + [ + inquirer.List( + "add_more", + message=click.style("How many nodes to add? (0 = done)", fg="blue", bold=True), + choices=["0", "4", "8", "12", "20", "50", "other"], + default="12", + ) + ] + ) + if not add_more_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + if add_more_prompt["add_more"].startswith("0"): + break + + if add_more_prompt["add_more"] == "other": + how_many_prompt = inquirer.prompt( + [ + inquirer.Text( + "how_many", + message=click.style("Enter the number of nodes", fg="blue", bold=True), + validate=lambda _, x: int(x) > 0, + ) + ] + ) + if not how_many_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + how_many = how_many_prompt["how_many"] + else: + how_many = add_more_prompt["add_more"] + + tank_details_prompt = inquirer.prompt( + [ + inquirer.List( + "version", + message=click.style( + "Which version would you like to add to network?", fg="blue", bold=True + ), + choices=["other"] + SUPPORTED_TAGS, + default=DEFAULT_TAG, + ), + inquirer.List( + "connections", + message=click.style( + "How many connections would you like each of these nodes to have?", + fg="blue", + bold=True, + ), + choices=["0", "1", "2", "8", "12", "other"], + default="8", + ), + ] + ) + if not tank_details_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + break + if tank_details_prompt["version"] == "other": + custom_version_prompt = inquirer.prompt( + [ + inquirer.Text( + "version", + message=click.style( + "Provide dockerhub repository/image:tag", fg="blue", bold=True + ), + validate=lambda _, x: "/" in x and ":" in x, + ) + ] + ) + if not custom_version_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + tank_details_prompt["version"] = custom_version_prompt["version"] + + if tank_details_prompt["connections"] == "other": + how_many_conn_prompt = inquirer.prompt( + [ + inquirer.Text( + "how_many_conn", + message=click.style( + "Enter the number of connections", fg="blue", bold=True + ), + validate=lambda _, x: int(x) > 0, + ) + ] + ) + if not how_many_conn_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + how_many_conn = how_many_conn_prompt["how_many_conn"] + else: + how_many_conn = tank_details_prompt["connections"] + + tanks.append( + { + "version": tank_details_prompt["version"], + "count": how_many, + "connections": how_many_conn, + } + ) + + fork_observer = click.prompt( + click.style( + "\nWould you like to enable fork-observer on the network?", fg="blue", bold=True + ), + type=bool, + default=True, + ) + fork_observer_query_interval = 20 + if fork_observer: + fork_observer_query_interval = click.prompt( + click.style( + "\nHow often would you like fork-observer to query node status (seconds)?", + fg="blue", + bold=True, + ), + type=int, + default=20, + ) + + logging = click.prompt( + click.style( + "\nWould you like to enable grafana logging on the network?", fg="blue", bold=True + ), + type=bool, + default=False, + ) + caddy = fork_observer | logging + custom_network_path = project_path / "networks" / network_name_prompt["network_name"] + click.secho("\nGenerating custom network...", fg="yellow", bold=True) + custom_graph( + tanks, + custom_network_path, + fork_observer, + fork_observer_query_interval, + caddy, + logging, + ) + return custom_network_path + + +@click.command() +def create(): + """Create a new warnet network""" + try: + project_path = Path(os.getcwd()) + # Check if the project has a networks directory + if not (project_path / "networks").exists(): + click.secho( + "The current directory does not have a 'networks' directory. Please run 'warnet init' or 'warnet new' first.", + fg="red", + bold=True, + ) + return False + custom_network_path = inquirer_create_network(project_path) + click.secho("\nNew network created successfully!", fg="green", bold=True) + click.echo("\nRun the following command to deploy this network:") + click.echo(f"warnet deploy {custom_network_path}") + except Exception as e: + click.echo(f"{e}\n\n") + click.secho(f"An error occurred while creating a new network:\n\n{e}\n\n", fg="red") + click.secho( + "Please report the above context to https://github.com/bitcoin-dev-project/warnet/issues", + fg="yellow", + ) + return False + + +@click.command() +@click.argument("graph_file_path", type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.argument("output_path", type=click.Path(exists=False, file_okay=False, dir_okay=True)) +def import_network(graph_file_path: str, output_path: str): + """Create a network from an imported lightning network graph JSON""" + print(_import_network(graph_file_path, output_path)) + + +def _import_network(graph_file_path, output_path): + output_path = Path(output_path) + graph_file_path = Path(graph_file_path).resolve() + with open(graph_file_path) as graph_file: + graph = json.loads(graph_file.read()) + + tanks = {} + pk_to_tank = {} + tank_to_pk = {} + index = 0 + for node in graph["nodes"]: + tank = f"tank-{index:04d}" + pk_to_tank[node["pub_key"]] = tank + tank_to_pk[tank] = node["pub_key"] + tanks[tank] = {"name": tank, "ln": {"lnd": True}, "lnd": {"channels": []}} + index += 1 + print(f"Imported {index} nodes") + + sorted_edges = sorted(graph["edges"], key=lambda x: int(x["channel_id"])) + + # Start including channel open txs at this block height + block = CHANNEL_OPEN_START_HEIGHT + # Coinbase occupies the 0 position! + index = 1 + count = 0 + for edge in sorted_edges: + source = pk_to_tank[edge["node1_pub"]] + channel = { + "id": {"block": block, "index": index}, + "target": pk_to_tank[edge["node2_pub"]] + "-ln", + "capacity": int(edge["capacity"]), + "push_amt": int(edge["capacity"]) // 2, + "source_policy": Policy.from_lnd_describegraph(edge["node1_policy"]).to_dict(), + "target_policy": Policy.from_lnd_describegraph(edge["node2_policy"]).to_dict(), + } + tanks[source]["lnd"]["channels"].append(channel) + index += 1 + if index > CHANNEL_OPENS_PER_BLOCK: + index = 1 + block += 1 + count += 1 + + print(f"Imported {count} channels") + + network = {"nodes": []} + prev_node_name = list(tanks.keys())[-1] + for name, obj in tanks.items(): + obj["name"] = name + obj["addnode"] = [prev_node_name] + prev_node_name = name + network["nodes"].append(obj) + + output_path.mkdir(parents=True, exist_ok=True) + # This file must exist and must contain at least one line of valid yaml + with open(output_path / "node-defaults.yaml", "w") as f: + f.write(f"imported_from: {graph_file_path}\n") + # Here's the good stuff + with open(output_path / "network.yaml", "w") as f: + f.write(yaml.dump(network, sort_keys=False)) + return f"Network created in {output_path.resolve()}" diff --git a/src/warnet/graph_schema.json b/src/warnet/graph_schema.json deleted file mode 100644 index ac1f7aa9f..000000000 --- a/src/warnet/graph_schema.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "graph": { - "type": "object", - "properties": { - "node": {"type": "object"}, - "edge": {"type": "object"}, - "node_default": {"type": "object"}, - "edge_default": {"type": "object"}, - "services": { - "type": "string", - "comment": "A space-separated list of extra service containers to deploy in the network. See [docs/services.md](services.md) for complete list of available services" - } - }, - "additionalProperties": false - }, - "node": { - "type": "object", - "properties": { - "version": { - "type": "string", - "comment": "Bitcoin Core version with an available Warnet tank image on Dockerhub. May also be a GitHub repository with format user/repository:branch to build from source code"}, - "image": { - "type": "string", - "comment": "Bitcoin Core Warnet tank image on Dockerhub with the format repository/image:tag"}, - "bitcoin_config": { - "type": "string", - "default": "", - "comment": "A string of Bitcoin Core options in command-line format, e.g. '-debug=net -blocksonly'"}, - "tc_netem": { - "type": "string", - "comment": "A tc-netem command as a string beginning with 'tc qdisc add dev eth0 root netem'"}, - "exporter": { - "type": "boolean", - "default": false, - "comment": "Whether to attach a Prometheus data exporter to the tank"}, - "metrics": { - "type": "string", - "comment": "A space-separated string of RPC queries to scrape by Prometheus"}, - "collect_logs": { - "type": "boolean", - "default": false, - "comment": "Whether to collect Bitcoin Core debug logs with Promtail"}, - "build_args": { - "type": "string", - "default": "", - "comment": "A string of configure options used when building Bitcoin Core from source code, e.g. '--without-gui --disable-tests'"}, - "ln": { - "type": "string", - "comment": "Attach a lightning network node of this implementation (currently only supports 'lnd' or 'cln')"}, - "ln_image": { - "type": "string", - "comment": "Specify a lightning network node image from Dockerhub with the format repository/image:tag"}, - "ln_cb_image": { - "type": "string", - "comment": "Specify a lnd Circuit Breaker image from Dockerhub with the format repository/image:tag"}, - "ln_config": { - "type": "string", - "comment": "A string of arguments for the lightning network node in command-line format, e.g. '--protocol.wumbo-channels --bitcoin.timelockdelta=80'"} - }, - "additionalProperties": false, - "oneOf": [ - {"required": ["version"]}, - {"required": ["image"]} - ], - "required": [] - }, - "edge": { - "type": "object", - "properties": { - "channel_open": { - "type": "string", - "comment": "Indicate that this edge is a lightning channel with these arguments passed to lnd openchannel"}, - "source_policy": { - "type": "string", - "comment": "Update the channel originator policy by passing these arguments passed to lnd updatechanpolicy"}, - "target_policy": { - "type": "string", - "comment": "Update the channel partner policy by passing these arguments passed to lnd updatechanpolicy"} - }, - "additionalProperties": false, - "required": [] - } -} diff --git a/src/warnet/image.py b/src/warnet/image.py new file mode 100644 index 000000000..a13fe8e6e --- /dev/null +++ b/src/warnet/image.py @@ -0,0 +1,37 @@ +import sys + +import click + +from .image_build import build_image + + +@click.group(name="image") +def image(): + """Build a custom Warnet Bitcoin Core image""" + + +@image.command() +@click.option("--repo", required=True, type=str) +@click.option("--commit-sha", required=True, type=str) +@click.option( + "--tags", + required=True, + type=str, + help="Comma-separated list of full tags including image names", +) +@click.option("--build-args", required=False, type=str) +@click.option("--arches", required=False, type=str) +@click.option("--action", required=False, type=str, default="load") +def build(repo, commit_sha, tags, build_args, arches, action): + """Build a Bitcoin Core Docker image with specified parameters. + + \b + Usage Examples: + # Build an image for Warnet repository + warnet image build --repo bitcoin/bitcoin --commit-sha d6db87165c6dc2123a759c79ec236ea1ed90c0e3 --tags bitcoindevproject/bitcoin:v29.0-rc2 --arches amd64,arm64,armhf --action push + # Build an image for local testing + warnet image build --repo bitcoin/bitcoin --commit-sha d6db87165c6dc2123a759c79ec236ea1ed90c0e3 --tags bitcoindevproject/bitcoin:v29.0-rc2 --action load + """ + res = build_image(repo, commit_sha, tags, build_args, arches, action) + if not res: + sys.exit(1) diff --git a/src/warnet/cli/image_build.py b/src/warnet/image_build.py similarity index 72% rename from src/warnet/cli/image_build.py rename to src/warnet/image_build.py index 1cc7864f8..b71be6d34 100644 --- a/src/warnet/cli/image_build.py +++ b/src/warnet/image_build.py @@ -3,7 +3,7 @@ ARCHES = ["amd64", "arm64", "armhf"] -dockerfile_path = files("images.bitcoin").joinpath("Dockerfile") +dockerfile_path = files("resources.images.bitcoin").joinpath("Dockerfile.dev") def run_command(command): @@ -17,14 +17,13 @@ def run_command(command): def build_image( repo: str, commit_sha: str, - docker_registry: str, - tag: str, + tags: str, build_args: str, arches: str, action: str, ): if not build_args: - build_args = '"--disable-tests --without-gui --disable-bench --disable-fuzz-binary --enable-suppress-external-warnings --disable-dependency-tracking "' + build_args = '"-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON "' else: build_args = f'"{build_args}"' @@ -41,8 +40,7 @@ def build_image( print(f"{repo=:}") print(f"{commit_sha=:}") - print(f"{docker_registry=:}") - print(f"{tag=:}") + print(f"{tags=:}") print(f"{build_args=:}") print(f"{build_arches=:}") @@ -52,14 +50,13 @@ def build_image( use_builder_cmd = f"docker buildx use --builder {builder_name}" cleanup_builder_cmd = f"docker buildx rm {builder_name}" - if not run_command(create_builder_cmd): # noqa: SIM102 - # try to use existing - if not run_command(use_builder_cmd): - print(f"Could not create or use builder {builder_name} and create new builder") - return False + if not run_command(create_builder_cmd) and not run_command(use_builder_cmd): + print(f"Could not create or use builder {builder_name} and create new builder") + return False - image_full_name = f"{docker_registry}:{tag}" - print(f"{image_full_name=}") + tag_list = tags.split(",") + tag_args = " ".join([f"--tag {tag.strip()}" for tag in tag_list]) + print(f"{tag_args=}") platforms = ",".join([f"linux/{arch}" for arch in build_arches]) @@ -69,7 +66,7 @@ def build_image( f" --build-arg REPO={repo}" f" --build-arg COMMIT_SHA={commit_sha}" f" --build-arg BUILD_ARGS={build_args}" - f" --tag {image_full_name}" + f" {tag_args}" f" --file {dockerfile_path}" f" {dockerfile_path.parent}" f" --{action}" @@ -82,7 +79,6 @@ def build_image( except Exception as e: print(f"Error:\n{e}") finally: - # Tidy up the buildx builder if not run_command(cleanup_builder_cmd): print("Warning: Failed to remove the buildx builder.") else: diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py new file mode 100644 index 000000000..9ca39feaa --- /dev/null +++ b/src/warnet/k8s.py @@ -0,0 +1,701 @@ +import json +import os +import sys +import tarfile +import tempfile +from pathlib import Path +from time import sleep, time +from typing import Optional + +import yaml +from kubernetes import client, config, watch +from kubernetes.client import CoreV1Api +from kubernetes.client.models import V1Namespace, V1Pod, V1PodList, V1TokenRequestSpec +from kubernetes.client.rest import ApiException +from kubernetes.dynamic import DynamicClient +from kubernetes.stream import stream + +from .constants import ( + CADDY_INGRESS_NAME, + DEFAULT_NAMESPACE, + INGRESS_NAMESPACE, + KUBE_INTERNAL_NAMESPACES, + KUBECONFIG, + LOGGING_NAMESPACE, +) +from .process import run_command, stream_command + + +class K8sError(Exception): + pass + + +def get_static_client() -> CoreV1Api: + config.load_kube_config(config_file=KUBECONFIG) + return client.CoreV1Api() + + +def get_dynamic_client() -> DynamicClient: + config.load_kube_config(config_file=KUBECONFIG) + return DynamicClient(client.ApiClient()) + + +def get_pods() -> list[V1Pod]: + sclient = get_static_client() + pods: list[V1Pod] = [] + namespaces = get_namespaces() + for ns in namespaces: + namespace = ns.metadata.name + try: + pod_list: V1PodList = sclient.list_namespaced_pod(namespace) + for pod in pod_list.items: + pods.append(pod) + except Exception as e: + raise e + return pods + + +def get_pod(name: str, namespace: Optional[str] = None) -> V1Pod: + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + return sclient.read_namespaced_pod(name=name, namespace=namespace) + + +def get_mission(mission: str) -> list[V1Pod]: + pods = get_pods() + crew: list[V1Pod] = [] + for pod in pods: + if "mission" in pod.metadata.labels and pod.metadata.labels["mission"] == mission: + crew.append(pod) + return crew + + +def get_pod_exit_status(pod_name, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + try: + sclient = get_static_client() + pod = sclient.read_namespaced_pod(name=pod_name, namespace=namespace) + for container_status in pod.status.container_statuses: + if container_status.state.terminated: + return container_status.state.terminated.exit_code + return None + except client.ApiException as e: + print(f"Exception when calling CoreV1Api->read_namespaced_pod: {e}") + return None + + +def get_channels(namespace: Optional[str] = None) -> any: + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + config_maps = sclient.list_namespaced_config_map( + namespace=namespace, label_selector="channels=true" + ) + channels = [] + for cm in config_maps.items: + channel_jsons = json.loads(cm.data["channels"]) + for channel_json in channel_jsons: + channel_json["source"] = cm.data["source"] + channels.append(channel_json) + return channels + + +def create_kubernetes_object( + kind: str, metadata: dict[str, any], spec: dict[str, any] = None +) -> dict[str, any]: + metadata["namespace"] = get_default_namespace() + obj = { + "apiVersion": "v1", + "kind": kind, + "metadata": metadata, + } + if spec is not None: + obj["spec"] = spec + return obj + + +def set_kubectl_context(namespace: str) -> bool: + """ + Set the default kubectl context to the specified namespace. + """ + command = f"kubectl config set-context --current --namespace={namespace}" + result = stream_command(command) + if result: + print(f"Kubectl context set to namespace: {namespace}") + else: + print(f"Failed to set kubectl context to namespace: {namespace}") + return result + + +def apply_kubernetes_yaml(yaml_file: str) -> bool: + command = f"kubectl apply -f {yaml_file}" + return stream_command(command) + + +def apply_kubernetes_yaml_obj(yaml_obj: str) -> None: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + yaml.dump(yaml_obj, temp_file) + temp_file_path = temp_file.name + + try: + apply_kubernetes_yaml(temp_file_path) + finally: + Path(temp_file_path).unlink() + + +def delete_namespace(namespace: str) -> bool: + command = f"kubectl delete namespace {namespace} --ignore-not-found" + return run_command(command) + + +def delete_pod(pod_name: str, namespace: Optional[str] = None) -> bool: + namespace = get_default_namespace_or(namespace) + command = f"kubectl -n {namespace} delete pod {pod_name}" + return stream_command(command) + + +def get_default_namespace() -> str: + command = "kubectl config view --minify -o jsonpath='{..namespace}'" + try: + kubectl_namespace = run_command(command) + except Exception as e: + print(e) + if str(e).find("command not found"): + print( + "It looks like kubectl is not installed. Please install it to continue: " + "https://kubernetes.io/docs/tasks/tools/" + ) + sys.exit(1) + return kubectl_namespace if kubectl_namespace else DEFAULT_NAMESPACE + + +def get_default_namespace_or(namespace: Optional[str]) -> str: + return namespace if namespace else get_default_namespace() + + +def snapshot_bitcoin_datadir( + pod_name: str, + chain: str, + local_path: str = "./", + filters: list[str] = None, + namespace: Optional[str] = None, +) -> None: + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + + try: + sclient.read_namespaced_pod(name=pod_name, namespace=namespace) + + # Filter down to the specified list of directories and files + # This allows for creating snapshots of only the relevant data, e.g., + # we may want to snapshot the blocks but not snapshot peers.dat or the node + # wallets. + # + # TODO: never snapshot bitcoin.conf, as this is managed by the helm config + if filters: + find_command = [ + "find", + f"/root/.bitcoin/{chain}", + "(", + "-type", + "f", + "-o", + "-type", + "d", + ")", + "(", + "-name", + filters[0], + ] + for f in filters[1:]: + find_command.extend(["-o", "-name", f]) + find_command.append(")") + else: + # If no filters, get everything in the Bitcoin directory (TODO: exclude bitcoin.conf) + find_command = ["find", f"/root/.bitcoin/{chain}"] + + resp = stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=find_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + file_list = [] + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + file_list.extend(resp.read_stdout().strip().split("\n")) + if resp.peek_stderr(): + print(f"Error: {resp.read_stderr()}") + + resp.close() + if not file_list: + print("No matching files or directories found.") + return + tar_command = ["tar", "-czf", "/tmp/bitcoin_data.tar.gz", "-C", f"/root/.bitcoin/{chain}"] + tar_command.extend( + [os.path.relpath(f, f"/root/.bitcoin/{chain}") for f in file_list if f.strip()] + ) + resp = stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=tar_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + print(f"Tar output: {resp.read_stdout()}") + if resp.peek_stderr(): + print(f"Error: {resp.read_stderr()}") + resp.close() + local_file_path = Path(local_path) / f"{pod_name}_bitcoin_data.tar.gz" + copy_command = ( + f"kubectl cp {namespace}/{pod_name}:/tmp/bitcoin_data.tar.gz {local_file_path}" + ) + if not stream_command(copy_command): + raise Exception("Failed to copy tar file from pod to local machine") + + print(f"Bitcoin data exported successfully to {local_file_path}") + cleanup_command = ["rm", "/tmp/bitcoin_data.tar.gz"] + stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=cleanup_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + ) + + print("To untar and repopulate the directory, use the following command:") + print(f"tar -xzf {local_file_path} -C /path/to/destination/.bitcoin/{chain}") + + except Exception as e: + print(f"An error occurred: {str(e)}") + + +def wait_for_pod_ready(name, namespace, timeout=300): + sclient = get_static_client() + w = watch.Watch() + for event in w.stream( + sclient.list_namespaced_pod, namespace=namespace, timeout_seconds=timeout + ): + pod = event["object"] + if pod.metadata.name == name and pod.status.phase == "Running": + conditions = pod.status.conditions or [] + ready_condition = next((c for c in conditions if c.type == "Ready"), None) + if ready_condition and ready_condition.status == "True": + w.stop() + return True + print(f"Timeout waiting for pod {name} to be ready.") + return False + + +def wait_for_init(pod_name, timeout=300, namespace: Optional[str] = None, quiet: bool = False): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + w = watch.Watch() + for event in w.stream( + sclient.list_namespaced_pod, namespace=namespace, timeout_seconds=timeout + ): + pod = event["object"] + if pod.metadata.name == pod_name: + if not pod.status.init_container_statuses: + continue + for init_container_status in pod.status.init_container_statuses: + if init_container_status.state.running: + if not quiet: + print(f"initContainer in pod {pod_name} ({namespace}) is ready") + w.stop() + return True + if not quiet: + print(f"Timeout waiting for initContainer in {pod_name} ({namespace}) to be ready.") + return False + + +def wait_for_ingress_controller(timeout=300): + # get name of ingress controller pod + sclient = get_static_client() + pods = sclient.list_namespaced_pod(namespace=INGRESS_NAMESPACE) + for pod in pods.items: + if "ingress-nginx-controller" in pod.metadata.name: + return wait_for_pod_ready(pod.metadata.name, INGRESS_NAMESPACE, timeout) + + +def wait_for_ingress_endpoint(timeout=300): + config.load_kube_config() + networking_v1 = client.NetworkingV1Api() + start = time() + while time() - start < timeout: + try: + ingress = networking_v1.read_namespaced_ingress(CADDY_INGRESS_NAME, LOGGING_NAMESPACE) + except ApiException as e: + msg = ( + f'Failed to read ingress with name "{CADDY_INGRESS_NAME}" from namespace "{LOGGING_NAMESPACE}"\n' + + str(e).rstrip() + ) + if e.status == 404: + msg += "\n\nDid you deploy a network with caddy enabled?" + raise Exception(msg) from e + lb_ingress = ingress.status.load_balancer.ingress + if lb_ingress and (lb_ingress[0].hostname or lb_ingress[0].ip): + return True + sleep(1) + msg = ( + f"Ingress endpoint not found within {timeout} seconds.\n" + + "If you are running Minikube please run 'minikube tunnel' in a separate terminal.\n" + + "If you are running in the cloud, you may need to wait a short while while the load balancer is provisioned" + ) + raise TimeoutError(msg) + + +def get_ingress_ip_or_host(): + config.load_kube_config() + networking_v1 = client.NetworkingV1Api() + try: + ingress = networking_v1.read_namespaced_ingress(CADDY_INGRESS_NAME, LOGGING_NAMESPACE) + if ingress.status.load_balancer.ingress[0].hostname: + return ingress.status.load_balancer.ingress[0].hostname + return ingress.status.load_balancer.ingress[0].ip + except Exception as e: + print(f"Error getting ingress IP: {e}") + return None + + +def pod_log( + pod_name, container_name=None, follow=False, namespace: Optional[str] = None, tail_lines=None +): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + + try: + return sclient.read_namespaced_pod_log( + name=pod_name, + namespace=namespace, + container=container_name, + follow=follow, + _preload_content=False, + tail_lines=tail_lines, + ) + except ApiException as e: + raise Exception(json.loads(e.body.decode("utf-8"))["message"]) from None + + +def wait_for_pod(pod_name, timeout_seconds=10, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + while timeout_seconds > 0: + pod = sclient.read_namespaced_pod_status(name=pod_name, namespace=namespace) + if pod.status.phase != "Pending": + return + sleep(1) + timeout_seconds -= 1 + + +def write_file_to_container( + pod_name, container_name, dst_path, data, namespace: Optional[str] = None, quiet: bool = False +): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + exec_command = ["sh", "-c", f"cat > {dst_path}.tmp && sync"] + try: + res = stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=exec_command, + container=container_name, + stdin=True, + stderr=True, + stdout=True, + tty=False, + _preload_content=False, + ) + res.write_stdin(data) + res.close() + rename_command = ["sh", "-c", f"mv {dst_path}.tmp {dst_path}"] + stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=rename_command, + container=container_name, + stdin=False, + stderr=True, + stdout=True, + tty=False, + ) + if not quiet: + print(f"Successfully copied data to {pod_name}({container_name}):{dst_path}") + return True + except Exception as e: + print(f"Failed to copy data to {pod_name}({container_name}):{dst_path}:\n{e}") + + +def get_kubeconfig_value(jsonpath): + command = f"kubectl config view --minify --raw -o jsonpath={jsonpath}" + return run_command(command) + + +def get_cluster_of_current_context(kubeconfig_data: dict) -> dict: + # Get the current context name + current_context_name = kubeconfig_data.get("current-context") + + if not current_context_name: + raise K8sError("No current context found in kubeconfig.") + + # Find the context entry for the current context + context_entry = next( + ( + context + for context in kubeconfig_data.get("contexts", []) + if context["name"] == current_context_name + ), + None, + ) + + if not context_entry: + raise K8sError(f"Context '{current_context_name}' not found in kubeconfig.") + + # Get the cluster name from the context entry + cluster_name = context_entry.get("context", {}).get("cluster") + + if not cluster_name: + raise K8sError(f"Cluster not specified in context '{current_context_name}'.") + + # Find the cluster entry associated with the cluster name + cluster_entry = next( + ( + cluster + for cluster in kubeconfig_data.get("clusters", []) + if cluster["name"] == cluster_name + ), + None, + ) + + if not cluster_entry: + raise K8sError(f"Cluster '{cluster_name}' not found in kubeconfig.") + + return cluster_entry + + +def get_namespaces() -> list[V1Namespace]: + sclient = get_static_client() + try: + return [ + ns + for ns in sclient.list_namespace().items + if ns.metadata.name not in KUBE_INTERNAL_NAMESPACES + ] + + except ApiException as e: + if e.status == 403: + ns = sclient.read_namespace(name=get_default_namespace()) + return [ns] + else: + return [] + + +def get_namespaces_by_type(namespace_type: str) -> list[V1Namespace]: + """ + Get all namespaces beginning with `prefix`. Returns empty list of no namespaces with the specified prefix are found. + """ + namespaces = get_namespaces() + return [ns for ns in namespaces if ns.metadata.name.startswith(namespace_type)] + + +def get_warnet_user_service_accounts_in_namespace(namespace): + """ + Get all service accounts in a namespace that were created for human users + (not scenario commanders or other pods) + Returns an empty list if no applicable service accounts are found in the specified namespace. + """ + sclient = get_static_client() + sas = sclient.list_namespaced_service_account(namespace) + return [ + sa + for sa in sas.items + if sa.metadata.labels + and "mission" in sa.metadata.labels + and sa.metadata.labels["mission"] == "user" + ] + + +def get_token_for_service_acount(sa, duration): + sclient = get_static_client() + spec = V1TokenRequestSpec( + audiences=["https://kubernetes.default.svc"], expiration_seconds=duration + ) + resp = sclient.create_namespaced_service_account_token( + name=sa.metadata.name, namespace=sa.metadata.namespace, body=spec + ) + return resp.status.token + + +def can_delete_pods(namespace: Optional[str] = None) -> bool: + namespace = get_default_namespace_or(namespace) + + get_static_client() + auth_api = client.AuthorizationV1Api() + + # Define the SelfSubjectAccessReview request for deleting pods + access_review = client.V1SelfSubjectAccessReview( + spec=client.V1SelfSubjectAccessReviewSpec( + resource_attributes=client.V1ResourceAttributes( + namespace=namespace, + verb="delete", # Action: 'delete' + resource="pods", # Resource: 'pods' + ) + ) + ) + + try: + # Perform the SelfSubjectAccessReview check + review_response = auth_api.create_self_subject_access_review(body=access_review) + + # Check the result and return + if review_response.status.allowed: + print(f"Service account can delete pods in namespace '{namespace}'.") + return True + else: + print(f"Service account CANNOT delete pods in namespace '{namespace}'.") + return False + + except ApiException as e: + print(f"An error occurred: {e}") + return False + + +def open_kubeconfig(kubeconfig_path: str) -> dict: + try: + with open(kubeconfig_path) as file: + return yaml.safe_load(file) + except FileNotFoundError as e: + raise K8sError(f"Kubeconfig file {kubeconfig_path} not found.") from e + except yaml.YAMLError as e: + raise K8sError(f"Error parsing kubeconfig: {e}") from e + + +def write_kubeconfig(kube_config: dict, kubeconfig_path: str) -> None: + dir_name = os.path.dirname(kubeconfig_path) + try: + with tempfile.NamedTemporaryFile("w", dir=dir_name, delete=False) as temp_file: + yaml.safe_dump(kube_config, temp_file) + os.replace(temp_file.name, kubeconfig_path) + except Exception as e: + os.remove(temp_file.name) + raise K8sError(f"Error writing kubeconfig: {kubeconfig_path}") from e + + +def download( + pod_name: str, + source_path: Path, + destination_path: Path = Path("."), + namespace: Optional[str] = None, +) -> Path: + """Download the item from the `source_path` to the `destination_path`""" + + namespace = get_default_namespace_or(namespace) + + v1 = get_static_client() + + target_folder = destination_path / source_path.stem + os.makedirs(target_folder, exist_ok=True) + + command = ["tar", "cf", "-", "-C", str(source_path.parent), str(source_path.name)] + + resp = stream( + v1.connect_get_namespaced_pod_exec, + name=pod_name, + namespace=namespace, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + tar_file = target_folder.with_suffix(".tar") + with open(tar_file, "wb") as f: + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + f.write(resp.read_stdout().encode("utf-8")) + if resp.peek_stderr(): + print(resp.read_stderr()) + resp.close() + + with tarfile.open(tar_file, "r") as tar: + tar.extractall(path=destination_path) + + os.remove(tar_file) + + return destination_path + + +def read_file_from_container( + pod_name, + source_path: Path, + container_name: str = "", + namespace: Optional[str] = None, + quiet: bool = False, +) -> str: + """Download the file from the `source_path` to the `destination_path`""" + + namespace = get_default_namespace_or(namespace) + + v1 = get_static_client() + + command = ["cat", str(source_path)] + + resp = stream( + v1.connect_get_namespaced_pod_exec, + name=pod_name, + namespace=namespace, + container=container_name, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + result = "" + while resp.is_open(): + resp.update(timeout=5) + if resp.peek_stdout(): + result += resp.read_stdout() + if resp.peek_stderr(): + raise Exception(resp.read_stderr()) + resp.close() + return result + + +def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): + namespace = get_default_namespace() + file_data = read_file_from_container(pod_name, source_path, src_container, namespace) + if write_file_to_container( + dst_name, + dst_container, + dst_path, + file_data, + namespace=namespace, + quiet=True, + ): + print(f"Copied {source_path} to {dst_path}") + else: + print(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") diff --git a/src/warnet/ln.py b/src/warnet/ln.py new file mode 100644 index 000000000..e01df85f8 --- /dev/null +++ b/src/warnet/ln.py @@ -0,0 +1,82 @@ +import json +from typing import Optional + +import click + +from .k8s import ( + get_default_namespace_or, + get_pod, +) +from .process import run_command + + +@click.group(name="ln") +def ln(): + """Control running lightning nodes""" + + +@ln.command(context_settings={"ignore_unknown_options": True}) +@click.argument("pod", type=str) +@click.argument("method", type=str) +@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments +@click.option("--namespace", default=None, show_default=True) +def rpc(pod: str, method: str, params: str, namespace: Optional[str]): + """ + Call lightning cli rpc on + """ + print(_rpc(pod, method, params, namespace)) + + +def _rpc(pod_name: str, method: str, params: str = "", namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + pod = get_pod(pod_name, namespace) + chain = pod.metadata.labels["chain"] + ln_client = "lncli" + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + ln_client = "lightning-cli" + cmd = f"kubectl -n {namespace} exec {pod_name} -- {ln_client} --network {chain} {method} {' '.join(map(str, params))}" + return run_command(cmd) + + +@ln.command() +@click.argument("pod", type=str) +def pubkey( + pod: str, +): + """ + Get lightning node pub key from + """ + print(_pubkey(pod)) + + +def _pubkey(pod_name: str): + info = _rpc(pod_name, "getinfo") + pod = get_pod(pod_name) + pubkey_key = "identity_pubkey" + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + pubkey_key = "id" + return json.loads(info)[pubkey_key] + + +@ln.command() +@click.argument("pod", type=str) +def host( + pod: str, +): + """ + Get lightning node host from + """ + print(_host(pod)) + + +def _host(pod_name: str): + info = _rpc(pod_name, "getinfo") + pod = get_pod(pod_name) + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + return json.loads(info)["alias"] + else: + uris = json.loads(info)["uris"] + if uris and len(uris) >= 0: + return uris[0].split("@")[1] + else: + return "" diff --git a/src/warnet/lnchannel.py b/src/warnet/lnchannel.py deleted file mode 100644 index 2d17460fb..000000000 --- a/src/warnet/lnchannel.py +++ /dev/null @@ -1,148 +0,0 @@ -import logging - - -class LNPolicy: - def __init__( - self, - min_htlc: int, - max_htlc: int, - base_fee_msat: int, - fee_rate_milli_msat: int, - time_lock_delta: int = 0, - ) -> None: - self.min_htlc = min_htlc - self.max_htlc = max_htlc - self.base_fee_msat = base_fee_msat - self.fee_rate_milli_msat = fee_rate_milli_msat - self.time_lock_delta = time_lock_delta - - def __str__(self) -> str: - return ( - f"LNPolicy(min_htlc={self.min_htlc}, " - f"max_htlc={self.max_htlc}, " - f"base_fee={self.base_fee_msat}, " - f"fee_rate={self.fee_rate_milli_msat}, " - f"time_lock_delta={self.time_lock_delta})" - ) - - -class LNChannel: - def __init__( - self, - node1_pub: str, - node2_pub: str, - capacity_msat: int = 0, - short_chan_id: str = "", - node1_policy: LNPolicy = None, - node2_policy: LNPolicy = None, - ) -> None: - # Ensure that the node with the lower pubkey is node1 - if node1_pub > node2_pub: - node1_pub, node2_pub = node2_pub, node1_pub - node1_policy, node2_policy = node2_policy, node1_policy - self.node1_pub = node1_pub - self.node2_pub = node2_pub - self.capacity_msat = capacity_msat - self.short_chan_id = short_chan_id - self.node1_policy = node1_policy - self.node2_policy = node2_policy - self.logger = logging.getLogger("lnchan") - - def __str__(self) -> str: - return ( - f"LNChannel(short_chan_id={self.short_chan_id}, " - f"capacity_msat={self.capacity_msat}, " - f"node1_pub={self.node1_pub[:8]}..., " - f"node2_pub={self.node2_pub[:8]}..., " - f"node1_policy=({self.node1_policy.__str__()}), " - f"node2_policy=({self.node2_policy.__str__()}))" - ) - - # Only used to compare warnet channels imported from a mainnet source file - # because pubkeys are unpredictable and node 1/2 might be swapped - def flip(self) -> "LNChannel": - return LNChannel( - # Keep the old pubkeys so the constructor doesn't just flip it back - node1_pub=self.node1_pub, - node2_pub=self.node2_pub, - capacity_msat=self.capacity_msat, - short_chan_id=self.short_chan_id, - node1_policy=self.node2_policy, - node2_policy=self.node1_policy, - ) - - def policy_match(self, ch2: "LNChannel") -> bool: - assert isinstance(ch2, LNChannel) - - node1_policy_match = False - node2_policy_match = False - - if self.node1_policy is None and ch2.node1_policy is None: - node1_policy_match = True - - if self.node2_policy is None and ch2.node2_policy is None: - node2_policy_match = True - - def compare_attributes(attr1, attr2, min_value=0, attr_name=""): - if attr1 == 0 or attr2 == 0: - return True - result = max(int(attr1), min_value) == max(int(attr2), min_value) - if not result: - self.logger.debug(f"Mismatch in {attr_name}: {attr1} != {attr2}") - return result - - if self.node1_policy is not None and ch2.node1_policy is not None: - attributes_to_compare = [ - ( - self.node1_policy.time_lock_delta, - ch2.node1_policy.time_lock_delta, - 18, - "node1_time_lock_delta", - ), - (self.node1_policy.min_htlc, ch2.node1_policy.min_htlc, 1, "node1_min_htlc"), - ( - self.node1_policy.base_fee_msat, - ch2.node1_policy.base_fee_msat, - 0, - "node1_base_fee_msat", - ), - ( - self.node1_policy.fee_rate_milli_msat, - ch2.node1_policy.fee_rate_milli_msat, - 0, - "node1_fee_rate_milli_msat", - ), - ] - node1_policy_match = all(compare_attributes(*attrs) for attrs in attributes_to_compare) - - if self.node2_policy is not None and ch2.node2_policy is not None: - attributes_to_compare = [ - ( - self.node2_policy.time_lock_delta, - ch2.node2_policy.time_lock_delta, - 18, - "node2_time_lock_delta", - ), - (self.node2_policy.min_htlc, ch2.node2_policy.min_htlc, 1, "node2_min_htlc"), - ( - self.node2_policy.base_fee_msat, - ch2.node2_policy.base_fee_msat, - 0, - "node2_base_fee_msat", - ), - ( - self.node2_policy.fee_rate_milli_msat, - ch2.node2_policy.fee_rate_milli_msat, - 0, - "node2_fee_rate_milli_msat", - ), - ] - node2_policy_match = all(compare_attributes(*attrs) for attrs in attributes_to_compare) - - return node1_policy_match and node2_policy_match - - def channel_match(self, ch2: "LNChannel") -> bool: - if self.capacity_msat != ch2.capacity_msat: - self.logger.debug(f"Capacity mismatch: {self.capacity_msat} != {ch2.capacity_msat}") - return False - return self.policy_match(ch2) diff --git a/src/warnet/lnd.py b/src/warnet/lnd.py deleted file mode 100644 index 3282d8253..000000000 --- a/src/warnet/lnd.py +++ /dev/null @@ -1,191 +0,0 @@ -import io -import tarfile - -from warnet.backend.kubernetes_backend import KubernetesBackend -from warnet.services import ServiceType -from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json - -from .lnchannel import LNChannel, LNPolicy -from .lnnode import LNNode, lnd_to_cl_scid -from .status import RunningStatus - -LND_CONFIG_BASE = " ".join( - [ - "--noseedbackup", - "--norest", - "--debuglevel=debug", - "--accept-keysend", - "--bitcoin.active", - "--bitcoin.regtest", - "--bitcoin.node=bitcoind", - "--maxpendingchannels=64", - "--trickledelay=1", - ] -) - - -class LNDNode(LNNode): - def __init__(self, warnet, tank, backend: KubernetesBackend, options): - self.warnet = warnet - self.tank = tank - self.backend = backend - self.image = options["ln_image"] - self.cb = options["cb_image"] - self.ln_config = options["ln_config"] - self.ipv4 = generate_ipv4_addr(self.warnet.subnet) - self.rpc_port = 10009 - self.impl = "lnd" - - @property - def status(self) -> RunningStatus: - return super().status - - @property - def cb_status(self) -> RunningStatus: - return super().cb_status - - def get_conf(self, ln_container_name, tank_container_name) -> str: - conf = LND_CONFIG_BASE - conf += f" --bitcoind.rpcuser={self.tank.rpc_user}" - conf += f" --bitcoind.rpcpass={self.tank.rpc_password}" - conf += f" --bitcoind.rpchost={tank_container_name}:{self.tank.rpc_port}" - conf += f" --bitcoind.zmqpubrawblock=tcp://{tank_container_name}:{self.tank.zmqblockport}" - conf += f" --bitcoind.zmqpubrawtx=tcp://{tank_container_name}:{self.tank.zmqtxport}" - conf += f" --rpclisten=0.0.0.0:{self.rpc_port}" - conf += f" --alias={self.tank.index}" - conf += f" --externalhosts={ln_container_name}" - conf += f" --tlsextradomain={ln_container_name}" - conf += " " + self.ln_config - return conf - - @exponential_backoff(max_retries=20, max_delay=300) - @handle_json - def lncli(self, cmd) -> dict: - cli = "lncli" - cmd = f"{cli} --network=regtest {cmd}" - return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd) - - def getnewaddress(self): - return self.lncli("newaddress p2wkh")["address"] - - def get_pub_key(self): - res = self.lncli("getinfo") - return res["identity_pubkey"] - - def getURI(self): - res = self.lncli("getinfo") - if len(res["uris"]) < 1: - return None - return res["uris"][0] - - def get_wallet_balance(self) -> int: - res = self.lncli("walletbalance")["confirmed_balance"] - return res - - # returns the channel point in the form txid:output_index - def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: - tank = self.warnet.tanks[index] - [pubkey, host] = tank.lnnode.getURI().split("@") - txid = self.lncli(f"openchannel --node_key={pubkey} --connect={host} {channel_open_data}")[ - "funding_txid" - ] - # Why doesn't LND return the output index as well? - # Do they charge by the RPC call or something?! - pending = self.lncli("pendingchannels") - for chan in pending["pending_open_channels"]: - if txid in chan["channel"]["channel_point"]: - return chan["channel"]["channel_point"] - raise Exception(f"Opened channel with txid {txid} not found in pending channels") - - def update_channel_policy(self, chan_point: str, policy: str) -> str: - ret = self.lncli(f"updatechanpolicy --chan_point={chan_point} {policy}") - if len(ret["failed_updates"]) == 0: - return ret - else: - raise Exception(ret) - - def get_graph_nodes(self) -> list[str]: - return list(n["pub_key"] for n in self.lncli("describegraph")["nodes"]) - - def get_graph_channels(self) -> list[LNChannel]: - edges = self.lncli("describegraph")["edges"] - return [self.lnchannel_from_json(edge) for edge in edges] - - @staticmethod - def lnchannel_from_json(edge: object) -> LNChannel: - node1_policy = ( - LNPolicy( - min_htlc=int(edge["node1_policy"]["min_htlc"]), - max_htlc=int(edge["node1_policy"]["max_htlc_msat"]), - base_fee_msat=int(edge["node1_policy"]["fee_base_msat"]), - fee_rate_milli_msat=int(edge["node1_policy"]["fee_rate_milli_msat"]), - time_lock_delta=int(edge["node1_policy"]["time_lock_delta"]), - ) - if edge["node1_policy"] - else None - ) - - node2_policy = ( - LNPolicy( - min_htlc=int(edge["node2_policy"]["min_htlc"]), - max_htlc=int(edge["node2_policy"]["max_htlc_msat"]), - base_fee_msat=int(edge["node2_policy"]["fee_base_msat"]), - fee_rate_milli_msat=int(edge["node2_policy"]["fee_rate_milli_msat"]), - time_lock_delta=int(edge["node2_policy"]["time_lock_delta"]), - ) - if edge["node2_policy"] - else None - ) - - return LNChannel( - node1_pub=edge["node1_pub"], - node2_pub=edge["node2_pub"], - capacity_msat=(int(edge["capacity"]) * 1000), - short_chan_id=lnd_to_cl_scid(edge["channel_id"]), - node1_policy=node1_policy, - node2_policy=node2_policy, - ) - - def get_peers(self) -> list[str]: - return list(p["pub_key"] for p in self.lncli("listpeers")["peers"]) - - def connect_to_tank(self, index): - return super().connect_to_tank(index) - - def generate_cli_command(self, command: list[str]): - network = f"--network={self.tank.warnet.bitcoin_network}" - cmd = f"{network} {' '.join(command)}" - cmd = f"lncli {cmd}" - return cmd - - def export(self, config: object, tar_file): - # Retrieve the credentials - macaroon = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon", - ) - cert = self.backend.get_file(self.tank.index, ServiceType.LIGHTNING, "/root/.lnd/tls.cert") - name = f"ln-{self.tank.index}" - macaroon_filename = f"{name}_admin.macaroon" - cert_filename = f"{name}_tls.cert" - host = self.backend.get_lnnode_hostname(self.tank.index) - - # Add the files to the in-memory tar archive - tarinfo1 = tarfile.TarInfo(name=macaroon_filename) - tarinfo1.size = len(macaroon) - fileobj1 = io.BytesIO(macaroon) - tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1) - tarinfo2 = tarfile.TarInfo(name=cert_filename) - tarinfo2.size = len(cert) - fileobj2 = io.BytesIO(cert) - tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2) - - config["nodes"].append( - { - "id": name, - "address": f"https://{host}:{self.rpc_port}", - "macaroon": f"/simln/{macaroon_filename}", - "cert": f"/simln/{cert_filename}", - } - ) diff --git a/src/warnet/lnnode.py b/src/warnet/lnnode.py deleted file mode 100644 index deda5da20..000000000 --- a/src/warnet/lnnode.py +++ /dev/null @@ -1,99 +0,0 @@ -from abc import ABC, abstractmethod - -from warnet.backend.kubernetes_backend import KubernetesBackend -from warnet.services import ServiceType -from warnet.utils import exponential_backoff, handle_json - -from .status import RunningStatus - - -class LNNode(ABC): - @abstractmethod - def __init__(self, warnet, tank, backend: KubernetesBackend, options): - pass - - @property - def status(self) -> RunningStatus: - return self.warnet.container_interface.get_status(self.tank.index, ServiceType.LIGHTNING) - - @property - def cb_status(self) -> RunningStatus: - if not self.cb: - return None - return self.warnet.container_interface.get_status( - self.tank.index, ServiceType.CIRCUITBREAKER - ) - - @abstractmethod - def get_conf(self, ln_container_name, tank_container_name) -> str: - pass - - @exponential_backoff(max_retries=20, max_delay=300) - @handle_json - @abstractmethod - def lncli(self, cmd) -> dict: - pass - - @abstractmethod - def getnewaddress(self): - pass - - @abstractmethod - def get_pub_key(self): - pass - - @abstractmethod - def getURI(self): - pass - - @abstractmethod - def get_wallet_balance(self) -> int: - pass - - @abstractmethod - def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: - """Return the channel point in the form txid:output_index""" - pass - - @abstractmethod - def update_channel_policy(self, chan_point: str, policy: str) -> str: - pass - - @abstractmethod - def get_graph_nodes(self) -> list[str]: - pass - - @abstractmethod - def get_graph_channels(self) -> list[dict]: - pass - - @abstractmethod - def get_peers(self) -> list[str]: - pass - - def connect_to_tank(self, index): - tank = self.warnet.tanks[index] - uri = tank.lnnode.getURI() - res = self.lncli(f"connect {uri}") - return res - - @abstractmethod - def generate_cli_command(self, command: list[str]): - pass - - @abstractmethod - def export(self, config: object, tar_file): - pass - - -def lnd_to_cl_scid(id) -> str: - s = int(id, 10) - block = s >> 40 - tx = s >> 16 & 0xFFFFFF - output = s & 0xFFFF - return f"{block}x{tx}x{output}" - - -def cl_to_lnd_scid(s) -> int: - s = [int(i) for i in s.split("x")] - return (s[0] << 40) | (s[1] << 16) | s[2] diff --git a/src/warnet/logging_config.json b/src/warnet/logging_config.json index 9ab9cabca..3aec3ad34 100644 --- a/src/warnet/logging_config.json +++ b/src/warnet/logging_config.json @@ -11,17 +11,11 @@ "datefmt": "%Y-%m-%d %H:%M:%S" } }, - "filters": { - "no_errors": { - "()": "warnet.utils.NonErrorFilter" - } - }, "handlers": { "stdout": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "simple", - "filters": ["no_errors"], "stream": "ext://sys.stdout" }, "stderr": { diff --git a/src/warnet/main.py b/src/warnet/main.py new file mode 100644 index 000000000..768a82f96 --- /dev/null +++ b/src/warnet/main.py @@ -0,0 +1,86 @@ +import click + +from .admin import admin +from .bitcoin import bitcoin +from .control import down, logs, run, snapshot, stop +from .dashboard import dashboard +from .deploy import deploy +from .graph import create, graph, import_network +from .image import image +from .ln import ln +from .project import init, new, setup +from .status import status +from .users import auth + + +@click.group() +def cli(): + pass + + +@click.command() +def version() -> None: + """Display the installed version of warnet""" + try: + from warnet._version import __version__ + + # For PyPI releases, this will be the exact tag (e.g. "1.1.11") + # For dev installs, it will be something like "1.1.11.post1.dev17+g27af3a7.d20250309" + # Which is .post .dev +g .d + # is the number of local commits since the checkout commit + # is the number of commits since the last tag + raw_version = __version__ + + # Format the version string to our desired format + if "+" in raw_version: + version_part, git_date_part = raw_version.split("+", 1) + + # Get just the git commit hash + commit_hash = ( + git_date_part[1:].split(".", 1)[0] + if git_date_part.startswith("g") + else git_date_part.split(".", 1)[0] + ) + + # Remove .dev component (from "no-guess-dev" scheme) + clean_version = version_part + if ".dev" in clean_version: + clean_version = clean_version.split(".dev")[0] + + # Apply dirty status (from "no-guess-dev" scheme) + if ".post" in clean_version: + base = clean_version.split(".post")[0] + version_str = f"{base}-{commit_hash}-dirty" + else: + version_str = f"{clean_version}-{commit_hash}" + else: + version_str = raw_version + + click.echo(f"warnet version {version_str}") + except ImportError: + click.echo("warnet version unknown") + + +cli.add_command(admin) +cli.add_command(auth) +cli.add_command(bitcoin) +cli.add_command(deploy) +cli.add_command(down) +cli.add_command(dashboard) +cli.add_command(graph) +cli.add_command(import_network) +cli.add_command(image) +cli.add_command(init) +cli.add_command(logs) +cli.add_command(ln) +cli.add_command(new) +cli.add_command(run) +cli.add_command(setup) +cli.add_command(snapshot) +cli.add_command(status) +cli.add_command(stop) +cli.add_command(create) +cli.add_command(version) + +if __name__ == "__main__": + cli() diff --git a/src/warnet/namespaces.py b/src/warnet/namespaces.py new file mode 100644 index 000000000..12357525b --- /dev/null +++ b/src/warnet/namespaces.py @@ -0,0 +1,90 @@ +import shutil +from pathlib import Path + +import click + +from .constants import ( + DEFAULT_NAMESPACES, + DEFAULTS_NAMESPACE_FILE, + NAMESPACES_DIR, + NAMESPACES_FILE, + WARGAMES_NAMESPACE_PREFIX, +) +from .process import run_command, stream_command + + +def copy_namespaces_defaults(directory: Path): + """Create the project structure for a warnet project""" + (directory / NAMESPACES_DIR.name / DEFAULT_NAMESPACES).mkdir(parents=True, exist_ok=True) + target_namespaces_defaults = ( + directory / NAMESPACES_DIR.name / DEFAULT_NAMESPACES / DEFAULTS_NAMESPACE_FILE + ) + target_namespaces_example = ( + directory / NAMESPACES_DIR.name / DEFAULT_NAMESPACES / NAMESPACES_FILE + ) + shutil.copy2( + NAMESPACES_DIR / DEFAULT_NAMESPACES / DEFAULTS_NAMESPACE_FILE, target_namespaces_defaults + ) + shutil.copy2(NAMESPACES_DIR / DEFAULT_NAMESPACES / NAMESPACES_FILE, target_namespaces_example) + + +@click.group(name="namespaces") +def namespaces(): + """Namespaces commands""" + + +@namespaces.command() +def list(): + """List all namespaces with 'wargames-' prefix""" + cmd = "kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'" + res = run_command(cmd) + all_namespaces = res.split() + warnet_namespaces = [ + ns for ns in all_namespaces if ns.startswith(f"{WARGAMES_NAMESPACE_PREFIX}") + ] + + if warnet_namespaces: + print("Warnet namespaces:") + for ns in warnet_namespaces: + print(f"- {ns}") + else: + print("No warnet namespaces found.") + + +@namespaces.command() +@click.option("--all", "destroy_all", is_flag=True, help="Destroy all warnet- prefixed namespaces") +@click.argument("namespace", required=False) +def destroy(destroy_all: bool, namespace: str): + """Destroy a specific namespace or all 'wargames-' prefixed namespaces""" + if destroy_all: + cmd = "kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'" + res = run_command(cmd) + + # Get the list of namespaces + all_namespaces = res.split() + warnet_namespaces = [ + ns for ns in all_namespaces if ns.startswith(f"{WARGAMES_NAMESPACE_PREFIX}") + ] + + if not warnet_namespaces: + print("No warnet namespaces found to destroy.") + return + + for ns in warnet_namespaces: + destroy_cmd = f"kubectl delete namespace {ns}" + if not stream_command(destroy_cmd): + print(f"Failed to destroy namespace: {ns}") + else: + print(f"Destroyed namespace: {ns}") + elif namespace: + if not namespace.startswith(f"{WARGAMES_NAMESPACE_PREFIX}"): + print(f"Error: Can only destroy namespaces with '{WARGAMES_NAMESPACE_PREFIX}' prefix") + return + + destroy_cmd = f"kubectl delete namespace {namespace}" + if not stream_command(destroy_cmd): + print(f"Failed to destroy namespace: {namespace}") + else: + print(f"Destroyed namespace: {namespace}") + else: + print("Error: Please specify a namespace or use --all flag.") diff --git a/src/warnet/network.py b/src/warnet/network.py new file mode 100644 index 000000000..920269f00 --- /dev/null +++ b/src/warnet/network.py @@ -0,0 +1,92 @@ +import json +import shutil +from pathlib import Path + +from rich import print + +from .bitcoin import _rpc +from .constants import ( + NETWORK_DIR, + PLUGINS_DIR, + SCENARIOS_DIR, +) +from .k8s import get_mission + + +def copy_defaults(directory: Path, target_subdir: str, source_path: Path, exclude_list: list[str]): + """Generic function to copy default files and directories""" + target_dir = directory / target_subdir + target_dir.mkdir(parents=True, exist_ok=True) + print(f"Creating directory: {target_dir}") + + shutil.copytree( + src=source_path, + dst=target_dir, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(*exclude_list), + ) + + print(f"Finished copying files to {target_dir}") + + +def copy_network_defaults(directory: Path): + """Create the project structure for a warnet project's network""" + copy_defaults( + directory, + NETWORK_DIR.name, + NETWORK_DIR, + ["__pycache__", "__init__.py"], + ) + + +def copy_scenario_defaults(directory: Path): + """Create the project structure for a warnet project's scenarios""" + copy_defaults( + directory, + SCENARIOS_DIR.name, + SCENARIOS_DIR, + ["__pycache__", "test_scenarios"], + ) + + +def copy_plugins_defaults(directory: Path): + """Create the project structure for a warnet project's scenarios""" + copy_defaults( + directory, + PLUGINS_DIR.name, + PLUGINS_DIR, + ["__pycache__", "__init__"], + ) + + +def is_connection_manual(peer): + # newer nodes specify a "connection_type" + return bool(peer.get("connection_type") == "manual" or peer.get("addnode") is True) + + +def _connected(end="\n"): + tanks = get_mission("tank") + for tank in tanks: + # Get actual + try: + peerinfo = json.loads( + _rpc(tank.metadata.name, "getpeerinfo", "", namespace=tank.metadata.namespace) + ) + actual = 0 + for peer in peerinfo: + if is_connection_manual(peer): + actual += 1 + expected = int(tank.metadata.annotations["init_peers"]) + print( + f"Tank: {tank.metadata.name:<15} peers expected: {expected:<3} actual: {actual:<3}", + end=end, + ) + # Even if more edges are specified, bitcoind only allows + # 8 manual outbound connections + if min(8, expected) > actual: + print("\nNetwork not connected") + return False + except Exception: + return False + print("Network connected ") + return True diff --git a/src/warnet/process.py b/src/warnet/process.py new file mode 100644 index 000000000..626124b71 --- /dev/null +++ b/src/warnet/process.py @@ -0,0 +1,31 @@ +import subprocess + + +def run_command(command: str) -> str: + result = subprocess.run(command, shell=True, capture_output=True, text=True, executable="bash") + if result.returncode != 0: + raise Exception(result.stderr) + return result.stdout + + +def stream_command(command: str) -> bool: + process = subprocess.Popen( + ["bash", "-c", command], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + + message = "" + for line in iter(process.stdout.readline, ""): + message += line + print(line, end="") + + process.stdout.close() + return_code = process.wait() + + if return_code != 0: + raise Exception(message) + return True diff --git a/src/warnet/project.py b/src/warnet/project.py new file mode 100644 index 000000000..6907c2bad --- /dev/null +++ b/src/warnet/project.py @@ -0,0 +1,630 @@ +import hashlib +import os +import platform +import shutil +import subprocess +import sys +import tarfile +import tempfile +from dataclasses import dataclass +from enum import Enum, auto +from pathlib import Path +from typing import Callable, Optional + +import click +import inquirer +import requests + +from .constants import ( + HELM_BINARY_NAME, + HELM_BLESSED_NAME_AND_CHECKSUMS, + HELM_BLESSED_VERSION, + HELM_DOWNLOAD_URL_STUB, + KUBECTL_BINARY_NAME, + KUBECTL_BLESSED_NAME_AND_CHECKSUMS, + KUBECTL_BLESSED_VERSION, + KUBECTL_DOWNLOAD_URL_STUB, +) +from .graph import inquirer_create_network +from .network import copy_network_defaults, copy_plugins_defaults, copy_scenario_defaults + + +@click.command() +def setup(): + """Setup warnet""" + + class ToolStatus(Enum): + Satisfied = auto() + Unsatisfied = auto() + + @dataclass + class ToolInfo: + tool_name: str + is_installed_func: Callable[[], tuple[bool, str]] + install_instruction: str + install_url: str + + __slots__ = ["tool_name", "is_installed_func", "install_instruction", "install_url"] + + def is_minikube_installed() -> tuple[bool, str]: + try: + version_result = subprocess.run( + ["minikube", "version", "--short"], + capture_output=True, + text=True, + ) + location_result = subprocess.run( + ["which", "minikube"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return True, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_minikube_running() -> tuple[bool, str]: + try: + result = subprocess.run( + ["minikube", "status"], + capture_output=True, + text=True, + ) + if result.returncode == 0 and "Running" in result.stdout: + return True, "minikube is running" + else: + return False, "" + except FileNotFoundError: + # Minikube command not found + return False, "" + + def is_docker_running() -> tuple[bool, str]: + try: + result = subprocess.run( + ["docker", "info"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return True, "docker is running" + else: + return False, "" + except FileNotFoundError: + # Docker command not found + return False, "" + + def is_minikube_version_valid_on_darwin() -> tuple[bool, str]: + try: + version_result = subprocess.run( + ["minikube", "version", "--short"], + capture_output=True, + text=True, + ) + location_result = subprocess.run( + ["which", "minikube"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + version = version_result.stdout.strip().split()[-1] # Get the version number + return version not in [ + "v1.32.0", + "1.33.0", + ], f"{location_result.stdout.strip()} ({version})" + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_platform_darwin() -> bool: + return platform.system() == "Darwin" + + def is_docker_installed() -> tuple[bool, str]: + try: + version_result = subprocess.run(["docker", "--version"], capture_output=True, text=True) + location_result = subprocess.run( + ["which", "docker"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return True, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_docker_desktop_running() -> tuple[bool, str]: + try: + version_result = subprocess.run(["docker", "info"], capture_output=True, text=True) + location_result = subprocess.run( + ["which", "docker"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return "Docker Desktop" in version_result.stdout, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_docker_desktop_kube_running() -> tuple[bool, str]: + try: + cluster_info = subprocess.run( + ["kubectl", "cluster-info", "--request-timeout=1"], + capture_output=True, + text=True, + ) + if cluster_info.returncode == 0: + indented_output = cluster_info.stdout.strip().replace("\n", "\n\t") + return True, f"\n\t{indented_output}" + else: + return False, "" + except Exception: + print() + return False, "Please enable kubernetes in Docker Desktop" + + def is_kubectl_installed_and_offer_if_not() -> tuple[bool, str]: + try: + version_result = subprocess.run( + ["kubectl", "version", "--client"], + capture_output=True, + text=True, + ) + location_result = subprocess.run( + ["which", "kubectl"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return True, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError: + print() + kubectl_answer = inquirer.prompt( + [ + inquirer.Confirm( + "install_kubectl", + message=click.style( + "Would you like Warnet to install Kubectl into your virtual environment?", + fg="blue", + bold=True, + ), + default=True, + ), + ] + ) + if kubectl_answer is None: + msg = "Setup cancelled by user." + click.secho(msg, fg="yellow") + return False, msg + if kubectl_answer["install_kubectl"]: + click.secho(" Installing Kubectl...", fg="yellow", bold=True) + install_kubectl_rootlessly_to_venv() + return is_kubectl_installed_and_offer_if_not() + return False, "Please install Kubectl." + + def is_helm_installed_and_offer_if_not() -> tuple[bool, str]: + try: + version_result = subprocess.run(["helm", "version"], capture_output=True, text=True) + location_result = subprocess.run( + ["which", "helm"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return version_result.returncode == 0, location_result.stdout.strip() + else: + return False, "" + + except FileNotFoundError: + print() + helm_answer = inquirer.prompt( + [ + inquirer.Confirm( + "install_helm", + message=click.style( + "Would you like Warnet to install Helm into your virtual environment?", + fg="blue", + bold=True, + ), + default=True, + ), + ] + ) + if helm_answer is None: + msg = "Setup cancelled by user." + click.secho(msg, fg="yellow") + return False, msg + if helm_answer["install_helm"]: + click.secho(" Installing Helm...", fg="yellow", bold=True) + install_helm_rootlessly_to_venv() + return is_helm_installed_and_offer_if_not() + return False, "Please install Helm." + + def check_installation(tool_info: ToolInfo) -> ToolStatus: + has_good_version, location = tool_info.is_installed_func() + if not has_good_version: + instruction_label = click.style(" Instruction: ", fg="yellow", bold=True) + instruction_text = click.style(f"{tool_info.install_instruction}", fg="yellow") + url_label = click.style(" URL: ", fg="yellow", bold=True) + url_text = click.style(f"{tool_info.install_url}", fg="yellow") + + click.secho(f" 💥 {tool_info.tool_name} is not satisfied. {location}", fg="yellow") + click.echo(instruction_label + instruction_text) + click.echo(url_label + url_text) + return ToolStatus.Unsatisfied + else: + click.secho(f" ⭐️ {tool_info.tool_name} is satisfied: {location}", bold=False) + return ToolStatus.Satisfied + + docker_info = ToolInfo( + tool_name="Docker", + is_installed_func=is_docker_installed, + install_instruction="Install Docker from Docker's official site.", + install_url="https://docs.docker.com/engine/install/", + ) + docker_desktop_info = ToolInfo( + tool_name="Docker Desktop", + is_installed_func=is_docker_desktop_running, + install_instruction="Make sure Docker Desktop is installed and running.", + install_url="https://docs.docker.com/desktop/", + ) + docker_running_info = ToolInfo( + tool_name="Running Docker", + is_installed_func=is_docker_running, + install_instruction="Please make sure docker is running", + install_url="https://docs.docker.com/engine/install/", + ) + docker_desktop_kube_running = ToolInfo( + tool_name="Kubernetes Running in Docker Desktop", + is_installed_func=is_docker_desktop_kube_running, + install_instruction="Please enable the local kubernetes cluster in Docker Desktop", + install_url="https://docs.docker.com/desktop/kubernetes/", + ) + minikube_running_info = ToolInfo( + tool_name="Running Minikube", + is_installed_func=is_minikube_running, + install_instruction="Please make sure minikube is running", + install_url="https://minikube.sigs.k8s.io/docs/start/", + ) + kubectl_info = ToolInfo( + tool_name="Kubectl", + is_installed_func=is_kubectl_installed_and_offer_if_not, + install_instruction="Install kubectl.", + install_url="https://kubernetes.io/docs/tasks/tools/install-kubectl/", + ) + helm_info = ToolInfo( + tool_name="Helm", + is_installed_func=is_helm_installed_and_offer_if_not, + install_instruction="Install Helm from Helm's official site, or rootlessly install Helm using Warnet's downloader when prompted.", + install_url="https://helm.sh/docs/intro/install/", + ) + minikube_info = ToolInfo( + tool_name="Minikube", + is_installed_func=is_minikube_installed, + install_instruction="Install Minikube from the official Minikube site.", + install_url="https://minikube.sigs.k8s.io/docs/start/", + ) + minikube_version_info = ToolInfo( + tool_name="Minikube's version", + is_installed_func=is_minikube_version_valid_on_darwin, + install_instruction="Install the latest Minikube from the official Minikube site.", + install_url="https://minikube.sigs.k8s.io/docs/start/", + ) + + print(" ") + print(" ╭───────────────────────────╮ ") + print(" │ Welcome to Warnet Setup │ ") + print(" ╰───────────────────────────╯ ") + print(" ") + print(" Let's find out if your system has what it takes to run Warnet...") + print("") + + try: + questions = [ + inquirer.List( + "platform", + message=click.style("Which platform would you like to use?", fg="blue", bold=True), + choices=[ + "Minikube", + "Docker Desktop", + "No Backend (Interacting with remote cluster, see `warnet auth --help`)", + ], + ) + ] + answers = inquirer.prompt(questions) + + check_results: list[ToolStatus] = [] + if answers: + check_results.append(check_installation(kubectl_info)) + check_results.append(check_installation(helm_info)) + if answers["platform"] == "Docker Desktop": + check_results.append(check_installation(docker_info)) + check_results.append(check_installation(docker_desktop_info)) + check_results.append(check_installation(docker_running_info)) + check_results.append(check_installation(docker_desktop_kube_running)) + elif answers["platform"] == "Minikube": + check_results.append(check_installation(docker_info)) + check_results.append(check_installation(docker_running_info)) + check_results.append(check_installation(minikube_info)) + if is_platform_darwin(): + check_results.append(check_installation(minikube_version_info)) + check_results.append(check_installation(minikube_running_info)) + else: + click.secho("Please re-run setup.", fg="yellow") + sys.exit(1) + + if ToolStatus.Unsatisfied in check_results: + click.secho( + "Please fix the installation issues above and try setup again.", fg="yellow" + ) + sys.exit(1) + else: + click.secho(" ⭐️ Warnet prerequisites look good.\n") + + except Exception as e: + click.echo(f"{e}\n\n") + click.secho(f"An error occurred while running the quick start script:\n\n{e}\n\n", fg="red") + click.secho( + "Please report the above context to https://github.com/bitcoin-dev-project/warnet/issues", + fg="yellow", + ) + return False + + +def create_warnet_project(directory: Path, check_empty: bool = False): + """Common function to create a warnet project""" + if check_empty and any(directory.iterdir()): + click.secho(f"Warning: Directory {directory} is not empty", fg="yellow") + if not click.confirm("Do you want to continue?", default=True): + return + + try: + copy_network_defaults(directory) + copy_scenario_defaults(directory) + copy_plugins_defaults(directory) + click.echo(f"Created warnet project structure in {directory}") + except Exception as e: + click.secho(f"Error creating project: {e}", fg="red") + raise e + + +@click.command() +@click.argument( + "directory", type=click.Path(file_okay=False, dir_okay=True, resolve_path=True, path_type=Path) +) +def new(directory: Path): + """Create a new warnet project in the specified directory""" + new_internal(directory) + + +def new_internal(directory: Path, from_init=False): + if directory.exists() and not from_init: + click.secho(f"Error: Directory {directory} already exists", fg="red") + return + + click.secho("\nCreating project structure...", fg="yellow", bold=True) + project_path = Path(os.path.expanduser(directory)) + create_warnet_project(project_path) + + custom_network_path = "" + click.secho("\nGenerating custom network...", fg="yellow", bold=True) + custom_network_path = inquirer_create_network(directory) + + if custom_network_path: + click.echo( + f"\nEdit the network files found under {custom_network_path}/ before deployment if you want to customise the network." + ) + click.echo("\nWhen you're ready, run the following command to deploy this network:") + click.echo(f" warnet deploy {custom_network_path}") + + +@click.command() +def init(): + """Initialize a warnet project in the current directory""" + current_dir = Path.cwd() + new_internal(directory=current_dir, from_init=True) + + +def get_os_name_for_helm() -> Optional[str]: + """Return a short operating system name suitable for downloading a helm binary.""" + uname_sys = platform.system().lower() + if "linux" in uname_sys: + return "linux" + elif uname_sys == "darwin": + return "darwin" + elif "win" in uname_sys: + return "windows" + return None + + +def is_in_virtualenv() -> bool: + """Check if the user is in a virtual environment.""" + return hasattr(sys, "real_prefix") or ( + hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix + ) + + +def download_file(url, destination): + click.secho(f" Downloading {url}", fg="blue") + response = requests.get(url, stream=True) + if response.status_code == 200: + with open(destination, "wb") as f: + for chunk in response.iter_content(1024): + f.write(chunk) + else: + raise Exception(f"Failed to download {url} (status code {response.status_code})") + + +def query_arch_from_uname(arch: str) -> Optional[str]: + if arch.startswith("armv5"): + return "armv5" + elif arch.startswith("armv6"): + return "armv6" + elif arch.startswith("armv7"): + return "arm" + elif arch == "aarch64" or arch == "arm64": + return "arm64" + elif arch == "x86": + return "386" + elif arch == "x86_64": + return "amd64" + elif arch == "i686" or arch == "i386": + return "386" + else: + return None + + +def write_blessed_kubectl_checksum(system: str, arch: str, dest_path: str): + checksum = next( + ( + b["checksum"] + for b in KUBECTL_BLESSED_NAME_AND_CHECKSUMS + if b["system"] == system and b["arch"] == arch + ), + None, + ) + if checksum: + with open(dest_path, "w") as f: + f.write(checksum) + else: + click.secho("Could not find a matching kubectl binary and checksum", fg="red") + + +def write_blessed_helm_checksum(helm_filename: str, dest_path: str): + checksum = next( + (b["checksum"] for b in HELM_BLESSED_NAME_AND_CHECKSUMS if b["name"] == helm_filename), None + ) + if checksum: + with open(dest_path, "w") as f: + f.write(checksum) + else: + click.secho("Could not find a matching helm binary and checksum", fg="red") + + +def verify_checksum(file_path, checksum_path): + click.secho(" Verifying checksum...", fg="blue") + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + + with open(checksum_path) as f: + expected_checksum = f.read().strip() + + if sha256_hash.hexdigest() != expected_checksum: + raise Exception("Checksum verification failed!") + click.secho(" Checksum verified.", fg="blue") + + +def install_to_venv(bin_path, binary_name): + venv_bin_dir = os.path.join(sys.prefix, "bin") + dst_path = os.path.join(venv_bin_dir, binary_name) + shutil.move(bin_path, dst_path) + os.chmod(dst_path, 0o755) + click.secho(f" {binary_name} installed into {dst_path}", fg="blue") + + +def install_helm_rootlessly_to_venv(): + if not is_in_virtualenv(): + click.secho( + "Error: You are not in a virtual environment. Please activate a virtual environment and try again.", + fg="yellow", + ) + sys.exit(1) + + version = HELM_BLESSED_VERSION + + os_name = get_os_name_for_helm() + if os_name is None: + click.secho( + "Error: Could not determine the operating system of this computer.", fg="yellow" + ) + sys.exit(1) + + uname_arch = os.uname().machine + arch = query_arch_from_uname(uname_arch) + if not arch: + click.secho(f"No Helm binary candidate for arch: {uname_arch}", fg="red") + sys.exit(1) + + helm_filename = f"{HELM_BINARY_NAME}-{version}-{os_name}-{arch}.tar.gz" + helm_url = f"{HELM_DOWNLOAD_URL_STUB}{helm_filename}" + + try: + with tempfile.TemporaryDirectory() as temp_dir: + helm_archive_path = os.path.join(temp_dir, helm_filename) + checksum_path = os.path.join(temp_dir, f"{helm_filename}.sha256") + + download_file(helm_url, helm_archive_path) + write_blessed_helm_checksum(helm_filename, checksum_path) + verify_checksum(helm_archive_path, checksum_path) + + # Extract Helm and install it in the virtual environment's bin folder + with tarfile.open(helm_archive_path, "r:gz") as tar: + tar.extractall(path=temp_dir) + helm_bin_path = os.path.join(temp_dir, os_name + "-" + arch, HELM_BINARY_NAME) + install_to_venv(helm_bin_path, HELM_BINARY_NAME) + + click.secho( + f" {HELM_BINARY_NAME} {version} installed successfully to your virtual environment!\n", + fg="blue", + ) + + except Exception as e: + click.secho(f"Error: {e}\nCould not install helm.", fg="yellow") + sys.exit(1) + + +def install_kubectl_rootlessly_to_venv(): + if not is_in_virtualenv(): + click.secho( + "Error: You are not in a virtual environment. Please activate a virtual environment and try again.", + fg="yellow", + ) + sys.exit(1) + + os_name = get_os_name_for_helm() + if os_name is None: + click.secho( + "Error: Could not determine the operating system of this computer.", fg="yellow" + ) + sys.exit(1) + + uname_arch = os.uname().machine + arch = query_arch_from_uname(uname_arch) + if arch not in ["arm64", "amd64"]: + click.secho(f"No Kubectl binary candidate for arch: {uname_arch}", fg="red") + sys.exit(1) + + uname_sys = os.uname().sysname.lower() + if uname_sys not in ["linux", "darwin"]: + click.secho(f"The following system is not supported: {uname_sys}", fg="red") + sys.exit(1) + + kubectl_url = f"{KUBECTL_DOWNLOAD_URL_STUB}/{uname_sys}/{arch}/{KUBECTL_BINARY_NAME}" + + try: + with tempfile.TemporaryDirectory() as temp_dir: + binary_path = os.path.join(temp_dir, KUBECTL_BINARY_NAME) + checksum_path = os.path.join(temp_dir, f"{KUBECTL_BINARY_NAME}.sha256") + + download_file(kubectl_url, binary_path) + write_blessed_kubectl_checksum(uname_sys, arch, checksum_path) + verify_checksum(binary_path, checksum_path) + + install_to_venv(binary_path, KUBECTL_BINARY_NAME) + + click.secho( + f" {KUBECTL_BINARY_NAME} {KUBECTL_BLESSED_VERSION} installed successfully to your virtual environment!\n", + fg="blue", + ) + + except Exception as e: + click.secho(f"Error: {e}\nCould not install helm.", fg="yellow") + sys.exit(1) diff --git a/src/warnet/scenarios/ln_init.py b/src/warnet/scenarios/ln_init.py deleted file mode 100644 index 98d8179df..000000000 --- a/src/warnet/scenarios/ln_init.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 - -from time import sleep - -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Fund LN wallets and open channels" - - -class LNInit(WarnetTestFramework): - def set_test_params(self): - self.num_nodes = None - - def run_test(self): - self.log.info("Lock out of IBD") - miner = ensure_miner(self.nodes[0]) - miner_addr = miner.getnewaddress() - self.generatetoaddress(self.nodes[0], 1, miner_addr) - - self.log.info("Get LN nodes and wallet addresses") - ln_nodes = [] - recv_addrs = [] - for tank in self.warnet.tanks: - if tank.lnnode is not None: - recv_addrs.append(tank.lnnode.getnewaddress()) - ln_nodes.append(tank.index) - - self.log.info("Fund LN wallets") - miner = ensure_miner(self.nodes[0]) - miner_addr = miner.getnewaddress() - # 298 block base - self.generatetoaddress(self.nodes[0], 297, miner_addr) - # divvy up the goods - split = (miner.getbalance() - 1) // len(recv_addrs) - sends = {} - for addr in recv_addrs: - sends[addr] = split - miner.sendmany("", sends) - # confirm funds in block 299 - self.generatetoaddress(self.nodes[0], 1, miner_addr) - - self.log.info( - f"Waiting for funds to be spendable: {split} BTC each for {len(recv_addrs)} LN nodes" - ) - - def funded_lnnodes(): - for tank in self.warnet.tanks: - if tank.lnnode is None: - continue - if int(tank.lnnode.get_wallet_balance()) < (split * 100000000): - return False - return True - - self.wait_until(funded_lnnodes, timeout=5 * 60) - - ln_nodes_uri = ln_nodes.copy() - while len(ln_nodes_uri) > 0: - self.log.info( - f"Waiting for all LN nodes to have URI, LN nodes remaining: {ln_nodes_uri}" - ) - for index in ln_nodes_uri: - lnnode = self.warnet.tanks[index].lnnode - if lnnode.getURI(): - ln_nodes_uri.remove(index) - sleep(5) - - self.log.info("Adding p2p connections to LN nodes") - for edge in self.warnet.graph.edges(data=True): - (src, dst, data) = edge - # Copy the L1 p2p topology (where applicable) to L2 - # so we get a more robust p2p graph for lightning - if ( - "channel_open" not in data - and self.warnet.tanks[src].lnnode - and self.warnet.tanks[dst].lnnode - ): - self.warnet.tanks[src].lnnode.connect_to_tank(dst) - - # Start confirming channel opens in block 300 - self.log.info("Opening channels, one per block") - chan_opens = [] - edges = self.warnet.graph.edges(data=True, keys=True) - edges = sorted(edges, key=lambda edge: edge[2]) - for edge in edges: - (src, dst, key, data) = edge - if "channel_open" in data: - src_node = self.warnet.get_ln_node_from_tank(src) - assert src_node is not None - assert self.warnet.get_ln_node_from_tank(dst) is not None - self.log.info(f"opening channel {src}->{dst}") - chan_pt = src_node.open_channel_to_tank(dst, data["channel_open"]) - # We can guarantee deterministic short channel IDs as long as - # the change output is greater than the channel funding output, - # which will then be output 0 - assert chan_pt[64:] == ":0" - chan_opens.append((edge, chan_pt)) - self.log.info(f" pending channel point: {chan_pt}") - self.wait_until( - lambda chan_pt=chan_pt: chan_pt[:64] in self.nodes[0].getrawmempool() - ) - self.generatetoaddress(self.nodes[0], 1, miner_addr) - assert chan_pt[:64] not in self.nodes[0].getrawmempool() - height = self.nodes[0].getblockcount() - self.log.info(f" confirmed in block {height}") - self.log.info( - f" channel_id should be: {int.from_bytes(height.to_bytes(3, 'big') + (1).to_bytes(3, 'big') + (0).to_bytes(2, 'big'), 'big')}" - ) - - # Ensure all channel opens are sufficiently confirmed - self.generatetoaddress(self.nodes[0], 10, miner_addr) - ln_nodes_gossip = ln_nodes.copy() - while len(ln_nodes_gossip) > 0: - self.log.info(f"Waiting for graph gossip sync, LN nodes remaining: {ln_nodes_gossip}") - for index in ln_nodes_gossip: - lnnode = self.warnet.tanks[index].lnnode - count_channels = len(lnnode.get_graph_channels()) - count_graph_nodes = len(lnnode.get_graph_nodes()) - if count_channels == len(chan_opens) and count_graph_nodes == len(ln_nodes): - ln_nodes_gossip.remove(index) - else: - self.log.info( - f" node {index} not synced (channels: {count_channels}/{len(chan_opens)}, nodes: {count_graph_nodes}/{len(ln_nodes)})" - ) - sleep(5) - - self.log.info("Updating channel policies") - for edge, chan_pt in chan_opens: - (src, dst, key, data) = edge - if "target_policy" in data: - target_node = self.warnet.get_ln_node_from_tank(dst) - target_node.update_channel_policy(chan_pt, data["target_policy"]) - if "source_policy" in data: - source_node = self.warnet.get_ln_node_from_tank(src) - source_node.update_channel_policy(chan_pt, data["source_policy"]) - - while True: - self.log.info("Waiting for all channel policies to match") - score = 0 - for tank_index, me in enumerate(ln_nodes): - you = (tank_index + 1) % len(ln_nodes) - my_channels = self.warnet.tanks[me].lnnode.get_graph_channels() - your_channels = self.warnet.tanks[you].lnnode.get_graph_channels() - match = True - for _chan_index, my_chan in enumerate(my_channels): - your_chan = [ - chan - for chan in your_channels - if chan.short_chan_id == my_chan.short_chan_id - ][0] - if not your_chan: - print(f"Channel policy missing for channel: {my_chan.short_chan_id}") - match = False - break - - try: - if not my_chan.channel_match(your_chan): - print( - f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}" - ) - match = False - break - except Exception as e: - print(f"Error comparing channel policies: {e}") - print( - f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}" - ) - match = False - break - if match: - print(f"All channel policies match between tanks {me} & {you}") - score += 1 - print(f"Score: {score} / {len(ln_nodes)}") - if score == len(ln_nodes): - break - sleep(5) - - self.log.info( - f"Warnet LN ready with {len(recv_addrs)} nodes and {len(chan_opens)} channels." - ) - - -if __name__ == "__main__": - LNInit().main() diff --git a/src/warnet/scenarios/sens_relay.py b/src/warnet/scenarios/sens_relay.py deleted file mode 100644 index 41cddf929..000000000 --- a/src/warnet/scenarios/sens_relay.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 - -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Send a transaction using sensitive relay" - - -class MinerStd(WarnetTestFramework): - def set_test_params(self): - self.num_nodes = 12 - - def run_test(self): - # PR branch node - test_node = self.nodes[11] - test_wallet = ensure_miner(test_node) - addr = test_wallet.getnewaddress() - - self.log.info("generating 110 blocks...") - self.generatetoaddress(test_node, 110, addr) - - self.log.info("adding onion addresses from all peers...") - for i in range(11): - info = self.nodes[i].getnetworkinfo() - for addr in info["localaddresses"]: - if "onion" in addr["address"]: - self.log.info(f"adding {addr['address']}:{addr['port']}") - test_node.addpeeraddress(addr["address"], addr["port"]) - - self.log.info("getting address from recipient...") - # some other node - recip = self.nodes[5] - recip_wallet = ensure_miner(recip) - recip_addr = recip_wallet.getnewaddress() - - self.log.info("sending transaction...") - self.log.info(test_wallet.sendtoaddress(recip_addr, 0.5)) - - -if __name__ == "__main__": - MinerStd().main() diff --git a/src/warnet/scenarios/utils.py b/src/warnet/scenarios/utils.py deleted file mode 100644 index b0204d461..000000000 --- a/src/warnet/scenarios/utils.py +++ /dev/null @@ -1,5 +0,0 @@ -def ensure_miner(node): - wallets = node.listwallets() - if "miner" not in wallets: - node.createwallet("miner", descriptors=True) - return node.get_wallet_rpc("miner") diff --git a/src/warnet/server.py b/src/warnet/server.py deleted file mode 100644 index 55a7371db..000000000 --- a/src/warnet/server.py +++ /dev/null @@ -1,611 +0,0 @@ -import argparse -import base64 -import importlib -import io -import json -import logging -import logging.config -import os -import pkgutil -import platform -import shutil -import subprocess -import sys -import tarfile -import tempfile -import threading -import time -import traceback -from datetime import datetime - -import warnet.scenarios as scenarios -from flask import Flask, jsonify, request -from flask_jsonrpc.app import JSONRPC -from flask_jsonrpc.exceptions import ServerError -from warnet import SRC_DIR - -from .services import ServiceType -from .utils import gen_config_dir -from .warnet import Warnet - -WARNET_SERVER_PORT = 9276 -CONFIG_DIR_ALREADY_EXISTS = 32001 - - -class Server: - def __init__(self): - system = os.name - if system == "nt" or platform.system() == "Windows": - self.basedir = os.path.join(os.path.expanduser("~"), "warnet") - elif system == "posix" or platform.system() == "Linux" or platform.system() == "Darwin": - self.basedir = os.environ.get("XDG_STATE_HOME") - if self.basedir is None: - # ~/.warnet/warnet.log - self.basedir = os.path.join(os.environ["HOME"], ".warnet") - else: - # XDG_STATE_HOME / warnet / warnet.log - self.basedir = os.path.join(self.basedir, "warnet") - else: - raise NotImplementedError("Unsupported operating system") - - self.running_scenarios = [] - - self.app = Flask(__name__) - self.jsonrpc = JSONRPC(self.app, "/api") - - self.log_file_path = os.path.join(self.basedir, "warnet.log") - self.setup_global_exception_handler() - self.setup_logging() - self.setup_rpc() - self.warnets: dict = dict() - self.logger.info("Started server") - - # register a well known /-/healthy endpoint for liveness tests - # we regard warnet as healthy if the http server is up - # /-/healthy and /-/ready are often used (e.g. by the prometheus server) - self.app.add_url_rule("/-/healthy", view_func=self.healthy) - - # This is set while we bring a warnet up, which may include building a new image - # After warnet is up this will be released. - # This is used to delay api calls which rely on and image being built dynamically - # before the config dir is populated with the deployment info - self.image_build_lock = threading.Lock() - - def setup_global_exception_handler(self): - """ - Use flask to log traceback of unhandled exceptions - """ - - @self.app.errorhandler(Exception) - def handle_exception(e): - trace = traceback.format_exc() - self.logger.error(f"Unhandled exception: {e}\n{trace}") - response = { - "jsonrpc": "2.0", - "error": { - "code": -32603, - "message": "Internal server error", - "data": str(e), - }, - "id": request.json.get("id", None) if request.json else None, - } - return jsonify(response), 500 - - def healthy(self): - return "warnet is healthy" - - def setup_logging(self): - os.makedirs(os.path.dirname(self.log_file_path), exist_ok=True) - with open(SRC_DIR / "logging_config.json") as f: - logging_config = json.load(f) - logging_config["handlers"]["file"]["filename"] = str(self.log_file_path) - logging.config.dictConfig(logging_config) - self.logger = logging.getLogger("server") - self.scenario_logger = logging.getLogger("scenario") - self.logger.info("Logging started") - - def log_request(): - if "healthy" in request.path: - return # No need to log all these - if not request.path.startswith("/api"): - self.logger.debug(request.path) - else: - self.logger.debug(request.json) - - def build_check(): - timeout = 600 - check_interval = 10 - time_elapsed = 0 - - while time_elapsed < timeout: - # Attempt to acquire the lock without blocking - lock_acquired = self.image_build_lock.acquire(blocking=False) - # If we get the lock, release it and continue - if lock_acquired: - self.image_build_lock.release() - return - # Otherwise wait before trying again - else: - time.sleep(check_interval) - time_elapsed += check_interval - - # If we've reached here, the lock wasn't acquired in time - raise Exception( - f"Failed to acquire the build lock within {timeout} seconds, aborting RPC." - ) - - self.app.before_request(log_request) - self.app.before_request(build_check) - - def setup_rpc(self): - # Tanks - self.jsonrpc.register(self.tank_bcli) - self.jsonrpc.register(self.tank_lncli) - self.jsonrpc.register(self.tank_debug_log) - self.jsonrpc.register(self.tank_messages) - self.jsonrpc.register(self.tank_ln_pub_key) - # Scenarios - self.jsonrpc.register(self.scenarios_available) - self.jsonrpc.register(self.scenarios_run) - self.jsonrpc.register(self.scenarios_run_file) - self.jsonrpc.register(self.scenarios_stop) - self.jsonrpc.register(self.scenarios_list_running) - # Networks - self.jsonrpc.register(self.network_up) - self.jsonrpc.register(self.network_from_file) - self.jsonrpc.register(self.network_down) - self.jsonrpc.register(self.network_info) - self.jsonrpc.register(self.network_status) - self.jsonrpc.register(self.network_connected) - self.jsonrpc.register(self.network_export) - # Debug - self.jsonrpc.register(self.generate_deployment) - self.jsonrpc.register(self.exec_run) - # Logs - self.jsonrpc.register(self.logs_grep) - - def scenario_log(self, proc): - while not proc.stdout and not proc.stderr: - time.sleep(0.1) - for line in proc.stdout: - self.scenario_logger.info(line.decode().rstrip()) - for line in proc.stderr: - self.scenario_logger.error(line.decode().rstrip()) - - def get_warnet(self, network: str) -> Warnet: - """ - Will get a warnet from the cache if it exists. - Otherwise it will create the network using from_network() and save it - to the cache before returning it. - """ - if network in self.warnets: - return self.warnets[network] - wn = Warnet.from_network(network) - if isinstance(wn, Warnet): - self.warnets[network] = wn - return wn - raise ServerError(f"Could not find warnet {network}") - - def tank_bcli( - self, node: int, method: str, params: list[str] | None = None, network: str = "warnet" - ) -> str: - """ - Call bitcoin-cli on in [network] - """ - wn = self.get_warnet(network) - try: - return wn.container_interface.get_bitcoin_cli(wn.tanks[node], method, params) - except Exception as e: - msg = f"Sever error calling bitcoin-cli {method}: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_lncli(self, node: int, command: list[str], network: str = "warnet") -> str: - """ - Call lightning cli on in [network] - """ - wn = self.get_warnet(network) - try: - return wn.container_interface.ln_cli(wn.tanks[node], command) - except Exception as e: - msg = f"Error calling lncli: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_ln_pub_key(self, node: int, network: str = "warnet") -> str: - """ - Get lightning pub key on in [network] - """ - wn = self.get_warnet(network) - try: - return wn.container_interface.ln_pub_key(wn.tanks[node]) - except Exception as e: - msg = f"Error getting pub key: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_debug_log(self, network: str, node: int) -> str: - """ - Fetch the Bitcoin Core debug log from - """ - wn = Warnet.from_network(network) - try: - return wn.container_interface.get_bitcoin_debug_log(wn.tanks[node].index) - except Exception as e: - msg = f"Error fetching debug logs: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_messages(self, network: str, node_a: int, node_b: int) -> str: - """ - Fetch messages sent between and . - """ - wn = self.get_warnet(network) - try: - messages = [ - msg - for msg in wn.container_interface.get_messages( - wn.tanks[node_a].index, wn.tanks[node_b].index, wn.bitcoin_network - ) - if msg is not None - ] - if not messages: - msg = f"No messages found between {node_a} and {node_b}" - self.logger.error(msg) - raise ServerError(message=msg) - - messages_str_list = [] - - for message in messages: - # Check if 'time' key exists and its value is a number - if not (message.get("time") and isinstance(message["time"], int | float)): - continue - - timestamp = datetime.utcfromtimestamp(message["time"] / 1e6).strftime( - "%Y-%m-%d %H:%M:%S" - ) - direction = ">>>" if message.get("outbound", False) else "<<<" - msgtype = message.get("msgtype", "") - body_dict = message.get("body", {}) - - if not isinstance(body_dict, dict): # messages will be in dict form - continue - - body_str = ", ".join(f"{key}: {value}" for key, value in body_dict.items()) - messages_str_list.append(f"{timestamp} {direction} {msgtype} {body_str}") - - result_str = "\n".join(messages_str_list) - - return result_str - - except Exception as e: - msg = f"Error fetching messages between nodes {node_a} and {node_b}: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_export(self, network: str, activity: str | None, exclude: list[int]) -> bool: - """ - Export all data for a simln container running on the network - """ - wn = self.get_warnet(network) - if "simln" not in wn.services: - raise Exception("No simln service in network") - - # JSON object that will eventually be written to simln config file - config = {"nodes": []} - if activity: - config["activity"] = json.loads(activity) - # In-memory file to build tar archive - tar_buffer = io.BytesIO() - with tarfile.open(fileobj=tar_buffer, mode="w") as tar_file: - # tank LN nodes add their credentials to tar archive - wn.export(config, tar_file, exclude=exclude) - # write config file - config_bytes = json.dumps(config).encode("utf-8") - config_stream = io.BytesIO(config_bytes) - tarinfo = tarfile.TarInfo(name="sim.json") - tarinfo.size = len(config_bytes) - tar_file.addfile(tarinfo=tarinfo, fileobj=config_stream) - - # Write the archive to the RPC server's config directory - source_file = wn.config_dir / "simln.tar" - with open(source_file, "wb") as output: - tar_buffer.seek(0) - output.write(tar_buffer.read()) - - # Copy the archive to the "emptydir" volume in the simln pod - wn.container_interface.write_service_config(source_file, "simln", "/simln/") - return True - - def scenarios_available(self) -> list[tuple]: - """ - List available scenarios in the Warnet Test Framework - """ - try: - scenario_list = [] - for s in pkgutil.iter_modules(scenarios.__path__): - module_name = f"warnet.scenarios.{s.name}" - try: - m = importlib.import_module(module_name) - if hasattr(m, "cli_help"): - scenario_list.append((s.name, m.cli_help())) - except ModuleNotFoundError as e: - print(f"Module not found: {module_name}, error: {e}") - raise - return scenario_list - except Exception as e: - msg = f"Error listing scenarios: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def _start_scenario( - self, - scenario_path: str, - scenario_name: str, - additional_args: list[str], - network: str, - ) -> str: - try: - run_cmd = [sys.executable, scenario_path] + additional_args + [f"--network={network}"] - self.logger.debug(f"Running {run_cmd}") - proc = subprocess.Popen( - run_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - t = threading.Thread(target=lambda: self.scenario_log(proc)) - t.daemon = True - t.start() - cmd = f"{scenario_name} {' '.join(additional_args)}".strip() - self.running_scenarios.append( - { - "pid": proc.pid, - "cmd": cmd, - "proc": proc, - "network": network, - } - ) - return f"Running scenario {scenario_name} with PID {proc.pid} in the background..." - except Exception as e: - msg = f"Error running scenario: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def scenarios_run_file( - self, - scenario_base64: str, - scenario_name: str, - additional_args: list[str], - network: str = "warnet", - ) -> str: - # Extract just the filename without path and extension - with tempfile.NamedTemporaryFile( - prefix=scenario_name, - suffix=".py", - delete=False, - ) as temp_file: - scenario_path = temp_file.name - temp_file.write(base64.b64decode(scenario_base64)) - - if not os.path.exists(scenario_path): - raise ServerError(f"Scenario not found at {scenario_path}.") - - return self._start_scenario(scenario_path, scenario_name, additional_args, network) - - def scenarios_run( - self, scenario: str, additional_args: list[str], network: str = "warnet" - ) -> str: - # Use importlib.resources to get the scenario path - scenario_package = "warnet.scenarios" - scenario_filename = f"{scenario}.py" - - # Ensure the scenario file exists within the package - with importlib.resources.path(scenario_package, scenario_filename) as scenario_path: - scenario_path = str(scenario_path) # Convert Path object to string - - if not os.path.exists(scenario_path): - raise ServerError(f"Scenario {scenario} not found at {scenario_path}.") - - return self._start_scenario(scenario_path, scenario, additional_args, network) - - def scenarios_stop(self, pid: int) -> str: - matching_scenarios = [sc for sc in self.running_scenarios if sc["pid"] == pid] - if matching_scenarios: - matching_scenarios[0]["proc"].terminate() # sends SIGTERM - # Remove from running list - self.running_scenarios = [sc for sc in self.running_scenarios if sc["pid"] != pid] - return f"Stopped scenario with PID {pid}." - else: - msg = f"Could not find scenario with PID {pid}" - self.logger.error(msg) - raise ServerError(message=msg) - - def scenarios_list_running(self) -> list[dict]: - running = [ - { - "pid": sc["pid"], - "cmd": sc["cmd"], - "active": sc["proc"].poll() is None, - "return_code": sc["proc"].returncode, - "network": sc["network"], - } - for sc in self.running_scenarios - ] - return running - - def network_up(self, network: str = "warnet") -> str: - def thread_start(server: Server, network): - try: - wn = server.get_warnet(network) - wn.apply_network_conditions() - wn.wait_for_health() - server.logger.info( - f"Successfully resumed warnet named '{network}' from config dir {wn.config_dir}" - ) - except Exception as e: - trace = traceback.format_exc() - server.logger.error(f"Unhandled exception bringing network up: {e}\n{trace}") - - try: - t = threading.Thread(target=lambda: thread_start(self, network)) - t.daemon = True - t.start() - return "Resuming warnet..." - except Exception as e: - msg = f"Error bring up warnet: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_from_file( - self, graph_file: str, force: bool = False, network: str = "warnet" - ) -> dict: - """ - Run a warnet with topology loaded from a - """ - - def thread_start(server: Server, network): - with server.image_build_lock: - try: - wn = server.get_warnet(network) - wn.generate_deployment() - wn.warnet_build() - wn.warnet_up() - wn.wait_for_health() - wn.apply_network_conditions() - self.logger.info("Warnet started successfully") - except Exception as e: - trace = traceback.format_exc() - self.logger.error(f"Unhandled exception starting warnet: {e}\n{trace}") - - config_dir = gen_config_dir(network) - if config_dir.exists(): - if force: - shutil.rmtree(config_dir) - else: - message = f"Config dir {config_dir} already exists, not overwriting existing warnet without --force" - self.logger.error(message) - raise ServerError(message=message, code=CONFIG_DIR_ALREADY_EXISTS) - - try: - self.warnets[network] = Warnet.from_graph_file( - graph_file, - config_dir, - network, - ) - t = threading.Thread(target=lambda: thread_start(self, network)) - t.daemon = True - t.start() - return self.warnets[network]._warnet_dict_representation() - except Exception as e: - msg = f"Error bring up warnet: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_down(self, network: str = "warnet") -> str: - """ - Stop all containers in . - """ - wn = self.get_warnet(network) - try: - wn.warnet_down() - return "Stopping warnet" - except Exception as e: - msg = f"Error bringing warnet down: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_info(self, network: str = "warnet") -> dict: - """ - Get info about a warnet network named - """ - wn = self.get_warnet(network) - return wn._warnet_dict_representation() - - def network_status(self, network: str = "warnet") -> list[dict]: - """ - Get running status of a warnet network named - """ - try: - wn = self.get_warnet(network) - stats = [] - for tank in wn.tanks: - status = {"tank_index": tank.index, "bitcoin_status": tank.status.name.lower()} - if tank.lnnode is not None: - status["lightning_status"] = tank.lnnode.status.name.lower() - if tank.lnnode.cb is not None: - status["circuitbreaker_status"] = tank.lnnode.cb_status.name.lower() - stats.append(status) - return stats - except Exception as e: - msg = f"Error getting network status: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_connected(self, network: str = "warnet") -> bool: - """ - Indicate whether all of the graph edges are connected in - """ - try: - wn = self.get_warnet(network) - return wn.network_connected() - except Exception as e: - self.logger.error(f"{e}") - return False - - def generate_deployment(self, graph_file: str, network: str = "warnet") -> str: - """ - Generate the deployment file for a graph file - """ - try: - config_dir = gen_config_dir(network) - if config_dir.exists(): - message = f"Config dir {config_dir} already exists, not overwriting existing warnet without --force" - self.logger.error(message) - raise ServerError(message=message, code=CONFIG_DIR_ALREADY_EXISTS) - wn = self.get_warnet(network) - wn.generate_deployment() - if not wn.deployment_file or not wn.deployment_file.is_file(): - raise ServerError(f"No deployment file found at {wn.deployment_file}") - with open(wn.deployment_file) as f: - return f.read() - except Exception as e: - msg = f"Error generating deployment file: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def logs_grep( - self, pattern: str, network: str = "warnet", k8s_timestamps=False, no_sort=False - ) -> str: - """ - Grep the logs from the fluentd container for a regex pattern - """ - try: - wn = self.get_warnet(network) - return wn.container_interface.logs_grep(pattern, network, k8s_timestamps, no_sort) - except Exception as e: - msg = f"Error grepping logs using pattern {pattern}: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def exec_run(self, index: int, service_type: int, cmd: str, network: str = "warnet") -> str: - """ - Execute an arbitrary command in an arbitrary container, - identified by tank index and ServiceType - """ - wn = self.get_warnet(network) - return wn.container_interface.exec_run(index, ServiceType(service_type), cmd) - - -def run_server(): - parser = argparse.ArgumentParser(description="Run the server") - parser.add_argument( - "--dev", action="store_true", help="Run in development mode with debug enabled" - ) - args = parser.parse_args() - debug_mode = args.dev - server = Server() - server.app.run(host="0.0.0.0", port=WARNET_SERVER_PORT, debug=debug_mode) - - -if __name__ == "__main__": - run_server() diff --git a/src/warnet/services.py b/src/warnet/services.py deleted file mode 100644 index 562813432..000000000 --- a/src/warnet/services.py +++ /dev/null @@ -1,36 +0,0 @@ -from enum import Enum - -FO_CONF_NAME = "fork_observer_config.toml" -AO_CONF_NAME = "addrman_observer_config.toml" -GRAFANA_PROVISIONING = "grafana-provisioning" -PROM_CONF_NAME = "prometheus.yml" - - -class ServiceType(Enum): - BITCOIN = 1 - LIGHTNING = 2 - CIRCUITBREAKER = 3 - - -SERVICES = { - # "forkobserver": { - # "image": "b10c/fork-observer:latest", - # "container_name_suffix": "fork-observer", - # "warnet_port": "23001", - # "container_port": "2323", - # "config_files": [f"{FO_CONF_NAME}:/app/config.toml"], - # }, - # "addrmanobserver": { - # "image": "b10c/addrman-observer:latest", - # "container_name_suffix": "addrman-observer", - # "warnet_port": "23005", - # "container_port": "3882", - # "config_files": [f"{AO_CONF_NAME}:/app/config.toml"], - # }, - "simln": { - "image": "bitcoindevproject/simln:0.2.0", - "container_name_suffix": "simln", - "environment": ["LOG_LEVEL=debug", "SIMFILE_PATH=/simln/sim.json"], - "config_files": ["simln/:/simln"], - }, -} diff --git a/src/warnet/status.py b/src/warnet/status.py index ac83d4140..b50ff8680 100644 --- a/src/warnet/status.py +++ b/src/warnet/status.py @@ -1,9 +1,128 @@ -from enum import Enum +import sys +import click +from kubernetes.config.config_exception import ConfigException +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from urllib3.exceptions import MaxRetryError -class RunningStatus(Enum): - PENDING = 1 - RUNNING = 2 - STOPPED = 3 - FAILED = 4 - UNKNOWN = 5 +from .constants import COMMANDER_MISSION, LIGHTNING_MISSION, TANK_MISSION +from .k8s import get_mission +from .network import _connected + + +@click.command() +def status(): + """Display the unified status of the Warnet network and active scenarios""" + console = Console() + + try: + tanks = _get_tank_status() + lns = _get_ln_status() + scenarios = _get_deployed_scenarios() + except ConfigException as e: + print(e) + print( + "The kubeconfig file has not been properly set. This may mean that you need to " + "authorize with a cluster such as by starting minikube, starting docker-desktop, or " + "authorizing with a configuration file provided by a cluster administrator." + ) + sys.exit(1) + except MaxRetryError as e: + print(e) + print( + "Warnet cannot get the status of a Warnet network. To resolve this, you may need to " + "confirm you have access to a Warnet cluster. Start by checking your network " + "connection. Then, if running a local cluster, check that minikube or docker-desktop " + "is running properly. If you are trying to connect to a remote cluster, check that " + "the relevant authorization file has been configured properly as instructed by a " + "cluster administrator." + ) + sys.exit(1) + + # Create a unified table + table = Table(title="Warnet Status", show_header=True, header_style="bold magenta") + table.add_column("Component", style="cyan") + table.add_column("Name", style="green") + table.add_column("Status", style="yellow") + table.add_column("Namespace", style="green") + + # Add tanks to the table + for tank in tanks: + table.add_row("Tank", tank["name"], tank["status"], tank["namespace"]) + + # Add a separator if there are both tanks and scenarios + if tanks and lns: + table.add_row("", "", "") + + for ln in lns: + table.add_row("Lightning", ln["name"], ln["status"], ln["namespace"]) + + table.add_row("", "", "") + + # Add scenarios to the table + active = 0 + if scenarios: + for scenario in scenarios: + table.add_row("Scenario", scenario["name"], scenario["status"], scenario["namespace"]) + if scenario["status"] == "running" or scenario["status"] == "pending": + active += 1 + else: + table.add_row("", "No active scenarios", "", style="red") + + # Create a panel to wrap the table + panel = Panel( + table, + title="Warnet Overview", + expand=False, + border_style="blue", + padding=(1, 1), + ) + + # Print the panel + console.print(panel) + + # Print summary + summary = Text() + summary.append(f"\nTotal Tanks: {len(tanks)}", style="bold cyan") + summary.append(f" | Active Scenarios: {active}", style="bold green") + console.print(summary) + _connected(end="\r") + + +def _get_tank_status(): + tanks = get_mission(TANK_MISSION) + return [ + { + "name": tank.metadata.name, + "status": tank.status.phase.lower(), + "namespace": tank.metadata.namespace, + } + for tank in tanks + ] + + +def _get_ln_status(): + tanks = get_mission(LIGHTNING_MISSION) + return [ + { + "name": tank.metadata.name, + "status": tank.status.phase.lower(), + "namespace": tank.metadata.namespace, + } + for tank in tanks + ] + + +def _get_deployed_scenarios(): + commanders = get_mission(COMMANDER_MISSION) + return [ + { + "name": c.metadata.name, + "status": c.status.phase.lower(), + "namespace": c.metadata.namespace, + } + for c in commanders + ] diff --git a/src/warnet/tank.py b/src/warnet/tank.py deleted file mode 100644 index ac04d3f70..000000000 --- a/src/warnet/tank.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Tanks are containerized bitcoind nodes -""" - -import logging - -from .services import ServiceType -from .status import RunningStatus -from .utils import ( - SUPPORTED_TAGS, - exponential_backoff, - generate_ipv4_addr, - sanitize_tc_netem_command, -) - -CONTAINER_PREFIX_PROMETHEUS = "prometheus_exporter" - -logger = logging.getLogger("tank") - -CONFIG_BASE = " ".join( - [ - "-regtest=1", - "-checkmempool=0", - "-acceptnonstdtxn=1", - "-debuglogfile=0", - "-logips=1", - "-logtimemicros=1", - "-capturemessages=1", - "-rpcallowip=0.0.0.0/0", - "-rpcbind=0.0.0.0", - "-fallbackfee=0.00001000", - "-listen=1", - ] -) - - -class Tank: - DEFAULT_BUILD_ARGS = "--disable-tests --with-incompatible-bdb --without-gui --disable-bench --disable-fuzz-binary --enable-suppress-external-warnings --enable-debug " - - def __init__(self, index: int, warnet): - from warnet.lnnode import LNNode - - self.index = index - self.warnet = warnet - self.network_name = warnet.network_name - self.bitcoin_network = warnet.bitcoin_network - self.version: str = "" - self.image: str = "" - self.bitcoin_config = "" - self.netem = None - self.exporter = False - self.metrics = None - self.collect_logs = False - self.build_args = "" - self.lnnode: LNNode | None = None - self.rpc_port = 18443 - self.rpc_user = "warnet_user" - self.rpc_password = "2themoon" - self.zmqblockport = 28332 - self.zmqtxport = 28333 - self._suffix = None - self._ipv4 = None - self._exporter_name = None - # index of integers imported from graph file - # indicating which tanks to initially connect to - self.init_peers = [] - - def _parse_version(self, version): - if not version: - return - if version not in SUPPORTED_TAGS and not ("/" in version and "#" in version): - raise Exception( - f"Unsupported version: can't be generated from Docker images: {self.version}" - ) - self.version = version - - def parse_graph_node(self, node): - # Dynamically parse properties based on the schema - graph_properties = {} - for property, specs in self.warnet.graph_schema["node"]["properties"].items(): - value = node.get(property, specs.get("default")) - if property == "version": - self._parse_version(value) - setattr(self, property, value) - graph_properties[property] = value - - if self.version and self.image: - raise Exception( - f"Tank has {self.version=:} and {self.image=:} supplied and can't be built. Provide one or the other." - ) - - # Special handling for complex properties - if "ln" in node: - options = { - "impl": node["ln"], - "cb_image": node.get("ln_cb_image", None), - "ln_config": node.get("ln_config", ""), - } - from warnet.cln import CLNNode - from warnet.lnd import LNDNode - - if options["impl"] == "lnd": - options["ln_image"] = node.get("ln_image", "lightninglabs/lnd:v0.18.0-beta") - self.lnnode = LNDNode(self.warnet, self, self.warnet.container_interface, options) - elif options["impl"] == "cln": - options["ln_image"] = node.get("ln_image", "elementsproject/lightningd:v23.11") - self.lnnode = CLNNode(self.warnet, self, self.warnet.container_interface, options) - else: - raise Exception(f"Unsupported Lightning Network implementation: {options['impl']}") - - if "metrics" in node: - self.metrics = node["metrics"] - - logger.debug( - f"Parsed graph node: {self.index} with attributes: {[f'{key}={value}' for key, value in graph_properties.items()]}" - ) - - @classmethod - def from_graph_node(cls, index, warnet, tank=None): - assert index is not None - index = int(index) - self = tank - if self is None: - self = cls(index, warnet) - node = warnet.graph.nodes[index] - self.parse_graph_node(node) - return self - - @property - def suffix(self): - if self._suffix is None: - self._suffix = f"{self.index:06}" - return self._suffix - - @property - def ipv4(self): - if self._ipv4 is None: - self._ipv4 = generate_ipv4_addr(self.warnet.subnet) - return self._ipv4 - - @property - def exporter_name(self): - if self._exporter_name is None: - self._exporter_name = f"{self.network_name}-{CONTAINER_PREFIX_PROMETHEUS}-{self.suffix}" - return self._exporter_name - - @property - def status(self) -> RunningStatus: - return self.warnet.container_interface.get_status(self.index, ServiceType.BITCOIN) - - @exponential_backoff() - def exec(self, cmd: str): - return self.warnet.container_interface.exec_run(self.index, ServiceType.BITCOIN, cmd=cmd) - - def get_dns_addr(self) -> str: - dns_addr = self.warnet.container_interface.get_tank_dns_addr(self.index) - return dns_addr - - def get_ip_addr(self) -> str: - ip_addr = self.warnet.container_interface.get_tank_ip_addr(self.index) - return ip_addr - - def get_bitcoin_conf(self, nodes: list[str]) -> str: - conf = CONFIG_BASE - conf += f" -rpcuser={self.rpc_user}" - conf += f" -rpcpassword={self.rpc_password}" - conf += f" -rpcport={self.rpc_port}" - conf += f" -zmqpubrawblock=tcp://0.0.0.0:{self.zmqblockport}" - conf += f" -zmqpubrawtx=tcp://0.0.0.0:{self.zmqtxport}" - conf += " " + self.bitcoin_config - for node in nodes: - conf += f" -addnode={node}" - return conf - - def apply_network_conditions(self): - if self.netem is None: - return - - if not sanitize_tc_netem_command(self.netem): - logger.warning( - f"Not applying unsafe tc-netem conditions to tank {self.index}: `{self.netem}`" - ) - return - - # Apply the network condition to the container - try: - self.exec(self.netem) - logger.info( - f"Successfully applied network conditions to tank {self.index}: `{self.netem}`" - ) - except Exception as e: - logger.error( - f"Error applying network conditions to tank {self.index}: `{self.netem}` ({e})" - ) - - def export(self, config: object, tar_file): - if self.lnnode is not None: - self.lnnode.export(config, tar_file) diff --git a/src/warnet/test_framework_bridge.py b/src/warnet/test_framework_bridge.py deleted file mode 100644 index 7872284d2..000000000 --- a/src/warnet/test_framework_bridge.py +++ /dev/null @@ -1,406 +0,0 @@ -import argparse -import configparser -import ipaddress -import logging -import os -import pathlib -import random -import signal -import sys -import tempfile - -from test_framework.authproxy import AuthServiceProxy -from test_framework.p2p import NetworkThread -from test_framework.test_framework import ( - TMPDIR_PREFIX, - BitcoinTestFramework, - TestStatus, -) -from test_framework.test_node import TestNode -from test_framework.util import PortSeed, get_rpc_proxy - -from .warnet import Warnet - - -# Ensure that all RPC calls are made with brand new http connections -def auth_proxy_request(self, method, path, postdata): - self._set_conn() # creates new http client connection - return self.oldrequest(method, path, postdata) - - -AuthServiceProxy.oldrequest = AuthServiceProxy._request -AuthServiceProxy._request = auth_proxy_request - - -class WarnetTestFramework(BitcoinTestFramework): - def set_test_params(self): - pass - - def run_test(self): - pass - - def handle_sigterm(self, signum, frame): - print("SIGTERM received, stopping...") - self.shutdown() - sys.exit(0) - - # The following functions are chopped-up hacks of - # the original methods from BitcoinTestFramework - - def setup(self): - signal.signal(signal.SIGTERM, self.handle_sigterm) - - # Must setup warnet first to avoid double formatting - self.warnet = Warnet.from_network(self.options.network) - # hacked from _start_logging() - # Scenarios will log plain messages to stdout only, which will can redirected by warnet - self.log = logging.getLogger(self.__class__.__name__) - self.log.setLevel(logging.INFO) # set this to DEBUG to see ALL RPC CALLS - - # Because scenarios run in their own subprocess, the logger here - # is not the same as the warnet server or other global loggers. - # Scenarios log directly to stdout which gets picked up by the - # subprocess manager in the server, and reprinted to the global log. - ch = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter(fmt="%(name)-8s %(message)s") - ch.setFormatter(formatter) - self.log.addHandler(ch) - - for i, tank in enumerate(self.warnet.tanks): - ip = tank.ipv4 - self.log.info(f"Adding TestNode {i} from tank {tank.index} with IP {ip}") - node = TestNode( - i, - pathlib.Path(), # datadir path - chain=tank.bitcoin_network, - rpchost=ip, - timewait=60, - timeout_factor=self.options.timeout_factor, - bitcoind=None, - bitcoin_cli=None, - cwd=self.options.tmpdir, - coverage_dir=self.options.coveragedir, - ) - node.rpc = get_rpc_proxy( - f"http://{tank.rpc_user}:{tank.rpc_password}@{ip}:{tank.rpc_port}", - i, - timeout=60, - coveragedir=self.options.coveragedir, - ) - node.rpc_connected = True - self.nodes.append(node) - - self.num_nodes = len(self.nodes) - - # Set up temp directory and start logging - if self.options.tmpdir: - self.options.tmpdir = os.path.abspath(self.options.tmpdir) - os.makedirs(self.options.tmpdir, exist_ok=False) - else: - self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) - - # self.options.cachedir = os.path.abspath(self.options.cachedir) - - # config = self.config - - # self.set_binary_paths() - - # os.environ['PATH'] = os.pathsep.join([ - # os.path.join(config['environment']['BUILDDIR'], 'src'), - # os.path.join(config['environment']['BUILDDIR'], 'src', 'qt'), os.environ['PATH'] - # ]) - - # Set up temp directory and start logging - # if self.options.tmpdir: - # self.options.tmpdir = os.path.abspath(self.options.tmpdir) - # os.makedirs(self.options.tmpdir, exist_ok=False) - # else: - # self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) - # self._start_logging() - - # Seed the PRNG. Note that test runs are reproducible if and only if - # a single thread accesses the PRNG. For more information, see - # https://docs.python.org/3/library/random.html#notes-on-reproducibility. - # The network thread shouldn't access random. If we need to change the - # network thread to access randomness, it should instantiate its own - # random.Random object. - seed = self.options.randomseed - - if seed is None: - seed = random.randrange(sys.maxsize) - else: - self.log.info(f"User supplied random seed {seed}") - - random.seed(seed) - self.log.info(f"PRNG seed is: {seed}") - - self.log.debug("Setting up network thread") - self.network_thread = NetworkThread() - self.network_thread.start() - - # if self.options.usecli: - # if not self.supports_cli: - # raise SkipTest("--usecli specified but test does not support using CLI") - # self.skip_if_no_cli() - # self.skip_test_if_missing_module() - # self.setup_chain() - # self.setup_network() - - self.success = TestStatus.PASSED - - def parse_args(self): - previous_releases_path = "" - parser = argparse.ArgumentParser(usage="%(prog)s [options]") - parser.add_argument( - "--nocleanup", - dest="nocleanup", - default=False, - action="store_true", - help="Leave bitcoinds and test.* datadir on exit or error", - ) - parser.add_argument( - "--nosandbox", - dest="nosandbox", - default=False, - action="store_true", - help="Don't use the syscall sandbox", - ) - parser.add_argument( - "--noshutdown", - dest="noshutdown", - default=False, - action="store_true", - help="Don't stop bitcoinds after the test execution", - ) - parser.add_argument( - "--cachedir", - dest="cachedir", - default=None, - help="Directory for caching pregenerated datadirs (default: %(default)s)", - ) - parser.add_argument( - "--tmpdir", dest="tmpdir", default=None, help="Root directory for datadirs" - ) - parser.add_argument( - "-l", - "--loglevel", - dest="loglevel", - default="DEBUG", - help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.", - ) - parser.add_argument( - "--tracerpc", - dest="trace_rpc", - default=False, - action="store_true", - help="Print out all RPC calls as they are made", - ) - parser.add_argument( - "--portseed", - dest="port_seed", - default=0, - help="The seed to use for assigning port numbers (default: current process id)", - ) - parser.add_argument( - "--previous-releases", - dest="prev_releases", - default=None, - action="store_true", - help="Force test of previous releases (default: %(default)s)", - ) - parser.add_argument( - "--coveragedir", - dest="coveragedir", - default=None, - help="Write tested RPC commands into this directory", - ) - parser.add_argument( - "--configfile", - dest="configfile", - default=None, - help="Location of the test framework config file (default: %(default)s)", - ) - parser.add_argument( - "--pdbonfailure", - dest="pdbonfailure", - default=False, - action="store_true", - help="Attach a python debugger if test fails", - ) - parser.add_argument( - "--usecli", - dest="usecli", - default=False, - action="store_true", - help="use bitcoin-cli instead of RPC for all commands", - ) - parser.add_argument( - "--perf", - dest="perf", - default=False, - action="store_true", - help="profile running nodes with perf for the duration of the test", - ) - parser.add_argument( - "--valgrind", - dest="valgrind", - default=False, - action="store_true", - help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required.", - ) - parser.add_argument( - "--randomseed", - default=0x7761726E6574, # "warnet" ascii - help="set a random seed for deterministically reproducing a previous test run", - ) - parser.add_argument( - "--timeout-factor", - dest="timeout_factor", - default=1, - help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts", - ) - parser.add_argument( - "--network", - dest="network", - default="warnet", - help="Designate which warnet this should run on (default: warnet)", - ) - parser.add_argument( - "--v2transport", - dest="v2transport", - default=False, - action="store_true", - help="use BIP324 v2 connections between all nodes by default", - ) - - self.add_options(parser) - # Running TestShell in a Jupyter notebook causes an additional -f argument - # To keep TestShell from failing with an "unrecognized argument" error, we add a dummy "-f" argument - # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168 - parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") - self.options = parser.parse_args() - if self.options.timeout_factor == 0: - self.options.timeout_factor = 99999 - self.options.timeout_factor = self.options.timeout_factor or ( - 4 if self.options.valgrind else 1 - ) - self.options.previous_releases_path = previous_releases_path - config = configparser.ConfigParser() - if self.options.configfile is not None: - with open(self.options.configfile) as f: - config.read_file(f) - - config["environment"] = {"PACKAGE_BUGREPORT": ""} - - self.config = config - - if "descriptors" not in self.options: - # Wallet is not required by the test at all and the value of self.options.descriptors won't matter. - # It still needs to exist and be None in order for tests to work however. - # So set it to None to force -disablewallet, because the wallet is not needed. - self.options.descriptors = None - elif self.options.descriptors is None: - # Some wallet is either required or optionally used by the test. - # Prefer SQLite unless it isn't available - if self.is_sqlite_compiled(): - self.options.descriptors = True - elif self.is_bdb_compiled(): - self.options.descriptors = False - else: - # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter - # It still needs to exist and be None in order for tests to work however. - # So set it to None, which will also set -disablewallet. - self.options.descriptors = None - - PortSeed.n = self.options.port_seed - - def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool = True): - """ - Kwargs: - wait_for_connect: if True, block until the nodes are verified as connected. You might - want to disable this when using -stopatheight with one of the connected nodes, - since there will be a race between the actual connection and performing - the assertions before one node shuts down. - """ - from_connection = self.nodes[a] - to_connection = self.nodes[b] - - to_ip_port = self.warnet.tanks[b].get_dns_addr() - from_ip_port = self.warnet.tanks[a].get_ip_addr() - - if peer_advertises_v2 is None: - peer_advertises_v2 = self.options.v2transport - - if peer_advertises_v2: - from_connection.addnode(node=to_ip_port, command="onetry", v2transport=True) - else: - # skip the optional third argument (default false) for - # compatibility with older clients - from_connection.addnode(to_ip_port, "onetry") - - if not wait_for_connect: - return - - def get_peer_ip(peer): - try: # we encounter a regular ip address - ip_addr = str(ipaddress.ip_address(peer["addr"].split(":")[0])) - return ip_addr - except ValueError as err: # or we encounter a service name - try: - # NETWORK-tank-TANK_INDEX-service - # NETWORK-test-TEST-tank-TANK_INDEX-service - tank_index = int(peer["addr"].split("-")[-2]) - except (ValueError, IndexError) as inner_err: - raise ValueError( - "could not derive tank index from service name: {} {}".format( - peer["addr"], inner_err - ) - ) from err - - ip_addr = self.warnet.tanks[tank_index].get_ip_addr() - return ip_addr - - # poll until version handshake complete to avoid race conditions - # with transaction relaying - # See comments in net_processing: - # * Must have a version message before anything else - # * Must have a verack message before anything else - self.wait_until( - lambda: any( - peer["addr"] == to_ip_port and peer["version"] != 0 - for peer in from_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - get_peer_ip(peer) == from_ip_port and peer["version"] != 0 - for peer in to_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - peer["addr"] == to_ip_port and peer["bytesrecv_per_msg"].pop("verack", 0) >= 21 - for peer in from_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - get_peer_ip(peer) == from_ip_port - and peer["bytesrecv_per_msg"].pop("verack", 0) >= 21 - for peer in to_connection.getpeerinfo() - ) - ) - # The message bytes are counted before processing the message, so make - # sure it was fully processed by waiting for a ping. - self.wait_until( - lambda: any( - peer["addr"] == to_ip_port and peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 - for peer in from_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - get_peer_ip(peer) == from_ip_port and peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 - for peer in to_connection.getpeerinfo() - ) - ) diff --git a/src/warnet/users.py b/src/warnet/users.py new file mode 100644 index 000000000..24ddd9ff2 --- /dev/null +++ b/src/warnet/users.py @@ -0,0 +1,139 @@ +import difflib +import json +import os +import sys + +import click + +from warnet.constants import KUBECONFIG, KUBECONFIG_UNDO +from warnet.k8s import K8sError, open_kubeconfig, write_kubeconfig + + +@click.command() +@click.option("--revert", is_flag=True, default=False, show_default=True) +@click.argument("auth_config", type=str, required=False) +def auth(revert, auth_config): + """Authenticate with a Warnet cluster using a kubernetes config file""" + if revert: + auth_config = KUBECONFIG_UNDO + elif not auth_config: + raise click.UsageError("Missing argument: AUTH_CONFIG") + + try: + auth_config = open_kubeconfig(auth_config) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open auth_config: {auth_config}", fg="red") + sys.exit(1) + + is_first_config = False + if not os.path.exists(KUBECONFIG): + os.makedirs(os.path.dirname(KUBECONFIG), exist_ok=True) + try: + write_kubeconfig(auth_config, KUBECONFIG) + is_first_config = True + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not write KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) + + try: + base_config = open_kubeconfig(KUBECONFIG) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) + + try: + write_kubeconfig(base_config, KUBECONFIG_UNDO) + click.secho(f"Backed up current kubeconfig to: {KUBECONFIG_UNDO}", fg="green") + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not backup current kubeconfig to {KUBECONFIG_UNDO}", fg="red") + + if not is_first_config: + for category in ["clusters", "users", "contexts"]: + if category in auth_config: + if category in base_config and not base_config[category]: + base_config[category] = [] + merge_entries(category, base_config, auth_config) + + new_current_context = auth_config.get("current-context") + base_config["current-context"] = new_current_context + + # Check if the new current context has an explicit namespace + context_entry = next( + (ctx for ctx in base_config["contexts"] if ctx["name"] == new_current_context), None + ) + if context_entry and "namespace" not in context_entry["context"]: + click.secho( + f"Warning: The context '{new_current_context}' does not have an explicit namespace.", + fg="yellow", + ) + + try: + write_kubeconfig(base_config, KUBECONFIG) + click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not write KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) + + try: + base_config = open_kubeconfig(KUBECONFIG) + click.secho( + f"Warnet's current context is now set to: {base_config['current-context']}", fg="green" + ) + except K8sError as e: + click.secho(f"Error reading from {KUBECONFIG}: {e}", fg="red") + sys.exit(1) + + +def merge_entries(category, base_config, auth_config): + name = "name" + base_list = base_config.setdefault(category, []) + auth_list = auth_config[category] + base_entry_names = {entry[name] for entry in base_list} # Extract existing names + for auth_entry in auth_list: + if auth_entry[name] in base_entry_names: + existing_entry = next( + base_entry for base_entry in base_list if base_entry[name] == auth_entry[name] + ) + if existing_entry != auth_entry: + # Show diff between existing and new entry + existing_entry_str = json.dumps(existing_entry, indent=2, sort_keys=True) + auth_entry_str = json.dumps(auth_entry, indent=2, sort_keys=True) + diff = difflib.unified_diff( + existing_entry_str.splitlines(), + auth_entry_str.splitlines(), + fromfile="Existing Entry", + tofile="New Entry", + lineterm="", + ) + click.echo("Differences between existing and new entry:\n") + click.echo("\n".join(diff)) + + if click.confirm( + f"The '{category}' section key '{auth_entry[name]}' already exists and differs. Overwrite?", + default=False, + ): + # Find and replace the existing entry + base_list[:] = [ + base_entry if base_entry[name] != auth_entry[name] else auth_entry + for base_entry in base_list + ] + click.secho( + f"Overwrote '{category}' section key '{auth_entry[name]}'", fg="yellow" + ) + else: + click.secho( + f"Skipped '{category}' section key '{auth_entry[name]}'", fg="yellow" + ) + else: + click.secho( + f"Entry for '{category}' section key '{auth_entry[name]}' is identical. No changes made.", + fg="blue", + ) + else: + base_list.append(auth_entry) + click.secho(f"Added new '{category}' section key '{auth_entry[name]}'", fg="green") diff --git a/src/warnet/util.py b/src/warnet/util.py new file mode 100644 index 000000000..90cc7b91d --- /dev/null +++ b/src/warnet/util.py @@ -0,0 +1,65 @@ +def create_cycle_graph(n: int, version: str, bitcoin_conf: str | None, random_version: bool): + raise NotImplementedError("create_cycle_graph function is not implemented") + + +def parse_bitcoin_conf(file_content): + """ + Custom parser for INI-style bitcoin.conf + + Args: + - file_content (str): The content of the INI-style file. + + Returns: + - dict: A dictionary representation of the file content. + Key-value pairs are stored as tuples so one key may have + multiple values. Sections are represented as arrays of these tuples. + """ + current_section = None + result = {current_section: []} + + for line in file_content.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + + if line.startswith("[") and line.endswith("]"): + current_section = line[1:-1] + result[current_section] = [] + elif "=" in line: + key, value = line.split("=", 1) + result[current_section].append((key.strip(), value.strip())) + + return result + + +def dump_bitcoin_conf(conf_dict, for_graph=False): + """ + Converts a dictionary representation of bitcoin.conf content back to INI-style string. + + Args: + - conf_dict (dict): A dictionary representation of the file content. + + Returns: + - str: The INI-style string representation of the input dictionary. + """ + result = [] + + # Print global section at the top first + values = conf_dict[None] + for sub_key, sub_value in values: + result.append(f"{sub_key}={sub_value}") + + # Then print any named subsections + for section, values in conf_dict.items(): + if section is not None: + result.append(f"\n[{section}]") + else: + continue + for sub_key, sub_value in values: + result.append(f"{sub_key}={sub_value}") + + if for_graph: + return ",".join(result) + + # Terminate file with newline + return "\n".join(result) + "\n" diff --git a/src/warnet/utils.py b/src/warnet/utils.py deleted file mode 100644 index 23dad566b..000000000 --- a/src/warnet/utils.py +++ /dev/null @@ -1,479 +0,0 @@ -import functools -import ipaddress -import json -import logging -import os -import random -import re -import stat -import subprocess -import sys -import time -from io import BytesIO -from pathlib import Path - -import networkx as nx -from jsonschema import validate -from test_framework.messages import ser_uint256 -from test_framework.p2p import MESSAGEMAP -from warnet import SRC_DIR - -logger = logging.getLogger("utils") - - -SUPPORTED_TAGS = ["27.0", "26.0", "25.1", "24.2", "23.2", "22.2"] -DEFAULT_TAG = SUPPORTED_TAGS[0] -WEIGHTED_TAGS = [ - tag for index, tag in enumerate(reversed(SUPPORTED_TAGS)) for _ in range(index + 1) -] - - -class NonErrorFilter(logging.Filter): - def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord: - return record.levelno <= logging.INFO - - -def exponential_backoff(max_retries=5, base_delay=1, max_delay=32): - """ - A decorator for exponential backoff. - - Parameters: - - max_retries: Maximum number of retries before giving up. - - base_delay: Initial delay in seconds. - - max_delay: Maximum delay in seconds. - """ - - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - retries = 0 - while retries < max_retries: - try: - return func(*args, **kwargs) - except Exception as e: - error_msg = str(e).replace("\n", " ").replace("\t", " ") - logger.error(f"rpc error: {error_msg}") - retries += 1 - if retries == max_retries: - raise e - delay = min(base_delay * (2**retries), max_delay) - logger.warning(f"exponential_backoff: retry in {delay} seconds...") - time.sleep(delay) - - return wrapper - - return decorator - - -def handle_json(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - result = "" - try: - result = func(*args, **kwargs) - logger.debug(f"{result=:}") - if isinstance(result, dict): - return result - parsed_result = json.loads(result) - return parsed_result - except json.JSONDecodeError as e: - logging.error( - f"JSON parsing error in {func.__name__}: {e}. Undecodable result: {result}" - ) - raise - except Exception as e: - logger.error(f"Error in {func.__name__}: {e}") - raise - - return wrapper - - -def get_architecture(): - """ - Get the architecture of the machine. - :return: The architecture of the machine or None if an error occurred - """ - result = subprocess.run(["uname", "-m"], stdout=subprocess.PIPE) - arch = result.stdout.decode("utf-8").strip() - if arch == "x86_64": - arch = "amd64" - if arch is None: - raise Exception("Failed to detect architecture.") - return arch - - -def generate_ipv4_addr(subnet): - """ - Generate a valid random IPv4 address within the given subnet. - - :param subnet: Subnet in CIDR notation (e.g., '100.0.0.0/8') - :return: Random IP address within the subnet - """ - reserved_ips = [ - "0.0.0.0/8", - "10.0.0.0/8", - "100.64.0.0/10", - "127.0.0.0/8", - "169.254.0.0/16", - "172.16.0.0/12", - "192.0.0.0/24", - "192.0.2.0/24", - "192.88.99.0/24", - "192.168.0.0/16", - "198.18.0.0/15", - "198.51.100.0/24", - "203.0.113.0/24", - "224.0.0.0/4", - ] - - def is_public(ip): - for reserved in reserved_ips: - if ipaddress.ip_address(ip) in ipaddress.ip_network(reserved, strict=False): - return False - return True - - network = ipaddress.ip_network(subnet, strict=False) - - # Generate a random IP within the subnet range - while True: - ip_int = random.randint(int(network.network_address), int(network.broadcast_address)) - ip_str = str(ipaddress.ip_address(ip_int)) - if is_public(ip_str): - return ip_str - - -def sanitize_tc_netem_command(command: str) -> bool: - """ - Sanitize the tc-netem command to ensure it's valid and safe to execute, as we run it as root on a container. - - Args: - - command (str): The tc-netem command to sanitize. - - Returns: - - bool: True if the command is valid and safe, False otherwise. - """ - if not command.startswith("tc qdisc add dev eth0 root netem"): - return False - - tokens = command.split()[7:] # Skip the prefix - - # Valid tc-netem parameters and their patterns - valid_params = { - "delay": r"^\d+ms(\s\d+ms)?(\sdistribution\s(normal|pareto|paretonormal|uniform))?$", - "loss": r"^\d+(\.\d+)?%$", - "duplicate": r"^\d+(\.\d+)?%$", - "corrupt": r"^\d+(\.\d+)?%$", - "reorder": r"^\d+(\.\d+)?%\s\d+(\.\d+)?%$", - "rate": r"^\d+(kbit|mbit|gbit)$", - } - - # Validate each param - i = 0 - while i < len(tokens): - param = tokens[i] - if param not in valid_params: - return False - i += 1 - value_tokens = [] - while i < len(tokens) and tokens[i] not in valid_params: - value_tokens.append(tokens[i]) - i += 1 - value = " ".join(value_tokens) - if not re.match(valid_params[param], value): - return False - - return True - - -def parse_bitcoin_conf(file_content): - """ - Custom parser for INI-style bitcoin.conf - - Args: - - file_content (str): The content of the INI-style file. - - Returns: - - dict: A dictionary representation of the file content. - Key-value pairs are stored as tuples so one key may have - multiple values. Sections are represented as arrays of these tuples. - """ - current_section = None - result = {current_section: []} - - for line in file_content.splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - - if line.startswith("[") and line.endswith("]"): - current_section = line[1:-1] - result[current_section] = [] - elif "=" in line: - key, value = line.split("=", 1) - result[current_section].append((key.strip(), value.strip())) - - return result - - -def dump_bitcoin_conf(conf_dict, for_graph=False): - """ - Converts a dictionary representation of bitcoin.conf content back to INI-style string. - - Args: - - conf_dict (dict): A dictionary representation of the file content. - - Returns: - - str: The INI-style string representation of the input dictionary. - """ - result = [] - - # Print global section at the top first - values = conf_dict[None] - for sub_key, sub_value in values: - result.append(f"{sub_key}={sub_value}") - - # Then print any named subsections - for section, values in conf_dict.items(): - if section is not None: - result.append(f"\n[{section}]") - else: - continue - for sub_key, sub_value in values: - result.append(f"{sub_key}={sub_value}") - - if for_graph: - return ",".join(result) - - # Terminate file with newline - return "\n".join(result) + "\n" - - -def to_jsonable(obj): - HASH_INTS = [ - "blockhash", - "block_hash", - "hash", - "hashMerkleRoot", - "hashPrevBlock", - "hashstop", - "prev_header", - "sha256", - "stop_hash", - ] - - HASH_INT_VECTORS = [ - "hashes", - "headers", - "vHave", - "vHash", - ] - - if hasattr(obj, "__dict__"): - return obj.__dict__ - elif hasattr(obj, "__slots__"): - ret = {} # type: Any - for slot in obj.__slots__: - val = getattr(obj, slot, None) - if slot in HASH_INTS and isinstance(val, int): - ret[slot] = ser_uint256(val).hex() - elif slot in HASH_INT_VECTORS and all(isinstance(a, int) for a in val): - ret[slot] = [ser_uint256(a).hex() for a in val] - else: - ret[slot] = to_jsonable(val) - return ret - elif isinstance(obj, list): - return [to_jsonable(a) for a in obj] - elif isinstance(obj, bytes): - return obj.hex() - else: - return obj - - -# This function is a hacked-up copy of process_file() from -# Bitcoin Core contrib/message-capture/message-capture-parser.py -def parse_raw_messages(blob, outbound): - TIME_SIZE = 8 - LENGTH_SIZE = 4 - MSGTYPE_SIZE = 12 - - messages = [] - offset = 0 - while True: - # Read the Header - header_len = TIME_SIZE + LENGTH_SIZE + MSGTYPE_SIZE - tmp_header_raw = blob[offset : offset + header_len] - - offset = offset + header_len - if not tmp_header_raw: - break - tmp_header = BytesIO(tmp_header_raw) - time = int.from_bytes(tmp_header.read(TIME_SIZE), "little") # type: int - msgtype = tmp_header.read(MSGTYPE_SIZE).split(b"\x00", 1)[0] # type: bytes - length = int.from_bytes(tmp_header.read(LENGTH_SIZE), "little") # type: int - - # Start converting the message to a dictionary - msg_dict = {} - msg_dict["outbound"] = outbound - msg_dict["time"] = time - msg_dict["size"] = length # "size" is less readable here, but more readable in the output - - msg_ser = BytesIO(blob[offset : offset + length]) - offset = offset + length - - # Determine message type - if msgtype not in MESSAGEMAP: - # Unrecognized message type - try: - msgtype_tmp = msgtype.decode() - if not msgtype_tmp.isprintable(): - raise UnicodeDecodeError - msg_dict["msgtype"] = msgtype_tmp - except UnicodeDecodeError: - msg_dict["msgtype"] = "UNREADABLE" - msg_dict["body"] = msg_ser.read().hex() - msg_dict["error"] = "Unrecognized message type." - messages.append(msg_dict) - print(f"WARNING - Unrecognized message type {msgtype}", file=sys.stderr) - continue - - # Deserialize the message - msg = MESSAGEMAP[msgtype]() - msg_dict["msgtype"] = msgtype.decode() - - try: - msg.deserialize(msg_ser) - except KeyboardInterrupt: - raise - except Exception: - # Unable to deserialize message body - msg_ser.seek(0, os.SEEK_SET) - msg_dict["body"] = msg_ser.read().hex() - msg_dict["error"] = "Unable to deserialize message." - messages.append(msg_dict) - print("WARNING - Unable to deserialize message", file=sys.stderr) - continue - - # Convert body of message into a jsonable object - if length: - msg_dict["body"] = to_jsonable(msg) - messages.append(msg_dict) - return messages - - -def gen_config_dir(network: str) -> Path: - """ - Determine a config dir based on network name - """ - config_dir = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.warnet")) - config_dir = Path(config_dir) / "warnet" / network - return config_dir - - -def remove_version_prefix(version_str): - if version_str.startswith("0."): - return version_str[2:] - return version_str - - -def set_execute_permission(file_path): - current_permissions = os.stat(file_path).st_mode - os.chmod(file_path, current_permissions | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - -def create_cycle_graph(n: int, version: str, bitcoin_conf: str | None, random_version: bool): - try: - # Use nx.MultiDiGraph() so we get directed edges (source->target) - # and still allow parallel edges (L1 p2p connections + LN channels) - graph = nx.generators.cycle_graph(n, nx.MultiDiGraph()) - except TypeError as e: - msg = f"Failed to create graph: {e}" - logger.error(msg) - return msg - - # Graph is a simply cycle graph with all nodes connected in a loop, including both ends. - # Ensure each node has at least 8 outbound connections by making 7 more outbound connections - for src_node in graph.nodes(): - logger.debug(f"Creating additional connections for node {src_node}") - for _ in range(8): - # Choose a random node to connect to - # Make sure it's not the same node and they aren't already connected in either direction - potential_nodes = [ - dst_node - for dst_node in range(n) - if dst_node != src_node - and not graph.has_edge(dst_node, src_node) - and not graph.has_edge(src_node, dst_node) - ] - if potential_nodes: - chosen_node = random.choice(potential_nodes) - graph.add_edge(src_node, chosen_node) - logger.debug(f"Added edge: {src_node}:{chosen_node}") - logger.debug(f"Node {src_node} edges: {graph.edges(src_node)}") - - # parse and process conf file - conf_contents = "" - if bitcoin_conf is not None: - conf = Path(bitcoin_conf) - if conf.is_file(): - with open(conf) as f: - # parse INI style conf then dump using for_graph - conf_dict = parse_bitcoin_conf(f.read()) - conf_contents = dump_bitcoin_conf(conf_dict, for_graph=True) - - # populate our custom fields - for i, node in enumerate(graph.nodes()): - if random_version: - graph.nodes[node]["version"] = random.choice(WEIGHTED_TAGS) - else: - # One node demoing the image tag - if i == 1: - graph.nodes[node]["image"] = f"bitcoindevproject/bitcoin:{version}" - else: - graph.nodes[node]["version"] = version - graph.nodes[node]["bitcoin_config"] = conf_contents - graph.nodes[node]["tc_netem"] = "" - graph.nodes[node]["build_args"] = "" - graph.nodes[node]["exporter"] = False - graph.nodes[node]["collect_logs"] = False - - convert_unsupported_attributes(graph) - return graph - - -def convert_unsupported_attributes(graph: nx.Graph): - # Sometimes networkx complains about invalid types when writing the graph - # (it just generated itself!). Try to convert them here just in case. - for _, node_data in graph.nodes(data=True): - for key, value in node_data.items(): - if isinstance(value, set): - node_data[key] = list(value) - elif isinstance(value, int | float | str): - continue - else: - node_data[key] = str(value) - - for _, _, edge_data in graph.edges(data=True): - for key, value in edge_data.items(): - if isinstance(value, set): - edge_data[key] = list(value) - elif isinstance(value, int | float | str): - continue - else: - edge_data[key] = str(value) - - -def load_schema(): - with open(SRC_DIR / "graph_schema.json") as schema_file: - return json.load(schema_file) - - -def validate_graph_schema(graph: nx.Graph): - """ - Validate a networkx.Graph against the node schema - """ - graph_schema = load_schema() - validate(instance=graph.graph, schema=graph_schema["graph"]) - for n in list(graph.nodes): - validate(instance=graph.nodes[n], schema=graph_schema["node"]) - for e in list(graph.edges): - validate(instance=graph.edges[e], schema=graph_schema["edge"]) diff --git a/src/warnet/warnet.py b/src/warnet/warnet.py deleted file mode 100644 index 67cd52002..000000000 --- a/src/warnet/warnet.py +++ /dev/null @@ -1,283 +0,0 @@ -""" -Warnet is the top-level class for a simulated network. -""" - -import base64 -import json -import logging -from pathlib import Path - -import networkx - -from .backend.kubernetes_backend import KubernetesBackend -from .tank import Tank -from .utils import gen_config_dir, load_schema, validate_graph_schema - -logger = logging.getLogger("warnet") - - -class Warnet: - def __init__(self, config_dir, network_name: str): - self.config_dir: Path = config_dir - self.config_dir.mkdir(parents=True, exist_ok=True) - self.container_interface = KubernetesBackend(config_dir, network_name) - self.bitcoin_network: str = "regtest" - self.network_name: str = "warnet" - self.subnet: str = "100.0.0.0/8" - self.graph: networkx.Graph | None = None - self.graph_name = "graph.graphml" - self.tanks: list[Tank] = [] - self.deployment_file: Path | None = None - self.graph_schema = load_schema() - self.services = [] - - def _warnet_dict_representation(self) -> dict: - repr = {} - # Warnet - repr["warnet_headers"] = [ - "Temp dir", - "Bitcoin network", - "Docker network", - "Subnet", - "Graph", - ] - repr["warnet"] = [ - [ - str(self.config_dir), - self.bitcoin_network, - self.network_name, - self.subnet, - str(self.graph), - ] - ] - - # Tanks - tank_headers = [ - "Index", - "Version", - "IPv4", - "bitcoin conf", - "tc_netem", - "LN", - "LN Image", - "LN IPv4", - ] - has_ln = any(tank.lnnode and tank.lnnode.impl for tank in self.tanks) - tanks = [] - for tank in self.tanks: - tank_data = [ - tank.index, - tank.version if tank.version else tank.image, - tank.ipv4, - tank.bitcoin_config, - tank.netem, - ] - if has_ln: - tank_data.extend( - [ - tank.lnnode.impl if tank.lnnode else "", - tank.lnnode.image if tank.lnnode else "", - tank.lnnode.ipv4 if tank.lnnode else "", - ] - ) - tanks.append(tank_data) - if not has_ln: - tank_headers.remove("LN") - tank_headers.remove("LN IPv4") - - repr["tank_headers"] = tank_headers - repr["tanks"] = tanks - - return repr - - @classmethod - def from_graph_file( - cls, - base64_graph: str, - config_dir: Path, - network: str = "warnet", - ): - self = cls(config_dir, network) - destination = self.config_dir / self.graph_name - destination.parent.mkdir(parents=True, exist_ok=True) - graph_file = base64.b64decode(base64_graph) - with open(destination, "wb") as f: - f.write(graph_file) - self.network_name = network - self.graph = networkx.parse_graphml( - graph_file.decode("utf-8"), node_type=int, force_multigraph=True - ) - validate_graph_schema(self.graph) - self.tanks_from_graph() - if "services" in self.graph.graph: - self.services = self.graph.graph["services"].split() - logger.info(f"Created Warnet using directory {self.config_dir}") - return self - - @classmethod - def from_graph(cls, graph, network="warnet"): - self = cls(Path(), network) - self.graph = graph - validate_graph_schema(self.graph) - self.tanks_from_graph() - if "services" in self.graph.graph: - self.services = self.graph.graph["services"].split() - logger.info(f"Created Warnet using directory {self.config_dir}") - return self - - @classmethod - def from_network(cls, network_name): - config_dir = gen_config_dir(network_name) - self = cls(config_dir, network_name) - self.network_name = network_name - # Get network graph edges from graph file (required for network restarts) - self.graph = networkx.read_graphml( - Path(self.config_dir / self.graph_name), node_type=int, force_multigraph=True - ) - validate_graph_schema(self.graph) - self.tanks_from_graph() - if "services" in self.graph.graph: - self.services = self.graph.graph["services"].split() - for tank in self.tanks: - tank._ipv4 = self.container_interface.get_tank_ipv4(tank.index) - return self - - def tanks_from_graph(self): - if not self.graph: - return - for node_id in self.graph.nodes(): - if int(node_id) != len(self.tanks): - raise Exception( - f"Node ID in graph must be incrementing integers (got '{node_id}', expected '{len(self.tanks)}')" - ) - tank = Tank.from_graph_node(node_id, self) - # import edges as list of destinations to connect to - for edge in self.graph.edges(data=True): - (src, dst, data) = edge - # Ignore LN edges for now - if "channel_open" in data: - continue - if src == node_id: - tank.init_peers.append(int(dst)) - self.tanks.append(tank) - logger.info(f"Imported {len(self.tanks)} tanks from graph") - - def apply_network_conditions(self): - for tank in self.tanks: - tank.apply_network_conditions() - - def warnet_build(self): - self.container_interface.build() - - def get_ln_node_from_tank(self, index): - return self.tanks[index].lnnode - - def warnet_up(self): - self.container_interface.up(self) - - def warnet_down(self): - self.container_interface.down(self) - - def generate_deployment(self): - self.container_interface.generate_deployment_file(self) - - # if "forkobserver" in self.services: - # self.write_fork_observer_config() - # if "addrmanobserver" in self.services: - # self.write_addrman_observer_config() - # if "grafana" in self.services: - # self.write_grafana_config() - # if "prometheus" in self.services: - # self.write_prometheus_config() - - # def write_fork_observer_config(self): - # src = FO_CONF_NAME - # dst = self.config_dir / FO_CONF_NAME - # shutil.copy(src, dst) - # with open(dst, "a") as f: - # for tank in self.tanks: - # f.write( - # f""" - # [[networks.nodes]] - # id = {tank.index} - # name = "Node {tank.index}" - # description = "Warnet tank {tank.index}" - # rpc_host = "{tank.ipv4}" - # rpc_port = {tank.rpc_port} - # rpc_user = "{tank.rpc_user}" - # rpc_password = "{tank.rpc_password}" - # """ - # ) - # logger.info(f"Wrote file: {dst}") - - # def write_addrman_observer_config(self): - # src = AO_CONF_NAME - # dst = self.config_dir / AO_CONF_NAME - # shutil.copy(src, dst) - # with open(dst, "a") as f: - # for tank in self.tanks: - # f.write( - # f""" - # [[nodes]] - # id = {tank.index} - # name = "node-{tank.index}" - # rpc_host = "{tank.ipv4}" - # rpc_port = {tank.rpc_port} - # rpc_user = "{tank.rpc_user}" - # rpc_password = "{tank.rpc_password}" - # """ - # ) - # logger.info(f"Wrote file: {dst}") - - # def write_grafana_config(self): - # src = GRAFANA_PROVISIONING - # dst = self.config_dir / GRAFANA_PROVISIONING - # shutil.copytree(src, dst, dirs_exist_ok=True) - # logger.info(f"Wrote directory: {dst}") - - # def write_prometheus_config(self): - # scrape_configs = [ - # { - # "job_name": "cadvisor", - # "scrape_interval": "15s", - # "static_configs": [{"targets": [f"{self.network_name}_cadvisor:8080"]}], - # } - # ] - # for tank in self.tanks: - # if tank.exporter: - # scrape_configs.append( - # { - # "job_name": tank.exporter_name, - # "scrape_interval": "5s", - # "static_configs": [{"targets": [f"{tank.exporter_name}:9332"]}], - # } - # ) - # config = {"global": {"scrape_interval": "15s"}, "scrape_configs": scrape_configs} - # prometheus_path = self.config_dir / PROM_CONF_NAME - # try: - # with open(prometheus_path, "w") as file: - # yaml.dump(config, file) - # logger.info(f"Wrote file: {prometheus_path}") - # except Exception as e: - # logger.error(f"An error occurred while writing to {prometheus_path}: {e}") - - def export(self, config: object, tar_file, exclude: list[int]): - for tank in self.tanks: - if tank.index not in exclude: - tank.export(config, tar_file) - - def wait_for_health(self): - self.container_interface.wait_for_healthy_tanks(self) - - def network_connected(self): - for tank in self.tanks: - peerinfo = json.loads(self.container_interface.get_bitcoin_cli(tank, "getpeerinfo")) - manuals = 0 - for peer in peerinfo: - if peer["connection_type"] == "manual": - manuals += 1 - # Even if more edges are specifed, bitcoind only allows - # 8 manual outbound connections - if min(8, len(tank.init_peers)) > manuals: - return False - return True diff --git a/test/bitcoin_rpc_args_test.py b/test/bitcoin_rpc_args_test.py new file mode 100755 index 000000000..4015c4e13 --- /dev/null +++ b/test/bitcoin_rpc_args_test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +import shlex +import sys +from pathlib import Path +from unittest.mock import patch + +# Import TestBase for consistent test structure +from test_base import TestBase + +from warnet.bitcoin import _rpc + +# Import _rpc from warnet.bitcoin and run_command from warnet.process +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +# Edge cases to test +EDGE_CASES = [ + # (params, expected_cmd_suffix, should_fail) + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "economical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "economical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "'economical'"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "'economical'"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", '"economical"'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", '"economical"'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco'nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco'nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", 'eco"nomical'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", 'eco"nomical'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco$nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco$nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco;nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco;nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco|nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco|nomical"], + False, + ), + # Malformed JSON (should fail gracefully) + ( + [ + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0}' + ], # Missing closing bracket + [ + "importdescriptors", + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0}', + ], + True, # Should fail due to malformed JSON + ), + # Unicode in descriptors + ( + [ + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0,"label":"测试"}' + ], + [ + "importdescriptors", + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0,"label":"测试"}', + ], + False, + ), + # Long descriptor (simulate, should not crash, may fail) + ( + [ + "[{'desc':'wpkh([d34db33f/84h/0h/0h/0/0]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...','range':[0,1000]}]" + ], + [ + "send", + "[{'desc':'wpkh([d34db33f/84h/0h/0h/0/0]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...','range':[0,1000]}]", + ], + False, # Updated to False since it now works correctly + ), + # Empty params + ([], ["send"], False), +] + + +class BitcoinRPCRPCArgsTest(TestBase): + def __init__(self): + super().__init__() + self.tank = "tank-0027" + self.namespace = "default" + self.captured_cmds = [] + + def run_test(self): + self.log.info("Testing bitcoin _rpc argument handling edge cases") + for params, expected_suffix, should_fail in EDGE_CASES: + # Extract the method from the expected suffix + method = expected_suffix[0] + + with patch("warnet.bitcoin.run_command") as mock_run_command: + mock_run_command.return_value = "MOCKED" + try: + _rpc(self.tank, method, params, self.namespace) + called_args = mock_run_command.call_args[0][0] + self.captured_cmds.append(called_args) + # Parse the command string into arguments for comparison + parsed_args = shlex.split(called_args) + assert parsed_args[-len(expected_suffix) :] == expected_suffix, ( + f"Params: {params} | Got: {parsed_args[-len(expected_suffix) :]} | Expected: {expected_suffix}" + ) + if should_fail: + self.log.info(f"Expected failure for params: {params}, but succeeded.") + except Exception as e: + if not should_fail: + raise AssertionError(f"Unexpected failure for params: {params}: {e}") from e + self.log.info(f"Expected failure for params: {params}: {e}") + self.log.info("All edge case argument tests passed.") + + +if __name__ == "__main__": + test = BitcoinRPCRPCArgsTest() + test.run_test() diff --git a/test/build_branch_test.py b/test/build_branch_test.py deleted file mode 100755 index bbce564ce..000000000 --- a/test/build_branch_test.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os -from pathlib import Path - -from test_base import TestBase - - -class BuildBranchTest(TestBase): - def __init__(self): - super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "build_v24_test.graphml" - - def run_test(self): - self.start_server() - try: - self.setup_network() - self.wait_for_p2p_connections() - self.check_build_flags() - finally: - self.stop_server() - - def setup_network(self): - self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) - self.wait_for_all_tanks_status(target="running", timeout=10 * 60) - self.wait_for_all_edges() - - def wait_for_p2p_connections(self): - self.log.info("Waiting for P2P connections") - self.wait_for_predicate(self.check_peers, timeout=5 * 60) - - def check_peers(self): - info0 = json.loads(self.warcli("bitcoin rpc 0 getpeerinfo")) - info1 = json.loads(self.warcli("bitcoin rpc 1 getpeerinfo")) - self.log.debug( - f"Waiting for both nodes to get one peer: node0: {len(info0)}, node1: {len(info1)}" - ) - return len(info0) == 1 and len(info1) == 1 - - def check_build_flags(self): - self.log.info("Checking build flags") - release_help = self.get_tank(0).exec("bitcoind -h") - build_help = self.get_tank(1).exec("bitcoind -h") - - assert "zmqpubhashblock" in release_help, "zmqpubhashblock not found in release help" - assert ( - "zmqpubhashblock" not in build_help - ), "zmqpubhashblock found in build help, but it shouldn't be" - - self.log.info("Build flags check passed") - - -if __name__ == "__main__": - test = BuildBranchTest() - test.run_test() diff --git a/test/conf_test.py b/test/conf_test.py new file mode 100755 index 000000000..fc3c8f4b9 --- /dev/null +++ b/test/conf_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import json +import os +import re +from pathlib import Path + +from test_base import TestBase + +from warnet.control import stop_scenario +from warnet.k8s import get_mission +from warnet.status import _get_deployed_scenarios as scenarios_deployed + + +class ConfTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "bitcoin_conf" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + + def run_test(self): + try: + self.setup_network() + self.check_uacomment() + self.check_single_miner() + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + self.log.info(self.warnet(f"deploy {self.network_dir}")) + self.wait_for_all_tanks_status(target="running") + + def check_uacomment(self): + tanks = get_mission("tank") + + def get_uacomment(): + for tank in tanks[::-1]: + try: + name = tank.metadata.name + info = json.loads(self.warnet(f"bitcoin rpc {name} getnetworkinfo")) + subver = info["subversion"] + + # Regex pattern to match the uacomment inside parentheses + # e.g. /Satoshi:27.0.0(tank-0027)/ + pattern = r"\(([^)]+)\)" + match = re.search(pattern, subver) + if match: + uacomment = match.group(1) + assert uacomment == name + else: + return False + except Exception: + return False + return True + + self.wait_for_predicate(get_uacomment) + + def check_single_miner(self): + scenario_file = self.scen_dir / "miner_std.py" + self.log.info(f"Running scenario from: {scenario_file}") + # Mine from a tank that is not first or last and + # is one of the only few in the network that even + # has rpc reatewallet method! + self.warnet(f"run {scenario_file} --tank=tank-0026 --interval=1") + self.wait_for_predicate( + lambda: int(self.warnet("bitcoin rpc tank-0026 getblockcount")) >= 10 + ) + running = scenarios_deployed() + assert len(running) == 1, f"Expected one running scenario, got {len(running)}" + assert running[0]["status"] == "running", "Scenario should be running" + stop_scenario(running[0]["name"]) + self.wait_for_all_scenarios() + + +if __name__ == "__main__": + test = ConfTest() + test.run_test() diff --git a/test/dag_connection_test.py b/test/dag_connection_test.py index 32c2ccc8c..dee38356a 100755 --- a/test/dag_connection_test.py +++ b/test/dag_connection_test.py @@ -9,42 +9,27 @@ class DAGConnectionTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = ( - Path(os.path.dirname(__file__)) / "data" / "ten_semi_unconnected.graphml" - ) + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ten_semi_unconnected" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" def run_test(self): - self.start_server() try: self.setup_network() self.run_connect_dag_scenario() - self.run_connect_dag_scenario_post_connection() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") self.wait_for_all_edges() def run_connect_dag_scenario(self): - self.log.info("Running connect_dag scenario") - self.log_expected_msgs = [ - "Successfully ran the connect_dag.py scenario using a temporary file" - ] - self.log_unexpected_msgs = ["Test failed."] - self.warcli("scenarios run-file test/data/scenario_connect_dag.py") + scenario_file = self.scen_dir / "test_scenarios" / "connect_dag.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir}") self.wait_for_all_scenarios() - self.assert_log_msgs() - - def run_connect_dag_scenario_post_connection(self): - self.log.info("Running connect_dag scenario") - self.log_expected_msgs = ["Successfully ran the connect_dag.py scenario"] - self.log_unexpected_msgs = ["Test failed"] - self.warcli("scenarios run-file test/data/scenario_connect_dag.py") - self.wait_for_all_scenarios() - self.assert_log_msgs() if __name__ == "__main__": diff --git a/test/data/12_node_ring.graphml b/test/data/12_node_ring.graphml deleted file mode 100644 index 7cdf3a7f7..000000000 --- a/test/data/12_node_ring.graphml +++ /dev/null @@ -1,81 +0,0 @@ - - diff --git a/test/data/12_node_ring/network.yaml b/test/data/12_node_ring/network.yaml new file mode 100644 index 000000000..62110f3d7 --- /dev/null +++ b/test/data/12_node_ring/network.yaml @@ -0,0 +1,61 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + config: | + debug=rpc + debug=validation + - name: tank-0001 + addnode: + - tank-0002 + config: | + debug=net + debug=validation + - name: tank-0002 + addnode: + - tank-0003 + config: | + debug=validation + - name: tank-0003 + addnode: + - tank-0004 + config: | + debug=validation + - name: tank-0004 + addnode: + - tank-0005 + - name: tank-0005 + addnode: + - tank-0006 + config: | + debug=validation + - name: tank-0006 + addnode: + - tank-0007 + - name: tank-0007 + config: | + debug=validation + addnode: + - tank-0008 + config: | + debug=validation + - name: tank-0008 + addnode: + - tank-0009 + config: | + debug=validation + - name: tank-0009 + addnode: + - tank-0010 + config: | + debug=validation + - name: tank-0010 + addnode: + - tank-0011 + config: | + debug=validation + - name: tank-0011 + addnode: + - tank-0000 + config: | + debug=validation \ No newline at end of file diff --git a/test/data/12_node_ring/node-defaults.yaml b/test/data/12_node_ring/node-defaults.yaml new file mode 100644 index 000000000..7e021cad1 --- /dev/null +++ b/test/data/12_node_ring/node-defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/LN_100.json b/test/data/LN_100.json index 2087c74d8..d40575803 100644 --- a/test/data/LN_100.json +++ b/test/data/LN_100.json @@ -1,22 +1,20 @@ { - "directed": false, - "multigraph": false, - "graph": {}, "nodes": [ { - "last_update": 1710607733, - "alias": "BlueWave", + "pub_key": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "last_update": 1708144920, + "alias": "ln02.bullbitcoin.com", "addresses": [ { "network": "tcp", - "addr": "bh32yyu7dgyodzq3wdunho5esew5qbvrd2flsynqzeyilnd7jnyeahqd.onion:9735" + "addr": "172.81.180.3:9735" } ], - "color": "#d6de37", + "color": "#5140bf", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -29,14 +27,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -49,50 +52,49 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", - "is_required": false, - "is_known": true - }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "47": { + "name": "scid-alias", "is_required": false, "is_known": true }, - "55": { - "name": "keysend", + "51": { + "name": "zero-conf", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "55": { + "name": "keysend", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6" + "custom_records": {} }, { - "last_update": 1702845433, - "alias": "03796678b7111abef10f", + "pub_key": "03e114a6bffa9ff420eb84491f3eceb3b2a5a5638f6d60d4324e16436c61acdcdc", + "last_update": 1710759555, + "alias": "zuragia", "addresses": [ { "network": "tcp", - "addr": "kgrxo24cm2ykhfazqer7bxzezcevrmir2du455o4mfbg3rpvqubpaqqd.onion:9735" + "addr": "185.21.223.27:9735" + }, + { + "network": "tcp", + "addr": "2qee6j4z7vnvq4q4z7h7s4czte3kgjshnrvuoikdhu2kf2gtf3b4xjad.onion:9735" } ], - "color": "#3399ff", + "color": "#77d133", "features": { "0": { "name": "data-loss-protect", @@ -149,34 +151,29 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5" + "custom_records": {} }, { - "last_update": 1709578090, - "alias": "REDWHISPER", + "pub_key": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "last_update": 1710667233, + "alias": "Fourth Turning \u267b\ufe0f LND", "addresses": [ { "network": "tcp", - "addr": "piwsugtc6hr3jukavptmv6tjrxt2wt2i2q35lvjx6lk25vwvi43zicqd.onion:9735" + "addr": "ge6wzwqraphpvmby3tturziuky3awlj7xui3bc2mqihxxtm5dahiq6id.onion:9735" } ], - "color": "#023bb9", + "color": "#ff9802", "features": { - "1": { + "0": { "name": "data-loss-protect", - "is_required": false, + "is_required": true, "is_known": true }, "5": { @@ -189,19 +186,14 @@ "is_required": false, "is_known": true }, - "8": { + "9": { "name": "tlv-onion", - "is_required": true, - "is_known": true - }, - "11": { - "name": "unknown", "is_required": false, - "is_known": false + "is_known": true }, - "13": { + "12": { "name": "static-remote-key", - "is_required": false, + "is_required": true, "is_known": true }, "14": { @@ -219,50 +211,50 @@ "is_required": false, "is_known": true }, - "25": { - "name": "unknown", + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, - "is_known": false + "is_known": true }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "31": { + "name": "amp", "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "51": { - "name": "zero-conf", + "55": { + "name": "keysend", "is_required": false, "is_known": true }, - "55": { - "name": "keysend", + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d" + "custom_records": {} }, { - "last_update": 1710772374, - "alias": "fr33node", + "pub_key": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "last_update": 1710856043, + "alias": "strike", "addresses": [ { "network": "tcp", - "addr": "ubzmye2tytcqarycr3jsirl3ajxallddfpvorc4t2odao4y5yovdalad.onion:9735" + "addr": "34.138.210.19:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -299,6 +291,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -325,23 +322,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199" + "custom_records": {} }, { - "last_update": 1710847816, - "alias": "LQwD-Canada", + "pub_key": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "last_update": 1710808183, + "alias": "\ud83d\udc08 MediumOfExchange \ud83d\udc08\u200d\u2b1b", "addresses": [ { "network": "tcp", - "addr": "192.243.215.102:9735" - }, - { - "network": "tcp", - "addr": "yit5nizyrgk4n2gx7hkadaas5iz33kn6wulr5vdndzpbae6tykqdw7ad.onion:9735" + "addr": "ld7l2ckvvtpj4wev4vegmj3yl5fsd2uuhcy5gesxhdjihmdx3afvn2ad.onion:9735" } ], - "color": "#3399ff", + "color": "#0000cd", "features": { "0": { "name": "data-loss-protect", @@ -378,11 +371,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -403,8 +391,8 @@ "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "55": { + "name": "keysend", "is_required": false, "is_known": true }, @@ -414,19 +402,27 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + "custom_records": {} }, { - "last_update": 1710855669, - "alias": "River Financial 2", + "pub_key": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "last_update": 1710856205, + "alias": "DorianIsMySatoshi", "addresses": [ { "network": "tcp", - "addr": "34.136.230.235:9735" + "addr": "45.142.235.46:9735" + }, + { + "network": "tcp", + "addr": "[2a10:3781:2c19::1]:9735" + }, + { + "network": "tcp", + "addr": "wqbh5jxqgbfxhb7wgjdfzkfcihkgcbvyzivme46x2yrqkdjghx6wesid.onion:9735" } ], - "color": "#ff9900", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -494,16 +490,20 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5" + "custom_records": {} }, { - "last_update": 1707020283, - "alias": "027d5369f807773ed75d", + "pub_key": "036fe0795980588a1489d2ddf11f47a0ac36671e2cbff46bc4ace52738f15a1956", + "last_update": 1710797125, + "alias": "NoGoodNode", "addresses": [ { "network": "tcp", - "addr": "leb7bxhf3ppdfssy2bu4udc2cnhbehw5nhkkiumh76fhxel6bqtqmqqd.onion:9735" + "addr": "170.75.163.98:9735" + }, + { + "network": "tcp", + "addr": "tk5urwrimyqdm36mfdiqarv2i7uoirqjazvpeqypmxdhtpiefwxhypyd.onion:9735" } ], "color": "#3399ff", @@ -543,6 +543,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -563,34 +568,25 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + "custom_records": {} }, { - "last_update": 1710847881, - "alias": "lnd1.relampago.cash", + "pub_key": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "last_update": 1710791376, + "alias": "LightningStation", "addresses": [ { "network": "tcp", - "addr": "44.225.98.1:9735" - }, - { - "network": "tcp", - "addr": "6hmm7rvxv6mu7sv5wetqfigjljrde2cd6kr474ncylbbfzdsczyvhlad.onion:9735" + "addr": "q3vax5ytpca2toncaoxbzzszque2iuhm2eu4l7a6nps4vhtbz2zhdwid.onion:9735" } ], - "color": "#ffa47a", + "color": "#ff3c00", "features": { "0": { "name": "data-loss-protect", @@ -663,19 +659,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + "custom_records": {} }, { - "last_update": 1710623243, - "alias": "bitpi", + "pub_key": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "last_update": 1710853945, + "alias": "Rust-eze (now BoltTrustiC)", "addresses": [ { "network": "tcp", - "addr": "iddfruwmorrrlr32akea422ajghfyicyohdd3n3a6mmiigd5x7druhyd.onion:9735" + "addr": "46.105.76.211:9735" + }, + { + "network": "tcp", + "addr": "wama6rprmtwzsmqyhcasko5q2ypov5apqa4ntlzraxpcgu54ewdkuhid.onion:9735" } ], - "color": "#68f442", + "color": "#b5603f", "features": { "0": { "name": "data-loss-protect", @@ -712,13 +712,13 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "19": { + "name": "wumbo-channels", "is_required": false, "is_known": true }, - "27": { - "name": "shutdown-any-segwit", + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, @@ -732,25 +732,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3" + "custom_records": {} }, { - "last_update": 1674198664, - "alias": "Bitcomoon.com", + "pub_key": "03518cfddeb18d50db01fc316984bac961a6d978115a52259cd5d2e47fbe363e94", + "last_update": 1704834837, + "alias": "MontanasRaspiBlitz", "addresses": [ { "network": "tcp", - "addr": "e3xx3u5oxvuo4bgix5cl2ngfeyvexoz7my3rktgrpmf6gq3sryi2orqd.onion:9735" + "addr": "jlffjofzvlbxwfbxvpugkhiflq3pi5c4fgmstlio6rnlcjds3jpiitad.onion:9735" } ], - "color": "#79e0ed", + "color": "#68f442", "features": { "0": { "name": "data-loss-protect", @@ -787,6 +792,11 @@ "is_required": false, "is_known": true }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, "27": { "name": "shutdown-any-segwit", "is_required": false, @@ -802,25 +812,25 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda" + "custom_records": {} }, { - "last_update": 1710586588, - "alias": "Bitrequest", + "pub_key": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "last_update": 1710815461, + "alias": "Beinardus [LND]", "addresses": [ { "network": "tcp", - "addr": "wvhczhxeyuo5gsbc6ub5n66yxycvojyx26eunh76l3ta7ks7yjckbgqd.onion:9735" + "addr": "jt6lh76prisvqvt3lyeyy2b5kcmudq6ab2tli6l3rvlwfcxzqzbhk5yd.onion:9735" } ], - "color": "#0cc0ed", + "color": "#5050a0", "features": { "0": { "name": "data-loss-protect", @@ -857,6 +867,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -888,19 +903,27 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296" + "custom_records": {} }, { - "last_update": 1710773661, - "alias": "raspiblitz", + "pub_key": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "last_update": 1710856661, + "alias": "allNice | torq.co", "addresses": [ { "network": "tcp", - "addr": "n75lmwb3ykmwvbqajzkdh6otc6ukasij4f556ub54ug4qvs22rb3rsyd.onion:9735" + "addr": "141.95.72.206:9735" + }, + { + "network": "tcp", + "addr": "[2001:41d0:700:5fce::]:9735" + }, + { + "network": "tcp", + "addr": "sioo43tvkudx56ksir3ndwynv2df3356dpjc6s4yy3abjtcmsys6wnad.onion:9735" } ], - "color": "#68f442", + "color": "#660000", "features": { "0": { "name": "data-loss-protect", @@ -937,6 +960,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -957,22 +985,32 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2" + "custom_records": {} }, { - "last_update": 1710825523, - "alias": "qubi1", + "pub_key": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "last_update": 1710836058, + "alias": "getlipa.com mainnet", "addresses": [ { "network": "tcp", - "addr": "mkejl5jnqe4ovaow57l47hi46xbjcxnf54jriiwvs5rsxcxxjbo5b4yd.onion:9735" + "addr": "62.171.183.114:9735" } ], "color": "#3399ff", @@ -1012,6 +1050,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1032,27 +1075,26 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3" + "custom_records": {} }, { - "last_update": 1710850518, - "alias": "Knoten", + "pub_key": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "last_update": 1710829358, + "alias": "MUTATRUM", "addresses": [ { "network": "tcp", - "addr": "xetu2kpbvonq2urqpadclyzy76gwhdrkz6lgazz65j66leapw4bmpoad.onion:9735" + "addr": "95.168.173.100:9735" + }, + { + "network": "tcp", + "addr": "qxlc2q5enou7vjuaa2g243fkoqg6w24biqtr7zrqlxprdhj2tqzzbsad.onion:9735" } ], "color": "#68f442", @@ -1092,6 +1134,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1112,25 +1159,38 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + "custom_records": {} }, { - "last_update": 1710568425, - "alias": "Viva Bitcoin", + "pub_key": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "last_update": 1710856592, + "alias": "LNB\u03dfG [Edge-2]", "addresses": [ { "network": "tcp", - "addr": "2ti2gy74nlyjsgcs42wemjwd3skmndbbv776ufldkngbzmpo7tbsntyd.onion:9735" + "addr": "46.229.165.140:9735" + }, + { + "network": "tcp", + "addr": "[2a02:b48:207:2:9735:9735:1:2]:9735" + }, + { + "network": "tcp", + "addr": "mw2d3bedftuixrqgl4qsstqzz55k5g6ionuo2arab7bqyc2mxoihwsad.onion:9735" } ], - "color": "#ffff00", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -1167,6 +1227,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1192,20 +1257,30 @@ "is_required": false, "is_known": true }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + "custom_records": {} }, { - "last_update": 1710394153, - "alias": "", - "addresses": [], - "color": "#007f00", + "pub_key": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "last_update": 1710834273, + "alias": "not benthecarman", + "addresses": [ + { + "network": "tcp", + "addr": "185.150.162.100:9735" + } + ], + "color": "#f2a900", "features": { "0": { "name": "data-loss-protect", @@ -1222,9 +1297,9 @@ "is_required": false, "is_known": true }, - "8": { + "9": { "name": "tlv-onion", - "is_required": true, + "is_required": false, "is_known": true }, "12": { @@ -1247,11 +1322,26 @@ "is_required": false, "is_known": true }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, "45": { "name": "explicit-commitment-type", "is_required": false, @@ -1262,25 +1352,34 @@ "is_required": false, "is_known": true }, - "51": { - "name": "zero-conf", - "is_required": false, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + "custom_records": {} }, { - "last_update": 1710769806, - "alias": "EcoNode", + "pub_key": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "last_update": 1710856043, + "alias": "IMF_SUCKS", "addresses": [ { "network": "tcp", - "addr": "kpa3fv64oxsgl7ymsmu27t5wwe4vpx47wer3tr7fk5ateeskmoh6hiqd.onion:9735" + "addr": "91.117.204.128:9735" + }, + { + "network": "tcp", + "addr": "jz4ydh5i3aajj7ib7h4wupn7vjybegywioh7mwyrpssfwtjmowkxnaad.onion:9735" } ], - "color": "#3399ff", + "color": "#ffff00", "features": { "0": { "name": "data-loss-protect", @@ -1317,6 +1416,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1348,19 +1452,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3" + "custom_records": {} }, { - "last_update": 1707656697, - "alias": "02f5be5f3d54b66bf531", + "pub_key": "037ff12b6a4e4bcb4b944b6d20af08cdff61b3461c1dff0d00a88697414d891bc7", + "last_update": 1710846456, + "alias": "\ud83e\udd16RoboSats\u26a1DevFund\ud83d\udcbb\ud83c\udf75", "addresses": [ { "network": "tcp", - "addr": "vedvjiibjrbbaaegvjqvx4thfobbj7gs5scb5gtpvxyb3t423ao23sad.onion:9735" + "addr": "6ivt774h43velhemo4uhdcxdojnwt6k2vfxzpvr4ya4ivr77d522jtid.onion:9735" } ], - "color": "#3399ff", + "color": "#1976d2", "features": { "0": { "name": "data-loss-protect", @@ -1397,6 +1501,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1417,25 +1526,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + "custom_records": {} }, { - "last_update": 1710693115, - "alias": "lightning-roulette.com", + "pub_key": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "last_update": 1710487611, + "alias": "iaconide", "addresses": [ { "network": "tcp", - "addr": "34.65.32.189:9735" + "addr": "34wnmpnzr5an4volnsthuklsk7n6xl5v63dca6ffvtbzbmekddyw3wad.onion:9735" } ], - "color": "#11ff22", + "color": "#196fc5", "features": { "0": { "name": "data-loss-protect", @@ -1472,11 +1586,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1503,19 +1612,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" + "custom_records": {} }, { - "last_update": 1662699597, - "alias": "BambergCityNode5", + "pub_key": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "last_update": 1710807593, + "alias": "OpenNode.com", "addresses": [ { "network": "tcp", - "addr": "tfsmtkznl6spgnm5j3xxa6wmawvmi4ipgbvuazdngy6xqbigqdiojpid.onion:9735" + "addr": "3.132.230.42:9735" } ], - "color": "#cd6d5f", + "color": "#000000", "features": { "0": { "name": "data-loss-protect", @@ -1551,54 +1660,14 @@ "name": "multi-path-payments", "is_required": false, "is_known": true - } - }, - "custom_records": {}, - "pub_key": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" - }, - { - "last_update": 1710392585, - "alias": "", - "addresses": [], - "color": "#007f00", - "features": { - "0": { - "name": "data-loss-protect", - "is_required": true, - "is_known": true - }, - "5": { - "name": "upfront-shutdown-script", - "is_required": false, - "is_known": true - }, - "7": { - "name": "gossip-queries", - "is_required": false, - "is_known": true - }, - "8": { - "name": "tlv-onion", - "is_required": true, - "is_known": true }, - "12": { - "name": "static-remote-key", - "is_required": true, - "is_known": true - }, - "14": { - "name": "payment-addr", - "is_required": true, - "is_known": true - }, - "17": { - "name": "multi-path-payments", + "19": { + "name": "wumbo-channels", "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, @@ -1607,35 +1676,35 @@ "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "31": { + "name": "amp", "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "51": { - "name": "zero-conf", + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + "custom_records": {} }, { - "last_update": 1710817951, - "alias": "bblocker21", + "pub_key": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "last_update": 1710847535, + "alias": "nodorobico", "addresses": [ { "network": "tcp", - "addr": "l4nxfimtxjjerzewyb4ihp4jhiauw2madq3krdnqz4s3x52kmya76lyd.onion:9735" + "addr": "vbkuzms43v6wnwyq2v2exrlzimx3lzfbtvmprwv6q4w6zk23g3dzueid.onion:9735" } ], - "color": "#ffb233", + "color": "#000000", "features": { "0": { "name": "data-loss-protect", @@ -1703,10 +1772,10 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + "custom_records": {} }, { + "pub_key": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", "last_update": 1710847967, "alias": "Bitrefill", "addresses": [ @@ -1783,23 +1852,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "last_update": 1710767460, - "alias": "lnd.cryptoassets.co.za", + "pub_key": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "last_update": 1710856657, + "alias": "WalletOfSatoshi.com", "addresses": [ { "network": "tcp", - "addr": "197.155.6.194:9735" - }, - { - "network": "tcp", - "addr": "xiigglrrhl6gl55g6ndou5pkg7y7gxmln6bs6eov2w5od2xavdfukrqd.onion:9735" + "addr": "170.75.163.209:9735" } ], - "color": "#000000", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -1861,30 +1926,20 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + "custom_records": {} }, { - "last_update": 1710821651, - "alias": "OpenNode.com", - "addresses": [ - { - "network": "tcp", - "addr": "18.222.70.85:9735" - } - ], - "color": "#000000", + "pub_key": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "last_update": 1710392585, + "alias": "", + "addresses": [], + "color": "#007f00", "features": { "0": { "name": "data-loss-protect", @@ -1901,9 +1956,9 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, "12": { @@ -1926,42 +1981,41 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", - "is_required": false, - "is_known": true - }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "47": { + "name": "scid-alias", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "51": { + "name": "zero-conf", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "last_update": 1710836095, - "alias": "021cb32426ed1a6be953", + "pub_key": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "last_update": 1710845923, + "alias": "OLYMPUS by ZEUS", "addresses": [ { "network": "tcp", - "addr": "g2vldjytfdx4r2hdmgg3ccqrxczxgxl4e3w7rr3emvp4br4xcoo4void.onion:9735" + "addr": "45.79.192.236:9735" + }, + { + "network": "tcp", + "addr": "r46dwvxcdri754hf6n3rwexmc53h5x4natg5g6hidnxfzejm5xrqn2id.onion:9735" } ], "color": "#3399ff", @@ -2001,6 +2055,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2021,25 +2080,53 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + "custom_records": {} }, { - "last_update": 1710776023, - "alias": "Fastlightning", + "pub_key": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "last_update": 1710823738, + "alias": "lnmarkets.com", "addresses": [ { "network": "tcp", - "addr": "tspdnxsdk77b6wdly6lujqr724hoq23buuez3roy6xo5ausv3riisbqd.onion:9735" + "addr": "3.95.117.200:9735" + }, + { + "network": "tcp", + "addr": "172.81.0.102:9735" + }, + { + "network": "tcp", + "addr": "heoznd3w4xvmiiihykdhviegejnvhzgoyfzhucx5n2cgvlsp3fkpvpad.onion:9735" } ], - "color": "#3399ff", + "color": "#327aff", "features": { "0": { "name": "data-loss-protect", @@ -2076,6 +2163,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2096,25 +2188,34 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2" + "custom_records": {} }, { - "last_update": 1710845779, - "alias": "LightningPremium", + "pub_key": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "last_update": 1710838956, + "alias": "Boltz", "addresses": [ { "network": "tcp", - "addr": "5u3crvcbydktdcwsk5utapo5vvd7s2uzkn45u7kemusvgikwqydytjad.onion:9735" + "addr": "45.86.229.190:9735" + }, + { + "network": "tcp", + "addr": "d7kak4gpnbamm3b4ufq54aatgm3alhx3jwmu6kyy2bgjaauinkipz3id.onion:9735" } ], - "color": "#68f442", + "color": "#ff9800", "features": { "0": { "name": "data-loss-protect", @@ -2187,19 +2288,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + "custom_records": {} }, { - "last_update": 1648917357, - "alias": "mynodebtc.cooper", + "pub_key": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "last_update": 1710821053, + "alias": "flypdev", "addresses": [ { "network": "tcp", - "addr": "bh4ooqtd3mngj4vk3m3gpqd3isaywi67ed5iochdg3lhexzikc56epad.onion:9735" + "addr": "54.194.246.117:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2236,11 +2337,21 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, "31": { "name": "amp", "is_required": false, @@ -2257,19 +2368,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + "custom_records": {} }, { - "last_update": 1710852615, - "alias": "Decentralized", + "pub_key": "026f46207fd290a33cbd86e29b3ad0a47cdd44ab9aa5267cde66483e10aa9d3180", + "last_update": 1710847116, + "alias": "Authenticity", "addresses": [ { "network": "tcp", - "addr": "sgcq35suws6ft63egprwsncj357yt355rsa3aqqpjjimma4nfjhzdyqd.onion:9735" + "addr": "m3beyfsudvoujj6yrvx4ubvjqcxnq3cosanllxuidq55ezji2zwhw2ad.onion:9735" } ], - "color": "#61f5b2", + "color": "#ff5000", "features": { "0": { "name": "data-loss-protect", @@ -2306,6 +2417,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2326,6 +2442,16 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, "55": { "name": "keysend", "is_required": false, @@ -2337,23 +2463,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + "custom_records": {} }, { - "last_update": 1710551722, - "alias": "02abb5e57ff442770d9d", + "pub_key": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "last_update": 1710809830, + "alias": "bfx-lnd0", "addresses": [ { "network": "tcp", - "addr": "172.67.158.44:9735" - }, - { - "network": "tcp", - "addr": "rjufmivql2si355mfczbg2wirc24rnjngpo5hsotmabiafuewojkbdqd.onion:9735" + "addr": "34.65.85.39:9735" } ], - "color": "#3399ff", + "color": "#16b157", "features": { "0": { "name": "data-loss-protect", @@ -2421,19 +2543,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + "custom_records": {} }, { - "last_update": 1710852249, - "alias": "Bitrefill Routing", + "pub_key": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "last_update": 1710847816, + "alias": "LQwD-Canada", "addresses": [ { "network": "tcp", - "addr": "54.77.250.40:9735" + "addr": "192.243.215.102:9735" + }, + { + "network": "tcp", + "addr": "yit5nizyrgk4n2gx7hkadaas5iz33kn6wulr5vdndzpbae6tykqdw7ad.onion:9735" } ], - "color": "#ff001c", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2495,29 +2621,30 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "last_update": 1710811679, - "alias": "bleskomat", + "pub_key": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "last_update": 1710723427, + "alias": "fixedfloat.com", "addresses": [ { "network": "tcp", - "addr": "165.227.161.245:9735" - }, - { - "network": "tcp", - "addr": "5aggdumr4x3qkx5ptshggvrdkgaxkpsklw4nwmnsa6diby6vwcvccyad.onion:9735" + "addr": "185.5.53.91:9735" } ], - "color": "#3399ff", + "color": "#053456", "features": { "0": { "name": "data-loss-protect", @@ -2554,6 +2681,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2580,16 +2712,20 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + "custom_records": {} }, { - "last_update": 1710852256, - "alias": "Liaoning", + "pub_key": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "last_update": 1710279837, + "alias": "node02.fulmo.org", "addresses": [ { "network": "tcp", - "addr": "vdhhkanmkfr7x2mlkbo7lah6sh7mhwxqt7gv73m44pkfk4y537tg54yd.onion:9735" + "addr": "62.171.165.119:9735" + }, + { + "network": "tcp", + "addr": "4wstfgqbt347r4p5sq3yjrqhfntxlxh5y5ludah6wa6px7norowcgnyd.onion:9735" } ], "color": "#3399ff", @@ -2629,6 +2765,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2649,25 +2790,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + "custom_records": {} }, { - "last_update": 1710848892, - "alias": "shtyrlitz", + "pub_key": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "last_update": 1710602731, + "alias": "Brrrrrak", "addresses": [ { "network": "tcp", - "addr": "aswabsuhftuqagrvxlhzc5la73wj5skg7shp4vjpwgialbcgkceby6qd.onion:9735" + "addr": "63.33.31.120:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2704,8 +2850,13 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, @@ -2735,19 +2886,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + "custom_records": {} }, { - "last_update": 1710803334, - "alias": "FinanzielleFreiheit", + "pub_key": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "last_update": 1710854956, + "alias": "DinheiroReal", "addresses": [ { "network": "tcp", - "addr": "valdvygv5w6bfnza4yfshqsvljhv7grgyq4wgpke2jwvugfretiqwcqd.onion:9735" + "addr": "6yt5b3xefj6js2qc3skyuvo3t6t63wijxismzvt674gbl4pqonjtwnad.onion:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2804,29 +2955,38 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + "custom_records": {} }, { - "last_update": 1710805061, - "alias": "lndwr2.zaphq.io", + "pub_key": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "last_update": 1710815967, + "alias": "Voltage Flow 2.0", "addresses": [ { "network": "tcp", - "addr": "34.74.114.254:9735" + "addr": "52.88.33.119:9735" + }, + { + "network": "tcp", + "addr": "m33ascvukogaknee5w2tahp4xzhtnotx3a3m2ipkavxeggeiia2otqad.onion:9735" } ], - "color": "#3399ff", + "color": "#ff5000", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -2839,14 +2999,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -2864,45 +3029,50 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "25": { + "name": "unknown", "is_required": false, - "is_known": true + "is_known": false }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "47": { + "name": "scid-alias", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + "custom_records": {} }, { - "last_update": 1710856675, - "alias": "Civilization Phaze IV", + "pub_key": "03ff4d7532fc749c731de8bdac7226c06fdb82a458cd7ee557a6e355f5b359fd84", + "last_update": 1663608503, + "alias": "dm8try_btc.lnd [myNode]", "addresses": [ { "network": "tcp", - "addr": "ko4qvn23p2txecujtt5nv67mb2ikzhbebh2hs4rgfvonfpk2tqhux4id.onion:9735" + "addr": "qnbyr4dywmlb7iix76o4aoyng6grecaapqgzxs45x3x6t2jpxrljcjad.onion:9735" } ], - "color": "#ff8733", + "color": "#68f442", "features": { "0": { "name": "data-loss-protect", @@ -2944,11 +3114,6 @@ "is_required": false, "is_known": true }, - "27": { - "name": "shutdown-any-segwit", - "is_required": false, - "is_known": true - }, "31": { "name": "amp", "is_required": false, @@ -2959,34 +3124,33 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7" + "custom_records": {} }, { - "last_update": 1710771876, - "alias": "0209a06852f1257d70e5", + "pub_key": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "last_update": 1710616318, + "alias": "lightningcheckout.eu", "addresses": [ { "network": "tcp", - "addr": "94.130.220.96:9735" + "addr": "5.75.133.40:9735" + }, + { + "network": "tcp", + "addr": "mzxf2hwuajxgxobh3uohbr5xnzxluz4wqhzu55yqia2scjq4b7ahipqd.onion:9735" } ], - "color": "#3399ff", + "color": "#f2a900", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -2999,14 +3163,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -3019,45 +3188,69 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "19": { + "name": "wumbo-channels", "is_required": false, "is_known": true }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "29": { + "name": "unknown", "is_required": false, - "is_known": true + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false }, "45": { "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + "custom_records": {} }, { - "last_update": 1710586096, - "alias": "PERLY", + "pub_key": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "last_update": 1710850065, + "alias": "\ud83c\udfdb\ufe0fTempleOfSats\ud83c\udfdb\ufe0f", "addresses": [ { "network": "tcp", - "addr": "twtvqk7jiz3luzkt346q326u5owmo73lfn4yhhr5efskkr4autuf6vqd.onion:9735" + "addr": "128.199.66.191:36224" + }, + { + "network": "tcp", + "addr": "yqaj6k7qqfrzasjlunt2s4u35y7h2oc44lkgyobhe44xo5iy42iacmad.onion:9735" } ], - "color": "#9245ff", + "color": "#ce0b2d", "features": { "0": { "name": "data-loss-protect", @@ -3125,19 +3318,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + "custom_records": {} }, { - "last_update": 1700638994, - "alias": "UmbrelMCL", + "pub_key": "03db3386adf0e2221555a7f3fc7282eca60c5eabfed8e1789d591a9241c342c091", + "last_update": 1701328211, + "alias": "sanju", "addresses": [ { "network": "tcp", - "addr": "rlubaw7sxcaqmostf3e4x7yollfo4x2tzfwxrj3ssvxqusdes2q6k6yd.onion:9735" + "addr": "bwmdhddpb477aed2xdjdqathml3o7bxemsumgnl6tcxel7to7wop4pad.onion:9735" } ], - "color": "#3399ff", + "color": "#ffe233", "features": { "0": { "name": "data-loss-protect", @@ -3174,11 +3367,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3199,30 +3387,29 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + "custom_records": {} }, { - "last_update": 1710736174, - "alias": "Laika", + "pub_key": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "last_update": 1710839517, + "alias": "IBEX_Ops1", "addresses": [ { "network": "tcp", - "addr": "tezwktlb2f3ah2tbla6emehwic4eelwyxhycfvzgzmc7n6jxz33ozead.onion:9735" + "addr": "18.221.65.244:9735" + }, + { + "network": "tcp", + "addr": "umtpb5erx6v7ouvomeg33lxpryw5ufnet5zywuqx6piwrvo5jyrd3qqd.onion:9735" } ], - "color": "#300fb0", + "color": "#044900", "features": { "0": { "name": "data-loss-protect", @@ -3259,6 +3446,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3290,19 +3482,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + "custom_records": {} }, { - "last_update": 1710803050, - "alias": "lndeu0.zaphq.io", + "pub_key": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "last_update": 1710754632, + "alias": "sebastix", "addresses": [ { "network": "tcp", - "addr": "35.196.134.164:9735" + "addr": "fjqqno7cidbbfox4rrrpnvn4vikzhzh3kxk22si3ckhpuiuhdi3bssad.onion:9735" + }, + { + "network": "tcp", + "addr": "rrbmesbr5dhh3ru64c6dwjm7o4cevmuwf23wwuae3ge5drdbvwhzoiyd.onion:9735" } ], - "color": "#3399ff", + "color": "#ffa500", "features": { "0": { "name": "data-loss-protect", @@ -3364,19 +3560,29 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "last_update": 1710790037, - "alias": "0294774ee02a9faa5a58", - "addresses": [], + "pub_key": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "last_update": 1710821064, + "alias": "LateStageAustriaHungary\ud83c\uddfa\ud83c\udde6", + "addresses": [ + { + "network": "tcp", + "addr": "nzlqmwcfqlsrhbis7dyaeagiotutdl7dmbndpotacrhkagz5xtx6dyqd.onion:9735" + } + ], "color": "#3399ff", "features": { "0": { @@ -3414,11 +3620,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3439,53 +3640,54 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + "custom_records": {} }, { - "last_update": 1709977298, - "alias": "ACINQ", + "pub_key": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "last_update": 1710853712, + "alias": "The Nodestrich \u267e\ufe0f", "addresses": [ { "network": "tcp", - "addr": "3.33.236.230:9735" - }, - { - "network": "tcp", - "addr": "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion:9735" + "addr": "6ozfljaeyguxijydmjlnbfpsra2kf5oo44wn3mhwwtcudiopxuqcfwqd.onion:9735" } ], - "color": "#49daaa", + "color": "#8e30eb", "features": { "0": { "name": "data-loss-protect", "is_required": true, "is_known": true }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, "7": { "name": "gossip-queries", "is_required": false, "is_known": true }, - "8": { + "9": { "name": "tlv-onion", - "is_required": true, - "is_known": true - }, - "11": { - "name": "unknown", "is_required": false, - "is_known": false + "is_known": true }, - "13": { + "12": { "name": "static-remote-key", - "is_required": false, + "is_required": true, "is_known": true }, "14": { @@ -3498,11 +3700,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3513,40 +3710,40 @@ "is_required": false, "is_known": true }, - "29": { - "name": "unknown", - "is_required": false, - "is_known": false - }, - "39": { - "name": "unknown", + "31": { + "name": "amp", "is_required": false, - "is_known": false + "is_known": true }, "45": { "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "last_update": 1710849539, - "alias": "PeterJFrancoIII", + "pub_key": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "last_update": 1710651061, + "alias": "domesticblend", "addresses": [ { "network": "tcp", - "addr": "oyonslv2muhpngxl3yip3z7mt27auzlsx2luqydkxadludv2cva3thad.onion:9735" + "addr": "gn77lxmq52cphlsz33sajhuhgbgkp6gebcuciov2qkewfzmrzowqryqd.onion:9735" } ], - "color": "#3399ff", + "color": "#da1913", "features": { "0": { "name": "data-loss-protect", @@ -3583,6 +3780,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3603,25 +3805,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "last_update": 1710850693, - "alias": "Wallet Sat's", + "pub_key": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "last_update": 1710768282, + "alias": "RaspiPuitz", "addresses": [ { "network": "tcp", - "addr": "4a2tjhiyz3rqgpfvragnk4uszv32dytzhapen46igyjlyzfpgix5zeqd.onion:9735" + "addr": "a7gya4fc3hhnymx76izyjd6l2nubdntxpnbn2plprdvjfljzio4sxvad.onion:9735" } ], - "color": "#3399ff", + "color": "#68f442", "features": { "0": { "name": "data-loss-protect", @@ -3684,23 +3891,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + "custom_records": {} }, { - "last_update": 1710617991, - "alias": "RUSHBNOSTOP", + "pub_key": "036dcba93cfc2d7294e586a658c90313d44fb742d832f0f79eb6e4361bd0afa707", + "last_update": 1673104053, + "alias": "YIRMIBIR", "addresses": [ { "network": "tcp", - "addr": "maeiws6uq6baeuim5q6mcavn3xpn4kkpaa5rv7uqhrs6wubhccnkn7ad.onion:9735" + "addr": "g7gw2gbnbaijq2ss2n2y22ifgi2feoamwjwbg2s4gc7phmb7ahx57zad.onion:9736" } ], - "color": "#68f442", + "color": "#036dcb", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -3713,14 +3920,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -3733,8 +3945,8 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "21": { + "name": "anchor-commitments", "is_required": false, "is_known": true }, @@ -3743,7 +3955,91 @@ "is_required": false, "is_known": true }, - "31": { + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "last_update": 1710835939, + "alias": "Tatooine", + "addresses": [ + { + "network": "tcp", + "addr": "159.69.32.62:22409" + }, + { + "network": "tcp", + "addr": "2la7tplwej7zaacie62hz4bfvtnxfmm64bnrf4d6bdyyl3bnte6w7qad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { "name": "amp", "is_required": false, "is_known": true @@ -3753,22 +4049,27 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "last_update": 1710774982, - "alias": "sheesh", + "pub_key": "02ab80cce7e7dff00c964e4b2006eb4c8af4e71187572156a88275d3c126b90b66", + "last_update": 1707575341, + "alias": "albis", "addresses": [ { "network": "tcp", - "addr": "esowgqg2jhjzofvgkwdnyrozdvowvx7ymo3jsdh7oxxc74y7ga4egyyd.onion:9735" + "addr": "lycaicag653yzcehmguxdursh6k662xmcij5jnlavyyolvrvoph2esyd.onion:9735" } ], "color": "#68f442", @@ -3808,6 +4109,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3839,19 +4145,108 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "last_update": 1710855402, - "alias": "lndus0.zaphq.io", + "pub_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "last_update": 1709977298, + "alias": "ACINQ", "addresses": [ { "network": "tcp", - "addr": "34.138.228.220:9735" + "addr": "3.33.236.230:9735" + }, + { + "network": "tcp", + "addr": "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion:9735" } ], - "color": "#3399ff", + "color": "#49daaa", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "29": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03213c5175050fc1dfe0b4560c0cd5c6dd6bf4b25c2f11df0285384d0402e0d397", + "last_update": 1710821524, + "alias": "sats.mobi", + "addresses": [ + { + "network": "tcp", + "addr": "c2j5rin2zjhhsfe3oyj6p45vvczob33cvjqj55dwyltofh774s7hadad.onion:9735" + } + ], + "color": "#ff5000", "features": { "0": { "name": "data-loss-protect", @@ -3913,320 +4308,17689 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" - } - ], - "edges": [ - { - "channel_id": "821685930207739904", - "chan_point": "b8d52e4be913496dd31204b80a6214d30934fd77cf18d33f1f4b6f2b17aabc0a:0", - "last_update": 1710852888, - "capacity": "130000", - "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": false, - "max_htlc_msat": "128700000", - "last_update": 1710852888, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": false, - "max_htlc_msat": "128700000", - "last_update": 1710852885, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" - }, - { - "channel_id": "851482695379255296", - "chan_point": "57457180438919be15029ad7763d11eb46d33057fe9ab02ca001439b48350e97:0", - "last_update": 1710451930, - "capacity": "300000", - "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "297000000", - "last_update": 1710451930, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "50", - "disabled": true, - "max_htlc_msat": "297000000", - "last_update": 1706955502, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", - "node2_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + "custom_records": {} }, { - "channel_id": "852238059856199681", - "chan_point": "48442adc84d9f96ea82a15d24eb5eed6ea66a8b633296a7a0f7cd0adebbf4685:1", - "last_update": 1710705189, - "capacity": "500000", - "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710617530, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", - "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710705189, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", - "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + "pub_key": "0238268dc72b78f0bc19398b46916246723c013911286b91b61e7f78d7a9a90174", + "last_update": 1710602103, + "alias": "yangnamers", + "addresses": [ + { + "network": "tcp", + "addr": "ijnhxrqr2ekvbbh4qjtfjrganyerwtslldbnaav2xrp2y6bi2s3blrid.onion:9735" + } + ], + "color": "#023826", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "last_update": 1710783641, + "alias": "DiamondHands\ud83d\udc8e\ud83d\ude4c", + "addresses": [ + { + "network": "tcp", + "addr": "62.77.157.54:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "last_update": 1710764530, + "alias": "unatco2", + "addresses": [ + { + "network": "tcp", + "addr": "p2s2w5go4h4xvr644m67kjfatkckw6u2yabpigahnqo2fvolcwmo7jyd.onion:9735" + } + ], + "color": "#f130f9", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "last_update": 1710842679, + "alias": "Btc-Tide", + "addresses": [ + { + "network": "tcp", + "addr": "79.7.125.218:9735" + }, + { + "network": "tcp", + "addr": "jh52l3y7waf2f7djheq7fahrjlsveisxxumgu3iznofxn4eqdyi3ggid.onion:9735" + } + ], + "color": "#5f1be8", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "last_update": 1710633903, + "alias": "Skaloshi", + "addresses": [ + { + "network": "tcp", + "addr": "vv3smsw3cuvsjmkzv7bh45etuialxjjspwnt3b25qfswym4btdztmayd.onion:9735" + } + ], + "color": "#15b331", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "last_update": 1710754265, + "alias": "satsophone", + "addresses": [ + { + "network": "tcp", + "addr": "uagt7hb6nw2dkv3gy2madsj4uu3xlpe47en7gtr7pilrvcse4j4pc4yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "021aa6f0fdc8ea3c85b98aff27e5ffe1f67921e75e7aa7bad6a36d6085d54117df", + "last_update": 1710620720, + "alias": "021aa6f0fdc8ea3c85b9", + "addresses": [ + { + "network": "tcp", + "addr": "167.172.139.109:9735" + }, + { + "network": "tcp", + "addr": "iqitu5tyy2k4wljm67n4rwcjlriruzlsibc2llgn227stoy5jjf7o4ad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "022b0de0e9156024d3f1ba54b3602f21eae93c4087a552140d25805175d3d9a1d3", + "last_update": 1710854247, + "alias": "ninjasats440", + "addresses": [ + { + "network": "tcp", + "addr": "jd2w2yf7l76dbumpyvhk7ghc3soyoqfnzvsrnzcpf46vjjwqmybzinad.onion:9735" + } + ], + "color": "#f733ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "last_update": 1710848135, + "alias": "YTMND", + "addresses": [ + { + "network": "tcp", + "addr": "216.18.188.159:9735" + }, + { + "network": "tcp", + "addr": "[2001:18b8:0:100:0:b00b:420:69]:9735" + }, + { + "network": "tcp", + "addr": "jhyw24mdotsjpatyzmo23563n7fn26hjopavv6bmpn3xjta36bdadoyd.onion:9735" + } + ], + "color": "#ff7541", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "last_update": 1710856655, + "alias": "Megalithic.me", + "addresses": [ + { + "network": "tcp", + "addr": "164.92.106.32:9735" + }, + { + "network": "tcp", + "addr": "m563h7c5fdzq63d4znhkl2pbhskbf4meeybjgxmp7mkkp5j7osadgwad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "last_update": 1710851620, + "alias": "glr.com.py", + "addresses": [ + { + "network": "tcp", + "addr": "181.126.37.65:9736" + }, + { + "network": "tcp", + "addr": "qt3gn3lzwq5fe5obgo3y7ey3twiq43b7ifb57k666vulp2whe54rboqd.onion:9735" + } + ], + "color": "#5f04c0", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "last_update": 1710149277, + "alias": "Gondolin \ud83c\udfd4\ud83c\udfef\ud83c\udfd4", + "addresses": [ + { + "network": "tcp", + "addr": "ksdbl2gqtzcrkluuo2donqfifrdj5fqsl7ikyxjxbqta5k3my34ev5yd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "last_update": 1710459313, + "alias": "gameb_2", + "addresses": [ + { + "network": "tcp", + "addr": "3.234.251.85:9735" + } + ], + "color": "#ffdc00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "last_update": 1710811714, + "alias": "Ludwig", + "addresses": [ + { + "network": "tcp", + "addr": "170.75.171.98:9735" + }, + { + "network": "tcp", + "addr": "5fcvqllrm6szaiyxznlkto3qzoj56xeactxowiq5our3ge5zj366xjyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "last_update": 1710666852, + "alias": "MindlinerTre", + "addresses": [ + { + "network": "tcp", + "addr": "62.12.168.100:9735" + } + ], + "color": "#fec89a", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "6": { + "name": "gossip-queries", + "is_required": true, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "29": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "41": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "43": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "105": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "163": { + "name": "unknown", + "is_required": false, + "is_known": false + } + }, + "custom_records": { + "1": "029a00320064000000024c4b40" + } + }, + { + "pub_key": "02b91642f44270c805e275ae59289f35d464de20cd3a546e3252710db6bab7c965", + "last_update": 1710840726, + "alias": "02b91642f44270c805e2", + "addresses": [ + { + "network": "tcp", + "addr": "zhnlchha3msn7um6yqztklrr7nmq2wh5w6mygbca55rhxjcxu3gbadyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "last_update": 1710804164, + "alias": "i'llTryAnythingOnce", + "addresses": [ + { + "network": "tcp", + "addr": "23.88.103.101:40009" + }, + { + "network": "tcp", + "addr": "buqjxr6ha4oob7knppq2ky4urrtohruowpdpldjdapgn2k5gt2nx74id.onion:9735" + }, + { + "network": "tcp", + "addr": "b2abr4r5xjkxy32yj3eclnggyrywarau3cbc5tjshogqaopbdoywrayd.onion:9735" + } + ], + "color": "#d7cfad", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "163": { + "name": "unknown", + "is_required": false, + "is_known": false + } + }, + "custom_records": { + "1": "029a0078000a000005dc03e8" + } + }, + { + "pub_key": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "last_update": 1710671588, + "alias": "Lightning.Watch", + "addresses": [ + { + "network": "tcp", + "addr": "[2a01:4f9:2a:1787:0:999:0:2095]:8888" + }, + { + "network": "tcp", + "addr": "95.216.23.120:8888" + }, + { + "network": "tcp", + "addr": "4gjavv3jm3ftzhtpprygss3y3yajxu2m6zx3isrlgb3irk3mzgtluwqd.onion:9735" + } + ], + "color": "#00fff8", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "last_update": 1710820471, + "alias": "southxchange.com", + "addresses": [ + { + "network": "tcp", + "addr": "54.187.0.23:9735" + } + ], + "color": "#428bca", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "035430a842187f9d27d639b05151f2100fb3e8370a0375e3c1c0ca8257cbe054d4", + "last_update": 1709325261, + "alias": "btc21m", + "addresses": [ + { + "network": "tcp", + "addr": "fftz6ctxdnd2io7bhi3yodx2onuzf7t5teh5rvqzmfdggskxybx3kkqd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "last_update": 1710638255, + "alias": "0215a106e71380f6b273", + "addresses": [ + { + "network": "tcp", + "addr": "bed7wg2ujvvapyzlxd64ou2jekuclworvdei6fnc5axojdhu4evz3qad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "last_update": 1710839604, + "alias": "02b4f019af3b3fa23681", + "addresses": [ + { + "network": "tcp", + "addr": "4eizywhndgugjt7ki3wkc4lgjtrg5opzcvq5eoqe2yau7ro5n7yn5pid.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "last_update": 1710856659, + "alias": "lnd.measite.de", + "addresses": [ + { + "network": "tcp", + "addr": "84.172.88.215:9735" + }, + { + "network": "tcp", + "addr": "lmbxbzc4vj565r7eryrwvs52jq5vohyk5r6uqoceeebxhcs737tjr5yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "last_update": 1710544303, + "alias": "JK-2", + "addresses": [ + { + "network": "tcp", + "addr": "ag6t5wvmumibm55dk5kh7spclqv54hy6zykyn4o6hvmx6c3mg2cmm4id.onion:9735" + } + ], + "color": "#9a34ad", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "last_update": 1710750973, + "alias": "Fulcrum", + "addresses": [ + { + "network": "tcp", + "addr": "54.70.181.161:9735" + }, + { + "network": "tcp", + "addr": "xp6nnobrvd2e7ojthcjadkfmansjndiu5t6dtfly6jg3vqf3nqbbwiyd.onion:9735" + } + ], + "color": "#ff5000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02d8853c04b69ce55c93d574bbb614e31f2654fb2f4049483774792cfd8d9af732", + "last_update": 1710824156, + "alias": "Zorbito", + "addresses": [ + { + "network": "tcp", + "addr": "jplka734tcxesjfmqzdacati4o5amnjapotobcrvkhriwog2fb43ggad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "last_update": 1710657422, + "alias": "KryptoKragon", + "addresses": [ + { + "network": "tcp", + "addr": "hs2zrv6qidyx5uptdw5jhouhc2xt6vwzregjir7fechc4a35dazxkwyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "last_update": 1709327134, + "alias": "FREE PEERSWAPS", + "addresses": [ + { + "network": "tcp", + "addr": "140.82.21.181:9735" + } + ], + "color": "#03a465", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "6": { + "name": "gossip-queries", + "is_required": true, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "41": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "43": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "163": { + "name": "unknown", + "is_required": false, + "is_known": false + } + }, + "custom_records": {} + }, + { + "pub_key": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "last_update": 1710854344, + "alias": "sparksNode \ud83d\udca5", + "addresses": [ + { + "network": "tcp", + "addr": "w7ytfe7tms7ph7l6usancypopci254ztmlaqqwatpcu2pxjzxaj673id.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "last_update": 1710774723, + "alias": "Aerarium", + "addresses": [ + { + "network": "tcp", + "addr": "2svin2f6lssximwg53ydukjv33vix6mhlijqe5w5njodyuuby2s562ad.onion:9735" + } + ], + "color": "#f2a900", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "last_update": 1710749524, + "alias": "PaymentHub", + "addresses": [ + { + "network": "tcp", + "addr": "mbdkjteft7mej56utlstw4hr34br7lwy2mq7bowqxjy66vqrwwkbmkid.onion:9735" + } + ], + "color": "#e11a1a", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "last_update": 1710856043, + "alias": "LNB\u03dfG [Hub-1]", + "addresses": [ + { + "network": "tcp", + "addr": "213.174.156.66:9735" + }, + { + "network": "tcp", + "addr": "[2a02:b48:207:2:9735:9735:0:1]:9735" + }, + { + "network": "tcp", + "addr": "qimt6abvc2iuexwrtl5tzyrygnu7mshjahvresve5hdli6nstdg7elyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "last_update": 1710736050, + "alias": "Moon (paywithmoon.com)", + "addresses": [ + { + "network": "tcp", + "addr": "52.86.210.65:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "last_update": 1710822085, + "alias": "QuantX", + "addresses": [ + { + "network": "tcp", + "addr": "45gmsvweqmvnms7w4jznv562ghnaubp7xm5c4surhnxmrekw6vlya5ad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "last_update": 1710623633, + "alias": "Arolla", + "addresses": [ + { + "network": "tcp", + "addr": "izpqo6qaiqu6ap4t6ylvo32bykms3li5tly3f26dkxeny5vgsyleikid.onion:9735" + } + ], + "color": "#4c0099", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "last_update": 1710856651, + "alias": "Friendspool\u26a1\ud83c\udf7b", + "addresses": [ + { + "network": "tcp", + "addr": "179.225.221.69:9735" + }, + { + "network": "tcp", + "addr": "uyf6gnzsf6nh5abwgktxdry3xcknigyzeulf4qiz5yn7mhzwhtppriqd.onion:9735" + } + ], + "color": "#15ae4d", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "last_update": 1710855943, + "alias": "\ud83e\udd81Bayer\ud83e\udd81", + "addresses": [ + { + "network": "tcp", + "addr": "2anl2m7xjazcgxskypq3upixmytqph4j7uhy5bx4y5up3kzdfh64zwqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "last_update": 1710654812, + "alias": "Crypto Lover Couple", + "addresses": [ + { + "network": "tcp", + "addr": "46.101.122.181:29041" + }, + { + "network": "tcp", + "addr": "36zm4aba2z77zzolft6uscugroxdfkvgbbpjoqj2grtx3avaxu4dmoqd.onion:9735" + } + ], + "color": "#00ff41", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "last_update": 1710821651, + "alias": "OpenNode.com", + "addresses": [ + { + "network": "tcp", + "addr": "18.222.70.85:9735" + } + ], + "color": "#000000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "last_update": 1710855272, + "alias": "SCP-096", + "addresses": [ + { + "network": "tcp", + "addr": "nnl5wxs3bjlm7yrqbmw4y3wjsas2mr7xw7km3gwvascgti6sbltbfpyd.onion:9735" + } + ], + "color": "#096096", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "last_update": 1710825844, + "alias": "bitcoin01", + "addresses": [ + { + "network": "tcp", + "addr": "5.75.184.195:14974" + }, + { + "network": "tcp", + "addr": "yiqo24vutzl62g7zvy2fzktquiipcstx4p6pzih5ufi7ds3yzhze5uid.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "last_update": 1710855551, + "alias": "Node Up or Shut Up II", + "addresses": [ + { + "network": "tcp", + "addr": "159.223.176.115:21845" + }, + { + "network": "tcp", + "addr": "fhhz77ifwvymzeq4gzrjjsus74a6zdstv224lae5qyqlgsjthub3aiqd.onion:9735" + } + ], + "color": "#2199ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "last_update": 1710855402, + "alias": "lndus0.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "34.138.228.220:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "last_update": 1710856653, + "alias": "BitcoinVoucherBot-X", + "addresses": [ + { + "network": "tcp", + "addr": "ks545foo7bl57qldqqkh3alshu27dtkocqwp35zm4i5iblophgwm3vyd.onion:9735" + } + ], + "color": "#ff5000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "last_update": 1710181560, + "alias": "GG", + "addresses": [ + { + "network": "tcp", + "addr": "jwesmeaxdm4ydj5s46waffpwekiwcbvspos3snhtjg3nxqqiugs7joyd.onion:9735" + } + ], + "color": "#b00b69", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "030066a41ce86e73e96ffcbc3cf445744d5c92144e0c254c421a0204aebbdbb2bc", + "last_update": 1665576046, + "alias": "030066a41ce86e73e96f", + "addresses": [ + { + "network": "tcp", + "addr": "n7n4dimdw7ikvdcl5lp7xwx3rtxthi5hikxoqlxcpeujlhjjmehp4pad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "last_update": 1710804163, + "alias": "tippin.me", + "addresses": [ + { + "network": "tcp", + "addr": "13.113.39.53:9735" + } + ], + "color": "#ff4681", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "last_update": 1710723270, + "alias": "Blockstream Store", + "addresses": [ + { + "network": "tcp", + "addr": "35.232.170.67:9735" + }, + { + "network": "tcp", + "addr": "giexynrrloc2fewstcybenljdksidtglfydecbellzkl63din6w73eid.onion:9735" + } + ], + "color": "#02df5f", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + } + ], + "edges": [ + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "620141050860732416", + "chan_point": "51f9e44982900447e0a1974a8750c4505962f9619aaff8dade1ef3f9847ad1d3:0", + "last_update": 1710827563, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710785048, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710827563, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "688771467187060737", + "chan_point": "b1291dec49d7c853927b6c9cc0080a2bdd1794e43d397ac82c1910faed89e8ef:1", + "last_update": 1710811629, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710811629, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 30, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1700", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1682692390, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "701149769035481088", + "chan_point": "a0bae4458e28736b64c0335d7f2108b6ee8c1070ddf09949c2fd1b5b27a90c29:0", + "last_update": 1710844897, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "500000000", + "last_update": 1710844897, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710806238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "701231132948234240", + "chan_point": "f65a27eed21a5d4ee23466e7ba6960971c8f85ec3c2d19edf99cbb018a785401:0", + "last_update": 1710855697, + "capacity": "800000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "20", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1710855697, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1710715963, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "701275113349120000", + "chan_point": "b6fb3aee05bffaa82bdaa6b5197c971beced63c27e371a6e888f417df0336787:0", + "last_update": 1710769297, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710769297, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 30, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1700", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710541727, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "715544575268356096", + "chan_point": "3108d456a2138600b302fc82be2dda3b1aea982a49ffcadeb6c25fdbb34aa6ac:0", + "last_update": 1710839931, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839931, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710773829, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035430a842187f9d27d639b05151f2100fb3e8370a0375e3c1c0ca8257cbe054d4", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "721567699888766976", + "chan_point": "33664578a67d720dd169aeb3a77928c7176a1fd385f84f10e768fe9e6d8d9ca9:0", + "last_update": 1710832963, + "capacity": "800000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1709162031, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": true, + "max_htlc_msat": "792000000", + "last_update": 1710832963, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "722185625535643649", + "chan_point": "f67e8d32b71600f0d2f73dde57215a4c38911e23cf8d0c0c1be55647fe52bf80:1", + "last_update": 1710817031, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710772420, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710817031, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "722285681141153792", + "chan_point": "722aca928ff65b4ce522972bbb06ff3663bff398f970f28bea886a5800e0832f:0", + "last_update": 1710808897, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710808897, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710714820, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "723662269581033472", + "chan_point": "ff2ea32c7db8d11d34e06235732449810ca3e1fa19efeea45375a11652904bc8:0", + "last_update": 1710786481, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710729220, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710786481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0238268dc72b78f0bc19398b46916246723c013911286b91b61e7f78d7a9a90174", + "channel_id": "739309419534417920", + "chan_point": "5f4e8328eb811a5d19ad685fa8e21373dcb2495873aea913c8ebab0b98b5d0d8:0", + "last_update": 1710822429, + "capacity": "4007237", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "7997", + "disabled": false, + "max_htlc_msat": "3967165000", + "last_update": 1710256181, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4007237000", + "last_update": 1710822429, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "744180256119783424", + "chan_point": "b74b95fceca412ebecf4bd672197af3be7c684f0e73c2af8574a350bac496d48:0", + "last_update": 1710855697, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710855697, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710540707, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "744329789687267328", + "chan_point": "fd22f30704401a9445dc98642e28867f28d1fae4c99197db15e79898bcf65883:0", + "last_update": 1710826897, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710804724, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710826897, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "035430a842187f9d27d639b05151f2100fb3e8370a0375e3c1c0ca8257cbe054d4", + "channel_id": "744640951516135424", + "chan_point": "a77c6ee8630e93062245f2a2bdbb687b7d6e92e63880d49dd23853ec1db44ad6:0", + "last_update": 1710536713, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1709328274, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": true, + "max_htlc_msat": "99000000", + "last_update": 1710536713, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "node2_pub": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "channel_id": "746389174898458624", + "chan_point": "e5f14c45f306e9969d1d43a68582540e41c506174d7f49cd00c5375c4f038067:0", + "last_update": 1710779200, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710779200, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "9", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710773240, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "749958189634486272", + "chan_point": "dce410d1f7f7bb1ff7fc14422e94859097c7ce9760b3e55198e85518318b9265:0", + "last_update": 1710852943, + "capacity": "12309789", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "12186692000", + "last_update": 1710852943, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "12186692000", + "last_update": 1710842221, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "750026359397154817", + "chan_point": "a594d8ec35606a44907f73d5e55cefedfca340f342ed1d6d9d6f7ff9c30277e0:1", + "last_update": 1710810322, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710810322, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710750913, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "channel_id": "756620130651340800", + "chan_point": "411123b7fea933ddb7c8d7160ee0fd6605fbda7e0a846124b02c74468b9db635:0", + "last_update": 1710817552, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1250", + "fee_rate_milli_msat": "155", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "935", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710802531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "758001117307863040", + "chan_point": "c7b275977af7a733df5770f5198a71aca908636350aa4ce9bd652c958d150adc:0", + "last_update": 1710842131, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710786343, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710842131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "758288089872334849", + "chan_point": "4a6286a1ed3704c87127ab09e4c02b271708692277822b53228c7f53a37460e2:1", + "last_update": 1710842620, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "4000", + "fee_rate_milli_msat": "70", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710842620, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710560636, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "758424429205323776", + "chan_point": "3591fa9a442c632e3ad1e3ec237a2a2dc1e4863e8bef1bf7b4d96d4775984f08:0", + "last_update": 1710856924, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710856924, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "200", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710779620, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "758660824200314880", + "chan_point": "dbc44c5f794126476de6dea961563903fb73d343127a969b441f36ee8970f262:0", + "last_update": 1710771931, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710651913, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "895", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710771931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "759079738155204608", + "chan_point": "596431238183ba525a8d51a2cceda27aa918cbc3dde56da36f9af7867094502c:0", + "last_update": 1710817552, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "228", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710786481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "760095686837338113", + "chan_point": "e71273fda83b976b718b6ae1e0fd345efcdf0372b328e6c2ee51093724284376:1", + "last_update": 1710806734, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "198", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710795430, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "761152317513072640", + "chan_point": "292765512abde4f0bea1ab8b99ff50cfb783a4115521165a39d8c1238b3d6266:0", + "last_update": 1710584731, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710561936, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3985", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710584731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "764089113104547840", + "chan_point": "b02a5d74c012604c1a39b5b6bbc79f992eed3b37ff22badeb4b76c39e939dff2:0", + "last_update": 1710828220, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710828220, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "13000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "715", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710631531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "764533315781001216", + "chan_point": "4c1c1da13e6101b9fb9b2df46d612f7e12994ec91521f455f20641f35736fd4e:0", + "last_update": 1710781948, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710781948, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "9000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710777724, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03ff4d7532fc749c731de8bdac7226c06fdb82a458cd7ee557a6e355f5b359fd84", + "channel_id": "766235359892471809", + "chan_point": "354971270acb1ee8720e098b8fc9c2a3dd7b15b9e8699f7f5cd842e62e51b1cc:1", + "last_update": 1710708531, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "990000000", + "last_update": 1710708531, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1663586903, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "769044612082958336", + "chan_point": "245cf3989871e89fe2ddc7920745e128483a700740b7ca41121dcab359377901:0", + "last_update": 1710781450, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710781450, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710753931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "node2_pub": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "channel_id": "772235394777808896", + "chan_point": "a6d336e928d17adee83056a7d197535af1726cf3472d3186ca8d274040cd923e:0", + "last_update": 1710796663, + "capacity": "1050000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1033507000", + "last_update": 1710439360, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1039500000", + "last_update": 1710796663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "772480585848061953", + "chan_point": "ba90c6dab9a8c1566510af082dfaa0f9fc9b52f08f3bf154a1b804006ff62389:1", + "last_update": 1710852935, + "capacity": "89000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "88110000000", + "last_update": 1710836821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "88110000000", + "last_update": 1710852935, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "773543813641273345", + "chan_point": "0e82cab15a220e7ccee1e1768a24ab0f70599531f6f70a9c9e06bb4934cc2f4a:1", + "last_update": 1710836328, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710752454, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "8", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710836328, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "773691148107710465", + "chan_point": "bf279306711674bc1912dee89c39bf5ae856a6cb93c00d0e0878eefed366e1fb:1", + "last_update": 1710120872, + "capacity": "800000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1706625249, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "340", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710120872, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "774886317251231745", + "chan_point": "99a561caf4ca8475fcdd8b53a2ea0e7c60cba61a204dfd5292f5a13e187668ef:1", + "last_update": 1710610081, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "640", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1709929677, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": true, + "max_htlc_msat": "16609443000", + "last_update": 1710610081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "775701055451299841", + "chan_point": "d166a84cdd3d7808ce364e1e546c11f699838d88ee10c21ef26cbea63815acff:1", + "last_update": 1710849420, + "capacity": "9242005", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1200", + "disabled": false, + "max_htlc_msat": "9149585000", + "last_update": 1710813366, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "9149585000", + "last_update": 1710849420, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "780701634360573953", + "chan_point": "788f904a9ac3f959c608bd6bafd6db79674ac080469e73d8968799836e8d2ae6:1", + "last_update": 1710806564, + "capacity": "2501000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "2475990000", + "last_update": 1710806564, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "480", + "disabled": false, + "max_htlc_msat": "2475990000", + "last_update": 1710038727, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "781361341298442241", + "chan_point": "ebd7caf7f989304f073345dd5724946b950f94bbc1dbef1974ee1146d6d23fa7:1", + "last_update": 1710780040, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710779135, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710780040, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "782331110584549377", + "chan_point": "bf2255817b69dc1ab29b89ad9dc4c076a306466d8061364695fc371ab23e501e:1", + "last_update": 1710720579, + "capacity": "18000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710720579, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710720579, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "783190928622419969", + "chan_point": "61633c6615cb1eb0a6972058258565a64d5c3f9a73464b11b3b4bfa49e17a014:1", + "last_update": 1710772997, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710757535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710772997, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "channel_id": "784059542792896515", + "chan_point": "44fee5d5755730f18c7e83062a5dbd2e71c70d4810848d8a345c2174e3c6e0cc:3", + "last_update": 1710779065, + "capacity": "5000001", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "24", + "disabled": false, + "max_htlc_msat": "4950001000", + "last_update": 1710773240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950001000", + "last_update": 1710779065, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "784409187488563201", + "chan_point": "59810083dd5f98adf1d5772c1e33b033d83429ece66c9d2ddef94b8832b1dbc3:1", + "last_update": 1710806734, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "197", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710794122, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "784923758971518977", + "chan_point": "cfd4b66a48df7e6e59ef255110b3f9e888b1dd82ed576fc67bb1d5591c9c86b2:1", + "last_update": 1710795421, + "capacity": "4500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "4455000000", + "last_update": 1710795421, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "197", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710794134, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "785205233929355265", + "chan_point": "cf1960c00d14fe723cf0255176a9bde30411789c30386ebf61f399af46bc14bc:1", + "last_update": 1710655534, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "97", + "disabled": false, + "max_htlc_msat": "3600000000", + "last_update": 1710655534, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "3960000000", + "last_update": 1710609480, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "785553779142426629", + "chan_point": "0cede6d3919f2e6bc4eb84811b3afe4d6e2c252843d1880708d97e9dfd35aff7:5", + "last_update": 1710855660, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "16", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710855660, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "789199759664087041", + "chan_point": "350139d94a9922a5b6da9e38617efbe8525fb23330df1e06b3d3c324561a5f6d:1", + "last_update": 1710838531, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710837054, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1975", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "channel_id": "789307511875502081", + "chan_point": "ce5998eb75e3442113c4f826ac241132534a02dc6e044a9fa96f3bd8ae6cc906:1", + "last_update": 1710811121, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710811121, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710788713, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "789538409204154370", + "chan_point": "47c306bb98b25779f0c09076c3920da5a2f0026a2c2e10baecd105b68834634d:2", + "last_update": 1710818239, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "91", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710818239, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1709312781, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "789757212095741952", + "chan_point": "65a50b8b2c6d4a7927c9fb21cd6d2d40fd31934f096f72cc751db053e2b04d2c:0", + "last_update": 1710839400, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "99", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839400, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710752120, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "790669806689648640", + "chan_point": "82ee77f42f481281c6d44f1f3b8f1f1c33355cd2817e25ec801bd2ff70c1431c:0", + "last_update": 1710609931, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "64", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1709915277, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "915", + "disabled": true, + "max_htlc_msat": "5940000000", + "last_update": 1710609931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "791507634547523585", + "chan_point": "33c4d97d7baf5e5eb1932e89225a812e33eb774245596e3f921eee0d86404365:1", + "last_update": 1710810322, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "75", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710810322, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1635", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710809731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "791756124250832900", + "chan_point": "ee1558769eb0eca1f6c5a1767bf3a8907b656a4b304a2e2df9ed462a7fd73c4c:4", + "last_update": 1710773731, + "capacity": "2653177", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "2626646000", + "last_update": 1710681712, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1385", + "disabled": false, + "max_htlc_msat": "2653177000", + "last_update": 1710773731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "791977126006685697", + "chan_point": "2a95a80acade5c17e48d9d459e71dd53ec5ccd4d8f097ebd4d388c1a9ca3f626:1", + "last_update": 1710796240, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710796240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "5445000000", + "last_update": 1710780607, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "792265198100545538", + "chan_point": "0213392bcbab4832e1407f89db7840290f46eb0d13b4d331bb1076ca762d9d63:2", + "last_update": 1710772081, + "capacity": "2420637", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "2420637000", + "last_update": 1710772081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "2396431000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "792920507072315401", + "chan_point": "65263a90b7a4540889cddc01e52394a8fb7e40ea20b31ce8014c2f9408f198b1:9", + "last_update": 1710804913, + "capacity": "5783984", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "5726145000", + "last_update": 1710681712, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "5783984000", + "last_update": 1710804913, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "793525238406971393", + "chan_point": "caadc0698b6056125abea9442a5eb176b1a8a618c167af61444003cb9699656c:1", + "last_update": 1710812440, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1708742048, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710812440, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "793978237322330113", + "chan_point": "33c4b8a2f9d6f9454ade27fa3b6eb1f87055e15f181145068da8b42d7775da2e:1", + "last_update": 1710786429, + "capacity": "12000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1536", + "disabled": false, + "max_htlc_msat": "11880000000", + "last_update": 1709945877, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "11880000000", + "last_update": 1710786429, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "794937011285655566", + "chan_point": "d89889cb7682308568524adb82717c87752ecafc7a00ecf5466c6def71762b12:14", + "last_update": 1710714007, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "4950000000", + "last_update": 1710614040, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2722500000", + "last_update": 1710714007, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "795094241517633537", + "chan_point": "076eadbbef31ce6fc3a220c7710ac7dd0e2decc2a44760db6c38c89f19295d35:1", + "last_update": 1710814009, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "114", + "disabled": false, + "max_htlc_msat": "300000000", + "last_update": 1710814009, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1707761788, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "795647295890849793", + "chan_point": "f3e568ad6cc71f29d594ad8b140d86285eb2438ab37b95f60fb486c1a4913282:1", + "last_update": 1710827830, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710815204, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710827830, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036dcba93cfc2d7294e586a658c90313d44fb742d832f0f79eb6e4361bd0afa707", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "796153071199649794", + "chan_point": "202a07531f018476a31d6cb53d44defd5940f3339b3bc0aa87239ac70e71a464:2", + "last_update": 1710833524, + "capacity": "1108125", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": true, + "max_htlc_msat": "1108125000", + "last_update": 1710833524, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "4", + "disabled": false, + "max_htlc_msat": "1097044000", + "last_update": 1697545004, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "797418609082433536", + "chan_point": "f7243b067bf35ac4514abba7f03397c3a2eed1b4d7a59884da02e41d15731d26:0", + "last_update": 1710806734, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "198", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710793681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "797748462667366401", + "chan_point": "da6638149f2fd5008740e24d534973207868dddad4eaa5fa6e63e5f7079e3685:1", + "last_update": 1710827830, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710827830, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710810322, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "798970020045848578", + "chan_point": "eac6beb07894a0fa465f1eff2d7aa54123331bf6d2f45172d66005b7f18b7d16:2", + "last_update": 1710708676, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "9999", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1709637423, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": true, + "max_htlc_msat": "4455000000", + "last_update": 1710708676, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "799250395424751617", + "chan_point": "a061ad2ecc55e55ea2d822218cd520128347bf1141b1199dd0e9f52f49e1a0a9:1", + "last_update": 1710852931, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710836821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1435", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710852931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "799701195284021248", + "chan_point": "d2c2328c6966c3109d6f65dff475a8bcbcf52a34ba7ea33742768d352043bc3f:0", + "last_update": 1710847301, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790869, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710847301, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "800123407672016896", + "chan_point": "9940e2aab0c5583f7a3499d09e769151f807040d1c3dcd758c5f8cba8337a05e:0", + "last_update": 1710796901, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710796901, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790503, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "800921653136850946", + "chan_point": "a8fd2db7a9fe5808c3e2ef823a6cb3d32e0489d12d49662271a78645cd6e5c07:2", + "last_update": 1710702089, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "250000000", + "last_update": 1710702089, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "801269098805985284", + "chan_point": "b49f6e0e627b9d818ce6b2e44694221e130d4d6e04b30f3a8a74120bd5f8ec05:4", + "last_update": 1710796901, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790875, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710796901, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "801355960168546304", + "chan_point": "ee3ee6f2e532c6d79fe9af90e4ca39fc6cc66fbc14208ec272f8d387c35f3365:0", + "last_update": 1710786952, + "capacity": "20000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710786952, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710717263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "802335625163309057", + "chan_point": "7eda2bb26517138b63413014089750f438bb70dd7e985f6c37c154897f9c53e8:1", + "last_update": 1710822553, + "capacity": "8100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "8019000000", + "last_update": 1710822553, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "8100000000", + "last_update": 1710814347, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "802866689216413699", + "chan_point": "0d4441935389214c0842df3d726bc9815f204bff89d5163717d721a764ebcee3:3", + "last_update": 1710791593, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710791593, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "998", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710791405, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "803464823554441216", + "chan_point": "af5690e2b228bff347e81d03deefb5ba1702fed8cf9438431a5ba577d54f5c24:0", + "last_update": 1710807040, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1709912076, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "980000000", + "last_update": 1710807040, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "803464823558176768", + "chan_point": "4ddde3c9c96fc04cc5a0b54fa979f6eee573c3c62bab2c13f3b600dad1e544a4:0", + "last_update": 1710801358, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710801358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "71", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710729322, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "803830960949624833", + "chan_point": "2eaf9f68f25532373b0bd0b759f717ec5e7bc40635046dcc6b644b7d85e0a1fb:1", + "last_update": 1710825931, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710719754, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1975", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ab80cce7e7dff00c964e4b2006eb4c8af4e71187572156a88275d3c126b90b66", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "803837558058713088", + "chan_point": "e03e1e5242eeb60a2aa836aea1b13695f86a38c4e828892cb0e7d4072d72b530:0", + "last_update": 1710838129, + "capacity": "999888", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "989890000", + "last_update": 1707685141, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "989890000", + "last_update": 1710838129, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "805329595270955009", + "chan_point": "bd0f1a6ad4311bda84a000a0fce87d2a13ee5a77ba9748bcc51910bbdfe5bac3:1", + "last_update": 1710847531, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710791331, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "475", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710847531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "805783693574995969", + "chan_point": "3bff0680981d6c2d3c677de40f607832c7cb23f7ba7f584f05b79319b2062a94:1", + "last_update": 1710794663, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710794663, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "735", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710788131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "806033282704408580", + "chan_point": "b277272a004f49af3463a4644090bb83c9b8d91703fccdd3918a3f9542692706:4", + "last_update": 1710734649, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "107", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710734649, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "8000000000", + "last_update": 1710657542, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "channel_id": "806638014049681412", + "chan_point": "9db0a59700a59ac3084d832c2c207d4b188478212edc09591b0a37ec4be51b4c:4", + "last_update": 1710839839, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710785158, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "94", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710839839, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "806738069744975873", + "chan_point": "e7e7789109224cebd2366587ca8092bbc0c346aaad8007599682660e2d63af3e:1", + "last_update": 1710849672, + "capacity": "18100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "888", + "disabled": false, + "max_htlc_msat": "17919000000", + "last_update": 1710403944, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "18100000000", + "last_update": 1710849672, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "807119600233152513", + "chan_point": "7757d9e9fd758cb20972c70bec8dc391436a2ddfd4d84fdcf84c98f4b888340e:1", + "last_update": 1710806758, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710806758, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710752129, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "807581395061833734", + "chan_point": "5f7b6de8466907c8c546787d032cb4fa53fee90d80e976a72712606d37ab675a:6", + "last_update": 1710854501, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790852, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710854501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "807616579452010497", + "chan_point": "e5d06a7863e10a94c821e1d06b36363ae9448233d62cf4e3beb70a4ef7ebf9c9:1", + "last_update": 1710655516, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "777", + "disabled": true, + "max_htlc_msat": "9999000000", + "last_update": 1710655516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "41", + "disabled": false, + "max_htlc_msat": "10100000000", + "last_update": 1710635722, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "808307072735903745", + "chan_point": "d649d302b4d9c390b5105d37b87b3386d55123c161f8b6ddecc42b30007c80d1:1", + "last_update": 1710815204, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2000000", + "last_update": 1710815204, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710752221, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "808473098955456512", + "chan_point": "8d9721d1be96c3f43efa06f1ae68b90e1dce339b9a0874d5d34e4dcd2106c834:0", + "last_update": 1710820522, + "capacity": "60000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710820522, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1699766688, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "808896410953383937", + "chan_point": "720323d7116dd77a538b2f4c15ada94a7c3a8f7ad7cb4e32db8f63b852755d0b:1", + "last_update": 1710806131, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "9900000000", + "last_update": 1710655517, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2435", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710806131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "808898609969102848", + "chan_point": "a723e097119278df2c3b0206c2d5ef13132dc943a7261ee3969c74b125b8e614:0", + "last_update": 1710800407, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "5445000000", + "last_update": 1710800407, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710797131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "810368657035362304", + "chan_point": "1ad351dcf4c3b6ca2aaf36604924067b3f9bb75d8a5db769c6809a884dea904e:0", + "last_update": 1710845063, + "capacity": "20000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710700997, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710845063, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "811119623527333903", + "chan_point": "c5f317747bede72f3396da01c89223226cb7810bba15224d0d75c8939124fdd3:15", + "last_update": 1710851454, + "capacity": "60000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1799", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710752758, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710851454, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "813320845707902984", + "chan_point": "6e558eb7da9d2f5c36747a07dc9879f7daa3ff804ccdd66fd903fa7872de796b:8", + "last_update": 1710811039, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "297", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710811039, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710810322, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "814367580879912961", + "chan_point": "1124dc3acc435e21baaa8aaa7d6c2cbb29a6f079c9ea6dc42099abb70c5957ab:1", + "last_update": 1710845428, + "capacity": "1300000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "459", + "disabled": false, + "max_htlc_msat": "1300000000", + "last_update": 1710845428, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710726786, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "814641359296331777", + "chan_point": "f2d4029d0996f11ca6647c02927ced47984202f5751ce87f1e32c8bbfa9db9a1:1", + "last_update": 1710849758, + "capacity": "1200000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "243", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710845430, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710849758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "02d8853c04b69ce55c93d574bbb614e31f2654fb2f4049483774792cfd8d9af732", + "channel_id": "814898644921352193", + "chan_point": "35b562152952cac28125638260d881e8506aa32ee172b0fc207765b3ab90e772:1", + "last_update": 1710847556, + "capacity": "10096408", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9995444000", + "last_update": 1710847556, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9995444000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "815290071134175232", + "chan_point": "29953ec6177517ecdd5cfa11e4628f0a3f6d58192b9378d370f148485664b349:0", + "last_update": 1710790558, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710790558, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710784534, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "815498978337095681", + "chan_point": "e0d844875750d25b0d241035abcfc8f8afa180c12d7ecd2e028f16e32fad4e06:1", + "last_update": 1710827831, + "capacity": "119810484", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "118612380000", + "last_update": 1710827831, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "118612380000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "815763960674189313", + "chan_point": "ea6b1b28400c8538a21f46b4dcc6c0c73581c49a3bf184483ad29e97c2057f7f:1", + "last_update": 1710836563, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710624431, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710836563, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "816879965000040450", + "chan_point": "eb8a7198857d38749735eee23e8eaf06f50412e708342ddd1e575f780a8b679f:2", + "last_update": 1710854901, + "capacity": "4900000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "55", + "disabled": false, + "max_htlc_msat": "4851000000", + "last_update": 1710854901, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "77", + "disabled": true, + "max_htlc_msat": "4900000000", + "last_update": 1710718926, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "817004209678909440", + "chan_point": "57cf522de8800edbf8256dc00576c13ce2b4d99e239395884ec16674c01e7814:0", + "last_update": 1710780632, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "672", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710780632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710457811, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "817314272016531457", + "chan_point": "b81013d0591a53ec0f54eda76cf275443e1a1e064dcb25bcb80f28fd5b35729b:1", + "last_update": 1710856501, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "90", + "fee_rate_milli_msat": "9", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856499, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "818579809851408384", + "chan_point": "1f4e9fd59d7993bc888e0b2ee6d22b76e0f46f3d9184db144c3ea8ddc8d5fccf:0", + "last_update": 1710818407, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710711542, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "15592500000", + "last_update": 1710818407, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "818992126750556161", + "chan_point": "6fb2f10d4d6edbeed8e407ff0353b383dc851b2bfd54f1b6c41ec7ee4395b5e5:1", + "last_update": 1710845431, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "63", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "7", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710845431, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "819130665267560448", + "chan_point": "a4eb10554e71c53d65c48fcc7c7c0880aa27915d06c8e0137534d4256f960ee4:0", + "last_update": 1710681712, + "capacity": "13715301", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": true, + "max_htlc_msat": "13578148000", + "last_update": 1709514628, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "13578148000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "819157053425451009", + "chan_point": "ff07d33e87d26d8dfaa7326e0b996a79954662c65d2dc3e0aab2ad8bd093b328:1", + "last_update": 1710854830, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1135", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710854830, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "820151011993255937", + "chan_point": "e67db547e7aaf49d6350eb2d96edca4c2b9f6212b37cc059de8ff799fb27e557:1", + "last_update": 1710780171, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710780171, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710780171, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "channel_id": "820470969959645185", + "chan_point": "a4e2007f486a5c9d09b52422e9913c27d38e97495ae506ac328ba0b8ccd92d82:1", + "last_update": 1710819358, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710819358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "821415450426408960", + "chan_point": "957f40ccb45b4370ad8cc925f2e57f743a3d65224c9a785ad98f4d827955eb17:0", + "last_update": 1710806281, + "capacity": "12100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1414", + "disabled": false, + "max_htlc_msat": "11979000000", + "last_update": 1710660964, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "12100000000", + "last_update": 1710806281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "821637551705751553", + "chan_point": "34cc54265d19df14e9c34690a2423e1e1e1cc4b761f23135fe7e7419cb20f432:1", + "last_update": 1710822101, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "8910000000", + "last_update": 1710822101, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1975", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710790879, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "821934419824279552", + "chan_point": "743ef873e1279e8adcc6123cd6a9e2c382efdc22b9ddd1bc01b746f14599a396:0", + "last_update": 1710856511, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710780774, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710856511, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "822279666624823297", + "chan_point": "05fd621b62f1eb2b9640b47e34ecab821817c71069ad01e658b4cc08b5826771:1", + "last_update": 1710783271, + "capacity": "1200000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1188000000", + "last_update": 1710783271, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "480", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710163678, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "822307154289688576", + "chan_point": "75dcbd5fa4f22cc19ffff1b8b748ed1e338ccb5a375db7e4f38fb2b327c96845:0", + "last_update": 1710793854, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "198", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710752734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710793854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "823024035938631682", + "chan_point": "7e9a24c1269351ee781d6bfdcd799e187e2ceef9be0e9cb1316be6ad2c3222b2:2", + "last_update": 1710806230, + "capacity": "12100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1750", + "disabled": true, + "max_htlc_msat": "11979000000", + "last_update": 1710655516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "12100000000", + "last_update": 1710806230, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "823777201318133760", + "chan_point": "cd8fc05f50bf3069dfa14deb9bb2e7c15519aef5cd2527cf690fd28e51e3065d:0", + "last_update": 1710705189, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "751", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710528461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "5000000000", + "last_update": 1710705189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "824553456504930304", + "chan_point": "dffd6b183bca5502f7aab8598667bceb4832e2ddfd69d7fa2c93ac3532bf88a4:0", + "last_update": 1710849654, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "824721681831821313", + "chan_point": "c44c77cad6456eafb76f5f367fdba69d05448808af661d38a0f0bd3492443a80:1", + "last_update": 1710855641, + "capacity": "8171129", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "8089418000", + "last_update": 1710705633, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "170000000", + "last_update": 1710855641, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "827267051220697089", + "chan_point": "bb1b6b514adf1e3faede61de4bcffe7d2bd45528ef4dd40e5c99f0f752ea368f:1", + "last_update": 1710856490, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710856490, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1345", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710856489, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "827800314376945665", + "chan_point": "fb9a59e2481c92d8502af4939b50030779855fbe81250a826dbe1ff6e57dc133:1", + "last_update": 1710849335, + "capacity": "40000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710849335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "299", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710785158, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "827920161201848320", + "chan_point": "5357e4bf6b9313b47d35369e7e54f63c9d226ae349d5d7fe44461d3e124e3ea0:0", + "last_update": 1710855358, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849421, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710855358, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "827925658839678977", + "chan_point": "caea5a15c32c07087d71fa8277a237dd0bdf5c8f4f41f554e053eb6ef9c7d3c6:1", + "last_update": 1710825484, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "15", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "397", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710786958, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "828558977430847494", + "chan_point": "b5f98866d566ef73a1373172f4ba2f0ff37c4d9a75e425a131c6626a58b8bb07:6", + "last_update": 1710796902, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710790857, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "26730000000", + "last_update": 1710796902, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "829339630716649475", + "chan_point": "659b3e91ea53c7f2401f14c7f1ca98587330ee94355f29ca185cf97d95c8149e:3", + "last_update": 1710838630, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710803158, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710838630, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "830314897423532033", + "chan_point": "14401ca9d16f4eb0c144b1b682ee4c01a1b046b7e234ff4227985c0af7668170:1", + "last_update": 1710822101, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "4455000000", + "last_update": 1710822101, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "735", + "disabled": false, + "max_htlc_msat": "2722500000", + "last_update": 1710790858, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "830941619156811776", + "chan_point": "05b504514e3aaef84321d1b3a9bd20510b1fad91203080ecd7bc1a787a277b21:0", + "last_update": 1710718954, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "9999", + "disabled": true, + "max_htlc_msat": "9999000000", + "last_update": 1710655516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "10100000000", + "last_update": 1710718954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "831240686274674689", + "chan_point": "5053cc618432941e788d246cadc7f07d5c47e031eb0b86889ed1cf3d8fe0e5c9:1", + "last_update": 1710825484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "60", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710394665, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "030066a41ce86e73e96ffcbc3cf445744d5c92144e0c254c421a0204aebbdbb2bc", + "channel_id": "832548005667143681", + "chan_point": "7548e1fb9f5c2a91680fd6393688c52c95b339ec8edad48d32584cb35b444af5:1", + "last_update": 1710025671, + "capacity": "70000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "400", + "disabled": true, + "max_htlc_msat": "62370000", + "last_update": 1710025671, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "69300000", + "last_update": 1665612046, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "834551315785383940", + "chan_point": "bbd3917761c03326819660e20670b1b72c599d2f0c4ba0844946fce61d4a1b37:4", + "last_update": 1710794446, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710089878, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": true, + "max_htlc_msat": "30000000", + "last_update": 1710794446, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "834677759617662977", + "chan_point": "7af18f3ee5ebf69f26b04c30f9003d1a2125e6266ba650fb9806b5e0444c18b7:1", + "last_update": 1710822654, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710790997, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710822654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "835349561197789185", + "chan_point": "29ab4a9ab9f30b30f7ade78fdbdfbba8c29ac589c4fda9356c38186917a81854:1", + "last_update": 1710817104, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710579861, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "70", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710817104, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "835373750426664960", + "chan_point": "0abcdcda4fd407d72cac18d53985a8186d298b59953dfeba2bac40d41ed76b96:0", + "last_update": 1710681712, + "capacity": "28000684", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "28000684000", + "last_update": 1710604631, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "835449616896294913", + "chan_point": "65e1dabac108d5939cfe10b9870de75cd5927316cd87ec865fd58eabfa091628:1", + "last_update": 1710845427, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710771857, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710845427, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "835488099714072577", + "chan_point": "358c3b77f7f02d5fbfbab039b2ff7acfbe359a8a71b7e235795becb5a2caa8d9:1", + "last_update": 1710849654, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "485", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "835800361030778880", + "chan_point": "1d539d646510790ce6a77d65e5784b44f6a325826b9b6ad05aba5bdf18272aab:0", + "last_update": 1710774014, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "1100000000", + "last_update": 1710756547, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710774014, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "835819052721373185", + "chan_point": "a0b9f7e77f1ff467e581b3552d1ebffe39e4363fb634721da19514eb3d7dc7aa:1", + "last_update": 1710837547, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710837547, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710808254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "836482058179837953", + "chan_point": "e2ca412e9c26593e47e2c1d231cfd63b0b761a421e5df566196ff72b06796003:1", + "last_update": 1710783054, + "capacity": "33554432", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "608", + "disabled": false, + "max_htlc_msat": "33218888000", + "last_update": 1710122277, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": true, + "max_htlc_msat": "33218888000", + "last_update": 1710783054, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "836551327455969281", + "chan_point": "95cb9b81b29212d1290f66810dfa192010de60c3eb4833a0d30af9851032b6d0:1", + "last_update": 1710829704, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "70", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710712237, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710829704, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "channel_id": "836585412261314561", + "chan_point": "ea7201810a997a2bf1474cc0f21d3857b5dc36260f50f018b49aa1f6d69566b5:1", + "last_update": 1710716304, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710712267, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710716304, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "836827304877162497", + "chan_point": "60f21c1650649ba1d5976ecfc14b8e28a6b3924ec688650bb940677dbf164b22:1", + "last_update": 1710805974, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "9999", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710778343, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710805974, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "838041165645676545", + "chan_point": "97789bc894484fa48367670eed6f02a619820ca69a60a81ef7672c7f572cc6ac:1", + "last_update": 1710806734, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "19", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710794910, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "channel_id": "839705826268413957", + "chan_point": "ed8cf55ac1db0368bed8f8085d5933fef592c5fc6f229226e997bee8295f0e60:5", + "last_update": 1710794839, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710780197, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "224", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710794839, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "839889444694392833", + "chan_point": "3a3d1d8314529a7729701539e3f68a5cc5cf92d5e091bcb3cbe93d9a6bd1befd:1", + "last_update": 1710856110, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710816528, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "840281970384240641", + "chan_point": "1037bb8d891e57fcb2255dbc84ce262b64abe8625edcb98168735f645d8f7daf:1", + "last_update": 1710631531, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1690812878, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "475", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710631531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0238268dc72b78f0bc19398b46916246723c013911286b91b61e7f78d7a9a90174", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "840879005253435392", + "chan_point": "2e0dc71846b9215b80ece290092e2572b8c00821e862a66da32fafdf8697d931:0", + "last_update": 1710807040, + "capacity": "1832587", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "543", + "disabled": false, + "max_htlc_msat": "1814262000", + "last_update": 1709953781, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "280000000", + "last_update": 1710807040, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "channel_id": "841391377607360512", + "chan_point": "cf7e20ce33e80514e8b166b719599098946b25207042d355e3b2cb63e6452050:0", + "last_update": 1710804504, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710799948, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710804504, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "842115955863977985", + "chan_point": "29216f31fe8e4fb3a88c7af2211f0d5d8a3cd8d9d611643cd195f083c6de9568:1", + "last_update": 1710844534, + "capacity": "3380000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "49", + "disabled": false, + "max_htlc_msat": "3380000000", + "last_update": 1710844534, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3346200000", + "last_update": 1710619552, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "843135203032432640", + "chan_point": "56e1113c87bb0cf7e3e633d781db6b39cdc992b59c9d9e8601c14d18211b1acf:0", + "last_update": 1710815230, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710800772, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710815230, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "843567311088451585", + "chan_point": "dcb65184c2e1dfc8c8f3fece5551d57555034a83f57242ad5e045830094ab13b:1", + "last_update": 1710803931, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710803931, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1701256078, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "844152251268005889", + "chan_point": "009eade7bacb5b98238154af06bb556f0fb14a9a94bb25b169b98c0d15579f03:1", + "last_update": 1710757572, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710757572, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710348140, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "844818555359657984", + "chan_point": "1636b5bcd90255cda029c661ade3f4ebcdb42453db962b4d9a1126dafdd69d53:0", + "last_update": 1710831407, + "capacity": "7947136", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "4730", + "fee_rate_milli_msat": "1272", + "disabled": false, + "max_htlc_msat": "7867665000", + "last_update": 1710831407, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1345", + "disabled": false, + "max_htlc_msat": "7947136000", + "last_update": 1710689767, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "845631094439870468", + "chan_point": "ade46379cee4582ac1bccd1c1825e1dc50632ec36afc12e0d1815df1df44288a:4", + "last_update": 1710799828, + "capacity": "11000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710799828, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710799828, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "845717955864035337", + "chan_point": "f8a5eb2ad759c134c7dd93a43b71694e46f1a3a22a4868cd43500f5de6bb2d6c:9", + "last_update": 1710856301, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710790865, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "998", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "18711000000", + "last_update": 1710856301, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "channel_id": "846207238618349569", + "chan_point": "beaf49ad8f5b79c08eea9185eed29f9877466d718bb9052a0f06a942f755bd64:1", + "last_update": 1710828054, + "capacity": "5100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "55", + "disabled": false, + "max_htlc_msat": "5049000000", + "last_update": 1710827141, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "5049000000", + "last_update": 1710828054, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "847062658600927235", + "chan_point": "ef963a1a111df59aae61f7ce065c89e93add544c8a85787bd8d5be85160af9e8:3", + "last_update": 1710803440, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710779221, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "10000000", + "last_update": 1710803440, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "847062658600927238", + "chan_point": "ef963a1a111df59aae61f7ce065c89e93add544c8a85787bd8d5be85160af9e8:6", + "last_update": 1710853841, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710853841, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710795330, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "847220988269428739", + "chan_point": "dbb0527b9a00d1b86d301b47af5a75175f53528444ac9d890e30f8551b9bec78:3", + "last_update": 1710822101, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "80", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710790856, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "5346000000", + "last_update": 1710822101, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "847360626226429953", + "chan_point": "661a41751b5adb52329833d9171487e490b7f9c7a5bdde171ee312c6e6836969:1", + "last_update": 1710757232, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1953", + "disabled": false, + "max_htlc_msat": "10100000000", + "last_update": 1710757232, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1710085980, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "847929073754636288", + "chan_point": "804e511c9972de3033dca820a4edd1191771ea7fe13c729743954eb2e31168e3:0", + "last_update": 1710855941, + "capacity": "10865362", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "215", + "disabled": false, + "max_htlc_msat": "10756709000", + "last_update": 1710855941, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "10756709000", + "last_update": 1710754081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "848085204485799937", + "chan_point": "05367bf193cd8765b4d4435b8883c2c64cbe034a3d2a1f36c45cbca4633f89c0:1", + "last_update": 1710838572, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838572, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710793854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "848783394325004291", + "chan_point": "3f6e9f9880010cc94a25f5f6ac0a482444be1b3d130fb07977b64d6f5c391548:3", + "last_update": 1710770243, + "capacity": "11000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710770243, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710696129, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "848882350473347075", + "chan_point": "7005d695f343bf9bd9cb2e7570c915847d576cea41051c451ef5009a3a741467:3", + "last_update": 1710856469, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "8009", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710602826, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710856469, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "node2_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "channel_id": "849038481014980609", + "chan_point": "12fcc7926fd12bca65bfbb7f4115b736c711fb5050ee5b2c3df60d3faab2716a:1", + "last_update": 1710817552, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "224", + "disabled": false, + "max_htlc_msat": "1956000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "99000", + "fee_base_msat": "750", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "22000000", + "last_update": 1710788729, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "849343045792432129", + "chan_point": "bd77b9808da30f57b75708f79a926a995fb8069fe34c8e96a725e609adb404a9:1", + "last_update": 1710827331, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "813", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710827331, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "849371633002348545", + "chan_point": "537a13bb91fa7785f66615e372194a74f99c3a4672d317f98dff5bc152d910c1:1", + "last_update": 1710856490, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "487", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856490, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856489, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021aa6f0fdc8ea3c85b98aff27e5ffe1f67921e75e7aa7bad6a36d6085d54117df", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "850774609906106368", + "chan_point": "50a4305afc057f342e1b4686c225ccb2e46bdfc06996b40a3dd8b29c34f9fc07:0", + "last_update": 1710786320, + "capacity": "30934", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "30625000", + "last_update": 1710786320, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "30625000", + "last_update": 1710611821, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "851304574543790096", + "chan_point": "fde30f6c5e6545f3bf5b7bbe7fb47c3f261614b336be7ab8d037909a8c931463:16", + "last_update": 1710823901, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710790869, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "13365000000", + "last_update": 1710823901, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021aa6f0fdc8ea3c85b98aff27e5ffe1f67921e75e7aa7bad6a36d6085d54117df", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "851467302217777153", + "chan_point": "ac6eb42bf4d03fd2a2cccd22ee95c6927f9b6a95eab3760cac0746aec81fd4a2:1", + "last_update": 1710807858, + "capacity": "20000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710719720, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710807858, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "channel_id": "851853230885109761", + "chan_point": "97513e54974887b73ba45289d77d089bf6608191a19fd7f7367ccc94b8c9139e:1", + "last_update": 1710847222, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "166", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "145", + "disabled": false, + "max_htlc_msat": "676724000", + "last_update": 1710847222, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "852275443262881792", + "chan_point": "05bc8e62087476abf0b6457cb451e109d6a919bb5204ff22584dfb52c12b6b2b:0", + "last_update": 1710807943, + "capacity": "6900000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "6831000000", + "last_update": 1710660474, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "6900000000", + "last_update": 1710807943, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "852421678319665157", + "chan_point": "0741193526c43dc659947228f037dcc40bb73a70423775e62095c674ecf8282a:5", + "last_update": 1710852439, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710782821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "304", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710852439, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "852525032407236609", + "chan_point": "b455866536c2a706db48622e1d976888cd6ba40e0cd981bfded493fe3c9558b2:1", + "last_update": 1710843032, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "6", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710843032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710826840, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "node2_pub": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "channel_id": "852606396236103680", + "chan_point": "18f5e4ccbcb40634fc6bc65dea251a0af52c33bfad24050ba9b1329dba36a8bd:0", + "last_update": 1710785663, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710061704, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710785663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "852847189292351489", + "chan_point": "8305315fe045996c70e1b5203f9f23ac33adfd7a2ee202e86bd3a945504a90da:1", + "last_update": 1710814352, + "capacity": "501115", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "496104000", + "last_update": 1710814352, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "496104000", + "last_update": 1710814232, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "853211127701635073", + "chan_point": "1510463d74758c5fa1d70b7c6b690149884800ed98bf7245a364aa4ff573dd7b:1", + "last_update": 1710849282, + "capacity": "300000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "297000000", + "last_update": 1710849282, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "550", + "disabled": false, + "max_htlc_msat": "297000000", + "last_update": 1710789264, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "853612449362935808", + "chan_point": "96d83d0eb907367e794440b91ed86c13dc0eb143ab7df3ddbb4dbc21592555b7:0", + "last_update": 1710845743, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710845743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710593844, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "channel_id": "853824655087828993", + "chan_point": "93b1d1efb4003f88e9bddcd1b0560e6129ce7dbe8f49fe63fbac47eccf727f6e:1", + "last_update": 1710785354, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710061705, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1500000000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "854116025735643136", + "chan_point": "b4dd72c7e215c6e35ba98ba30347b6d0386fc816b5e3695a1cec401d8abd3031:0", + "last_update": 1710843032, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710816935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "11", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710843032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "854252365134299137", + "chan_point": "b74cd9820a4be1d74ae14bde9037a919ab4cbfbc4c00bcb121c3d6b8d6bb96d2:1", + "last_update": 1710829207, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "239", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710829207, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "node2_pub": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "channel_id": "854700965874368513", + "chan_point": "2d9f3ea67203bde8db39e50c7a14e1c4c129525d5113dc63a9541fd0caeb49cd:1", + "last_update": 1710844294, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "968000000", + "last_update": 1710649488, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710844294, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "854929664299827200", + "chan_point": "1434652e9f165c89fe4bcf8b702f8c5b0aaaa7a8a9fb696206f81e7a01a9345f:0", + "last_update": 1710829154, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710829154, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "298", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710757039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "855098989111869441", + "chan_point": "777f4738859fad58185ba5144c02da3d35dc2c5ef2c8159a7a900a70656ca226:1", + "last_update": 1710843032, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1460", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710843032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710802681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "855298000662102017", + "chan_point": "6614b1f0e5deff9cb22dafe11960db09fde32cc36eb0aca017e601cd02c050d0:1", + "last_update": 1710814807, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710814352, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710814807, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "855687227868250113", + "chan_point": "c1331b8a88f458d538115fbbdb67eb52c7521296e72874a34e6e5c4250110eb4:1", + "last_update": 1710843033, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "787", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710843033, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710817254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "857312306009145344", + "chan_point": "fccdafb6af4011be5ab317254b2eeaa32b6607ba97c97badbe066f00493efbb4:0", + "last_update": 1710789264, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710787765, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789264, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "857942326103179265", + "chan_point": "ae92472c182f1aba9da85c73dc1fbbaffa7e8fd502729b0d83002923e625c051:1", + "last_update": 1710856800, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710821548, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "63", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856800, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "858245791320571905", + "chan_point": "7815ce2c19b877ab55619d042ac17ba2a8d16888f8eb66faa8115988f9f79a19:1", + "last_update": 1710829457, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710829457, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858552555091591171", + "chan_point": "8f56547f78665a50c8f2b5ba7453a2c4abcee9528ae7d2887a2418e8421e343c:3", + "last_update": 1710849922, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "410", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710810362, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "104", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "858552555091591175", + "chan_point": "8f56547f78665a50c8f2b5ba7453a2c4abcee9528ae7d2887a2418e8421e343c:7", + "last_update": 1710812663, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710786922, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710812663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858566848720732163", + "chan_point": "b881d3a276b344af7e3e70c44b98729b3c0104ce5290960392d544526dfda915:3", + "last_update": 1710840654, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710840654, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "61", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710772522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858566848720732164", + "chan_point": "b881d3a276b344af7e3e70c44b98729b3c0104ce5290960392d544526dfda915:4", + "last_update": 1710583522, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710203740, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710583522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858566848720732165", + "chan_point": "b881d3a276b344af7e3e70c44b98729b3c0104ce5290960392d544526dfda915:5", + "last_update": 1710637522, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1709340874, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "38", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710637522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "860461307324923905", + "chan_point": "171a9db90339f0417f14ab00a58f3b28d02a38b79077ae3d2cc49f6077322eaa:1", + "last_update": 1710807858, + "capacity": "8105381", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "8024328000", + "last_update": 1710807858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "8024328000", + "last_update": 1710805491, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "860958286649425922", + "chan_point": "8f12bfefb5777e2d4e3c9028f65ad84979e0ab4083e7e390c87d422ce8f88ad0:2", + "last_update": 1710829039, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710782735, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "214", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710829039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "860958286649425926", + "chan_point": "8f12bfefb5777e2d4e3c9028f65ad84979e0ab4083e7e390c87d422ce8f88ad0:6", + "last_update": 1710806233, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710806233, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "297", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710793039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "861423380097138692", + "chan_point": "67eb0e8d784817da6cf6c9253924b6df24839d4565f4ec17c57464134cc9284c:4", + "last_update": 1710850636, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "3953", + "fee_rate_milli_msat": "277", + "disabled": false, + "max_htlc_msat": "50000000000", + "last_update": 1710768442, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "13003", + "fee_rate_milli_msat": "65", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710850636, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "861423380097138693", + "chan_point": "67eb0e8d784817da6cf6c9253924b6df24839d4565f4ec17c57464134cc9284c:5", + "last_update": 1710856501, + "capacity": "130000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "5300", + "disabled": false, + "max_htlc_msat": "128700000000", + "last_update": 1710856501, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "128700000000", + "last_update": 1710856500, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "861519037497737220", + "chan_point": "4cb6d3d3eb1cc1bc6c9371ef77dfd85556690f37c8cb80b240403cb84a274b82:4", + "last_update": 1710843607, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710798929, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710843607, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "861735641247449091", + "chan_point": "049562f40a1e281238f5f4f6c23858cd27ec6da08d7d946fd36d2383c89eb556:3", + "last_update": 1710846600, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "315", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710846600, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710838363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "862864839716372481", + "chan_point": "2d7228d7df2158148d1da4e3be99db7479eef9a4c44b004297e7835ef99a5d68:1", + "last_update": 1710851454, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710851454, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "450", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710839263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "863081443571073025", + "chan_point": "afadbf08cd122a91cbfd79b481e067fbaee33cbf2f705c2b95b8446e69a80d99:1", + "last_update": 1710831429, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710831429, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1100", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710781663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "864244726917431297", + "chan_point": "42147b93add7c26897e5062d278dc4553e67fb3cfc85fc20a214df4f816192d9:1", + "last_update": 1710800407, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710799031, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1375", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710800407, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "864361275012743169", + "chan_point": "c44360fc1fb935c38c733a34051b8139211caa02bb4aab37a4aaacaccd95443c:1", + "last_update": 1710843033, + "capacity": "3200000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "3168000000", + "last_update": 1710803071, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "162", + "disabled": false, + "max_htlc_msat": "3168000000", + "last_update": 1710843033, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "864505311075368960", + "chan_point": "caff4478d3275c81ab34f076b8bad1b5f4d29e5fc600877c55ce44f920e0b0b7:0", + "last_update": 1709875367, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "599", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709636731, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709875367, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "channel_id": "866072115312787457", + "chan_point": "4c3bc762430ff6b7c2571a1c74b0602eb72382dd90a5337220c52881f2090381:1", + "last_update": 1710847457, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1500000000", + "last_update": 1710847457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710631189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "867668606052073473", + "chan_point": "592dd4151c78bc6f240c59ec4aa0375a25b38801163e8ff18e286e029606dd94:1", + "last_update": 1710786335, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710786335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1706575582, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "868494339200909313", + "chan_point": "e03908a9bfaa2366b68a9c105b189a434379261d583ae8fce1d848a403525f3c:1", + "last_update": 1710851954, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "11", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710845427, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710851954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "869129857120993282", + "chan_point": "78c5d2798c3eedc24aba7b71b9ab4618e71d97d3857fc79c5aa12009c5ed5f2b:2", + "last_update": 1710820457, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710820457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710805807, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "869744484135600129", + "chan_point": "9563b4ff69a191f6b7b613b9978e8a4d05cf92037ad4ffd6e08139e69b9c9895:1", + "last_update": 1710855641, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710778731, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710855641, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "869744484135600134", + "chan_point": "9563b4ff69a191f6b7b613b9978e8a4d05cf92037ad4ffd6e08139e69b9c9895:6", + "last_update": 1710853270, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710814353, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "86", + "disabled": false, + "max_htlc_msat": "14840000000", + "last_update": 1710853270, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "870145805817085953", + "chan_point": "7e3fe6961fb04b0d6bb332f8a5029aac660f912931ae93116361b54d8825c5c9:1", + "last_update": 1710848160, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710783891, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710848160, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "870162298416070667", + "chan_point": "2b6b9fd1a87fd9adaccc7c00171a74bcbf7a3a75eae3c02f6a6b9e8c2cffb3fd:11", + "last_update": 1710854804, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710854804, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710796154, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "871372860814393345", + "chan_point": "5fbf1bed89c9673723390d85e87fc351e3ed4cae91d128dee852756baff516db:1", + "last_update": 1710810979, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "4725", + "fee_rate_milli_msat": "2362", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710810979, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "0", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710237045, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "871581767934869505", + "chan_point": "3a8aaacae4315d338db60807db6cfe8d8564ab3963fdaba4be59b75a975b64ce:1", + "last_update": 1710835032, + "capacity": "40000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "4083", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710835032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "871868740602232834", + "chan_point": "e3d4dbe173d3b2b187de3668097d6c659f84f7bb4568f0899f951882c1bde729:2", + "last_update": 1710829232, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "945", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1710829232, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1710803931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "872036965794447362", + "chan_point": "fc6de16a69a24154566282822ebc705dfc75724bdd841d835af377d41e8ed06e:2", + "last_update": 1710788484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710777850, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710788484, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "872385510883131399", + "chan_point": "f60a3a740eb3c8229164018d706941801c0c7a0677d279101daa8de699a48479:7", + "last_update": 1710814008, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "327", + "disabled": false, + "max_htlc_msat": "600000000", + "last_update": 1710814008, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "40", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710804922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "872385510883131400", + "chan_point": "f60a3a740eb3c8229164018d706941801c0c7a0677d279101daa8de699a48479:8", + "last_update": 1710855054, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710814008, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710855054, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "872443784965980161", + "chan_point": "9838f109ffd5875afa7ab83e656726c83b586a0b19060de5b42d43bd7f95ee09:1", + "last_update": 1710838621, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710838621, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710788281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "channel_id": "873894040860622849", + "chan_point": "86225d5500aedd394e172df3aa4dc4e6bd13f5e7dc693543e75df9c5400a9ff2:1", + "last_update": 1710754254, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710754254, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1709510190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "873973205729280001", + "chan_point": "f01825aeb06b0a401599be0b8c401c35d28e2ef08ff34593f83f618eed8e4c39:1", + "last_update": 1710848353, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710785048, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "874017186217918464", + "chan_point": "a5fc35368281ffbdc696b06bdcb23f3e563977ef9f99b55faf901efc9d917fae:0", + "last_update": 1710772753, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710681712, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "5000000000", + "last_update": 1710772753, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "874659301032722433", + "chan_point": "3f053f0b81f641f583568d3893cdd12f8ac7dbe3449ea733876157a4a1b7a23b:1", + "last_update": 1710817997, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710817997, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1709474894, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "874673594643185670", + "chan_point": "395bfd28de332b83abb59ba0e470672382f1497e163fd596c538128307011543:6", + "last_update": 1710814989, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710794564, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710814989, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "874694485266268160", + "chan_point": "b9c28e5e616473ce5a0258d3fc28fac5a34c1047beb62f13a028e4557196b1a9:0", + "last_update": 1710826513, + "capacity": "1993097", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1973167000", + "last_update": 1710794020, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "1973167000", + "last_update": 1710826513, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "874841819846541313", + "chan_point": "5c7a3e4f467f08f1c29954eff6ff08b1efdd5ccdd803918240e5a058d146d619:1", + "last_update": 1710853280, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710788758, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "1760000000", + "last_update": 1710853280, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "874848417047838721", + "chan_point": "0b337d91493ec80db5bc84134553a22f2bb22a2426e7d6242813f974ac8e6048:1", + "last_update": 1710819748, + "capacity": "14000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14000000000", + "last_update": 1710819748, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "13860000000", + "last_update": 1709824082, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "874942974993629186", + "chan_point": "ee8d03bb7d3aea4037e9727484def09cbe1f2730f7fb3b61f3ec14705c0ee6b3:2", + "last_update": 1710818239, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "306", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710818239, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710445964, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "875337699844816898", + "chan_point": "3c0ee164148756ea89f9ff517add780a0a5a7461d93a575b675635f9935bfed8:2", + "last_update": 1710779620, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710779620, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "80000000", + "last_update": 1710769241, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "875337699844816899", + "chan_point": "3c0ee164148756ea89f9ff517add780a0a5a7461d93a575b675635f9935bfed8:3", + "last_update": 1710812441, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710812441, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710777922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "875337699844816900", + "chan_point": "3c0ee164148756ea89f9ff517add780a0a5a7461d93a575b675635f9935bfed8:4", + "last_update": 1710803441, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "140000000", + "last_update": 1710803441, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1985", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710779131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "875850072099651585", + "chan_point": "a758034965f930a90a8a8e7e5b2553dd427294a5c2140aba3ae502372ffbcc18:1", + "last_update": 1710853110, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710842650, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710853110, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "876652715588321282", + "chan_point": "7671908e6dbb224458a79282cd53d1c165cb7c02f16c5e76126d7bede9a5925b:2", + "last_update": 1710805807, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "623", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710798632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710805807, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "876747273627631616", + "chan_point": "7ebb9b7609486d9d86fdcc111a27a83fefe70b60820b93dfda51ebb843096576:0", + "last_update": 1710814728, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710814355, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710814728, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e114a6bffa9ff420eb84491f3eceb3b2a5a5638f6d60d4324e16436c61acdcdc", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "877328915132317697", + "chan_point": "d8af5721b580140894c8f8ea7195b7e83edc07a86e6e24c8efa620e5fabedbfe:1", + "last_update": 1710836501, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710604842, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710836501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "879261856582598657", + "chan_point": "28865bb2a78214d1fb70d840ff310c552fc7fb325e31fca08da8da6c3e859abc:1", + "last_update": 1710843033, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710818821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "408", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710843033, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "879261856626966529", + "chan_point": "8d972be2ed57e9bf954efc0b0505baeed5d1fcce7e8c4fea5a6ea58e5d8619f3:1", + "last_update": 1710786991, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "77", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710786991, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710771093, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "channel_id": "879293742446936064", + "chan_point": "6a02bcab54f063af738ab7ace6e5268e820fadc70883959d3ef6cac89a064ad4:0", + "last_update": 1710843033, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "4", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710786990, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "112", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710843033, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "channel_id": "879916066066464769", + "chan_point": "68876042d3c7fc84b3694756da25c0e1e65fa4098d9e16894f837460396815a9:1", + "last_update": 1710847222, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "915", + "disabled": false, + "max_htlc_msat": "1115397000", + "last_update": 1710847222, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710210235, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "880673629523804161", + "chan_point": "457b7a0adac9ee0c43407552bd06cc0dbee5abe62da4da9916b5a4df9bb5b7f5:1", + "last_update": 1710841153, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710779929, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710841153, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "880829760205291521", + "chan_point": "1b832daa4a4a72efd68ca390dc01f03e4a6597d6de6a8850af39d6794dd1b1ef:1", + "last_update": 1710853278, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "189", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710789632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "287", + "disabled": false, + "max_htlc_msat": "4930000000", + "last_update": 1710853278, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "880960602030997505", + "chan_point": "ad6e1f232934cac85ccec5c1bb37fb505a29a681ff23215654a408ef5256c3b9:1", + "last_update": 1710816528, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710796154, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710816528, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "881278361018957825", + "chan_point": "6a4b3b530fe4f4aadd1272a3351f5ad716bea8c71e53f08cb34f474b882f8082:1", + "last_update": 1710805240, + "capacity": "3991937", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "3952018000", + "last_update": 1710779281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710805240, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "882651651019177985", + "chan_point": "1d0cc9ce5b74bb2a6d011c8b1a85eca97bf4228dce944fa64ebfd814302088be:1", + "last_update": 1710845731, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710784632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2485", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710845731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "883062868309508098", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:2", + "last_update": 1710807858, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710807858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "12", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710784039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "883062868309508102", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:6", + "last_update": 1710805240, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710805240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "43", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778639, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "883062868309508104", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:8", + "last_update": 1710784039, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710782331, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "301", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710784039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "883062868309508105", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:9", + "last_update": 1710838854, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710838854, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "282", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710816439, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "883170620405907457", + "chan_point": "6e6160dd05bdca23efe934bfb53f0059d1736b18d77823ba00d8be0203414582:1", + "last_update": 1710825484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "260000000", + "last_update": 1710777664, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "883852317749870592", + "chan_point": "63891404aff07ba780b969c1a77d34a7af1c3036d0c821bb2248cb97c8c9962c:0", + "last_update": 1710839213, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710779464, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "555", + "disabled": false, + "max_htlc_msat": "3000000000", + "last_update": 1710839213, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "884138190664040449", + "chan_point": "767617e8b706bcbac41f00621cabf986e01a360b5e40db331fd2a8aa128fa0d7:1", + "last_update": 1710855001, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1450", + "disabled": true, + "max_htlc_msat": "69300000", + "last_update": 1710855001, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710841063, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "885149741433028609", + "chan_point": "4107a7f73589ca82ec54dfec294fe3341bfc0d7f2d1868cfd829939a20a9f504:1", + "last_update": 1710791398, + "capacity": "5500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "680", + "disabled": false, + "max_htlc_msat": "54450000", + "last_update": 1710778441, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "5445000000", + "last_update": 1710791398, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "885190423360765953", + "chan_point": "935129cfbb3be5d579ef07f95bb3af4be0b3bba960a60052154246be91f0ced4:1", + "last_update": 1710845481, + "capacity": "16500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "399", + "disabled": false, + "max_htlc_msat": "16335000000", + "last_update": 1710827432, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "16335000000", + "last_update": 1710845481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "885493888613941248", + "chan_point": "ca918cef40ba2df8f455f12c07f43a4c18ff7480c027d259f37de004c16a6e6e:0", + "last_update": 1710822654, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "50000000000", + "last_update": 1710822654, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1250", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710404038, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "885513679789293569", + "chan_point": "a71f6baad8bcc0eb7726d596f6c61a57d58527d8ab995c212fd585617af878e4:1", + "last_update": 1710817958, + "capacity": "1995967", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1976008000", + "last_update": 1710817958, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1976008000", + "last_update": 1710812020, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "886056838652755969", + "chan_point": "d4ceaa8ac97c64506fe6ca53d5b1a05ae28dae719f87f021ae2e0b9dad0e15c2:1", + "last_update": 1710854657, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710854657, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710631492, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "886194277456412672", + "chan_point": "f3dd1145a8406d37d6604fd17574c8e1f6336c2a77fb8c790c0379904013995f:0", + "last_update": 1710803683, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "350", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710778781, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710803683, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "886308626709544962", + "chan_point": "4837f7a9dfc2d0cad1dc1d6d9f5dff2342eb12aa750f3db6d2654fcd82b6c81d:2", + "last_update": 1710803839, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "385", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710777032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "99", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710803839, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "886493344707575808", + "chan_point": "c49fb182cfb29a69118369566af52fe86911f0242c2092ffde9fd890d12a4f11:0", + "last_update": 1710839832, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710839832, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1985", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710803683, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "886573608920219652", + "chan_point": "898f2d355733eae6d3faaf63d956edd4b4c1ec6761c0833cbd9a1b60300dc841:4", + "last_update": 1710817552, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710803347, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "886592300790185997", + "chan_point": "b118e2f5494e53302d817ef1132a207b01b08d0ee057cdf949c92f2ed87decf3:13", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790450, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "886592300790186000", + "chan_point": "b118e2f5494e53302d817ef1132a207b01b08d0ee057cdf949c92f2ed87decf3:16", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "2000000000", + "last_update": 1710791589, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "886619788490833928", + "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:8", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "886619788490833939", + "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:19", + "last_update": 1710843033, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "478", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710843033, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "886632982537699334", + "chan_point": "75f178a4f984736b1210e62a4bbc66c9e7aee62b340d3aef67d9da7fd4549a7e:6", + "last_update": 1710822190, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710528519, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "2000000000", + "last_update": 1710822190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "886632982537699336", + "chan_point": "75f178a4f984736b1210e62a4bbc66c9e7aee62b340d3aef67d9da7fd4549a7e:8", + "last_update": 1710856473, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1777", + "fee_rate_milli_msat": "199", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791766, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710856473, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "886641778647564318", + "chan_point": "e79f7e0d0e5da93c547b3f9d75cd0a0647a4857ef6eafb5c26bf7d07200295f7:30", + "last_update": 1710791590, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "480", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710109678, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": true, + "max_htlc_msat": "1980000000", + "last_update": 1710791590, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "886759426467037211", + "chan_point": "5bc09590890f03e5e0a338c2352a2e3f87f30790814d056779b4881bea2cf68e:27", + "last_update": 1710846600, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793390, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "316", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710846600, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "886767123023659009", + "chan_point": "10fe3292a03ca195c69ad861bb07b6e4c6486d9f6858c29533ce0d94d417aa97:1", + "last_update": 1710841048, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710810497, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710841048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "channel_id": "886775919173238800", + "chan_point": "d1cdbc9ff87bc82d71b42f1c2be1996581ddc914928990bdb7fe5d192e21558a:16", + "last_update": 1710779210, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "138", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710773240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710779210, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "886802307381854213", + "chan_point": "64162d468ce15d175fbdce4c0d94b9047abf4400582da10236bcdca6623a7593:5", + "last_update": 1710807858, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710807858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710805989, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "886811103491129349", + "chan_point": "e6e65cc949066dc849d8130f81bf8bfc9a52109c432318322053660ebe9deb1d:5", + "last_update": 1710790663, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710784389, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "886941945458982913", + "chan_point": "150cff20eb45b78b7c969134151ca85592dccab727d6e091a83fd0ebe3d1b8fc:1", + "last_update": 1710848832, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "3150000000", + "last_update": 1710848832, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3150000000", + "last_update": 1710605616, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "886973831192641543", + "chan_point": "541df3deb94bafbd71885609b51bc1de27187cc93d1705c3788a47508af7c2bf:7", + "last_update": 1710827589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790747, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710827589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "886973831192641573", + "chan_point": "541df3deb94bafbd71885609b51bc1de27187cc93d1705c3788a47508af7c2bf:37", + "last_update": 1710795189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "49", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710792334, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "887290490550484993", + "chan_point": "8aca0ea50a44772ebb0ffaec9a04a6e0d047a3e0c30403530686497ceb905504:1", + "last_update": 1710838517, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710838517, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710826522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "887308082692030465", + "chan_point": "b634fad4611de0a11988fab615f6f175a6d781d497f7ee50266e3a951c44c4b7:1", + "last_update": 1710854909, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "25000000000", + "last_update": 1710814357, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "40", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710854909, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "887361958759432192", + "chan_point": "358dd54644e0ce38bd9c8d658b54bc5da28e3a9f4e1ca3979a87c31f951cb14a:0", + "last_update": 1710854911, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "54528", + "fee_rate_milli_msat": "41987", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710854911, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "100000000000", + "last_update": 1710775681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "887465312824524801", + "chan_point": "a59dec9e18805e41b1f42ddc92210e6bc5f2a01cc3456548db641b298186ea90:1", + "last_update": 1710837519, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "2745", + "fee_rate_milli_msat": "914", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710837519, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "50000000000", + "last_update": 1710777958, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "887465312824721408", + "chan_point": "fbd30e889cd756b8e18b04c3175fb5e13bfa2c704527014cf55e030d841227e8:0", + "last_update": 1710837519, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "942", + "fee_rate_milli_msat": "471", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710837519, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "25000000000", + "last_update": 1710775131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "887535681579319297", + "chan_point": "8622d9b19c80349dac710728ec72670e898ca6e59113db56d4c468970f34d90b:1", + "last_update": 1710843033, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710843033, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710838363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "887725897158885402", + "chan_point": "ebdba1f9d2a671c43a5a7a2d15b0871bcb9d8c8dbd90f739f990c604220b5e5b:26", + "last_update": 1710845428, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "165", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710845428, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793390, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "channel_id": "887757782897721367", + "chan_point": "8ae029111d3e44a0ef02a19caa5ba36b41fa78f30e1eb76293822180760611f6:23", + "last_update": 1710796663, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710443151, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710796663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "887850141923278849", + "chan_point": "5b774ae290201675d94d3d096a1fe7a8cae8b281445afd122bd3973ce18600ee:1", + "last_update": 1710787013, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710786430, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "6666666000", + "last_update": 1710787013, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "888351519282954242", + "chan_point": "a3536a05411a39acd5f0afd6bcf48b03ac9a5b57e530698614a8c022c349f292:2", + "last_update": 1710853288, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4420000000", + "last_update": 1710853288, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1709913614, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "channel_id": "888405395390595073", + "chan_point": "041bad6c7c41bf539933f9991ab45cbb0efff701ec74238a1d6a14158ebe2d82:1", + "last_update": 1710845796, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "3375000000", + "last_update": 1710845796, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3375000000", + "last_update": 1710488135, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "888414191473721344", + "chan_point": "1f79a56d029cefd483fa1dbd4110a453802d61b040fc4d36b543dac2a96c0243:0", + "last_update": 1710803684, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710803266, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710803684, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "channel_id": "888497754257227777", + "chan_point": "8f2522b419779e7ea5f95c6cab17789ddf7c38c76b1485a2a0025d39e1dec22c:1", + "last_update": 1710794569, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710779196, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710794569, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "888503251937067009", + "chan_point": "0e25169b920fe5598e0d6d886f27cc94ad3ebbfb0ab33c1a4cc26358fb597be4:1", + "last_update": 1710807858, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710806196, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710807858, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "channel_id": "888545033254010881", + "chan_point": "67a90c8dd80d368cf387389d8283efa81f9bccc97980ca1b435158beb8035e53:1", + "last_update": 1710782832, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710780996, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710782832, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "888591212859424774", + "chan_point": "bf7474636c1048baa26ef72aa51b0b9626f8755efe4a2df2c8f7bcbeb4a074bb:6", + "last_update": 1710803684, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "125", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710803032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710803684, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "channel_id": "888600008882126851", + "chan_point": "ba49acbcba48ccb1f9543fe0ec07a8dcf8ce29a62764bd46e1413d7932511d85:3", + "last_update": 1710855115, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1782000000", + "last_update": 1710855115, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710855115, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "888868289736212481", + "chan_point": "8026fb66e5c7d087a1f5c505c0f7163e258e0b44c8d67cf91a9ee11fc6844722:1", + "last_update": 1710803213, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710786333, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710803213, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "888918867285508096", + "chan_point": "222b58df9ce514d033994fd520f8d94b4403421ee97a644d5c88a20be1cdf5ff:0", + "last_update": 1710788428, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710788428, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710502897, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "channel_id": "888921066311188480", + "chan_point": "2edf357094378b3e52a4ba0e987ebcadf3477b15d38a1f9f745f739e61ff1f9a:0", + "last_update": 1710804504, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710798790, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710804504, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "888925464317853705", + "chan_point": "8f9d7a2d95293c013b3684ae3d23bf953ff68de267e45d26722e6b12e61d38a5:9", + "last_update": 1710804189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710801697, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710804189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "888974942422827012", + "chan_point": "9d52945a0a59466d10a64ab77ed70c9a0f05d037f63f714739ff6502e894ae86:4", + "last_update": 1710795189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795189, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790560, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "889106883778707456", + "chan_point": "571fde9db1eb61bc1ecb405db7d135910cd6a95a2def4db4618815988c4ec0ce:0", + "last_update": 1710837063, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1980000000", + "last_update": 1710837063, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710803695, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "889200342210969600", + "chan_point": "afaa4a25d07946a47d64f351496f18a7d8fa2ebd30e813ee24e428e1a285ade4:0", + "last_update": 1709978556, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709448931, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709978556, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "node2_pub": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "channel_id": "889225631013142529", + "chan_point": "48bc5386312bf4a937042325de22ccf309d06397a7dd34a9ac08d2b1138d86e0:1", + "last_update": 1710851082, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "27", + "disabled": false, + "max_htlc_msat": "980100000", + "last_update": 1710790261, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710851082, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "node2_pub": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "channel_id": "889306994934349825", + "chan_point": "302028cc66b9a0a1d0164d57094abbe74ea7ef680e270cfb0daaf4edc93ef381:1", + "last_update": 1710771882, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710771565, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710771882, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "889569778255462400", + "chan_point": "a96e595d644013688f2408c99f6b40ff6ddc60dade8456628573f41c7a7ef156:0", + "last_update": 1710839832, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "602", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710803683, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839832, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "889698420993490945", + "chan_point": "8e14ad183c9023ac39cab27dd6ad7c5f873e53f66aa9a89015cb4899e87d9fca:1", + "last_update": 1710829389, + "capacity": "1985495", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1965641000", + "last_update": 1710790420, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1965641000", + "last_update": 1710829389, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "889698420993622017", + "chan_point": "32a86a229314f2786aed224268b5df1fb61cc13c1dd5dd20708603886c6ea813:1", + "last_update": 1710792863, + "capacity": "1992011", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1972091000", + "last_update": 1710786820, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "1972091000", + "last_update": 1710792863, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "889809471625953281", + "chan_point": "3e555b10e98f49e349b991ef9597b3180c474d3fbfe13ae3c16d5804888eeefd:1", + "last_update": 1710844918, + "capacity": "12918498", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1000", + "fee_base_msat": "5353", + "fee_rate_milli_msat": "4035", + "disabled": false, + "max_htlc_msat": "12789314000", + "last_update": 1710844918, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "12918498000", + "last_update": 1710836831, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "889825964323700737", + "chan_point": "8f10761d38bc3c0e30cbd8b42118cc0e152cf1f7b57384bfb92dc8980e09119c:1", + "last_update": 1710785329, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1121", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710785329, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710754081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "889855651147612169", + "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:9", + "last_update": 1710813189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710813189, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790753, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "889855651147612176", + "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:16", + "last_update": 1710795190, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795190, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710792313, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "890019478399877121", + "chan_point": "98c43b13d184bb3e4f729e03f477d12c0f04af9c979f5b905371ab330b705c1a:1", + "last_update": 1710845484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710845484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "55", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710700501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "890105240352456705", + "chan_point": "549dee2681643c82424ca3917a8612a8299d378d515d6389c6f0c2a0cbc1e54e:1", + "last_update": 1710838854, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "10000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "74", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710776329, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036fe0795980588a1489d2ddf11f47a0ac36671e2cbff46bc4ace52738f15a1956", + "node2_pub": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "channel_id": "890283361157185537", + "chan_point": "74dbaf90b518e0fab1b2fd7360ec9c584438e422ed0beefcb82f2f0727c5ebfd:1", + "last_update": 1710778765, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710778765, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710771715, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "890316346463027201", + "chan_point": "ee471cdfcd778ba38848b2d5c6e7dabc4cdb9a280ecda1eacccbd76b8df77f3e:1", + "last_update": 1710825908, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "340", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710825908, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710756696, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "890336137740222464", + "chan_point": "6f267a3a120c625b7dce07faf19b647ec53777a4a297f69f704861371b09c2f6:0", + "last_update": 1710024268, + "capacity": "76000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1200", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710024268, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709834584, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "890507661605732359", + "chan_point": "b911aa7d9350b8891554835262a7856fc2bf670cebc2463d6c5141892a74e164:7", + "last_update": 1710838363, + "capacity": "16000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710788281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710838363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890507661610319873", + "chan_point": "5ed0d6486be9fd4241fe4b7c44070cdfda10570c06857709f6501f6b30246e6d:1", + "last_update": 1710831319, + "capacity": "16000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710788281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710831319, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d8853c04b69ce55c93d574bbb614e31f2654fb2f4049483774792cfd8d9af732", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890521955192995845", + "chan_point": "e79bcf5ea0a02a36940c60c33c8c1846a652a9ef7ed2781975d1fbcf07e47aad:5", + "last_update": 1710847556, + "capacity": "16000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710847556, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710788281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "890537348345036803", + "chan_point": "d4137b4b3afabee85f2ee2b23ce074ca807bca0c584ebf18526e1af2c2aaabc7:3", + "last_update": 1710847514, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710818804, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710847514, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "890817723814248449", + "chan_point": "183644bebe6807d9960f5a032a4ce87f8363f8a796efea39285f3c495e6cf892:1", + "last_update": 1710853964, + "capacity": "25100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1708", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710853964, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710808030, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "channel_id": "890834216531001350", + "chan_point": "789018c2cbfaf1b21a41991bb147f9866f7c7c8605235c990f4950685409823e:6", + "last_update": 1710816510, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710816510, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "9", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710765004, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "890918878931189760", + "chan_point": "1d0e4c810323cf04490a6a977ebcfb0b8efdea5cdc6aeab9d79e524dc4f42a8a:0", + "last_update": 1710816263, + "capacity": "300000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "105000", + "last_update": 1710788861, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "297000000", + "last_update": 1710816263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "890970556045983746", + "chan_point": "0346e8a51bc2e61160e02f2191c9dd3d5f15dca08d30a933b69fad8db8ccd37b:2", + "last_update": 1710788281, + "capacity": "88888888", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710755091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710788281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890970556045983759", + "chan_point": "0346e8a51bc2e61160e02f2191c9dd3d5f15dca08d30a933b69fad8db8ccd37b:15", + "last_update": 1710802429, + "capacity": "88888888", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710799081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710802429, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890970556046114830", + "chan_point": "abb7dcbdf515266a788b6acfd636cf18b88bf847de190e9b966334ce6f3cc241:14", + "last_update": 1710848353, + "capacity": "88888888", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "890998043747811342", + "chan_point": "6a82f93ea79d3786b71052e86bd96a41c6fc94e44f37dc32de6af24572564c15:14", + "last_update": 1710838535, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710838535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "890998043747811345", + "chan_point": "6a82f93ea79d3786b71052e86bd96a41c6fc94e44f37dc32de6af24572564c15:17", + "last_update": 1710805241, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710805241, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710777848, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "891011237861851137", + "chan_point": "59d5749ec7c46ce5f6913e3893ba04c600db0efb05be79fda5cbacbbbf071d5d:1", + "last_update": 1710844729, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "662", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710844729, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710302904, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891086004748681217", + "chan_point": "dcf80c10558dcbc8aaafd325bff0b8c2e847e36ee3f77a729121eaaaf3e2c52d:1", + "last_update": 1710842281, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710842143, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710842281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891086004748681221", + "chan_point": "dcf80c10558dcbc8aaafd325bff0b8c2e847e36ee3f77a729121eaaaf3e2c52d:5", + "last_update": 1710803683, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710776981, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710803683, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891087104157220867", + "chan_point": "80656ab60c3125d0c2330841e0e579d52546aa3c51e5e38a0b900aba4bbb36eb:3", + "last_update": 1710829389, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710829389, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "891088203768856582", + "chan_point": "2944e0642d5f48923d1b2975b6ecfbe57d53a741cdb477dee8041fd0dfd9a660:6", + "last_update": 1710820457, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710820457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710806281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891088203768856583", + "chan_point": "2944e0642d5f48923d1b2975b6ecfbe57d53a741cdb477dee8041fd0dfd9a660:7", + "last_update": 1710840850, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710840850, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891088203768856585", + "chan_point": "2944e0642d5f48923d1b2975b6ecfbe57d53a741cdb477dee8041fd0dfd9a660:9", + "last_update": 1710843632, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1841", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710843632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710804481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "891147577330434057", + "chan_point": "326b7165f8a8bf4cdfc713c6b52e295642a9d28bd1424a2d1675ff9ac4590dad:9", + "last_update": 1710827881, + "capacity": "35024288", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "5500", + "fee_rate_milli_msat": "3714", + "disabled": false, + "max_htlc_msat": "34674046000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "34674046000", + "last_update": 1710827881, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "891176164630134785", + "chan_point": "1e85a3afa255ec21c92522be49ce314600a57a26a69b4a3fa18cba4d2a9245e4:1", + "last_update": 1710831421, + "capacity": "20100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "19899000000", + "last_update": 1710831421, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "826", + "disabled": false, + "max_htlc_msat": "19899000000", + "last_update": 1710831032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891176164675026951", + "chan_point": "6b374e693fcc44e54167b958a70568c16dc6a7c01d4f3e03de58ed5caf622fb5:7", + "last_update": 1710838131, + "capacity": "16829112", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "16660821000", + "last_update": 1710838131, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16660821000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891176164675158027", + "chan_point": "6466c59736d408e10a11e2127710bd94b025f148f35a2db6ba4069b19af89a23:11", + "last_update": 1710799598, + "capacity": "186058085", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "184197505000", + "last_update": 1710783358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "184197505000", + "last_update": 1710799598, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891176164675223563", + "chan_point": "be9c2008ad15ab4dbfe34aab8f9829410f7a342479416d2715d1eba5feb08145:11", + "last_update": 1710831430, + "capacity": "333333333", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "330000000000", + "last_update": 1710831430, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "330000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891181662246666250", + "chan_point": "7219175edcc8d63018d094e6c3e45c5edfb0bb889207ede2306514878cb2e968:10", + "last_update": 1710790081, + "capacity": "46094394", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710730652, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "45633451000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "891297110893592577", + "chan_point": "a98fd8d47d454a0bbef55468f69cc03dc22c55506eee1a569c4773f6b4150631:1", + "last_update": 1710806758, + "capacity": "25100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "322", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710800551, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1249", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710806758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891310305098596356", + "chan_point": "888876aa8768df1c36ab32aea53fee372a9d661c297f2c0f282378027e51466f:4", + "last_update": 1710842454, + "capacity": "400000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "396000000000", + "last_update": 1710838681, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "396000000000", + "last_update": 1710842454, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "891772099941040129", + "chan_point": "afbe18facda34e8727ef64d6bf85aa2560da91b85020307fcd7e72048508f441:1", + "last_update": 1710814922, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710814358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710814922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891774298923008001", + "chan_point": "786d85b944a16dbd21282eb26a229114bd3c4e6c187e8a5ed6d753be96d18c52:1", + "last_update": 1710856681, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710792825, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "891803985800986625", + "chan_point": "5dc3a53fe8fc448c6fe9fb589e785173d1f2ae5148bc45de8b322265604fe950:1", + "last_update": 1710816225, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710816225, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2250", + "disabled": false, + "max_htlc_msat": "10000", + "last_update": 1710769273, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "891820478477369344", + "chan_point": "d78c7fa0571fce5b5923bdcffbceeece4782fb9df762ab08651ea1c64becf797:0", + "last_update": 1710856389, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710791025, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710856389, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "892074465645494273", + "chan_point": "b50e6b5ed9ee90291dfdf981a3b67e2d6548c9bc745ad946130340cd2b44d26b:1", + "last_update": 1710643142, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1709649248, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710643142, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "892074465645494281", + "chan_point": "b50e6b5ed9ee90291dfdf981a3b67e2d6548c9bc745ad946130340cd2b44d26b:9", + "last_update": 1710801136, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710801136, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "299", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710801136, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "892175620656660482", + "chan_point": "a4cd35df1aa434f71a09a6a084324faae4d60bffff01cce83130c917457e3808:2", + "last_update": 1710854911, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "5352", + "fee_rate_milli_msat": "3345", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710854911, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710775854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "892364736701595649", + "chan_point": "073878b3efb96104ff7a0740f2e72d61774578740b491b9d3e3d6d9a97f59471:1", + "last_update": 1710836574, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "5880", + "fee_rate_milli_msat": "2940", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710807742, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "892391124966703104", + "chan_point": "99042e998da35954718904204837325d73faa49e0956987b407e3372ae85d9f5:0", + "last_update": 1710807743, + "capacity": "5417500", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "51898", + "fee_rate_milli_msat": "5242", + "disabled": false, + "max_htlc_msat": "5363325000", + "last_update": 1710807743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "5417500000", + "last_update": 1710795190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "892476886835920896", + "chan_point": "d2be799b55f95db7bd69a952cb50926d35664293b4ab562b2dbb8fa8b6ce7c4f:0", + "last_update": 1710850635, + "capacity": "16416666", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "16416666000", + "last_update": 1710775535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1702", + "fee_rate_milli_msat": "919", + "disabled": false, + "max_htlc_msat": "16252500000", + "last_update": 1710850635, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "channel_id": "892613226335764480", + "chan_point": "412185e9d19aea85e45d9ab0ae7e4fe307e2d9f40cc88cdd0c0965bbddb18deb:0", + "last_update": 1710851758, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "305", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710851758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "892613226335764485", + "chan_point": "412185e9d19aea85e45d9ab0ae7e4fe307e2d9f40cc88cdd0c0965bbddb18deb:5", + "last_update": 1710849850, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "599", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710756358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710849850, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "892623121899978752", + "chan_point": "8e71461f277563ecca33e8aeea08b93f09051aadd038922a90e5eb3d7c815478:0", + "last_update": 1710840661, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "23", + "disabled": false, + "max_htlc_msat": "1960200000", + "last_update": 1710840661, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710821050, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "892839725692485633", + "chan_point": "6e06d79c8d3819474254f598539bcd1cb58725e895d4d9392298a6ca8398e416:1", + "last_update": 1710849481, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "node2_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "channel_id": "892878208667025408", + "chan_point": "09c3a29e4d743f0be5460529760a6a62488ace254f89d537f6ab0e73312c6f65:0", + "last_update": 1710855079, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710855079, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": true, + "max_htlc_msat": "1980000000", + "last_update": 1710743539, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "892958473079488513", + "chan_point": "ee5c98899edd2f39e69c1f09fdf01f677f113c873e27c67cc3826604d9fffb87:1", + "last_update": 1710799753, + "capacity": "2950000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "2920500000", + "last_update": 1710780722, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2920500000", + "last_update": 1710799753, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "892984861294002177", + "chan_point": "e2a0450a2b7ee14fd9ec0a186ed5b46c55a3e302c95c96d0e15ca8bc26946df7:1", + "last_update": 1710820354, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "520", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710820354, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710756696, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "893122300194717699", + "chan_point": "d0e7662192eee27e3340207d040b1345cef8ac14704525207bda2acc83adf399:3", + "last_update": 1710848329, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710848329, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710839931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "893179474859065345", + "chan_point": "c1a93914af48ad632774f427cffe8aeaec3023d0318c4dac54b75e270ab4171f:1", + "last_update": 1710811949, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "5000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710800284, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710811949, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "893215758799798273", + "chan_point": "cfa8650babe493c66405774a745a911aff86656ef151c20185225ced026ec8e4:1", + "last_update": 1710856106, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856106, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710756786, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "893215758837481473", + "chan_point": "66237ef935ebf01a06db6e782faa79208e26aedf0afea726858d3f04d7a86fa1:1", + "last_update": 1710778425, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778425, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710769269, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "893283928513904641", + "chan_point": "ced9802a90b8d23d837295caad8133e13000d3c68d456e90d1a3a94d1f8f296c:1", + "last_update": 1710837891, + "capacity": "27044530", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "26774085000", + "last_update": 1710837891, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "26774085000", + "last_update": 1710835344, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "channel_id": "893321311815008257", + "chan_point": "a8129b2d4cb6a82bffa0cea0c710db9ecc512bdddf6852cc129b6914a9ee2b7d:1", + "last_update": 1710853254, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838543, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710853254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "893526920477802497", + "chan_point": "94aad85fc31c03bfa97d829f86ddeae70a6754532fd67bfdb24b853b39b45c2d:1", + "last_update": 1710794571, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710794571, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710776347, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036fe0795980588a1489d2ddf11f47a0ac36671e2cbff46bc4ace52738f15a1956", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "893526920477802498", + "chan_point": "94aad85fc31c03bfa97d829f86ddeae70a6754532fd67bfdb24b853b39b45c2d:2", + "last_update": 1710778147, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778147, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710775315, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "893537915591065600", + "chan_point": "d8ad7f1d03911c10ad7013666cb890d61ed06078e6772c771e4bb699d5ce24fc:0", + "last_update": 1710840771, + "capacity": "12928658", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "128", + "disabled": false, + "max_htlc_msat": "12799372000", + "last_update": 1710796523, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "517", + "disabled": false, + "max_htlc_msat": "12799372000", + "last_update": 1710840771, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "893599488252772352", + "chan_point": "ca6faddd6b7eeac3e73dba9b245641c994249cf2dc1cedfc4918fdcdfe3e441e:0", + "last_update": 1710827934, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "889", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710827934, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "893600587721605121", + "chan_point": "58aee236193915286bb417185495b192a4e5128741302cf963466e8a43ca715b:1", + "last_update": 1710844134, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "10000000", + "last_update": 1710356804, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "675000000", + "last_update": 1710844134, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "893782007232331777", + "chan_point": "ca07b70fe79264ce94b7c21592b1be5643a13ef90fac011c8b75ee7c2f4ca03b:1", + "last_update": 1710820883, + "capacity": "80000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "4910000000", + "last_update": 1710820883, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "79200000000", + "last_update": 1710790254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "893803997415079936", + "chan_point": "9751d6cec683f435b087e042b29ac009bdfb7f5538aeedda49d92764de4380f4:0", + "last_update": 1710810978, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "4566", + "fee_rate_milli_msat": "3653", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710810978, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710755830, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "893820490095919105", + "chan_point": "0ef0714101852b6ff7d2ed00f02b5fce8b72573488361fe64cd6b4d07227f71b:1", + "last_update": 1710833334, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710755830, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710833334, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "893834783817859073", + "chan_point": "9d5956d9a475b7e93767489b12d4fcd7edadebef73337441e358b156fffe10a6:1", + "last_update": 1710856361, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710856361, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710844560, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "894117358259339267", + "chan_point": "ed3bf9ff6eca6522555a96d510f6976fc6badd87320fbbc1df8190aa4bb07ee5:3", + "last_update": 1710851231, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1087", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710844216, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710851231, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "894195423551356929", + "chan_point": "1caf42d9ecbe23f4791cb8c94119bd77b41d251edd409491a2d73c24a230fad5:1", + "last_update": 1710850637, + "capacity": "6044616", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "1644", + "fee_rate_milli_msat": "49", + "disabled": false, + "max_htlc_msat": "5984170000", + "last_update": 1710850637, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "6044616000", + "last_update": 1710776263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "894195423551356930", + "chan_point": "1caf42d9ecbe23f4791cb8c94119bd77b41d251edd409491a2d73c24a230fad5:2", + "last_update": 1710837521, + "capacity": "16416666", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "357", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "16252500000", + "last_update": 1710837521, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "16416666000", + "last_update": 1710776354, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "894388937633300480", + "chan_point": "e7b4402c33c52131e5ea4e0866e7f2a3c0c579403761972d6ee43d4d19982b22:0", + "last_update": 1710681712, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "954", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710595864, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "894647322887716864", + "chan_point": "904c884e96788fc281949714f7c03fb30dda6c544295c367708b0ef3b49cd7c4:0", + "last_update": 1710837235, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710837235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710822654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "894650621489053696", + "chan_point": "0fc7f1e247d222dd3ee04cdcd793e6abc0613b968a8ffd211ee39c507726061b:0", + "last_update": 1710849335, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710849335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710828002, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "894816647619280897", + "chan_point": "f3b565fc8b85693bc6f4fadd825e5a282f884aaac2c5423185434c3da7ee5095:1", + "last_update": 1710785048, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710730650, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "894843035960147968", + "chan_point": "cfcfe8f1dc8cf0f20a6292facfc00d7e820e89cff3de64bd057801fc3ec3fc8d:0", + "last_update": 1710757535, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710757535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710312917, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "894887016460189697", + "chan_point": "cf33839abd2cb02af6981fb58c16c9ccfda9876e6f258d050c05dc19c97bf979:1", + "last_update": 1710856363, + "capacity": "3500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710792827, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710856363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "895058540187353088", + "chan_point": "e23020b018bea64b0a950b0c60686a6e78405eb62bfc11c143b5edf0dbd8ffcc:0", + "last_update": 1710845574, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710845574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "200000000", + "last_update": 1710828479, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "895437871736815616", + "chan_point": "3a40021f9958722258455c2e5499124242786eeee93f1a42d236ceed36e861f0:0", + "last_update": 1710731035, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710731035, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1709724541, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "895630286316109825", + "chan_point": "ed798db81091601e6d3eaa5b3439e654d6d341dcebb1b0ef592fa204e20caf0a:1", + "last_update": 1710845971, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "8000000000", + "last_update": 1710775543, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "30", + "fee_rate_milli_msat": "31", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710845971, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02ab80cce7e7dff00c964e4b2006eb4c8af4e71187572156a88275d3c126b90b66", + "channel_id": "896055797301116951", + "chan_point": "06643d87bd247596ecf2e34130542cceba2d5075c5a2bbbb8ca86156d5dacfc0:23", + "last_update": 1710856039, + "capacity": "4500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "4455000000", + "last_update": 1707686941, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "278", + "disabled": true, + "max_htlc_msat": "4455000000", + "last_update": 1710856039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "896650633019129857", + "chan_point": "ff46179b5b533306e297b3d962ba256462d913e0ee097e4538e829b94c7aeb59:1", + "last_update": 1710822835, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710781030, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1550", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710822835, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "896672623394095105", + "chan_point": "0b0a82d6fbb784a009e6a19c9de1a8b0f67bf4928aec3923b0fe7b04b54abaa1:1", + "last_update": 1710853267, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "60", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710783988, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2190000000", + "last_update": 1710853267, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "896692414404231169", + "chan_point": "28bad9c4d16c70e5beecaa00eb8eceb50a5629c73b3eefe4b404cdc6b2d462fc:1", + "last_update": 1710825484, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "3419", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710777682, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "897487361531641857", + "chan_point": "5c8f5688296b403ac44bf5fc47e69071a606d1b1045b0f46a172d89458ebd2ae:1", + "last_update": 1710825484, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1090", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710320815, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "897503853995032577", + "chan_point": "fb87ed329859a4e5803ed742ac43130d91ffd20eff70b1f266fb8781c68664b8:1", + "last_update": 1710757322, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710757322, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710064588, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "channel_id": "897846901912305665", + "chan_point": "d484798b7859376b67a07c58b67d57f7e7262858304d80a203b5dd2fa09b7ae9:1", + "last_update": 1710846093, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "157", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710846093, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778840, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "898362572660342784", + "chan_point": "323c7d26cf44221c29e177b3cb04061cdc479b0e307dda0e65c89f1e09050505:0", + "last_update": 1710856517, + "capacity": "51764750", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "51247103000", + "last_update": 1710856516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "51247103000", + "last_update": 1710856517, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "898938716679176192", + "chan_point": "1edf84a0e977ed957b1d66f74192fdb81f857cedb3b08bf33ea6670007a604c4:0", + "last_update": 1710853283, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710777835, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "547", + "disabled": false, + "max_htlc_msat": "6270000000", + "last_update": 1710853283, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "channel_id": "899076155690909697", + "chan_point": "4b78e9775001132a318ff64ca4536ca7c52fdba585d45b6bb7a9a5b843ded665:1", + "last_update": 1710823639, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710822835, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "220", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710823639, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "899683086217183233", + "chan_point": "8695cd7ec166dd9c1b720a4d25df16f0047b869f69ecdf51cf913c8fe238b3d7:1", + "last_update": 1710825485, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "110", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710825485, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "29479", + "fee_rate_milli_msat": "14739", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710817271, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "899683086217183236", + "chan_point": "8695cd7ec166dd9c1b720a4d25df16f0047b869f69ecdf51cf913c8fe238b3d7:4", + "last_update": 1710856264, + "capacity": "12625395", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "100", + "fee_base_msat": "2", + "fee_rate_milli_msat": "158", + "disabled": false, + "max_htlc_msat": "12499142000", + "last_update": 1710854910, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "413", + "disabled": false, + "max_htlc_msat": "12625395000", + "last_update": 1710856264, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "899802932799995905", + "chan_point": "87dbbc3dc88280f0c100593130075caca22ce2e357b2eb4d577c88ef3a6af572:1", + "last_update": 1710854909, + "capacity": "5442253", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "28", + "disabled": false, + "max_htlc_msat": "5442253000", + "last_update": 1710786986, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "988", + "disabled": false, + "max_htlc_msat": "5387831000", + "last_update": 1710854909, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "899934874331316224", + "chan_point": "c29809316b6dbb9947f719a1a1cadcd41ff483a2bce0f5bb4335f89eaa2141b1:0", + "last_update": 1710842221, + "capacity": "8149610", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "8149610000", + "last_update": 1710842221, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "3470", + "fee_rate_milli_msat": "4626", + "disabled": false, + "max_htlc_msat": "8068114000", + "last_update": 1710810977, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03db3386adf0e2221555a7f3fc7282eca60c5eabfed8e1789d591a9241c342c091", + "node2_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "channel_id": "900579188194344961", + "chan_point": "fbf5726e17d4bbf99c6cd0277d0a91aecb3f46936d1c0677a5a1d5a6834e27ef:1", + "last_update": 1710719763, + "capacity": "50000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1710719763, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "49500000", + "last_update": 1701327976, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "022b0de0e9156024d3f1ba54b3602f21eae93c4087a552140d25805175d3d9a1d3", + "channel_id": "900586884660658177", + "chan_point": "18d1cd672fca25bdee14d9c8dcd5c1bd3cb9aeaaa4996e9fa4afa1720ae33c88:1", + "last_update": 1710855388, + "capacity": "360000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "356400000", + "last_update": 1710855387, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "356400000", + "last_update": 1710855388, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "900623168648970241", + "chan_point": "63b7c4332fbba23e2a094ad38ff9df10266bfa881dca949577e21a1bc520067c:1", + "last_update": 1710851954, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710846235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710851954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "900653954925920257", + "chan_point": "a9a2f5c029db95b5f66d5f73ec3db47e05dcdd332f4da4d1552bd2a4a67ca9fc:1", + "last_update": 1710807743, + "capacity": "12312489", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "32", + "disabled": false, + "max_htlc_msat": "12189365000", + "last_update": 1710807743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "12189365000", + "last_update": 1710254954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "900784796753264641", + "chan_point": "22b4d8ea9c88febed493587429513aa35fe3f5f0f159aa366c0fbabe6eab203e:1", + "last_update": 1710857048, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710782025, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710857048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "900911240727166976", + "chan_point": "648897d747d6462bb815f84d1b0cf8b9ec87cb9aa76f806a55cc40ea165fab3d:0", + "last_update": 1710837520, + "capacity": "15712344", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "787", + "fee_rate_milli_msat": "444", + "disabled": false, + "max_htlc_msat": "15555221000", + "last_update": 1710837520, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "15712344000", + "last_update": 1710256226, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "901597335843504129", + "chan_point": "79e26ede939fb09edc4e2df45c4f26a517a7ee188bf336b392e88944468cc9b7:1", + "last_update": 1710855809, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710843531, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "599", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710855809, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "902036040901853185", + "chan_point": "dc0e8ea7a3d9ff7252a26c72fcff828f105e970584c08c29f7216d6d49f22cdb:1", + "last_update": 1710797734, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "80", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710796922, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710797734, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "902083319902568448", + "chan_point": "91c822587c576e11376d1a0db73bacf277502ec90fb7ef33a519aa40239fa62b:0", + "last_update": 1710834238, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710833564, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "504", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710834238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "902437362674958337", + "chan_point": "e7e8ca1cd9b79a6cc5b93b5b28e8cb5129e0e31f24987a5da588ccc2b1554e70:1", + "last_update": 1710833133, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "80", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710833133, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "28", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710756698, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "channel_id": "902454955058790401", + "chan_point": "8de56f42ca25fe06c55819ce7a5d08087ef208d218513bd50007171bfa72fb75:1", + "last_update": 1710849343, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710849343, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "299", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710756358, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "902555010536505345", + "chan_point": "6961497cb9f493eded56bf4c9366f49dbf4883de337987d3dcb450d6408d1bb0:1", + "last_update": 1710827331, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710827113, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710827331, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "902555010536570880", + "chan_point": "11ce565b1f6d471da3f54d9a6165213734c49c476b7aceb71595c26438ede675:0", + "last_update": 1710853279, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710845484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "288", + "disabled": false, + "max_htlc_msat": "4020000000", + "last_update": 1710853279, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "903624835328049152", + "chan_point": "1c22101dd1011f942afb18f3b01b1611f44ced4a329b5b8993e2c90ce869c543:0", + "last_update": 1710850348, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710850348, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "252", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710843238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "903624835440967680", + "chan_point": "e308cc0ab7eb0c9dda103ca49f2186acfc3239809c27d25725f3d1eb2870b6c7:0", + "last_update": 1710795190, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "756", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710789238, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710795190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "903775468383240193", + "chan_point": "340be371ec84dbe5025805906b0a5f28732d79911d12a7d56f7aa708d1855fb5:1", + "last_update": 1710812213, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710636569, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1111", + "disabled": false, + "max_htlc_msat": "3777777000", + "last_update": 1710812213, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "904165795090530308", + "chan_point": "1401ebf5b6dc3ab27932bcaa9a185bb0b039b6d523b9d68984c1234c399e7e0f:4", + "last_update": 1710839035, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1248", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710819358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710839035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "904165795090530313", + "chan_point": "1401ebf5b6dc3ab27932bcaa9a185bb0b039b6d523b9d68984c1234c399e7e0f:9", + "last_update": 1710838700, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "602", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710838700, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710803035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "904165795090530315", + "chan_point": "1401ebf5b6dc3ab27932bcaa9a185bb0b039b6d523b9d68984c1234c399e7e0f:11", + "last_update": 1710848122, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1600", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710810235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "73", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710848122, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "904192183207723009", + "chan_point": "59332efb90e365807de7d491a05855029c8adb7d0060b124cff19bcfdbc10254:1", + "last_update": 1710855413, + "capacity": "10101010", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710853558, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710855413, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "904362607750152193", + "chan_point": "de3570cdf2cb8978730951a6a474cefa32d0fc2245d731d5477920f3d07899cb:1", + "last_update": 1710849032, + "capacity": "80000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000000", + "last_update": 1710848548, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2002", + "disabled": false, + "max_htlc_msat": "79200000000", + "last_update": 1710849032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "904379100430532609", + "chan_point": "6e2ab5573253b612d91f0f5a957af770f5bbed188c51f3f754b0b22c0e89b35a:1", + "last_update": 1710827743, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "334", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710827743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710825485, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "904553922785443840", + "chan_point": "281459d204deabb7a23b8f5d2ee09ba38df3e47ebe2e1653e809c51eaaf75909:0", + "last_update": 1710851758, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "504", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710787438, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710851758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "904646281748021248", + "chan_point": "8f3d58ae324262465bdfa1e1d4391c1ebad7e7d7576f3c27e1f228f34558ee1b:0", + "last_update": 1710791779, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1143", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710791779, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710254915, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "904813407494864896", + "chan_point": "676fb7fee2e2ed1bcd1565b42249111e900c7edbeee2e82cb241cbd3af484e82:0", + "last_update": 1710849481, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1550", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710783235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "904816705950580736", + "chan_point": "d5ced6c98cc691ca7ed652f1ae0f77fc14c7b1b05ab1a340ed354363f49b961f:0", + "last_update": 1710839638, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "252", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710782331, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "905254311684341760", + "chan_point": "73ec5fef61ec16ba615daf596cf52305e9ed7e096c118f822eabf8616a059024:0", + "last_update": 1710827833, + "capacity": "75000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1197", + "disabled": false, + "max_htlc_msat": "74250000000", + "last_update": 1710783838, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "74250000000", + "last_update": 1710827833, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "905260908694208512", + "chan_point": "a24361793736b046f77a06b3b45f0d1695b766378a319b20f71fe7568e0b4162:0", + "last_update": 1710790081, + "capacity": "60000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "882", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710785638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "905312585750020097", + "chan_point": "4c39599e6789f6aca2c255f00982f434ce9a26bde1d687d2bbf97bc06349ca33:1", + "last_update": 1710851722, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "103", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710851722, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "210", + "disabled": false, + "max_htlc_msat": "1000", + "last_update": 1710814013, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "905851346364071937", + "chan_point": "8efa558c5d4f9fd1e15214f59b0baf7a1635c47f436ec0011da9a15728b725da:1", + "last_update": 1710852912, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1074", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710771021, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710852912, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "906005278028201985", + "chan_point": "d2d492a21d03428fe127976f75597cd9d62856c3c2603fff11dfc98725d768b2:1", + "last_update": 1710844832, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "516", + "disabled": false, + "max_htlc_msat": "675000000", + "last_update": 1710844832, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "675000000", + "last_update": 1710249572, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "906008576572784643", + "chan_point": "da862edd8d4edfe35c656cd6e1497e88d41af38bbc55a42cf215e4ade5c93546:3", + "last_update": 1710853558, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710853558, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849960, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "906008576572784647", + "chan_point": "da862edd8d4edfe35c656cd6e1497e88d41af38bbc55a42cf215e4ade5c93546:7", + "last_update": 1710801681, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710801681, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "99", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710780001, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "906089940340047872", + "chan_point": "c17197cb792f365d7a22fbb2e40d102fa72754b40fd35cc0e4a8827f8a117082:0", + "last_update": 1710855001, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1710855001, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710841067, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "906356022148268032", + "chan_point": "3e6fb02329691cc1ac41f9dabfd2adaff1de4d07552615386387d557bc418de7:0", + "last_update": 1710775110, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710775110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710686087, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "906413196839944193", + "chan_point": "971be419180492c9dc08dcb3b4b51e24b7072e8a36a676fd4873db66e1318ef1:1", + "last_update": 1710836831, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "2718", + "fee_rate_milli_msat": "924", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710835407, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710836831, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "906415395757752321", + "chan_point": "83afd531ec95bfaf4367604bbd71ff564b1168801fbf28df00980972456c3cbf:1", + "last_update": 1710845480, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710845480, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1249", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710828358, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "node2_pub": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "channel_id": "906699069780066305", + "chan_point": "563834d041662028eb0ae34fb98bdf9646d21de2b3aec3c486dfbb1bd6c4287b:1", + "last_update": 1710847222, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1436000000", + "last_update": 1710804929, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "6", + "disabled": false, + "max_htlc_msat": "3507225000", + "last_update": 1710847222, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "907088297038446592", + "chan_point": "6a5795e372ac56b4555dcc179a1f125d39f2f48800f079cd3ef8584cafae4817:0", + "last_update": 1710848353, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710774858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03518cfddeb18d50db01fc316984bac961a6d978115a52259cd5d2e47fbe363e94", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "907149869565607937", + "chan_point": "a5696d10831ddd16cb46253cb0ddd8ecaf8a2a8ed7517d78449ee09a00d7e533:1", + "last_update": 1710811646, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "450000000", + "last_update": 1710811646, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710018886, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "channel_id": "907167461862735872", + "chan_point": "b3125bff3e6b5448d162233e7569de581037fdebff94c78b0648108793084550:0", + "last_update": 1710827141, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "125", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710827141, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "620", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710771238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "908236186969309184", + "chan_point": "eedddbd82a167710a122bad1ce4229bbbd6fe5522e5e8448c0666eb68aec4a0f:0", + "last_update": 1710849850, + "capacity": "7000467", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "6930463000", + "last_update": 1710755091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "6930463000", + "last_update": 1710849850, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "908240585088761857", + "chan_point": "62a6bfdf6f16889b5b72c63c3e3551c9b98e8ad5c60619b965b124b20ed69588:1", + "last_update": 1710854318, + "capacity": "4650000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710072529, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "1000", + "last_update": 1710854318, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "908252679706509314", + "chan_point": "12632e2bc4ef20018a7c26a4b58ae94659dbd032f56f6b5df0a536fbc3a981d1:2", + "last_update": 1710842281, + "capacity": "200000000", + "node1_policy": { + "time_lock_delta": 18, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "198000000000", + "last_update": 1710842281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 18, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "198000000000", + "last_update": 1710795848, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908409910008217600", + "chan_point": "47e3870351dbacc24b26ec18d21afdc80cebf690573576a15e626335f5bb50ab:0", + "last_update": 1710788650, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1512", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710785638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710788650, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908552846380564480", + "chan_point": "d2fb0b1538d787f2b57b9ce48ccb0eb70098885506f4c06dc5c0107ab1968787:0", + "last_update": 1710783838, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1071", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710783838, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710748936, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "908655100992880641", + "chan_point": "0acfd2470db4eb06268b3f22b4c45c0fdcc11d10b6519577662e54823a0f264e:1", + "last_update": 1710841427, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710841427, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710835874, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "908677091317514240", + "chan_point": "60ea6bc0bf78c321439693d86d80a3b9eb9208f358941ab5595624e926d7b86e:0", + "last_update": 1710853799, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710772034, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710853799, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "908689185928642561", + "chan_point": "6f6e4a04e9fab0e5d24c77e06b80a0a0e1cc6600a802b00d839ae25d1bdef1e8:1", + "last_update": 1710844254, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710839050, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710844254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908715574232940544", + "chan_point": "681d6807b9e787832e69ce657201e3c61d3b7937dbc0bbe840d064350a56ae58:0", + "last_update": 1710785638, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "441", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710785638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1707848075, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "908718872592187392", + "chan_point": "fbb0146d1c27898e7aed5d29383c29c6e4187219cd2475bcf320bd3962beaf86:0", + "last_update": 1710820457, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710820457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "819", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710807238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "node2_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "channel_id": "908794739123027969", + "chan_point": "dddb9425d70ccac822a0ba70caea446c35fd5b4aa688f0cfa811fade74aeb4d5:1", + "last_update": 1710833461, + "capacity": "2750000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "2695275000", + "last_update": 1710833461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "2722500000", + "last_update": 1708774500, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908833222024953856", + "chan_point": "e0e3646f1a14bd290358cb497a6adad15c9514e9f81e06db1669f897106b2343:0", + "last_update": 1710855838, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "24", + "fee_rate_milli_msat": "2361", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710762647, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "249", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710855838, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "037ff12b6a4e4bcb4b944b6d20af08cdff61b3461c1dff0d00a88697414d891bc7", + "channel_id": "909009143810555904", + "chan_point": "47c4d4b5091f746fffeabe3c59b5e80c4117a2f55adc24599dcbc76037baae6d:0", + "last_update": 1710815648, + "capacity": "5263278", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "100000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "5210646000", + "last_update": 1710798106, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "5210646000", + "last_update": 1710815648, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "909127891028279296", + "chan_point": "d8f25f010648f84f5121afb53f5b1a4b23b7bc7f4aa7235ac02f1e700e34ee80:0", + "last_update": 1710796438, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "882", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710796438, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710777007, + "custom_records": {} + }, + "custom_records": {} }, { - "channel_id": "892895800825741312", - "chan_point": "997c879c28e44b11493fffc1e3bdd41bfab8ebcb815a3b8b0c6d0f7baeabe86b:0", - "last_update": 1710607091, - "capacity": "50000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "909179568003022849", + "chan_point": "baa137019225e3f3722c0d99f881f893ec4458b928b10a5ab1d82f77a0c34c43:1", + "last_update": 1710828747, + "capacity": "50000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "49500000", - "last_update": 1710607091, + "fee_base_msat": "0", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710828747, "custom_records": {} }, "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710821035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "909311509549088769", + "chan_point": "b922eb33b01690ab70e096663b535aebbae04dd8820bd12d01e67181bb649a12:1", + "last_update": 1710856854, + "capacity": "10000000", + "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "49500000", - "last_update": 1705276253, + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710841428, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5", - "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856854, + "custom_records": {} + }, + "custom_records": {} }, { - "channel_id": "892768257448935424", - "chan_point": "7b2351fe1f9dc287e376f481ae3e52cb39ad417cb8387087b91054e300ddc486:0", - "last_update": 1710639306, - "capacity": "2000000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "909522615680565249", + "chan_point": "c887a5c2d22b13b96575a5db83f8aacda9e539eb9ec73ad83752c1669c53214f:1", + "last_update": 1710843032, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1310", "disabled": false, - "max_htlc_msat": "2000000000", - "last_update": 1710607091, + "max_htlc_msat": "9900000000", + "last_update": 1710843032, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 34, + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710821035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "909625969739366402", + "chan_point": "15e7e26c970899bab80128a61493ef8410e9fd1a15fa4ec9c7830353c9b209f7:2", + "last_update": 1710804119, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1798", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710804119, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, "min_htlc": "1", - "fee_base_msat": "5805", - "fee_rate_milli_msat": "6", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710639306, + "max_htlc_msat": "20000000000", + "last_update": 1710414556, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d", - "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + "custom_records": {} }, { - "channel_id": "915246673262739457", - "chan_point": "4c2e260ea115e813a1255e12a1597e2577216d37d770c3e2328170fc28ffb153:1", - "last_update": 1710772374, - "capacity": "100000", + "node1_pub": "02b91642f44270c805e275ae59289f35d464de20cd3a546e3252710db6bab7c965", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "909922837832728577", + "chan_point": "46cc2b8b35c5d42c2175aa3337540f71a861d70ef04e65db2eaab2c8ed208048:1", + "last_update": 1710841865, + "capacity": "250000", "node1_policy": { "time_lock_delta": 40, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710771876, + "max_htlc_msat": "247500000", + "last_update": 1710841865, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", + "fee_base_msat": "0", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710772374, + "max_htlc_msat": "247500000", + "last_update": 1710792632, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199", - "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + "custom_records": {} }, { - "channel_id": "888979340321554436", - "chan_point": "0055cf02d8bbc7125784e8fee50605969c6cfe76fa24cea14e9ae66a6be4a7c8:4", - "last_update": 1710793392, - "capacity": "2000000", + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "910070172455731202", + "chan_point": "ef0c127a0cee70011cc961b6a9d4df565d02eec3b0f89108cf046fa0a3a33500:2", + "last_update": 1710791064, + "capacity": "1000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "58", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710790557, + "max_htlc_msat": "990000000", + "last_update": 1710787639, "custom_records": {} }, "node2_policy": { + "time_lock_delta": 42, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710791064, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037ff12b6a4e4bcb4b944b6d20af08cdff61b3461c1dff0d00a88697414d891bc7", + "node2_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "channel_id": "910303268974821377", + "chan_point": "952333bd48adb3f320bc3f42b5be6f020a5d0d6a5be272f0b69eccdf302f3a5b:1", + "last_update": 1710817661, + "capacity": "3000000", + "node1_policy": { "time_lock_delta": 40, + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "2940300000", + "last_update": 1710817661, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "100000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710814306, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "910319761537892352", + "chan_point": "e357d028134934f07317e7160ac73a6521862f2d3f2f042e828fdf53eb204f72:0", + "last_update": 1710840163, + "capacity": "1990000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "222", + "disabled": false, + "max_htlc_msat": "1900000000", + "last_update": 1710778013, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1970100000", + "last_update": 1710840163, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "910347249392549889", + "chan_point": "77c7e11f89d02751adc6617367aa2c5c54f40df07608a3b8fcbc61c12b059657:1", + "last_update": 1710856631, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710827025, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710793392, + "max_htlc_msat": "4950000000", + "last_update": 1710856631, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + "custom_records": {} }, { - "channel_id": "886619788490833928", - "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:8", - "last_update": 1710791589, - "capacity": "2000000", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "910361543043186689", + "chan_point": "35989c59c96ed7067682113d339c364809e1064e8f53f22eea951a38a686b151:1", + "last_update": 1710854701, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9405000000", + "last_update": 1710854701, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710841102, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "910426414165262339", + "chan_point": "b1a26a7ab60d8e0711ecbb9e6db125131e434c6e110566b1456c2f80681b9dbe:3", + "last_update": 1710798607, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710789935, + "max_htlc_msat": "9900000000", + "last_update": 1710796731, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710798607, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "910426414165262342", + "chan_point": "b1a26a7ab60d8e0711ecbb9e6db125131e434c6e110566b1456c2f80681b9dbe:6", + "last_update": 1710840007, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710777972, "custom_records": {} }, "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "120", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710840007, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "910493484353191941", + "chan_point": "4485a7608cfc9c53184d7b6d2fe56bf0a77902c89bab595cfbd908390cdc5f91:5", + "last_update": 1710797221, + "capacity": "15000000", + "node1_policy": { "time_lock_delta": 40, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710791589, + "max_htlc_msat": "14850000000", + "last_update": 1710797221, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710778807, + "custom_records": {} + }, + "custom_records": {} }, { - "channel_id": "914391253219868685", - "chan_point": "acf8a90a8ba334f408b13a1e581538c6fcc3d2398293a59b5fcad77f26769ca2:13", - "last_update": 1710813189, - "capacity": "3000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "910568251362770945", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:1", + "last_update": 1710804302, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "660", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710804302, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710755882, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "channel_id": "910568251362770949", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:5", + "last_update": 1710849835, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "305", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710848402, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849835, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "910568251362770950", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:6", + "last_update": 1710849421, + "capacity": "100000000", "node1_policy": { - "time_lock_delta": 200, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "2000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", "disabled": false, - "max_htlc_msat": "2970000000", - "last_update": 1710789782, + "max_htlc_msat": "99000000000", + "last_update": 1710849421, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "2970000000", - "last_update": 1710813189, + "max_htlc_msat": "99000000000", + "last_update": 1710804835, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "channel_id": "886786914241085444", - "chan_point": "c34a46e8d7fd82170c9fc87cfcbc848c477a40799d638512bb60054aafd69cf3:4", - "last_update": 1710830106, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "910568251362770955", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:11", + "last_update": 1710822835, + "capacity": "100000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "159", + "fee_rate_milli_msat": "5000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710828768, + "max_htlc_msat": "20000000000", + "last_update": 1710822835, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710830106, + "max_htlc_msat": "20000000000", + "last_update": 1710447911, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "channel_id": "890068956432236548", - "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:4", - "last_update": 1710795190, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "910586942962008065", + "chan_point": "6f768219e7e0d4e027737cc74f115eefb63a54a50c44917d05c06beb69026f05:1", + "last_update": 1710803284, + "capacity": "100000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710790263, + "max_htlc_msat": "99000000000", + "last_update": 1710803284, "custom_records": {} }, "node2_policy": { @@ -4235,1129 +21999,1100 @@ "fee_base_msat": "1000", "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "1980000000", + "max_htlc_msat": "99000000000", "last_update": 1710795190, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + "custom_records": {} }, { - "channel_id": "889913925275484176", - "chan_point": "48cd67b0995fb7e49876f4e18fba74bbde68bf3c713b6cc5ceb93c4f3272f4ee:16", - "last_update": 1710829389, - "capacity": "2000000", + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "910646316663767041", + "chan_point": "cc8fb78edccff1ce73a63a78be2a32418855f7c21c139c8173d4ba904ea20bca:1", + "last_update": 1710856131, + "capacity": "20000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710789499, + "max_htlc_msat": "19800000000", + "last_update": 1710836025, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710829389, + "max_htlc_msat": "19800000000", + "last_update": 1710856131, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + "custom_records": {} }, { - "channel_id": "888987036985851905", - "chan_point": "094ff6c4b2ee8473bf7839d6d48c389576209ef4c581abb7abcab755bd20a4b8:1", - "last_update": 1710791182, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "910652913616551937", + "chan_point": "e435235eea52cf80e19571f372d6d7040a21984763af9662c4d6ba566b742526:1", + "last_update": 1710852124, + "capacity": "15000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710791182, + "max_htlc_msat": "14850000000", + "last_update": 1710852124, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710775389, + "max_htlc_msat": "14850000000", + "last_update": 1710783313, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "886894666356949005", - "chan_point": "638f26ea27dfba927dfb3897f9fe9489d604f12b8b7721eb4f76fe09896f9f01:13", - "last_update": 1710841936, - "capacity": "2000000", + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "910669406377869313", + "chan_point": "6a4c18260cc3e7e8f9beecdac99c18e1e0723de35f3cdd7610f4f3a059f8621d:1", + "last_update": 1710854378, + "capacity": "4900000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "499", + "fee_rate_milli_msat": "210", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710841936, + "max_htlc_msat": "4851000000", + "last_update": 1710854378, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 222, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710793390, + "max_htlc_msat": "4851000000", + "last_update": 1710772753, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + "custom_records": {} }, { - "channel_id": "889855651147612169", - "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:9", - "last_update": 1710813189, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "910686998444048388", + "chan_point": "c46465632d61e9da42138f4d2a01c0b00aa9e34e1b42e9a717272f6c36c4bb7a:4", + "last_update": 1710851760, + "capacity": "15000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1600", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710813189, + "max_htlc_msat": "14850000000", + "last_update": 1710851635, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710790753, + "max_htlc_msat": "14850000000", + "last_update": 1710851760, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "channel_id": "890068956432236557", - "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:13", - "last_update": 1710793392, - "capacity": "2000000", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "910688097982808065", + "chan_point": "bb516ed4c15ffb282f61bf2fdaae9e388c369b9e87bcc5031193a627480bfffb:1", + "last_update": 1710855749, + "capacity": "30000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "4000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710793392, + "max_htlc_msat": "13500000000", + "last_update": 1710855749, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "999", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710789318, + "max_htlc_msat": "13500000000", + "last_update": 1710688399, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + "custom_records": {} }, { - "channel_id": "918488033557348353", - "chan_point": "3c57def65d9db037ef262b94448e0b19413365d50874dadf6c8706704ef2a6a0:1", - "last_update": 1710855657, - "capacity": "16770000", + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "910818939924774912", + "chan_point": "02e6c31e514e1b09fff9da0d3de36b3060d4c2df87a418295baaa2d8d9c83580:0", + "last_update": 1710828417, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 72, + "time_lock_delta": 80, "min_htlc": "1", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "fee_base_msat": "811", + "fee_rate_milli_msat": "237", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710855657, + "max_htlc_msat": "9900000000", + "last_update": 1710828417, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1985", "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710855493, + "max_htlc_msat": "10000000000", + "last_update": 1710775531, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + "custom_records": {} }, { - "channel_id": "913633689575358465", - "chan_point": "f4ba21d0d25d0d064fab44160da431b099ffa8f7fe5a7d1289a4ed3d2ce3c60b:1", - "last_update": 1710822543, - "capacity": "25000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "910991563189387264", + "chan_point": "93faf32b05528bae4ceaf0e212157ad2c3ff62985df47bbcc5277c3fd3f87022:0", + "last_update": 1710821035, + "capacity": "100000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1750", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710789935, + "max_htlc_msat": "99000000000", + "last_update": 1710821035, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710822543, + "max_htlc_msat": "99000000000", + "last_update": 1710782734, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "channel_id": "914491308634472449", - "chan_point": "a0e58782bcff1c828a74744932f8ab32bafc27b275001372c2e62b5955387602:1", - "last_update": 1710790143, - "capacity": "100000000", + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "911436865352499201", + "chan_point": "29d778a7e2c22d332aa85870a99e3c5c64d026387e702892dce77bbdd59ad161:1", + "last_update": 1710839832, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 50, "min_htlc": "1000", "fee_base_msat": "0", "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "99000000000", - "last_update": 1710790037, + "max_htlc_msat": "9900000000", + "last_update": 1710839832, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 120, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "99000000000", - "last_update": 1710790143, + "max_htlc_msat": "280000000", + "last_update": 1710803683, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + "custom_records": {} }, { - "channel_id": "918488033557217280", - "chan_point": "9fd21e0a421924545e30b891812695791696c676367094ffe2467460eff56c36:0", - "last_update": 1710855500, - "capacity": "16770000", + "node1_pub": "03213c5175050fc1dfe0b4560c0cd5c6dd6bf4b25c2f11df0285384d0402e0d397", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "911491840958857217", + "chan_point": "ac7684a397733c23ecc00d1403224ce1d764e9b98dd8879d2c43a965e3c8cba6:1", + "last_update": 1710827955, + "capacity": "5000000", "node1_policy": { - "time_lock_delta": 72, - "min_htlc": "1", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", - "disabled": true, - "max_htlc_msat": "15093000000", - "last_update": 1710855462, + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "261", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710825485, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "5000", "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710855500, + "max_htlc_msat": "4950000000", + "last_update": 1710827955, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + "custom_records": {} }, { - "channel_id": "917417109119041536", - "chan_point": "9af4014c8a9dafc53ef2913a4bde5e2111cf10205c1905ae585e5511de108bcc:0", - "last_update": 1710854361, - "capacity": "99995000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "911605090606972929", + "chan_point": "301210f850411a867cd87f3d46df8fe42e7ff585a5e9143ade2d7ae51352130f:1", + "last_update": 1710845714, + "capacity": "300000000", "node1_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "999", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710854361, + "max_htlc_msat": "297000000000", + "last_update": 1710842454, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "750", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710822543, + "max_htlc_msat": "297000000000", + "last_update": 1710845714, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "914489109751398401", - "chan_point": "c51bb911ae676ddcbd7207a596dbea51151ba678399287ab8221f27cf93fdbf2:1", - "last_update": 1710790753, - "capacity": "25000000", + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "912080079757312001", + "chan_point": "0b9d0effb6cbb96fabf0e26c93743a8db7a67cdcb3287e2f75f410a0dc336481:1", + "last_update": 1710835555, + "capacity": "16777215", "node1_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "5292", + "fee_rate_milli_msat": "2646", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710777543, + "max_htlc_msat": "7549746000", + "last_update": 1710835555, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "0", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710790753, + "max_htlc_msat": "7549746000", + "last_update": 1710000163, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "channel_id": "850763614782029825", - "chan_point": "595e98da49698fdb9d9e25fbe9279c7f15897193bb13da9faef8ef1213a13fa5:1", - "last_update": 1710609731, - "capacity": "500000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "912271394755641344", + "chan_point": "f4f6591ee6e71a122c765f1d676943657d66a816ee2f22c404f739e8b9c58a19:0", + "last_update": 1710837838, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "50", + "fee_base_msat": "500", + "fee_rate_milli_msat": "567", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1707058294, + "max_htlc_msat": "49500000000", + "last_update": 1710837838, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "300", - "disabled": true, - "max_htlc_msat": "495000000", - "last_update": 1710609731, + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710786654, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5", - "node2_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + "custom_records": {} }, { - "channel_id": "912038298208501760", - "chan_point": "42df48fd0764c7777e7e0f290e9538f3082222648941e6078c348f6f02a4457d:0", - "last_update": 1710855461, - "capacity": "6710669", + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "912555068775071745", + "chan_point": "b70554c80fe432384f58868977023a98c56c51f060ebac51774a6e8c3e74a3ed:1", + "last_update": 1710842648, + "capacity": "3045714", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", "disabled": false, - "max_htlc_msat": "6643563000", - "last_update": 1710855461, + "max_htlc_msat": "100000000", + "last_update": 1710758012, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", "disabled": false, - "max_htlc_msat": "6643563000", - "last_update": 1710846063, + "max_htlc_msat": "1370571000", + "last_update": 1710842648, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", - "node2_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + "custom_records": {} }, { - "channel_id": "891582983965769729", - "chan_point": "2dd71734bb100d72ae15439e6e707f6ca451040f21dc8f79df75d8fac835caf4:1", - "last_update": 1710855891, - "capacity": "10000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "912558367295537154", + "chan_point": "e0576d85be3e8e5a2539a145e7e280b8446cc1542806e790d369a2828faf43bd:2", + "last_update": 1710855529, + "capacity": "12500000", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710855891, + "max_htlc_msat": "12375000000", + "last_update": 1710855529, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, - "min_htlc": "1000", + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710847863, + "max_htlc_msat": "12375000000", + "last_update": 1710839035, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "854880186313605121", - "chan_point": "78dc9a67189d0d44d3678cb35bb8b297fde81743c3ebdfaa1651fe4ad731542e:1", - "last_update": 1710786746, - "capacity": "1000000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "912821150514741248", + "chan_point": "6b228c36e358af7c041ac830b3a8ec3851c1b5d31b32cc577d5f941ee20513a1:0", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710770149, + "max_htlc_msat": "9900000000", + "last_update": 1710838854, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "521", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710786746, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + "custom_records": {} }, { - "channel_id": "781908898078588929", - "chan_point": "2d4c7735462b434dc79bd300e9d788cb78af0643c668531dab090c5d539fb3d1:1", - "last_update": 1710844710, - "capacity": "1500000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "912825548574162945", + "chan_point": "1b9643cced03480ce9f4bfe7a4b3472392809f94cc5699e0924b8e44b0506e02:1", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1500000000", - "last_update": 1710844710, + "max_htlc_msat": "9900000000", + "last_update": 1710783235, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "319", "disabled": false, - "max_htlc_msat": "400000000", - "last_update": 1710684918, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "793454869742813185", - "chan_point": "5327c98b3ce6a42c8baeebdc740bf24fbf0bedf5aa1f56eeebcc2b58fab5d54f:1", - "last_update": 1710791299, - "capacity": "2000000", + "node1_pub": "026f46207fd290a33cbd86e29b3ad0a47cdd44ab9aa5267cde66483e10aa9d3180", + "node2_pub": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "channel_id": "912961888014761984", + "chan_point": "29129a9d5f8d62561c3666a06c00ad46eb6ac32a1c8393795dce524fd9f55eca:0", + "last_update": 1710845085, + "capacity": "5000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710787110, + "max_htlc_msat": "4950000000", + "last_update": 1710845085, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710791299, + "max_htlc_msat": "4950000000", + "last_update": 1710796690, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + "custom_records": {} }, { - "channel_id": "835777271311630337", - "chan_point": "26bedad6cbd11dd2054557a51631971287141b0b487ef09e1482601e8f24f28f:1", - "last_update": 1710785354, - "capacity": "1620000", + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "913098227473121280", + "chan_point": "1ffe54a97cb8d8cf0b80764f5b6d6fcd38b409731f58c2f3ec3ac2df0dc2d408:0", + "last_update": 1710809725, + "capacity": "250000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1603800000", - "last_update": 1710778110, + "max_htlc_msat": "20000000000", + "last_update": 1710809725, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "50", + "fee_rate_milli_msat": "999", "disabled": false, - "max_htlc_msat": "1603800000", - "last_update": 1710785354, + "max_htlc_msat": "10000000", + "last_update": 1710758532, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "channel_id": "817806853236391936", - "chan_point": "2d7da2726a4724f8a350315840bd7e45b383c4e909e749b235d62339880b67fb:0", - "last_update": 1710791182, - "capacity": "90000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "913132312309661698", + "chan_point": "f323335ab1c8fbf360a4cb38b39463d7f6b484b8ad8fd00db8bbfcdfbb9786c6:2", + "last_update": 1710807743, + "capacity": "25000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "4610", "disabled": false, - "max_htlc_msat": "89100000", - "last_update": 1710509231, + "max_htlc_msat": "24750000000", + "last_update": 1710807743, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 80, + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "89100000", - "last_update": 1710791182, + "max_htlc_msat": "25000000000", + "last_update": 1710776035, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "885613735298072577", - "chan_point": "aae3c5dac5bc5a9b5813bb1f37592ca1aa0747d3b8089facdbf584d368ba94cb:1", - "last_update": 1710846929, - "capacity": "24639", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913211477159444481", + "chan_point": "1a70c078693acdd62603f9ac2adab67507a511851da6f44a9492f7ec3d5aae5a:1", + "last_update": 1710814907, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "50", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "24393000", - "last_update": 1710710327, + "max_htlc_msat": "9900000000", + "last_update": 1710814363, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "24393000", - "last_update": 1710846929, + "max_htlc_msat": "9900000000", + "last_update": 1710814907, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", - "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + "custom_records": {} }, { - "channel_id": "817627632791715840", - "chan_point": "349640c4fbb05af12ebc1bc904758dae19e90b3aa3ad2de840ad3b7c84889e56:0", - "last_update": 1710803782, - "capacity": "80000", + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913211477159444483", + "chan_point": "1a70c078693acdd62603f9ac2adab67507a511851da6f44a9492f7ec3d5aae5a:3", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "79200000", - "last_update": 1710775588, + "max_htlc_msat": "4500000000", + "last_update": 1710697901, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "908", "disabled": false, - "max_htlc_msat": "79200000", - "last_update": 1710803782, + "max_htlc_msat": "4500000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "868515230059724800", - "chan_point": "5c1b233a9778af0356a5e02dd8d758ebb8b85ddbcf8252d793217d1a28dedd67:0", - "last_update": 1710635188, - "capacity": "80000", + "node1_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "913218074207059969", + "chan_point": "b0a1f85a4ef3d92896f9e03a0ddf92d97fae54c76ecbcb3377f1b78ea7eac0eb:1", + "last_update": 1710849107, + "capacity": "15000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "79200000", - "last_update": 1710635188, + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710784535, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "72", "disabled": false, - "max_htlc_msat": "79200000", - "last_update": 1707066297, + "max_htlc_msat": "14850000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", - "node2_pub": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + "custom_records": {} }, { - "channel_id": "909468739601629185", - "chan_point": "ca9c5d9014ff6701875ec69d4db910fb7592f39ed5ad44eac23ff1f1031267f2:1", - "last_update": 1710783381, - "capacity": "500000", + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913298338556280834", + "chan_point": "122820a930e49889b21046cc5ec76f438eef99a6109b96f5cab36b3ac3015150:2", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "167", - "disabled": true, - "max_htlc_msat": "495000000", - "last_update": 1710783379, + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710784681, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "100", - "disabled": true, - "max_htlc_msat": "495000000", - "last_update": 1710783381, + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1299", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", - "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + "custom_records": {} }, { - "channel_id": "913193885013966849", - "chan_point": "087e6845eefd047bb0dd3ee591a2dbea28b504b8a07fd733f5eb51bf89b0ca24:1", - "last_update": 1710834571, - "capacity": "500000", + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913298338556280835", + "chan_point": "122820a930e49889b21046cc5ec76f438eef99a6109b96f5cab36b3ac3015150:3", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "100", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710779061, + "max_htlc_msat": "9900000000", + "last_update": 1710802631, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "0", - "fee_rate_milli_msat": "30", + "fee_rate_milli_msat": "1280", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710834571, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", - "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "channel_id": "769006129141776384", - "chan_point": "e5ebf62a8e888892b505156f95c8c6c7cb60ecf9801ec9f6650d3f303e53cba9:0", - "last_update": 1710704923, - "capacity": "250000", + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "913301637187371009", + "chan_point": "2a5dc9d835f9227920037110c442f45ff5c59d3ade08d4ccc8c6ffdc58ab49ff:1", + "last_update": 1710845485, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1710531493, + "max_htlc_msat": "9900000000", + "last_update": 1710845485, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1710704923, + "max_htlc_msat": "9900000000", + "last_update": 1710653670, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", - "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + "custom_records": {} }, { - "channel_id": "770339836665790464", - "chan_point": "fd89d822c95341eb4948000720b9747ce4b517e3ec6726db5774bdda4f25164c:0", - "last_update": 1710773323, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "913301637187371010", + "chan_point": "2a5dc9d835f9227920037110c442f45ff5c59d3ade08d4ccc8c6ffdc58ab49ff:2", + "last_update": 1710852963, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710771935, + "max_htlc_msat": "9900000000", + "last_update": 1710845485, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710773323, + "max_htlc_msat": "9900000000", + "last_update": 1710852963, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "channel_id": "860278788451991553", - "chan_point": "34f76ac4003f888ceeeeb9f71b3912ca731bebf78c3a808d3abfb4abd5beb96a:1", - "last_update": 1710851757, - "capacity": "100000", + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913374204975120385", + "chan_point": "27a36c1b36c1aba4d880060e9261df46a9ec028c6d1117d79692c72b17671982:1", + "last_update": 1710801640, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 120, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710851757, + "max_htlc_msat": "30000000", + "last_update": 1710801640, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710817465, + "max_htlc_msat": "9900000000", + "last_update": 1710796907, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", - "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + "custom_records": {} }, { - "channel_id": "865665295861612544", - "chan_point": "95a81b50d4b7055f0af96915ff3226e0d8b97d6a1e3d6975e02a295301468c8f:0", - "last_update": 1710594913, - "capacity": "100000", + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913442374560710658", + "chan_point": "b965586ebc9f708b5d063a0398715dea514745e1cdbbf2a733ca6181668bfe0c:2", + "last_update": 1710849107, + "capacity": "20000000", "node1_policy": { "time_lock_delta": 40, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710125353, + "max_htlc_msat": "19800000000", + "last_update": 1710795190, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "310", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710594913, + "max_htlc_msat": "19800000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", - "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + "custom_records": {} }, { - "channel_id": "850260038455197696", - "chan_point": "81b147059ab5ee28ccb9e1af5158254e6a0ceb3170ca1ebd2c062bad44ddf479:0", - "last_update": 1710798303, - "capacity": "5000223", + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913475359943426049", + "chan_point": "fb61d122c3dc8c644a7d04b0b4f1a8a7c7b01ac155a88daa01454d89b5db4167:1", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "0", - "disabled": false, - "max_htlc_msat": "4950221000", - "last_update": 1710798303, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "4950221000", - "last_update": 1710798303, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666", - "node2_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" - }, - { - "channel_id": "918392376041340929", - "chan_point": "f494169e156a65a8cfb628bd036a591d659af6aadff5604c89c0faf7fc92d527:1", - "last_update": 1710841167, - "capacity": "16770000", - "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", - "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710841167, + "max_htlc_msat": "9900000000", + "last_update": 1710784131, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 72, - "min_htlc": "1000", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "213", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710802200, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "918425361248288768", - "chan_point": "036f4a9be00df25cc8abfcdeeae828e0fbc7d82c92109e907aaf9837501680f7:0", - "last_update": 1710829005, - "capacity": "16770000", + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "913799715935813632", + "chan_point": "5fcb5970932ce7e05a6a70d78eae81f43735e02d4e2a4b3a1950b7155a6a6419:0", + "last_update": 1710850636, + "capacity": "20000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", - "disabled": true, - "max_htlc_msat": "1677000000", - "last_update": 1710829005, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "222", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710850636, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 72, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "fee_base_msat": "0", + "fee_rate_milli_msat": "127", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710816411, + "max_htlc_msat": "20000000000", + "last_update": 1710838039, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "channel_id": "763640512349143040", - "chan_point": "50ad5a9f0c31ec00e8e5dfc4a3947ea363542be0db57a538c014d1693c00782f:0", - "last_update": 1710398772, - "capacity": "100000", + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "913801914890452992", + "chan_point": "3eca8c0d91d34e5bb33e1c07806d7f7a9c53271586894c91ed5d8d1ad567e17b:0", + "last_update": 1710820876, + "capacity": "20000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "20", + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "88", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1709879445, + "max_htlc_msat": "19800000000", + "last_update": 1710807743, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 120, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710398772, + "max_htlc_msat": "570000000", + "last_update": 1710820876, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", - "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + "custom_records": {} }, { - "channel_id": "908048170593550337", - "chan_point": "0d45ab108774a44b8ed9a0a09c6ae6ac6390447c8c4f31762c9984e268dbf63a:1", - "last_update": 1710856405, - "capacity": "500000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "914147161649184768", + "chan_point": "8f6adbc61fbe2c49b6dfc952596a24d9bef00e74110b4efe952b49d259494099:0", + "last_update": 1710806635, + "capacity": "35000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "1", + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "153", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710856405, + "max_htlc_msat": "34650000000", + "last_update": 1710734645, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "100", + "time_lock_delta": 80, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710801772, + "max_htlc_msat": "35000000000", + "last_update": 1710806635, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", - "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + "custom_records": {} }, { - "channel_id": "756393631196446720", - "chan_point": "f64829c1e94063373474fc4be07bb1e594b4fd639be17b437af1d84fa1c629c1:0", - "last_update": 1710811915, - "capacity": "1000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "channel_id": "914193341093511170", + "chan_point": "b565410e7cbcd0c7422bbd2274208cf042a1c3feed1e6f9ad3b9bb90e5f7605c:2", + "last_update": 1710804343, + "capacity": "25000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "0", + "fee_base_msat": "1000", "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1662603210, + "max_htlc_msat": "24750000000", + "last_update": 1710804343, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 80, + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "750", - "disabled": true, - "max_htlc_msat": "990000000", - "last_update": 1710811915, + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710803035, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", - "node2_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" + "custom_records": {} }, { - "channel_id": "771570190275051521", - "chan_point": "6b2d5942bdadd04f26ad4f7ee86cceabd68531f66c7b370d27e02d9222c02bd1:1", - "last_update": 1710819115, - "capacity": "1000000", + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "914263709754523648", + "chan_point": "91e45024cc8e0677e64f20c0288f179a63c94931d84ac0dbcfcf28010992e2ee:0", + "last_update": 1710843510, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "750", - "disabled": true, - "max_htlc_msat": "990000000", - "last_update": 1710819115, + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710843510, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "200", - "fee_rate_milli_msat": "200", + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1648881967, + "max_htlc_msat": "4500000000", + "last_update": 1710325293, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", - "node2_pub": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + "custom_records": {} }, { - "channel_id": "887983182865104897", - "chan_point": "713685d0215c28e1ea8f56357459afb65fc86fd26075d2cc760589ab82f2f053:1", - "last_update": 1710842515, - "capacity": "50000000", + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914351670755065860", + "chan_point": "a79f8f42050337c50f62f854153a18dcc6a569c579c41dd8ceda1db7712b5c21:4", + "last_update": 1710829389, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710842515, + "max_htlc_msat": "2970000000", + "last_update": 1710789510, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", + "time_lock_delta": 80, + "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710246269, + "max_htlc_msat": "2970000000", + "last_update": 1710829389, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "756389233140760576", - "chan_point": "207b45f0cb6cb068b332bbdade71c936097d97277aafc212fea2548598398291:0", - "last_update": 1710393060, - "capacity": "250000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "914356068737351680", + "chan_point": "a65ab52cda51875300c1fe4b43bb165e67d05cc4819419f2de32b9a0fa9d7e36:0", + "last_update": 1710789238, + "capacity": "100000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", - "disabled": true, - "max_htlc_msat": "247500000", - "last_update": 1710393060, + "fee_base_msat": "0", + "fee_rate_milli_msat": "441", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710789238, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1662580373, + "max_htlc_msat": "99000000000", + "last_update": 1710783235, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1", - "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + "custom_records": {} }, { - "channel_id": "918390177017757696", - "chan_point": "d46c48c0d9410ab9597860b8ba10c14ed4c1d86338f80a7f7ff36ecf881269b3:0", - "last_update": 1710806052, - "capacity": "16770000", + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914357168314974208", + "chan_point": "98d2fb70999e71b86815dec7b676d83e4e5f8ffaea2ed3ddc860d0416f915947:0", + "last_update": 1710854589, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 72, - "min_htlc": "1000", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "8244", + "fee_rate_milli_msat": "833", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710804699, + "max_htlc_msat": "2970000000", + "last_update": 1710835407, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710806052, + "max_htlc_msat": "3000000000", + "last_update": 1710854589, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "899069558556065792", - "chan_point": "9483b700e1dfa3512defbb27097dc48da750ae6ea07b3baeb3c1a34da4cdec52:0", - "last_update": 1710846357, - "capacity": "40000", + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914357168314974214", + "chan_point": "98d2fb70999e71b86815dec7b676d83e4e5f8ffaea2ed3ddc860d0416f915947:6", + "last_update": 1710795190, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "10", - "disabled": true, - "max_htlc_msat": "39600000", - "last_update": 1710846357, + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710789197, "custom_records": {} }, "node2_policy": { @@ -5366,541 +23101,541 @@ "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "39600000", - "last_update": 1700598649, + "max_htlc_msat": "2970000000", + "last_update": 1710795190, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8", - "node2_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + "custom_records": {} }, { - "channel_id": "877726938508689409", - "chan_point": "5175773efb30bcae88dca5c47c6dcf8ba78b3fd8c31397d08db8f7b9e7e85019:1", - "last_update": 1710785354, - "capacity": "84803717", + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914389054085070854", + "chan_point": "bf91a203197160fc07cbb8bc9841a9d9027d01f00ad848cd2b51fba568eee726:6", + "last_update": 1710795190, + "capacity": "3000000", "node1_policy": { "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "83955680000", - "last_update": 1710777182, + "max_htlc_msat": "2970000000", + "last_update": 1710789943, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "83955680000", - "last_update": 1710785354, + "max_htlc_msat": "2970000000", + "last_update": 1710795190, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "channel_id": "889215735335550977", - "chan_point": "6dafd3c899572ba7c9cb984fa9108c8a7eb1a93e46df25966fc17090f4a240c2:1", - "last_update": 1710776050, - "capacity": "1000000", + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "channel_id": "914389054085070856", + "chan_point": "bf91a203197160fc07cbb8bc9841a9d9027d01f00ad848cd2b51fba568eee726:8", + "last_update": 1710829389, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710776050, + "max_htlc_msat": "3000000000", + "last_update": 1710829389, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710772753, + "max_htlc_msat": "2970000000", + "last_update": 1710441695, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "channel_id": "861583908627218433", - "chan_point": "21c74ee01ff0eda80631adcb10469619adecd70f3d937d27d4fec60f0225019d:1", - "last_update": 1710856349, + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914389054085070859", + "chan_point": "bf91a203197160fc07cbb8bc9841a9d9027d01f00ad848cd2b51fba568eee726:11", + "last_update": 1710805990, "capacity": "3000000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "159", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "144", "disabled": false, "max_htlc_msat": "2970000000", - "last_update": 1710856349, + "last_update": 1710786987, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_rate_milli_msat": "1", "disabled": false, "max_htlc_msat": "2970000000", - "last_update": 1710795398, + "last_update": 1710805990, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "channel_id": "907088297038446592", - "chan_point": "6a5795e372ac56b4555dcc179a1f125d39f2f48800f079cd3ef8584cafae4817:0", - "last_update": 1710848353, - "capacity": "100000000", + "node1_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "914423139045998595", + "chan_point": "57096610ffbee253cdc5ac665f9d68ff1e01e3744208dbe585aafbbbb06845f5:3", + "last_update": 1710849107, + "capacity": "5000000", "node1_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "999", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710774858, + "max_htlc_msat": "4950000000", + "last_update": 1710785160, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", - "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710848353, + "fee_rate_milli_msat": "656", + "disabled": true, + "max_htlc_msat": "4950000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "737654654585667584", - "chan_point": "0bebc81ceefe54c2568e4b762f2878eb4e7a85514f9346ba4352034521447269:0", - "last_update": 1710812050, - "capacity": "2000000", + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "914530891105763329", + "chan_point": "9cae8075084090c7a5fe2bdd2ab769448df70e2d764775b539286048fe2d30a7:1", + "last_update": 1710809735, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 37, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1700", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710786335, + "max_htlc_msat": "49500000000", + "last_update": 1710809735, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 140, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "fee_base_msat": "0", + "fee_rate_milli_msat": "693", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710812050, + "max_htlc_msat": "49500000000", + "last_update": 1710785638, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", - "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "channel_id": "894843035960147968", - "chan_point": "cfcfe8f1dc8cf0f20a6292facfc00d7e820e89cff3de64bd057801fc3ec3fc8d:0", - "last_update": 1710757535, - "capacity": "100000000", + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "914557279499714560", + "chan_point": "67feffbc619088def18fd128a3495fa1847adc6137f7f4f593f8d57652ecdee2:0", + "last_update": 1710845485, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710757535, + "max_htlc_msat": "4950000000", + "last_update": 1710845485, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, + "time_lock_delta": 222, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710312917, + "max_htlc_msat": "4950000000", + "last_update": 1710826753, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "884413068699238401", - "chan_point": "92dcfce24fc43ea96273efbe4304ea5a2f34c69ab8067d06bea37b3436926cad:1", - "last_update": 1710816935, - "capacity": "550000", + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "914557279499714563", + "chan_point": "67feffbc619088def18fd128a3495fa1847adc6137f7f4f593f8d57652ecdee2:3", + "last_update": 1710845481, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "544500000", - "last_update": 1710816935, + "max_htlc_msat": "4950000000", + "last_update": 1710827735, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "544500000", - "last_update": 1710801275, + "max_htlc_msat": "4950000000", + "last_update": 1710845481, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", - "node2_pub": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + "custom_records": {} }, { - "channel_id": "892859516987572224", - "chan_point": "726039ab50c25e38a2df12dc67394879c36bb8420dc481b843d0833c8ee93303:0", - "last_update": 1710835918, - "capacity": "2000000", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "026f46207fd290a33cbd86e29b3ad0a47cdd44ab9aa5267cde66483e10aa9d3180", + "channel_id": "914875038317084673", + "chan_point": "f44eb66f7166eba4d9bf82e0e95a78a429d180129d2145514235637907126b74:1", + "last_update": 1710855749, + "capacity": "30000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", + "fee_base_msat": "0", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710802736, + "max_htlc_msat": "29700000000", + "last_update": 1710855749, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "97", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710835918, + "max_htlc_msat": "29700000000", + "last_update": 1710799752, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000", - "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + "custom_records": {} }, { - "channel_id": "907853557235646464", - "chan_point": "72d24dea72f3e930a219cfccfa352cc10040d1bcf49db365025d263e82c7de83:0", - "last_update": 1710776927, - "capacity": "50000", + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "914952004175986689", + "chan_point": "3e71151953a626e025a0a5f5341c41258b185dd3e1fb47c81a58bcd6f6ccfabe:1", + "last_update": 1710853832, + "capacity": "2500000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 100, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "50", + "fee_rate_milli_msat": "2463", "disabled": false, - "max_htlc_msat": "49500000", - "last_update": 1710776927, + "max_htlc_msat": "2475000000", + "last_update": 1710853832, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, + "time_lock_delta": 222, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "49500000", - "last_update": 1710630223, + "max_htlc_msat": "2475000000", + "last_update": 1710817753, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2", - "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + "custom_records": {} }, { - "channel_id": "763639412848394241", - "chan_point": "490f8ac59304fd951b06e6b56e07fba5623ccf567ed5ec0aa0f00f994a80995d:1", - "last_update": 1710846930, - "capacity": "100000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "channel_id": "915499560920612865", + "chan_point": "f75a0832dd73745124b0bea578081958ab5e8cbf65f0ac1d2dcd9e0bf84c43b9:1", + "last_update": 1710849820, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1", - "fee_rate_milli_msat": "100", + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "71", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1709509568, + "max_htlc_msat": "9801000000", + "last_update": 1710817203, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710846930, + "max_htlc_msat": "9900000000", + "last_update": 1710849820, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac", - "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + "custom_records": {} }, { - "channel_id": "855546490317897729", - "chan_point": "e1998ecf9d5bdf045a2b2e0fc868cb66532bc8e3c3910fac54039cb72e7b666b:1", - "last_update": 1710789782, - "capacity": "1100000", + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "915772239817867266", + "chan_point": "161bc2351f2ed836de0e4d638c46ee090e6795b421a0968aac46ad71aa1fc856:2", + "last_update": 1710849107, + "capacity": "7300000", "node1_policy": { - "time_lock_delta": 200, + "time_lock_delta": 120, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "2000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", "disabled": false, - "max_htlc_msat": "1089000000", - "last_update": 1710789782, + "max_htlc_msat": "7227000000", + "last_update": 1710794158, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "202", "disabled": false, - "max_htlc_msat": "1089000000", - "last_update": 1710777136, + "max_htlc_msat": "7227000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", - "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "channel_id": "817245002822582272", - "chan_point": "4f774c8da0276be4aa4a488bff1877f1f0d42ce6d919b294d401ae9fceb5402b:0", - "last_update": 1710845182, - "capacity": "60000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "916338488352964608", + "chan_point": "4d0be2d98f261a3856dc53275711041441f11f517f0badf8731a3e926dac9de6:0", + "last_update": 1710786654, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "5000", "disabled": false, - "max_htlc_msat": "59400000", - "last_update": 1710845182, + "max_htlc_msat": "20000000000", + "last_update": 1710786654, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "100", + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "699", "disabled": false, - "max_htlc_msat": "59400000", - "last_update": 1710775336, + "max_htlc_msat": "20000000000", + "last_update": 1710779520, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "858923090495012865", - "chan_point": "6ecf4b8b50ffcaa4180c686d37ac2f7d5208a385b3ff6cdfacd7bd49b0795716:1", - "last_update": 1710780782, - "capacity": "650000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "channel_id": "916409956472061954", + "chan_point": "e996c7149a97620ad97aed0c5f541874881ad8f847533f8776d78badf02867d4:2", + "last_update": 1710846600, + "capacity": "1000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "313", "disabled": false, - "max_htlc_msat": "643500000", - "last_update": 1710775476, + "max_htlc_msat": "990000000", + "last_update": 1710846600, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "643500000", - "last_update": 1710780782, + "max_htlc_msat": "990000000", + "last_update": 1710836407, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", - "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + "custom_records": {} }, { - "channel_id": "837860845868220417", - "chan_point": "3188e2c87379e1784b904fc4ee2f6e75d846d87a9b8dd1db0e5217c11d8ce4ea:1", - "last_update": 1710775382, - "capacity": "3500000", + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "916409956472061956", + "chan_point": "e996c7149a97620ad97aed0c5f541874881ad8f847533f8776d78badf02867d4:4", + "last_update": 1710846600, + "capacity": "2000000", "node1_policy": { - "time_lock_delta": 144, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "1000", - "disabled": true, - "max_htlc_msat": "3465000000", - "last_update": 1710775382, + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710806734, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "213", "disabled": false, - "max_htlc_msat": "3465000000", - "last_update": 1710537660, + "max_htlc_msat": "1980000000", + "last_update": 1710846600, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", - "node2_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + "custom_records": {} }, { - "channel_id": "861833497883574273", - "chan_point": "0482621aeb0de1762640b99dfc600326903a52d11c8dcdf02a5e5d2652732c8f:1", - "last_update": 1710843782, - "capacity": "20000000", + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "916437444409950209", + "chan_point": "9c167b7ea19e3e411fdb05efb853a088927f211497a3efedc634c876776d1364:1", + "last_update": 1710818091, + "capacity": "90000000", "node1_policy": { - "time_lock_delta": 144, - "min_htlc": "1000", + "time_lock_delta": 140, + "min_htlc": "100", "fee_base_msat": "2000", - "fee_rate_milli_msat": "4000", + "fee_rate_milli_msat": "2500", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710843782, + "max_htlc_msat": "89100000000", + "last_update": 1710818091, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "504", "disabled": false, - "max_htlc_msat": "19800000000", - "last_update": 1710773696, + "max_htlc_msat": "89100000000", + "last_update": 1710789238, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "840309458235949057", - "chan_point": "012e468c6298ff7a689d671bfca97387e45ab7a7b6e7fc3dbfa46e55a95c6b59:1", - "last_update": 1710825499, - "capacity": "2000000", + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "916512211176128513", + "chan_point": "0f59f910e580db96927ed45af05dd4eec3230a749384e6ed516d271f91bfff37:1", + "last_update": 1710854279, + "capacity": "5889000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 34, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710825118, + "max_htlc_msat": "5830110000", + "last_update": 1709812621, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710825499, + "max_htlc_msat": "5889000000", + "last_update": 1710854279, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", - "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + "custom_records": {} }, { - "channel_id": "826733788095447041", - "chan_point": "dffc24350df473a58d4715c31f8ed35a7440387dbf59e16895a01a79c3d64ce2:1", - "last_update": 1710805699, - "capacity": "1000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "916526504728199168", + "chan_point": "6df1e8b752327328a4bc11065c820ec3465b17c606d9b655b1ec17ac929168d0:0", + "last_update": 1710772435, + "capacity": "15000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1000000000", - "last_update": 1710805699, + "max_htlc_msat": "15000000000", + "last_update": 1710772435, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, + "time_lock_delta": 34, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", "disabled": false, - "max_htlc_msat": "800000000", - "last_update": 1710798104, + "max_htlc_msat": "14850000000", + "last_update": 1710681712, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "909819483881668608", - "chan_point": "735fca87374ceba0dff8adec0bffafb1a870638f79f2b44246f1686644d8cdc9:0", - "last_update": 1710783381, - "capacity": "200000", + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "917082857725362177", + "chan_point": "137184a0412be2a6fb6f6bc7d65b00bac727120e7e68e01d6b02da091e5706e9:1", + "last_update": 1710855565, + "capacity": "6000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "30", - "disabled": true, - "max_htlc_msat": "198000000", - "last_update": 1710783323, + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710855565, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "100", - "disabled": true, - "max_htlc_msat": "198000000", - "last_update": 1710783381, + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710836589, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880", - "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "channel_id": "900662750928764928", - "chan_point": "c5babe10624dc1f24fd3e20eb59d0e6dbfc7dedf1ea0eaccb70f55a7abf3b38c:0", - "last_update": 1710844661, - "capacity": "100000000", + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "917416009602236416", + "chan_point": "10f782b804347657cdb925866a7b9616f15dd8fd203b3de709245ecc808939a7:0", + "last_update": 1710785691, + "capacity": "99995000", "node1_policy": { "time_lock_delta": 140, "min_htlc": "100", @@ -5908,283 +23643,281 @@ "fee_rate_milli_msat": "2500", "disabled": false, "max_htlc_msat": "20000000000", - "last_update": 1710844661, + "last_update": 1710785691, "custom_records": {} }, "node2_policy": { "time_lock_delta": 144, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "999", + "fee_rate_milli_msat": "499", "disabled": false, "max_htlc_msat": "20000000000", - "last_update": 1710009152, + "last_update": 1710265468, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "838462278610059264", - "chan_point": "6a7091a5715eda464ead722f2acbc4c5c1aeddab5fd4ae44c1ae9025b7f1d6d2:0", - "last_update": 1710846461, - "capacity": "50000000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "918004248425529346", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:2", + "last_update": 1710856207, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "150", "disabled": false, - "max_htlc_msat": "49500000000", - "last_update": 1710846461, + "max_htlc_msat": "4950000000", + "last_update": 1710835513, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "425", "disabled": false, - "max_htlc_msat": "49500000000", - "last_update": 1710771291, + "max_htlc_msat": "4950000000", + "last_update": 1710856207, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "846395255002038273", - "chan_point": "ee07f74fc67ad1534106327880fae377fe91c9c7776f9d46699d28f99e9684c5:1", - "last_update": 1710848050, - "capacity": "10000000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "918004248425529349", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:5", + "last_update": 1710775604, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", + "min_htlc": "1", "fee_base_msat": "0", "fee_rate_milli_msat": "0", - "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710788861, + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710775604, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "75", "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710848050, + "max_htlc_msat": "4950000000", + "last_update": 1710773407, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", - "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "channel_id": "898725411427385345", - "chan_point": "96fe24d1c0ae58e89df72f3a0898f2bacc0dd58d11938325a54c46ad7b728804:1", - "last_update": 1710856837, - "capacity": "1000000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "918004248425529357", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:13", + "last_update": 1710856207, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 50, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "10", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "50000000", - "last_update": 1710856837, + "max_htlc_msat": "9900000000", + "last_update": 1710798790, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "600", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710832983, + "max_htlc_msat": "9900000000", + "last_update": 1710856207, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "channel_id": "910155934451761152", - "chan_point": "888c6225644dc0f6fa8d22807a3cf9acb99bc1558806e6b2d5f15dad6cacfb59:0", - "last_update": 1710834571, - "capacity": "250000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "918004248425529358", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:14", + "last_update": 1710811804, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 50, + "time_lock_delta": 40, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "10", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "99", "disabled": false, - "max_htlc_msat": "50000000", - "last_update": 1710782196, + "max_htlc_msat": "9900000000", + "last_update": 1710811804, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "30", + "fee_rate_milli_msat": "160", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1710834571, + "max_htlc_msat": "9900000000", + "last_update": 1710780607, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", - "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "channel_id": "885818244503437313", - "chan_point": "0a0ac608d703bb383fd51e2ef0eebfb40e737d916f4e7810785ec8b9f7915d4e:1", - "last_update": 1710611588, - "capacity": "700000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "918015243549736961", + "chan_point": "42dbf34ea726dc1e64e803aa04c65000268f4a5b53ebfdc2f6b4b201b63ab400:1", + "last_update": 1710841049, + "capacity": "20000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", "disabled": false, - "max_htlc_msat": "315000000", - "last_update": 1710558538, + "max_htlc_msat": "1980000000", + "last_update": 1710782702, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", - "disabled": true, - "max_htlc_msat": "315000000", - "last_update": 1710611588, + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710841049, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "899068459022417920", - "chan_point": "f8751c293cfefc7e80bb4ae6c452ce17d33541ef420081b913ffc985ec246c16:0", - "last_update": 1710780553, - "capacity": "51000", + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "918075716598693888", + "chan_point": "8a5823d42061f70ee8dbcc00ffb0d3920f3fa8e06afb58814e8034eb09f9f436:0", + "last_update": 1710832438, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1575", "disabled": false, - "max_htlc_msat": "50490000", - "last_update": 1700598649, + "max_htlc_msat": "49500000000", + "last_update": 1710832438, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1234", + "fee_base_msat": "1000", "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "50490000", - "last_update": 1710780553, + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710802528, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597", - "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + "custom_records": {} }, { - "channel_id": "846395255027269632", - "chan_point": "7fc299cd258401fef62f746434cefa05d42cd8895eec4f65d435ec2cd3bb3ec5:0", - "last_update": 1710848050, - "capacity": "20000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "918152682457726977", + "chan_point": "faae42340d44bf90b3d0a7b0b313cd412e1bc8517ec330c3d2adb2541b06f261:1", + "last_update": 1710846891, + "capacity": "60150670", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 18, "min_htlc": "1000", "fee_base_msat": "0", "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "19800000000", - "last_update": 1710787491, + "max_htlc_msat": "59549164000", + "last_update": 1710846891, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, + "time_lock_delta": 20, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "800", "disabled": false, - "max_htlc_msat": "19800000000", - "last_update": 1710848050, + "max_htlc_msat": "59549164000", + "last_update": 1710779404, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "917416009602236416", - "chan_point": "10f782b804347657cdb925866a7b9616f15dd8fd203b3de709245ecc808939a7:0", - "last_update": 1710785691, - "capacity": "99995000", + "node1_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "918390177017757696", + "chan_point": "d46c48c0d9410ab9597860b8ba10c14ed4c1d86338f80a7f7ff36ecf881269b3:0", + "last_update": 1710806052, + "capacity": "16770000", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710785691, + "max_htlc_msat": "15093000000", + "last_update": 1710804699, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710265468, + "max_htlc_msat": "1677000000", + "last_update": 1710806052, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "865735664596090880", - "chan_point": "317d529be3e15cccad634f20811a98c2ba686188b9cad9803a322e8b29c5e4bc:0", - "last_update": 1710845901, - "capacity": "75000", + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "channel_id": "918393475539337217", + "chan_point": "b4cc302ae8d05a73006fb00cb873bb7ff39b521441abba49719139a1c539aabd:1", + "last_update": 1710841643, + "capacity": "16770000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 72, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", "disabled": false, - "max_htlc_msat": "74250000", - "last_update": 1710845901, + "max_htlc_msat": "15093000000", + "last_update": 1710804699, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "74250000", - "last_update": 1710845871, + "max_htlc_msat": "1677000000", + "last_update": 1710841643, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} } ] } \ No newline at end of file diff --git a/test/data/LN_50.json b/test/data/LN_50.json new file mode 100644 index 000000000..2087c74d8 --- /dev/null +++ b/test/data/LN_50.json @@ -0,0 +1,6190 @@ +{ + "directed": false, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "last_update": 1710607733, + "alias": "BlueWave", + "addresses": [ + { + "network": "tcp", + "addr": "bh32yyu7dgyodzq3wdunho5esew5qbvrd2flsynqzeyilnd7jnyeahqd.onion:9735" + } + ], + "color": "#d6de37", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6" + }, + { + "last_update": 1702845433, + "alias": "03796678b7111abef10f", + "addresses": [ + { + "network": "tcp", + "addr": "kgrxo24cm2ykhfazqer7bxzezcevrmir2du455o4mfbg3rpvqubpaqqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5" + }, + { + "last_update": 1709578090, + "alias": "REDWHISPER", + "addresses": [ + { + "network": "tcp", + "addr": "piwsugtc6hr3jukavptmv6tjrxt2wt2i2q35lvjx6lk25vwvi43zicqd.onion:9735" + } + ], + "color": "#023bb9", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d" + }, + { + "last_update": 1710772374, + "alias": "fr33node", + "addresses": [ + { + "network": "tcp", + "addr": "ubzmye2tytcqarycr3jsirl3ajxallddfpvorc4t2odao4y5yovdalad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199" + }, + { + "last_update": 1710847816, + "alias": "LQwD-Canada", + "addresses": [ + { + "network": "tcp", + "addr": "192.243.215.102:9735" + }, + { + "network": "tcp", + "addr": "yit5nizyrgk4n2gx7hkadaas5iz33kn6wulr5vdndzpbae6tykqdw7ad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + }, + { + "last_update": 1710855669, + "alias": "River Financial 2", + "addresses": [ + { + "network": "tcp", + "addr": "34.136.230.235:9735" + } + ], + "color": "#ff9900", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5" + }, + { + "last_update": 1707020283, + "alias": "027d5369f807773ed75d", + "addresses": [ + { + "network": "tcp", + "addr": "leb7bxhf3ppdfssy2bu4udc2cnhbehw5nhkkiumh76fhxel6bqtqmqqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + }, + { + "last_update": 1710847881, + "alias": "lnd1.relampago.cash", + "addresses": [ + { + "network": "tcp", + "addr": "44.225.98.1:9735" + }, + { + "network": "tcp", + "addr": "6hmm7rvxv6mu7sv5wetqfigjljrde2cd6kr474ncylbbfzdsczyvhlad.onion:9735" + } + ], + "color": "#ffa47a", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + }, + { + "last_update": 1710623243, + "alias": "bitpi", + "addresses": [ + { + "network": "tcp", + "addr": "iddfruwmorrrlr32akea422ajghfyicyohdd3n3a6mmiigd5x7druhyd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3" + }, + { + "last_update": 1674198664, + "alias": "Bitcomoon.com", + "addresses": [ + { + "network": "tcp", + "addr": "e3xx3u5oxvuo4bgix5cl2ngfeyvexoz7my3rktgrpmf6gq3sryi2orqd.onion:9735" + } + ], + "color": "#79e0ed", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda" + }, + { + "last_update": 1710586588, + "alias": "Bitrequest", + "addresses": [ + { + "network": "tcp", + "addr": "wvhczhxeyuo5gsbc6ub5n66yxycvojyx26eunh76l3ta7ks7yjckbgqd.onion:9735" + } + ], + "color": "#0cc0ed", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296" + }, + { + "last_update": 1710773661, + "alias": "raspiblitz", + "addresses": [ + { + "network": "tcp", + "addr": "n75lmwb3ykmwvbqajzkdh6otc6ukasij4f556ub54ug4qvs22rb3rsyd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2" + }, + { + "last_update": 1710825523, + "alias": "qubi1", + "addresses": [ + { + "network": "tcp", + "addr": "mkejl5jnqe4ovaow57l47hi46xbjcxnf54jriiwvs5rsxcxxjbo5b4yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3" + }, + { + "last_update": 1710850518, + "alias": "Knoten", + "addresses": [ + { + "network": "tcp", + "addr": "xetu2kpbvonq2urqpadclyzy76gwhdrkz6lgazz65j66leapw4bmpoad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + }, + { + "last_update": 1710568425, + "alias": "Viva Bitcoin", + "addresses": [ + { + "network": "tcp", + "addr": "2ti2gy74nlyjsgcs42wemjwd3skmndbbv776ufldkngbzmpo7tbsntyd.onion:9735" + } + ], + "color": "#ffff00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + }, + { + "last_update": 1710394153, + "alias": "", + "addresses": [], + "color": "#007f00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + }, + { + "last_update": 1710769806, + "alias": "EcoNode", + "addresses": [ + { + "network": "tcp", + "addr": "kpa3fv64oxsgl7ymsmu27t5wwe4vpx47wer3tr7fk5ateeskmoh6hiqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3" + }, + { + "last_update": 1707656697, + "alias": "02f5be5f3d54b66bf531", + "addresses": [ + { + "network": "tcp", + "addr": "vedvjiibjrbbaaegvjqvx4thfobbj7gs5scb5gtpvxyb3t423ao23sad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + }, + { + "last_update": 1710693115, + "alias": "lightning-roulette.com", + "addresses": [ + { + "network": "tcp", + "addr": "34.65.32.189:9735" + } + ], + "color": "#11ff22", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" + }, + { + "last_update": 1662699597, + "alias": "BambergCityNode5", + "addresses": [ + { + "network": "tcp", + "addr": "tfsmtkznl6spgnm5j3xxa6wmawvmi4ipgbvuazdngy6xqbigqdiojpid.onion:9735" + } + ], + "color": "#cd6d5f", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" + }, + { + "last_update": 1710392585, + "alias": "", + "addresses": [], + "color": "#007f00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + }, + { + "last_update": 1710817951, + "alias": "bblocker21", + "addresses": [ + { + "network": "tcp", + "addr": "l4nxfimtxjjerzewyb4ihp4jhiauw2madq3krdnqz4s3x52kmya76lyd.onion:9735" + } + ], + "color": "#ffb233", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + }, + { + "last_update": 1710847967, + "alias": "Bitrefill", + "addresses": [ + { + "network": "tcp", + "addr": "54.155.41.207:9735" + } + ], + "color": "#002b28", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "last_update": 1710767460, + "alias": "lnd.cryptoassets.co.za", + "addresses": [ + { + "network": "tcp", + "addr": "197.155.6.194:9735" + }, + { + "network": "tcp", + "addr": "xiigglrrhl6gl55g6ndou5pkg7y7gxmln6bs6eov2w5od2xavdfukrqd.onion:9735" + } + ], + "color": "#000000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + }, + { + "last_update": 1710821651, + "alias": "OpenNode.com", + "addresses": [ + { + "network": "tcp", + "addr": "18.222.70.85:9735" + } + ], + "color": "#000000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "last_update": 1710836095, + "alias": "021cb32426ed1a6be953", + "addresses": [ + { + "network": "tcp", + "addr": "g2vldjytfdx4r2hdmgg3ccqrxczxgxl4e3w7rr3emvp4br4xcoo4void.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + }, + { + "last_update": 1710776023, + "alias": "Fastlightning", + "addresses": [ + { + "network": "tcp", + "addr": "tspdnxsdk77b6wdly6lujqr724hoq23buuez3roy6xo5ausv3riisbqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2" + }, + { + "last_update": 1710845779, + "alias": "LightningPremium", + "addresses": [ + { + "network": "tcp", + "addr": "5u3crvcbydktdcwsk5utapo5vvd7s2uzkn45u7kemusvgikwqydytjad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + }, + { + "last_update": 1648917357, + "alias": "mynodebtc.cooper", + "addresses": [ + { + "network": "tcp", + "addr": "bh4ooqtd3mngj4vk3m3gpqd3isaywi67ed5iochdg3lhexzikc56epad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + }, + { + "last_update": 1710852615, + "alias": "Decentralized", + "addresses": [ + { + "network": "tcp", + "addr": "sgcq35suws6ft63egprwsncj357yt355rsa3aqqpjjimma4nfjhzdyqd.onion:9735" + } + ], + "color": "#61f5b2", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + }, + { + "last_update": 1710551722, + "alias": "02abb5e57ff442770d9d", + "addresses": [ + { + "network": "tcp", + "addr": "172.67.158.44:9735" + }, + { + "network": "tcp", + "addr": "rjufmivql2si355mfczbg2wirc24rnjngpo5hsotmabiafuewojkbdqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + }, + { + "last_update": 1710852249, + "alias": "Bitrefill Routing", + "addresses": [ + { + "network": "tcp", + "addr": "54.77.250.40:9735" + } + ], + "color": "#ff001c", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "last_update": 1710811679, + "alias": "bleskomat", + "addresses": [ + { + "network": "tcp", + "addr": "165.227.161.245:9735" + }, + { + "network": "tcp", + "addr": "5aggdumr4x3qkx5ptshggvrdkgaxkpsklw4nwmnsa6diby6vwcvccyad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + }, + { + "last_update": 1710852256, + "alias": "Liaoning", + "addresses": [ + { + "network": "tcp", + "addr": "vdhhkanmkfr7x2mlkbo7lah6sh7mhwxqt7gv73m44pkfk4y537tg54yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + }, + { + "last_update": 1710848892, + "alias": "shtyrlitz", + "addresses": [ + { + "network": "tcp", + "addr": "aswabsuhftuqagrvxlhzc5la73wj5skg7shp4vjpwgialbcgkceby6qd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + }, + { + "last_update": 1710803334, + "alias": "FinanzielleFreiheit", + "addresses": [ + { + "network": "tcp", + "addr": "valdvygv5w6bfnza4yfshqsvljhv7grgyq4wgpke2jwvugfretiqwcqd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + }, + { + "last_update": 1710805061, + "alias": "lndwr2.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "34.74.114.254:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + }, + { + "last_update": 1710856675, + "alias": "Civilization Phaze IV", + "addresses": [ + { + "network": "tcp", + "addr": "ko4qvn23p2txecujtt5nv67mb2ikzhbebh2hs4rgfvonfpk2tqhux4id.onion:9735" + } + ], + "color": "#ff8733", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7" + }, + { + "last_update": 1710771876, + "alias": "0209a06852f1257d70e5", + "addresses": [ + { + "network": "tcp", + "addr": "94.130.220.96:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + }, + { + "last_update": 1710586096, + "alias": "PERLY", + "addresses": [ + { + "network": "tcp", + "addr": "twtvqk7jiz3luzkt346q326u5owmo73lfn4yhhr5efskkr4autuf6vqd.onion:9735" + } + ], + "color": "#9245ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + }, + { + "last_update": 1700638994, + "alias": "UmbrelMCL", + "addresses": [ + { + "network": "tcp", + "addr": "rlubaw7sxcaqmostf3e4x7yollfo4x2tzfwxrj3ssvxqusdes2q6k6yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + }, + { + "last_update": 1710736174, + "alias": "Laika", + "addresses": [ + { + "network": "tcp", + "addr": "tezwktlb2f3ah2tbla6emehwic4eelwyxhycfvzgzmc7n6jxz33ozead.onion:9735" + } + ], + "color": "#300fb0", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + }, + { + "last_update": 1710803050, + "alias": "lndeu0.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "35.196.134.164:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "last_update": 1710790037, + "alias": "0294774ee02a9faa5a58", + "addresses": [], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + }, + { + "last_update": 1709977298, + "alias": "ACINQ", + "addresses": [ + { + "network": "tcp", + "addr": "3.33.236.230:9735" + }, + { + "network": "tcp", + "addr": "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion:9735" + } + ], + "color": "#49daaa", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "29": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "last_update": 1710849539, + "alias": "PeterJFrancoIII", + "addresses": [ + { + "network": "tcp", + "addr": "oyonslv2muhpngxl3yip3z7mt27auzlsx2luqydkxadludv2cva3thad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "last_update": 1710850693, + "alias": "Wallet Sat's", + "addresses": [ + { + "network": "tcp", + "addr": "4a2tjhiyz3rqgpfvragnk4uszv32dytzhapen46igyjlyzfpgix5zeqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + }, + { + "last_update": 1710617991, + "alias": "RUSHBNOSTOP", + "addresses": [ + { + "network": "tcp", + "addr": "maeiws6uq6baeuim5q6mcavn3xpn4kkpaa5rv7uqhrs6wubhccnkn7ad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "last_update": 1710774982, + "alias": "sheesh", + "addresses": [ + { + "network": "tcp", + "addr": "esowgqg2jhjzofvgkwdnyrozdvowvx7ymo3jsdh7oxxc74y7ga4egyyd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "last_update": 1710855402, + "alias": "lndus0.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "34.138.228.220:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + } + ], + "edges": [ + { + "channel_id": "821685930207739904", + "chan_point": "b8d52e4be913496dd31204b80a6214d30934fd77cf18d33f1f4b6f2b17aabc0a:0", + "last_update": 1710852888, + "capacity": "130000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "128700000", + "last_update": 1710852888, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "128700000", + "last_update": 1710852885, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "851482695379255296", + "chan_point": "57457180438919be15029ad7763d11eb46d33057fe9ab02ca001439b48350e97:0", + "last_update": 1710451930, + "capacity": "300000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "297000000", + "last_update": 1710451930, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": true, + "max_htlc_msat": "297000000", + "last_update": 1706955502, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", + "node2_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + }, + { + "channel_id": "852238059856199681", + "chan_point": "48442adc84d9f96ea82a15d24eb5eed6ea66a8b633296a7a0f7cd0adebbf4685:1", + "last_update": 1710705189, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710617530, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710705189, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + }, + { + "channel_id": "892895800825741312", + "chan_point": "997c879c28e44b11493fffc1e3bdd41bfab8ebcb815a3b8b0c6d0f7baeabe86b:0", + "last_update": 1710607091, + "capacity": "50000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1710607091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1705276253, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5", + "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + }, + { + "channel_id": "892768257448935424", + "chan_point": "7b2351fe1f9dc287e376f481ae3e52cb39ad417cb8387087b91054e300ddc486:0", + "last_update": 1710639306, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2000000000", + "last_update": 1710607091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "5805", + "fee_rate_milli_msat": "6", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710639306, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d", + "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + }, + { + "channel_id": "915246673262739457", + "chan_point": "4c2e260ea115e813a1255e12a1597e2577216d37d770c3e2328170fc28ffb153:1", + "last_update": 1710772374, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710771876, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710772374, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199", + "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + }, + { + "channel_id": "888979340321554436", + "chan_point": "0055cf02d8bbc7125784e8fee50605969c6cfe76fa24cea14e9ae66a6be4a7c8:4", + "last_update": 1710793392, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790557, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793392, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + }, + { + "channel_id": "886619788490833928", + "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:8", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "914391253219868685", + "chan_point": "acf8a90a8ba334f408b13a1e581538c6fcc3d2398293a59b5fcad77f26769ca2:13", + "last_update": 1710813189, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 200, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710789782, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710813189, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "channel_id": "886786914241085444", + "chan_point": "c34a46e8d7fd82170c9fc87cfcbc848c477a40799d638512bb60054aafd69cf3:4", + "last_update": 1710830106, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "159", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710828768, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710830106, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "channel_id": "890068956432236548", + "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:4", + "last_update": 1710795190, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790263, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795190, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + }, + { + "channel_id": "889913925275484176", + "chan_point": "48cd67b0995fb7e49876f4e18fba74bbde68bf3c713b6cc5ceb93c4f3272f4ee:16", + "last_update": 1710829389, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789499, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710829389, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + }, + { + "channel_id": "888987036985851905", + "chan_point": "094ff6c4b2ee8473bf7839d6d48c389576209ef4c581abb7abcab755bd20a4b8:1", + "last_update": 1710791182, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791182, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710775389, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "886894666356949005", + "chan_point": "638f26ea27dfba927dfb3897f9fe9489d604f12b8b7721eb4f76fe09896f9f01:13", + "last_update": 1710841936, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710841936, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793390, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + }, + { + "channel_id": "889855651147612169", + "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:9", + "last_update": 1710813189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710813189, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790753, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "channel_id": "890068956432236557", + "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:13", + "last_update": 1710793392, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793392, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789318, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + }, + { + "channel_id": "918488033557348353", + "chan_point": "3c57def65d9db037ef262b94448e0b19413365d50874dadf6c8706704ef2a6a0:1", + "last_update": 1710855657, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 72, + "min_htlc": "1", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710855657, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710855493, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + }, + { + "channel_id": "913633689575358465", + "chan_point": "f4ba21d0d25d0d064fab44160da431b099ffa8f7fe5a7d1289a4ed3d2ce3c60b:1", + "last_update": 1710822543, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710789935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710822543, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "914491308634472449", + "chan_point": "a0e58782bcff1c828a74744932f8ab32bafc27b275001372c2e62b5955387602:1", + "last_update": 1710790143, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710790037, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710790143, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + }, + { + "channel_id": "918488033557217280", + "chan_point": "9fd21e0a421924545e30b891812695791696c676367094ffe2467460eff56c36:0", + "last_update": 1710855500, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 72, + "min_htlc": "1", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": true, + "max_htlc_msat": "15093000000", + "last_update": 1710855462, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710855500, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + }, + { + "channel_id": "917417109119041536", + "chan_point": "9af4014c8a9dafc53ef2913a4bde5e2111cf10205c1905ae585e5511de108bcc:0", + "last_update": 1710854361, + "capacity": "99995000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710854361, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710822543, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "914489109751398401", + "chan_point": "c51bb911ae676ddcbd7207a596dbea51151ba678399287ab8221f27cf93fdbf2:1", + "last_update": 1710790753, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710777543, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710790753, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "channel_id": "850763614782029825", + "chan_point": "595e98da49698fdb9d9e25fbe9279c7f15897193bb13da9faef8ef1213a13fa5:1", + "last_update": 1710609731, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1707058294, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "300", + "disabled": true, + "max_htlc_msat": "495000000", + "last_update": 1710609731, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5", + "node2_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + }, + { + "channel_id": "912038298208501760", + "chan_point": "42df48fd0764c7777e7e0f290e9538f3082222648941e6078c348f6f02a4457d:0", + "last_update": 1710855461, + "capacity": "6710669", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "6643563000", + "last_update": 1710855461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "6643563000", + "last_update": 1710846063, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", + "node2_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + }, + { + "channel_id": "891582983965769729", + "chan_point": "2dd71734bb100d72ae15439e6e707f6ca451040f21dc8f79df75d8fac835caf4:1", + "last_update": 1710855891, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710855891, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710847863, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "854880186313605121", + "chan_point": "78dc9a67189d0d44d3678cb35bb8b297fde81743c3ebdfaa1651fe4ad731542e:1", + "last_update": 1710786746, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710770149, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710786746, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + }, + { + "channel_id": "781908898078588929", + "chan_point": "2d4c7735462b434dc79bd300e9d788cb78af0643c668531dab090c5d539fb3d1:1", + "last_update": 1710844710, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1500000000", + "last_update": 1710844710, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710684918, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "793454869742813185", + "chan_point": "5327c98b3ce6a42c8baeebdc740bf24fbf0bedf5aa1f56eeebcc2b58fab5d54f:1", + "last_update": 1710791299, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710787110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791299, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + }, + { + "channel_id": "835777271311630337", + "chan_point": "26bedad6cbd11dd2054557a51631971287141b0b487ef09e1482601e8f24f28f:1", + "last_update": 1710785354, + "capacity": "1620000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1603800000", + "last_update": 1710778110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1603800000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "channel_id": "817806853236391936", + "chan_point": "2d7da2726a4724f8a350315840bd7e45b383c4e909e749b235d62339880b67fb:0", + "last_update": 1710791182, + "capacity": "90000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "89100000", + "last_update": 1710509231, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "89100000", + "last_update": 1710791182, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "885613735298072577", + "chan_point": "aae3c5dac5bc5a9b5813bb1f37592ca1aa0747d3b8089facdbf584d368ba94cb:1", + "last_update": 1710846929, + "capacity": "24639", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "24393000", + "last_update": 1710710327, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "24393000", + "last_update": 1710846929, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", + "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + }, + { + "channel_id": "817627632791715840", + "chan_point": "349640c4fbb05af12ebc1bc904758dae19e90b3aa3ad2de840ad3b7c84889e56:0", + "last_update": 1710803782, + "capacity": "80000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000", + "last_update": 1710775588, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000", + "last_update": 1710803782, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "868515230059724800", + "chan_point": "5c1b233a9778af0356a5e02dd8d758ebb8b85ddbcf8252d793217d1a28dedd67:0", + "last_update": 1710635188, + "capacity": "80000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "79200000", + "last_update": 1710635188, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000", + "last_update": 1707066297, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", + "node2_pub": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + }, + { + "channel_id": "909468739601629185", + "chan_point": "ca9c5d9014ff6701875ec69d4db910fb7592f39ed5ad44eac23ff1f1031267f2:1", + "last_update": 1710783381, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "167", + "disabled": true, + "max_htlc_msat": "495000000", + "last_update": 1710783379, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "100", + "disabled": true, + "max_htlc_msat": "495000000", + "last_update": 1710783381, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", + "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + }, + { + "channel_id": "913193885013966849", + "chan_point": "087e6845eefd047bb0dd3ee591a2dbea28b504b8a07fd733f5eb51bf89b0ca24:1", + "last_update": 1710834571, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710779061, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "30", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710834571, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", + "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "channel_id": "769006129141776384", + "chan_point": "e5ebf62a8e888892b505156f95c8c6c7cb60ecf9801ec9f6650d3f303e53cba9:0", + "last_update": 1710704923, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710531493, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710704923, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", + "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + }, + { + "channel_id": "770339836665790464", + "chan_point": "fd89d822c95341eb4948000720b9747ce4b517e3ec6726db5774bdda4f25164c:0", + "last_update": 1710773323, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710771935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710773323, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "860278788451991553", + "chan_point": "34f76ac4003f888ceeeeb9f71b3912ca731bebf78c3a808d3abfb4abd5beb96a:1", + "last_update": 1710851757, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710851757, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710817465, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", + "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + }, + { + "channel_id": "865665295861612544", + "chan_point": "95a81b50d4b7055f0af96915ff3226e0d8b97d6a1e3d6975e02a295301468c8f:0", + "last_update": 1710594913, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710125353, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710594913, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", + "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + }, + { + "channel_id": "850260038455197696", + "chan_point": "81b147059ab5ee28ccb9e1af5158254e6a0ceb3170ca1ebd2c062bad44ddf479:0", + "last_update": 1710798303, + "capacity": "5000223", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950221000", + "last_update": 1710798303, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950221000", + "last_update": 1710798303, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666", + "node2_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" + }, + { + "channel_id": "918392376041340929", + "chan_point": "f494169e156a65a8cfb628bd036a591d659af6aadff5604c89c0faf7fc92d527:1", + "last_update": 1710841167, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710841167, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710802200, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "918425361248288768", + "chan_point": "036f4a9be00df25cc8abfcdeeae828e0fbc7d82c92109e907aaf9837501680f7:0", + "last_update": 1710829005, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "1677000000", + "last_update": 1710829005, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710816411, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "763640512349143040", + "chan_point": "50ad5a9f0c31ec00e8e5dfc4a3947ea363542be0db57a538c014d1693c00782f:0", + "last_update": 1710398772, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "20", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1709879445, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710398772, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", + "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + }, + { + "channel_id": "908048170593550337", + "chan_point": "0d45ab108774a44b8ed9a0a09c6ae6ac6390447c8c4f31762c9984e268dbf63a:1", + "last_update": 1710856405, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710856405, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710801772, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", + "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + }, + { + "channel_id": "756393631196446720", + "chan_point": "f64829c1e94063373474fc4be07bb1e594b4fd639be17b437af1d84fa1c629c1:0", + "last_update": 1710811915, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1662603210, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": true, + "max_htlc_msat": "990000000", + "last_update": 1710811915, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", + "node2_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" + }, + { + "channel_id": "771570190275051521", + "chan_point": "6b2d5942bdadd04f26ad4f7ee86cceabd68531f66c7b370d27e02d9222c02bd1:1", + "last_update": 1710819115, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": true, + "max_htlc_msat": "990000000", + "last_update": 1710819115, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "200", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1648881967, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", + "node2_pub": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + }, + { + "channel_id": "887983182865104897", + "chan_point": "713685d0215c28e1ea8f56357459afb65fc86fd26075d2cc760589ab82f2f053:1", + "last_update": 1710842515, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710842515, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710246269, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "756389233140760576", + "chan_point": "207b45f0cb6cb068b332bbdade71c936097d97277aafc212fea2548598398291:0", + "last_update": 1710393060, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "247500000", + "last_update": 1710393060, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1662580373, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1", + "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + }, + { + "channel_id": "918390177017757696", + "chan_point": "d46c48c0d9410ab9597860b8ba10c14ed4c1d86338f80a7f7ff36ecf881269b3:0", + "last_update": 1710806052, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710804699, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710806052, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "899069558556065792", + "chan_point": "9483b700e1dfa3512defbb27097dc48da750ae6ea07b3baeb3c1a34da4cdec52:0", + "last_update": 1710846357, + "capacity": "40000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "10", + "disabled": true, + "max_htlc_msat": "39600000", + "last_update": 1710846357, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "39600000", + "last_update": 1700598649, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8", + "node2_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + }, + { + "channel_id": "877726938508689409", + "chan_point": "5175773efb30bcae88dca5c47c6dcf8ba78b3fd8c31397d08db8f7b9e7e85019:1", + "last_update": 1710785354, + "capacity": "84803717", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "83955680000", + "last_update": 1710777182, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "83955680000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "channel_id": "889215735335550977", + "chan_point": "6dafd3c899572ba7c9cb984fa9108c8a7eb1a93e46df25966fc17090f4a240c2:1", + "last_update": 1710776050, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710776050, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710772753, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "channel_id": "861583908627218433", + "chan_point": "21c74ee01ff0eda80631adcb10469619adecd70f3d937d27d4fec60f0225019d:1", + "last_update": 1710856349, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "159", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710856349, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710795398, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "channel_id": "907088297038446592", + "chan_point": "6a5795e372ac56b4555dcc179a1f125d39f2f48800f079cd3ef8584cafae4817:0", + "last_update": 1710848353, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710774858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "737654654585667584", + "chan_point": "0bebc81ceefe54c2568e4b762f2878eb4e7a85514f9346ba4352034521447269:0", + "last_update": 1710812050, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 37, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1700", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710786335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 140, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710812050, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "channel_id": "894843035960147968", + "chan_point": "cfcfe8f1dc8cf0f20a6292facfc00d7e820e89cff3de64bd057801fc3ec3fc8d:0", + "last_update": 1710757535, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710757535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710312917, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "884413068699238401", + "chan_point": "92dcfce24fc43ea96273efbe4304ea5a2f34c69ab8067d06bea37b3436926cad:1", + "last_update": 1710816935, + "capacity": "550000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "544500000", + "last_update": 1710816935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "544500000", + "last_update": 1710801275, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "node2_pub": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + }, + { + "channel_id": "892859516987572224", + "chan_point": "726039ab50c25e38a2df12dc67394879c36bb8420dc481b843d0833c8ee93303:0", + "last_update": 1710835918, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710802736, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710835918, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000", + "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + }, + { + "channel_id": "907853557235646464", + "chan_point": "72d24dea72f3e930a219cfccfa352cc10040d1bcf49db365025d263e82c7de83:0", + "last_update": 1710776927, + "capacity": "50000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "49500000", + "last_update": 1710776927, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "49500000", + "last_update": 1710630223, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2", + "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + }, + { + "channel_id": "763639412848394241", + "chan_point": "490f8ac59304fd951b06e6b56e07fba5623ccf567ed5ec0aa0f00f994a80995d:1", + "last_update": 1710846930, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1709509568, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710846930, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac", + "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + }, + { + "channel_id": "855546490317897729", + "chan_point": "e1998ecf9d5bdf045a2b2e0fc868cb66532bc8e3c3910fac54039cb72e7b666b:1", + "last_update": 1710789782, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 200, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710789782, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710777136, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", + "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "channel_id": "817245002822582272", + "chan_point": "4f774c8da0276be4aa4a488bff1877f1f0d42ce6d919b294d401ae9fceb5402b:0", + "last_update": 1710845182, + "capacity": "60000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "59400000", + "last_update": 1710845182, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "59400000", + "last_update": 1710775336, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "858923090495012865", + "chan_point": "6ecf4b8b50ffcaa4180c686d37ac2f7d5208a385b3ff6cdfacd7bd49b0795716:1", + "last_update": 1710780782, + "capacity": "650000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "643500000", + "last_update": 1710775476, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "643500000", + "last_update": 1710780782, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + }, + { + "channel_id": "837860845868220417", + "chan_point": "3188e2c87379e1784b904fc4ee2f6e75d846d87a9b8dd1db0e5217c11d8ce4ea:1", + "last_update": 1710775382, + "capacity": "3500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "3465000000", + "last_update": 1710775382, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710537660, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "node2_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + }, + { + "channel_id": "861833497883574273", + "chan_point": "0482621aeb0de1762640b99dfc600326903a52d11c8dcdf02a5e5d2652732c8f:1", + "last_update": 1710843782, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "4000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710843782, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710773696, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "840309458235949057", + "chan_point": "012e468c6298ff7a689d671bfca97387e45ab7a7b6e7fc3dbfa46e55a95c6b59:1", + "last_update": 1710825499, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710825118, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710825499, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", + "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + }, + { + "channel_id": "826733788095447041", + "chan_point": "dffc24350df473a58d4715c31f8ed35a7440387dbf59e16895a01a79c3d64ce2:1", + "last_update": 1710805699, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710805699, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710798104, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "909819483881668608", + "chan_point": "735fca87374ceba0dff8adec0bffafb1a870638f79f2b44246f1686644d8cdc9:0", + "last_update": 1710783381, + "capacity": "200000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "30", + "disabled": true, + "max_htlc_msat": "198000000", + "last_update": 1710783323, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "100", + "disabled": true, + "max_htlc_msat": "198000000", + "last_update": 1710783381, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880", + "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "channel_id": "900662750928764928", + "chan_point": "c5babe10624dc1f24fd3e20eb59d0e6dbfc7dedf1ea0eaccb70f55a7abf3b38c:0", + "last_update": 1710844661, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710844661, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710009152, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "838462278610059264", + "chan_point": "6a7091a5715eda464ead722f2acbc4c5c1aeddab5fd4ae44c1ae9025b7f1d6d2:0", + "last_update": 1710846461, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710846461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710771291, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "846395255002038273", + "chan_point": "ee07f74fc67ad1534106327880fae377fe91c9c7776f9d46699d28f99e9684c5:1", + "last_update": 1710848050, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710788861, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710848050, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", + "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "channel_id": "898725411427385345", + "chan_point": "96fe24d1c0ae58e89df72f3a0898f2bacc0dd58d11938325a54c46ad7b728804:1", + "last_update": 1710856837, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "50000000", + "last_update": 1710856837, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710832983, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "channel_id": "910155934451761152", + "chan_point": "888c6225644dc0f6fa8d22807a3cf9acb99bc1558806e6b2d5f15dad6cacfb59:0", + "last_update": 1710834571, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "50000000", + "last_update": 1710782196, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "30", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710834571, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", + "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "channel_id": "885818244503437313", + "chan_point": "0a0ac608d703bb383fd51e2ef0eebfb40e737d916f4e7810785ec8b9f7915d4e:1", + "last_update": 1710611588, + "capacity": "700000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "315000000", + "last_update": 1710558538, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": true, + "max_htlc_msat": "315000000", + "last_update": 1710611588, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "899068459022417920", + "chan_point": "f8751c293cfefc7e80bb4ae6c452ce17d33541ef420081b913ffc985ec246c16:0", + "last_update": 1710780553, + "capacity": "51000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "50490000", + "last_update": 1700598649, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1234", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "50490000", + "last_update": 1710780553, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597", + "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + }, + { + "channel_id": "846395255027269632", + "chan_point": "7fc299cd258401fef62f746434cefa05d42cd8895eec4f65d435ec2cd3bb3ec5:0", + "last_update": 1710848050, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710787491, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710848050, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "917416009602236416", + "chan_point": "10f782b804347657cdb925866a7b9616f15dd8fd203b3de709245ecc808939a7:0", + "last_update": 1710785691, + "capacity": "99995000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710785691, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710265468, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "865735664596090880", + "chan_point": "317d529be3e15cccad634f20811a98c2ba686188b9cad9803a322e8b29c5e4bc:0", + "last_update": 1710845901, + "capacity": "75000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "74250000", + "last_update": 1710845901, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "74250000", + "last_update": 1710845871, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + } + ] +} \ No newline at end of file diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml new file mode 100644 index 000000000..75cc8e42c --- /dev/null +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml @@ -0,0 +1,18 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager +# the pod-viewer and pod-manager roles are the default +# roles defined in values.yaml for the namespaces charts +# +# if you need a different set of roles for a particular namespaces +# deployment, you can override values.yaml by providing your own +# role definitions below +# +# roles: +# - name: my-custom-role +# rules: +# - apiGroups: "" +# resources: "" +# verbs: "" diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml new file mode 100644 index 000000000..413d3bcb7 --- /dev/null +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml @@ -0,0 +1,19 @@ +namespaces: + - name: wargames-red-team-warnettest + users: + - name: alice-warnettest + roles: + - pod-viewer + - name: bob-warnettest + roles: + - pod-viewer + - pod-manager + - name: wargames-blue-team-warnettest + users: + - name: mallory-warnettest + roles: + - pod-viewer + - name: carol-warnettest + roles: + - pod-viewer + - pod-manager diff --git a/test/data/bitcoin_conf/network.yaml b/test/data/bitcoin_conf/network.yaml new file mode 100644 index 000000000..06ec79290 --- /dev/null +++ b/test/data/bitcoin_conf/network.yaml @@ -0,0 +1,64 @@ +nodes: + - name: tank-0016 + image: + tag: "0.16.1" + addnode: + - tank-0017 + config: + uacomment=tank-0016 + - name: tank-0017 + image: + tag: "0.17.0" + addnode: + - tank-0019 + config: + uacomment=tank-0017 + - name: tank-0019 + image: + tag: "0.19.2" + addnode: + - tank-0020 + config: + uacomment=tank-0019 + - name: tank-0020 + image: + tag: "0.20.0" + addnode: + - tank-0021 + config: + uacomment=tank-0020 + - name: tank-0021 + image: + tag: "0.21.1" + addnode: + - tank-0024 + config: + uacomment=tank-0021 + - name: tank-0024 + image: + tag: "24.2" + addnode: + - tank-0025 + config: + uacomment=tank-0024 + - name: tank-0025 + image: + tag: "25.1" + addnode: + - tank-0026 + config: + uacomment=tank-0025 + - name: tank-0026 + image: + tag: "26.0" + addnode: + - tank-0027 + config: + uacomment=tank-0026 + - name: tank-0027 + image: + tag: "27.0" + addnode: + - tank-0016 + config: + uacomment=tank-0027 \ No newline at end of file diff --git a/test/data/bitcoin_conf/node-defaults.yaml b/test/data/bitcoin_conf/node-defaults.yaml new file mode 100644 index 000000000..a7ca1a373 --- /dev/null +++ b/test/data/bitcoin_conf/node-defaults.yaml @@ -0,0 +1,12 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" + +resources: + limits: + cpu: 4000m + memory: 500Mi + requests: + cpu: 100m + memory: 200Mi \ No newline at end of file diff --git a/test/data/build_v24_test.graphml b/test/data/build_v24_test.graphml deleted file mode 100644 index 5dc8c7297..000000000 --- a/test/data/build_v24_test.graphml +++ /dev/null @@ -1,30 +0,0 @@ -- - - - - - - - - - - - - - - - - -- 27.0 - -uacomment=w0 -debug=validation - -- 27.0 - -uacomment=w1 -debug=validation - -- 27.0 - -uacomment=w2 -debug=validation - -- 27.0 - -uacomment=w3 - -- 27.0 - -uacomment=w4 - -- 27.0 - -uacomment=w5 - -- 27.0 - -uacomment=w6 - -- 27.0 - -uacomment=w7 - -- 27.0 - -uacomment=w8 - -- 27.0 - -uacomment=w9 - -- 27.0 - -uacomment=w10 - -- 27.0 - - - -- - - - - - - - - - - - - diff --git a/test/data/ln.graphml b/test/data/ln.graphml deleted file mode 100644 index e0606c93f..000000000 --- a/test/data/ln.graphml +++ /dev/null @@ -1,71 +0,0 @@ -- - - - - - - - - - - - - - - - - -- 27.0 - -uacomment=w0 - -- bitcoin/bitcoin#24.x - -uacomment=v24_build - --disable-zmq - -- - \ No newline at end of file diff --git a/test/data/ln/network.yaml b/test/data/ln/network.yaml new file mode 100644 index 000000000..a5e10ad7c --- /dev/null +++ b/test/data/ln/network.yaml @@ -0,0 +1,53 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + ln: + lnd: false + cln: true + - name: tank-0001 + addnode: + - tank-0002 + - name: tank-0002 + addnode: + - tank-0000 + - name: tank-0003 + addnode: + - tank-0000 + lnd: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 500 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + circuitbreaker: + enabled: true + httpPort: 9235 + # Just 32 bytes of entropy encoded in base64 + macaroonRootKey: nmPScpcYkBBUXvEryzpYfjgY27j8hO9SiXO9qNQAJFs= + # Derived from root key with `lncli bakemacaroon --root_key=...` + adminMacaroon: 0201036c6e6402f801030a10b676fd9c5cbdfccedb1b932865ad57451201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620cff489a728f1be62c41db20607e78e3b3173f5b51679b4489e429cc83648dac4 + + - name: tank-0004 + addnode: + - tank-0000 + lnd: + channels: + - id: + block: 500 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + # Just 32 bytes of entropy encoded in base64 + macaroonRootKey: FmEMD2X1hKzxR5yAWgbAT5CbQWPOW+OdyztMMCTBThU= + # Derived from root key with `lncli bakemacaroon --root_key=...` + adminMacaroon: 0201036c6e6402f801030a102616e41d07f5df28869fe8f8147ed2861201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620e40032ba0d91a7f046ed4596da67aa129cca0ff162e51444df2ce68fa6d16944 + + - name: tank-0005 + addnode: + - tank-0000 \ No newline at end of file diff --git a/test/data/ln/node-defaults.yaml b/test/data/ln/node-defaults.yaml new file mode 100644 index 000000000..be5b8bcd3 --- /dev/null +++ b/test/data/ln/node-defaults.yaml @@ -0,0 +1,40 @@ +# enable collectLogs and metricsExport to activate publish lnd-exporter metrics + +#Core configs +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" +collectLogs: false +metricsExport: false + +#LN configs +ln: + lnd: true +lnd: + defaultConfig: | + color=#000000 + config: | + bitcoin.timelockdelta=33 + metricsExport: false + prometheusMetricsPort: 9332 + extraContainers: + - name: lnd-exporter + image: bitcoindevproject/lnd-exporter:0.2.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - name: config + mountPath: /macaroon.hex + subPath: MACAROON_HEX + env: + - name: METRICS + value: > + lnd_balance_channels=parse("/v1/balance/channels","balance") + lnd_local_balance_channels=parse("/v1/balance/channels","local_balance.sat") + lnd_remote_balance_channels=parse("/v1/balance/channels","remote_balance.sat") + lnd_block_height=parse("/v1/getinfo","block_height") + lnd_peers=parse("/v1/getinfo","num_peers") + ports: + - name: prom-metrics + containerPort: 9332 + protocol: TCP \ No newline at end of file diff --git a/test/data/logging.graphml b/test/data/logging.graphml deleted file mode 100644 index 54b3b73cb..000000000 --- a/test/data/logging.graphml +++ /dev/null @@ -1,35 +0,0 @@ -- - - - - - - - - - - - - - - - - simln - -- 27.0 - -uacomment=w0 - lnd - lightninglabs/lnd:v0.17.5-beta - true - -- 27.0 - -uacomment=w1 - lnd - pinheadmz/circuitbreaker:278737d - true - -- 27.0 - -uacomment=w2 - lnd - pinheadmz/circuitbreaker:278737d - --bitcoin.timelockdelta=33 - -- 27.0 - -uacomment=w2 - cln - --cltv-delta=33 - -- 27.0 - -uacomment=w3 - -- - - - - - - - --local_amt=100000 - --base_fee_msat=2200 --fee_rate_ppm=13 --time_lock_delta=20 - -- --local_amt=100000 --push_amt=50000 - --base_fee_msat=5500 --fee_rate_ppm=3 --time_lock_delta=40 - -- amount=100000 push_msat=50000000 - feebase=5500 feeppm=3 - -- diff --git a/test/data/logging/network.yaml b/test/data/logging/network.yaml new file mode 100644 index 000000000..129de21a1 --- /dev/null +++ b/test/data/logging/network.yaml @@ -0,0 +1,37 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0002 + metricsExport: true + - name: tank-0001 + addnode: + - tank-0002 + metricsExport: true + metrics: txrate=getchaintxstats(10)["txrate"] ipv4peers=COUNT:getpeerinfo(),network,ipv4 + - name: tank-0002 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + metricsExport: true + prometheusMetricsPort: 9332 + extraContainers: + - name: lnd-exporter + image: bitcoindevproject/lnd-exporter:0.2.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - name: config + mountPath: /macaroon.hex + subPath: MACAROON_HEX + env: + - name: METRICS + value: > + lnd_block_height=parse("/v1/getinfo","block_height") + lnd_peers=parse("/v1/getinfo","num_peers") + ports: + - name: prom-metrics + containerPort: 9332 + protocol: TCP +caddy: + enabled: true \ No newline at end of file diff --git a/test/data/logging/node-defaults.yaml b/test/data/logging/node-defaults.yaml new file mode 100644 index 000000000..b914c8bba --- /dev/null +++ b/test/data/logging/node-defaults.yaml @@ -0,0 +1,5 @@ +collectLogs: true +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/network_with_plugins/network.yaml b/test/data/network_with_plugins/network.yaml new file mode 100644 index 000000000..c0d8d9c7f --- /dev/null +++ b/test/data/network_with_plugins/network.yaml @@ -0,0 +1,91 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + ln: + lnd: true + + - name: tank-0001 + addnode: + - tank-0002 + ln: + lnd: true + + - name: tank-0002 + addnode: + - tank-0000 + ln: + lnd: true + + - name: tank-0003 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 500 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + circuitbreaker: + enabled: true # This enables circuitbreaker for this node + httpPort: 9235 # Can override defaults per-node + + - name: tank-0004 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 500 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + + - name: tank-0005 + addnode: + - tank-0000 + ln: + lnd: true + +plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc) + preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code + hello: + entrypoint: "../plugins/hello" # This entrypoint path is relative to the network.yaml file + podName: "hello-pre-deploy" + helloTo: "preDeploy!" + postDeploy: + hello: + entrypoint: "../plugins/hello" + podName: "hello-post-deploy" + helloTo: "postDeploy!" + simln: # You can have multiple plugins per hook + entrypoint: "../../../resources/plugins/simln" + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + preNode: # preNode plugins run before each node is deployed + hello: + entrypoint: "../plugins/hello" + helloTo: "preNode!" + postNode: + hello: + entrypoint: "../plugins/hello" + helloTo: "postNode!" + preNetwork: + hello: + entrypoint: "../plugins/hello" + helloTo: "preNetwork!" + podName: "hello-pre-network" + postNetwork: + hello: + entrypoint: "../plugins/hello" + helloTo: "postNetwork!" + podName: "hello-post-network" + \ No newline at end of file diff --git a/test/data/network_with_plugins/node-defaults.yaml b/test/data/network_with_plugins/node-defaults.yaml new file mode 100644 index 000000000..24a00b5c8 --- /dev/null +++ b/test/data/network_with_plugins/node-defaults.yaml @@ -0,0 +1,8 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" + +lnd: + defaultConfig: | + color=#000000 diff --git a/test/data/onion/network.yaml b/test/data/onion/network.yaml new file mode 100644 index 000000000..be4aa77dc --- /dev/null +++ b/test/data/onion/network.yaml @@ -0,0 +1,23 @@ +caddy: + enabled: false +fork_observer: + configQueryInterval: 20 + enabled: false +nodes: +- image: + tag: '29.0' + name: tank-0000 +- image: + tag: '29.0' + name: tank-0001 +- image: + tag: '29.0' + name: tank-0002 +- image: + tag: '29.0' + name: tank-0003 + +plugins: + preDeploy: + tor: + entrypoint: "../../../resources/plugins/tor" \ No newline at end of file diff --git a/test/data/onion/node-defaults.yaml b/test/data/onion/node-defaults.yaml new file mode 100644 index 000000000..9038774c6 --- /dev/null +++ b/test/data/onion/node-defaults.yaml @@ -0,0 +1,22 @@ +chain: regtest +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent +defaultConfig: | + debug=net + debug=tor + proxy=127.0.0.1:9050 + listen=1 + onlynet=onion + torcontrol=127.0.0.1:9051 + +collectLogs: false +metricsExport: false + +extraContainers: + - name: tor + image: bitcoindevproject/tor-relay:latest + ports: + - name: toror + containerPort: 9001 + protocol: TCP \ No newline at end of file diff --git a/test/data/permutations.graphml b/test/data/permutations.graphml deleted file mode 100644 index 0c4686f61..000000000 --- a/test/data/permutations.graphml +++ /dev/null @@ -1,93 +0,0 @@ -- - - - - - - - - - - - - - - - - -- 27.0 - true - -- 27.0 - true - txrate=getchaintxstats(10)["txrate"] - -- 27.0 - -- - - - \ No newline at end of file diff --git a/test/data/plugins/hello/README.md b/test/data/plugins/hello/README.md new file mode 100644 index 000000000..77bb5040f --- /dev/null +++ b/test/data/plugins/hello/README.md @@ -0,0 +1,124 @@ +# Hello Plugin + +## Hello World! +*Hello* is an example plugin to demonstrate the features of Warnet's plugin architecture. It uses each of the hooks available in the `warnet deploy` command (see the example below for details). + +## Usage +In your python virtual environment with Warnet installed and setup, create a new Warnet user folder (follow the prompts): + +`$ warnet new user_folder` + +`$ cd user_folder` + +Deploy the *hello* network. + +`$ warnet deploy networks/hello` + +While that is launching, take a look inside the `networks/hello/network.yaml` file. You can also see the copy below which includes commentary on the structure of plugins in the `network.yaml` file. + +Also, take a look at the `plugins/hello/plugin.py` file to see how plugins work and to find out how to author your own plugin. + +Once `deploy` completes, view the pods of the *hello* network by invoking `kubectl get all -A`. + +To view the various "Hello World!" messages, run `kubectl logs pod/POD_NAME` + +### A `network.yaml` example +When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. You can modify these files to fit your needs. + +For example, the `network.yaml` file below includes the *hello* plugin, lightning nodes, and the *simln* plugin. + +- - - - - - - - - - - - - - - - - -- 27.0 - - - - False - False - -- bitcoindevproject/bitcoin:26.0 - - - - False - False - -- 27.0 - - - - False - False - -- 27.0 - - - - False - False - -- 27.0 - - - - False - False - -- 27.0 - - - - False - False - -- 27.0 - - - - False - False - -- 27.0 - - - - False - False - -- - - - - - - - - ++ diff --git a/test/data/plugins/hello/charts/hello/Chart.yaml b/test/data/plugins/hello/charts/hello/Chart.yaml new file mode 100644 index 000000000..abd94467e --- /dev/null +++ b/test/data/plugins/hello/charts/hello/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: hello-chart +description: A Helm chart for a hello Pod +version: 0.1.0 +appVersion: "1.0" \ No newline at end of file diff --git a/test/data/plugins/hello/charts/hello/templates/pod.yaml b/test/data/plugins/hello/charts/hello/templates/pod.yaml new file mode 100644 index 000000000..ba5319670 --- /dev/null +++ b/test/data/plugins/hello/charts/hello/templates/pod.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ .Values.podName }} + labels: + app: {{ .Chart.Name }} +spec: + restartPolicy: Never + containers: + - name: {{ .Values.podName }}-container + image: alpine:latest + command: ["sh", "-c"] + args: + - echo "Hello {{ .Values.helloTo }}"; + resources: {} \ No newline at end of file diff --git a/test/data/plugins/hello/charts/hello/values.yaml b/test/data/plugins/hello/charts/hello/values.yaml new file mode 100644 index 000000000..302da3c15 --- /dev/null +++ b/test/data/plugins/hello/charts/hello/values.yaml @@ -0,0 +1,2 @@ +podName: hello-pod +helloTo: "world" \ No newline at end of file diff --git a/test/data/plugins/hello/plugin.py b/test/data/plugins/hello/plugin.py new file mode 100755 index 000000000..cb5a8373a --- /dev/null +++ b/test/data/plugins/hello/plugin.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import json +import logging +from enum import Enum +from pathlib import Path +from typing import Optional + +import click + +from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent +from warnet.process import run_command + +# It is common for Warnet objects to have a "mission" label to help query them in the cluster. +MISSION = "hello" +PRIMARY_CONTAINER = MISSION + +PLUGIN_DIR_TAG = "plugin_dir" + + +class PluginError(Exception): + pass + + +log = logging.getLogger(MISSION) +if not log.hasHandlers(): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + console_handler.setFormatter(formatter) + log.addHandler(console_handler) +log.setLevel(logging.DEBUG) +log.propagate = True + + +# Plugins look like this in the `network.yaml` file: +# +# plugins: +# hello: +# podName: "a-pod-name" +# helloTo: "World!" +# +# "podName" and "helloTo" are essentially dictionary keys, and it helps to keep those keys in an +# enum in order to prevent typos. +class PluginContent(Enum): + POD_NAME = "podName" + HELLO_TO = "helloTo" + + +# Warnet uses a python package called "click" to manage terminal interactions with the user. +# To use click, we must declare a click "group" by decorating a function named after the plugin. +# While optional, using click makes it easy for users to interact with your plugin. +@click.group() +@click.pass_context +def hello(ctx): + """Commands for the Hello plugin""" + ctx.ensure_object(dict) + plugin_dir = Path(__file__).resolve().parent + ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) + + +# Each Warnet plugin must have an entrypoint function which takes two JSON objects: plugin_content +# and warnet_content. We have seen the PluginContent enum above. Warnet also has a WarnetContent +# enum which holds the keys to the warnet_content dictionary. +@hello.command() +@click.argument("plugin_content", type=str) +@click.argument("warnet_content", type=str) +@click.pass_context +def entrypoint(ctx, plugin_content: str, warnet_content: str): + """Plugin entrypoint""" + plugin_content: dict = json.loads(plugin_content) + warnet_content: dict = json.loads(warnet_content) + + hook_value = warnet_content.get(WarnetContent.HOOK_VALUE.value) + + assert hook_value in {item.value for item in HookValue}, ( + f"{hook_value} is not a valid HookValue" + ) + + if warnet_content.get(PLUGIN_ANNEX): + for annex_member in [annex_item for annex_item in warnet_content.get(PLUGIN_ANNEX)]: + assert annex_member in {item.value for item in AnnexMember}, ( + f"{annex_member} is not a valid AnnexMember" + ) + + warnet_content[WarnetContent.HOOK_VALUE.value] = HookValue(hook_value) + + _entrypoint(ctx, plugin_content, warnet_content) + + +def _entrypoint(ctx, plugin_content: dict, warnet_content: dict): + """Called by entrypoint""" + hook_value = warnet_content[WarnetContent.HOOK_VALUE.value] + + if hook_value in ( + HookValue.PRE_NETWORK, + HookValue.POST_NETWORK, + HookValue.PRE_DEPLOY, + HookValue.POST_DEPLOY, + ): + data = get_data(plugin_content) + if data: + _launch_pod(ctx, install_name=hook_value.value.lower() + "-hello", **data) + else: + _launch_pod(ctx, install_name=hook_value.value.lower() + "-hello") + elif hook_value == HookValue.PRE_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-hello-pod" + _launch_pod(ctx, install_name=hook_value.value.lower() + "-" + name, podName=name) + elif hook_value == HookValue.POST_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-hello-pod" + _launch_pod(ctx, install_name=hook_value.value.lower() + "-" + name, podName=name) + + +def get_data(plugin_content: dict) -> Optional[dict]: + data = { + key: plugin_content.get(key) + for key in (PluginContent.POD_NAME.value, PluginContent.HELLO_TO.value) + if plugin_content.get(key) + } + return data or None + + +def _launch_pod( + ctx, install_name: str = "hello", podName: str = "hello-pod", helloTo: str = "World!" +): + command = ( + f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/hello " + f"--set podName={podName} --set helloTo={helloTo}" + ) + log.info(command) + log.info(run_command(command)) + + +if __name__ == "__main__": + hello() diff --git a/test/data/services.graphml b/test/data/services.graphml deleted file mode 100644 index c9e0a0d01..000000000 --- a/test/data/services.graphml +++ /dev/null @@ -1,27 +0,0 @@ -network.yaml
+ +````yaml +nodes: + - name: tank-0000 + addnode: + - tank-0001 + ln: + lnd: true + + - name: tank-0001 + addnode: + - tank-0002 + ln: + lnd: true + + - name: tank-0002 + addnode: + - tank-0000 + ln: + lnd: true + + - name: tank-0003 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + + - name: tank-0004 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + + - name: tank-0005 + addnode: + - tank-0000 + ln: + lnd: true + +plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc) + preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code + hello: + entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file + podName: "hello-pre-deploy" + helloTo: "preDeploy!" + postDeploy: + hello: + entrypoint: "../../plugins/hello" + podName: "hello-post-deploy" + helloTo: "postDeploy!" + simln: # You can have multiple plugins per hook + entrypoint: "../../plugins/simln" + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + preNode: # preNode plugins run before each node is deployed + hello: + entrypoint: "../../plugins/hello" + helloTo: "preNode!" + postNode: + hello: + entrypoint: "../../plugins/hello" + helloTo: "postNode!" + preNetwork: + hello: + entrypoint: "../../plugins/hello" + helloTo: "preNetwork!" + podName: "hello-pre-network" + postNetwork: + hello: + entrypoint: "../../plugins/hello" + helloTo: "postNetwork!" + podName: "hello-post-network" +```` + +- diff --git a/test/data/services/network.yaml b/test/data/services/network.yaml new file mode 100644 index 000000000..d8709f857 --- /dev/null +++ b/test/data/services/network.yaml @@ -0,0 +1,42 @@ +nodes: + - name: john + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 + addnode: + - paul + - name: paul + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 + - name: george + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 + addnode: + - ringo + - name: ringo + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 +fork_observer: + enabled: true +caddy: + enabled: true +services: + - title: Ringo REST + path: /ringo/ + host: ringo.default + port: 18443 \ No newline at end of file diff --git a/test/data/services/node-defaults.yaml b/test/data/services/node-defaults.yaml new file mode 100644 index 000000000..7e021cad1 --- /dev/null +++ b/test/data/services/node-defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/signet-signer.json b/test/data/signet-signer.json new file mode 100644 index 000000000..6cf25b9fd --- /dev/null +++ b/test/data/signet-signer.json @@ -0,0 +1,52 @@ +{ + "address": + { + "address": "tb1q6vakuyw2jhzwmnxcaxryxs6c670fr9esfvrrhj", + "scriptPubKey": "0014d33b6e11ca95c4edccd8e986434358d79e919730", + "ismine": true, + "solvable": true, + "desc": "wpkh([2e239023/84h/1h/0h/0/0]03d28a77a6ea884727049c72818c312d1f7fefb4aa5e62c9211403fd6218eaa6fa)#vp55uze6", + "parent_desc": "wpkh([2e239023/84h/1h/0h]tpubDDsmgRhtuNzhYbEiUbucryCNyjshjKf4fawmVWAAwej5HhSLmXVWD9z8U8QHgaSQmYmGBfTfab6nsM4bLQkRR1qdpazc258PGtcyVJeDLbj/0/*)#r5lwvzmj", + "iswatchonly": false, + "isscript": false, + "iswitness": true, + "witness_version": 0, + "witness_program": "d33b6e11ca95c4edccd8e986434358d79e919730", + "pubkey": "03d28a77a6ea884727049c72818c312d1f7fefb4aa5e62c9211403fd6218eaa6fa", + "ischange": false, + "timestamp": 1725633470, + "hdkeypath": "m/84h/1h/0h/0/0", + "hdseedid": "0000000000000000000000000000000000000000", + "hdmasterfingerprint": "2e239023", + "labels": [ + "bech32" + ] + }, + "descriptors": + [ + { + "desc": "wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps", + "timestamp": 0, + "active": true, + "internal": false, + "range": [ + 0, + 999 + ], + "next": 0, + "next_index": 0 + }, + { + "desc": "wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/1/*)#9xl6dz3g", + "timestamp": 0, + "active": true, + "internal": true, + "range": [ + 0, + 999 + ], + "next": 0, + "next_index": 0 + } + ] +} \ No newline at end of file diff --git a/test/data/signet/network.yaml b/test/data/signet/network.yaml new file mode 100644 index 000000000..630f6f626 --- /dev/null +++ b/test/data/signet/network.yaml @@ -0,0 +1,84 @@ +nodes: + - name: miner + image: + tag: "29.0-util" + - name: tank-1 + image: + tag: "0.16.1" + addnode: + - miner + - name: tank-2 + image: + tag: "0.17.0" + addnode: + - miner + - name: tank-3 + image: + tag: "0.19.2" + addnode: + - miner + - name: tank-4 + image: + tag: "0.20.0" + addnode: + - miner + - name: tank-5 + image: + tag: "0.21.1" + addnode: + - miner + - name: tank-6 + image: + tag: "24.2" + addnode: + - miner + - name: tank-7 + image: + tag: "25.1" + addnode: + - miner + - name: tank-8 + image: + tag: "26.0" + addnode: + - miner + - name: tank-9 + image: + tag: "27.0" + addnode: + - miner + - name: tank-10 + image: + tag: "0.16.1" + addnode: + - miner + - name: tank-11 + image: + tag: "98.0.0-invalid-blocks" + addnode: + - miner + - name: tank-12 + image: + tag: "94.0.0-5k-inv" + addnode: + - miner + - name: tank-13 + image: + tag: "95.0.0-disabled-opcodes" + addnode: + - miner + - name: tank-14 + image: + tag: "96.0.0-no-mp-trim" + addnode: + - miner + - name: tank-15 + image: + tag: "97.0.0-50-orphans" + addnode: + - miner + - name: tank-16 + image: + tag: "99.0.0-unknown-message" + addnode: + - miner diff --git a/test/data/signet/node-defaults.yaml b/test/data/signet/node-defaults.yaml new file mode 100644 index 000000000..941f03881 --- /dev/null +++ b/test/data/signet/node-defaults.yaml @@ -0,0 +1,15 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: Always + tag: "27.0" + +global: + chain: signet + +spec: + restartPolicy: Never + +defaultConfig: | + debug=rpc + debug=net + signetchallenge=0014d33b6e11ca95c4edccd8e986434358d79e919730 \ No newline at end of file diff --git a/test/data/ten_semi_unconnected.graphml b/test/data/ten_semi_unconnected.graphml deleted file mode 100644 index c2277407c..000000000 --- a/test/data/ten_semi_unconnected.graphml +++ /dev/null @@ -1,101 +0,0 @@ -- - - - - - - - - - - - - - - - - - -- 27.0 - -uacomment=w0 -debug=validation - true - lnd - -- diff --git a/test/data/ten_semi_unconnected/network.yaml b/test/data/ten_semi_unconnected/network.yaml new file mode 100644 index 000000000..f058ac70f --- /dev/null +++ b/test/data/ten_semi_unconnected/network.yaml @@ -0,0 +1,31 @@ +nodes: + - name: tank-0000 + config: | + debug=rpc + debug=validation + - name: tank-0001 + config: | + debug=net + debug=validation + - name: tank-0002 + config: | + debug=validation + - name: tank-0003 + config: | + debug=validation + - name: tank-0004 + - name: tank-0005 + config: | + debug=validation + - name: tank-0006 + - name: tank-0007 + config: | + debug=validation + - name: tank-0008 + addnode: + - tank-0009 + config: | + debug=validation + - name: tank-0009 + config: | + debug=validation diff --git a/test/data/ten_semi_unconnected/node-defaults.yaml b/test/data/ten_semi_unconnected/node-defaults.yaml new file mode 100644 index 000000000..7e021cad1 --- /dev/null +++ b/test/data/ten_semi_unconnected/node-defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/wargames/namespaces/armies/namespace-defaults.yaml b/test/data/wargames/namespaces/armies/namespace-defaults.yaml new file mode 100644 index 000000000..fa7f68ecc --- /dev/null +++ b/test/data/wargames/namespaces/armies/namespace-defaults.yaml @@ -0,0 +1,6 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager +podLimit: 3 diff --git a/test/data/wargames/namespaces/armies/namespaces.yaml b/test/data/wargames/namespaces/armies/namespaces.yaml new file mode 100644 index 000000000..86cde68af --- /dev/null +++ b/test/data/wargames/namespaces/armies/namespaces.yaml @@ -0,0 +1,2 @@ +namespaces: +- name: wargames-red \ No newline at end of file diff --git a/test/data/wargames/networks/armada/network.yaml b/test/data/wargames/networks/armada/network.yaml new file mode 100644 index 000000000..9cb614810 --- /dev/null +++ b/test/data/wargames/networks/armada/network.yaml @@ -0,0 +1,6 @@ +nodes: +- name: armada + image: + tag: '27.0' + addnode: + - miner.default \ No newline at end of file diff --git a/test/data/wargames/networks/armada/node-defaults.yaml b/test/data/wargames/networks/armada/node-defaults.yaml new file mode 100644 index 000000000..bb219cf19 --- /dev/null +++ b/test/data/wargames/networks/armada/node-defaults.yaml @@ -0,0 +1,6 @@ +global: + chain: regtest +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: '27.0' \ No newline at end of file diff --git a/test/data/wargames/networks/battlefield/network.yaml b/test/data/wargames/networks/battlefield/network.yaml new file mode 100644 index 000000000..5cf96504f --- /dev/null +++ b/test/data/wargames/networks/battlefield/network.yaml @@ -0,0 +1,10 @@ +nodes: +- name: miner + image: + tag: '27.0' +- name: target-red + addnode: + - miner + image: + tag: '27.0' + \ No newline at end of file diff --git a/test/data/wargames/networks/battlefield/node-defaults.yaml b/test/data/wargames/networks/battlefield/node-defaults.yaml new file mode 100644 index 000000000..7399f5c34 --- /dev/null +++ b/test/data/wargames/networks/battlefield/node-defaults.yaml @@ -0,0 +1,6 @@ +global: + chain: regtest +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: '27.0' diff --git a/test/graph_test.py b/test/graph_test.py index 68485f93d..60888180d 100755 --- a/test/graph_test.py +++ b/test/graph_test.py @@ -2,111 +2,89 @@ import json import os -import tempfile -import uuid -from pathlib import Path +import sys +import pexpect from test_base import TestBase -from warnet.lnd import LNDNode -from warnet.utils import DEFAULT_TAG + +from warnet.process import stream_command + +NETWORKS_DIR = "networks" class GraphTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "services.graphml" - self.json_file_path = Path(os.path.dirname(__file__)) / "data" / "LN_10.json" - self.NUM_IMPORTED_NODES = 10 - self.test_dir = tempfile.TemporaryDirectory() - self.tf_create = f"{self.test_dir.name}/{str(uuid.uuid4())}.graphml" - self.tf_import = f"{self.test_dir.name}/{str(uuid.uuid4())}.graphml" def run_test(self): - self.test_graph_creation_and_import() - self.validate_graph_schema() - - self.start_server() try: - self.test_graph_with_optional_services() - self.test_created_graph() - self.test_imported_graph() + # cwd out of the git repo for remainder of script + os.chdir(self.tmpdir) + self.directory_not_exist() + os.mkdir(NETWORKS_DIR) + self.directory_exists() + self.run_created_network() finally: - self.stop_server() - - def test_graph_creation_and_import(self): - self.log.info(f"CLI tool creating test graph file: {self.tf_create}") - self.log.info( - self.warcli( - f"graph create 10 --outfile={self.tf_create} --version={DEFAULT_TAG}", network=False - ) - ) - self.wait_for_predicate(lambda: Path(self.tf_create).exists()) - - self.log.info(f"CLI tool importing json and writing test graph file: {self.tf_import}") - self.log.info( - self.warcli( - f"graph import-json {self.json_file_path} --outfile={self.tf_import} --ln_image=carlakirkcohen/lnd:attackathon --cb=carlakirkcohen/circuitbreaker:attackathon-test", - network=False, - ) - ) - self.wait_for_predicate(lambda: Path(self.tf_import).exists()) - - def validate_graph_schema(self): - self.log.info("Validating graph schema") - assert "invalid" not in self.warcli(f"graph validate {Path(self.tf_create)}", False) - assert "invalid" not in self.warcli(f"graph validate {Path(self.tf_import)}", False) - assert "invalid" not in self.warcli(f"graph validate {self.graph_file_path}", False) - - def test_graph_with_optional_services(self): - self.log.info("Testing graph with optional services...") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) - self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - self.warcli("bitcoin rpc 0 getblockcount") - - self.log.info("Checking services...") - self.warcli("network down") - self.wait_for_all_tanks_status(target="stopped") + self.cleanup() - def test_created_graph(self): - self.log.info("Testing created graph...") - self.log.info(self.warcli(f"network start {Path(self.tf_create)} --force")) - self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("network down") - self.wait_for_all_tanks_status(target="stopped") - - def test_imported_graph(self): - self.log.info("Testing imported graph...") - self.log.info(self.warcli(f"network start {Path(self.tf_import)} --force")) + def directory_not_exist(self): + try: + self.log.info("testing warnet create, dir doesn't exist") + self.sut = pexpect.spawn("warnet create") + self.sut.expect("init", timeout=10) + except Exception as e: + print(f"\nReceived prompt text:\n {self.sut.before.decode('utf-8')}\n") + raise e + + def directory_exists(self): + try: + self.log.info("testing warnet create, dir does exist") + self.sut = pexpect.spawn("warnet create", encoding="utf-8") + self.sut.logfile = sys.stdout + self.sut.expect("name", timeout=30) + self.sut.sendline("ANewNetwork") + self.sut.expect("many", timeout=30) + self.sut.sendline("") + self.sut.expect("version", timeout=30) + self.sut.sendline("") + self.sut.expect("connections", timeout=30) + self.sut.sendline("") + self.sut.expect("many", timeout=30) + # Up arrow three times: [12] -> 8 -> 4 -> 0 (done) + self.sut.sendline("\x1b[A" * 3) + self.sut.expect("enable fork-observer", timeout=30) + self.sut.sendline("") + self.sut.expect("seconds", timeout=30) + self.sut.sendline("") + self.sut.expect("enable grafana", timeout=30) + self.sut.sendline("true") + self.sut.expect("successfully", timeout=50) + except Exception as e: + print(f"\nReceived prompt text:\n {self.sut.before.decode('utf-8')}\n") + raise e + + def run_created_network(self): + self.log.info("adding custom config to one tank") + with open("networks/ANewNetwork/network.yaml") as f: + s = f.read() + s = s.replace(" name: tank-0000\n", " name: tank-0000\n config: debug=mempool\n") + with open("networks/ANewNetwork/network.yaml", "w") as f: + f.write(s) + + self.log.info("deploying new network") + stream_command("warnet deploy networks/ANewNetwork") self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("scenarios run ln_init") - self.wait_for_all_scenarios() - - self.verify_ln_channel_policies() - - def verify_ln_channel_policies(self): - self.log.info("Ensuring warnet LN channel policies match imported JSON description") - with open(self.json_file_path) as file: - actual = json.loads(self.warcli("ln rpc 0 describegraph"))["edges"] - expected = json.loads(file.read())["edges"] - expected = sorted(expected, key=lambda chan: int(chan["channel_id"])) - for chan_index, actual_chan_json in enumerate(actual): - expected_chan = LNDNode.lnchannel_from_json(expected[chan_index]) - actual_chan = LNDNode.lnchannel_from_json(actual_chan_json) - if not expected_chan.channel_match(actual_chan): - self.log.info( - f"Channel {chan_index} policy mismatch, testing flipped channel: {actual_chan.short_chan_id}" - ) - if not expected_chan.channel_match(actual_chan.flip()): - raise Exception( - f"Channel policy doesn't match source: {actual_chan.short_chan_id}\n" - + f"Actual:\n{actual_chan}\n" - + f"Expected:\n{expected_chan}\n" - ) + debugs = json.loads(self.warnet("bitcoin rpc tank-0000 logging")) + # set in defaultConfig + assert debugs["rpc"] + # set in config just for this tank + assert debugs["mempool"] + # santy check + assert not debugs["zmq"] + # verify that prometheus exporter is making its rpc calls + self.wait_for_predicate( + lambda: "method=getblockcount user=user" in self.warnet("logs tank-0000") + ) if __name__ == "__main__": diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py new file mode 100755 index 000000000..d2ebf13bb --- /dev/null +++ b/test/ln_basic_test.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import json +import os +import subprocess +from pathlib import Path +from time import sleep + +from test_base import TestBase + +from warnet.process import stream_command + + +class LNBasicTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.lns = [ + "tank-0000-ln", + "tank-0001-ln", + "tank-0002-ln", + "tank-0003-ln", + "tank-0004-ln", + "tank-0005-ln", + ] + + self.cb_port = 9235 + self.cb_node = "tank-0003-ln" + self.port_forward = None + + def run_test(self): + try: + # Wait for all nodes to wake up. ln_init will start automatically + self.setup_network() + + # Test pyln-proto package in scenario + self.test_pyln_scenario() + + # Test manually configured macroons + self.test_admin_macaroons() + + # Test circuit breaker API + self.test_circuit_breaker_api() + + # Send a payment across channels opened automatically by ln_init + self.pay_invoice(sender="tank-0005-ln", recipient="tank-0003-ln") + + # Manually open two more channels between first three nodes + # and send a payment using warnet RPC + self.manual_open_channels() + self.wait_for_gossip_sync(self.lns[:3], 2 + 2) + self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") + + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + stream_command(f"warnet deploy {self.network_dir}") + + def test_pyln_scenario(self): + self.log.info("Running pyln_connect scenario") + scenario_file = self.scen_dir / "test_scenarios" / "pyln_connect.py" + self.log.info(f"Running scenario from: {scenario_file}") + stream_command(f"warnet run {scenario_file} --source_dir={self.scen_dir} --debug") + + def test_admin_macaroons(self): + self.log.info("Testing lnd nodes with same macaroon root key can query each other") + # These tanks all use the same default macaroon root key, meaning the macaroons + # generated at ~/.lnd/.../admin.macaroon in each lnd container are authorized + # to make requests to each other. + info = json.loads( + self.warnet("ln rpc tank-0001-ln --rpcserver=tank-0002-ln.default:10009 getinfo") + ) + assert info["alias"] == "tank-0002-ln.default" + info = json.loads( + self.warnet("ln rpc tank-0002-ln --rpcserver=tank-0005-ln.default:10009 getinfo") + ) + assert info["alias"] == "tank-0005-ln.default" + + self.log.info("Testing lnd nodes with unique macaroon root key can NOT query each other") + # These tanks are configured with unique macaroon root keys + try: + self.warnet("ln rpc tank-0001-ln --rpcserver=tank-0003-ln.default:10009 getinfo") + raise AssertionError("That should not have worked!") + except Exception as e: + assert "verification failed: signature mismatch after caveat verification" in str(e) + try: + self.warnet("ln rpc tank-0001-ln --rpcserver=tank-0004-ln.default:10009 getinfo") + raise AssertionError("That should not have worked!") + except Exception as e: + assert "verification failed: signature mismatch after caveat verification" in str(e) + try: + self.warnet("ln rpc tank-0003-ln --rpcserver=tank-0004-ln.default:10009 getinfo") + raise AssertionError("That should not have worked!") + except Exception as e: + assert "verification failed: signature mismatch after caveat verification" in str(e) + + def fund_wallets(self): + for ln in self.lns: + if ln == "tank-0000-ln": + # cln + addr = json.loads(self.warnet(f"ln rpc {ln} newaddr p2tr"))["p2tr"] + else: + # lnd + addr = json.loads(self.warnet(f"ln rpc {ln} newaddress p2tr"))["address"] + self.warnet(f"bitcoin rpc tank-0000 sendtoaddress {addr} 10") + self.wait_for_predicate( + lambda: ( + json.loads(self.warnet("bitcoin rpc tank-0000 getmempoolinfo"))["size"] + == len(self.lns) + ) + ) + self.warnet("bitcoin rpc tank-0000 -generate 1") + # cln takes a long time to register its own balance? + self.wait_for_predicate( + lambda: ( + len( + json.loads(self.warnet("ln rpc tank-0000-ln bkpr-listbalances"))["accounts"][0][ + "balances" + ] + ) + > 0 + ) + ) + + def manual_open_channels(self): + self.fund_wallets() + # 0 -> 1 -> 2 + pk1 = self.warnet("ln pubkey tank-0001-ln") + pk2 = self.warnet("ln pubkey tank-0002-ln") + + host1 = "" + host2 = "" + + while not host1 or not host2: + if not host1: + host1 = self.warnet("ln host tank-0001-ln") + if not host2: + host2 = self.warnet("ln host tank-0002-ln") + sleep(1) + + print(self.warnet(f"ln rpc tank-0000-ln connect {pk1} {host1}")) + print(self.warnet(f"ln rpc tank-0000-ln fundchannel {pk1} 100000")) + + print( + self.warnet( + f"ln rpc tank-0001-ln openchannel --node_key {pk2} --local_amt 100000 --connect {host2}" + ) + ) + + self.wait_for_predicate( + lambda: json.loads(self.warnet("bitcoin rpc tank-0000 getmempoolinfo"))["size"] == 2 + ) + + self.warnet("bitcoin rpc tank-0000 -generate 10") + + def wait_for_gossip_sync(self, nodes, expected): + while len(nodes) > 0: + for node in nodes: + if node == "tank-0000-ln": + # cln + chs = json.loads(self.warnet(f"ln rpc {node} listchannels"))["channels"] + chs = [ch for ch in chs if ch["direction"] == 1] + else: + # lnd + chs = json.loads(self.warnet(f"ln rpc {node} describegraph"))["edges"] + if len(chs) >= expected: + nodes.remove(node) + sleep(1) + + def pay_invoice(self, sender: str, recipient: str): + init_balance = int(json.loads(self.warnet(f"ln rpc {recipient} channelbalance"))["balance"]) + inv = json.loads(self.warnet(f"ln rpc {recipient} addinvoice --amt 1000")) + print(inv) + if sender == "tank-0000-ln": + # cln + print(self.warnet(f"ln rpc {sender} pay {inv['payment_request']}")) + else: + # lnd + print(self.warnet(f"ln rpc {sender} payinvoice -f {inv['payment_request']}")) + + def wait_for_success(): + return ( + int(json.loads(self.warnet(f"ln rpc {recipient} channelbalance"))["balance"]) + == init_balance + 1000 + ) + + self.wait_for_predicate(wait_for_success) + + def test_circuit_breaker_api(self): + self.log.info("Testing Circuit Breaker API with direct kubectl commands") + + # Test /info endpoint + info_cmd = f"kubectl exec {self.cb_node} -c circuitbreaker -- wget -qO - 127.0.0.1:{self.cb_port}/api/info" + info = json.loads(subprocess.check_output(info_cmd, shell=True).decode()) + assert "nodeKey" in info, "Circuit breaker info missing nodeKey" + self.log.info(f"Got node info: {info}") + + # Test /limits endpoint + limits_cmd = f"kubectl exec {self.cb_node} -c circuitbreaker -- wget -qO - 127.0.0.1:{self.cb_port}/api/limits" + limits = json.loads(subprocess.check_output(limits_cmd, shell=True).decode()) + assert "limits" in limits, "Circuit breaker limits missing" + self.log.info(f"Got limits: {limits}") + + self.log.info("✅ Circuit Breaker API tests passed") + + +if __name__ == "__main__": + test = LNBasicTest() + test.run_test() diff --git a/test/ln_test.py b/test/ln_test.py index 31e8be112..ee27b6256 100755 --- a/test/ln_test.py +++ b/test/ln_test.py @@ -1,125 +1,122 @@ #!/usr/bin/env python3 - +import ast import json import os from pathlib import Path +from typing import Optional from test_base import TestBase -from warnet.services import ServiceType + +from warnet.k8s import wait_for_pod +from warnet.process import run_command, stream_command class LNTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "ln.graphml" + self.graph_file = Path(os.path.dirname(__file__)) / "data" / "LN_10.json" + self.imported_network_dir = self.tmpdir / "imported_network" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.plugins_dir = Path(os.path.dirname(__file__)).parent / "resources" / "plugins" + self.simln_exec = Path("simln/plugin.py") def run_test(self): - self.start_server() try: + self.import_network() self.setup_network() - self.run_ln_init_scenario() self.test_channel_policies() - self.test_ln_payment_0_to_2() - self.test_ln_payment_2_to_0() - self.test_simln() + self.test_payments() + self.run_simln() finally: - self.stop_server() + self.cleanup() + + def import_network(self): + self.log.info("Importing network graph from JSON...") + res = self.warnet(f"import-network {self.graph_file} {self.imported_network_dir}") + self.log.info(f"\n{res}") def setup_network(self): - self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) - self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - - def get_cb_forwards(self, index): - cmd = "wget -q -O - 127.0.0.1:9235/api/forwarding_history" - res = self.wait_for_rpc( - "exec_run", [index, ServiceType.CIRCUITBREAKER.value, cmd, self.network_name] - ) - return json.loads(res) - - def run_ln_init_scenario(self): - self.log.info("Running LN Init scenario") - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("scenarios run ln_init") - self.wait_for_all_scenarios() - scenario_return_code = self.get_scenario_return_code("ln_init") - if scenario_return_code != 0: - raise Exception("LN Init scenario failed") + self.log.info("Setting up network...") + stream_command(f"warnet deploy {self.imported_network_dir}") def test_channel_policies(self): self.log.info("Ensuring node-level channel policy settings") - node2pub, node2host = json.loads(self.warcli("ln rpc 2 getinfo"))["uris"][0].split("@") - chan_id = json.loads(self.warcli("ln rpc 2 listchannels"))["channels"][0]["chan_id"] - chan = json.loads(self.warcli(f"ln rpc 2 getchaninfo {chan_id}")) - - # node_1 or node_2 is tank 2 with its non-default --bitcoin.timelockdelta=33 - if chan["node1_policy"]["time_lock_delta"] != 33: - assert ( - chan["node2_policy"]["time_lock_delta"] == 33 - ), "Expected time_lock_delta to be 33" - - self.log.info("Ensuring no circuit breaker forwards yet") - assert len(self.get_cb_forwards(1)["forwards"]) == 0, "Expected no circuit breaker forwards" - - def test_ln_payment_0_to_2(self): - self.log.info("Test LN payment from 0 -> 2") - inv = json.loads(self.warcli("ln rpc 2 addinvoice --amt=2000"))["payment_request"] - self.log.info(f"Got invoice from node 2: {inv}") - self.log.info("Paying invoice from node 0...") - self.log.info(self.warcli(f"ln rpc 0 payinvoice -f {inv}")) - - self.wait_for_predicate(self.check_invoice_settled) - - self.log.info("Ensuring channel-level channel policy settings: source") - payment = json.loads(self.warcli("ln rpc 0 listpayments"))["payments"][0] - assert ( - payment["fee_msat"] == "5506" - ), f"Expected fee_msat to be 5506, got {payment['fee_msat']}" - - self.log.info("Ensuring circuit breaker tracked payment") - assert len(self.get_cb_forwards(1)["forwards"]) == 1, "Expected one circuit breaker forward" - - def test_ln_payment_2_to_0(self): - self.log.info("Test LN payment from 2 -> 0") - inv = json.loads(self.warcli("ln rpc 0 addinvoice --amt=1000"))["payment_request"] - self.log.info(f"Got invoice from node 0: {inv}") - self.log.info("Paying invoice from node 2...") - self.log.info(self.warcli(f"ln rpc 2 payinvoice -f {inv}")) - - self.wait_for_predicate(lambda: self.check_invoices(0) == 1) - - self.log.info("Ensuring channel-level channel policy settings: target") - payment = json.loads(self.warcli("ln rpc 2 listpayments"))["payments"][0] - assert ( - payment["fee_msat"] == "2213" - ), f"Expected fee_msat to be 2213, got {payment['fee_msat']}" - - def test_simln(self): - self.log.info("Engaging simln") - node2pub, _ = json.loads(self.warcli("ln rpc 2 getinfo"))["uris"][0].split("@") - activity = [ - {"source": "ln-0", "destination": node2pub, "interval_secs": 1, "amount_msat": 2000} - ] - self.warcli( - f"network export --exclude=[1] --activity={json.dumps(activity).replace(' ', '')}" - ) - self.wait_for_predicate(lambda: self.check_invoices(2) > 1) - assert self.check_invoices(0) == 1, "Expected one invoice for node 0" - assert self.check_invoices(1) == 0, "Expected no invoices for node 1" - - def check_invoice_settled(self): - invs = json.loads(self.warcli("ln rpc 2 listinvoices"))["invoices"] - if len(invs) > 0 and invs[0]["state"] == "SETTLED": - self.log.info("Invoice settled") - return True - return False - - def check_invoices(self, index): - invs = json.loads(self.warcli(f"ln rpc {index} listinvoices"))["invoices"] - settled = sum(1 for inv in invs if inv["state"] == "SETTLED") - self.log.debug(f"Node {index} has {settled} settled invoices") - return settled + graphs = [] + for n in range(10): + ln = f"tank-{n:04d}-ln" + res = self.warnet(f"ln rpc {ln} describegraph") + graphs.append(json.loads(res)["edges"]) + + def check_policy(node: int, index: int, field: str, values: tuple): + self.log.info(f"Checking policy: Node={node} ch={index} Expected={field}:{values}") + graph = graphs[node] + assert len(graph) == 13 + ch = graph[index] + a = int(ch["node1_policy"][field]) + b = int(ch["node2_policy"][field]) + assert values == (a, b) or values == ( + b, + a, + ), f"policy check failed:\nActual:\n{ch}\nExpected:\n{field}:{values}" + + # test one property of one channel from each node + check_policy(0, 0, "fee_base_msat", (250, 1000)) + check_policy(1, 1, "time_lock_delta", (40, 100)) + check_policy(2, 2, "fee_rate_milli_msat", (1, 4000)) + check_policy(3, 3, "fee_rate_milli_msat", (499, 4000)) + check_policy(4, 4, "time_lock_delta", (40, 144)) + check_policy(5, 5, "max_htlc_msat", (1980000000, 1500000000)) + check_policy(6, 6, "fee_rate_milli_msat", (550, 71)) + check_policy(7, 7, "min_htlc", (1000, 1)) + check_policy(8, 8, "time_lock_delta", (80, 144)) + check_policy(9, 9, "fee_base_msat", (616, 1000)) + + def test_payments(self): + def get_and_pay(src, tgt): + src = f"tank-{src:04d}-ln" + tgt = f"tank-{tgt:04d}-ln" + invoice = json.loads(self.warnet(f"ln rpc {tgt} addinvoice --amt 230118"))[ + "payment_request" + ] + print(self.warnet(f"ln rpc {src} payinvoice {invoice} --force")) + + get_and_pay(0, 5) + get_and_pay(2, 3) + get_and_pay(1, 9) + get_and_pay(8, 7) + get_and_pay(4, 6) + + def run_simln(self): + self.log.info("Running SimLN...") + activity_cmd = f"{self.plugins_dir}/{self.simln_exec} get-example-activity" + activity = run_command(activity_cmd) + launch_cmd = f"{self.plugins_dir}/{self.simln_exec} launch-activity '{activity}'" + pod = run_command(launch_cmd).strip() + wait_for_pod(pod) + self.log.info("Checking SimLN...") + self.wait_for_predicate(self.found_results_remotely) + self.log.info("SimLN was successful.") + + def found_results_remotely(self, pod: Optional[str] = None) -> bool: + if pod is None: + pod = self.get_first_simln_pod() + self.log.info(f"Checking for results file in {pod}") + results_file = run_command( + f"{self.plugins_dir}/{self.simln_exec} sh {pod} ls /working/results" + ).strip() + self.log.info(f"Results file: {results_file}") + results = run_command( + f"{self.plugins_dir}/{self.simln_exec} sh {pod} cat /working/results/{results_file}" + ).strip() + self.log.info(results) + return results.find("Success") > 0 + + def get_first_simln_pod(self): + command = f"{self.plugins_dir}/{self.simln_exec} list-pod-names" + pod_names_literal = run_command(command) + self.log.info(f"{command}: {pod_names_literal}") + pod_names = ast.literal_eval(pod_names_literal) + return pod_names[0] if __name__ == "__main__": diff --git a/test/local_test_runner.py b/test/local_test_runner.py new file mode 100755 index 000000000..0fe744ed2 --- /dev/null +++ b/test/local_test_runner.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +import subprocess +import sys + +import yaml + +# developers can use this tool to verify all repo workflow test execute to completion +# +# execute from repo root like this to execute all tests +# python test/local_test_runner.py +# +# add optional argument to just execute any matching tests +# python test/local_test_runner.py [ln_ | graph] + + +def has_key_path(d, key_path, separator="."): + """Check if a nested key path (dotted notation) exists in a dictionary.""" + keys = key_path.split(separator) + for key in keys: + if not isinstance(d, dict) or key not in d: + return False + d = d[key] + return True + + +# Load the workflow file +with open(".github/workflows/test.yml") as file: + workflow = yaml.safe_load(file) + +tests_total = 0 +tests_skipped = 0 +tests_completed = 0 + +for job_details in workflow.get("jobs", {}).values(): + if has_key_path(job_details, "strategy.matrix.test"): + print("Found test strategy job, starting serial execution of each test") + tests = job_details["strategy"]["matrix"]["test"] + + for test in tests: + tests_total += 1 + if len(sys.argv) > 1 and sys.argv[1] not in test: + print("skipping test as requested:", test) + tests_skipped += 1 + continue + command = f"python test/{test}" + print( + "###################################################################################################" + ) + print("############## executing:", command) + print( + "###################################################################################################" + ) + process = subprocess.run(command, shell=True) + if process.returncode != 0: + print("******** testing failed") + if process.stdout: + print("stdout:", process.stdout) + if process.stderr: + print("stderr:", process.stderr) + sys.exit(1) + tests_completed += 1 + +print( + "###################################################################################################" +) +print("testing complete") +print(f"{tests_completed} of {tests_total} complete - skipped: {tests_skipped} tests") +print( + "###################################################################################################" +) diff --git a/test/logging_test.py b/test/logging_test.py index 2b7797970..230a55d41 100755 --- a/test/logging_test.py +++ b/test/logging_test.py @@ -1,66 +1,51 @@ #!/usr/bin/env python3 -import logging import os -import threading from datetime import datetime from pathlib import Path -from subprocess import PIPE, Popen, run import requests from test_base import TestBase +from warnet.k8s import get_ingress_ip_or_host + class LoggingTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "logging.graphml" + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "logging" self.scripts_dir = Path(os.path.dirname(__file__)) / ".." / "resources" / "scripts" - self.connect_logging_process = None - self.connect_logging_thread = None - self.connect_logging_logger = logging.getLogger("cnct_log") def run_test(self): - self.start_server() try: - self.start_logging() self.setup_network() + self.wait_for_endpoint_ready() self.test_prometheus_and_grafana() finally: - if self.connect_logging_process is not None: - self.log.info("Terminating background connect_logging.sh process...") - self.connect_logging_process.terminate() - self.stop_server() - - def start_logging(self): - self.log.info("Running install_logging.sh") - # Block until complete - run([f"{self.scripts_dir / 'install_logging.sh'}"]) - self.log.info("Running connect_logging.sh") - # Stays alive in background - self.connect_logging_process = Popen( - [f"{self.scripts_dir / 'connect_logging.sh'}"], - stdout=PIPE, - stderr=PIPE, - bufsize=1, - universal_newlines=True, - ) - self.log.info("connect_logging.sh started...") - self.connect_logging_thread = threading.Thread( - target=self.output_reader, - args=(self.connect_logging_process.stdout, self.connect_logging_logger.info), - ) - self.connect_logging_thread.daemon = True - self.connect_logging_thread.start() - - self.log.info("Waiting for RPC") - self.wait_for_rpc("scenarios_available") + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running", timeout=10 * 60) self.wait_for_all_edges() + self.wait_for_predicate(lambda: get_ingress_ip_or_host()) + ingress_ip = get_ingress_ip_or_host() + self.grafana_url = f"http://{ingress_ip}/grafana" + self.log.info(f"Grafana URL: {self.grafana_url}") + + def wait_for_endpoint_ready(self): + self.log.info("Waiting for Grafana to be ready to receive API calls...") + + def check_endpoint(): + try: + response = requests.get(f"{self.grafana_url}/login") + return response.status_code == 200 + except requests.RequestException: + return False + + self.wait_for_predicate(check_endpoint, timeout=120) + self.log.info("Grafana login endpoint returned status code 200") def make_grafana_api_request(self, ds_uid, start, metric): self.log.info("Making Grafana request...") @@ -69,18 +54,24 @@ def make_grafana_api_request(self, ds_uid, start, metric): "from": f"{start}", "to": "now", } - reply = requests.post("http://localhost:3000/api/ds/query", json=data) - assert reply.status_code == 200 + reply = requests.post(f"{self.grafana_url}/api/ds/query", json=data) + if reply.status_code != 200: + self.log.error(f"Grafana API request failed with status code {reply.status_code}") + self.log.error(f"Response content: {reply.text}") + return None # Default ref ID is "A", only inspecting one "frame" return reply.json()["results"]["A"]["frames"][0]["data"]["values"] def test_prometheus_and_grafana(self): self.log.info("Starting network activity scenarios") - self.warcli("scenarios run miner_std --allnodes --interval=5 --mature") - self.warcli("scenarios run tx_flood --interval=1") - prometheus_ds = requests.get("http://localhost:3000/api/datasources/name/Prometheus") + miner_file = "resources/scenarios/miner_std.py" + tx_flood_file = "resources/scenarios/tx_flood.py" + self.warnet(f"run {miner_file} --allnodes --interval=5 --mature") + self.warnet(f"run {tx_flood_file} --interval=1") + + prometheus_ds = requests.get(f"{self.grafana_url}/api/datasources/name/Prometheus") assert prometheus_ds.status_code == 200 prometheus_uid = prometheus_ds.json()["uid"] self.log.info(f"Got Prometheus data source uid from Grafana: {prometheus_uid}") @@ -89,6 +80,9 @@ def test_prometheus_and_grafana(self): def get_five_values_for_metric(metric): data = self.make_grafana_api_request(prometheus_uid, start, metric) + if data is None: + self.log.info(f"Failed to get Grafana data for {metric}") + return False if len(data) < 1: self.log.info(f"No Grafana data yet for {metric}") return False @@ -100,6 +94,11 @@ def get_five_values_for_metric(metric): self.wait_for_predicate(lambda: get_five_values_for_metric("blocks")) self.wait_for_predicate(lambda: get_five_values_for_metric("txrate")) + self.wait_for_predicate(lambda: get_five_values_for_metric("lnd_block_height")) + + # Verify default dashboard exists + dbs = requests.get(f"{self.grafana_url}/api/search").json() + assert dbs[0]["title"] == "Default Warnet Dashboard" if __name__ == "__main__": diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py new file mode 100755 index 000000000..1431cc65d --- /dev/null +++ b/test/namespace_admin_test.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +from typing import Callable, Optional + +from scenarios_test import ScenariosTest +from test_base import TestBase + +from warnet.constants import KUBECONFIG, WARGAMES_NAMESPACE_PREFIX +from warnet.k8s import ( + K8sError, + get_kubeconfig_value, + get_static_client, + open_kubeconfig, + write_kubeconfig, +) +from warnet.process import run_command + + +class NamespaceAdminTest(ScenariosTest, TestBase): + def __init__(self): + super().__init__() + + self.namespace_dir = ( + Path(os.path.dirname(__file__)) + / "data" + / "admin" + / "namespaces" + / "two_namespaces_two_users" + ) + + self.initial_context = None + self.current_context = None + self.bob_user = "bob-warnettest" + self.bob_auth_file = "bob-warnettest-wargames-red-team-warnettest-kubeconfig" + self.bob_context = "bob-warnettest-wargames-red-team-warnettest" + + self.blue_namespace = "wargames-blue-team-warnettest" + self.red_namespace = "wargames-red-team-warnettest" + self.blue_users = ["carol-warnettest", "default", "mallory-warnettest"] + self.red_users = ["alice-warnettest", self.bob_user, "default"] + + def run_test(self): + try: + os.chdir(self.tmpdir) + self.log.info(f"Running test in: {self.tmpdir}") + self.establish_initial_context() + self.setup_namespaces() + self.setup_service_accounts() + self.setup_network() + self.authenticate_and_become_bob() + self.bob_runs_scenario_tests() + finally: + self.return_to_initial_context() + try: + self.cleanup_kubeconfig() + except K8sError as e: + self.log.info(f"KUBECONFIG cleanup error: {e}") + self.cleanup() + + def establish_initial_context(self): + self.initial_context = get_kubeconfig_value("{.current-context}") + self.log.info(f"Initial context: {self.initial_context}") + self.current_context = self.initial_context + self.log.info(f"Current context: {self.current_context}") + + def setup_namespaces(self): + self.log.info("Setting up the namespaces") + self.log.info(self.warnet(f"deploy {self.namespace_dir}")) + self.wait_for_predicate(self.two_namespaces_are_validated) + self.log.info("Namespace setup complete") + + def setup_service_accounts(self): + self.log.info("Creating service accounts...") + self.log.info(self.warnet("admin create-kubeconfigs")) + self.wait_for_predicate(self.service_accounts_are_validated) + self.log.info("Service accounts have been set up and validated") + + def setup_network(self): + if self.current_context == self.bob_context: + self.log.info(f"Allowing {self.current_context} to update the network...") + assert self.this_is_the_current_context(self.bob_context) + self.warnet(f"deploy {self.network_dir}") + else: + self.log.info("Deploy networks to team namespaces") + assert self.this_is_the_current_context(self.initial_context) + self.log.info(self.warnet(f"deploy {self.network_dir} --to-all-users")) + self.wait_for_all_tanks_status() + self.log.info("Waiting for all edges") + self.wait_for_all_edges() + + def authenticate_and_become_bob(self): + self.log.info("Authenticating and becoming bob...") + self.log.info(f"Current context: {self.current_context}") + assert self.initial_context == self.current_context + assert get_kubeconfig_value("{.current-context}") == self.initial_context + self.warnet(f"auth kubeconfigs/{self.bob_auth_file}") + self.current_context = self.bob_context + assert get_kubeconfig_value("{.current-context}") == self.current_context + self.log.info(f"Current context: {self.current_context}") + + def service_accounts_are_validated(self) -> bool: + self.log.info("Checking service accounts") + sclient = get_static_client() + namespaces = sclient.list_namespace().items + + filtered_namespaces = [ + ns.metadata.name + for ns in namespaces + if ns.metadata.name.startswith(WARGAMES_NAMESPACE_PREFIX) + ] + assert len(filtered_namespaces) != 0 + + maybe_service_accounts = {} + + for namespace in filtered_namespaces: + service_accounts = sclient.list_namespaced_service_account(namespace=namespace).items + for sa in service_accounts: + maybe_service_accounts.setdefault(namespace, []).append(sa.metadata.name) + + expected = { + self.blue_namespace: self.blue_users, + self.red_namespace: self.red_users, + } + + return maybe_service_accounts == expected + + def get_namespaces(self) -> Optional[list[str]]: + self.log.info("Querying the namespaces...") + resp = self.warnet("admin namespaces list") + if resp == "No warnet namespaces found.": + return None + namespaces = [] + for line in resp.splitlines(): + if line.startswith("- "): + namespaces.append(line.lstrip("- ")) + self.log.info(f"Namespaces: {namespaces}") + return namespaces + + def two_namespaces_are_validated(self) -> bool: + maybe_namespaces = self.get_namespaces() + if maybe_namespaces is None: + return False + if self.blue_namespace not in maybe_namespaces: + return False + return self.red_namespace in maybe_namespaces + + def return_to_initial_context(self): + self.log.info(self.warnet("auth --revert")) + self.wait_for_predicate(self.this_is_the_current_context(self.initial_context)) + + def this_is_the_current_context(self, context: str) -> Callable[[], bool]: + cmd = "kubectl config current-context" + current_context = run_command(cmd).strip() + self.log.info(f"Current context: {current_context} {context == current_context}") + return lambda: current_context == context + + def cleanup_kubeconfig(self): + try: + kubeconfig_data = open_kubeconfig(KUBECONFIG) + except K8sError as e: + raise K8sError(f"Could not open KUBECONFIG: {KUBECONFIG}") from e + + kubeconfig_data = remove_user(kubeconfig_data, self.bob_user) + kubeconfig_data = remove_context(kubeconfig_data, self.bob_context) + + try: + write_kubeconfig(kubeconfig_data, KUBECONFIG) + except Exception as e: + raise K8sError(f"Could not write to KUBECONFIG: {KUBECONFIG}") from e + + def bob_runs_scenario_tests(self): + assert self.this_is_the_current_context(self.bob_context) + super().run_test() + assert self.this_is_the_current_context(self.bob_context) + + +def remove_user(kubeconfig_data: dict, username: str) -> dict: + kubeconfig_data["users"] = [ + user for user in kubeconfig_data["users"] if user["name"] != username + ] + return kubeconfig_data + + +def remove_context(kubeconfig_data: dict, context_name: str) -> dict: + kubeconfig_data["contexts"] = [ + context for context in kubeconfig_data["contexts"] if context["name"] != context_name + ] + return kubeconfig_data + + +if __name__ == "__main__": + test = NamespaceAdminTest() + test.run_test() diff --git a/test/onion_test.py b/test/onion_test.py index 7f7454b60..75ec6b049 100755 --- a/test/onion_test.py +++ b/test/onion_test.py @@ -6,64 +6,75 @@ from test_base import TestBase +from warnet.k8s import pod_log + class OnionTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "12_node_ring.graphml" - self.onion_addr = None + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "onion" def run_test(self): - self.start_server() try: self.setup_network() - self.test_reachability() - self.test_onion_peer_connection() + self.check_tor() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - def test_reachability(self): - self.log.info("Checking IPv4 and onion reachability") - self.wait_for_predicate(self.check_reachability, timeout=10 * 60) + def check_tor(self): + onions = {"tank-0001": None, "tank-0002": None} - def check_reachability(self): - try: - info = json.loads(self.warcli("bitcoin rpc 0 getnetworkinfo")) - for net in info["networks"]: - if net["name"] == "ipv4" and not net["reachable"]: - return False - if net["name"] == "onion" and not net["reachable"]: - return False - if len(info["localaddresses"]) != 2: - return False - for addr in info["localaddresses"]: - assert "100." in addr["address"] or ".onion" in addr["address"] - if ".onion" in addr["address"]: - self.onion_addr = addr["address"] - return True - except Exception as e: - self.log.error(f"Error checking reachability: {e}") - return False - - def test_onion_peer_connection(self): - self.log.info("Attempting addnode to onion peer") - self.warcli(f"bitcoin rpc 1 addnode {self.onion_addr} add") - # Might take up to 10 minutes - self.wait_for_predicate(self.check_onion_peer, timeout=10 * 60) - - def check_onion_peer(self): - peers = json.loads(self.warcli("bitcoin rpc 0 getpeerinfo")) - for peer in peers: - self.log.debug(f"Checking peer: {peer['network']} {peer['addr']}") - if peer["network"] == "onion": + def get_onions(): + peers = ["tank-0001", "tank-0002"] + for tank in peers: + if not onions[tank]: + self.log.info(f"Getting local onion address from {tank}...") + info = json.loads(self.warnet(f"bitcoin rpc {tank} getnetworkinfo")) + for addr in info["localaddresses"]: + if "onion" in addr["address"]: + onions[tank] = addr["address"] + self.log.info(f" ... got: {addr['address']}") + return all(onions[tank] for tank in peers) + + self.wait_for_predicate(get_onions) + + self.log.info("Adding 1 block") + self.warnet("bitcoin rpc tank-0001 createwallet miner") + self.warnet("bitcoin rpc tank-0001 -generate 1") + + self.log.info("Adding connections via onion: 0000->0001->0002") + self.warnet(f"bitcoin rpc tank-0000 addnode {onions['tank-0001']} add") + self.warnet(f"bitcoin rpc tank-0001 addnode {onions['tank-0002']} add") + + def onion_connect(): + peers = json.loads(self.warnet("bitcoin rpc tank-0001 getpeerinfo")) + self.log.info("\n") + self.log.info("Waiting for tank-0001 to have at least two onion peers:") + self.log.info(json.dumps(peers, indent=2)) + if len(peers) >= 2: + for peer in peers: + assert peer["network"] == "onion" return True - return False + else: + self.log.info("tank-0001 tor log tail:") + stream = pod_log( + pod_name="tank-0001", + container_name="tor", + namespace="default", + follow=False, + tail_lines=5, + ) + for line in stream: + msg = line.decode("utf-8").rstrip() + msg = msg.split("]") + self.log.info(msg[-1]) + + self.wait_for_predicate(onion_connect, timeout=20 * 60) if __name__ == "__main__": diff --git a/test/plugin_test.py b/test/plugin_test.py new file mode 100755 index 000000000..a0358a585 --- /dev/null +++ b/test/plugin_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import ast +import os +from functools import partial +from pathlib import Path +from typing import Optional + +from test_base import TestBase + +from warnet.k8s import download, wait_for_pod +from warnet.process import run_command + + +class PluginTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "network_with_plugins" + self.plugins_dir = Path(os.path.dirname(__file__)).parent / "resources" / "plugins" + self.simln_exec = self.plugins_dir / "simln" / "plugin.py" + + def run_test(self): + try: + os.chdir(self.tmpdir) + self.deploy_with_plugin() + self.copy_results() + self.assert_hello_plugin() + finally: + self.cleanup() + + def deploy_with_plugin(self): + self.log.info("Deploy the ln network with a SimLN plugin") + results = self.warnet(f"deploy {self.network_dir}") + self.log.info(results) + wait_for_pod(self.get_first_simln_pod()) + + def copy_results(self): + pod = self.get_first_simln_pod() + partial_func = partial(self.found_results_remotely, pod) + self.wait_for_predicate(partial_func) + + download(pod, Path("/working/results"), Path(".")) + self.wait_for_predicate(self.found_results_locally) + + def found_results_remotely(self, pod: Optional[str] = None) -> bool: + if pod is None: + pod = self.get_first_simln_pod() + self.log.info(f"Checking for results file in {pod}") + results_file = run_command(f"{self.simln_exec} sh {pod} ls /working/results").strip() + self.log.info(f"Results file: {results_file}") + results = run_command( + f"{self.simln_exec} sh {pod} cat /working/results/{results_file}" + ).strip() + self.log.info(results) + return results.find("Success") > 0 + + def get_first_simln_pod(self): + command = f"{self.simln_exec} list-pod-names" + pod_names_literal = run_command(command) + self.log.info(f"{command}: {pod_names_literal}") + pod_names = ast.literal_eval(pod_names_literal) + return pod_names[0] + + def found_results_locally(self) -> bool: + directory = "results" + self.log.info(f"Searching {directory}") + for root, _dirs, files in os.walk(Path(directory)): + for file_name in files: + file_path = os.path.join(root, file_name) + + with open(file_path) as file: + content = file.read() + if "Success" in content: + self.log.info(f"Found downloaded results in directory: {directory}.") + return True + self.log.info(f"Did not find downloaded results in directory: {directory}.") + return False + + def assert_hello_plugin(self): + self.log.info("Waiting for the 'hello' plugin pods.") + wait_for_pod("hello-pre-deploy") + wait_for_pod("hello-post-deploy") + wait_for_pod("hello-pre-network") + wait_for_pod("hello-post-network") + wait_for_pod("tank-0000-post-hello-pod") + wait_for_pod("tank-0000-pre-hello-pod") + wait_for_pod("tank-0001-post-hello-pod") + wait_for_pod("tank-0001-pre-hello-pod") + wait_for_pod("tank-0002-post-hello-pod") + wait_for_pod("tank-0002-pre-hello-pod") + wait_for_pod("tank-0003-post-hello-pod") + wait_for_pod("tank-0003-pre-hello-pod") + wait_for_pod("tank-0004-post-hello-pod") + wait_for_pod("tank-0004-pre-hello-pod") + wait_for_pod("tank-0005-post-hello-pod") + wait_for_pod("tank-0005-pre-hello-pod") + + +if __name__ == "__main__": + test = PluginTest() + test.run_test() diff --git a/test/rpc_test.py b/test/rpc_test.py index 642151e5f..ba466ea53 100755 --- a/test/rpc_test.py +++ b/test/rpc_test.py @@ -10,10 +10,9 @@ class RPCTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "12_node_ring.graphml" + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "12_node_ring" def run_test(self): - self.start_server() try: self.setup_network() self.test_rpc_commands() @@ -21,44 +20,44 @@ def run_test(self): self.test_message_exchange() self.test_address_manager() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") self.wait_for_all_edges() def test_rpc_commands(self): self.log.info("Testing basic RPC commands") - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("bitcoin rpc 1 createwallet miner") - self.warcli("bitcoin rpc 1 -generate 101") - self.wait_for_predicate(lambda: "101" in self.warcli("bitcoin rpc 0 getblockcount")) + self.warnet("bitcoin rpc tank-0000 getblockcount") + self.warnet("bitcoin rpc tank-0001 createwallet miner") + self.warnet("bitcoin rpc tank-0001 -generate 101") + self.wait_for_predicate(lambda: "101" in self.warnet("bitcoin rpc tank-0000 getblockcount")) def test_transaction_propagation(self): self.log.info("Testing transaction propagation") address = "bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46" - txid = self.warcli(f"bitcoin rpc 1 sendtoaddress {address} 0.1") - self.wait_for_predicate(lambda: txid in self.warcli("bitcoin rpc 0 getrawmempool")) + txid = self.warnet(f"bitcoin rpc tank-0001 sendtoaddress {address} 0.1") + self.wait_for_predicate(lambda: txid in self.warnet("bitcoin rpc tank-0000 getrawmempool")) - node_log = self.warcli("bitcoin debug-log 1") + node_log = self.warnet("bitcoin debug-log tank-0001") assert txid in node_log, "Transaction ID not found in node log" - all_logs = self.warcli(f"bitcoin grep-logs {txid}") + all_logs = self.warnet(f"bitcoin grep-logs {txid}") count = all_logs.count("Enqueuing TransactionAddedToMempool") assert count > 1, f"Transaction not propagated to enough nodes (count: {count})" def test_message_exchange(self): self.log.info("Testing message exchange between nodes") - msgs = self.warcli("bitcoin messages 0 1") + msgs = self.warnet("bitcoin messages tank-0000 tank-0001") assert "verack" in msgs, "VERACK message not found in exchange" def test_address_manager(self): self.log.info("Testing address manager") def got_addrs(): - addrman = json.loads(self.warcli("bitcoin rpc 0 getrawaddrman")) + addrman = json.loads(self.warnet("bitcoin rpc tank-0000 getrawaddrman")) for key in ["tried", "new"]: obj = addrman[key] keys = list(obj.keys()) diff --git a/test/scenarios_test.py b/test/scenarios_test.py index 734b2cae4..9d89cf8ed 100755 --- a/test/scenarios_test.py +++ b/test/scenarios_test.py @@ -1,109 +1,136 @@ #!/usr/bin/env python3 import os +import re from pathlib import Path from test_base import TestBase +from warnet.control import stop_scenario +from warnet.process import run_command +from warnet.status import _get_deployed_scenarios as scenarios_deployed + class ScenariosTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "12_node_ring.graphml" + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "12_node_ring" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" def run_test(self): try: - self.start_server() + self.check_help() self.setup_network() - self.test_scenarios() + self.run_and_check_miner_scenario_from_file() + self.run_and_check_scenario_from_file() + self.run_and_check_scenario_from_file_debug() + self.check_regtest_recon() + self.check_active_count() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") - - def test_scenarios(self): - self.check_available_scenarios() - self.run_and_check_miner_scenario("miner_std") - self.run_and_check_miner_scenario_from_file("src/warnet/scenarios/miner_std.py") - self.run_and_check_scenario_from_file("test/data/scenario_p2p_interface.py") - - def check_available_scenarios(self): - self.log.info("Checking available scenarios") - # Use rpc instead of warcli so we get raw JSON object - scenarios = self.rpc("scenarios_available") - assert len(scenarios) == 4, f"Expected 4 available scenarios, got {len(scenarios)}" - self.log.info(f"Found {len(scenarios)} available scenarios") + self.wait_for_all_edges() def scenario_running(self, scenario_name: str): """Check that we are only running a single scenario of the correct name""" - active = self.rpc("scenarios_list_running") - running = scenario_name in active[0]["cmd"] - return running and len(active) == 1 + deployed = scenarios_deployed() + assert len(deployed) == 1 + return scenario_name in deployed[0]["name"] + + def check_scenario_stopped(self): + running = scenarios_deployed() + self.log.debug(f"Checking if scenario stopped. Running scenarios: {len(running)}") + return len(running) == 0 - def run_and_check_scenario_from_file(self, scenario_file): - scenario_name = self.get_scenario_name_from_path(scenario_file) + def check_scenario_clean_exit(self): + deployed = scenarios_deployed() + return all(scenario["status"] == "succeeded" for scenario in deployed) - def check_scenario_clean_exit(): - running = self.rpc("scenarios_list_running") - scenarios = [s for s in running if s["cmd"].strip() == scenario_name] - if not scenarios: - return False - scenario = scenarios[0] - if scenario["active"]: - return False - if scenario["return_code"] != 0: - raise Exception( - f"Scenario {scenario_name} failed with return code {scenario['return_code']}" - ) - return True - - self.log.info(f"Running scenario: {scenario_name}") - self.warcli(f"scenarios run-file {scenario_file}") - self.wait_for_predicate(lambda: check_scenario_clean_exit()) - - def run_and_check_miner_scenario(self, scenario_name): - self.log.info(f"Running scenario: {scenario_name}") - self.warcli(f"scenarios run {scenario_name} --allnodes --interval=1") - self.wait_for_predicate(lambda: self.scenario_running(scenario_name)) - self.wait_for_predicate(lambda: self.check_blocks(30)) - self.stop_scenario() + def stop_scenario(self): + self.log.info("Stopping running scenario") + running = scenarios_deployed() + assert len(running) == 1, f"Expected one running scenario, got {len(running)}" + assert running[0]["status"] == "running", "Scenario should be running" + stop_scenario(running[0]["name"]) + self.wait_for_predicate(self.check_scenario_stopped) + + def check_blocks(self, target_blocks, start: int = 0): + count = int(self.warnet("bitcoin rpc tank-0000 getblockcount")) + self.log.debug(f"Current block count: {count}, target: {start + target_blocks}") + + try: + deployed = scenarios_deployed() + commander = deployed[0]["commander"] + command = f"kubectl logs {commander}" + print("\ncommander output:") + print(run_command(command)) + print("\n") + except Exception: + pass - def run_and_check_miner_scenario_from_file(self, scenario_file): + return count >= start + target_blocks + + def check_help(self): + scenario_file = self.scen_dir / "miner_std.py" + self.log.info(f"Running scenario from file with -- --help: {scenario_file}") + help_text = self.warnet(f"run {scenario_file} -- --help") + assert "usage" in help_text + # no commander pods actually deployed + assert len(scenarios_deployed()) == 0 + + def run_and_check_miner_scenario_from_file(self): + scenario_file = self.scen_dir / "miner_std.py" self.log.info(f"Running scenario from file: {scenario_file}") - self.warcli(f"scenarios run-file {scenario_file} --allnodes --interval=1") - start = int(self.warcli("bitcoin rpc 0 getblockcount")) - scenario_name = self.get_scenario_name_from_path(scenario_file) - self.wait_for_predicate(lambda: self.scenario_running(scenario_name)) + self.warnet(f"run {scenario_file} --allnodes --interval=1") + start = int(self.warnet("bitcoin rpc tank-0000 getblockcount")) + self.wait_for_predicate(lambda: self.scenario_running("commander-minerstd")) self.wait_for_predicate(lambda: self.check_blocks(2, start=start)) + table = self.warnet("status") + assert "Active Scenarios: 1" in table self.stop_scenario() - def get_scenario_name_from_path(self, scenario_file): - return os.path.splitext(os.path.basename(scenario_file))[0] + def run_and_check_scenario_from_file_debug(self): + scenario_file = self.scen_dir / "test_scenarios" / "p2p_interface.py" + self.log.info(f"Running scenario from: {scenario_file}") + output = self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --debug") + self.check_for_pod_deletion_message(output) - def check_blocks(self, target_blocks, start: int = 0): - running = self.rpc("scenarios_list_running") - assert len(running) == 1, f"Expected one running scenario, got {len(running)}" - assert running[0]["active"], "Scenario should be active" + def run_and_check_scenario_from_file(self): + scenario_file = self.scen_dir / "test_scenarios" / "p2p_interface.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir}") + self.wait_for_predicate(self.check_scenario_clean_exit) - count = int(self.warcli("bitcoin rpc 0 getblockcount")) - self.log.debug(f"Current block count: {count}, target: {start + target_blocks}") - return count >= start + target_blocks + def check_regtest_recon(self): + scenario_file = self.scen_dir / "reconnaissance.py" + self.log.info(f"Running scenario from file: {scenario_file}") + self.warnet(f"run {scenario_file}") + self.wait_for_predicate(self.check_scenario_clean_exit) - def stop_scenario(self): - self.log.info("Stopping running scenario") - running = self.rpc("scenarios_list_running") - assert len(running) == 1, f"Expected one running scenario, got {len(running)}" - assert running[0]["active"], "Scenario should be active" - self.warcli(f"scenarios stop {running[0]['pid']}", False) - self.wait_for_predicate(self.check_scenario_stopped) + def check_active_count(self): + scenario_file = self.scen_dir / "test_scenarios" / "buggy_failure.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir}") - def check_scenario_stopped(self): - running = self.rpc("scenarios_list_running") - self.log.debug(f"Checking if scenario stopped. Running scenarios: {len(running)}") - return len(running) == 0 + def two_pass_one_fail(): + deployed = scenarios_deployed() + if len([s for s in deployed if s["status"] == "succeeded"]) != 2: + return False + return len([s for s in deployed if s["status"] == "failed"]) == 1 + + self.wait_for_predicate(two_pass_one_fail) + table = self.warnet("status") + assert "Active Scenarios: 0" in table + + def check_for_pod_deletion_message(self, input): + message = "Deleting pod..." + self.log.info(f"Checking for message: '{message}'") + assert re.search(re.escape(message), input, flags=re.MULTILINE) + self.log.info(f"Found message: '{message}'") if __name__ == "__main__": diff --git a/test/services_test.py b/test/services_test.py new file mode 100755 index 000000000..87f0a6f3f --- /dev/null +++ b/test/services_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import json +import os +from pathlib import Path +from time import sleep + +import requests +from test_base import TestBase + +from warnet.k8s import get_ingress_ip_or_host, wait_for_ingress_controller + + +class ServicesTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "services" + self.ingress_ip = None + + def run_test(self): + try: + self.setup_network() + self.get_ingress_ip() + self.check_fork_observer() + self.check_extra_services() + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + self.log.info(self.warnet(f"deploy {self.network_dir}")) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def get_ingress_ip(self): + self.log.info("Waiting for ingress controller") + wait_for_ingress_controller() + self.log.info("Waiting for ingress host") + attempts = 100 + while not self.ingress_ip: + self.ingress_ip = get_ingress_ip_or_host() + attempts -= 1 + if attempts < 0: + raise Exception("Never got ingress host") + sleep(1) + + def check_fork_observer(self): + self.log.info("Creating chain split") + self.warnet("bitcoin rpc john createwallet miner") + self.warnet("bitcoin rpc john -generate 1") + + # network id is 0xDEADBE in decimal + fo_data_uri = f"http://{self.ingress_ip}/fork-observer/api/14593470/data.json" + + def call_fo_api(): + # if on minikube remember to run `minikube tunnel` for this test to run + try: + self.log.info(f"Getting: {fo_data_uri}") + fo_data = requests.get(fo_data_uri) + # fork observed! + return len(fo_data.json()["header_infos"]) == 2 + except Exception as e: + self.log.info(f"Fork Observer API error: {e}") + self.log.info("No Fork observed yet") + return False + + self.wait_for_predicate(call_fo_api) + self.log.info("Fork observed!") + + self.log.info("Checking node description...") + fo_data = requests.get(fo_data_uri) + nodes = fo_data.json()["nodes"] + assert len(nodes) == 4 + assert nodes[1]["name"] == "john" + assert nodes[1]["description"] == "john.default.svc:18444" + + self.log.info("Checking reachable address is provided...") + self.warnet("bitcoin rpc george addnode john.default.svc:18444 onetry") + self.wait_for_predicate( + lambda: len(json.loads(self.warnet("bitcoin rpc george getpeerinfo"))) > 1 + ) + + def check_extra_services(self): + self.log.info("Checking extra web services added to caddy") + uri = f"http://{self.ingress_ip}/ringo/rest/chaininfo.json" + rest_data = requests.get(uri) + rest_json = rest_data.json() + assert rest_json["chain"] == "regtest" + + +if __name__ == "__main__": + test = ServicesTest() + test.run_test() diff --git a/test/signet_test.py b/test/signet_test.py new file mode 100755 index 000000000..726fdd3a7 --- /dev/null +++ b/test/signet_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +import json +import os +from pathlib import Path + +from test_base import TestBase + +from warnet.status import _get_deployed_scenarios as scenarios_deployed + + +class SignetTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "signet" + signer_data_path = Path(os.path.dirname(__file__)) / "data" / "signet-signer.json" + with open(signer_data_path) as f: + self.signer_data = json.loads(f.read()) + + def run_test(self): + try: + self.setup_network() + self.check_signet_recon() + self.check_signet_scenario_miner() + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + self.log.info(self.warnet(f"deploy {self.network_dir}")) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def check_signet_recon(self): + scenario_file = "resources/scenarios/reconnaissance.py" + self.log.info(f"Running scenario from file: {scenario_file}") + self.warnet(f"run {scenario_file}") + + def check_scenario_clean_exit(): + deployed = scenarios_deployed() + return all(scenario["status"] == "succeeded" for scenario in deployed) + + self.wait_for_predicate(check_scenario_clean_exit) + + def check_signet_scenario_miner(self): + self.warnet("bitcoin rpc miner createwallet miner") + self.warnet( + f"bitcoin rpc miner importdescriptors {json.dumps(self.signer_data['descriptors'])}" + ) + before_count = int(self.warnet("bitcoin rpc tank-1 getblockcount")) + + self.log.info("Generate 1 signet block from a scenario using the bitcoin-util grinder") + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + scenario_file = self.scen_dir / "test_scenarios" / "signet_grinder.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --admin") + self.wait_for_all_scenarios() + + after_count = int(self.warnet("bitcoin rpc tank-1 getblockcount")) + assert after_count - before_count == 1 + + deployed = scenarios_deployed() + found = False + for sc in deployed: + if "grinder" in sc["name"]: + found = True + log = self.warnet(f"logs {sc['name']}") + assert "Error grinding" not in log + assert found + + +if __name__ == "__main__": + test = SignetTest() + test.run_test() diff --git a/test/test_base.py b/test/test_base.py index 5ad3c9125..59a026256 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -1,26 +1,28 @@ -import atexit import json import logging import logging.config import os import re +import sys import threading from pathlib import Path -from subprocess import PIPE, Popen, run +from subprocess import run from tempfile import mkdtemp from time import sleep +import pexpect + from warnet import SRC_DIR -from warnet.cli.rpc import rpc_call -from warnet.utils import exponential_backoff -from warnet.warnet import Warnet +from warnet.k8s import get_pod_exit_status +from warnet.network import _connected as network_connected +from warnet.status import _get_deployed_scenarios as scenarios_deployed +from warnet.status import _get_tank_status as network_status class TestBase: def __init__(self): self.setup_environment() self.setup_logging() - atexit.register(self.cleanup) self.log_expected_msgs: None | [str] = None self.log_unexpected_msgs: None | [str] = None self.log_msg_assertions_passed = False @@ -30,11 +32,6 @@ def setup_environment(self): self.tmpdir = Path(mkdtemp(prefix="warnet-test-")) os.environ["XDG_STATE_HOME"] = str(self.tmpdir) self.logfilepath = self.tmpdir / "warnet.log" - # Use the same dir name for the warnet network name - # replacing underscores which throws off k8s - self.network_name = self.tmpdir.name.replace("_", "") - self.server = None - self.server_thread = None self.stop_threads = threading.Event() self.network = True @@ -45,23 +42,21 @@ def setup_logging(self): logging.config.dictConfig(logging_config) self.log = logging.getLogger("test") self.log.info("Logging started") + self.log.info(f"Testdir: {self.tmpdir}") def cleanup(self, signum=None, frame=None): - if self.server is None: - return try: self.log.info("Stopping network") if self.network: - self.warcli("network down") + session = pexpect.spawn("warnet down", encoding="utf-8") + session.logfile = sys.stdout + session.expect("Do you want to bring down the running Warnet?", timeout=30) + session.sendline("y") self.wait_for_all_tanks_status(target="stopped", timeout=60, interval=1) except Exception as e: self.log.error(f"Error bringing network down: {e}") finally: self.stop_threads.set() - self.server.terminate() - self.server.wait() - self.server_thread.join() - self.server = None def _print_and_assert_msgs(self, message): print(message) @@ -71,74 +66,33 @@ def _print_and_assert_msgs(self, message): self.log_msg_assertions_passed = True def assert_log_msgs(self): - assert ( - self.log_msg_assertions_passed - ), f"Log assertion failed. Expected message not found: {self.log_expected_msgs}" + assert self.log_msg_assertions_passed, ( + f"Log assertion failed. Expected message not found: {self.log_expected_msgs}" + ) self.log_msg_assertions_passed = False - def warcli(self, cmd, network=True): - self.log.debug(f"Executing warcli command: {cmd}") - command = ["warcli"] + cmd.split() - if network: - command += ["--network", self.network_name] + def warnet(self, cmd): + self.log.debug(f"Executing warnet command: {cmd}") + command = ["warnet"] + cmd.split() proc = run(command, capture_output=True) if proc.stderr: raise Exception(proc.stderr.decode().strip()) return proc.stdout.decode().strip() - def rpc(self, method, params=None) -> dict | list: - """Execute a warnet RPC API call directly""" - self.log.debug(f"Executing RPC method: {method}") - return rpc_call(method, params) - - @exponential_backoff(max_retries=20) - def wait_for_rpc(self, method, params=None): - """Repeatedly execute an RPC until it succeeds""" - return self.rpc(method, params) - def output_reader(self, pipe, func): while not self.stop_threads.is_set(): line = pipe.readline().strip() if line: func(line) - def start_server(self): - """Start the Warnet server and wait for RPC interface to respond""" - - if self.server is not None: - raise Exception("Server is already running") - - # TODO: check for conflicting warnet process - # maybe also ensure that no conflicting docker networks exist - - # For kubernetes we assume the server is started outside test base, - # but we can still read its log output - self.log.info("Starting Warnet server") - self.server = Popen( - ["kubectl", "logs", "-f", "rpc-0", "--since=1s"], - stdout=PIPE, - stderr=PIPE, - bufsize=1, - universal_newlines=True, - ) - - self.server_thread = threading.Thread( - target=self.output_reader, args=(self.server.stdout, self._print_and_assert_msgs) - ) - self.server_thread.daemon = True - self.server_thread.start() - - self.log.info("Waiting for RPC") - self.wait_for_rpc("scenarios_available") - - def stop_server(self): - self.cleanup() - def wait_for_predicate(self, predicate, timeout=5 * 60, interval=5): self.log.debug(f"Waiting for predicate with timeout {timeout}s and interval {interval}s") while timeout > 0: - if predicate(): - return + try: + if predicate(): + return + except Exception: + pass sleep(interval) timeout -= interval import inspect @@ -148,8 +102,8 @@ def wait_for_predicate(self, predicate, timeout=5 * 60, interval=5): ) def get_tank(self, index): - wn = Warnet.from_network(self.network_name) - return wn.tanks[index] + # TODO + return None def wait_for_all_tanks_status(self, target="running", timeout=20 * 60, interval=5): """Poll the warnet server for container status @@ -157,14 +111,15 @@ def wait_for_all_tanks_status(self, target="running", timeout=20 * 60, interval= """ def check_status(): - tanks = self.wait_for_rpc("network_status", {"network": self.network_name}) + tanks = network_status() stats = {"total": 0} + # "Probably" means all tanks are stopped and deleted + if len(tanks) == 0: + return True for tank in tanks: - for service in ["bitcoin", "lightning", "circuitbreaker"]: - status = tank.get(f"{service}_status") - if status: - stats["total"] += 1 - stats[status] = stats.get(status, 0) + 1 + status = tank["status"] + stats["total"] += 1 + stats[status] = stats.get(status, 0) + 1 self.log.info(f"Waiting for all tanks to reach '{target}': {stats}") return target in stats and stats[target] == stats["total"] @@ -174,26 +129,22 @@ def wait_for_all_edges(self, timeout=20 * 60, interval=5): """Ensure all tanks have all the connections they are supposed to have Block until all success """ - - def check_status(): - return self.wait_for_rpc("network_connected", {"network": self.network_name}) - - self.wait_for_predicate(check_status, timeout, interval) + self.wait_for_predicate(network_connected, timeout, interval) def wait_for_all_scenarios(self): def check_scenarios(): - scns = self.rpc("scenarios_list_running") - return all(not scn["active"] for scn in scns) + scns = scenarios_deployed() + if len(scns) == 0: + return True + for s in scns: + exit_status = get_pod_exit_status(s["name"], s["namespace"]) + self.log.debug(f"Scenario {s['name']} exited with code {exit_status}") + if exit_status != 0: + return False + return True self.wait_for_predicate(check_scenarios) - def get_scenario_return_code(self, scenario_name): - scns = self.rpc("scenarios_list_running") - scns = [scn for scn in scns if scn["cmd"].strip() == scenario_name] - if len(scns) == 0: - raise Exception(f"Scenario {scenario_name} not found in running scenarios") - return scns[0]["return_code"] - def assert_equal(thing1, thing2, *args): if thing1 != thing2 or any(thing1 != arg for arg in args): diff --git a/test/wargames_test.py b/test/wargames_test.py new file mode 100755 index 000000000..c304c51ac --- /dev/null +++ b/test/wargames_test.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path + +import pexpect +from test_base import TestBase + +from warnet.k8s import get_kubeconfig_value +from warnet.process import stream_command +from warnet.status import _get_deployed_scenarios as scenarios_deployed + + +class WargamesTest(TestBase): + def __init__(self): + super().__init__() + self.wargame_dir = Path(os.path.dirname(__file__)) / "data" / "wargames" + self.scen_src_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.scen_test_dir = ( + Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" / "test_scenarios" + ) + self.initial_context = get_kubeconfig_value("{.current-context}") + self.kconfigdir = self.tmpdir / "kubeconfigs" + + def run_test(self): + try: + self.setup_battlefield() + self.setup_armies() + self.check_scenario_permissions() + finally: + self.log.info("Restoring initial_context") + stream_command(f"kubectl config use-context {self.initial_context}") + self.cleanup() + + def setup_battlefield(self): + self.log.info("Setting up battlefield") + self.log.info(self.warnet(f"deploy {self.wargame_dir / 'networks' / 'battlefield'}")) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def setup_armies(self): + self.log.info("Deploying namespaces and armadas") + self.log.info(self.warnet(f"deploy {self.wargame_dir / 'namespaces' / 'armies'}")) + self.log.info( + self.warnet(f"deploy {self.wargame_dir / 'networks' / 'armada'} --to-all-users") + ) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def check_scenario_permissions(self): + self.log.info("Admin without --admin can not command a node outside of default namespace") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --debug" + ) + # Only miner.default and target-red.default were accesible + assert self.warnet("bitcoin rpc miner getblockcount") == "2" + + self.log.info("Admin with --admin can command all nodes in any namespace") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --admin --debug" + ) + # armada.wargames-red, miner.default and target-red.default were accesible + assert self.warnet("bitcoin rpc miner getblockcount") == "5" + + self.log.info("Switch to wargames player context") + self.log.info(self.warnet(f"admin create-kubeconfigs --kubeconfig-dir={self.kconfigdir}")) + clicker = pexpect.spawn( + f"warnet auth {self.kconfigdir}/warnet-user-wargames-red-kubeconfig" + ) + while clicker.expect(["Overwrite", "Updated kubeconfig"]) == 0: + print(clicker.before, clicker.after) + clicker.sendline("y") + print(clicker.before, clicker.after) + + self.log.info("Player without --admin can only command the node inside their own namespace") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --debug" + ) + # Only armada.wargames-red was (and is) accesible + assert self.warnet("bitcoin rpc armada getblockcount") == "6" + + self.log.info("Player attempting to use --admin is gonna have a bad time") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --admin --debug" + ) + # Nothing was accesible + assert self.warnet("bitcoin rpc armada getblockcount") == "6" + + self.log.info("Check pod limit per namespace") + for _ in range(10): + stream_command( + f"warnet run {self.scen_test_dir / 'nothing.py'} --source_dir={self.scen_src_dir}" + ) + assert len(scenarios_deployed()) == 2, ( + f"Unexpected scenarios deployed:{scenarios_deployed()}" + ) + + self.log.info("Restore admin context") + stream_command(f"kubectl config use-context {self.initial_context}") + # Sanity check + assert self.warnet("bitcoin rpc miner getblockcount") == "6" + + self.log.info("Re-generate kubeconfigs after a scenario has been run") + self.log.info(self.warnet(f"admin create-kubeconfigs --kubeconfig-dir={self.kconfigdir}")) + count = 0 + for p in self.kconfigdir.iterdir(): + if p.is_file(): + print(p) + assert "warnet-user" in str(p) + count += 1 + assert count <= 1 + + +if __name__ == "__main__": + test = WargamesTest() + test.run_test() diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..586994544 --- /dev/null +++ b/uv.lock @@ -0,0 +1,904 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "ansicon" +version = "1.89.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/e2/1c866404ddbd280efedff4a9f15abfe943cb83cde6e895022370f3a61f85/ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", size = 67312 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "blessed" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinxed", marker = "platform_system == 'Windows'" }, + { name = "six" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/ae/92e9968ad23205389ec6bd82e2d4fca3817f1cdef34e10aa8d529ef8b1d7/blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680", size = 6655612 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/98/584f211c3a4bb38f2871fa937ee0cc83c130de50c955d6c7e2334dbf4acb/blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058", size = 58372 }, +] + +[[package]] +name = "blinker" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/57/a6a1721eff09598fb01f3c7cda070c1b6a0f12d63c83236edf79a440abcc/blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83", size = 23161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456 }, +] + +[[package]] +name = "build" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/9e/2d725d2f7729c6e79ca62aeb926492abbc06e25910dd30139d60a68bcb19/build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d", size = 44781 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", size = 21911 }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, +] + +[[package]] +name = "cffi" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/bf/82c351342972702867359cfeba5693927efe0a8dd568165490144f554b18/cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", size = 516073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/cc/9298fb6235522e00e47d78d6aa7f395332ef4e5f6fe124f9a03aa60600f7/cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", size = 181912 }, + { url = "https://files.pythonhosted.org/packages/e7/79/dc5334fbe60635d0846c56597a8d2af078a543ff22bc48d36551a0de62c2/cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", size = 178297 }, + { url = "https://files.pythonhosted.org/packages/39/d7/ef1b6b16b51ccbabaced90ff0d821c6c23567fc4b2e4a445aea25d3ceb92/cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", size = 444909 }, + { url = "https://files.pythonhosted.org/packages/29/b8/6e3c61885537d985c78ef7dd779b68109ba256263d74a2f615c40f44548d/cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", size = 468854 }, + { url = "https://files.pythonhosted.org/packages/0b/49/adad1228e19b931e523c2731e6984717d5f9e33a2f9971794ab42815b29b/cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", size = 476890 }, + { url = "https://files.pythonhosted.org/packages/76/54/c00f075c3e7fd14d9011713bcdb5b4f105ad044c5ad948db7b1a0a7e4e78/cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", size = 459374 }, + { url = "https://files.pythonhosted.org/packages/f3/b9/f163bb3fa4fbc636ee1f2a6a4598c096cdef279823ddfaa5734e556dd206/cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", size = 466891 }, + { url = "https://files.pythonhosted.org/packages/31/52/72bbc95f6d06ff2e88a6fa13786be4043e542cb24748e1351aba864cb0a7/cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91", size = 477658 }, + { url = "https://files.pythonhosted.org/packages/67/20/d694811457eeae0c7663fa1a7ca201ce495533b646c1180d4ac25684c69c/cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", size = 453890 }, + { url = "https://files.pythonhosted.org/packages/dc/79/40cbf5739eb4f694833db5a27ce7f63e30a9b25b4a836c4f25fb7272aacc/cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", size = 478254 }, + { url = "https://files.pythonhosted.org/packages/e9/eb/2c384c385cca5cae67ca10ac4ef685277680b8c552b99aedecf4ea23ff7e/cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", size = 171285 }, + { url = "https://files.pythonhosted.org/packages/ca/42/74cb1e0f1b79cb64672f3cb46245b506239c1297a20c0d9c3aeb3929cb0c/cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", size = 180842 }, + { url = "https://files.pythonhosted.org/packages/1a/1f/7862231350cc959a3138889d2c8d33da7042b22e923457dfd4cd487d772a/cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", size = 182826 }, + { url = "https://files.pythonhosted.org/packages/8b/8c/26119bf8b79e05a1c39812064e1ee7981e1f8a5372205ba5698ea4dd958d/cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", size = 178494 }, + { url = "https://files.pythonhosted.org/packages/61/94/4882c47d3ad396d91f0eda6ef16d45be3d752a332663b7361933039ed66a/cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", size = 454459 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/a6beb119ad515058c5ee1829742d96b25b2b9204ff920746f6e13bf574eb/cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", size = 478502 }, + { url = "https://files.pythonhosted.org/packages/61/8a/2575cd01a90e1eca96a30aec4b1ac101a6fae06c49d490ac2704fa9bc8ba/cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", size = 485381 }, + { url = "https://files.pythonhosted.org/packages/cd/66/85899f5a9f152db49646e0c77427173e1b77a1046de0191ab3b0b9a5e6e3/cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", size = 470907 }, + { url = "https://files.pythonhosted.org/packages/00/13/150924609bf377140abe6e934ce0a57f3fc48f1fd956ec1f578ce97a4624/cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", size = 479074 }, + { url = "https://files.pythonhosted.org/packages/17/fd/7d73d7110155c036303b0a6462c56250e9bc2f4119d7591d27417329b4d1/cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", size = 484225 }, + { url = "https://files.pythonhosted.org/packages/fc/83/8353e5c9b01bb46332dac3dfb18e6c597a04ceb085c19c814c2f78a8c0d0/cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", size = 488388 }, + { url = "https://files.pythonhosted.org/packages/73/0c/f9d5ca9a095b1fc88ef77d1f8b85d11151c374144e4606da33874e17b65b/cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", size = 172096 }, + { url = "https://files.pythonhosted.org/packages/72/21/8c5d285fe20a6e31d29325f1287bb0e55f7d93630a5a44cafdafb5922495/cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", size = 181478 }, + { url = "https://files.pythonhosted.org/packages/17/8f/581f2f3c3464d5f7cf87c2f7a5ba9acc6976253e02d73804240964243ec2/cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", size = 182638 }, + { url = "https://files.pythonhosted.org/packages/8d/1c/c9afa66684b7039f48018eb11b229b659dfb32b7a16b88251bac106dd1ff/cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", size = 178453 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/1a134d479d3a5a1ff2fabbee551d1d3f1dd70f453e081b5f70d604aae4c0/cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", size = 454441 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/e1569475d63aad8042b0935dbf62ae2a54d1e9142424e2b0e924d2d4a529/cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", size = 478543 }, + { url = "https://files.pythonhosted.org/packages/d2/40/a9ad03fbd64309dec5bb70bc803a9a6772602de0ee164d7b9a6ca5a89249/cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", size = 485463 }, + { url = "https://files.pythonhosted.org/packages/a6/1a/f10be60e006dd9242a24bcc2b1cd55c34c578380100f742d8c610f7a5d26/cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", size = 470854 }, + { url = "https://files.pythonhosted.org/packages/cc/b3/c035ed21aa3d39432bd749fe331ee90e4bc83ea2dbed1f71c4bc26c41084/cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", size = 479096 }, + { url = "https://files.pythonhosted.org/packages/00/cb/6f7edde01131de9382c89430b8e253b8c8754d66b63a62059663ceafeab2/cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", size = 484013 }, + { url = "https://files.pythonhosted.org/packages/b9/83/8e4e8c211ea940210d293e951bf06b1bfb90f2eeee590e9778e99b4a8676/cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", size = 488119 }, + { url = "https://files.pythonhosted.org/packages/5e/52/3f7cfbc4f444cb4f73ff17b28690d12436dde665f67d68f1e1687908ab6c/cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", size = 172122 }, + { url = "https://files.pythonhosted.org/packages/94/19/cf5baa07ee0f0e55eab7382459fbddaba0fdb0ba45973dd92556ae0d02db/cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", size = 181504 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "43.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/ec/9fb9dcf4f91f0e5e76de597256c43eedefd8423aa59be95c70c4c3db426a/cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", size = 686873 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/46/dcd2eb6840b9452e7fbc52720f3dc54a85eb41e68414733379e8f98e3275/cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", size = 6239718 }, + { url = "https://files.pythonhosted.org/packages/e8/23/b0713319edff1d8633775b354f8b34a476e4dd5f4cd4b91e488baec3361a/cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", size = 3808466 }, + { url = "https://files.pythonhosted.org/packages/77/9d/0b98c73cebfd41e4fb0439fe9ce08022e8d059f51caa7afc8934fc1edcd9/cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", size = 3998060 }, + { url = "https://files.pythonhosted.org/packages/ae/71/e073795d0d1624847f323481f7d84855f699172a632aa37646464b0e1712/cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", size = 3792596 }, + { url = "https://files.pythonhosted.org/packages/83/25/439a8ddd8058e7f898b7d27c36f94b66c8c8a2d60e1855d725845f4be0bc/cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", size = 4008355 }, + { url = "https://files.pythonhosted.org/packages/c7/a2/1607f1295eb2c30fcf2c07d7fd0c3772d21dcdb827de2b2730b02df0af51/cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", size = 3899133 }, + { url = "https://files.pythonhosted.org/packages/5e/64/f41f42ddc9c583737c9df0093affb92c61de7d5b0d299bf644524afe31c1/cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", size = 4096946 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/d165adcf3e707d6a049d44ade6ca89973549bed0ab3686fa49efdeefea53/cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", size = 2616826 }, + { url = "https://files.pythonhosted.org/packages/f9/b7/38924229e84c41b0e88d7a5eed8a29d05a44364f85fbb9ddb3984b746fd2/cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", size = 3078700 }, + { url = "https://files.pythonhosted.org/packages/66/d7/397515233e6a861f921bd0365b162b38e0cc513fcf4f1bdd9cc7bc5a3384/cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", size = 6242814 }, + { url = "https://files.pythonhosted.org/packages/58/aa/99b2c00a4f54c60d210d6d1759c720ecf28305aa32d6fb1bb1853f415be6/cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", size = 3809467 }, + { url = "https://files.pythonhosted.org/packages/76/eb/ab783b47b3b9b55371b4361c7ec695144bde1a3343ff2b7a8c1d8fe617bb/cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", size = 3998617 }, + { url = "https://files.pythonhosted.org/packages/a3/62/62770f34290ebb1b6542bd3f13b3b102875b90aed4804e296f8d2a5ac6d7/cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", size = 3794003 }, + { url = "https://files.pythonhosted.org/packages/0f/6c/b42660b3075ff543065b2c1c5a3d9bedaadcff8ebce2ee981be2babc2934/cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", size = 4008774 }, + { url = "https://files.pythonhosted.org/packages/f7/74/028cea86db9315ba3f991e307adabf9f0aa15067011137c38b2fb2aa16eb/cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0", size = 3900098 }, + { url = "https://files.pythonhosted.org/packages/bd/f6/e4387edb55563e2546028ba4c634522fe727693d3cdd9ec0ecacedc75411/cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", size = 4096867 }, + { url = "https://files.pythonhosted.org/packages/ce/61/55560405e75432bdd9f6cf72fa516cab623b83a3f6d230791bc8fc4afeee/cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", size = 2616481 }, + { url = "https://files.pythonhosted.org/packages/e6/3d/696e7a0f04555c58a2813d47aaa78cb5ba863c1f453c74a4f45ae772b054/cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", size = 3081462 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "editor" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "runs" }, + { name = "xmod" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/92/734a4ab345914259cb6146fd36512608ea42be16195375c379046f33283d/editor-1.6.6.tar.gz", hash = "sha256:bb6989e872638cd119db9a4fce284cd8e13c553886a1c044c6b8d8a160c871f8", size = 3197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/c2/4bc8cd09b14e28ce3f406a8b05761bed0d785d1ca8c2a5c6684d884c66a2/editor-1.6.6-py3-none-any.whl", hash = "sha256:e818e6913f26c2a81eadef503a2741d7cca7f235d20e217274a009ecd5a74abf", size = 4017 }, +] + +[[package]] +name = "flask" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735 }, +] + +[[package]] +name = "google-auth" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/ae/634dafb151366d91eb848a25846a780dbce4326906ef005d199723fbbca0/google_auth-2.34.0.tar.gz", hash = "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc", size = 257875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fb/9af9e3f2996677bdda72734482934fe85a3abde174e5f0783ac2f817ba98/google_auth-2.34.0-py2.py3-none-any.whl", hash = "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", size = 200870 }, +] + +[[package]] +name = "idna" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/bd/fa8ce65b0a7d4b6d143ec23b0f5fd3f7ab80121078c465bc02baeaab22dc/importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5", size = 54320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/14/362d31bf1076b21e1bcdcb0dc61944822ff263937b804a79231df2774d28/importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", size = 26269 }, +] + +[[package]] +name = "inquirer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blessed" }, + { name = "editor" }, + { name = "readchar" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/06/ef91eb8f3feafb736aa33dcb278fc9555d17861aa571b684715d095db24d/inquirer-3.4.0.tar.gz", hash = "sha256:8edc99c076386ee2d2204e5e3653c2488244e82cb197b2d498b3c1b5ffb25d0b", size = 14472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/b2/be907c8c0f8303bc4b10089f5470014c3bf3521e9b8d3decf3037fd94725/inquirer-3.4.0-py3-none-any.whl", hash = "sha256:bb0ec93c833e4ce7b51b98b1644b0a4d2bb39755c39787f6a504e4fee7a11b60", size = 18077 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b1/6ca3c2052e584e9908a2c146f00378939b3c51b839304ab8ef4de067f042/jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5", size = 18319 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/54/7623e24ffc63730c3a619101361b08860c6b7c7cfc1aef6edb66d80ed708/jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3", size = 9883 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jinxed" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansicon", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085 }, +] + +[[package]] +name = "keyring" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/30/bfdde7294ba6bb2f519950687471dc6a0996d4f77ab30d75c841fa4994ed/keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef", size = 61495 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/42/ea8c9726e5ee5ff0731978aaf7cd5fa16674cf549c46279b279d7167c2b4/keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae", size = 38742 }, +] + +[[package]] +name = "kubernetes" +version = "30.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/3c/9f29f6cab7f35df8e54f019e5719465fa97b877be2454e99f989270b4f34/kubernetes-30.1.0.tar.gz", hash = "sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc", size = 887810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/2027ddede72d33be2effc087580aeba07e733a7360780ae87226f1f91bd8/kubernetes-30.1.0-py2.py3-none-any.whl", hash = "sha256:e212e8b7579031dd2e512168b617373bc1e03888d41ac4e04039240a292d478d", size = 1706042 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/0d/ad6a82320cb8eba710fd0dceb0f678d5a1b58d67d03ae5be14874baa39e0/more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923", size = 120755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/0b/6a51175e1395774449fca317fb8861379b7a2d59be411b8cce3d19d6ce78/more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27", size = 60935 }, +] + +[[package]] +name = "nh3" +version = "0.2.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", size = 15028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", size = 1374474 }, + { url = "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", size = 694573 }, + { url = "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", size = 844082 }, + { url = "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", size = 782460 }, + { url = "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", size = 879827 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/375afcc710dbe2d64cfbd69e31f82f3e423d43737258af01f6a56d844085/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", size = 841080 }, + { url = "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", size = 924144 }, + { url = "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", size = 769192 }, + { url = "https://files.pythonhosted.org/packages/a4/17/59391c28580e2c32272761629893e761442fc7666da0b1cdb479f3b67b88/nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", size = 791042 }, + { url = "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", size = 1010073 }, + { url = "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", size = 1029782 }, + { url = "https://files.pythonhosted.org/packages/63/1d/842fed85cf66c973be0aed8770093d6a04741f65e2c388ddd4c07fd3296e/nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", size = 942504 }, + { url = "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", size = 941541 }, + { url = "https://files.pythonhosted.org/packages/78/48/54a788fc9428e481b2f58e0cd8564f6c74ffb6e9ef73d39e8acbeae8c629/nh3-0.2.18-cp37-abi3-win32.whl", hash = "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", size = 573750 }, + { url = "https://files.pythonhosted.org/packages/26/8d/53c5b19c4999bdc6ba95f246f4ef35ca83d7d7423e5e38be43ad66544e5d/nh3-0.2.18-cp37-abi3-win_amd64.whl", hash = "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", size = 579012 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pkginfo" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz", hash = "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", size = 378457 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", size = 30392 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/a3/d2157f333900747f20984553aca98008b6dc843eb62f3a36030140ccec0d/pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", size = 148088 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/7e/5f50d07d5e70a2addbccd90ac2950f81d1edd0783630651d9268d7f1db49/pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473", size = 85313 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/00/e7bd1dec10667e3f2be602686537969a7ac92b0a7c5165be2e5875dc3971/pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", size = 307859 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/68/8906226b15ef38e71dc926c321d2fe99de8048e9098b5dfd38343011c886/pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b", size = 181220 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/07/6f63dda440d4abb191b91dc383b472dae3dd9f37e4c1e4a5c3db150531c6/pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965", size = 7838 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", size = 9184 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pywin32" +version = "306" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689 }, + { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547 }, + { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324 }, + { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705 }, + { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429 }, + { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "readchar" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/31/2934981710c63afa9c58947d2e676093ce4bb6c7ce60aac2fcc4be7d98d0/readchar-4.2.0.tar.gz", hash = "sha256:44807cbbe377b72079fea6cba8aa91c809982d7d727b2f0dbb2d1a8084914faa", size = 9691 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/6f/ca076ad4d18b3d33c31c304fb7e68dd9ce2bfdb49fb8874611ad7c55e969/readchar-4.2.0-py3-none-any.whl", hash = "sha256:2a587a27c981e6d25a518730ad4c88c429c315439baa6fda55d7a8b3ac4cb62a", size = 9349 }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + +[[package]] +name = "rich" +version = "13.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, +] + +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + +[[package]] +name = "runs" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xmod" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/6d/b9aace390f62db5d7d2c77eafce3d42774f27f1829d24fa9b6f598b3ef71/runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1", size = 5474 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/d6/17caf2e4af1dec288477a0cbbe4a96fbc9b8a28457dce3f1f452630ce216/runs-1.2.2-py3-none-any.whl", hash = "sha256:0980dcbc25aba1505f307ac4f0e9e92cbd0be2a15a1e983ee86c24c87b839dfd", size = 7033 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "twine" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "pkginfo" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", size = 225531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", size = 38650 }, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, +] + +[[package]] +name = "warnet" +version = "0.10.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "docker" }, + { name = "flask" }, + { name = "inquirer" }, + { name = "kubernetes" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tabulate" }, +] + +[package.optional-dependencies] +build = [ + { name = "build" }, + { name = "twine" }, +] + +[package.metadata] +requires-dist = [ + { name = "build", marker = "extra == 'build'" }, + { name = "click", specifier = "==8.1.7" }, + { name = "docker", specifier = "==7.1.0" }, + { name = "flask", specifier = "==3.0.3" }, + { name = "inquirer", specifier = "==3.4.0" }, + { name = "kubernetes", specifier = "==30.1.0" }, + { name = "pyyaml", specifier = "==6.0.2" }, + { name = "rich", specifier = "==13.7.1" }, + { name = "tabulate", specifier = "==0.9.0" }, + { name = "twine", marker = "extra == 'build'" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "werkzeug" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/e2/6dbcaab07560909ff8f654d3a2e5a60552d937c909455211b1b36d7101dc/werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306", size = 803966 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", size = 227554 }, +] + +[[package]] +name = "xmod" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/b2/e3edc608823348e628a919e1d7129e641997afadd946febdd704aecc5881/xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377", size = 3988 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/0dc75b64a764ea1cb8e4c32d1fb273c147304d4e5483cd58be482dc62e45/xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48", size = 4610 }, +] + +[[package]] +name = "zipp" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/af/9f2de5bd32549a1b705af7a7c054af3878816a1267cb389c03cc4f342a51/zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", size = 23244 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/cc/b9958af9f9c86b51f846d8487440af495ecf19b16e426fce1ed0b0796175/zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d", size = 9432 }, +]- - - - - - - - - - - - - - - - - -- 26.0 - - - - False - False - -- bitcoindevproject/bitcoin:26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - -- 26.0 - - - - False - False - --