Skip to content

Commit b1b6339

Browse files
authored
feat(android): add cktap-android bindings
1 parent 891de46 commit b1b6339

22 files changed

Lines changed: 939 additions & 1 deletion

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,18 @@ lib*.a
2727
# Python ck-tap emulator
2828
/emulator_env/
2929
*.log
30+
31+
# Android
32+
cktap-android/.gradle/
33+
cktap-android/.kotlin/
34+
cktap-android/build/
35+
cktap-android/lib/build/
36+
cktap-android/lib/src/main/jniLibs/
37+
cktap-android/lib/src/main/kotlin/
38+
cktap-android/local.properties
39+
*.so
40+
41+
# Credentials / signing (must never be committed)
42+
secring.gpg
43+
*.asc
44+
*.gpg

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ opt-level = 'z'
1515
lto = true
1616
codegen-units = 1
1717
panic = "abort"
18-
strip = true
18+
# NOTE: do not set `strip = true` here. uniffi-bindgen's `--library` mode reads
19+
# UNIFFI_META_* symbols from the compiled .so to discover the interface, and
20+
# stripping at compile time removes them. Strip the copies that ship in the
21+
# Android AAR explicitly (e.g. with `llvm-strip`) after bindings have been
22+
# generated — see cktap-android/scripts/release/build-release-*.sh.

cktap-android/README.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# cktap-android
2+
3+
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].
4+
5+
## How it works
6+
7+
`cktap-android` is a thin Android packaging layer on top of the Rust implementation. The stack looks like this:
8+
9+
```
10+
Android app (Kotlin)
11+
└─ cktap-android Kotlin bindings + prebuilt native libs, shipped as an .aar
12+
└─ cktap-ffi Rust crate that exports the UniFFI scaffolding (cdylib → libcktap_ffi.so)
13+
└─ rust-cktap Pure-Rust implementation of the Coinkite Tap Protocol (SATSCARD, TAPSIGNER, SATSCHIP)
14+
```
15+
16+
What the build produces:
17+
18+
- **`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>/`.
19+
- **`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/`.
20+
- **`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.
21+
22+
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.
23+
24+
## How to Use
25+
26+
Once published to Maven Central, add the following to your Android project's `build.gradle.kts`:
27+
28+
```kotlin
29+
repositories {
30+
mavenCentral()
31+
}
32+
33+
dependencies {
34+
implementation("org.bitcoindevkit:cktap-android:<version>")
35+
}
36+
```
37+
38+
### Snapshot releases
39+
40+
To use a snapshot release, add the snapshot repository:
41+
42+
```kotlin
43+
repositories {
44+
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
45+
}
46+
47+
dependencies {
48+
implementation("org.bitcoindevkit:cktap-android:<version>-SNAPSHOT")
49+
}
50+
```
51+
52+
### Local Maven (`~/.m2/repository`)
53+
54+
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:
55+
56+
```kotlin
57+
repositories {
58+
mavenLocal()
59+
mavenCentral()
60+
}
61+
62+
dependencies {
63+
implementation("org.bitcoindevkit:cktap-android:0.1.0-SNAPSHOT")
64+
}
65+
```
66+
67+
See [How to publish to your local Maven repository](#how-to-publish-to-your-local-maven-repository) below for the publish flow.
68+
69+
## How to build locally
70+
71+
_Note: Kotlin `2.3.10`+, JDK 17, Android SDK API 34+, and Android NDK `27.2.12479018`+ are required._
72+
73+
1. Clone this repository:
74+
```shell
75+
git clone https://github.com/notmandatory/rust-cktap
76+
```
77+
78+
2. Set up environment variables for Android SDK and NDK:
79+
```shell
80+
# macOS
81+
export ANDROID_SDK_ROOT=~/Library/Android/sdk
82+
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/27.2.12479018
83+
```
84+
85+
3. Build the native libraries and Kotlin bindings:
86+
```shell
87+
cd cktap-android
88+
just build macos-aarch64
89+
```
90+
91+
4. (Optional) Run instrumented tests on a connected emulator/device:
92+
```shell
93+
just test
94+
```
95+
96+
## How to publish to your local Maven repository
97+
98+
This flow works **without any signing keys or Sonatype credentials** and is ideal for local development and testing.
99+
100+
### Prerequisites
101+
102+
- [Rust toolchain](https://rustup.rs/) (`cargo`, `rustup`)
103+
- [`just`](https://github.com/casey/just) (`brew install just` on macOS)
104+
- JDK 17
105+
- Android SDK API 34+ and Android NDK `27.2.12479018`+ (install via Android Studio → SDK Manager → SDK Tools → NDK)
106+
- `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`
107+
108+
### Steps
109+
110+
1. Export the SDK/NDK environment variables (adjust the NDK version to match what is installed):
111+
112+
```shell
113+
export ANDROID_SDK_ROOT=~/Library/Android/sdk
114+
export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/27.2.12479018
115+
```
116+
117+
2. Build the Rust library and generate the Kotlin bindings for your host ABI:
118+
119+
```shell
120+
cd cktap-android
121+
just build macos-aarch64
122+
```
123+
124+
This compiles `libcktap_ffi.so` for `arm64-v8a` and regenerates `lib/src/main/kotlin/com/coinkite/cktap/cktap_ffi.kt` via UniFFI.
125+
126+
3. Publish the AAR to your local Maven repository:
127+
128+
```shell
129+
just publish-local # alias for: ./gradlew publishToMavenLocal -P localBuild
130+
```
131+
132+
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/`.
133+
134+
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.
135+
136+
### Consuming the local build
137+
138+
```kotlin
139+
repositories {
140+
mavenLocal()
141+
mavenCentral()
142+
}
143+
144+
dependencies {
145+
implementation("org.bitcoindevkit:cktap-android:0.1.0-SNAPSHOT")
146+
}
147+
```
148+
149+
## How to publish to Maven Central (official release)
150+
151+
This flow **requires signing keys and Sonatype credentials**. Configure them in `~/.gradle/gradle.properties` (outside the repo):
152+
153+
```properties
154+
# Sonatype Central Publisher Portal tokens
155+
mavenCentralUsername=<TOKEN_USERNAME>
156+
mavenCentralPassword=<TOKEN_PASSWORD>
157+
158+
# GPG signing
159+
signing.gnupg.keyName=<GPG_KEY_ID>
160+
signing.gnupg.passphrase=<GPG_PASSPHRASE>
161+
```
162+
163+
Then:
164+
165+
```shell
166+
# 1. Update version in lib/build.gradle.kts (remove -SNAPSHOT)
167+
# 2. Build native libraries for all supported ABIs
168+
just build macos-aarch64
169+
170+
# 3. Publish
171+
just publish-central # alias for: ./gradlew publishAndReleaseToMavenCentral
172+
173+
# 4. Tag and push
174+
git tag v0.1.0 && git push --tags
175+
```
176+
177+
## Known issues
178+
179+
### JNA dependency
180+
181+
Depending on the JVM version used by your consumer project, JNA may not be on the classpath. If you see:
182+
183+
```
184+
class file for com.sun.jna.Pointer not found
185+
```
186+
187+
Add JNA explicitly:
188+
189+
```kotlin
190+
dependencies {
191+
implementation("net.java.dev.jna:jna:5.14.0@aar")
192+
}
193+
```
194+
195+
### x86 emulators
196+
197+
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.
198+
199+
[rust-cktap]: https://github.com/notmandatory/rust-cktap
200+
[uniffi-rs]: https://github.com/mozilla/uniffi-rs

cktap-android/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
plugins {
2+
id("com.android.library").version("8.13.2").apply(false)
3+
id("org.jetbrains.kotlin.android").version("2.3.10").apply(false)
4+
id("org.jetbrains.dokka").version("2.1.0").apply(false)
5+
id("com.vanniktech.maven.publish").version("0.36.0").apply(false)
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Module cktap-android
2+
3+
The [rust-cktap](https://github.com/notmandatory/rust-cktap) language bindings library for Kotlin on Android.
4+
5+
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.
6+
7+
# Package org.bitcoindevkit.cktap

cktap-android/gradle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
org.gradle.jvmargs=-Xmx1536m
2+
android.useAndroidX=true
3+
android.enableJetifier=true
4+
kotlin.code.style=official
5+
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
6+
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
42.4 KB
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)