diff --git a/README.md b/README.md index 5bf19b4..7c754f1 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,37 @@ This project is intended to be an easy-to-use starting point for those intereste ## Getting Started -You can install the cli app with `cargo install fedimint-clientd` or by cloning the repo and running `cargo build --release` in the root directory. - -`fedimint-clientd` runs from the command line and takes a few arguments, which are also available as environment variables. Fedimint uses rocksDB, an embedded key-value store, to store its state. The `--fm_db_path` argument is required and should be an absolute path to a directory where the database will be stored. - -``` -CLI USAGE: -fedimint-clientd \ - --db-path=/absolute/path/to/dir/to/store/database \ - --password="some-secure-password-that-becomes-the-bearer-token" \ - --addr="127.0.0.1:8080" - --mode="rest" - --invite-code="fed1-fedimint-invite-code" - -ENV USAGE: -FEDIMINT_CLIENTD_DB_PATH=/absolute/path/to/dir/to/store/database -FEDIMINT_CLIENTD_PASSWORD="some-secure-password-that-becomes-the-bearer-token" -FEDIMINT_CLIENTD_ADDR="127.0.0.1:8080" -FEDIMINT_CLIENTD_MODE="rest" -FEDIMINT_CLIENTD_INVITE_CODE="fed1-fedimint-invite-code" -``` +1. You can install the cli app with `cargo install fedimint-clientd` or by cloning the repo and running `cargo build --release` in the root directory. + + `fedimint-clientd` runs from the command line and takes a few arguments, which are also available as environment variables. Fedimint uses rocksDB, an embedded key-value store, to store its state. The `--fm_db_path` argument is required and should be an absolute path to a directory where the database will be stored. + + ```shell + CLI USAGE: + fedimint-clientd \ + --db-path=/absolute/path/to/dir/to/store/database \ + --password="some-secure-password-that-becomes-the-bearer-token" \ + --addr="127.0.0.1:3333" + --mode="rest" + --invite-code="fed1-fedimint-invite-code" + ``` + +2. With a Nix environment already setup: +- Clone the repo and run `nix develop` to install and build the dependencies. +- Run `just dev` to fire up the client app. + + Make sure to configure your environment variables as shown below: + + ```shell + ENV USAGE: + FEDIMINT_CLIENTD_DB_PATH=/absolute/path/to/dir/to/store/database + FEDIMINT_CLIENTD_PASSWORD="some-secure-password-that-becomes-the-bearer-token" + FEDIMINT_CLIENTD_ADDR="127.0.0.1:3333" + FEDIMINT_CLIENTD_BASE_URL="127.0.0.1:3333" + FEDIMINT_CLIENTD_MODE="rest" + # this is the invite code to the Fedi Alpha federation mutinynet, + # you can replace it with another but its the most useful one for testing so good to at least have it + FEDIMINT_CLIENTD_INVITE_CODE="fed11qgqrgvnhwden5te0v9k8q6rp9ekh2arfdeukuet595cr2ttpd3jhq6rzve6zuer9wchxvetyd938gcewvdhk6tcqqysptkuvknc7erjgf4em3zfh90kffqf9srujn6q53d6r056e4apze5cw27h75" + ``` ## Fedimint Clientd Endpoints diff --git a/flake.nix b/flake.nix index dae2af1..2b0aa0d 100644 --- a/flake.nix +++ b/flake.nix @@ -103,7 +103,13 @@ legacyPackages = outputs; packages = { default = outputs.fedimint-clientd; }; devShells = flakeboxLib.mkShells { - packages = [ ]; + packages = with pkgs; [ + jdk21 # JDK 22 will be in $JAVA_HOME (and in javaToolchains) + jextract # jextract (Nix package) contains a jlinked executable and bundles its own JDK 22 + (gradle.override { # Gradle 8.7 (Nix package) depends-on and directly uses JDK 21 to launch Gradle itself + javaToolchains = [ jdk21 ]; + }) + ]; buildInputs = commonArgs.buildInputs; nativeBuildInputs = [ pkgs.mprocs @@ -120,6 +126,7 @@ fedimint.packages.${system}.fedimint-pkgs ]; shellHook = '' + export JAVA_HOME="${pkgs.jdk21}" export RUSTFLAGS="--cfg tokio_unstable" export RUSTDOCFLAGS="--cfg tokio_unstable" export RUST_LOG="info" diff --git a/justfile.local.just b/justfile.local.just index eda0478..54ddb9a 100644 --- a/justfile.local.just +++ b/justfile.local.just @@ -16,12 +16,16 @@ test-py-async: test-go: cd wrappers/fedimint-go && go run cmd/main.go +test-kotlin: + cd wrappers/fedimint-kotlin && ./gradlew build && ./gradlew run + test-all: set -e (just test-ts && echo "test-ts completed successfully") & (just test-py && echo "test-py completed successfully") & (just test-py-async && echo "test-py-async completed successfully") & (just test-go && echo "test-go completed successfully") & + (just test-kotlin && echo "test-kotlin completed successfully") & wait wscat: diff --git a/wrappers/fedimint-kotlin/.gitignore b/wrappers/fedimint-kotlin/.gitignore new file mode 100644 index 0000000..63d2f1a --- /dev/null +++ b/wrappers/fedimint-kotlin/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/wrappers/fedimint-kotlin/README.md b/wrappers/fedimint-kotlin/README.md new file mode 100644 index 0000000..b83ac75 --- /dev/null +++ b/wrappers/fedimint-kotlin/README.md @@ -0,0 +1,34 @@ + +# Fedimint SDK for Kotlin + +This is a Kotlin client that consumes the Fedimint Http Client (https://github.com/kodylow/fedimint-http-client)[https://github.com/kodylow/fedimint-http-client], communicating with it via HTTP and a password. It's a hacky prototype, but it works. All of the federation handling code happens in the fedimint-http-client, this just exposes a simple API for interacting with the client from Kotlin. + +Start the following in the fedimint-http-client .env environment variables: + +```bash +FEDIMINT_CLIENTD_DB_PATH="YOUR-DATABASE-PATH" +FEDIMINT_CLIENTD_PASSWORD="YOUR-PASSWORD" +FEDIMINT_CLIENTD_ADDR="127.0.0.1:3333" +FEDIMINT_CLIENTD_MODE="rest" +FEDIMINT_CLIENTD_INVITE_CODE="fed11qgqrgvnhwden5te0v9k8q6rp9ekh2arfdeukuet595cr2ttpd3jhq6rzve6zuer9wchxvetyd938gcewvdhk6tcqqysptkuvknc7erjgf4em3zfh90kffqf9srujn6q53d6r056e4apze5cw27h75" +FEDIMINT_CLIENTD_BASE_URL="127.0.0.1:3333" +``` + +Then start the fedimint-http-client server: + +```bash +cargo run +``` + +Then you are ready to run the Kotlin client. You have 2 oprions to fire it up: + +1. Use an Intellij IDE or Android Studio. + + This is the simplest and best way to work with the Kotlin wrapper. Open the Kotlin project with any Gradle based IDE such as Intellij or Android Studio. + +2. Run the following commands from the Kotlin project root folder. + + ```bash + ./gradlew build + ./gradlew run + ``` diff --git a/wrappers/fedimint-kotlin/build.gradle.kts b/wrappers/fedimint-kotlin/build.gradle.kts new file mode 100644 index 0000000..d6ce3da --- /dev/null +++ b/wrappers/fedimint-kotlin/build.gradle.kts @@ -0,0 +1,43 @@ + +val ktor_version: String by project +val kotlin_version: String by project +val logback_version: String by project + +plugins { + kotlin("jvm") version "1.9.24" + id("io.ktor.plugin") version "2.3.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" +} + +group = "com.fedimintClientd" +version = "0.0.1" + +application { + mainClass.set("com.fedimintClientd.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.ktor:ktor-server-content-negotiation-jvm") + implementation("io.ktor:ktor-server-core-jvm") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") + implementation("io.ktor:ktor-server-netty-jvm") + implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-cio:$ktor_version") + implementation("io.ktor:ktor-client-auth:$ktor_version") + implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") + implementation("io.ktor:ktor-client-logging:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("io.ktor:ktor-serialization-gson:$ktor_version") + implementation(kotlin("script-runtime")) + implementation ("io.github.cdimascio:dotenv-kotlin:6.4.1") + testImplementation("io.ktor:ktor-server-tests-jvm") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") +} diff --git a/wrappers/fedimint-kotlin/gradle.properties b/wrappers/fedimint-kotlin/gradle.properties new file mode 100644 index 0000000..0444b3c --- /dev/null +++ b/wrappers/fedimint-kotlin/gradle.properties @@ -0,0 +1,4 @@ +ktor_version=2.3.10 +kotlin_version=1.9.24 +logback_version=1.4.14 +kotlin.code.style=official diff --git a/wrappers/fedimint-kotlin/gradle/wrapper/gradle-wrapper.jar b/wrappers/fedimint-kotlin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/wrappers/fedimint-kotlin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/wrappers/fedimint-kotlin/gradle/wrapper/gradle-wrapper.properties b/wrappers/fedimint-kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e411586 --- /dev/null +++ b/wrappers/fedimint-kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/wrappers/fedimint-kotlin/gradlew b/wrappers/fedimint-kotlin/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/wrappers/fedimint-kotlin/gradlew @@ -0,0 +1,234 @@ +#!/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/master/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 \ + "$@" + +# 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/wrappers/fedimint-kotlin/gradlew.bat b/wrappers/fedimint-kotlin/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/wrappers/fedimint-kotlin/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wrappers/fedimint-kotlin/settings.gradle.kts b/wrappers/fedimint-kotlin/settings.gradle.kts new file mode 100644 index 0000000..d53fd25 --- /dev/null +++ b/wrappers/fedimint-kotlin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "com.fedimintClientd.fedimint-kotlin" diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/Application.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/Application.kt new file mode 100644 index 0000000..89939f1 --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/Application.kt @@ -0,0 +1,205 @@ +package com.fedimintClientd + +import io.github.cdimascio.dotenv.Dotenv +import kotlinx.coroutines.runBlocking +import kotlin.system.exitProcess +import io.github.cdimascio.dotenv.dotenv + +fun main() { + try { + val dotenv = dotenv { + // ignoreIfMalformed = true + ignoreIfMissing = true + } + + val fedimintClient = buildFedimintClient(dotenv) ?: exitProcess(500) + + val mint = fedimintClient.MintModule() + val ln = fedimintClient.LightningModule() + val onchain = fedimintClient.OnChainModule() + runBlocking { + // Admin + + logMethod("/v2/admin/info") + val info = fedimintClient.info() + if(info.second!=null){ + throw Exception(info.second) + } + logInputAndOutput(emptyMap(), info) + + logMethod("/v2/admin/config") + val config = fedimintClient.config() + if(config.second!=null){ + throw Exception(config.second) + } + logInputAndOutput(emptyMap(), config) + + logMethod("/v2/admin/discover-version") + val version = fedimintClient.discoverVersion(1) + if(version.second!=null){ + throw Exception(version.second) + } + logInputAndOutput(emptyMap(), version) + + logMethod("/v2/admin/federation-ids") + val federationId = fedimintClient.federationIds() + if(federationId.second!=null){ + throw Exception(federationId.second) + } + logInputAndOutput(emptyMap(), federationId) + + logMethod("/v2/admin/join") + val inviteCode = dotenv["FEDIMINT_CLIENTD_INVITE_CODE"] + ?: "fed11qgqrgvnhwden5te0v9k8q6rp9ekh2arfdeukuet595cr2ttpd3jhq6rzve6zuer9wchxvetyd938gcewvdhk6tcqqysptkuvknc7erjgf4em3zfh90kffqf9srujn6q53d6r056e4apze5cw27h75" + val join = + fedimintClient.join(inviteCode) + if(join.second!=null){ + throw Exception(join.second) + } + logInputAndOutput(mapOf( "inviteCode" to inviteCode ), join) + + logMethod("/v2/admin/list-operations") + val operations = fedimintClient.listOperations(10) + if(operations.second!=null){ + throw Exception(operations.second) + } + logInputAndOutput(mapOf( "limit" to 10 ), operations) + + // Onchain + + logMethod("/v2/onchain/deposit-address") + val address = onchain.createDepositAddress(1000) + if(address.second!=null){ + throw Exception(address.second) + } + logInputAndOutput(mapOf( "timeout" to 1000 ), address) + + logMethod("/v2/onchain/withdraw") + val withdraw = address.first?.let { onchain.withdraw(it.address, 1000) } + if(withdraw?.second!=null){ + throw Exception(withdraw.second) + } + logInputAndOutput(mapOf( + "address" to address.first?.address, + "amountSat" to 1000 + ), withdraw) + + // Lightning + + logMethod("/v2/ln/list-gateways") + val gateways = ln.listGateways() + logInputAndOutput(emptyMap(), gateways) + if (gateways.isNotEmpty()) { + fedimintClient.activeGatewayId = gateways.first().info.gatewayId + } + + logMethod("/v2/ln/invoice") + val invoice = ln.createInvoice(1000, "Test") + if(invoice.second!=null){ + throw Exception(invoice.second) + } + logInputAndOutput(mapOf( + "amountMsat" to 1000, + "description" to "Test" + ), invoice) + + logMethod("/v2/ln/await-invoice") + val awaitInvoice = invoice.first?.let { ln.awaitInvoice(operationId = it.operationId) } + if(awaitInvoice?.second!=null){ + throw Exception(awaitInvoice.second) + } + logInputAndOutput(mapOf( "operationId" to invoice.first?.operationId ), awaitInvoice) + + logMethod("/v2/ln/pay") + val pay = invoice.first?.let { ln.pay(paymentInfo = it.invoice) } + if(pay?.second!=null){ + throw Exception(pay.second) + } + logInputAndOutput(mapOf( "paymentInfo" to invoice.first?.invoice ), pay) + + val awaitPay = pay?.first?.let { ln.awaitPay(operationId = it.operationId) } + if(awaitPay?.second!=null){ + throw Exception(awaitPay.second) + } + logInputAndOutput(mapOf( "operationId" to pay?.first?.operationId ), awaitPay) + + // Mint + + logMethod("/v2/mint/spend") + val spend = mint.spend(amountMsat = 3000, allowOverpay = true, timeout = 1000, includeInvite = false) + if(spend.second!=null){ + throw Exception(spend.second) + } + logInputAndOutput(mapOf( + "amountMsat" to 3000, + "allowOverpay" to true, + "timeout" to 1000, + "includeInvite" to false + ), spend) + + logMethod("/v2/mint/decode-notes") + val notes = spend.first?.notes?.let { mint.decodeNotes(it) } + if(notes?.second!=null){ + throw Exception(notes.second) + } + logInputAndOutput(mapOf( "notes" to spend.first?.notes ), notes) + + logMethod("/v2/mint/encode-notes") + val encodedNotes = notes?.first?.notesJson?.let { mint.encodeNotes(it) } + if(encodedNotes?.second!=null){ + throw Exception(encodedNotes.second) + } + logInputAndOutput(mapOf( "notesJson" to notes?.first?.notesJson ), encodedNotes) + + logMethod("/v2/mint/validate") + val validate = spend.first?.notes?.let { mint.validate(it) } + if(validate?.second!=null){ + throw Exception(validate.second) + } + logInputAndOutput(mapOf( "notes" to spend.first?.notes ), validate) + + logMethod("/v2/mint/reissue") + val reissue = spend.first?.notes?.let { mint.reissue(it) } + if(reissue?.second!=null){ + throw Exception(reissue.second) + } + logInputAndOutput(mapOf( "notes" to spend.first?.notes ), reissue) + + logMethod("/v2/mint/split") + val split = spend.first?.notes?.let { mint.split(it) } + if(split?.second!=null){ + throw Exception(split.second) + } + logInputAndOutput(mapOf( "notes" to spend.first?.notes ), split) + + println("🚀Done: All Methods Tested Successfully🚀") + } + } catch (e: Exception) { + println("Test Failed:: ${e.localizedMessage}") + } +} + +fun buildFedimintClient(dotenv: Dotenv): FedimintClient? { + try { + val baseUrl = dotenv["FEDIMINT_CLIENTD_BASE_URL"] ?: "http://127.0.0.1:3333" + val password = dotenv["FEDIMINT_CLIENTD_PASSWORD"] ?: "password" + val federationId = dotenv["FEDIMINT_CLIENTD_ACTIVE_FEDERATION_ID"] + ?: "15db8cb4f1ec8e484d73b889372bec94812580f929e8148b7437d359af422cd3" + + return FedimintClient(baseUrl = baseUrl, password = password, activeFederationId = federationId) + } catch (e: Exception) { + println("Test Failed:: ${e.localizedMessage}") + } + return null +} + +fun logMethod(method: String) { + println("--------------------") + println("Method: $method") +} + +fun logInputAndOutput(inputData: Any, output: Any?) { + println("Input: $inputData") + println("Output: $output") + println("--------------------") +} diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/FedimintClient.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/FedimintClient.kt new file mode 100644 index 0000000..faffa13 --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/FedimintClient.kt @@ -0,0 +1,434 @@ +package com.fedimintClientd + +import com.fedimintClientd.models.* +import com.google.gson.Gson +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.gson.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +class FedimintClient( + var baseUrl: String, + val password: String, + val activeFederationId: String, + var activeGatewayId: String? = null +) { + init { + baseUrl = "$baseUrl/v2/" + } + + private val client = HttpClient(CIO) { + install(ContentNegotiation) { + gson() + } + install(Auth) { + bearer { + loadTokens { + BearerTokens(password, "") + } + } + } + } + + suspend fun _get(endpoint: String): Pair { + try { + val response = client.get("${baseUrl}${endpoint}") + return Pair(response.body(), null) + } catch (e: Exception) { + return Pair(null, e.localizedMessage) + } + } + + suspend fun _post(endpoint: String, data: String? = null): Pair { + try { + val response = client.post("${baseUrl}${endpoint}") { + contentType(ContentType.Application.Json) + setBody(data) + } + return if (response.status.value == 200) { + Pair(response.bodyAsText(), null) + } else { + Pair(null, "${response.status}") + } + } catch (e: Exception) { + return Pair(null, e.localizedMessage) + } + } + + suspend fun _postWithFederationId( + endpoint: String, + federationId: String? = null, + data: Map = emptyMap() + ): Pair { + try { + val data = data.toMutableMap() + data["federationId"] = federationId ?: this.activeFederationId + val body = Gson().toJson(data) + val response = this._post(endpoint, body) + return response + } catch (e: Exception) { + return Pair(null, e.localizedMessage) + } + } + + suspend fun _postWithGatewayIdAndFederationId( + endpoint: String, + federationId: String? = null, + gatewayId: String? = null, + data: Map = emptyMap() + ): Pair { + try { + val data = data.toMutableMap() + data["federationId"] = federationId ?: this.activeFederationId + data["gatewayId"] = gatewayId ?: this.activeGatewayId ?: throw Exception("Must set Active Gateway ID!") + val body = Gson().toJson(data) + val response = this._post(endpoint, body) + return response + } catch (e: Exception) { + return Pair(null, e.localizedMessage) + } + } + + suspend fun info(): Pair { + return this._get("admin/info") + } + + suspend fun config(): Pair { + return this._get("admin/config") + } + + suspend fun discoverVersion(threshold: Int): Pair { + val data = mutableMapOf("inviteCode" to threshold) + return this._post("admin/discover-version", data = Json.encodeToString(data)) + } + + suspend fun federationIds(): Pair { + return this._get("admin/federation-ids") + } + + suspend fun join(inviteCode: String, useManualSecret: Boolean = false): Pair { + val data = buildJsonObject { + put("useManualSecret", useManualSecret) + put("inviteCode", inviteCode) + } + return _post("admin/join", data = data.toString()) + } + + suspend fun listOperations(limit: Int): Pair { + return this._postWithFederationId("admin/list-operations", data = mapOf("limit" to limit)) + } + + inner class MintModule { + private val json = Json { ignoreUnknownKeys = true } + + suspend fun spend( + amountMsat: Int, + allowOverpay: Boolean, + timeout: Int, + includeInvite: Boolean, + federationId: String? = null + ): Pair { + val mintSpendRequest = mapOf( + "amountMsat" to amountMsat, + "allowOverpay" to allowOverpay, + "timeout" to timeout, + "includeInvite" to includeInvite, + ) + val res = _postWithFederationId( + "mint/spend", + federationId = federationId, + data = mintSpendRequest + ) + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun decodeNotes(notes: String, federationId: String? = null): Pair { + val res = _postWithFederationId( + "mint/decode-notes", + federationId = federationId, + data = mapOf("notes" to notes) + ) + + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun encodeNotes(notes: NotesJson, federationId: String? = null): Pair { + return _postWithFederationId( + "mint/encode-notes", + federationId = federationId, + data = mapOf("notesJsonStr" to json.encodeToString(notes)) + ) + } + + suspend fun validate(notes: String, federationId: String? = null): Pair { + return _postWithFederationId( + "mint/validate", + federationId = federationId, + data = mapOf("notes" to notes) + ) + } + + suspend fun combine(notesVec: List, federationId: String? = null): Pair { + return _postWithFederationId( + "mint/combine", + federationId = federationId, + data = mapOf("notesVec" to notesVec) + ) + } + + suspend fun reissue(notes: String, federationId: String? = null): Pair { + return _postWithFederationId( + "mint/reissue", + federationId = federationId, + data = mapOf("notes" to notes) + ) + } + + suspend fun split(notes: String, federationId: String? = null): Pair { + return _postWithFederationId( + "mint/split", + federationId = federationId, + data = mapOf("notes" to notes) + ) + } + } + + inner class LightningModule { + private val json = Json { ignoreUnknownKeys = true } + + suspend fun listGateways(): List { + try { + val res = _postWithFederationId( + "ln/list-gateways", + ) + if (res.first != null) { + return json.decodeFromString>(res.first!!) + } + } catch (e: Exception) { + println(e.localizedMessage) + } + return emptyList() + } + + suspend fun createInvoice( + amountMsat: Int, + description: String, + expiryTime: Int? = null, + federationId: String? = null, + gatewayId: String? = null + ): Pair { + val request = mutableMapOf( + "amountMsat" to amountMsat, + "description" to description, + ) + if (expiryTime != null) { + request["expiryTime"] = expiryTime + } + val res = _postWithGatewayIdAndFederationId( + "ln/invoice", + federationId = federationId, + gatewayId = gatewayId, + data = request + ) + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun awaitInvoice( + operationId: String, + federationId: String? = null, + ): Pair { + val request = mutableMapOf( + "operationId" to operationId, + ) + val res = _postWithGatewayIdAndFederationId( + "ln/await-invoice", + federationId = federationId, + data = request + ) + + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun pay( + amountMsat: Int? = null, + paymentInfo: String, + lightningUrlComment: String? = null, + federationId: String? = null, + gatewayId: String? = null + ): Pair { + val request = mutableMapOf( + "amountMsat" to amountMsat, + "lightningUrlComment" to lightningUrlComment, + "paymentInfo" to paymentInfo, + ) + + val res = _postWithGatewayIdAndFederationId( + "ln/pay", + federationId = federationId, + gatewayId = gatewayId, + data = request + ) + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun awaitPay( + operationId: String, + federationId: String? = null, + ): Pair { + val request = mutableMapOf( + "operationId" to operationId, + ) + return _postWithGatewayIdAndFederationId( + "ln/await-pay", + federationId = federationId, + data = request + ) + } + + suspend fun createInvoiceForPubkeyTweak( + tweak: Int, + pubkey: String, + amountMsat: Int, + description: String, + expiryTime: Int? = null, + federationId: String? = null, + gatewayId: String? = null + ): Pair { + val request = mutableMapOf( + "tweak" to tweak, + "externalPubkey" to pubkey, + "amountMsat" to amountMsat, + "description" to description, + ) + if (expiryTime != null) { + request["expiryTime"] = expiryTime + } + val res = _postWithGatewayIdAndFederationId( + "ln/invoice-external-pubkey-tweaked", + federationId = federationId, + gatewayId = gatewayId, + data = request + ) + + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + + } + } + + suspend fun claimPubkeyTweakReceives( + privateKey: String, + tweaks: List, + federationId: String? = null, + ): Pair { + val request = mapOf( + "tweaks" to tweaks, + "privateKey" to privateKey, + ) + + val res = _postWithFederationId( + "ln/claim-external-receive-tweaked", + federationId = federationId, + data = request + ) + + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + } + + inner class OnChainModule { + private val json = Json { ignoreUnknownKeys = true } + + suspend fun createDepositAddress( + timeout: Int, + federationId: String? = null + ): Pair { + val res = _postWithFederationId( + "onchain/deposit-address", + federationId = federationId, + data = mapOf("timeout" to timeout) + ) + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun awaitDeposit( + operationId: String, + federationId: String? = null + ): Pair { + val res = _postWithFederationId( + "onchain/await-deposit", + federationId = federationId, + data = mapOf("operationId" to operationId) + ) + + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + + suspend fun withdraw( + address: String, + amountSat: Int?, + withdrawAllSats: Boolean = false, + federationId: String? = null + ): Pair { + var amnt = amountSat.toString() + if (withdrawAllSats && amountSat == null) { + amnt = "all" + } + + val res = _postWithFederationId( + "onchain/withdraw", + federationId = federationId, + data = mapOf("address" to address, "amountSat" to amnt) + ) + + return if (res.first != null) { + Pair(json.decodeFromString(res.first!!), null) + } else { + Pair(null, res.second) + } + } + } +} diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Lightning.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Lightning.kt new file mode 100644 index 0000000..72e565e --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Lightning.kt @@ -0,0 +1,94 @@ +package com.fedimintClientd.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LightningCreateInvoiceResponse( + val operationId: String, + val invoice: String +) + +@Serializable +data class LightningInvoiceForPubkeyTweakResponse( + val operationId: String, + val invoice: String +) + +@Serializable +data class LightningPayResponse( + val operationId: String, + val paymentType: PaymentType, + val contractId: String, + val fee: Int +) + +@Serializable +data class PaymentType( + val internal: String, +) + +@Serializable +sealed class LnPaymentResult { + data class WaitingForPayment(val paymentRequest: String) : LnPaymentResult() + object Canceled : LnPaymentResult() +} + +@Serializable +data class LightningPaymentResponse( + val state: LnReceiveState, + val details: LnPaymentResult? = null +) + +@Serializable +enum class LnReceiveState { + Created, + WaitingForPayment, + Canceled, + Funded, + AwaitingFunds, + Claimed +} + +@Serializable +data class GatewayFees( + @SerialName("base_msat") + val baseMsat: Int, + @SerialName("proportional_millionths") + val proportionalMillionths: Int +) + +@Serializable +data class GatewayInfo( + val api: String, + val fees: GatewayFees, + @SerialName("gateway_id") + val gatewayId: String, + @SerialName("gateway_redeem_key") + val gatewayRedeemKey: String, + @SerialName("lightning_alias") + val lightningAlias: String, + @SerialName("mint_channel_id") + val mintChannelId: Int, + @SerialName("node_pub_key") + val nodePubKey: String, + @SerialName("route_hints") + val routeHints: List, + @SerialName("supports_private_payments") + val supportsPrivatePayments: Boolean +) + +@Serializable +data class GatewayTTL( + val nanos: Long, + val secs: Int +) + +@Serializable +data class Gateway( + @SerialName("federation_id") + val federationId: String, + val info: GatewayInfo, + val ttl: GatewayTTL, + val vetted: Boolean +) diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Mint.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Mint.kt new file mode 100644 index 0000000..20dbe59 --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Mint.kt @@ -0,0 +1,29 @@ +package com.fedimintClientd.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class NotesJson( + @SerialName("federation_id_prefix") + val federationIdPrefix: String, + val notes: Map> +) + +@Serializable +data class Note( + val signature: String, + @SerialName("spend_key") + val spendKey: String +) + +@Serializable +data class MintSpendResponse( + val operation: String, + val notes: String +) + +@Serializable +data class MintDecodeNotesResponse( + val notesJson: NotesJson, +) diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Onchain.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Onchain.kt new file mode 100644 index 0000000..c8769e7 --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/models/Onchain.kt @@ -0,0 +1,53 @@ +package com.fedimintClientd.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +sealed class OnchainAwaitDepositResponse(val status: Any) { + data class Confirmed(val confirmed: AwaitDepositResponseConfirmed) : OnchainAwaitDepositResponse(confirmed) + data class Error(val message: String) : OnchainAwaitDepositResponse(message) +} + +@Serializable +data class OnchainWithdrawResponse( + val fees_sat: Int, + val txid: String +) + +@Serializable +data class OnchainCreateAddressResponse( + val address: String, + val operationId: String +) + +@Serializable +data class AwaitDepositResponseConfirmed( + val btcTransaction: BTCTransaction, + val outIdx: Int +) + +@Serializable +data class BTCInput( + @SerialName("previous_output") + val previousOutput: String, + @SerialName("script_sig") + val scriptSig: String, + val sequence: Int, + val witness: List +) + +@Serializable +data class BTCOutput( + val value: Long, + @SerialName("script_pubkey") + val scriptPubkey: String +) + +@Serializable +data class BTCTransaction( + val version: Int, + @SerialName("lock_time") + val lockTime: Int, + val input: List, + val output: List +) diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/plugins/Routing.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/plugins/Routing.kt new file mode 100644 index 0000000..e1ce113 --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/plugins/Routing.kt @@ -0,0 +1,15 @@ +package com.fedimintClientd.plugins + +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureRouting() { + + routing { + get("/") { + call.respondText("Hello World!") + } + + } +} diff --git a/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/plugins/Serialization.kt b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/plugins/Serialization.kt new file mode 100644 index 0000000..3b689d0 --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/kotlin/com/fedimintClientd/plugins/Serialization.kt @@ -0,0 +1,18 @@ +package com.fedimintClientd.plugins + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + } + routing { + get("/json/kotlinx-serialization") { + call.respond(mapOf("hello" to "world")) + } + } +} diff --git a/wrappers/fedimint-kotlin/src/main/resources/logback.xml b/wrappers/fedimint-kotlin/src/main/resources/logback.xml new file mode 100644 index 0000000..bdbb64e --- /dev/null +++ b/wrappers/fedimint-kotlin/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + +