diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9a0bf1356..8e0be61c1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,9 @@ jobs:
profile: minimal
- name: Build on Rust ${{ matrix.toolchain }}
run: cargo build --verbose --color always
- - name: Check formatting
+ - name: Check release build on Rust ${{ matrix.toolchain }}
+ run: cargo check --release --verbose --color always
+ - name: Check formatting on Rust ${{ matrix.toolchain }}
if: matrix.check-fmt
run: rustup component add rustfmt && cargo fmt --all -- --check
- name: Test on Rust ${{ matrix.toolchain }}
diff --git a/.gitignore b/.gitignore
index 088ba6ba7..e1ed12604 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,19 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
+
+# Ignore generated swift related files
+.swiftpm/
+ldk_nodeFFI.*
+ldk_node.swift
+libldk_node.dylib
+LightningDevKitNode.swift
+ldk_node.swiftmodule
+swift.swiftsourceinfo
+swift.abi.json
+swift.swiftdoc
+
+# Ignore ldk_nodeFFI.xcframework files
+/bindings/swift/ldk_nodeFFI.xcframework
+/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs
+/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.kt
diff --git a/Cargo.toml b/Cargo.toml
index 1770c0aab..a0c9d81c4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,22 @@ categories = ["cryptography::cryptocurrencies"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+crate-type = ["staticlib", "cdylib"]
+name = "ldk_node"
+
+[[bin]]
+name = "uniffi-bindgen"
+path = "uniffi-bindgen.rs"
+
+[profile.release-smaller]
+inherits = "release"
+opt-level = 'z' # Optimize for size.
+lto = true # Enable Link Time Optimization
+codegen-units = 1 # Reduce number of codegen units to increase optimizations.
+panic = 'abort' # Abort on panic
+strip = true # Strip symbols from binary*
+
[dependencies]
lightning = { version = "0.0.115", features = ["max_level_trace", "std"] }
lightning-invoice = { version = "0.23" }
@@ -51,6 +67,7 @@ serde_json = { version = "1.0" }
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "sync" ] }
esplora-client = { version = "0.4", default-features = false }
libc = "0.2"
+uniffi = { version = "0.23.0", features = ["build"] }
[dev-dependencies]
electrsd = { version = "0.22.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_23_0"] }
@@ -58,6 +75,10 @@ electrum-client = "0.12.0"
proptest = "1.0.0"
regex = "1.5.6"
+[build-dependencies]
+uniffi = { version = "0.23.0", features = ["build", "cli"] }
+
+
[profile.release]
panic = "abort"
diff --git a/bindings/kotlin/ldk-node-android/.gitattributes b/bindings/kotlin/ldk-node-android/.gitattributes
new file mode 100644
index 000000000..097f9f98d
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/.gitattributes
@@ -0,0 +1,9 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# Linux start script should use lf
+/gradlew text eol=lf
+
+# These are Windows script files and should use crlf
+*.bat text eol=crlf
+
diff --git a/bindings/kotlin/ldk-node-android/.gitignore b/bindings/kotlin/ldk-node-android/.gitignore
new file mode 100644
index 000000000..1b6985c00
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/.gitignore
@@ -0,0 +1,5 @@
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# Ignore Gradle build output directory
+build
diff --git a/bindings/kotlin/ldk-node-android/build.gradle.kts b/bindings/kotlin/ldk-node-android/build.gradle.kts
new file mode 100644
index 000000000..88a431ed6
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/build.gradle.kts
@@ -0,0 +1,17 @@
+buildscript {
+ repositories {
+ google()
+ }
+ dependencies {
+ classpath("com.android.tools.build:gradle:7.1.2")
+ }
+}
+
+plugins {
+ id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
+}
+
+// library version is defined in gradle.properties
+val libraryVersion: String by project
+
+version = libraryVersion
diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties
new file mode 100644
index 000000000..12da413c0
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/gradle.properties
@@ -0,0 +1,5 @@
+org.gradle.jvmargs=-Xmx1536m
+android.useAndroidX=true
+android.enableJetifier=true
+kotlin.code.style=official
+libraryVersion=0.1
diff --git a/bindings/kotlin/ldk-node-android/gradle/wrapper/gradle-wrapper.jar b/bindings/kotlin/ldk-node-android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..943f0cbfa
Binary files /dev/null and b/bindings/kotlin/ldk-node-android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/bindings/kotlin/ldk-node-android/gradle/wrapper/gradle-wrapper.properties b/bindings/kotlin/ldk-node-android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..f398c33c4
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/bindings/kotlin/ldk-node-android/gradlew b/bindings/kotlin/ldk-node-android/gradlew
new file mode 100755
index 000000000..65dcd68d6
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/gradlew
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/bindings/kotlin/ldk-node-android/gradlew.bat b/bindings/kotlin/ldk-node-android/gradlew.bat
new file mode 100644
index 000000000..93e3f59f1
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/bindings/kotlin/ldk-node-android/lib/build.gradle.kts b/bindings/kotlin/ldk-node-android/lib/build.gradle.kts
new file mode 100644
index 000000000..6fe1d3669
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/lib/build.gradle.kts
@@ -0,0 +1,51 @@
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat.*
+import org.gradle.api.tasks.testing.logging.TestLogEvent.*
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android") version "1.6.10"
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+android {
+ compileSdk = 31
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 31
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(file("proguard-android-optimize.txt"), file("proguard-rules.pro"))
+ }
+ }
+
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ withJavadocJar()
+ }
+ }
+}
+
+dependencies {
+ implementation("net.java.dev.jna:jna:5.8.0@aar")
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7")
+ implementation("androidx.appcompat:appcompat:1.4.0")
+ implementation("androidx.core:core-ktx:1.7.0")
+ api("org.slf4j:slf4j-api:1.7.30")
+
+ androidTestImplementation("com.github.tony19:logback-android:2.0.0")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+ androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1")
+ androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit")
+}
diff --git a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt
new file mode 100644
index 000000000..6bef9ded8
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt
@@ -0,0 +1,57 @@
+/*
+ * This Kotlin source file was generated by the Gradle 'init' task.
+ */
+package org.lightningdevkit.ldknode
+
+import kotlin.UInt
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.io.path.createTempDirectory
+import org.junit.runner.RunWith
+import org.lightningdevkit.ldknode.*;
+import android.content.Context.MODE_PRIVATE
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+@RunWith(AndroidJUnit4::class)
+class AndroidLibTest {
+ @Test fun node_start_stop() {
+ val network: Network = "regtest"
+ assertEquals(network, "regtest")
+
+ val tmpDir1 = createTempDirectory("ldk_node").toString()
+ println("Random dir 1: $tmpDir1")
+ val tmpDir2 = createTempDirectory("ldk_node").toString()
+ println("Random dir 2: $tmpDir2")
+
+ val listenAddress1 = "127.0.0.1:2323"
+ val listenAddress2 = "127.0.0.1:2324"
+
+ val config1 = Config(tmpDir1, "http://127.0.0.1:3002", network, listenAddress1, 2048u)
+ val config2 = Config(tmpDir2, "http://127.0.0.1:3002", network, listenAddress2, 2048u)
+
+ val builder1 = Builder.fromConfig(config1)
+ val builder2 = Builder.fromConfig(config2)
+
+ val node1 = builder1.build()
+ val node2 = builder2.build()
+
+ node1.start()
+ node2.start()
+
+ val nodeId1 = node1.nodeId()
+ println("Node Id 1: $nodeId1")
+
+ val nodeId2 = node2.nodeId()
+ println("Node Id 2: $nodeId2")
+
+ val address1 = node1.newFundingAddress()
+ println("Funding address 1: $address1")
+
+ val address2 = node2.newFundingAddress()
+ println("Funding address 2: $address2")
+
+ node1.stop()
+ node2.stop()
+ }
+}
diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/AndroidManifest.xml b/bindings/kotlin/ldk-node-android/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..035ab0e3f
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/Library.kt b/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/Library.kt
new file mode 100644
index 000000000..179dda05c
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/Library.kt
@@ -0,0 +1,10 @@
+/*
+ * This Kotlin source file was generated by the Gradle 'init' task.
+ */
+package org.lightningdevkit.ldknode
+
+class Library {
+ fun someLibraryMethod(): Boolean {
+ return true
+ }
+}
diff --git a/bindings/kotlin/ldk-node-android/settings.gradle.kts b/bindings/kotlin/ldk-node-android/settings.gradle.kts
new file mode 100644
index 000000000..8676d5b11
--- /dev/null
+++ b/bindings/kotlin/ldk-node-android/settings.gradle.kts
@@ -0,0 +1,11 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * The settings file is used to specify which projects to include in your build.
+ *
+ * Detailed information about configuring a multi-project build in Gradle can be found
+ * in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html
+ */
+
+rootProject.name = "ldk-node-android"
+include("lib")
diff --git a/bindings/kotlin/ldk-node-jvm/.gitattributes b/bindings/kotlin/ldk-node-jvm/.gitattributes
new file mode 100644
index 000000000..097f9f98d
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/.gitattributes
@@ -0,0 +1,9 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# Linux start script should use lf
+/gradlew text eol=lf
+
+# These are Windows script files and should use crlf
+*.bat text eol=crlf
+
diff --git a/bindings/kotlin/ldk-node-jvm/.gitignore b/bindings/kotlin/ldk-node-jvm/.gitignore
new file mode 100644
index 000000000..1b6985c00
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/.gitignore
@@ -0,0 +1,5 @@
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# Ignore Gradle build output directory
+build
diff --git a/bindings/kotlin/ldk-node-jvm/gradle/wrapper/gradle-wrapper.jar b/bindings/kotlin/ldk-node-jvm/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..943f0cbfa
Binary files /dev/null and b/bindings/kotlin/ldk-node-jvm/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/bindings/kotlin/ldk-node-jvm/gradle/wrapper/gradle-wrapper.properties b/bindings/kotlin/ldk-node-jvm/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..f398c33c4
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/bindings/kotlin/ldk-node-jvm/gradlew b/bindings/kotlin/ldk-node-jvm/gradlew
new file mode 100755
index 000000000..65dcd68d6
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/gradlew
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/bindings/kotlin/ldk-node-jvm/gradlew.bat b/bindings/kotlin/ldk-node-jvm/gradlew.bat
new file mode 100644
index 000000000..93e3f59f1
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/bindings/kotlin/ldk-node-jvm/lib/build.gradle.kts b/bindings/kotlin/ldk-node-jvm/lib/build.gradle.kts
new file mode 100644
index 000000000..f263307db
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/lib/build.gradle.kts
@@ -0,0 +1,50 @@
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat.*
+import org.gradle.api.tasks.testing.logging.TestLogEvent.*
+
+plugins {
+ // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin.
+ id("org.jetbrains.kotlin.jvm") version "1.7.10"
+
+ // Apply the java-library plugin for API and implementation separation.
+ `java-library`
+}
+
+repositories {
+ // Use Maven Central for resolving dependencies.
+ mavenCentral()
+}
+
+dependencies {
+ // Use the Kotlin JUnit 5 integration.
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
+
+ // Use the JUnit 5 integration.
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.1")
+
+ //// This dependency is exported to consumers, that is to say found on their compile classpath.
+ //api("org.apache.commons:commons-math3:3.6.1")
+
+ //// This dependency is used internally, and not exposed to consumers on their own compile classpath.
+ //implementation("com.google.guava:guava:31.1-jre")
+ // Align versions of all Kotlin components
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
+
+ // Use the Kotlin JDK 8 standard library.
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+
+ implementation("net.java.dev.jna:jna:5.8.0")
+}
+
+tasks.named("test") {
+ // Use JUnit Platform for unit tests.
+ useJUnitPlatform()
+
+ testLogging {
+ events(PASSED, SKIPPED, FAILED, STANDARD_OUT, STANDARD_ERROR)
+ exceptionFormat = FULL
+ showExceptions = true
+ showCauses = true
+ showStackTraces = true
+ showStandardStreams = true
+ }
+}
diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/Library.kt b/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/Library.kt
new file mode 100644
index 000000000..179dda05c
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/Library.kt
@@ -0,0 +1,10 @@
+/*
+ * This Kotlin source file was generated by the Gradle 'init' task.
+ */
+package org.lightningdevkit.ldknode
+
+class Library {
+ fun someLibraryMethod(): Boolean {
+ return true
+ }
+}
diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt
new file mode 100644
index 000000000..ff38c2af0
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt
@@ -0,0 +1,216 @@
+/*
+ * This Kotlin source file was generated by the Gradle 'init' task.
+ */
+package org.lightningdevkit.ldknode
+
+import kotlin.UInt
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.io.path.createTempDirectory
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+
+import org.lightningdevkit.ldknode.*;
+
+fun runCommandAndWait(cmd: String): String {
+ println("Running command \"$cmd\"")
+ val p = Runtime.getRuntime().exec(cmd)
+ p.waitFor()
+ val stdout = p.inputStream.bufferedReader().lineSequence().joinToString("\n")
+ val stderr = p.errorStream.bufferedReader().lineSequence().joinToString("\n")
+ return stdout + stderr
+}
+
+fun mine(blocks: UInt) {
+ val address = runCommandAndWait("bitcoin-cli -regtest getnewaddress")
+ val output = runCommandAndWait("bitcoin-cli -regtest generatetoaddress $blocks $address")
+ println("Mining output: $output")
+}
+
+fun sendToAddress(address: String, amountSats: UInt): String {
+ val amountBtc = amountSats.toDouble() / 100000000.0
+ val output = runCommandAndWait("bitcoin-cli -regtest sendtoaddress $address $amountBtc")
+ return output
+}
+
+fun setup() {
+ runCommandAndWait("bitcoin-cli -regtest createwallet ldk_node_test")
+ runCommandAndWait("bitcoin-cli -regtest loadwallet ldk_node_test true")
+ mine(101u)
+}
+
+fun waitForTx(esploraEndpoint: String, txid: String) {
+ var esploraPickedUpTx = false
+ val re = Regex("\"txid\":\"$txid\"");
+ while (!esploraPickedUpTx) {
+ val client = HttpClient.newBuilder().build()
+ val request = HttpRequest.newBuilder()
+ .uri(URI.create(esploraEndpoint + "/tx/" + txid))
+ .build();
+
+ val response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ esploraPickedUpTx = re.containsMatchIn(response.body());
+ Thread.sleep(1_000)
+ }
+}
+
+class LibraryTest {
+ @Test fun fullCycle() {
+ setup()
+
+ val network: Network = "regtest"
+ assertEquals(network, "regtest")
+
+ val tmpDir1 = createTempDirectory("ldk_node").toString()
+ println("Random dir 1: $tmpDir1")
+ val tmpDir2 = createTempDirectory("ldk_node").toString()
+ println("Random dir 2: $tmpDir2")
+
+ val listenAddress1 = "127.0.0.1:2323"
+ val listenAddress2 = "127.0.0.1:2324"
+
+ val esploraEndpoint = "http://127.0.0.1:3002"
+
+ val config1 = Config(tmpDir1, esploraEndpoint, network, listenAddress1, 2048u)
+ val config2 = Config(tmpDir2, esploraEndpoint, network, listenAddress2, 2048u)
+
+ val builder1 = Builder.fromConfig(config1)
+ val builder2 = Builder.fromConfig(config2)
+
+ val node1 = builder1.build()
+ val node2 = builder2.build()
+
+ node1.start()
+ node2.start()
+
+ val nodeId1 = node1.nodeId()
+ println("Node Id 1: $nodeId1")
+
+ val nodeId2 = node2.nodeId()
+ println("Node Id 2: $nodeId2")
+
+ val address1 = node1.newFundingAddress()
+ println("Funding address 1: $address1")
+
+ val address2 = node2.newFundingAddress()
+ println("Funding address 2: $address2")
+
+ val txid1 = sendToAddress(address1, 100000u)
+ val txid2 = sendToAddress(address2, 100000u)
+ mine(6u)
+
+ waitForTx(esploraEndpoint, txid1)
+ waitForTx(esploraEndpoint, txid2)
+
+ node1.syncWallets()
+ node2.syncWallets()
+
+ val spendableBalance1 = node1.spendableOnchainBalanceSats()
+ val spendableBalance2 = node2.spendableOnchainBalanceSats()
+ val totalBalance1 = node1.totalOnchainBalanceSats()
+ val totalBalance2 = node2.totalOnchainBalanceSats()
+ println("Spendable balance 1: $spendableBalance1")
+ println("Spendable balance 2: $spendableBalance1")
+ println("Total balance 1: $totalBalance1")
+ println("Total balance 2: $totalBalance1")
+ assertEquals(100000u, spendableBalance1)
+ assertEquals(100000u, spendableBalance2)
+ assertEquals(100000u, totalBalance1)
+ assertEquals(100000u, totalBalance2)
+
+ node1.connectOpenChannel(nodeId2, listenAddress2, 50000u, null, true)
+
+ val channelPendingEvent1 = node1.nextEvent()
+ println("Got event: $channelPendingEvent1")
+ assert(channelPendingEvent1 is Event.ChannelPending)
+ node1.eventHandled()
+
+ val channelPendingEvent2 = node2.nextEvent()
+ println("Got event: $channelPendingEvent2")
+ assert(channelPendingEvent2 is Event.ChannelPending)
+ node2.eventHandled()
+
+ val fundingTxid = when (channelPendingEvent1) {
+ is Event.ChannelPending -> channelPendingEvent1.fundingTxo.txid
+ else -> return
+ }
+
+ waitForTx(esploraEndpoint, fundingTxid)
+
+ mine(6u)
+
+ node1.syncWallets()
+ node2.syncWallets()
+
+ val spendableBalance1AfterOpen = node1.spendableOnchainBalanceSats()
+ val spendableBalance2AfterOpen = node2.spendableOnchainBalanceSats()
+ println("Spendable balance 1 after open: $spendableBalance1AfterOpen")
+ println("Spendable balance 2 after open: $spendableBalance2AfterOpen")
+ assert(spendableBalance1AfterOpen > 49000u)
+ assert(spendableBalance1AfterOpen < 50000u)
+ assertEquals(100000u, spendableBalance2AfterOpen)
+
+ val channelReadyEvent1 = node1.nextEvent()
+ println("Got event: $channelReadyEvent1")
+ assert(channelReadyEvent1 is Event.ChannelReady)
+ node1.eventHandled()
+
+ val channelReadyEvent2 = node2.nextEvent()
+ println("Got event: $channelReadyEvent2")
+ assert(channelReadyEvent2 is Event.ChannelReady)
+ node2.eventHandled()
+
+ val channelId = when (channelReadyEvent2) {
+ is Event.ChannelReady -> channelReadyEvent2.channelId
+ else -> return
+ }
+
+ val invoice = node2.receivePayment(1000000u, "asdf", 9217u)
+
+ node1.sendPayment(invoice)
+
+ val paymentSuccessfulEvent = node1.nextEvent()
+ println("Got event: $paymentSuccessfulEvent")
+ assert(paymentSuccessfulEvent is Event.PaymentSuccessful)
+ node1.eventHandled()
+
+ val paymentReceivedEvent = node2.nextEvent()
+ println("Got event: $paymentReceivedEvent")
+ assert(paymentReceivedEvent is Event.PaymentReceived)
+ node2.eventHandled()
+
+ node2.closeChannel(channelId, nodeId1)
+
+ val channelClosedEvent1 = node1.nextEvent()
+ println("Got event: $channelClosedEvent1")
+ assert(channelClosedEvent1 is Event.ChannelClosed)
+ node1.eventHandled()
+
+ val channelClosedEvent2 = node2.nextEvent()
+ println("Got event: $channelClosedEvent2")
+ assert(channelClosedEvent2 is Event.ChannelClosed)
+ node2.eventHandled()
+
+ mine(1u)
+
+ // Sleep a bit to allow for the block to propagate to esplora
+ Thread.sleep(3_000)
+
+ node1.syncWallets()
+ node2.syncWallets()
+
+ val spendableBalance1AfterClose = node1.spendableOnchainBalanceSats()
+ val spendableBalance2AfterClose = node2.spendableOnchainBalanceSats()
+ println("Spendable balance 1 after close: $spendableBalance1AfterClose")
+ println("Spendable balance 2 after close: $spendableBalance2AfterClose")
+ assert(spendableBalance1AfterClose > 95000u)
+ assert(spendableBalance1AfterClose < 100000u)
+ assertEquals(101000u, spendableBalance2AfterClose)
+
+ node1.stop()
+ node2.stop()
+ }
+}
diff --git a/bindings/kotlin/ldk-node-jvm/settings.gradle.kts b/bindings/kotlin/ldk-node-jvm/settings.gradle.kts
new file mode 100644
index 000000000..7a18d4ff7
--- /dev/null
+++ b/bindings/kotlin/ldk-node-jvm/settings.gradle.kts
@@ -0,0 +1,11 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * The settings file is used to specify which projects to include in your build.
+ *
+ * Detailed information about configuring a multi-project build in Gradle can be found
+ * in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html
+ */
+
+rootProject.name = "ldk-node-jvm"
+include("lib")
diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl
new file mode 100644
index 000000000..8cd8d17a3
--- /dev/null
+++ b/bindings/ldk_node.udl
@@ -0,0 +1,157 @@
+namespace ldk_node {
+};
+
+dictionary Config {
+ string storage_dir_path;
+ string esplora_server_url;
+ Network network;
+ SocketAddr? listening_address;
+ u32 default_cltv_expiry_delta;
+};
+
+interface Builder {
+ constructor();
+ [Name=from_config]
+ constructor(Config config);
+ Node build();
+};
+
+interface Node {
+ [Throws=NodeError]
+ void start();
+ [Throws=NodeError]
+ void stop();
+ Event next_event();
+ void event_handled();
+ PublicKey node_id();
+ SocketAddr? listening_address();
+ [Throws=NodeError]
+ Address new_funding_address();
+ [Throws=NodeError]
+ Txid send_to_onchain_address([ByRef]Address address, u64 amount_msat);
+ [Throws=NodeError]
+ Txid send_all_to_onchain_address([ByRef]Address address);
+ [Throws=NodeError]
+ u64 spendable_onchain_balance_sats();
+ [Throws=NodeError]
+ u64 total_onchain_balance_sats();
+ [Throws=NodeError]
+ void connect(PublicKey node_id, SocketAddr address, boolean permanently);
+ [Throws=NodeError]
+ void disconnect([ByRef]PublicKey node_id);
+ [Throws=NodeError]
+ void connect_open_channel(PublicKey node_id, SocketAddr address, u64 channel_amount_sats, u64? push_to_counterparty_msat, boolean announce_channel);
+ [Throws=NodeError]
+ void close_channel([ByRef]ChannelId channel_id, [ByRef]PublicKey counterparty_node_id);
+ [Throws=NodeError]
+ void sync_wallets();
+ [Throws=NodeError]
+ PaymentHash send_payment([ByRef]Invoice invoice);
+ [Throws=NodeError]
+ PaymentHash send_payment_using_amount([ByRef]Invoice invoice, u64 amount_msat);
+ [Throws=NodeError]
+ PaymentHash send_spontaneous_payment(u64 amount_msat, [ByRef]PublicKey node_id);
+ [Throws=NodeError]
+ Invoice receive_payment(u64 amount_msat, [ByRef]string description, u32 expiry_secs);
+ [Throws=NodeError]
+ Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs);
+ PaymentDetails? payment([ByRef]PaymentHash payment_hash);
+ [Throws=NodeError]
+ boolean remove_payment([ByRef]PaymentHash payment_hash);
+};
+
+[Error]
+enum NodeError {
+ "AlreadyRunning",
+ "NotRunning",
+ "OnchainTxCreationFailed",
+ "ConnectionFailed",
+ "InvoiceCreationFailed",
+ "PaymentFailed",
+ "PeerInfoParseFailed",
+ "ChannelCreationFailed",
+ "ChannelClosingFailed",
+ "PersistenceFailed",
+ "WalletOperationFailed",
+ "WalletSigningFailed",
+ "TxSyncFailed",
+ "InvalidAddress",
+ "InvalidPublicKey",
+ "InvalidPaymentHash",
+ "InvalidPaymentPreimage",
+ "InvalidPaymentSecret",
+ "InvalidAmount",
+ "InvalidInvoice",
+ "InvalidChannelId",
+ "InvalidNetwork",
+ "NonUniquePaymentHash",
+ "InsufficientFunds",
+};
+
+[Enum]
+interface Event {
+ PaymentSuccessful( PaymentHash payment_hash );
+ PaymentFailed( PaymentHash payment_hash );
+ PaymentReceived( PaymentHash payment_hash, u64 amount_msat);
+ ChannelPending ( ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo );
+ ChannelReady ( ChannelId channel_id, UserChannelId user_channel_id );
+ ChannelClosed ( ChannelId channel_id, UserChannelId user_channel_id );
+};
+
+enum PaymentDirection {
+ "Inbound",
+ "Outbound",
+};
+
+enum PaymentStatus {
+ "Pending",
+ "Succeeded",
+ "Failed",
+};
+
+dictionary PaymentDetails {
+ PaymentHash hash;
+ PaymentPreimage? preimage;
+ PaymentSecret? secret;
+ u64? amount_msat;
+ PaymentDirection direction;
+ PaymentStatus status;
+};
+
+dictionary OutPoint {
+ Txid txid;
+ u32 vout;
+};
+
+[Custom]
+typedef string Txid;
+
+[Custom]
+typedef string SocketAddr;
+
+[Custom]
+typedef string PublicKey;
+
+[Custom]
+typedef string Address;
+
+[Custom]
+typedef string Invoice;
+
+[Custom]
+typedef string PaymentHash;
+
+[Custom]
+typedef string PaymentPreimage;
+
+[Custom]
+typedef string PaymentSecret;
+
+[Custom]
+typedef string ChannelId;
+
+[Custom]
+typedef string UserChannelId;
+
+[Custom]
+typedef string Network;
diff --git a/bindings/swift/Package.swift b/bindings/swift/Package.swift
new file mode 100644
index 000000000..7ae8aee70
--- /dev/null
+++ b/bindings/swift/Package.swift
@@ -0,0 +1,37 @@
+// swift-tools-version:5.5
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "ldk-node",
+ platforms: [
+ .macOS(.v12),
+ .iOS(.v15)
+ ],
+ products: [
+ // Products define the executables and libraries a package produces, and make them visible to other packages.
+ .library(
+ name: "LightningDevKitNode",
+ targets: ["ldk_nodeFFI", "LightningDevKitNode"]),
+ ],
+ dependencies: [
+ // Dependencies declare other packages that this package depends on.
+ // .package(url: /* package url */, from: "1.0.0"),
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+ // Targets can depend on other targets in this package, and on products in packages this package depends on.
+// .binaryTarget(
+// name: "ldk_nodeFFI",
+// url: "https://github.com/lightningdevkit/ldk-node/releases/download/0.3.0/ldk_nodeFFI.xcframework.zip",
+// checksum: ""),
+ .binaryTarget(name: "ldk_nodeFFI", path: "./ldk_nodeFFI.xcframework"),
+ .target(
+ name: "LightningDevKitNode",
+ dependencies: ["ldk_nodeFFI"]),
+// .testTarget(
+// name: "LightningDevKitNodeTests",
+// dependencies: ["LightningDevKitNode"]),
+ ]
+)
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/Info.plist b/bindings/swift/ldk_nodeFFI.xcframework/Info.plist
new file mode 100644
index 000000000..d0d1ee6fc
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/Info.plist
@@ -0,0 +1,53 @@
+
+
+
+
+ AvailableLibraries
+
+
+ LibraryIdentifier
+ macos-arm64_x86_64
+ LibraryPath
+ ldk_nodeFFI.framework
+ SupportedArchitectures
+
+ arm64
+ x86_64
+
+ SupportedPlatform
+ macos
+
+
+ LibraryIdentifier
+ ios-arm64_x86_64-simulator
+ LibraryPath
+ ldk_nodeFFI.framework
+ SupportedArchitectures
+
+ arm64
+ x86_64
+
+ SupportedPlatform
+ ios
+ SupportedPlatformVariant
+ simulator
+
+
+ LibraryIdentifier
+ ios-arm64
+ LibraryPath
+ ldk_nodeFFI.framework
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+
+
+ CFBundlePackageType
+ XFWK
+ XCFrameworkFormatVersion
+ 1.0
+
+
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h
new file mode 100644
index 000000000..ecc6775bc
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h
@@ -0,0 +1,4 @@
+// This is the "umbrella header" for our combined Rust code library.
+// It needs to import all of the individual headers.
+
+#import "ldk_nodeFFI.h"
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Modules/module.modulemap b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Modules/module.modulemap
new file mode 100644
index 000000000..7a6998828
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Modules/module.modulemap
@@ -0,0 +1,6 @@
+framework module ldk_nodeFFI {
+ umbrella header "ldk_nodeFFI-umbrella.h"
+
+ export *
+ module * { export * }
+}
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h
new file mode 100644
index 000000000..ecc6775bc
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h
@@ -0,0 +1,4 @@
+// This is the "umbrella header" for our combined Rust code library.
+// It needs to import all of the individual headers.
+
+#import "ldk_nodeFFI.h"
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Modules/module.modulemap b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Modules/module.modulemap
new file mode 100644
index 000000000..7a6998828
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Modules/module.modulemap
@@ -0,0 +1,6 @@
+framework module ldk_nodeFFI {
+ umbrella header "ldk_nodeFFI-umbrella.h"
+
+ export *
+ module * { export * }
+}
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h b/bindings/swift/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h
new file mode 100644
index 000000000..ecc6775bc
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Headers/ldk_nodeFFI-umbrella.h
@@ -0,0 +1,4 @@
+// This is the "umbrella header" for our combined Rust code library.
+// It needs to import all of the individual headers.
+
+#import "ldk_nodeFFI.h"
diff --git a/bindings/swift/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Modules/module.modulemap b/bindings/swift/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Modules/module.modulemap
new file mode 100644
index 000000000..7a6998828
--- /dev/null
+++ b/bindings/swift/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Modules/module.modulemap
@@ -0,0 +1,6 @@
+framework module ldk_nodeFFI {
+ umbrella header "ldk_nodeFFI-umbrella.h"
+
+ export *
+ module * { export * }
+}
diff --git a/build.rs b/build.rs
new file mode 100644
index 000000000..f5dd351c1
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ uniffi::generate_scaffolding("bindings/ldk_node.udl").unwrap();
+}
diff --git a/scripts/uniffi_bindgen_generate.sh b/scripts/uniffi_bindgen_generate.sh
new file mode 100755
index 000000000..0658cabdb
--- /dev/null
+++ b/scripts/uniffi_bindgen_generate.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+source ./scripts/uniffi_bindgen_generate_kotlin.sh || exit 1
+source ./scripts/uniffi_bindgen_generate_python.sh || exit 1
+source ./scripts/uniffi_bindgen_generate_swift.sh || exit 1
+
diff --git a/scripts/uniffi_bindgen_generate_kotlin.sh b/scripts/uniffi_bindgen_generate_kotlin.sh
new file mode 100755
index 000000000..84d5e3534
--- /dev/null
+++ b/scripts/uniffi_bindgen_generate_kotlin.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+BINDINGS_DIR="bindings/kotlin"
+TARGET_DIR="target/bindings/kotlin"
+PROJECT_DIR="ldk-node-jvm"
+PACKAGE_DIR="org/lightningdevkit/ldknode"
+UNIFFI_BINDGEN_BIN="cargo run --features=uniffi/cli --bin uniffi-bindgen"
+
+#rustup target add aarch64-apple-darwin
+#cargo build --target aarch64-apple-darwin || exit 1
+cargo build --release || exit 1
+$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language kotlin -o "$TARGET_DIR" || exit 1
+
+mkdir -p "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/kotlin/"$PACKAGE_DIR" || exit 1
+mkdir -p "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/resources/darwin-aarch64/ || exit 1
+
+cp "$TARGET_DIR"/"$PACKAGE_DIR"/ldk_node.kt "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/kotlin/"$PACKAGE_DIR"/ || exit 1
+#cp ./target/aarch64-apple-darwin/debug/libldk_node.dylib "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/resources/darwin-aarch64/libldk_node.dylib || exit 1
+cp target/release/libldk_node.dylib "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/resources/libldk_node.dylib || exit 1
diff --git a/scripts/uniffi_bindgen_generate_kotlin_android.sh b/scripts/uniffi_bindgen_generate_kotlin_android.sh
new file mode 100755
index 000000000..71734b94e
--- /dev/null
+++ b/scripts/uniffi_bindgen_generate_kotlin_android.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+BINDINGS_DIR="bindings/kotlin"
+TARGET_DIR="target"
+PROJECT_DIR="ldk-node-android"
+PACKAGE_DIR="org/lightningdevkit/ldknode"
+UNIFFI_BINDGEN_BIN="cargo +nightly run --features=uniffi/cli --bin uniffi-bindgen"
+ANDROID_NDK_ROOT="/opt/homebrew/share/android-ndk"
+LLVM_ARCH_PATH="darwin-x86_64"
+PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$LLVM_ARCH_PATH/bin:$PATH"
+
+rustup +nightly target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi
+#cargo build --release || exit 1
+CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo +nightly build --profile release-smaller --target x86_64-linux-android || exit 1
+CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo +nightly build --profile release-smaller --target armv7-linux-androideabi || exit 1
+CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo +nightly build --profile release-smaller --target aarch64-linux-android || exit 1
+$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language kotlin -o "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/kotlin || exit 1
+
+JNI_LIB_DIR="$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/jniLibs/
+mkdir -p $JNI_LIB_DIR/x86_64 || exit 1
+mkdir -p $JNI_LIB_DIR/armeabi-v7a || exit 1
+mkdir -p $JNI_LIB_DIR/arm64-v8a || exit 1
+cp $TARGET_DIR/x86_64-linux-android/release-smaller/libldk_node.so $JNI_LIB_DIR/x86_64/ || exit 1
+cp $TARGET_DIR/armv7-linux-androideabi/release-smaller/libldk_node.so $JNI_LIB_DIR/armeabi-v7a/ || exit 1
+cp $TARGET_DIR/aarch64-linux-android/release-smaller/libldk_node.so $JNI_LIB_DIR/arm64-v8a/ || exit 1
diff --git a/scripts/uniffi_bindgen_generate_python.sh b/scripts/uniffi_bindgen_generate_python.sh
new file mode 100755
index 000000000..8dd937fba
--- /dev/null
+++ b/scripts/uniffi_bindgen_generate_python.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+BINDINGS_DIR="./bindings/python"
+UNIFFI_BINDGEN_BIN="cargo +nightly run --features=uniffi/cli --bin uniffi-bindgen"
+
+cargo +nightly build --release || exit 1
+$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language python -o "$BINDINGS_DIR" || exit 1
+cp ./target/release/libldk_node.dylib "$BINDINGS_DIR"/libldk_node.dylib || exit 1
diff --git a/scripts/uniffi_bindgen_generate_swift.sh b/scripts/uniffi_bindgen_generate_swift.sh
new file mode 100755
index 000000000..d821581e2
--- /dev/null
+++ b/scripts/uniffi_bindgen_generate_swift.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+BINDINGS_DIR="./bindings/swift"
+UNIFFI_BINDGEN_BIN="cargo run --features=uniffi/cli --bin uniffi-bindgen"
+
+cargo build --release || exit 1
+$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language swift -o "$BINDINGS_DIR" || exit 1
+
+mkdir -p $BINDINGS_DIR
+
+# Install rust target toolchains
+rustup install nightly-x86_64-apple-darwin
+rustup component add rust-src --toolchain nightly-x86_64-apple-darwin
+rustup target add aarch64-apple-ios x86_64-apple-ios
+rustup target add aarch64-apple-ios-sim --toolchain nightly
+rustup target add aarch64-apple-darwin x86_64-apple-darwin
+
+# Build rust target libs
+cargo build --profile release-smaller || exit 1
+cargo build --profile release-smaller --target x86_64-apple-darwin || exit 1
+cargo build --profile release-smaller --target aarch64-apple-darwin || exit 1
+cargo build --profile release-smaller --target x86_64-apple-ios || exit 1
+cargo build --profile release-smaller --target aarch64-apple-ios || exit 1
+cargo +nightly build --release --target aarch64-apple-ios-sim || exit 1
+
+# Combine ios-sim and apple-darwin (macos) libs for x86_64 and aarch64 (m1)
+mkdir -p target/lipo-ios-sim/release-smaller || exit 1
+lipo target/aarch64-apple-ios-sim/release/libldk_node.a target/x86_64-apple-ios/release-smaller/libldk_node.a -create -output target/lipo-ios-sim/release-smaller/libldk_node.a || exit 1
+mkdir -p target/lipo-macos/release-smaller || exit 1
+lipo target/aarch64-apple-darwin/release-smaller/libldk_node.a target/x86_64-apple-darwin/release-smaller/libldk_node.a -create -output target/lipo-macos/release-smaller/libldk_node.a || exit 1
+
+$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language swift -o "$BINDINGS_DIR" || exit 1
+
+swiftc -module-name ldk_node -emit-library -o "$BINDINGS_DIR"/libldk_node.dylib -emit-module -emit-module-path "$BINDINGS_DIR" -parse-as-library -L ./target/release-smaller -lldk_node -Xcc -fmodule-map-file="$BINDINGS_DIR"/ldk_nodeFFI.modulemap "$BINDINGS_DIR"/ldk_node.swift -v || exit 1
+
+# Create xcframework from bindings Swift file and libs
+mkdir -p "$BINDINGS_DIR"/Sources/LightningDevKitNode || exit 1
+mv "$BINDINGS_DIR"/ldk_node.swift "$BINDINGS_DIR"/Sources/LightningDevKitNode/LightningDevKitNode.swift || exit 1
+cp "$BINDINGS_DIR"/ldk_nodeFFI.h "$BINDINGS_DIR"/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/Headers || exit 1
+cp "$BINDINGS_DIR"/ldk_nodeFFI.h "$BINDINGS_DIR"/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/Headers || exit 1
+cp "$BINDINGS_DIR"/ldk_nodeFFI.h "$BINDINGS_DIR"/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/Headers || exit 1
+cp target/aarch64-apple-ios/release-smaller/libldk_node.a "$BINDINGS_DIR"/ldk_nodeFFI.xcframework/ios-arm64/ldk_nodeFFI.framework/ldk_nodeFFI || exit 1
+cp target/lipo-ios-sim/release-smaller/libldk_node.a "$BINDINGS_DIR"/ldk_nodeFFI.xcframework/ios-arm64_x86_64-simulator/ldk_nodeFFI.framework/ldk_nodeFFI || exit 1
+cp target/lipo-macos/release-smaller/libldk_node.a "$BINDINGS_DIR"/ldk_nodeFFI.xcframework/macos-arm64_x86_64/ldk_nodeFFI.framework/ldk_nodeFFI || exit 1
+# rm "$BINDINGS_DIR"/ldk_nodeFFI.h || exit 1
+# rm "$BINDINGS_DIR"/ldk_nodeFFI.modulemap || exit 1
+echo finished successfully!
diff --git a/src/error.rs b/src/error.rs
index eb0028006..9a9a9c512 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -11,16 +11,8 @@ pub enum Error {
OnchainTxCreationFailed,
/// A network connection has been closed.
ConnectionFailed,
- /// Payment of the given invoice has already been intiated.
- NonUniquePaymentHash,
- /// The given amount is invalid.
- InvalidAmount,
- /// The given invoice is invalid.
- InvalidInvoice,
/// Invoice creation failed.
InvoiceCreationFailed,
- /// There are insufficient funds to complete the given operation.
- InsufficientFunds,
/// An attempted payment has failed.
PaymentFailed,
/// A given peer info could not be parsed.
@@ -37,6 +29,28 @@ pub enum Error {
WalletSigningFailed,
/// A transaction sync operation failed.
TxSyncFailed,
+ /// The given address is invalid.
+ InvalidAddress,
+ /// The given public key is invalid.
+ InvalidPublicKey,
+ /// The given payment hash is invalid.
+ InvalidPaymentHash,
+ /// The given payment preimage is invalid.
+ InvalidPaymentPreimage,
+ /// The given payment secret is invalid.
+ InvalidPaymentSecret,
+ /// The given amount is invalid.
+ InvalidAmount,
+ /// The given invoice is invalid.
+ InvalidInvoice,
+ /// The given channel ID is invalid.
+ InvalidChannelId,
+ /// The given network is invalid.
+ InvalidNetwork,
+ /// Payment of the given invoice has already been intiated.
+ NonUniquePaymentHash,
+ /// There are insufficient funds to complete the given operation.
+ InsufficientFunds,
}
impl fmt::Display for Error {
@@ -48,13 +62,7 @@ impl fmt::Display for Error {
write!(f, "On-chain transaction could not be created.")
}
Self::ConnectionFailed => write!(f, "Network connection closed."),
- Self::NonUniquePaymentHash => write!(f, "An invoice must not get payed twice."),
- Self::InvalidAmount => write!(f, "The given amount is invalid."),
- Self::InvalidInvoice => write!(f, "The given invoice is invalid."),
Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."),
- Self::InsufficientFunds => {
- write!(f, "There are insufficient funds to complete the given operation.")
- }
Self::PaymentFailed => write!(f, "Failed to send the given payment."),
Self::PeerInfoParseFailed => write!(f, "Failed to parse the given peer information."),
Self::ChannelCreationFailed => write!(f, "Failed to create channel."),
@@ -63,6 +71,19 @@ impl fmt::Display for Error {
Self::WalletOperationFailed => write!(f, "Failed to conduct wallet operation."),
Self::WalletSigningFailed => write!(f, "Failed to sign given transaction."),
Self::TxSyncFailed => write!(f, "Failed to sync transactions."),
+ Self::InvalidAddress => write!(f, "The given address is invalid."),
+ Self::InvalidPublicKey => write!(f, "The given public key is invalid."),
+ Self::InvalidPaymentHash => write!(f, "The given payment hash is invalid."),
+ Self::InvalidPaymentPreimage => write!(f, "The given payment preimage is invalid."),
+ Self::InvalidPaymentSecret => write!(f, "The given payment secret is invalid."),
+ Self::InvalidAmount => write!(f, "The given amount is invalid."),
+ Self::InvalidInvoice => write!(f, "The given invoice is invalid."),
+ Self::InvalidChannelId => write!(f, "The given channel ID is invalid."),
+ Self::InvalidNetwork => write!(f, "The given network is invalid."),
+ Self::NonUniquePaymentHash => write!(f, "An invoice must not get payed twice."),
+ Self::InsufficientFunds => {
+ write!(f, "There are insufficient funds to complete the given operation.")
+ }
}
}
}
diff --git a/src/event.rs b/src/event.rs
index 24c0e2af6..6197dc111 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -1,4 +1,7 @@
-use crate::{hex_utils, ChannelManager, Config, Error, KeysManager, NetworkGraph, Wallet};
+use crate::{
+ hex_utils, ChannelId, ChannelManager, Config, Error, KeysManager, NetworkGraph, UserChannelId,
+ Wallet,
+};
use crate::payment_store::{
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentStatus, PaymentStore,
@@ -51,11 +54,11 @@ pub enum Event {
/// A channel has been created and is pending confirmation on-chain.
ChannelPending {
/// The `channel_id` of the channel.
- channel_id: [u8; 32],
+ channel_id: ChannelId,
/// The `user_channel_id` of the channel.
- user_channel_id: u128,
+ user_channel_id: UserChannelId,
/// The `temporary_channel_id` this channel used to be known by during channel establishment.
- former_temporary_channel_id: [u8; 32],
+ former_temporary_channel_id: ChannelId,
/// The `node_id` of the channel counterparty.
counterparty_node_id: PublicKey,
/// The outpoint of the channel's funding transaction.
@@ -64,16 +67,16 @@ pub enum Event {
/// A channel is ready to be used.
ChannelReady {
/// The `channel_id` of the channel.
- channel_id: [u8; 32],
+ channel_id: ChannelId,
/// The `user_channel_id` of the channel.
- user_channel_id: u128,
+ user_channel_id: UserChannelId,
},
/// A channel has been closed.
ChannelClosed {
/// The `channel_id` of the channel.
- channel_id: [u8; 32],
+ channel_id: ChannelId,
/// The `user_channel_id` of the channel.
- user_channel_id: u128,
+ user_channel_id: UserChannelId,
},
}
@@ -641,9 +644,11 @@ where
);
self.event_queue
.add_event(Event::ChannelPending {
- channel_id,
- user_channel_id,
- former_temporary_channel_id: former_temporary_channel_id.unwrap(),
+ channel_id: ChannelId(channel_id),
+ user_channel_id: UserChannelId(user_channel_id),
+ former_temporary_channel_id: ChannelId(
+ former_temporary_channel_id.unwrap(),
+ ),
counterparty_node_id,
funding_txo,
})
@@ -659,7 +664,10 @@ where
counterparty_node_id,
);
self.event_queue
- .add_event(Event::ChannelReady { channel_id, user_channel_id })
+ .add_event(Event::ChannelReady {
+ channel_id: ChannelId(channel_id),
+ user_channel_id: UserChannelId(user_channel_id),
+ })
.expect("Failed to push to event queue");
}
LdkEvent::ChannelClosed { channel_id, reason, user_channel_id } => {
@@ -670,7 +678,10 @@ where
reason
);
self.event_queue
- .add_event(Event::ChannelClosed { channel_id, user_channel_id })
+ .add_event(Event::ChannelClosed {
+ channel_id: ChannelId(channel_id),
+ user_channel_id: UserChannelId(user_channel_id),
+ })
.expect("Failed to push to event queue");
}
LdkEvent::DiscardFunding { .. } => {}
@@ -690,7 +701,10 @@ mod tests {
let logger = Arc::new(TestLogger::new());
let event_queue = EventQueue::new(Arc::clone(&store), Arc::clone(&logger));
- let expected_event = Event::ChannelReady { channel_id: [23u8; 32], user_channel_id: 2323 };
+ let expected_event = Event::ChannelReady {
+ channel_id: ChannelId([23u8; 32]),
+ user_channel_id: UserChannelId(2323),
+ };
event_queue.add_event(expected_event.clone()).unwrap();
assert!(store.get_and_clear_did_persist());
diff --git a/src/lib.rs b/src/lib.rs
index 94a98f127..0d6213512 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -62,7 +62,12 @@
//! [`connect_open_channel`]: Node::connect_open_channel
//! [`send_payment`]: Node::send_payment
//!
-#![deny(missing_docs)]
+
+// We currently disable the missing_docs lint due to incompatibility with the generated Uniffi
+// scaffolding.
+// TODO: Re-enable after https://github.com/mozilla/uniffi-rs/issues/1502 has been
+// addressed.
+//#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_intra_doc_links)]
#![allow(bare_trait_objects)]
@@ -86,7 +91,9 @@ pub use bitcoin;
pub use lightning;
pub use lightning_invoice;
-pub use error::Error;
+pub use error::Error as NodeError;
+use error::Error;
+
pub use event::Event;
use event::{EventHandler, EventQueue};
use io::fs_store::FilesystemStore;
@@ -98,6 +105,7 @@ use types::{
ChainMonitor, ChannelManager, GossipSync, KeysManager, NetworkGraph, OnionMessenger,
PeerManager, Scorer,
};
+pub use types::{ChannelId, UserChannelId};
use wallet::Wallet;
use logger::{log_error, log_info, FilesystemLogger, Logger};
@@ -109,7 +117,7 @@ use lightning::ln::channelmanager::{
Retry,
};
use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler};
-use lightning::ln::{PaymentHash, PaymentPreimage};
+use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
use lightning::routing::gossip::P2PGossipSync;
use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringParameters};
use lightning::routing::utxo::UtxoLookup;
@@ -133,7 +141,9 @@ use bdk::template::Bip84;
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::PublicKey;
-use bitcoin::{BlockHash, Txid};
+use bitcoin::Network;
+
+use bitcoin::{Address, BlockHash, OutPoint, Txid};
use rand::Rng;
@@ -141,10 +151,13 @@ use std::convert::TryInto;
use std::default::Default;
use std::fs;
use std::net::SocketAddr;
+use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant, SystemTime};
+uniffi::include_scaffolding!("ldk_node");
+
// The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold
// number of blocks after which BDK stops looking for scripts belonging to the wallet.
const BDK_CLIENT_STOP_GAP: usize = 20;
@@ -169,7 +182,7 @@ pub struct Config {
/// The URL of the utilized Esplora server.
pub esplora_server_url: String,
/// The used Bitcoin network.
- pub network: bitcoin::Network,
+ pub network: Network,
/// The IP address and TCP port the node will listen on.
pub listening_address: Option,
/// The default CLTV expiry delta to be used for payments.
@@ -181,7 +194,7 @@ impl Default for Config {
Self {
storage_dir_path: "/tmp/ldk_node/".to_string(),
esplora_server_url: "http://localhost:3002".to_string(),
- network: bitcoin::Network::Regtest,
+ network: Network::Regtest,
listening_address: Some("0.0.0.0:9735".parse().unwrap()),
default_cltv_expiry_delta: 144,
}
@@ -262,16 +275,9 @@ impl Builder {
///
/// Options: `mainnet`/`bitcoin`, `testnet`, `regtest`, `signet`
///
- /// Default: `testnet`
+ /// Default: `regtest`
pub fn set_network(&mut self, network: &str) -> &mut Self {
- self.config.network = match network {
- "mainnet" => bitcoin::Network::Bitcoin,
- "bitcoin" => bitcoin::Network::Bitcoin,
- "testnet" => bitcoin::Network::Testnet,
- "regtest" => bitcoin::Network::Regtest,
- "signet" => bitcoin::Network::Signet,
- _ => bitcoin::Network::Regtest,
- };
+ self.config.network = Network::from_str(network).unwrap_or(Network::Regtest);
self
}
@@ -283,8 +289,8 @@ impl Builder {
self
}
- /// Builds an [`Node`] instance according to the options previously configured.
- pub fn build(&self) -> Node {
+ /// Builds a [`Node`] instance according to the options previously configured.
+ pub fn build(&self) -> Arc {
let config = Arc::new(self.config.clone());
let ldk_data_dir = format!("{}/ldk", config.storage_dir_path);
@@ -461,9 +467,7 @@ impl Builder {
} else {
// We're starting a fresh node.
let genesis_block_hash =
- bitcoin::blockdata::constants::genesis_block(config.network)
- .header
- .block_hash();
+ bitcoin::blockdata::constants::genesis_block(config.network).block_hash();
let chain_params = ChainParameters {
network: config.network,
@@ -564,7 +568,7 @@ impl Builder {
let stop_running = Arc::new(AtomicBool::new(false));
- Node {
+ Arc::new(Node {
runtime,
stop_running,
config,
@@ -582,7 +586,7 @@ impl Builder {
scorer,
peer_store,
payment_store,
- }
+ })
}
}
@@ -844,12 +848,12 @@ impl Node {
}
/// Returns our own listening address.
- pub fn listening_address(&self) -> Option<&SocketAddr> {
- self.config.listening_address.as_ref()
+ pub fn listening_address(&self) -> Option {
+ self.config.listening_address
}
/// Retrieve a new on-chain/funding address.
- pub fn new_funding_address(&self) -> Result {
+ pub fn new_funding_address(&self) -> Result {
let funding_address = self.wallet.get_new_address()?;
log_info!(self.logger, "Generated new funding address: {}", funding_address);
Ok(funding_address)
@@ -887,6 +891,16 @@ impl Node {
self.wallet.send_to_address(address, None)
}
+ /// Retrieve the currently spendable on-chain balance in satoshis.
+ pub fn spendable_onchain_balance_sats(&self) -> Result {
+ Ok(self.wallet.get_balance().map(|bal| bal.get_spendable())?)
+ }
+
+ /// Retrieve the current total on-chain balance in satoshis.
+ pub fn total_onchain_balance_sats(&self) -> Result {
+ Ok(self.wallet.get_balance().map(|bal| bal.get_total())?)
+ }
+
/// Retrieve a list of known channels.
pub fn list_channels(&self) -> Vec {
self.channel_manager.list_channels()
@@ -1112,10 +1126,10 @@ impl Node {
/// Close a previously opened channel.
pub fn close_channel(
- &self, channel_id: &[u8; 32], counterparty_node_id: &PublicKey,
+ &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
) -> Result<(), Error> {
self.peer_store.remove_peer(counterparty_node_id)?;
- match self.channel_manager.close_channel(channel_id, counterparty_node_id) {
+ match self.channel_manager.close_channel(&channel_id.0, counterparty_node_id) {
Ok(_) => Ok(()),
Err(_) => Err(Error::ChannelClosingFailed),
}
@@ -1355,12 +1369,7 @@ impl Node {
fn receive_payment_inner(
&self, amount_msat: Option, description: &str, expiry_secs: u32,
) -> Result {
- let currency = match self.config.network {
- bitcoin::Network::Bitcoin => Currency::Bitcoin,
- bitcoin::Network::Testnet => Currency::BitcoinTestnet,
- bitcoin::Network::Regtest => Currency::Regtest,
- bitcoin::Network::Signet => Currency::Signet,
- };
+ let currency = Currency::from(self.config.network);
let keys_manager = Arc::clone(&self.keys_manager);
let invoice = match lightning_invoice::utils::create_invoice_from_channelmanager(
&self.channel_manager,
diff --git a/src/test/functional_tests.rs b/src/test/functional_tests.rs
index c4b1d1662..088d8bfc1 100644
--- a/src/test/functional_tests.rs
+++ b/src/test/functional_tests.rs
@@ -39,7 +39,7 @@ fn channel_full_cycle() {
node_a
.connect_open_channel(
node_b.node_id(),
- *node_b.listening_address().unwrap(),
+ node_b.listening_address().unwrap(),
funding_amount_sat,
Some(push_msat),
true,
@@ -76,8 +76,9 @@ fn channel_full_cycle() {
expect_event!(node_a, ChannelReady);
- let channel_id = match node_b.next_event() {
- ref e @ Event::ChannelReady { channel_id, .. } => {
+ let ev = node_b.next_event();
+ let channel_id = match ev {
+ ref e @ Event::ChannelReady { ref channel_id, .. } => {
println!("{} got event {:?}", std::stringify!(node_b), e);
node_b.event_handled();
channel_id
@@ -240,7 +241,7 @@ fn channel_open_fails_when_funds_insufficient() {
Err(Error::InsufficientFunds),
node_a.connect_open_channel(
node_b.node_id(),
- *node_b.listening_address().unwrap(),
+ node_b.listening_address().unwrap(),
120000,
None,
true
diff --git a/src/types.rs b/src/types.rs
index 457dab04e..8aa77dbbd 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,18 +1,32 @@
+use crate::error::Error;
+use crate::hex_utils;
use crate::io::fs_store::FilesystemStore;
use crate::logger::FilesystemLogger;
use crate::wallet::{Wallet, WalletKeysManager};
+use crate::UniffiCustomTypeConverter;
use lightning::chain::chainmonitor;
use lightning::chain::keysinterface::InMemorySigner;
use lightning::ln::peer_handler::IgnoringMessageHandler;
+use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
use lightning::routing::gossip;
use lightning::routing::gossip::P2PGossipSync;
use lightning::routing::router::DefaultRouter;
use lightning::routing::scoring::ProbabilisticScorer;
use lightning::routing::utxo::UtxoLookup;
+use lightning::util::ser::{Readable, Writeable, Writer};
+use lightning_invoice::{Invoice, SignedRawInvoice};
use lightning_net_tokio::SocketDescriptor;
use lightning_transaction_sync::EsploraSyncClient;
+use bitcoin::hashes::sha256::Hash as Sha256;
+use bitcoin::hashes::Hash;
+use bitcoin::secp256k1::PublicKey;
+use bitcoin::{Address, Network, Txid};
+
+use std::convert::TryInto;
+use std::net::SocketAddr;
+use std::str::FromStr;
use std::sync::{Arc, Mutex};
pub(crate) type ChainMonitor = chainmonitor::ChainMonitor<
@@ -62,3 +76,217 @@ pub(crate) type OnionMessenger = lightning::onion_message::OnionMessenger<
Arc,
IgnoringMessageHandler,
>;
+
+impl UniffiCustomTypeConverter for SocketAddr {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Ok(addr) = SocketAddr::from_str(&val) {
+ return Ok(addr);
+ }
+
+ Err(Error::InvalidPublicKey.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.to_string()
+ }
+}
+
+impl UniffiCustomTypeConverter for PublicKey {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Ok(key) = PublicKey::from_str(&val) {
+ return Ok(key);
+ }
+
+ Err(Error::InvalidPublicKey.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.to_string()
+ }
+}
+
+impl UniffiCustomTypeConverter for Address {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Ok(addr) = Address::from_str(&val) {
+ return Ok(addr);
+ }
+
+ Err(Error::InvalidAddress.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.to_string()
+ }
+}
+
+impl UniffiCustomTypeConverter for Invoice {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Ok(signed) = val.parse::() {
+ if let Ok(invoice) = Invoice::from_signed(signed) {
+ return Ok(invoice);
+ }
+ }
+
+ Err(Error::InvalidInvoice.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.to_string()
+ }
+}
+
+impl UniffiCustomTypeConverter for PaymentHash {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Ok(hash) = Sha256::from_str(&val) {
+ Ok(PaymentHash(hash.into_inner()))
+ } else {
+ Err(Error::InvalidPaymentHash.into())
+ }
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ Sha256::from_slice(&obj.0).unwrap().to_string()
+ }
+}
+
+impl UniffiCustomTypeConverter for PaymentPreimage {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Some(bytes_vec) = hex_utils::to_vec(&val) {
+ let bytes_res = bytes_vec.try_into();
+ if let Ok(bytes) = bytes_res {
+ return Ok(PaymentPreimage(bytes));
+ }
+ }
+ Err(Error::InvalidPaymentPreimage.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ hex_utils::to_string(&obj.0)
+ }
+}
+
+impl UniffiCustomTypeConverter for PaymentSecret {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Some(bytes_vec) = hex_utils::to_vec(&val) {
+ let bytes_res = bytes_vec.try_into();
+ if let Ok(bytes) = bytes_res {
+ return Ok(PaymentSecret(bytes));
+ }
+ }
+ Err(Error::InvalidPaymentSecret.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ hex_utils::to_string(&obj.0)
+ }
+}
+
+/// The global identifier of a channel.
+///
+/// Note that this will start out to be a temporary ID until channel funding negotiation is
+/// finalized, at which point it will change to be a permanent global ID tied to the on-chain
+/// funding transaction.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct ChannelId(pub [u8; 32]);
+
+impl UniffiCustomTypeConverter for ChannelId {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ if let Some(hex_vec) = hex_utils::to_vec(&val) {
+ if hex_vec.len() == 32 {
+ let mut channel_id = [0u8; 32];
+ channel_id.copy_from_slice(&hex_vec[..]);
+ return Ok(Self(channel_id));
+ }
+ }
+ Err(Error::InvalidChannelId.into())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ hex_utils::to_string(&obj.0)
+ }
+}
+
+impl Writeable for ChannelId {
+ fn write(&self, writer: &mut W) -> Result<(), lightning::io::Error> {
+ Ok(self.0.write(writer)?)
+ }
+}
+
+impl Readable for ChannelId {
+ fn read(
+ reader: &mut R,
+ ) -> Result {
+ Ok(Self(Readable::read(reader)?))
+ }
+}
+
+/// A local, potentially user-provided, identifier of a channel.
+///
+/// By default, this will be randomly generated for the user to ensure local uniqueness.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct UserChannelId(pub u128);
+
+impl UniffiCustomTypeConverter for UserChannelId {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ Ok(UserChannelId(u128::from_str(&val).map_err(|_| Error::InvalidChannelId)?))
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.0.to_string()
+ }
+}
+
+impl Writeable for UserChannelId {
+ fn write(&self, writer: &mut W) -> Result<(), lightning::io::Error> {
+ Ok(self.0.write(writer)?)
+ }
+}
+
+impl Readable for UserChannelId {
+ fn read(
+ reader: &mut R,
+ ) -> Result {
+ Ok(Self(Readable::read(reader)?))
+ }
+}
+
+impl UniffiCustomTypeConverter for Network {
+ type Builtin = String;
+
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ Ok(Network::from_str(&val).map_err(|_| Error::InvalidNetwork)?)
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.to_string()
+ }
+}
+
+impl UniffiCustomTypeConverter for Txid {
+ type Builtin = String;
+ fn into_custom(val: Self::Builtin) -> uniffi::Result {
+ Ok(Txid::from_str(&val).unwrap())
+ }
+
+ fn from_custom(obj: Self) -> Self::Builtin {
+ obj.to_string()
+ }
+}
diff --git a/uniffi-bindgen.rs b/uniffi-bindgen.rs
new file mode 100644
index 000000000..2aea96784
--- /dev/null
+++ b/uniffi-bindgen.rs
@@ -0,0 +1,3 @@
+fn main() {
+ uniffi::uniffi_bindgen_main()
+}
diff --git a/uniffi.toml b/uniffi.toml
new file mode 100644
index 000000000..57655582d
--- /dev/null
+++ b/uniffi.toml
@@ -0,0 +1,9 @@
+[bindings.kotlin]
+package_name = "org.lightningdevkit.ldknode"
+cdylib_name = "ldk_node"
+
+[bindings.python]
+cdylib_name = "ldk_node"
+
+[bindings.swift]
+cdylib_name = "ldk_node"