Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,18 @@ lib*.a
# Python ck-tap emulator
/emulator_env/
*.log

# Android
cktap-android/.gradle/
cktap-android/.kotlin/
cktap-android/build/
cktap-android/lib/build/
cktap-android/lib/src/main/jniLibs/
cktap-android/lib/src/main/kotlin/
cktap-android/local.properties
*.so

# Credentials / signing (must never be committed)
secring.gpg
*.asc
*.gpg
200 changes: 200 additions & 0 deletions cktap-android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# cktap-android

This project builds an `.aar` package for the Android platform that provides Kotlin language bindings for the [rust-cktap] library. The Kotlin bindings are generated by the [`cktap-ffi`](../cktap-ffi) crate using [uniffi-rs].

## How it works

`cktap-android` is a thin Android packaging layer on top of the Rust implementation. The stack looks like this:

```
Android app (Kotlin)
└─ cktap-android Kotlin bindings + prebuilt native libs, shipped as an .aar
└─ cktap-ffi Rust crate that exports the UniFFI scaffolding (cdylib → libcktap_ffi.so)
└─ rust-cktap Pure-Rust implementation of the Coinkite Tap Protocol (SATSCARD, TAPSIGNER, SATSCHIP)
```

What the build produces:

- **`libcktap_ffi.so`** — the Rust library compiled for each supported Android ABI (`arm64-v8a`, `armeabi-v7a`, `x86_64`), placed under `lib/src/main/jniLibs/<abi>/`.
- **`cktap_ffi.kt`** — the Kotlin API surface auto-generated by UniFFI from the Rust signatures in `cktap-ffi`, placed under `lib/src/main/kotlin/com/coinkite/cktap/`.
- **`cktap-android-<version>.aar`** — the final artifact that bundles the generated Kotlin sources together with the native libraries. It depends on [JNA](https://github.com/java-native-access/jna) at runtime to dispatch Kotlin calls through JNI into the Rust functions.

Consumers see plain Kotlin types (`suspend` functions, typed exceptions, data classes) and never interact with the JNI bridge directly — UniFFI handles marshalling in both directions.

## How to Use

Once published to Maven Central, add the following to your Android project's `build.gradle.kts`:

```kotlin
repositories {
mavenCentral()
}

dependencies {
implementation("org.bitcoindevkit:cktap-android:<version>")
}
```

### Snapshot releases

To use a snapshot release, add the snapshot repository:

```kotlin
repositories {
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}

dependencies {
implementation("org.bitcoindevkit:cktap-android:<version>-SNAPSHOT")
}
```

### Local Maven (`~/.m2/repository`)

During development — or until an artifact is published to Maven Central — consume the library from your local Maven repository. Declare `mavenLocal()` **before** `mavenCentral()` so Gradle prefers the locally built snapshot and falls back to Maven Central automatically once a release is available:

```kotlin
repositories {
mavenLocal()
mavenCentral()
}

dependencies {
implementation("org.bitcoindevkit:cktap-android:0.1.0-SNAPSHOT")
}
```

See [How to publish to your local Maven repository](#how-to-publish-to-your-local-maven-repository) below for the publish flow.

## How to build locally

_Note: Kotlin `2.3.10`+, JDK 17, Android SDK API 34+, and Android NDK `27.2.12479018`+ are required._

1. Clone this repository:
```shell
git clone https://github.com/notmandatory/rust-cktap
```

2. Set up environment variables for Android SDK and NDK:
```shell
# macOS
export ANDROID_SDK_ROOT=~/Library/Android/sdk
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/27.2.12479018
```

3. Build the native libraries and Kotlin bindings:
```shell
cd cktap-android
just build macos-aarch64
```

4. (Optional) Run instrumented tests on a connected emulator/device:
```shell
just test
```

## How to publish to your local Maven repository

This flow works **without any signing keys or Sonatype credentials** and is ideal for local development and testing.

### Prerequisites

- [Rust toolchain](https://rustup.rs/) (`cargo`, `rustup`)
- [`just`](https://github.com/casey/just) (`brew install just` on macOS)
- JDK 17
- Android SDK API 34+ and Android NDK `27.2.12479018`+ (install via Android Studio → SDK Manager → SDK Tools → NDK)
- `local.properties` inside `cktap-android/` pointing at the SDK — either create the file with `sdk.dir=/path/to/Android/sdk` or export `ANDROID_SDK_ROOT`

### Steps

1. Export the SDK/NDK environment variables (adjust the NDK version to match what is installed):

```shell
export ANDROID_SDK_ROOT=~/Library/Android/sdk
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/27.2.12479018
```

2. Build the Rust library and generate the Kotlin bindings for your host ABI:

```shell
cd cktap-android
just build macos-aarch64
```

This compiles `libcktap_ffi.so` for `arm64-v8a` and regenerates `lib/src/main/kotlin/com/coinkite/cktap/cktap_ffi.kt` via UniFFI.

3. Publish the AAR to your local Maven repository:

```shell
just publish-local # alias for: ./gradlew publishToMavenLocal -P localBuild
```

The `-P localBuild` flag disables GPG signing so no keys or Sonatype credentials are required. The AAR, sources JAR, and POM metadata are deposited under `~/.m2/repository/org/bitcoindevkit/cktap-android/0.1.0-SNAPSHOT/`.

Because the version ends in `-SNAPSHOT`, Gradle revalidates the local Maven cache on every build in consumer projects — re-run `just publish-local` after any change and the next consumer build will pick up the fresh copy automatically.

### Consuming the local build

```kotlin
repositories {
mavenLocal()
mavenCentral()
}

dependencies {
implementation("org.bitcoindevkit:cktap-android:0.1.0-SNAPSHOT")
}
```

## How to publish to Maven Central (official release)

This flow **requires signing keys and Sonatype credentials**. Configure them in `~/.gradle/gradle.properties` (outside the repo):

```properties
# Sonatype Central Publisher Portal tokens
mavenCentralUsername=<TOKEN_USERNAME>
mavenCentralPassword=<TOKEN_PASSWORD>

# GPG signing
signing.gnupg.keyName=<GPG_KEY_ID>
signing.gnupg.passphrase=<GPG_PASSPHRASE>
```

Then:

```shell
# 1. Update version in lib/build.gradle.kts (remove -SNAPSHOT)
# 2. Build native libraries for all supported ABIs
just build macos-aarch64

# 3. Publish
just publish-central # alias for: ./gradlew publishAndReleaseToMavenCentral

# 4. Tag and push
git tag v0.1.0 && git push --tags
```

## Known issues

### JNA dependency

Depending on the JVM version used by your consumer project, JNA may not be on the classpath. If you see:

```
class file for com.sun.jna.Pointer not found
```

Add JNA explicitly:

```kotlin
dependencies {
implementation("net.java.dev.jna:jna:5.14.0@aar")
}
```

### x86 emulators

The library currently ships native binaries for `arm64-v8a`, `armeabi-v7a`, and `x86_64`. It does **not** ship 32-bit `x86`. Use an `x86_64` emulator when testing on macOS/Linux x86 hosts.

[rust-cktap]: https://github.com/notmandatory/rust-cktap
[uniffi-rs]: https://github.com/mozilla/uniffi-rs
6 changes: 6 additions & 0 deletions cktap-android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
plugins {
id("com.android.library").version("8.13.2").apply(false)
id("org.jetbrains.kotlin.android").version("2.3.10").apply(false)
id("org.jetbrains.dokka").version("2.1.0").apply(false)
id("com.vanniktech.maven.publish").version("0.36.0").apply(false)
}
7 changes: 7 additions & 0 deletions cktap-android/docs/DOKKA_LANDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Module cktap-android

The [rust-cktap](https://github.com/notmandatory/rust-cktap) language bindings library for Kotlin on Android.

This library exposes the Coinkite Tap Protocol (cktap) for use with [SATSCARD](https://satscard.com/), [TAPSIGNER](https://tapsigner.com/), and [SATSCHIP](https://satschip.com/) products via NFC.

# Package org.bitcoindevkit.cktap
6 changes: 6 additions & 0 deletions cktap-android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
Binary file added cktap-android/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions cktap-android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading