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"