From b27971bcde3bed44589b1527da38746242741db8 Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:08:27 -0400 Subject: [PATCH 1/6] feat(kotlin-sdk): add maven-publish for the release AAR Publishes org.dashfoundation:dash-sdk-android with sources jar and POM (Room/DataStore/Biometric api deps carried). publishToMavenLocal works today; a remote repository block can be added when a hosting decision is made. Co-Authored-By: Claude Fable 5 --- packages/kotlin-sdk/sdk/build.gradle.kts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/kotlin-sdk/sdk/build.gradle.kts b/packages/kotlin-sdk/sdk/build.gradle.kts index 4a97e0c515e..590dacd96a4 100644 --- a/packages/kotlin-sdk/sdk/build.gradle.kts +++ b/packages/kotlin-sdk/sdk/build.gradle.kts @@ -3,8 +3,12 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) + `maven-publish` } +group = "org.dashfoundation" +version = project.findProperty("sdkVersion")?.toString() ?: "0.1.0-SNAPSHOT" + android { namespace = "org.dashfoundation.dashsdk" compileSdk = 35 @@ -35,6 +39,12 @@ android { // libdash_sdk_jni.so is produced by ../build_android.sh (cargo-ndk) into // src/main/jniLibs// — AGP packages it from there. NDK r28 emits // 16 KB-aligned ELF segments by default (Android 15+ requirement). + publishing { + singleVariant("release") { + withSourcesJar() + } + } + packaging { jniLibs { useLegacyPackaging = false @@ -83,3 +93,33 @@ dependencies { androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.room.testing) } + +// Publishes the release AAR (incl. libdash_sdk_jni.so if ../build_android.sh ran first) with a +// POM carrying the Room/DataStore/Biometric api dependencies. +// Local: ./gradlew :sdk:publishToMavenLocal Coordinates: org.dashfoundation:dash-sdk-android: +// Override the version with -PsdkVersion=x.y.z (defaults to 0.1.0-SNAPSHOT). +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + groupId = "org.dashfoundation" + artifactId = "dash-sdk-android" + pom { + name.set("Dash Platform Kotlin SDK") + description.set( + "Kotlin SDK for Dash Core (L1 SPV) and Dash Platform " + + "(identities, DPNS, DashPay, shielded balances)" + ) + url.set("https://github.com/dashpay/platform") + licenses { + license { + name.set("MIT License") + url.set("https://github.com/dashpay/platform/blob/master/LICENSE") + } + } + } + } + } + } +} From 3d05a4f61e1848f3c43b0e10e0e3cc65967a99e6 Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:06:13 -0400 Subject: [PATCH 2/6] fix(kotlin-sdk): guard maven publish against missing JNI; coroutines api; POM scm/developers Address review of the release-AAR maven-publish. - Fail the publish tasks (verifyJniLibsPresent, wired to publishToMavenLocal / publishToMavenRepository) when src/main/jniLibs//libdash_sdk_jni.so is absent for the arm64-v8a / x86_64 ABI policy build_android.sh produces. That directory is gitignored and empty in a clean checkout, so without this a publish could ship a coordinate that installs but dies at runtime with UnsatisfiedLinkError. Escape hatch: -PallowMissingJni warns and continues (metadata-only dry run). - Move kotlinx-coroutines-core from implementation to api: the public SDK surface returns Flow/StateFlow (Room DAOs, PlatformWalletManager.pendingIdentityKeys), so consumers of the coordinate need it on their compile classpath. - Add POM scm + developers sections (required for Maven Central / remote POM validation), pointed at github.com/dashpay/platform rather than a vanity domain, plus a note that the org.dashfoundation groupId can't go to Maven Central until the matching domain is owned (per HashEngineering). Co-Authored-By: Claude Fable 5 --- packages/kotlin-sdk/sdk/build.gradle.kts | 62 +++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/kotlin-sdk/sdk/build.gradle.kts b/packages/kotlin-sdk/sdk/build.gradle.kts index 590dacd96a4..ab33f5e1cfd 100644 --- a/packages/kotlin-sdk/sdk/build.gradle.kts +++ b/packages/kotlin-sdk/sdk/build.gradle.kts @@ -1,3 +1,6 @@ +import org.gradle.api.publish.maven.tasks.PublishToMavenLocal +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -6,6 +9,11 @@ plugins { `maven-publish` } +// NOTE (Maven Central, future): the `org.dashfoundation` groupId can't be +// published to Maven Central yet — that requires owning the matching domain, +// and dashfoundation.org is held by a third party (HashEngineering, PR #4045). +// This coordinate is for `publishToMavenLocal` / an internal repo for now; a +// Maven Central move is a separate groupId/hosting decision. group = "org.dashfoundation" version = project.findProperty("sdkVersion")?.toString() ?: "0.1.0-SNAPSHOT" @@ -69,7 +77,11 @@ ksp { } dependencies { - implementation(libs.kotlinx.coroutines.core) + // `api`, not `implementation`: the public SDK surface returns coroutines + // types (Room DAO `Flow<…>`, `PlatformWalletManager.pendingIdentityKeys: + // StateFlow<…>`, etc.), so a consumer of the published coordinate needs + // coroutines-core on its own compile classpath. + api(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) @@ -118,8 +130,56 @@ afterEvaluate { url.set("https://github.com/dashpay/platform/blob/master/LICENSE") } } + // scm + developers: required by Maven Central (and most + // remote repos) for POM validation. Pointed at the GitHub + // repo rather than a vanity domain (see the group-id note + // above re: dashfoundation.org ownership). + scm { + url.set("https://github.com/dashpay/platform") + connection.set("scm:git:git://github.com/dashpay/platform.git") + developerConnection.set( + "scm:git:ssh://git@github.com/dashpay/platform.git" + ) + } + developers { + developer { + id.set("dashpay") + name.set("Dash Platform Contributors") + url.set("https://github.com/dashpay/platform") + } + } } } } } } + +// Never publish a coordinate whose AAR lacks the native JNI library +// (libdash_sdk_jni.so). ../build_android.sh (cargo-ndk) produces it into +// src/main/jniLibs//; that directory is gitignored and absent in a clean +// checkout, so without this guard a publish from a fresh tree yields an artifact +// that installs cleanly but dies at runtime with UnsatisfiedLinkError +// (NativeLoader.ensureLoaded → System.loadLibrary("dash_sdk_jni")). Run +// ../build_android.sh first, or pass -PallowMissingJni to publish anyway (e.g. a +// POM/metadata-only dry run). +val requiredJniAbis = listOf("arm64-v8a", "x86_64") // build_android.sh ABI policy +val verifyJniLibsPresent = tasks.register("verifyJniLibsPresent") { + doLast { + val missing = requiredJniAbis.filterNot { abi -> + file("src/main/jniLibs/$abi/libdash_sdk_jni.so").isFile + } + if (missing.isNotEmpty()) { + val message = "Native JNI library libdash_sdk_jni.so is missing for ABI(s) " + + "${missing.joinToString()} under src/main/jniLibs/ — run ./build_android.sh " + + "before publishing so the coordinate isn't broken at runtime." + if (project.hasProperty("allowMissingJni")) { + logger.warn("WARNING: $message (continuing anyway: -PallowMissingJni is set)") + } else { + throw GradleException("$message Pass -PallowMissingJni to override.") + } + } + } +} + +tasks.withType().configureEach { dependsOn(verifyJniLibsPresent) } +tasks.withType().configureEach { dependsOn(verifyJniLibsPresent) } From b0b8d6196020d38c0f6c492ea8d05bf051c53210 Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:39:30 -0400 Subject: [PATCH 3/6] feat(kotlin-sdk): publish under org.dashj; add jreleaser Maven Central/Sonatype deploy Per HashEngineering's decision on PR #4045, switch the published Maven coordinate group from org.dashfoundation to org.dashj. Dash already owns dashj.org and ships its legacy Core/Platform SDKs there, so the org.dashj group needs no separate domain verification for Maven Central; the org.dashfoundation group would have (dashfoundation.org is third-party-held). This is a coordinates-only change: the Kotlin source packages stay org.dashfoundation.dashsdk, and the artifactId stays dash-sdk-android. Add a jreleaser deploy config mirroring dashj/core/build.gradle: maven-publish stages the signed artifacts into build/staging-deploy, and jreleaserDeploy uploads release versions to Maven Central (Sonatype portal) and -SNAPSHOT versions to the Sonatype snapshots repo. Signing + Sonatype credentials are read from jreleaser env/props at deploy time only. maven-publish still drives publishToMavenLocal / internal-repo publishing. The gradle signing plugin is guarded to require a key only when a real PublishToMavenRepository task is in the graph, so publishToMavenLocal and plain assembles need no GPG setup. Validated: `./gradlew :sdk:publishToMavenLocal` lands org.dashj:dash-sdk-android:0.1.0-SNAPSHOT under ~/.m2/repository/org/dashj/ with signReleasePublication SKIPPED. Co-Authored-By: Claude Fable 5 --- packages/kotlin-sdk/gradle/libs.versions.toml | 2 + packages/kotlin-sdk/sdk/build.gradle.kts | 111 ++++++++++++++++-- 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/packages/kotlin-sdk/gradle/libs.versions.toml b/packages/kotlin-sdk/gradle/libs.versions.toml index 67d6cace340..5bbdaaa7a3e 100644 --- a/packages/kotlin-sdk/gradle/libs.versions.toml +++ b/packages/kotlin-sdk/gradle/libs.versions.toml @@ -20,6 +20,7 @@ androidxTestExtJunit = "1.2.1" androidxTestRunner = "1.6.2" espresso = "3.6.1" robolectric = "4.14.1" +jreleaser = "1.17.0" [libraries] kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } @@ -66,3 +67,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" } diff --git a/packages/kotlin-sdk/sdk/build.gradle.kts b/packages/kotlin-sdk/sdk/build.gradle.kts index ab33f5e1cfd..91fdc088582 100644 --- a/packages/kotlin-sdk/sdk/build.gradle.kts +++ b/packages/kotlin-sdk/sdk/build.gradle.kts @@ -1,5 +1,7 @@ +import java.util.concurrent.Callable import org.gradle.api.publish.maven.tasks.PublishToMavenLocal import org.gradle.api.publish.maven.tasks.PublishToMavenRepository +import org.jreleaser.model.Active plugins { alias(libs.plugins.android.library) @@ -7,14 +9,20 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) `maven-publish` + id("signing") + alias(libs.plugins.jreleaser) } -// NOTE (Maven Central, future): the `org.dashfoundation` groupId can't be -// published to Maven Central yet — that requires owning the matching domain, -// and dashfoundation.org is held by a third party (HashEngineering, PR #4045). -// This coordinate is for `publishToMavenLocal` / an internal repo for now; a -// Maven Central move is a separate groupId/hosting decision. -group = "org.dashfoundation" +// Published Maven coordinate group. Per HashEngineering's call on PR #4045: +// publish under the existing `org.dashj` group — Dash already owns dashj.org and +// ships its legacy Core/Platform SDKs there, so this needs no separate domain +// verification for Maven Central (`org.dashfoundation` would have, since +// dashfoundation.org is held by a third party). The Kotlin source packages stay +// `org.dashfoundation.dashsdk`; this is a Maven-coordinates decision only. +// `maven-publish` handles local / internal-repo publishing; jreleaser (bottom of +// file) handles Maven Central releases and Sonatype SNAPSHOTs, mirroring +// dashj/core/build.gradle. +group = "org.dashj" version = project.findProperty("sdkVersion")?.toString() ?: "0.1.0-SNAPSHOT" android { @@ -108,14 +116,25 @@ dependencies { // Publishes the release AAR (incl. libdash_sdk_jni.so if ../build_android.sh ran first) with a // POM carrying the Room/DataStore/Biometric api dependencies. -// Local: ./gradlew :sdk:publishToMavenLocal Coordinates: org.dashfoundation:dash-sdk-android: +// Local: ./gradlew :sdk:publishToMavenLocal Coordinates: org.dashj:dash-sdk-android: +// Remote: `:sdk:publishReleasePublicationToStagingRepository` stages the signed +// artifacts into build/staging-deploy, then `:sdk:jreleaserDeploy` uploads +// them to Maven Central (release) / Sonatype snapshots (see jreleaser block). // Override the version with -PsdkVersion=x.y.z (defaults to 0.1.0-SNAPSHOT). afterEvaluate { publishing { + repositories { + // Local staging dir that jreleaser reads from and uploads (below); it is + // not itself a network repo, so staging never needs credentials. + maven { + name = "staging" + url = uri(layout.buildDirectory.dir("staging-deploy")) + } + } publications { create("release") { from(components["release"]) - groupId = "org.dashfoundation" + groupId = "org.dashj" artifactId = "dash-sdk-android" pom { name.set("Dash Platform Kotlin SDK") @@ -131,9 +150,7 @@ afterEvaluate { } } // scm + developers: required by Maven Central (and most - // remote repos) for POM validation. Pointed at the GitHub - // repo rather than a vanity domain (see the group-id note - // above re: dashfoundation.org ownership). + // remote repos) for POM validation. Pointed at the GitHub repo. scm { url.set("https://github.com/dashpay/platform") connection.set("scm:git:git://github.com/dashpay/platform.git") @@ -152,6 +169,18 @@ afterEvaluate { } } } + + // Sign published artifacts, but only for a real remote publish. `publishToMavenLocal` + // and a bare `assemble` never put a PublishToMavenRepository task in the graph, so a + // local build requires no GPG key. Keys come from the standard signing properties/env + // (signing.keyId + signing.password + signing.secretKeyRingFile, or the in-memory + // ORG_GRADLE_PROJECT_signingKey / signingPassword pair) when a remote publish runs. + signing { + setRequired(Callable { + gradle.taskGraph.allTasks.any { it is PublishToMavenRepository } + }) + sign(publishing.publications["release"]) + } } // Never publish a coordinate whose AAR lacks the native JNI library @@ -183,3 +212,63 @@ val verifyJniLibsPresent = tasks.register("verifyJniLibsPresent") { tasks.withType().configureEach { dependsOn(verifyJniLibsPresent) } tasks.withType().configureEach { dependsOn(verifyJniLibsPresent) } + +// Maven Central / Sonatype deployment via jreleaser, mirroring +// dashj/core/build.gradle (https://github.com/dashpay/dashj/blob/master/core/build.gradle#L139). +// Flow: `maven-publish` stages the signed artifacts into build/staging-deploy (the +// `staging` repository configured above), then `./gradlew :sdk:jreleaserDeploy` +// uploads them — release versions to Maven Central, -SNAPSHOT versions to the +// Sonatype snapshots repo. jreleaser reads its GPG key and Sonatype credentials +// from env/props (JRELEASER_GPG_*, JRELEASER_MAVENCENTRAL_*/JRELEASER_NEXUS2_*) +// at deploy time only, so `publishToMavenLocal` and any build that never invokes a +// jreleaser task need no signing key or account. +jreleaser { + project { + name.set("dash-sdk-android") + description.set( + "Kotlin SDK for Dash Core (L1 SPV) and Dash Platform " + + "(identities, DPNS, DashPay, shielded balances)" + ) + links { + homepage.set("https://github.com/dashpay/platform") + } + authors.set(listOf("Dash Platform Contributors")) + license.set("MIT") + gitRootSearch.set(true) + } + + signing { + active.set(Active.ALWAYS) + armored.set(true) + } + + deploy { + maven { + // Tagged release versions -> Maven Central via the Sonatype portal. + mavenCentral { + create("sonatype") { + active.set(Active.RELEASE) + url.set("https://central.sonatype.com/api/v1/publisher") + stagingRepository( + layout.buildDirectory.dir("staging-deploy").get().asFile.path + ) + } + } + // -SNAPSHOT versions -> Sonatype snapshots repository. + nexus2 { + create("snapshots") { + active.set(Active.SNAPSHOT) + url.set("https://central.sonatype.com/repository/maven-snapshots/") + snapshotUrl.set("https://central.sonatype.com/repository/maven-snapshots/") + applyMavenCentralRules.set(true) + snapshotSupported.set(true) + closeRepository.set(true) + releaseRepository.set(true) + stagingRepository( + layout.buildDirectory.dir("staging-deploy").get().asFile.path + ) + } + } + } + } +} From fcaa91fa14868aebcbb275d4fded7f0332dbacdf Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Sat, 11 Jul 2026 23:10:58 -0400 Subject: [PATCH 4/6] fix(kotlin-sdk): javadoc jar for Central; JNI guard hard-fails remote; wire in-memory signing keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the latest #4045 review round. - Missing javadoc jar (blocking 3acc85ab85d4): singleVariant("release") now calls withJavadocJar() in addition to withSourcesJar(). Maven Central's Publisher Portal rejects any non-POM component without a javadoc jar, and the snapshot deployer's applyMavenCentralRules(true) enforces it before upload. AGP emits a Kotlin-appropriate javadoc jar — no Dokka dependency added. - JNI-less AAR guard (blocking 8b899bc2e5e8): -PallowMissingJni previously downgraded verifyJniLibsPresent to a warning for EVERY publish task, so it could push a nativeless AAR to a remote repo / jreleaser's staging dir. Split into verifyJniLibsForRemotePublish (ALWAYS hard-fails; flag not honored — wired to every PublishToMavenRepository and to jreleaserDeploy/Upload/Release) and verifyJniLibsForLocalPublish (honors the flag; wired to PublishToMavenLocal). Verified end-to-end: remote task fails with -PallowMissingJni; local warns. - In-memory signing keys (blocking d251acfba63b, from last round): the signing block documented ORG_GRADLE_PROJECT_signingKey/signingPassword but never called useInMemoryPgpKeys, so a CI release on that path failed in signReleasePublication. Now reads the signingKey/signingPassword properties and calls useInMemoryPgpKeys when present; absent (local dev) it's skipped and required=false needs no key. - kotlinx-serialization-json scope (suggestion 96b7b2a236ff): Sdk.MasternodeEntry is a wire DTO touched only by a private envelope and internal parse — never returned by a public function. Marked it `internal` so its generated serializer isn't public API; the dependency correctly stays `implementation` (cleaner than promoting it to `api`, the reviewer's own preferred fix). Validated: :sdk:publishToMavenLocal lands sources + javadoc jars under ~/.m2/repository/org/dashj/; :sdk unit suite green. Co-Authored-By: Claude Fable 5 --- packages/kotlin-sdk/sdk/build.gradle.kts | 83 ++++++++++++++++--- .../kotlin/org/dashfoundation/dashsdk/Sdk.kt | 10 ++- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/packages/kotlin-sdk/sdk/build.gradle.kts b/packages/kotlin-sdk/sdk/build.gradle.kts index 91fdc088582..f9e93719741 100644 --- a/packages/kotlin-sdk/sdk/build.gradle.kts +++ b/packages/kotlin-sdk/sdk/build.gradle.kts @@ -58,6 +58,12 @@ android { publishing { singleVariant("release") { withSourcesJar() + // Maven Central's Publisher Portal rejects any non-POM component that + // lacks a javadoc jar, and the snapshot deployer sets + // applyMavenCentralRules(true) which enforces it before upload + // (dashpay/platform#4045). AGP emits a (Kotlin-appropriate) javadoc jar + // here — no Dokka dependency required. + withJavadocJar() } } @@ -176,6 +182,19 @@ afterEvaluate { // (signing.keyId + signing.password + signing.secretKeyRingFile, or the in-memory // ORG_GRADLE_PROJECT_signingKey / signingPassword pair) when a remote publish runs. signing { + // Wire the documented in-memory-key path: the ORG_GRADLE_PROJECT_signingKey + // / signingPassword env vars land as the Gradle project properties + // `signingKey` / `signingPassword`, but the signing plugin does not consume + // them automatically — a CI release following that path would otherwise fail + // in signReleasePublication before staging anything (dashpay/platform#4045). + // Absent (local dev), this is skipped and, with required=false below, no key + // is needed. The legacy signing.keyId/password/secretKeyRingFile path is + // untouched and still handled by Gradle's standard signing properties. + val signingKey = project.findProperty("signingKey") as String? + val signingPassword = project.findProperty("signingPassword") as String? + if (!signingKey.isNullOrBlank()) { + useInMemoryPgpKeys(signingKey, signingPassword) + } setRequired(Callable { gradle.taskGraph.allTasks.any { it is PublishToMavenRepository } }) @@ -188,30 +207,68 @@ afterEvaluate { // src/main/jniLibs//; that directory is gitignored and absent in a clean // checkout, so without this guard a publish from a fresh tree yields an artifact // that installs cleanly but dies at runtime with UnsatisfiedLinkError -// (NativeLoader.ensureLoaded → System.loadLibrary("dash_sdk_jni")). Run -// ../build_android.sh first, or pass -PallowMissingJni to publish anyway (e.g. a -// POM/metadata-only dry run). +// (NativeLoader.ensureLoaded → System.loadLibrary("dash_sdk_jni")). val requiredJniAbis = listOf("arm64-v8a", "x86_64") // build_android.sh ABI policy -val verifyJniLibsPresent = tasks.register("verifyJniLibsPresent") { + +fun missingJniAbis(): List = requiredJniAbis.filterNot { abi -> + file("src/main/jniLibs/$abi/libdash_sdk_jni.so").isFile +} + +fun jniMissingMessage(missing: List): String = + "Native JNI library libdash_sdk_jni.so is missing for ABI(s) " + + "${missing.joinToString()} under src/main/jniLibs/ — run ./build_android.sh " + + "before publishing so the coordinate isn't broken at runtime." + +// Remote / staging publish: ALWAYS hard-fail on a missing .so. `-PallowMissingJni` +// is deliberately NOT honored here — a nativeless AAR must never reach a network +// repo, nor jreleaser's build/staging-deploy dir (which `jreleaserDeploy` then +// uploads to Maven Central). This closes the escape where `-PallowMissingJni` +// previously downgraded the check to a warning for every publish task +// (dashpay/platform#4045, finding 8b899bc2e5e8). +val verifyJniLibsForRemotePublish = tasks.register("verifyJniLibsForRemotePublish") { doLast { - val missing = requiredJniAbis.filterNot { abi -> - file("src/main/jniLibs/$abi/libdash_sdk_jni.so").isFile + val missing = missingJniAbis() + if (missing.isNotEmpty()) { + throw GradleException( + jniMissingMessage(missing) + + " -PallowMissingJni is a local-only escape and is NOT honored for " + + "remote/staging deploys." + ) } + } +} + +// Local publish (`publishToMavenLocal`): a dev may pass `-PallowMissingJni` for a +// POM/metadata-only dry run into the local ~/.m2, which never leaves the machine. +val verifyJniLibsForLocalPublish = tasks.register("verifyJniLibsForLocalPublish") { + doLast { + val missing = missingJniAbis() if (missing.isNotEmpty()) { - val message = "Native JNI library libdash_sdk_jni.so is missing for ABI(s) " + - "${missing.joinToString()} under src/main/jniLibs/ — run ./build_android.sh " + - "before publishing so the coordinate isn't broken at runtime." + val message = jniMissingMessage(missing) if (project.hasProperty("allowMissingJni")) { - logger.warn("WARNING: $message (continuing anyway: -PallowMissingJni is set)") + logger.warn( + "WARNING: $message (continuing: -PallowMissingJni is set — local publish only.)" + ) } else { - throw GradleException("$message Pass -PallowMissingJni to override.") + throw GradleException("$message Pass -PallowMissingJni for a local-only dry run.") } } } } -tasks.withType().configureEach { dependsOn(verifyJniLibsPresent) } -tasks.withType().configureEach { dependsOn(verifyJniLibsPresent) } +tasks.withType().configureEach { + dependsOn(verifyJniLibsForRemotePublish) +} +tasks.withType().configureEach { + dependsOn(verifyJniLibsForLocalPublish) +} +// jreleaser's deploy/upload/release tasks push the already-staged artifacts, so +// gate them on the strict remote check too — a staging dir populated out-of-band +// (or by a future task rewiring) can't be uploaded without the native library. +tasks.matching { task -> + task.name.startsWith("jreleaser") && + listOf("Deploy", "Upload", "Release").any { task.name.contains(it) } +}.configureEach { dependsOn(verifyJniLibsForRemotePublish) } // Maven Central / Sonatype deployment via jreleaser, mirroring // dashj/core/build.gradle (https://github.com/dashpay/dashj/blob/master/core/build.gradle#L139). diff --git a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/Sdk.kt b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/Sdk.kt index 07900fa976a..9c8063a73ce 100644 --- a/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/Sdk.kt +++ b/packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/Sdk.kt @@ -124,8 +124,16 @@ class Sdk private constructor( * `discoverActiveMasternodes`) — a single strict field would fail the * whole devnet discovery. */ + // `internal`, not public: this is a wire-format DTO for the `/masternodes` + // response, touched only by the `private` [MasternodesEnvelope] and + // `internal` [parseActiveMasternodes] — never returned by a public function + // (public discovery returns the non-@Serializable [ActiveMasternode]). Keeping + // it out of the public ABI means its generated `serializer()` isn't public + // API, so consumers of the published coordinate don't need + // kotlinx-serialization on their compile classpath and it can stay + // `implementation` (dashpay/platform#4045, finding 96b7b2a236ff). @Serializable - data class MasternodeEntry( + internal data class MasternodeEntry( val address: String, val status: String = "", val platformHTTPPort: Int? = null, From a2e94fc8c77ddcf9e72dc20ebde85156f9678c1a Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:42:25 -0400 Subject: [PATCH 5/6] docs(kotlin-sdk): PUBLISHING.md runbook for the org.dashj Maven Central release One-page post-merge runbook now that hosting is settled on Maven Central under org.dashj (dashpay/platform#4045): prerequisites (Central Portal token for org.dashj, GPG key, NDK/cargo-ndk toolchain), the env-var-only credential wiring (ORG_GRADLE_PROJECT_signingKey/Password for the staging publish, JRELEASER_MAVENCENTRAL_SONATYPE_* / JRELEASER_NEXUS2_SNAPSHOTS_* / JRELEASER_GPG_* for the deploy), the three release commands in order (build_android.sh --verify -> staged publish -> jreleaserDeploy) with -PsdkVersion for real releases (default stays 0.1.0-SNAPSHOT), what the verifyJniLibsForRemotePublish guard refuses and why, and the consumer coordinate org.dashj:dash-sdk-android:. Co-Authored-By: Claude Fable 5 --- packages/kotlin-sdk/PUBLISHING.md | 84 +++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/kotlin-sdk/PUBLISHING.md diff --git a/packages/kotlin-sdk/PUBLISHING.md b/packages/kotlin-sdk/PUBLISHING.md new file mode 100644 index 00000000000..4d93bc8a0e4 --- /dev/null +++ b/packages/kotlin-sdk/PUBLISHING.md @@ -0,0 +1,84 @@ +# Publishing `org.dashj:dash-sdk-android` to Maven Central + +The SDK publishes under the existing **`org.dashj`** namespace (same as +`dashj-core`), so no new Central namespace verification is needed. This is a +Maven-coordinates decision only — the Kotlin source packages remain +`org.dashfoundation.dashsdk`. Deployment uses **JReleaser**, mirroring +[dashj/core/build.gradle](https://github.com/dashpay/dashj/blob/master/core/build.gradle): +release versions go to Maven Central via the Central Portal; `-SNAPSHOT` +versions go to the Sonatype snapshots repository. + +## Prerequisites + +- **Sonatype Central Portal token** for the `org.dashj` namespace (held by + HashEngineering — the same account that publishes `dashj-core`). +- **GPG signing key** (the dashj release key works), with the public key on the + keyservers. +- **Native toolchain**: Android NDK r28+ (`28.1.13356709`), `cargo-ndk`, rustup + targets `aarch64-linux-android` and `x86_64-linux-android`, JDK 17. + +## Credentials — environment only, never committed + +Gradle signing (signs artifacts during the staging publish; the classic +`signing.keyId` / `signing.password` / `signing.secretKeyRingFile` entries in +`~/.gradle/gradle.properties`, as used for dashj-core, also work): + +```bash +export ORG_GRADLE_PROJECT_signingKey="$(cat private-key.asc)" # ASCII-armored +export ORG_GRADLE_PROJECT_signingPassword="…" +``` + +JReleaser deploy (read only when a `jreleaser*` task runs): + +```bash +export JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME="…" # Central Portal token name +export JRELEASER_MAVENCENTRAL_SONATYPE_PASSWORD="…" # Central Portal token value +export JRELEASER_NEXUS2_SNAPSHOTS_USERNAME="…" # same token (snapshots repo) +export JRELEASER_NEXUS2_SNAPSHOTS_PASSWORD="…" +export JRELEASER_GPG_PUBLIC_KEY="$(cat public-key.asc)" +export JRELEASER_GPG_SECRET_KEY="$(cat private-key.asc)" +export JRELEASER_GPG_PASSPHRASE="…" +``` + +## Release steps (from `packages/kotlin-sdk/`) + +```bash +# 1. Build the native JNI library (arm64-v8a + x86_64) and verify its exports. +./build_android.sh --verify + +# 2. Stage the signed release AAR + sources/javadoc jars into sdk/build/staging-deploy. +./gradlew :sdk:publishReleasePublicationToStagingRepository -PsdkVersion=0.1.0 + +# 3. Upload the staged artifacts. Release versions -> Maven Central; +# -SNAPSHOT versions -> Sonatype snapshots. +./gradlew :sdk:jreleaserDeploy -PsdkVersion=0.1.0 +``` + +Then approve/verify the deployment at and +confirm the artifact appears on search.maven.org (sync takes ~30 min). + +**Versioning:** the default is `0.1.0-SNAPSHOT`; pass the same +`-PsdkVersion=x.y.z` to steps 2 and 3. The first release should be a real +version (e.g. `0.1.0`) — the Android wallet's CI must depend on a pinned +release, never a snapshot. + +## The JNI guard + +`sdk/src/main/jniLibs/` is gitignored, so a clean checkout has no +`libdash_sdk_jni.so` — and an AAR published without it installs fine but dies +at runtime with `UnsatisfiedLinkError`. The `verifyJniLibsForRemotePublish` +task therefore **hard-fails** every staging/remote/jreleaser publish task when +the `.so` is missing for `arm64-v8a` or `x86_64`. There is no override for +remote publishes; `-PallowMissingJni` only downgrades the check for +`publishToMavenLocal` (metadata-only dry runs into `~/.m2`). If the guard +fires, run step 1. + +## Consuming the artifact + +```kotlin +repositories { mavenCentral() } +dependencies { implementation("org.dashj:dash-sdk-android:0.1.0") } +``` + +Requires `minSdk 29`. The AAR bundles the native library for `arm64-v8a` and +`x86_64`; `armeabi-v7a` is intentionally unsupported. From ffeeb8294d7652d1c8991426713b75349def9288 Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:09:51 -0400 Subject: [PATCH 6/6] fix(kotlin-sdk): wipe staging-deploy before every staging publish maven-publish adds/replaces the current coordinates but never removes other versions' directories from sdk/build/staging-deploy, so a release deploy run after an earlier snapshot/release staging could hand JReleaser stale coordinates. cleanStagingDeploy now runs before every *ToStagingRepository publish (verified in the task graph via --dry-run); PUBLISHING.md notes it. Co-Authored-By: Claude Fable 5 --- packages/kotlin-sdk/PUBLISHING.md | 3 +++ packages/kotlin-sdk/sdk/build.gradle.kts | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/kotlin-sdk/PUBLISHING.md b/packages/kotlin-sdk/PUBLISHING.md index 4d93bc8a0e4..caa3c6cdc33 100644 --- a/packages/kotlin-sdk/PUBLISHING.md +++ b/packages/kotlin-sdk/PUBLISHING.md @@ -48,6 +48,9 @@ export JRELEASER_GPG_PASSPHRASE="…" # 2. Stage the signed release AAR + sources/javadoc jars into sdk/build/staging-deploy. ./gradlew :sdk:publishReleasePublicationToStagingRepository -PsdkVersion=0.1.0 +# (the staging dir sdk/build/staging-deploy is wiped automatically before every +# staging publish — cleanStagingDeploy — so stale versions from earlier runs +# can never reach JReleaser) # 3. Upload the staged artifacts. Release versions -> Maven Central; # -SNAPSHOT versions -> Sonatype snapshots. diff --git a/packages/kotlin-sdk/sdk/build.gradle.kts b/packages/kotlin-sdk/sdk/build.gradle.kts index f9e93719741..bdf8cf33a36 100644 --- a/packages/kotlin-sdk/sdk/build.gradle.kts +++ b/packages/kotlin-sdk/sdk/build.gradle.kts @@ -137,6 +137,19 @@ afterEvaluate { url = uri(layout.buildDirectory.dir("staging-deploy")) } } + + // The staging dir is reused across runs and maven-publish never removes + // other versions' directories — a release deploy after an earlier + // snapshot/release staging run would hand JReleaser stale coordinates. + // Wipe it before every staging publish so each deploy starts clean. + val cleanStagingDeploy = tasks.register("cleanStagingDeploy") { + delete(layout.buildDirectory.dir("staging-deploy")) + } + tasks.withType().configureEach { + if (name.endsWith("ToStagingRepository")) { + dependsOn(cleanStagingDeploy) + } + } publications { create("release") { from(components["release"])