diff --git a/resources/charts/bitcoincore/Chart.yaml b/resources/charts/bitcoincore/Chart.yaml
index 4feb6e32e..36f7498af 100644
--- a/resources/charts/bitcoincore/Chart.yaml
+++ b/resources/charts/bitcoincore/Chart.yaml
@@ -6,6 +6,9 @@ dependencies:
   - name: lnd
     version: 0.1.0
     condition: ln.lnd
+  - name: cln
+    version: 0.1.0
+    condition: ln.cln
 
 # A chart can be either an 'application' or a 'library' chart.
 #
diff --git a/resources/charts/bitcoincore/charts/cln/.helmignore b/resources/charts/bitcoincore/charts/cln/.helmignore
new file mode 100644
index 000000000..0e8a0eb36
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/resources/charts/bitcoincore/charts/cln/Chart.yaml b/resources/charts/bitcoincore/charts/cln/Chart.yaml
new file mode 100644
index 000000000..79eeda983
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: cln
+description: A Helm chart for CLN
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "0.1.0"
diff --git a/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl
new file mode 100644
index 000000000..d983355cf
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl
@@ -0,0 +1,78 @@
+{{/*
+Expand the name of the PARENT chart.
+*/}}
+{{- define "bitcoincore.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified PARENT app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "bitcoincore.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+
+
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "cln.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "cln.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "cln.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "cln.labels" -}}
+helm.sh/chart: {{ include "cln.chart" . }}
+{{ include "cln.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "cln.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "cln.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "cln.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "cln.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml
new file mode 100644
index 000000000..c69ac3c5d
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml
@@ -0,0 +1,36 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "cln.fullname" . }}
+  labels:
+    {{- include "cln.labels" . | nindent 4 }}
+data:
+  config: |
+    {{- .Values.baseConfig | nindent 4 }}
+    {{- .Values.defaultConfig | nindent 4 }}
+    {{- .Values.config | nindent 4 }}
+    network={{ .Values.global.chain }}
+    addr=0.0.0.0:{{ .Values.P2PPort }}
+    bitcoin-rpcconnect={{ include "bitcoincore.fullname" . }}
+    bitcoin-rpcport={{ index .Values.global .Values.global.chain "RPCPort" }}
+    bitcoin-rpcpassword={{ .Values.global.rpcpassword }}
+    alias={{ include "cln.fullname" . }}
+    announce-addr=dns:{{ include "cln.fullname" . }}:{{ .Values.P2PPort }}
+    database-upgrade=true
+    bitcoin-retry-timeout=600
+    grpc-port={{ .Values.RPCPort }}
+    grpc-host=0.0.0.0
+    clnrest-host=0.0.0.0
+    clnrest-port=3010
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "cln.fullname" . }}-channels
+  labels:
+    channels: "true"
+    {{- include "cln.labels" . | nindent 4 }}
+data:
+  source: {{ include "cln.fullname" . }}
+  channels: |
+    {{ .Values.channels | toJson }}
diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml
new file mode 100644
index 000000000..b2d651173
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml
@@ -0,0 +1,100 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: {{ include "cln.fullname" . }}
+  labels:
+    {{- include "cln.labels" . | nindent 4 }}
+    {{- with .Values.podLabels }}
+        {{- toYaml . | nindent 4 }}
+    {{- end }}
+    app: {{ include "cln.fullname" . }}
+    {{- if .Values.collectLogs }}
+    collect_logs: "true"
+    {{- end }}
+    chain: {{ .Values.global.chain }}
+  annotations:
+    kubectl.kubernetes.io/default-container: "cln"
+spec:
+  {{- with .Values.imagePullSecrets }}
+  imagePullSecrets:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+  securityContext:
+    {{- toYaml .Values.podSecurityContext | nindent 4 }}
+  containers:
+    - name: {{ .Chart.Name }}
+      securityContext:
+        {{- toYaml .Values.securityContext | nindent 8 }}
+      image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+      imagePullPolicy: {{ .Values.image.pullPolicy }}
+      ports:
+        - name: p2p
+          containerPort: {{ .Values.P2PPort }}
+          protocol: TCP
+        - name: rpc
+          containerPort: {{ .Values.RPCPort }}
+          protocol: TCP
+        - name: rest
+          containerPort: {{ .Values.RestPort }}
+          protocol: TCP
+      command:
+        - /bin/sh
+        - -c
+        - |
+          lightningd --conf=/root/.lightning/config &
+          sleep 1
+          lightning-cli createrune > /working/rune.json
+          echo "Here is the rune file contents"
+          cat /working/rune.json
+          wait
+      livenessProbe:
+        {{- toYaml .Values.livenessProbe | nindent 8 }}
+      readinessProbe:
+        {{- toYaml .Values.readinessProbe | nindent 8 }}
+      startupProbe:
+        {{- toYaml .Values.startupProbe | nindent 8 }}
+      resources:
+        {{- toYaml .Values.resources | nindent 8 }}
+      volumeMounts:
+        {{- with .Values.volumeMounts }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+        - mountPath: /root/.lightning/config
+          name: config
+          subPath: config
+    {{- with .Values.extraContainers }}
+    {{- toYaml . | nindent 4 }}
+    {{- end }}
+    - name: http-server
+      image: busybox
+      command: ["/bin/sh", "-c"]
+      args:
+        - |
+          echo "Starting HTTP server..."
+          busybox httpd -f -p 8080 -h /working
+      ports:
+        - containerPort: 8080
+          name: http
+      volumeMounts:
+        {{- with .Values.volumeMounts }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+  volumes:
+    {{- with .Values.volumes }}
+      {{- toYaml . | nindent 4 }}
+    {{- end }}
+    - configMap:
+        name: {{ include "cln.fullname" . }}
+      name: config
+  {{- with .Values.nodeSelector }}
+  nodeSelector:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+  {{- with .Values.affinity }}
+  affinity:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+  {{- with .Values.tolerations }}
+  tolerations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml
new file mode 100644
index 000000000..565f50182
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml
@@ -0,0 +1,24 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "cln.fullname" . }}
+  labels:
+    {{- include "cln.labels" . | nindent 4 }}
+    app: {{ include "cln.fullname" . }} 
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.P2PPort }}
+      targetPort: p2p
+      protocol: TCP
+      name: p2p
+    - port: {{ .Values.RPCPort }}
+      targetPort: rpc
+      protocol: TCP
+      name: rpc
+    - port: {{ .Values.RestPort }}
+      targetPort: rest
+      protocol: TCP
+      name: rest
+  selector:
+    {{- include "cln.selectorLabels" . | nindent 4 }}
diff --git a/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml b/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml
new file mode 100644
index 000000000..4565a17d1
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml
@@ -0,0 +1,15 @@
+{{- if .Values.metricsExport }}
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: {{ include "cln.fullname" . }}
+  labels:
+    app.kubernetes.io/name: cln-metrics
+    release: prometheus
+spec:
+  endpoints:
+    - port: prometheus-metrics
+  selector:
+    matchLabels:
+      app: {{ include "cln.fullname" . }}
+{{- end }}
diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml
new file mode 100644
index 000000000..d34144100
--- /dev/null
+++ b/resources/charts/bitcoincore/charts/cln/values.yaml
@@ -0,0 +1,102 @@
+# Default values for cln.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+namespace: warnet
+
+image:
+  repository: elementsproject/lightningd
+  pullPolicy: IfNotPresent
+  tag: "v25.02"
+
+nameOverride: ""
+fullnameOverride: ""
+
+podAnnotations: {}
+podLabels:
+  app: "warnet"
+  mission: "lightning"
+
+podSecurityContext: {}
+
+securityContext: {}
+
+service:
+  type: ClusterIP
+
+P2PPort: 9735
+RPCPort: 9736
+RestPort: 3010
+
+ingress:
+  enabled: false
+  className: ""
+  annotations: {}
+  hosts:
+    - host: chart-example.local
+      paths:
+        - path: /
+          pathType: ImplementationSpecific
+  tls: []
+
+resources: {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #   cpu: 100m
+  #   memory: 128Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 128Mi
+
+livenessProbe:
+  exec:
+    command:
+      - "/bin/sh"
+      - "-c"
+      - "lightning-cli getinfo >/dev/null 2>&1"
+  failureThreshold: 3
+  initialDelaySeconds: 5
+  periodSeconds: 5
+  successThreshold: 1
+  timeoutSeconds: 1
+readinessProbe:
+  failureThreshold: 10
+  periodSeconds: 30
+  successThreshold: 1
+  timeoutSeconds: 60
+  exec:
+    command:
+      - "/bin/sh"
+      - "-c"
+      - "lightning-cli getinfo 2>/dev/null | grep -q 'id' || exit 1"
+
+# Additional volumes on the output Deployment definition.
+volumes:
+  - name: working
+    emptyDir: {}
+
+# Additional volumeMounts on the output Deployment definition.
+volumeMounts:
+  - name: working
+    mountPath: "/working"
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+
+baseConfig: |
+  log-level=info
+  developer
+  dev-fast-gossip
+  bitcoin-rpcuser=user
+  # bitcoind.rpcpass are set in configmap.yaml
+
+config: ""
+
+defaultConfig: ""
+
+channels: []
diff --git a/resources/charts/bitcoincore/values.yaml b/resources/charts/bitcoincore/values.yaml
index 8c9f3215f..67f1b9b10 100644
--- a/resources/charts/bitcoincore/values.yaml
+++ b/resources/charts/bitcoincore/values.yaml
@@ -141,4 +141,5 @@ loadSnapshot:
   url: ""
 
 ln:
-  lnd: false
\ No newline at end of file
+  lnd: false
+  cln: false
diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md
index f6b24ef92..a627813af 100644
--- a/resources/plugins/simln/README.md
+++ b/resources/plugins/simln/README.md
@@ -19,8 +19,18 @@ SimLN also requires access details for each node; however, the SimLN plugin will
   "cert": <path_to_tls_cert>
 }
 ````
+SimLN plugin also supports Core Lightning (CLN).  CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing.
+```` JSON
+{
+  "id": <node_id>,
+  "address": https://<domain:port>,
+  "ca_cert": /working/<node_id>-ca.pem,
+  "client_cert": /working/<node_id>-client.pem,
+  "client_key": /working/<node_id>-client-key.pem
+}
+````
 
-Since SimLN already has access to those LND connection details, it means you can focus on the "activity" definitions.
+Since SimLN already has access to those LND and CLN connection details, it means you can focus on the "activity" definitions.
 
 ### Launch activity definitions from the command line
 The SimLN plugin takes "activity" definitions like so:
@@ -73,8 +83,8 @@ nodes:
     addnode:
       - tank-0000
     ln:
-      lnd: true
-    lnd:
+      cln: true
+    cln:
       channels:
         - id:
             block: 300
diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py
index 93c217f32..78df1a917 100755
--- a/resources/plugins/simln/plugin.py
+++ b/resources/plugins/simln/plugin.py
@@ -11,6 +11,7 @@
 
 from warnet.constants import LIGHTNING_MISSION, PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent
 from warnet.k8s import (
+    copyfile,
     download,
     get_default_namespace,
     get_mission,
@@ -145,6 +146,8 @@ def _launch_activity(activity: Optional[list[dict]], plugin_dir: str) -> str:
     activity_json = _generate_activity_json(activity)
     wait_for_init(name, namespace=get_default_namespace(), quiet=True)
 
+    # write cert files to container
+    transfer_cln_certs(name)
     if write_file_to_container(
         name,
         "init",
@@ -162,13 +165,18 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str:
     nodes = []
 
     for i in get_mission(LIGHTNING_MISSION):
-        name = i.metadata.name
-        node = {
-            "id": name,
-            "address": f"https://{name}:10009",
-            "macaroon": "/working/admin.macaroon",
-            "cert": "/working/tls.cert",
-        }
+        ln_name = i.metadata.name
+        port = 10009
+        node = {"id": ln_name}
+        if "cln" in i.metadata.labels["app.kubernetes.io/name"]:
+            port = 9736
+            node["ca_cert"] = f"/working/{ln_name}-ca.pem"
+            node["client_cert"] = f"/working/{ln_name}-client.pem"
+            node["client_key"] = f"/working/{ln_name}-client-key.pem"
+        else:
+            node["macaroon"] = "/working/admin.macaroon"
+            node["cert"] = "/working/tls.cert"
+        node["address"] = f"https://{ln_name}:{port}"
         nodes.append(node)
 
     if activity:
@@ -179,6 +187,39 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str:
     return json.dumps(data, indent=2)
 
 
+def transfer_cln_certs(name):
+    dst_container = "init"
+    for i in get_mission(LIGHTNING_MISSION):
+        ln_name = i.metadata.name
+        if "cln" in i.metadata.labels["app.kubernetes.io/name"]:
+            chain = i.metadata.labels["chain"]
+            cln_root = f"/root/.lightning/{chain}"
+            copyfile(
+                ln_name,
+                "cln",
+                f"{cln_root}/ca.pem",
+                name,
+                dst_container,
+                f"/working/{ln_name}-ca.pem",
+            )
+            copyfile(
+                ln_name,
+                "cln",
+                f"{cln_root}/client.pem",
+                name,
+                dst_container,
+                f"/working/{ln_name}-client.pem",
+            )
+            copyfile(
+                ln_name,
+                "cln",
+                f"{cln_root}/client-key.pem",
+                name,
+                dst_container,
+                f"/working/{ln_name}-client-key.pem",
+            )
+
+
 def _sh(pod, method: str, params: tuple[str, ...]) -> str:
     namespace = get_default_namespace()
 
diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py
index 240d9c61c..4a44ab4a7 100644
--- a/resources/scenarios/commander.py
+++ b/resources/scenarios/commander.py
@@ -13,8 +13,7 @@
 from time import sleep
 
 from kubernetes import client, config
-from ln_framework.ln import LND
-
+from ln_framework.ln import CLN, LND, LNNode
 from test_framework.authproxy import AuthServiceProxy
 from test_framework.p2p import NetworkThread
 from test_framework.test_framework import (
@@ -71,7 +70,10 @@
         )
 
     if pod.metadata.labels["mission"] == "lightning":
-        WARNET["lightning"].append(pod.metadata.name)
+        lnnode = LND(pod.metadata.name, pod.status.pod_ip)
+        if "cln" in pod.metadata.labels["app.kubernetes.io/name"]:
+            lnnode = CLN(pod.metadata.name, pod.status.pod_ip)
+        WARNET["lightning"].append(lnnode)
 
 for cm in cmaps.items:
     if not cm.metadata.labels or "channels" not in cm.metadata.labels:
@@ -92,6 +94,30 @@ def auth_proxy_request(self, method, path, postdata):
 AuthServiceProxy._request = auth_proxy_request
 
 
+# Create a custom formatter
+class ColorFormatter(logging.Formatter):
+    """Custom formatter to add color based on log level."""
+
+    # Define ANSI color codes
+    RED = "\033[91m"
+    YELLOW = "\033[93m"
+    GREEN = "\033[92m"
+    RESET = "\033[0m"
+
+    FORMATS = {
+        logging.DEBUG: f"{RESET}%(name)-8s - Thread-%(thread)d - %(message)s{RESET}",
+        logging.INFO: f"{RESET}%(name)-8s - %(message)s{RESET}",
+        logging.WARNING: f"{YELLOW}%(name)-8s - %(message)s{RESET}",
+        logging.ERROR: f"{RED}%(name)-8s - %(message)s{RESET}",
+        logging.CRITICAL: f"{RED}##%(name)-8s - %(message)s##{RESET}",
+    }
+
+    def format(self, record):
+        log_fmt = self.FORMATS.get(record.levelno)
+        formatter = logging.Formatter(log_fmt)
+        return formatter.format(record)
+
+
 class Commander(BitcoinTestFramework):
     # required by subclasses of BitcoinTestFramework
     def set_test_params(self):
@@ -132,7 +158,7 @@ def tank_connected(self, tank):
                 if count >= tank.init_peers:
                     break
                 else:
-                    sleep(1)
+                    sleep(5)
 
         conn_threads = [
             threading.Thread(target=tank_connected, args=(self, tank)) for tank in self.nodes
@@ -164,13 +190,12 @@ def setup(self):
         # 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)
+        ch.setFormatter(ColorFormatter())
         self.log.addHandler(ch)
 
         # Keep a separate index of tanks by pod name
         self.tanks: dict[str, TestNode] = {}
-        self.lns: dict[str, LND] = {}
+        self.lns: dict[str, LNNode] = {}
         self.channels = WARNET["channels"]
 
         for i, tank in enumerate(WARNET["tanks"]):
@@ -203,7 +228,7 @@ def setup(self):
             self.tanks[tank["tank"]] = node
 
         for ln in WARNET["lightning"]:
-            self.lns[ln] = LND(ln)
+            self.lns[ln.name] = ln
 
         self.num_nodes = len(self.nodes)
 
diff --git a/resources/scenarios/ln_framework/ln.py b/resources/scenarios/ln_framework/ln.py
index 8fcdc1bc7..f2ec4cbc2 100644
--- a/resources/scenarios/ln_framework/ln.py
+++ b/resources/scenarios/ln_framework/ln.py
@@ -1,7 +1,12 @@
+import base64
 import http.client
 import json
+import logging
 import ssl
-import time
+from abc import ABC, abstractmethod
+from time import sleep
+
+import requests
 
 # hard-coded deterministic lnd credentials
 ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6"
@@ -73,41 +78,331 @@ def to_lnd_chanpolicy(self, capacity):
         }
 
 
-class LND:
-    def __init__(self, pod_name):
+class LNNode(ABC):
+    @abstractmethod
+    def __init__(self, pod_name, ip_address):
         self.name = pod_name
+        self.ip_address = ip_address
+        self.log = logging.getLogger(pod_name)
+        handler = logging.StreamHandler()
+        formatter = logging.Formatter("%(name)-8s - %(levelname)s: %(message)s")
+        handler.setFormatter(formatter)
+        self.log.addHandler(handler)
+        self.log.setLevel(logging.INFO)
+
+    @staticmethod
+    def hex_to_b64(hex):
+        return base64.b64encode(bytes.fromhex(hex)).decode()
+
+    @staticmethod
+    def b64_to_hex(b64, reverse=False):
+        if reverse:
+            return base64.b64decode(b64)[::-1].hex()
+        else:
+            return base64.b64decode(b64).hex()
+
+    @abstractmethod
+    def newaddress(self, max_tries=10) -> tuple[bool, str]:
+        pass
+
+    @abstractmethod
+    def uri(self) -> str:
+        pass
+
+    @abstractmethod
+    def walletbalance(self) -> int:
+        pass
+
+    @abstractmethod
+    def connect(self, target_uri) -> dict:
+        pass
+
+    @abstractmethod
+    def channel(self, pk, capacity, push_amt, fee_rate) -> dict:
+        pass
+
+    @abstractmethod
+    def graph(self) -> dict:
+        pass
+
+    @abstractmethod
+    def update(self, txid_hex: str, policy: dict, capacity: int) -> dict:
+        pass
+
+
+class CLN(LNNode):
+    def __init__(self, pod_name, ip_address):
+        super().__init__(pod_name, ip_address)
+        self.conn = None
+        self.headers = {}
+        self.impl = "cln"
+        self.reset_connection()
+
+    def reset_connection(self):
+        self.conn = http.client.HTTPSConnection(
+            host=self.name, port=3010, timeout=5, context=INSECURE_CONTEXT
+        )
+
+    def setRune(self, rune):
+        self.headers = {"Rune": rune}
+
+    def get(self, uri):
+        attempt = 0
+        while True:
+            try:
+                self.log.warning(f"headers: {self.headers}")
+                self.conn.request(
+                    method="GET",
+                    url=uri,
+                    headers=self.headers,
+                )
+                return self.conn.getresponse().read().decode("utf8")
+            except Exception as e:
+                self.reset_connection()
+                attempt += 1
+                if attempt > 5:
+                    self.log.error(f"Error CLN GET, Abort: {e}")
+                    return None
+                sleep(1)
+
+    def post(self, uri, data=None):
+        if not data:
+            data = {}
+        body = json.dumps(data)
+        post_header = self.headers
+        post_header["Content-Length"] = str(len(body))
+        post_header["Content-Type"] = "application/json"
+        attempt = 0
+        while True:
+            try:
+                self.conn.request(
+                    method="POST",
+                    url=uri,
+                    body=body,
+                    headers=post_header,
+                )
+                # Stream output, otherwise we get a timeout error
+                res = self.conn.getresponse()
+                stream = ""
+                while True:
+                    try:
+                        data = res.read(1)
+                        if len(data) == 0:
+                            break
+                        else:
+                            stream += data.decode("utf8")
+                    except Exception:
+                        break
+                return stream
+            except Exception as e:
+                self.reset_connection()
+                attempt += 1
+                if attempt > 5:
+                    self.log.error(f"Error CLN POST, Abort: {e}")
+                    return None
+                sleep(1)
+
+    def createrune(self, max_tries=2):
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = requests.get(f"http://{self.ip_address}:8080/rune.json", timeout=5).text
+            if not response:
+                sleep(2)
+                continue
+            self.log.debug(response)
+            res = json.loads(response)
+            self.setRune(res["rune"])
+            return
+        raise Exception(f"Unable to fetch rune from {self.name}")
+
+    def newaddress(self, max_tries=2):
+        self.createrune()
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post("/v1/newaddr")
+            if not response:
+                sleep(2)
+                continue
+            res = json.loads(response)
+            if "bech32" in res:
+                return True, res["bech32"]
+            else:
+                self.log.warning(
+                    f"Couldn't get wallet address from {self.name}:\n  {res}\n  wait and retry..."
+                )
+            sleep(2)
+        return False, ""
+
+    def uri(self):
+        res = json.loads(self.post("/v1/getinfo"))
+        if len(res["address"]) < 1:
+            return None
+        return f"{res['id']}@{res['address'][0]['address']}:{res['address'][0]['port']}"
+
+    def walletbalance(self, max_tries=2) -> int:
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post("/v1/listfunds")
+            if not response:
+                sleep(2)
+                continue
+            res = json.loads(response)
+            return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000)
+        return 0
+
+    def channelbalance(self, max_tries=2) -> int:
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post("/v1/listfunds")
+            if not response:
+                sleep(2)
+                continue
+            res = json.loads(response)
+            return int(sum(o["our_amount_msat"] for o in res["channels"]) / 1000)
+        return 0
+
+    def connect(self, target_uri, max_tries=5) -> dict:
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post("/v1/connect", {"id": target_uri})
+            if response:
+                res = json.loads(response)
+                if "id" in res:
+                    return {}
+                elif "code" in res and res["code"] == 402:
+                    self.log.warning(f"failed connect 402: {response}, wait and retry...")
+                    sleep(5)
+                else:
+                    return res
+            else:
+                self.log.debug(f"connect response: {response}, wait and retry...")
+                sleep(2)
+        return None
+
+    def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict:
+        data = {
+            "amount": capacity,
+            "push_msat": push_amt,
+            "id": pk,
+            "feerate": fee_rate,
+        }
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post("/v1/fundchannel", data)
+            if response:
+                res = json.loads(response)
+                if "txid" in res:
+                    return {"txid": res["txid"], "outpoint": f"{res['txid']}:{res['outnum']}"}
+                else:
+                    self.log.warning(f"unable to open channel: {res}, wait and retry...")
+                    sleep(1)
+            else:
+                self.log.debug(f"channel response: {response}, wait and retry...")
+                sleep(2)
+        return None
+
+    def createinvoice(self, sats, label, description="new invoice") -> str:
+        response = self.post(
+            "invoice", {"amount_msat": sats * 1000, "label": label, "description": description}
+        )
+        if response:
+            res = json.loads(response)
+            return res["bolt11"]
+        return None
+
+    def payinvoice(self, payment_request) -> str:
+        response = self.post("/v1/pay", {"bolt11": payment_request})
+        if response:
+            res = json.loads(response)
+            if "code" in res:
+                return res["message"]
+            else:
+                return res["payment_hash"]
+        return None
+
+    def graph(self, max_tries=2) -> dict:
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post("/v1/listchannels")
+            if response:
+                res = json.loads(response)
+                if "channels" in res:
+                    # Map to desired output
+                    filtered_channels = [ch for ch in res["channels"] if ch["direction"] == 1]
+                    # Sort by short_channel_id - block -> index -> output
+                    sorted_channels = sorted(filtered_channels, key=lambda x: x["short_channel_id"])
+                    # Add capacity by dividing amount_msat by 1000
+                    for channel in sorted_channels:
+                        channel["capacity"] = channel["amount_msat"] // 1000
+                    return {"edges": sorted_channels}
+                else:
+                    self.log.warning(f"unable to open channel: {res}, wait and retry...")
+                    sleep(1)
+            else:
+                self.log.debug(f"channel response: {response}, wait and retry...")
+                sleep(2)
+        return None
+
+    def update(self, txid_hex: str, policy: dict, capacity: int, max_tries=2) -> dict:
+        self.log.warning("Channel Policy Updates not supported by CLN yet!")
+        return None
+
+
+class LND(LNNode):
+    def __init__(self, pod_name, ip_address):
+        super().__init__(pod_name, ip_address)
         self.conn = http.client.HTTPSConnection(
             host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT
         )
+        self.headers = {
+            "Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX,
+            "Connection": "close",
+        }
+        self.impl = "lnd"
+
+    def reset_connection(self):
+        self.conn = http.client.HTTPSConnection(
+            host=self.name, port=8080, timeout=5, context=INSECURE_CONTEXT
+        )
 
     def get(self, uri):
+        attempt = 0
         while True:
             try:
                 self.conn.request(
                     method="GET",
                     url=uri,
-                    headers={"Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, "Connection": "close"},
+                    headers=self.headers,
                 )
                 return self.conn.getresponse().read().decode("utf8")
-            except Exception:
-                time.sleep(1)
+            except Exception as e:
+                self.reset_connection()
+                attempt += 1
+                if attempt > 5:
+                    self.log.error(f"Error LND POST, Abort: {e}")
+                    return None
+                sleep(1)
 
     def post(self, uri, data):
         body = json.dumps(data)
+        post_header = self.headers
+        post_header["Content-Length"] = str(len(body))
+        post_header["Content-Type"] = "application/json"
         attempt = 0
         while True:
-            attempt += 1
             try:
                 self.conn.request(
                     method="POST",
                     url=uri,
                     body=body,
-                    headers={
-                        "Content-Type": "application/json",
-                        "Content-Length": str(len(body)),
-                        "Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX,
-                        "Connection": "close",
-                    },
+                    headers=post_header,
                 )
                 # Stream output, otherwise we get a timeout error
                 res = self.conn.getresponse()
@@ -122,17 +417,37 @@ def post(self, uri, data):
                     except Exception:
                         break
                 return stream
-            except Exception:
-                time.sleep(1)
+            except Exception as e:
+                self.reset_connection()
+                attempt += 1
+                if attempt > 5:
+                    self.log.error(f"Error LND POST, Abort: {e}")
+                    return None
+                sleep(1)
 
-    def newaddress(self):
-        res = self.get("/v1/newaddress")
-        return json.loads(res)
+    def newaddress(self, max_tries=10):
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.get("/v1/newaddress")
+            res = json.loads(response)
+            if "address" in res:
+                return True, res["address"]
+            else:
+                self.log.warning(
+                    f"Couldn't get wallet address from {self.name}:\n  {res}\n  wait and retry..."
+                )
+            sleep(1)
+        return False, ""
 
-    def walletbalance(self):
+    def walletbalance(self) -> int:
         res = self.get("/v1/balance/blockchain")
         return int(json.loads(res)["confirmed_balance"])
 
+    def channelbalance(self) -> int:
+        res = self.get("/v1/balance/channels")
+        return int(json.loads(res)["balance"])
+
     def uri(self):
         res = self.get("/v1/getinfo")
         info = json.loads(res)
@@ -145,17 +460,35 @@ def connect(self, target_uri):
         res = self.post("/v1/peers", data={"addr": {"pubkey": pk, "host": host}})
         return json.loads(res)
 
-    def channel(self, pk, capacity, push_amt, fee_rate):
-        res = self.post(
-            "/v1/channels/stream",
-            data={
-                "local_funding_amount": capacity,
-                "push_sat": push_amt,
-                "node_pubkey": pk,
-                "sat_per_vbyte": fee_rate,
-            },
-        )
-        return json.loads(res)
+    def channel(self, pk, capacity, push_amt, fee_rate, max_tries=2):
+        b64_pk = self.hex_to_b64(pk)
+        attempt = 0
+        while attempt < max_tries:
+            attempt += 1
+            response = self.post(
+                "/v1/channels/stream",
+                data={
+                    "local_funding_amount": capacity,
+                    "push_sat": push_amt,
+                    "node_pubkey": b64_pk,
+                    "sat_per_vbyte": fee_rate,
+                },
+            )
+            try:
+                res = json.loads(response)
+                if "result" in res:
+                    res["txid"] = self.b64_to_hex(
+                        res["result"]["chan_pending"]["txid"], reverse=True
+                    )
+                    res["outpoint"] = (
+                        f"{res['txid']}:{res['result']['chan_pending']['output_index']}"
+                    )
+                    return res
+                self.log.warning(f"Open LND channel error: {res}")
+            except Exception as e:
+                self.log.error(f"Error opening LND channel: {e}")
+            sleep(2)
+        return None
 
     def update(self, txid_hex: str, policy: dict, capacity: int):
         ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity)
@@ -169,6 +502,28 @@ def update(self, txid_hex: str, policy: dict, capacity: int):
         )
         return json.loads(res)
 
+    def createinvoice(self, sats, label, description="new invoice") -> str:
+        b64_desc = base64.b64encode(description.encode("utf-8"))
+        response = self.post(
+            "/v1/invoices", data={"value": sats, "memo": label, "description_hash": b64_desc}
+        )
+        if response:
+            res = json.loads(response)
+            return res["payment_request"]
+        return None
+
+    def payinvoice(self, payment_request) -> str:
+        response = self.post(
+            "/v1/channels/transaction-stream", data={"payment_request": payment_request}
+        )
+        if response:
+            res = json.loads(response)
+            if "payment_error" in res:
+                return res["payment_error"]
+            else:
+                return res["payment_hash"]
+        return None
+
     def graph(self):
         res = self.get("/v1/graph")
         return json.loads(res)
diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py
index 74b0ebdf1..416763ed7 100644
--- a/resources/scenarios/ln_init.py
+++ b/resources/scenarios/ln_init.py
@@ -42,18 +42,12 @@ def gen(n):
         ln_addrs = []
 
         def get_ln_addr(self, ln):
-            while True:
-                res = ln.newaddress()
-                if "address" in res:
-                    addr = res["address"]
-                    ln_addrs.append(addr)
-                    self.log.info(f"Got wallet address {addr} from {ln.name}")
-                    break
-                else:
-                    self.log.info(
-                        f"Couldn't get wallet address from {ln.name}:\n  {res}\n  wait and retry..."
-                    )
-                    sleep(1)
+            success, address = ln.newaddress()
+            if success:
+                ln_addrs.append(address)
+                self.log.info(f"Got wallet address {address} from {ln.name}")
+            else:
+                self.log.info(f"Couldn't get wallet address from {ln.name}")
 
         addr_threads = [
             threading.Thread(target=get_ln_addr, args=(self, ln)) for ln in self.lns.values()
@@ -166,15 +160,18 @@ def connect_ln(self, pair):
                         )
                         sleep(1)
                     else:
-                        self.log.info(
+                        self.log.error(
                             f"Unexpected response attempting to connect {pair[0].name} -> {pair[1].name}:\n  {res}\n  ABORTING"
                         )
-                        break
+                        raise Exception(
+                            f"Unable to connect {pair[0].name} -> {pair[1].name}:\n  {res}"
+                        )
 
         p2p_threads = [
             threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections
         ]
         for thread in p2p_threads:
+            sleep(0.25)
             thread.start()
 
         all(thread.join() is None for thread in p2p_threads)
@@ -188,7 +185,8 @@ def connect_ln(self, pair):
         # so their channel ids are deterministic
         ch_by_block = {}
         for ch in self.channels:
-            # TODO: if "id" not in ch ...
+            if "id" not in ch or "block" not in ch["id"]:
+                raise Exception(f"LN Channel {ch} not found")
             block = ch["id"]["block"]
             if block not in ch_by_block:
                 ch_by_block[block] = [ch]
@@ -214,24 +212,24 @@ def open_channel(self, ch, fee_rate):
                     f"Sending channel open from {ch['source']} -> {ch['target']} with fee_rate={fee_rate}"
                 )
                 res = src.channel(
-                    pk=self.hex_to_b64(tgt_pk),
+                    pk=tgt_pk,
                     capacity=ch["capacity"],
                     push_amt=ch["push_amt"],
                     fee_rate=fee_rate,
                 )
-                if "result" not in res:
+                if res and "txid" in res:
+                    ch["txid"] = res["txid"]
                     self.log.info(
-                        "Unexpected channel open response:\n  "
-                        + f"From {ch['source']} -> {ch['target']} fee_rate={fee_rate}\n  "
-                        + f"{res}"
+                        f"Channel open {ch['source']} -> {ch['target']}\n  "
+                        + f"outpoint={res['outpoint']}\n  "
+                        + f"expected channel id: {ch['id']}"
                     )
                 else:
-                    txid = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True)
-                    ch["txid"] = txid
+                    ch["txid"] = "N/A"
                     self.log.info(
-                        f"Channel open {ch['source']} -> {ch['target']}\n  "
-                        + f"outpoint={txid}:{res['result']['chan_pending']['output_index']}\n  "
-                        + f"expected channel id: {ch['id']}"
+                        "Unexpected channel open response:\n  "
+                        + f"From {ch['source']} -> {ch['target']} fee_rate={fee_rate}\n  "
+                        + f"{res}"
                     )
 
             channels = sorted(ch_by_block[target_block], key=lambda ch: ch["id"]["index"])
@@ -244,6 +242,7 @@ def open_channel(self, ch, fee_rate):
                 assert index == ch["id"]["index"], "Channel ID indexes are not consecutive"
                 assert fee_rate >= 1, "Too many TXs in block, out of fee range"
                 t = threading.Thread(target=open_channel, args=(self, ch, fee_rate))
+                sleep(0.25)
                 t.start()
                 ch_threads.append(t)
 
@@ -260,6 +259,7 @@ def open_channel(self, ch, fee_rate):
             block_txs = block["tx"]
             block_height = block["height"]
             for ch in channels:
+                assert ch["txid"] != "N/A", f"Channel:{ch} did not receive txid"
                 assert ch["id"]["block"] == block_height, f"Actual block:{block_height}\n{ch}"
                 assert block_txs[ch["id"]["index"]] == ch["txid"], (
                     f"Actual txid:{block_txs[ch['id']['index']]}\n{ch}"
@@ -273,14 +273,26 @@ def open_channel(self, ch, fee_rate):
 
         def ln_all_chs(self, ln):
             expected = len(self.channels)
-            while len(ln.graph()["edges"]) != expected:
-                sleep(1)
-            self.log.info(f"LN {ln.name} has graph with all {expected} channels")
+            attempts = 0
+            actual = 0
+            while actual != expected:
+                actual = len(ln.graph()["edges"])
+                if attempts > 10:
+                    break
+                attempts += 1
+                sleep(5)
+            if actual == expected:
+                self.log.info(f"LN {ln.name} has graph with all {expected} channels")
+            else:
+                self.log.error(
+                    f"LN {ln.name} graph is INCOMPLETE - {actual} of {expected} channels"
+                )
 
         ch_ann_threads = [
             threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values()
         ]
         for thread in ch_ann_threads:
+            sleep(0.25)
             thread.start()
 
         all(thread.join() is None for thread in ch_ann_threads)
@@ -311,6 +323,7 @@ def update_policy(self, ln, txid_hex, policy, capacity):
                         ch["capacity"],
                     ),
                 )
+                sleep(0.25)
                 ts.start()
                 update_threads.append(ts)
             if "target_policy" in ch:
@@ -324,6 +337,7 @@ def update_policy(self, ln, txid_hex, policy, capacity):
                         ch["capacity"],
                     ),
                 )
+                sleep(0.25)
                 tt.start()
                 update_threads.append(tt)
         count = len(update_threads)
@@ -337,16 +351,23 @@ def policy_equal(pol1, pol2, capacity):
             return pol1.to_lnd_chanpolicy(capacity) == pol2.to_lnd_chanpolicy(capacity)
 
         def matching_graph(self, expected, ln):
-            while True:
+            done = False
+            while not done:
                 actual = ln.graph()["edges"]
-                assert len(expected) == len(actual)
-                done = True
+                self.log.debug(f"LN {ln.name} channel graph edges: {actual}")
+                if len(actual) > 0:
+                    done = True
+                    assert len(expected) == len(actual), (
+                        f"Expected edges {len(expected)}, actual edges {len(actual)}\n{actual}"
+                    )
                 for i, actual_ch in enumerate(actual):
                     expected_ch = expected[i]
                     capacity = expected_ch["capacity"]
                     # We assert this because it isn't updated as part of policy.
                     # If this fails we have a bigger issue
-                    assert int(actual_ch["capacity"]) == capacity
+                    assert int(actual_ch["capacity"]) == capacity, (
+                        f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch['capacity']}\n expected: {capacity}"
+                    )
 
                     # Policies were not defined in network.yaml
                     if "source_policy" not in expected_ch or "target_policy" not in expected_ch:
@@ -367,12 +388,10 @@ def matching_graph(self, expected, ln):
                     ):
                         continue
                     done = False
-                    break
                 if done:
                     self.log.info(f"LN {ln.name} graph channel policies all match expected source")
-                    break
                 else:
-                    sleep(1)
+                    sleep(5)
 
         expected = sorted(self.channels, key=lambda ch: (ch["id"]["block"], ch["id"]["index"]))
         policy_threads = [
@@ -380,6 +399,7 @@ def matching_graph(self, expected, ln):
             for ln in self.lns.values()
         ]
         for thread in policy_threads:
+            sleep(0.25)
             thread.start()
 
         all(thread.join() is None for thread in policy_threads)
diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py
index dd06c773c..9d0c54f50 100644
--- a/src/warnet/bitcoin.py
+++ b/src/warnet/bitcoin.py
@@ -6,10 +6,9 @@
 from typing import Optional
 
 import click
-from urllib3.exceptions import MaxRetryError
-
 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
diff --git a/src/warnet/constants.py b/src/warnet/constants.py
index 017c9a749..5e79bea8c 100644
--- a/src/warnet/constants.py
+++ b/src/warnet/constants.py
@@ -65,7 +65,6 @@ class AnnexMember(Enum):
 
 # Helm charts
 BITCOIN_CHART_LOCATION = str(CHARTS_DIR.joinpath("bitcoincore"))
-LND_CHART_LOCATION = str(CHARTS_DIR.joinpath("lnd"))
 FORK_OBSERVER_CHART = str(CHARTS_DIR.joinpath("fork-observer"))
 COMMANDER_CHART = str(CHARTS_DIR.joinpath("commander"))
 NAMESPACES_CHART_LOCATION = CHARTS_DIR.joinpath("namespaces")
diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py
index 20e431f29..b8c22d932 100644
--- a/src/warnet/deploy.py
+++ b/src/warnet/deploy.py
@@ -37,7 +37,6 @@
     get_mission,
     get_namespaces_by_type,
     wait_for_ingress_controller,
-    wait_for_pod,
     wait_for_pod_ready,
 )
 from .process import run_command, stream_command
@@ -365,11 +364,22 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str
         network_file = yaml.safe_load(f)
 
     needs_ln_init = False
+    supported_ln_projects = ["lnd", "cln"]
     for node in network_file["nodes"]:
-        if "lnd" in node and "channels" in node["lnd"] and len(node["lnd"]["channels"]) > 0:
-            needs_ln_init = True
+        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))
@@ -388,7 +398,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str
             admin=False,
             namespace=namespace,
         )
-        wait_for_pod(name, namespace=namespace)
+        wait_for_pod_ready(name, namespace=namespace)
         _logs(pod_name=name, follow=True, namespace=namespace)
 
 
diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py
index c12e4de1b..d5da36dc4 100644
--- a/src/warnet/k8s.py
+++ b/src/warnet/k8s.py
@@ -596,3 +596,58 @@ def download(
     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
index a1f7c1eb2..d3691bd4d 100644
--- a/src/warnet/ln.py
+++ b/src/warnet/ln.py
@@ -31,7 +31,10 @@ def _rpc(pod_name: str, method: str, params: str = "", namespace: Optional[str]
     pod = get_pod(pod_name)
     namespace = get_default_namespace_or(namespace)
     chain = pod.metadata.labels["chain"]
-    cmd = f"kubectl -n {namespace} exec {pod_name} -- lncli --network {chain} {method} {' '.join(map(str, params))}"
+    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)
 
 
@@ -46,9 +49,13 @@ def pubkey(
     print(_pubkey(pod))
 
 
-def _pubkey(pod: str):
-    info = _rpc(pod, "getinfo")
-    return json.loads(info)["identity_pubkey"]
+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()
@@ -62,10 +69,14 @@ def host(
     print(_host(pod))
 
 
-def _host(pod):
-    info = _rpc(pod, "getinfo")
-    uris = json.loads(info)["uris"]
-    if uris and len(uris) >= 0:
-        return uris[0].split("@")[1]
+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:
-        return ""
+        uris = json.loads(info)["uris"]
+        if uris and len(uris) >= 0:
+            return uris[0].split("@")[1]
+        else:
+            return ""
diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py
index fdb479dbd..dbbbb767a 100755
--- a/test/ln_basic_test.py
+++ b/test/ln_basic_test.py
@@ -47,8 +47,8 @@ def setup_network(self):
 
     def fund_wallets(self):
         outputs = ""
-        for lnd in self.lns:
-            addr = json.loads(self.warnet(f"ln rpc {lnd} newaddress p2wkh"))["address"]
+        for ln in self.lns:
+            addr = json.loads(self.warnet(f"ln rpc {ln} newaddress p2wkh"))["address"]
             outputs += f',"{addr}":10'
         # trim first comma
         outputs = outputs[1:]