From 85de8691c13efaf1beded4e464c023e2c94bdd37 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 3 Jun 2026 21:55:36 +0200 Subject: [PATCH 01/22] chore: retain native debug symbols --- Justfile | 8 ++++ app/build.gradle.kts | 2 +- .../bitkit/build/NativeReleaseConfigTest.kt | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt diff --git a/Justfile b/Justfile index 1d282ff82c..a27f96eed9 100644 --- a/Justfile +++ b/Justfile @@ -173,7 +173,15 @@ build task="assembleDevDebug": {{ gradle }} {{ task }} release: + #!/usr/bin/env sh + set -eu + symbols_dir="app/build/outputs/native-debug-symbols/mainnetRelease" + rm -f "$symbols_dir"/native-debug-symbols*.zip NDK_VERSION={{ ndk_ver }} {{ gradle }} assembleMainnetRelease bundleMainnetRelease + NDK_VERSION={{ ndk_ver }} {{ gradle }} :app:syncNativeDebugSymbolArtifacts + scripts/create-native-debug-symbols.sh + symbols="$(find "$symbols_dir" -maxdepth 1 -name 'native-debug-symbols-*.zip' -type f | sort | tail -n 1)" + echo "Attach this exact file to GitHub releases, upload it to Play Console for this release, and verify Play lists it: $symbols" install: {{ gradle }} installDevDebug diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 13af5ccbb7..079272e6a9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -232,7 +232,7 @@ android { ) signingConfig = signingConfigs.getByName("release") ndk { - debugSymbolLevel = "SYMBOL_TABLE" + debugSymbolLevel = "FULL" // noinspection ChromeOsAbiSupport abiFilters += listOf("armeabi-v7a", "arm64-v8a") } diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt new file mode 100644 index 0000000000..db775db8b2 --- /dev/null +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -0,0 +1,37 @@ +package to.bitkit.build + +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.test.Test +import kotlin.test.assertTrue + +class NativeReleaseConfigTest { + + private val repoRoot = generateSequence( + Path(requireNotNull(System.getProperty("user.dir")) { "user.dir is required" }), + ) { it.parent } + .first { it.resolve("gradle/libs.versions.toml").exists() } + + @Test + fun `release build keeps full native debug symbols`() { + val buildFile = repoRoot.resolve("app/build.gradle.kts").readText() + + assertTrue( + buildFile.contains("""debugSymbolLevel = "FULL""""), + "Release builds must keep full native debug symbols for Play crash symbolication.", + ) + } + + @Test + fun `release recipe verifies native debug symbols archive`() { + val justfile = repoRoot.resolve("Justfile").readText() + + assertTrue( + justfile.contains( + """symbols="app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip"""", + ), + "Release builds must verify the native debug symbols archive before publishing.", + ) + } +} From b2d83b423ecc1236cc306fdd91cebcb2b74c447a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 15:41:20 +0200 Subject: [PATCH 02/22] docs: clarify release symbols --- .agents/commands/release.md | 73 ++++--------------- README.md | 14 ++++ .../bitkit/build/NativeReleaseConfigTest.kt | 20 +++++ 3 files changed, 50 insertions(+), 57 deletions(-) diff --git a/.agents/commands/release.md b/.agents/commands/release.md index da2fe615ba..e39f686fa1 100644 --- a/.agents/commands/release.md +++ b/.agents/commands/release.md @@ -1,5 +1,5 @@ --- -description: "Create a new release: bump version, create PR, run release workflow, tag, draft release" +description: "Create a new release: bump version, create PR, build mainnet, tag, draft release" allowed_tools: Bash, Read, Edit, Write, Glob, Grep, AskUserQuestion, mcp__github__create_pull_request, mcp__github__list_pull_requests, mcp__github__pull_request_read, mcp__github__get_file_contents, mcp__github__update_pull_request --- @@ -77,8 +77,6 @@ Cherry-pick the commits you need onto this branch now, then continue. ``` Wait for the user to confirm they are done cherry-picking before proceeding. -If the base is a tag that predates the release workflow changes, port the current release workflow support onto the release branch before proceeding. At minimum, the release branch/tag must contain the updated artifact naming in `.github/workflows/release.yml`, otherwise Step 7 will dispatch an old workflow and then look for an artifact name it cannot produce. - Finalize changelog after the release branch contains all release commits: ```bash @@ -204,66 +202,28 @@ gh release edit v{newVersionName} --notes-file /tmp/release-notes.md Print the path to the release notes file so the user can share it for review. -### 7. Run Store Release Workflow +### 7. Build Mainnet Release ```bash -release_ref="v{newVersionName}" -release_artifact_dir=".ai/release-artifacts-{newVersionName}" -if ! git show "$release_ref:.github/workflows/release.yml" | grep -q 'bitkit-release-$build_number-$GITHUB_RUN_NUMBER'; then - echo "Release ref $release_ref does not contain the current release artifact naming." >&2 - echo "Port the release workflow changes onto the release branch, retag, then rerun /release." >&2 - exit 1 -fi -dispatch_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" -gh workflow run release.yml --ref "$release_ref" -run_id="" -for attempt in {1..30}; do - run_id="$(gh run list \ - --workflow release.yml \ - --branch "$release_ref" \ - --event workflow_dispatch \ - --created ">=$dispatch_started_at" \ - --limit 1 \ - --json databaseId \ - --jq '.[0].databaseId // empty')" - if [ -n "$run_id" ]; then - break - fi - sleep 5 -done -if [ -z "$run_id" ]; then - echo "Failed to find release workflow run for $release_ref" >&2 - exit 1 -fi -gh run watch "$run_id" --exit-status -workflow_run_url="$(gh run view "$run_id" --json url --jq .url)" -run_number="$(gh run view "$run_id" --json number --jq .number)" -rm -rf "$release_artifact_dir" -mkdir -p "$release_artifact_dir" -gh run download "$run_id" \ - --name "bitkit-release-{newVersionCode}-${run_number}" \ - --dir "$release_artifact_dir" -if command -v sha256sum >/dev/null; then - (cd "$release_artifact_dir" && sha256sum -c SHA256SUMS.txt) -else - (cd "$release_artifact_dir" && shasum -a 256 -c SHA256SUMS.txt) -fi +just release ``` -Expected APK path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk` -Expected AAB path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}.aab` - -Verify both files exist. If the workflow fails or the artifact checksum verification fails, stop and report the error to the user. +Expected APK path: `app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk` +Expected AAB path: `app/build/outputs/bundle/mainnetRelease/bitkit-mainnet-release-{newVersionCode}.aab` +Expected native debug symbols path: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip` -Store `workflow_run_url` for the summary. +Verify all three files exist. The native debug symbols file must be from the same `just release` build as the APK/AAB. Keep the build-numbered filename, e.g. `native-debug-symbols-{newVersionCode}.zip`, so it matches the APK/AAB build number. `just release` resolves upstream native debug symbol artifacts from the Rust dependency packages, merges them into the final archive, and refuses placeholder symbols from stripped packaged `.so` files. -### 8. Upload Workflow APK to Draft Release +### 8. Upload APK and Native Symbols to Draft Release ```bash gh release upload v{newVersionName} \ - .ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk + app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk \ + app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip ``` +For the Play Store release, upload the AAB as usual, then upload `native-debug-symbols-{newVersionCode}.zip` for the exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. Keep the release-built archive in GitHub releases or internal release storage; Play Console may only show delete/replace controls after upload, which is enough for release verification. + ### 9. Return to Master ```bash @@ -279,16 +239,15 @@ Version bump PR: {PR URL} Release branch: release-{newVersionName} Tag: v{newVersionName} Draft release: {release URL} -Release workflow: {workflow run URL} -Artifacts: .ai/release-artifacts-{newVersionName} APK uploaded: bitkit-mainnet-release-{newVersionCode}-universal.apk +Native debug symbols uploaded: native-debug-symbols-{newVersionCode}.zip Store release notes: .ai/release-notes-{newVersionName}.md Next steps: - Share release notes with Jacobo for review -- QA the workflow-built APK -- Submit the workflow-built AAB to Play Store when QA passes -- If patching the release branch: increment only versionCode, re-tag, rerun the release workflow, and re-upload +- QA the APK +- If patching the release branch: increment only versionCode, re-tag, rebuild, and re-upload +- Submit to Play Store when QA passes - Publish the draft release on GitHub after store release - Merge release branch PR into master ``` diff --git a/README.md b/README.md index 8995692502..ab1133d903 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,20 @@ To build the mainnet flavor for release run: just release ``` +`just release` builds the mainnet APK, Play Store AAB, and the native debug symbols archive. + +Release artifacts: + +- APK: `app/build/outputs/apk/mainnet/release/` +- AAB: `app/build/outputs/bundle/mainnetRelease/` +- Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip` + +The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. + +For Play Store releases, upload the AAB as usual and verify Play Console shows native debug symbols for that exact version/build in App bundle explorer. If Play did not pick them up from the AAB, manually upload `native-debug-symbols.zip`. + +For GitHub releases, attach `native-debug-symbols.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. + #### Android App Bundle (AAB) `just release` builds both the mainnet APK and Play Store AAB. AAB is generated in `app/build/outputs/bundle/mainnetRelease/`. diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index db775db8b2..fe3930fbcc 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -33,5 +33,25 @@ class NativeReleaseConfigTest { ), "Release builds must verify the native debug symbols archive before publishing.", ) + assertTrue( + justfile.contains("Attach this exact file to GitHub releases"), + "Release builds must tell the releaser to attach native debug symbols.", + ) + } + + @Test + fun `release command uploads native debug symbols archive`() { + val releaseCommand = repoRoot.resolve(".agents/commands/release.md").readText() + + assertTrue( + releaseCommand.contains( + "app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip", + ), + "Release command must include the native debug symbols archive path.", + ) + assertTrue( + releaseCommand.contains("Native debug symbols uploaded: native-debug-symbols.zip"), + "Release command summary must report the native debug symbols archive.", + ) } } From e6e0d56aafc57b587e172ad8e79db4a248fde831 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 15:53:23 +0200 Subject: [PATCH 03/22] fix: create native symbols zip --- README.md | 2 +- .../bitkit/build/NativeReleaseConfigTest.kt | 32 +++++++-- scripts/create-native-debug-symbols.sh | 67 +++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100755 scripts/create-native-debug-symbols.sh diff --git a/README.md b/README.md index ab1133d903..765a332469 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Release artifacts: - AAB: `app/build/outputs/bundle/mainnetRelease/` - Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip` -The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. +The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit this archive because native dependency metadata is already stripped, `just release` creates it from the exact merged release `.so` files. For Play Store releases, upload the AAB as usual and verify Play Console shows native debug symbols for that exact version/build in App bundle explorer. If Play did not pick them up from the AAB, manually upload `native-debug-symbols.zip`. diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index fe3930fbcc..368cd0eec7 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -14,12 +14,12 @@ class NativeReleaseConfigTest { .first { it.resolve("gradle/libs.versions.toml").exists() } @Test - fun `release build keeps full native debug symbols`() { + fun `release build requests full native debug symbols`() { val buildFile = repoRoot.resolve("app/build.gradle.kts").readText() assertTrue( buildFile.contains("""debugSymbolLevel = "FULL""""), - "Release builds must keep full native debug symbols for Play crash symbolication.", + "Release builds must request full native debug symbols for Play crash symbolication.", ) } @@ -29,9 +29,13 @@ class NativeReleaseConfigTest { assertTrue( justfile.contains( - """symbols="app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip"""", + """rm -f "${'$'}symbols"""", ), - "Release builds must verify the native debug symbols archive before publishing.", + "Release builds must remove stale native debug symbols before rebuilding.", + ) + assertTrue( + justfile.contains("scripts/create-native-debug-symbols.sh"), + "Release builds must create the native debug symbols archive before publishing.", ) assertTrue( justfile.contains("Attach this exact file to GitHub releases"), @@ -54,4 +58,24 @@ class NativeReleaseConfigTest { "Release command summary must report the native debug symbols archive.", ) } + + @Test + fun `native debug symbols script archives release libraries`() { + val symbolsScript = repoRoot.resolve("scripts/create-native-debug-symbols.sh").readText() + + assertTrue( + symbolsScript.contains( + "app/build/outputs/native-debug-symbols/${'$'}variant/native-debug-symbols.zip", + ), + "Native debug symbols script must write the canonical archive path.", + ) + assertTrue( + symbolsScript.contains("arm64-v8a armeabi-v7a"), + "Native debug symbols script must archive Play release ABIs.", + ) + assertTrue( + symbolsScript.contains("zip -qr"), + "Native debug symbols script must create a zip archive.", + ) + } } diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh new file mode 100755 index 0000000000..bf35a0cec0 --- /dev/null +++ b/scripts/create-native-debug-symbols.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env sh +set -eu + +script_dir=$(cd "$(dirname "$0")" && pwd) +repo_root=$(cd "$script_dir/.." && pwd) +cd "$repo_root" + +variant="mainnetRelease" +output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" +output_dir=$(dirname "$output") + +if [ -f "$output" ]; then + zip -T "$output" >/dev/null + echo "Native debug symbols: $output" + ls -lh "$output" + exit 0 +fi + +native_lib_dir="" +for candidate in "app/build/intermediates/merged_native_libs/$variant"/*/out/lib; do + if [ -d "$candidate" ]; then + native_lib_dir="$candidate" + break + fi +done + +if [ -z "$native_lib_dir" ]; then + echo "No merged native libraries found for '$variant'." >&2 + exit 1 +fi + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +for abi in arm64-v8a armeabi-v7a; do + source_dir="$native_lib_dir/$abi" + if [ ! -d "$source_dir" ]; then + echo "Missing native libraries for '$abi' in '$native_lib_dir'." >&2 + exit 1 + fi + + mkdir -p "$tmp_dir/$abi" + found_lib=false + for lib in "$source_dir"/*.so; do + if [ -f "$lib" ]; then + cp "$lib" "$tmp_dir/$abi/" + found_lib=true + fi + done + + if [ "$found_lib" = false ]; then + echo "No native libraries found for '$abi' in '$source_dir'." >&2 + exit 1 + fi +done + +mkdir -p "$output_dir" +rm -f "$output" + +( + cd "$tmp_dir" + zip -qr "$repo_root/$output" arm64-v8a armeabi-v7a +) + +zip -T "$output" >/dev/null +echo "Native debug symbols: $output" +ls -lh "$output" From ad025ac522b4b4439b4e1c006af4e489f4263363 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 18:48:04 +0200 Subject: [PATCH 04/22] docs: clarify play symbol upload --- README.md | 4 +++- .../java/to/bitkit/build/NativeReleaseConfigTest.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 765a332469..8d6da61437 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,9 @@ Release artifacts: The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit this archive because native dependency metadata is already stripped, `just release` creates it from the exact merged release `.so` files. -For Play Store releases, upload the AAB as usual and verify Play Console shows native debug symbols for that exact version/build in App bundle explorer. If Play did not pick them up from the AAB, manually upload `native-debug-symbols.zip`. +For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play Console lists the native debug symbols after upload. + +Do not rely on Play Console to download or recover `native-debug-symbols.zip` later. Keep the release-built archive in GitHub releases or internal release storage. For GitHub releases, attach `native-debug-symbols.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 368cd0eec7..63228e04c5 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -4,6 +4,7 @@ import kotlin.io.path.Path import kotlin.io.path.exists import kotlin.io.path.readText import kotlin.test.Test +import kotlin.test.assertFalse import kotlin.test.assertTrue class NativeReleaseConfigTest { @@ -41,6 +42,14 @@ class NativeReleaseConfigTest { justfile.contains("Attach this exact file to GitHub releases"), "Release builds must tell the releaser to attach native debug symbols.", ) + assertTrue( + justfile.contains("upload it to Play Console for this release"), + "Release builds must tell the releaser to upload native debug symbols to Play.", + ) + assertFalse( + justfile.contains("verify Play Console"), + "Release builds must not imply Play is the source of native debug symbols.", + ) } @Test @@ -57,6 +66,10 @@ class NativeReleaseConfigTest { releaseCommand.contains("Native debug symbols uploaded: native-debug-symbols.zip"), "Release command summary must report the native debug symbols archive.", ) + assertFalse( + releaseCommand.contains("Play " + "did not"), + "Release command must not use stale Play native symbol wording.", + ) } @Test From fddafd40636e062ddcfdf137b96358b7ba8a6abd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 18:57:54 +0200 Subject: [PATCH 05/22] fix: reject stripped native symbols --- README.md | 8 +- .../bitkit/build/NativeReleaseConfigTest.kt | 24 ++++- scripts/create-native-debug-symbols.sh | 97 ++++++++++++++++++- 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8d6da61437..3e9e4fc449 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ To build the mainnet flavor for release run: just release ``` -`just release` builds the mainnet APK, Play Store AAB, and the native debug symbols archive. +`just release` builds the mainnet APK, Play Store AAB, and validates the native debug symbols archive. Release artifacts: @@ -188,11 +188,11 @@ Release artifacts: - AAB: `app/build/outputs/bundle/mainnetRelease/` - Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip` -The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit this archive because native dependency metadata is already stripped, `just release` creates it from the exact merged release `.so` files. +The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit a usable archive because native dependency metadata is already stripped, `just release` fails instead of creating a placeholder zip from stripped `.so` files. Stop the release and publish or consume native dependencies with usable debug metadata first. -For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play Console lists the native debug symbols after upload. +For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. -Do not rely on Play Console to download or recover `native-debug-symbols.zip` later. Keep the release-built archive in GitHub releases or internal release storage. +Do not rely on Play Console to download or recover `native-debug-symbols.zip` later. If Play only shows delete/replace controls for an uploaded symbol file, that is enough for release verification. Keep the release-built archive in GitHub releases or internal release storage. For GitHub releases, attach `native-debug-symbols.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 63228e04c5..8782545365 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -47,7 +47,7 @@ class NativeReleaseConfigTest { "Release builds must tell the releaser to upload native debug symbols to Play.", ) assertFalse( - justfile.contains("verify Play Console"), + justfile.contains("download"), "Release builds must not imply Play is the source of native debug symbols.", ) } @@ -70,10 +70,18 @@ class NativeReleaseConfigTest { releaseCommand.contains("Play " + "did not"), "Release command must not use stale Play native symbol wording.", ) + assertTrue( + releaseCommand.contains("fails instead of creating a placeholder zip from stripped `.so` files"), + "Release command must fail instead of publishing fake native debug symbols.", + ) + assertTrue( + releaseCommand.contains("If Play only shows delete/replace controls"), + "Release command must document the verified Play Console behavior.", + ) } @Test - fun `native debug symbols script archives release libraries`() { + fun `native debug symbols script rejects stripped release libraries`() { val symbolsScript = repoRoot.resolve("scripts/create-native-debug-symbols.sh").readText() assertTrue( @@ -90,5 +98,17 @@ class NativeReleaseConfigTest { symbolsScript.contains("zip -qr"), "Native debug symbols script must create a zip archive.", ) + assertTrue( + symbolsScript.contains("""required_libs="libbitkitcore.so libldk_node.so""""), + "Native debug symbols script must validate crash-relevant native libraries.", + ) + assertTrue( + symbolsScript.contains("""grep -Eq '\.(symtab|debug_|gnu_debugdata)'"""), + "Native debug symbols script must validate usable debug metadata before zipping.", + ) + assertTrue( + symbolsScript.contains("Refusing to create '${'$'}output' from stripped native libraries."), + "Native debug symbols script must refuse placeholder archives.", + ) } } diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index bf35a0cec0..f1b722cbe5 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -8,9 +8,99 @@ cd "$repo_root" variant="mainnetRelease" output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" output_dir=$(dirname "$output") +required_libs="libbitkitcore.so libldk_node.so" + +tmp_dirs="" +cleanup() { + for dir in $tmp_dirs; do + rm -rf "$dir" + done +} +trap cleanup EXIT + +make_tmp_dir() { + dir=$(mktemp -d) + tmp_dirs="$tmp_dirs $dir" + echo "$dir" +} + +find_readelf() { + for sdk_dir in "${ANDROID_NDK_HOME:-}" "${ANDROID_HOME:-}/ndk" "${ANDROID_SDK_ROOT:-}/ndk"; do + if [ -z "$sdk_dir" ]; then + continue + fi + + for candidate in \ + "$sdk_dir"/toolchains/llvm/prebuilt/*/bin/llvm-readelf \ + "$sdk_dir"/*/toolchains/llvm/prebuilt/*/bin/llvm-readelf; do + if [ -x "$candidate" ]; then + echo "$candidate" + return + fi + done + done + + if command -v llvm-readelf >/dev/null 2>&1; then + command -v llvm-readelf + return + fi + + if command -v readelf >/dev/null 2>&1; then + command -v readelf + return + fi + + echo "llvm-readelf or readelf is required to validate native debug symbols." >&2 + exit 1 +} + +readelf_bin=$(find_readelf) + +has_debug_metadata() { + "$readelf_bin" -S "$1" | grep -Eq '\.(symtab|debug_|gnu_debugdata)' +} + +validate_symbol_tree() { + root="$1" + + for abi in arm64-v8a armeabi-v7a; do + for lib_name in $required_libs; do + lib="$root/$abi/$lib_name" + if [ ! -f "$lib" ]; then + echo "Missing required native symbol library '$abi/$lib_name'." >&2 + exit 1 + fi + + if ! has_debug_metadata "$lib"; then + echo "Native debug symbols unavailable: '$abi/$lib_name' has no .symtab, .debug_*, or .gnu_debugdata sections." >&2 + echo "Refusing to create '$output' from stripped native libraries." >&2 + echo "Publish or consume native dependencies with usable debug metadata before releasing." >&2 + exit 1 + fi + done + done +} + +validate_output_zip() { + archive="$1" + zip -T "$archive" >/dev/null + + tmp_dir=$(make_tmp_dir) + for abi in arm64-v8a armeabi-v7a; do + for lib_name in $required_libs; do + entry="$abi/$lib_name" + if ! unzip -q "$archive" "$entry" -d "$tmp_dir"; then + echo "Native debug symbols archive is missing '$entry'." >&2 + exit 1 + fi + done + done + + validate_symbol_tree "$tmp_dir" +} if [ -f "$output" ]; then - zip -T "$output" >/dev/null + validate_output_zip "$output" echo "Native debug symbols: $output" ls -lh "$output" exit 0 @@ -29,8 +119,7 @@ if [ -z "$native_lib_dir" ]; then exit 1 fi -tmp_dir=$(mktemp -d) -trap 'rm -rf "$tmp_dir"' EXIT +tmp_dir=$(make_tmp_dir) for abi in arm64-v8a armeabi-v7a; do source_dir="$native_lib_dir/$abi" @@ -54,6 +143,8 @@ for abi in arm64-v8a armeabi-v7a; do fi done +validate_symbol_tree "$tmp_dir" + mkdir -p "$output_dir" rm -f "$output" From 6126e43444c458f751c9e6407ffc96283b564b1d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 20:13:29 +0200 Subject: [PATCH 06/22] fix: accept agp symbol entries --- .../bitkit/build/NativeReleaseConfigTest.kt | 4 +++ scripts/create-native-debug-symbols.sh | 30 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 8782545365..48e7ceb4cf 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -102,6 +102,10 @@ class NativeReleaseConfigTest { symbolsScript.contains("""required_libs="libbitkitcore.so libldk_node.so""""), "Native debug symbols script must validate crash-relevant native libraries.", ) + assertTrue( + symbolsScript.contains("""archive_symbol_suffixes=".dbg .sym""""), + "Native debug symbols script must accept AGP native debug symbol entry suffixes.", + ) assertTrue( symbolsScript.contains("""grep -Eq '\.(symtab|debug_|gnu_debugdata)'"""), "Native debug symbols script must validate usable debug metadata before zipping.", diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index f1b722cbe5..5079102e3a 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -9,6 +9,7 @@ variant="mainnetRelease" output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" output_dir=$(dirname "$output") required_libs="libbitkitcore.so libldk_node.so" +archive_symbol_suffixes=".dbg .sym" tmp_dirs="" cleanup() { @@ -81,6 +82,29 @@ validate_symbol_tree() { done } +extract_archive_lib() { + archive="$1" + tmp_dir="$2" + abi="$3" + lib_name="$4" + + entry="$abi/$lib_name" + if unzip -q "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + return + fi + + for suffix in $archive_symbol_suffixes; do + entry="$abi/$lib_name$suffix" + if unzip -q "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + mv "$tmp_dir/$entry" "$tmp_dir/$abi/$lib_name" + return + fi + done + + echo "Native debug symbols archive is missing '$abi/$lib_name' or accepted AGP variants '$abi/$lib_name.dbg' / '$abi/$lib_name.sym'." >&2 + exit 1 +} + validate_output_zip() { archive="$1" zip -T "$archive" >/dev/null @@ -88,11 +112,7 @@ validate_output_zip() { tmp_dir=$(make_tmp_dir) for abi in arm64-v8a armeabi-v7a; do for lib_name in $required_libs; do - entry="$abi/$lib_name" - if ! unzip -q "$archive" "$entry" -d "$tmp_dir"; then - echo "Native debug symbols archive is missing '$entry'." >&2 - exit 1 - fi + extract_archive_lib "$archive" "$tmp_dir" "$abi" "$lib_name" done done From e9481f410adae570041e4cde15d60a645c46ea2b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 20:19:18 +0200 Subject: [PATCH 07/22] docs: clean release wording --- README.md | 2 +- .../test/java/to/bitkit/build/NativeReleaseConfigTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3e9e4fc449..4607e5a6e4 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ The native debug symbols archive must come from the same `just release` build as For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. -Do not rely on Play Console to download or recover `native-debug-symbols.zip` later. If Play only shows delete/replace controls for an uploaded symbol file, that is enough for release verification. Keep the release-built archive in GitHub releases or internal release storage. +Keep the release-built `native-debug-symbols.zip` in GitHub releases or internal release storage. Play Console may only show delete/replace controls after upload, which is enough for release verification. For GitHub releases, attach `native-debug-symbols.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 48e7ceb4cf..cf9b4bee3e 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -48,7 +48,7 @@ class NativeReleaseConfigTest { ) assertFalse( justfile.contains("download"), - "Release builds must not imply Play is the source of native debug symbols.", + "Release builds should keep native debug symbols in release storage.", ) } @@ -68,14 +68,14 @@ class NativeReleaseConfigTest { ) assertFalse( releaseCommand.contains("Play " + "did not"), - "Release command must not use stale Play native symbol wording.", + "Release command should use current Play native symbol wording.", ) assertTrue( releaseCommand.contains("fails instead of creating a placeholder zip from stripped `.so` files"), "Release command must fail instead of publishing fake native debug symbols.", ) assertTrue( - releaseCommand.contains("If Play only shows delete/replace controls"), + releaseCommand.contains("Play Console may only show delete/replace controls"), "Release command must document the verified Play Console behavior.", ) } From 849860c2b023a6aad5137e4a3bc3bf8ea9c3cec0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 4 Jun 2026 22:46:27 +0200 Subject: [PATCH 08/22] fix: gate vss native symbols --- app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt | 4 ++-- scripts/create-native-debug-symbols.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index cf9b4bee3e..089e1bfdb0 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -99,8 +99,8 @@ class NativeReleaseConfigTest { "Native debug symbols script must create a zip archive.", ) assertTrue( - symbolsScript.contains("""required_libs="libbitkitcore.so libldk_node.so""""), - "Native debug symbols script must validate crash-relevant native libraries.", + symbolsScript.contains("""required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so""""), + "Native debug symbols script must validate Rust native libraries.", ) assertTrue( symbolsScript.contains("""archive_symbol_suffixes=".dbg .sym""""), diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index 5079102e3a..f1ac99b2d8 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -8,7 +8,7 @@ cd "$repo_root" variant="mainnetRelease" output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" output_dir=$(dirname "$output") -required_libs="libbitkitcore.so libldk_node.so" +required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so" archive_symbol_suffixes=".dbg .sym" tmp_dirs="" From bb459622c12382ec2229dc659139fd9be437c25e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 5 Jun 2026 01:21:08 +0200 Subject: [PATCH 09/22] fix: use native symbol releases --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 287c726f9e..946c7f6c78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,8 +21,8 @@ activity-compose = { module = "androidx.activity:activity-compose", version = "1 appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } -bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.72" } -paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc8" } +bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.73" } +paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc18" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } @@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.46" } +ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.49" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } @@ -88,7 +88,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.12" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.18" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } From 69852f72dbbd043e81b14945ea025a7b7cc1fa40 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 5 Jun 2026 20:28:09 +0200 Subject: [PATCH 10/22] fix: consume native symbol packages --- gradle/libs.versions.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 946c7f6c78..30b329ada3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,7 +88,11 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } +<<<<<<< HEAD vss-client = { module = "com.synonym:vss-client-android", version = "0.5.18" } +======= +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.17" } +>>>>>>> 8f40ef543 (fix: consume native symbol packages) work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } From 641ee5bf23bbe1250067c935d4880e5ec0c44c01 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 12 Jun 2026 01:05:32 +0200 Subject: [PATCH 11/22] fix: include paykit native symbols --- .../bitkit/repositories/PrivatePaykitRepo.kt | 20 +++++-- .../java/to/bitkit/repositories/PubkyRepo.kt | 6 +- .../bitkit/repositories/PublicPaykitRepo.kt | 4 +- .../java/to/bitkit/services/PubkyService.kt | 26 ++++----- .../bitkit/build/NativeReleaseConfigTest.kt | 14 +++-- .../repositories/PrivatePaykitRepoTest.kt | 58 ++++++++++--------- .../to/bitkit/repositories/PubkyRepoTest.kt | 8 +-- .../repositories/PublicPaykitRepoTest.kt | 8 +-- scripts/create-native-debug-symbols.sh | 12 ++-- 9 files changed, 87 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt b/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt index ff828e8e91..2884534465 100644 --- a/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt @@ -1,7 +1,7 @@ package to.bitkit.repositories import com.synonym.bitkitcore.Scanner -import com.synonym.paykit.FfiPaymentEntry +import com.synonym.paykit.FfiPaymentEndpoint import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,6 +47,16 @@ private data class PrivatePaymentAttempt( val shouldDeferPublicFallback: Boolean, ) +private fun StoredPaymentEntry.toFfiPaymentEndpoint() = FfiPaymentEndpoint( + paymentEndpointIdentifier = methodId, + paymentEndpointPayload = endpointData, +) + +private fun FfiPaymentEndpoint.toStoredPaymentEntry() = StoredPaymentEntry( + methodId = paymentEndpointIdentifier, + endpointData = paymentEndpointPayload, +) + @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) @Singleton @Suppress("TooManyFunctions", "LongParameterList", "LargeClass") @@ -955,7 +965,7 @@ class PrivatePaykitRepo @Inject constructor( ensureCurrentGeneration(generation) if (!canPublishPrivateEndpoints() || knownSavedContact(publicKey) == null) return@withLock - pubkyService.setPrivatePayments(linkId, entries.map { FfiPaymentEntry(it.methodId, it.endpointData) }) + pubkyService.setPrivatePayments(linkId, entries.map { it.toFfiPaymentEndpoint() }) ensureCurrentGeneration(generation) persistLinkSnapshot(linkId, publicKey, linkWasReplaced = false, generation = generation).getOrThrow() contactState.lastLocalPayloadHash = payloadHash @@ -1100,9 +1110,9 @@ class PrivatePaykitRepo @Inject constructor( ensureCurrentGeneration(generation) if (remotePayload == null) return@runCatching 0 - val remoteEntries = remotePayload.entries + val remoteEntries = remotePayload.paymentEndpoints val contactState = ensureState().contacts.getOrPut(publicKey) { ContactState() } - contactState.remoteEndpoints = remoteEntries.map { StoredPaymentEntry(it.methodId, it.endpointData) } + contactState.remoteEndpoints = remoteEntries.map { it.toStoredPaymentEntry() } persistState(markWalletBackup = true) remoteEntries.count() } @@ -1614,7 +1624,7 @@ class PrivatePaykitRepo @Inject constructor( PrivatePaykitPayloads.validateNoisePayload(entries) pubkyService.setPrivatePayments( linkId, - entries.map { FfiPaymentEntry(it.methodId, it.endpointData) }, + entries.map { it.toFfiPaymentEndpoint() }, ) ensureCurrentGeneration(generation) ensureState().contacts[publicKey]?.lastLocalPayloadHash = null diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt index 48b61bdeb2..f1120b3063 100644 --- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt @@ -3,7 +3,7 @@ package to.bitkit.repositories import android.graphics.Bitmap import android.graphics.BitmapFactory import coil3.ImageLoader -import com.synonym.paykit.FfiPaymentEntry +import com.synonym.paykit.FfiPaymentEndpoint import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.post @@ -389,7 +389,7 @@ class PubkyRepo @Inject constructor( // region Payment endpoints - suspend fun getPaymentList(publicKey: String): Result> = withContext(ioDispatcher) { + suspend fun getPaymentList(publicKey: String): Result> = withContext(ioDispatcher) { runCatching { pubkyService.getPaymentList(publicKey.ensurePubkyPrefix()) } @@ -417,7 +417,7 @@ class PubkyRepo @Inject constructor( .toSet() pubkyService.getPaymentList(currentPublicKey) - .map { it.methodId } + .map { it.paymentEndpointIdentifier } .filter { it in managedMethodIds } .forEach { pubkyService.removePaymentEndpoint(it) } } diff --git a/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt b/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt index 3e7e41ff0c..4a71a96ffe 100644 --- a/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt @@ -220,7 +220,7 @@ class PublicPaykitRepo @Inject constructor( runCatching { val normalizedKey = PubkyPublicKeyFormat.normalized(publicKey) ?: publicKey pubkyRepo.getPaymentList(normalizedKey).getOrThrow() - .mapNotNull { parseEndpoint(it.methodId, it.endpointData) } + .mapNotNull { parseEndpoint(it.paymentEndpointIdentifier, it.paymentEndpointPayload) } .associateBy { it.methodId } .values .sortedBy { endpoint -> payablePreferenceOrder.indexOf(endpoint.methodId) } @@ -255,7 +255,7 @@ class PublicPaykitRepo @Inject constructor( private suspend fun currentPublishedMethodIds(): Set { return pubkyRepo.getPaymentList(requireCurrentPublicKey()).getOrThrow() - .map { it.methodId } + .map { it.paymentEndpointIdentifier } .toSet() } diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt index d7dcbedf6b..55d0a22f47 100644 --- a/app/src/main/java/to/bitkit/services/PubkyService.kt +++ b/app/src/main/java/to/bitkit/services/PubkyService.kt @@ -20,8 +20,8 @@ import com.synonym.bitkitcore.pubkySignIn import com.synonym.bitkitcore.pubkySignUp import com.synonym.bitkitcore.startPubkyAuth import com.synonym.paykit.FfiHandshakeProgress -import com.synonym.paykit.FfiPaymentEntry -import com.synonym.paykit.FfiPrivatePaymentsPayload +import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPrivatePaymentList import com.synonym.paykit.PaykitAndroid import com.synonym.paykit.paykitAcceptEncryptedLink import com.synonym.paykit.paykitAdvanceHandshake @@ -31,22 +31,22 @@ import com.synonym.paykit.paykitEncryptedLinkHandshakeSnapshotRecipient import com.synonym.paykit.paykitEncryptedLinkSnapshotRecipient import com.synonym.paykit.paykitExportSession import com.synonym.paykit.paykitForceSignOut -import com.synonym.paykit.paykitGeneratePaymentReference import com.synonym.paykit.paykitGetCurrentPublicKey import com.synonym.paykit.paykitGetPaymentEndpoint import com.synonym.paykit.paykitGetPaymentList -import com.synonym.paykit.paykitGetPrivatePayments import com.synonym.paykit.paykitImportSession import com.synonym.paykit.paykitInitialize import com.synonym.paykit.paykitInitiateEncryptedLink import com.synonym.paykit.paykitIsAuthenticated +import com.synonym.paykit.paykitParsePrivatePaymentListJson +import com.synonym.paykit.paykitReceivePrivateApplicationMessages import com.synonym.paykit.paykitRemovePaymentEndpoint import com.synonym.paykit.paykitRestoreEncryptedLink import com.synonym.paykit.paykitRestoreEncryptedLinkHandshake import com.synonym.paykit.paykitSerializeEncryptedLink import com.synonym.paykit.paykitSerializeEncryptedLinkHandshake import com.synonym.paykit.paykitSetPaymentEndpoint -import com.synonym.paykit.paykitSetPrivatePayments +import com.synonym.paykit.paykitSetPrivatePaymentList import com.synonym.paykit.paykitSignOut import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CompletableDeferred @@ -108,7 +108,7 @@ class PubkyService @Inject constructor( // region Payment endpoints - suspend fun getPaymentList(publicKey: String): List = ServiceQueue.CORE.background { + suspend fun getPaymentList(publicKey: String): List = ServiceQueue.CORE.background { isSetup.await() paykitGetPaymentList(publicKey) } @@ -192,19 +192,17 @@ class PubkyService @Inject constructor( paykitDropEncryptedLinkHandshake(handshakeId) } - suspend fun setPrivatePayments(linkId: String, entries: List) = + suspend fun setPrivatePayments(linkId: String, entries: List) = ServiceQueue.CORE.background { isSetup.await() - val payload = FfiPrivatePaymentsPayload( - reference = paykitGeneratePaymentReference(), - entries = entries, - ) - paykitSetPrivatePayments(linkId, payload) + paykitSetPrivatePaymentList(linkId, FfiPrivatePaymentList(paymentEndpoints = entries)) } - suspend fun getPrivatePayments(linkId: String): FfiPrivatePaymentsPayload? = ServiceQueue.CORE.background { + suspend fun getPrivatePayments(linkId: String): FfiPrivatePaymentList? = ServiceQueue.CORE.background { isSetup.await() - paykitGetPrivatePayments(linkId) + paykitReceivePrivateApplicationMessages(linkId) + .asReversed() + .firstNotNullOfOrNull { runCatching { paykitParsePrivatePaymentListJson(it.rawJson) }.getOrNull() } } // endregion diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 089e1bfdb0..c9640129f2 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -99,16 +99,22 @@ class NativeReleaseConfigTest { "Native debug symbols script must create a zip archive.", ) assertTrue( - symbolsScript.contains("""required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so""""), - "Native debug symbols script must validate Rust native libraries.", + symbolsScript.contains( + """required_libs="libbitkitcore.so libldk_node.so libpaykit.so libvss_rust_client_ffi.so"""", + ), + "Native debug symbols script must validate release-critical native libraries.", ) assertTrue( symbolsScript.contains("""archive_symbol_suffixes=".dbg .sym""""), "Native debug symbols script must accept AGP native debug symbol entry suffixes.", ) assertTrue( - symbolsScript.contains("""grep -Eq '\.(symtab|debug_|gnu_debugdata)'"""), - "Native debug symbols script must validate usable debug metadata before zipping.", + symbolsScript.contains("""grep -Eq '\.debug_info'"""), + "Native debug symbols script must validate full DWARF debug metadata before zipping.", + ) + assertFalse( + symbolsScript.contains("symtab|debug_|gnu_debugdata"), + "Native debug symbols script must not accept symbol-table-only metadata for FULL symbols.", ) assertTrue( symbolsScript.contains("Refusing to create '${'$'}output' from stripped native libraries."), diff --git a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt index cb01195c09..0ad998a9a8 100644 --- a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt @@ -4,8 +4,8 @@ import android.app.Activity import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.NetworkType import com.synonym.bitkitcore.Scanner -import com.synonym.paykit.FfiPaymentEntry -import com.synonym.paykit.FfiPrivatePaymentsPayload +import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPrivatePaymentList import com.synonym.paykit.PaykitFfiException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -403,8 +403,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - isNotEmpty() && all { it.endpointData == TOMBSTONE_PAYLOAD } + argThat> { + isNotEmpty() && all { it.paymentEndpointPayload == TOMBSTONE_PAYLOAD } }, ) verify(addressReservationRepo).clearContactAssignment(CONTACT_KEY) @@ -461,8 +461,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(publicPaykitRepo, never()).syncPublishedEndpoints(false) verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - isNotEmpty() && all { it.endpointData == TOMBSTONE_PAYLOAD } + argThat> { + isNotEmpty() && all { it.paymentEndpointPayload == TOMBSTONE_PAYLOAD } }, ) verify(addressReservationRepo).clearContactAssignment(CONTACT_KEY) @@ -671,8 +671,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).getPrivatePayments(LINK_ID) verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - any { it.endpointData == PublicPaykitRepo.serializePayload("bcrt1qprivate") } + argThat> { + any { it.paymentEndpointPayload == PublicPaykitRepo.serializePayload("bcrt1qprivate") } }, ) } @@ -819,10 +819,10 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { + argThat> { any { - it.methodId == MethodId.Bolt11.rawValue && - it.endpointData == PublicPaykitRepo.serializePayload(bolt11) + it.paymentEndpointIdentifier == MethodId.Bolt11.rawValue && + it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(bolt11) } }, ) @@ -864,11 +864,11 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - none { it.methodId == MethodId.Bolt11.rawValue } && + argThat> { + none { it.paymentEndpointIdentifier == MethodId.Bolt11.rawValue } && any { - it.methodId == MethodId.P2wpkh.rawValue && - it.endpointData == PublicPaykitRepo.serializePayload(address) + it.paymentEndpointIdentifier == MethodId.P2wpkh.rawValue && + it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(address) } }, ) @@ -1060,8 +1060,14 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { .thenReturn( privatePaymentsPayload( listOf( - FfiPaymentEntry(MethodId.Bolt11.rawValue, TOMBSTONE_PAYLOAD), - FfiPaymentEntry(MethodId.P2wpkh.rawValue, TOMBSTONE_PAYLOAD), + FfiPaymentEndpoint( + paymentEndpointIdentifier = MethodId.Bolt11.rawValue, + paymentEndpointPayload = TOMBSTONE_PAYLOAD, + ), + FfiPaymentEndpoint( + paymentEndpointIdentifier = MethodId.P2wpkh.rawValue, + paymentEndpointPayload = TOMBSTONE_PAYLOAD, + ), ), ), ) @@ -1101,9 +1107,9 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { .thenReturn( privatePaymentsPayload( listOf( - FfiPaymentEntry( - MethodId.Bolt11.rawValue, - PublicPaykitRepo.serializePayload(PRIVATE_BOLT11), + FfiPaymentEndpoint( + paymentEndpointIdentifier = MethodId.Bolt11.rawValue, + paymentEndpointPayload = PublicPaykitRepo.serializePayload(PRIVATE_BOLT11), ), ), ), @@ -1293,9 +1299,9 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { whenever(pubkyService.getPrivatePayments(retryLinkId)).thenReturn( privatePaymentsPayload( listOf( - FfiPaymentEntry( - methodId = MethodId.P2wpkh.rawValue, - endpointData = PublicPaykitRepo.serializePayload("bcrt1qprivate"), + FfiPaymentEndpoint( + paymentEndpointIdentifier = MethodId.P2wpkh.rawValue, + paymentEndpointPayload = PublicPaykitRepo.serializePayload("bcrt1qprivate"), ), ), ), @@ -1466,10 +1472,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { payeeNodeId = null, ) - private fun privatePaymentsPayload(entries: List) = FfiPrivatePaymentsPayload( - reference = "550e8400-e29b-41d4-a716-446655440000", - entries = entries, - ) + private fun privatePaymentsPayload(entries: List) = + FfiPrivatePaymentList(paymentEndpoints = entries) private fun secretStateJson( linkSnapshotHex: String? = LINK_SNAPSHOT, diff --git a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt index 63b5a9f66c..e0ede78a20 100644 --- a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt @@ -4,7 +4,7 @@ import app.cash.turbine.test import coil3.ImageLoader import coil3.disk.DiskCache import coil3.memory.MemoryCache -import com.synonym.paykit.FfiPaymentEntry +import com.synonym.paykit.FfiPaymentEndpoint import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking @@ -1209,9 +1209,9 @@ class PubkyRepoTest : BaseUnitTest() { sut.completeAuthentication() } - private fun paymentEntry(methodId: MethodId) = FfiPaymentEntry( - methodId = methodId.rawValue, - endpointData = """{"value":"value"}""", + private fun paymentEntry(methodId: MethodId) = FfiPaymentEndpoint( + paymentEndpointIdentifier = methodId.rawValue, + paymentEndpointPayload = """{"value":"value"}""", ) private suspend fun startAuthForTesting(authUri: String = "auth_uri") { diff --git a/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt index ff072126c4..ffadd9e7f0 100644 --- a/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt @@ -3,7 +3,7 @@ package to.bitkit.repositories import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.NetworkType import com.synonym.bitkitcore.Scanner -import com.synonym.paykit.FfiPaymentEntry +import com.synonym.paykit.FfiPaymentEndpoint import kotlinx.coroutines.flow.MutableStateFlow import org.junit.After import org.junit.Before @@ -446,9 +446,9 @@ class PublicPaykitRepoTest : BaseUnitTest() { clock = clock, ) - private fun paymentEntry(methodId: MethodId, value: String) = FfiPaymentEntry( - methodId = methodId.rawValue, - endpointData = """{"value":"$value"}""", + private fun paymentEntry(methodId: MethodId, value: String) = FfiPaymentEndpoint( + paymentEndpointIdentifier = methodId.rawValue, + paymentEndpointPayload = """{"value":"$value"}""", ) private fun setSettings(settings: SettingsData) { diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index f1ac99b2d8..9154a41ea6 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -8,7 +8,7 @@ cd "$repo_root" variant="mainnetRelease" output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" output_dir=$(dirname "$output") -required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so" +required_libs="libbitkitcore.so libldk_node.so libpaykit.so libvss_rust_client_ffi.so" archive_symbol_suffixes=".dbg .sym" tmp_dirs="" @@ -57,8 +57,8 @@ find_readelf() { readelf_bin=$(find_readelf) -has_debug_metadata() { - "$readelf_bin" -S "$1" | grep -Eq '\.(symtab|debug_|gnu_debugdata)' +has_dwarf_debug_metadata() { + "$readelf_bin" -S "$1" | grep -Eq '\.debug_info' } validate_symbol_tree() { @@ -72,10 +72,10 @@ validate_symbol_tree() { exit 1 fi - if ! has_debug_metadata "$lib"; then - echo "Native debug symbols unavailable: '$abi/$lib_name' has no .symtab, .debug_*, or .gnu_debugdata sections." >&2 + if ! has_dwarf_debug_metadata "$lib"; then + echo "Native debug symbols unavailable: '$abi/$lib_name' has no .debug_info DWARF metadata." >&2 echo "Refusing to create '$output' from stripped native libraries." >&2 - echo "Publish or consume native dependencies with usable debug metadata before releasing." >&2 + echo "Publish or consume native dependencies with full DWARF debug metadata before releasing." >&2 exit 1 fi done From 7b0c97326ae196cf3cbbc979edc41300959f81d4 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 15 Jun 2026 14:12:43 +0200 Subject: [PATCH 12/22] fix: measure private paykit payloads --- .../repositories/PrivatePaykitPayloads.kt | 22 ++++++------- .../repositories/PrivatePaykitRepoTest.kt | 33 +++++++++++++++++-- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt b/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt index ec22395822..07b927e009 100644 --- a/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt +++ b/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt @@ -1,5 +1,7 @@ package to.bitkit.repositories +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import to.bitkit.di.json @@ -8,8 +10,7 @@ import java.security.MessageDigest internal object PrivatePaykitPayloads { private const val MAX_NOISE_PAYLOAD_BYTES = 1000 private const val PRIVATE_ENDPOINT_REMOVAL_PAYLOAD = """{"value":""}""" - private const val PRIVATE_PAYMENTS_ENVELOPE_KIND = "paykit.private_payments" - private const val PRIVATE_PAYMENTS_REFERENCE_PLACEHOLDER = "550e8400-e29b-41d4-a716-446655440000" + private const val PRIVATE_PAYMENT_LIST_KIND = "paykit.private_payment_list" private val noisePayloadJson = Json(json) { prettyPrint = false @@ -52,23 +53,22 @@ internal object PrivatePaykitPayloads { endpoints.toSortedMap().map { StoredPaymentEntry(it.key, it.value) } private fun isNoisePayloadWithinLimit(entries: List): Boolean { - val payload = entries.associate { it.methodId to it.endpointData } - val envelope = PrivatePaymentsEnvelope( + val paymentEndpoints = entries.associate { it.methodId to it.endpointData } + val envelope = PrivatePaymentListEnvelope( version = 1, - kind = PRIVATE_PAYMENTS_ENVELOPE_KIND, - reference = PRIVATE_PAYMENTS_REFERENCE_PLACEHOLDER, - entries = payload, + kind = PRIVATE_PAYMENT_LIST_KIND, + paymentEndpoints = paymentEndpoints, ) return noisePayloadJson.encodeToString(envelope).encodeToByteArray().size <= MAX_NOISE_PAYLOAD_BYTES } } -@kotlinx.serialization.Serializable -private data class PrivatePaymentsEnvelope( +@Serializable +private data class PrivatePaymentListEnvelope( val version: Int, val kind: String, - val reference: String, - val entries: Map, + @SerialName("payment_endpoints") + val paymentEndpoints: Map, ) internal data class PrivatePaykitPayloadSelection( diff --git a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt index 0ad998a9a8..ec99548843 100644 --- a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt @@ -14,6 +14,9 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import org.junit.After import org.junit.Before import org.junit.Test @@ -829,9 +832,11 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `prepareSavedContacts drops lightning when raw endpoint map fits but envelope is too large`() = test { - val bolt11 = "l".repeat(850) + fun `prepareSavedContacts keeps lightning when new private payment list payload fits`() = test { + val bolt11 = "l".repeat(830) val address = "bcrt1qprivate" + assertTrue(privatePaymentListPayloadSize(bolt11, address) <= 1_000) + assertTrue(legacyPrivatePaymentsPayloadSize(bolt11, address) > 1_000) val entriesOnlyPayload = mapOf( MethodId.Bolt11.rawValue to PublicPaykitRepo.serializePayload(bolt11), MethodId.P2wpkh.rawValue to PublicPaykitRepo.serializePayload(address), @@ -865,7 +870,10 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), argThat> { - none { it.paymentEndpointIdentifier == MethodId.Bolt11.rawValue } && + any { + it.paymentEndpointIdentifier == MethodId.Bolt11.rawValue && + it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(bolt11) + } && any { it.paymentEndpointIdentifier == MethodId.P2wpkh.rawValue && it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(address) @@ -1472,6 +1480,25 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { payeeNodeId = null, ) + private fun privatePaymentListPayloadSize(bolt11: String, address: String) = buildJsonObject { + put("version", 1) + put("kind", "paykit.private_payment_list") + putJsonObject("payment_endpoints") { + put(MethodId.Bolt11.rawValue, PublicPaykitRepo.serializePayload(bolt11)) + put(MethodId.P2wpkh.rawValue, PublicPaykitRepo.serializePayload(address)) + } + }.toString().encodeToByteArray().size + + private fun legacyPrivatePaymentsPayloadSize(bolt11: String, address: String) = buildJsonObject { + put("version", 1) + put("kind", "paykit.private_payments") + put("reference", "550e8400-e29b-41d4-a716-446655440000") + putJsonObject("entries") { + put(MethodId.Bolt11.rawValue, PublicPaykitRepo.serializePayload(bolt11)) + put(MethodId.P2wpkh.rawValue, PublicPaykitRepo.serializePayload(address)) + } + }.toString().encodeToByteArray().size + private fun privatePaymentsPayload(entries: List) = FfiPrivatePaymentList(paymentEndpoints = entries) From e0201a1f75dfae544911743f6efacd76d83e764f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 15 Jun 2026 14:29:57 +0200 Subject: [PATCH 13/22] fix: find ndk readelf paths --- .../bitkit/build/NativeReleaseConfigTest.kt | 10 +++++++ scripts/create-native-debug-symbols.sh | 28 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index c9640129f2..c698fb6927 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -112,6 +112,16 @@ class NativeReleaseConfigTest { symbolsScript.contains("""grep -Eq '\.debug_info'"""), "Native debug symbols script must validate full DWARF debug metadata before zipping.", ) + assertTrue( + symbolsScript.contains("ANDROID_NDK_ROOT"), + "Native debug symbols script must use the same NDK env paths Gradle can use.", + ) + assertTrue( + symbolsScript.contains("local.properties") && + symbolsScript.contains("ndk.dir") && + symbolsScript.contains("sdk.dir"), + "Native debug symbols script must use local.properties NDK/SDK paths before PATH fallback.", + ) assertFalse( symbolsScript.contains("symtab|debug_|gnu_debugdata"), "Native debug symbols script must not accept symbol-table-only metadata for FULL symbols.", diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index 9154a41ea6..846a743f3e 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -25,15 +25,35 @@ make_tmp_dir() { echo "$dir" } +local_properties_value() { + key="$1" + if [ ! -f "$repo_root/local.properties" ]; then + return + fi + + awk -F= -v key="$key" '$1 == key { value = $0; sub(/^[^=]*=/, "", value) } END { print value }' \ + "$repo_root/local.properties" | sed 's/\\ / /g' +} + find_readelf() { - for sdk_dir in "${ANDROID_NDK_HOME:-}" "${ANDROID_HOME:-}/ndk" "${ANDROID_SDK_ROOT:-}/ndk"; do - if [ -z "$sdk_dir" ]; then + local_ndk_dir=$(local_properties_value "ndk.dir") + local_sdk_dir=$(local_properties_value "sdk.dir") + + for ndk_dir in \ + "${ANDROID_NDK_ROOT:-}" \ + "${ANDROID_NDK_HOME:-}" \ + "${NDK_HOME:-}" \ + "$local_ndk_dir" \ + "${ANDROID_HOME:+$ANDROID_HOME/ndk}" \ + "${ANDROID_SDK_ROOT:+$ANDROID_SDK_ROOT/ndk}" \ + "${local_sdk_dir:+$local_sdk_dir/ndk}"; do + if [ -z "$ndk_dir" ]; then continue fi for candidate in \ - "$sdk_dir"/toolchains/llvm/prebuilt/*/bin/llvm-readelf \ - "$sdk_dir"/*/toolchains/llvm/prebuilt/*/bin/llvm-readelf; do + "$ndk_dir"/toolchains/llvm/prebuilt/*/bin/llvm-readelf \ + "$ndk_dir"/*/toolchains/llvm/prebuilt/*/bin/llvm-readelf; do if [ -x "$candidate" ]; then echo "$candidate" return From b1508c3c0856648ea57c0531d7733438ef37307f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 16 Jun 2026 16:55:30 +0200 Subject: [PATCH 14/22] fix: consume native symbol artifacts --- README.md | 4 +- app/build.gradle.kts | 32 +++++++++++ .../bitkit/build/NativeReleaseConfigTest.kt | 22 ++++++-- gradle/libs.versions.toml | 4 -- scripts/create-native-debug-symbols.sh | 56 ++++++++++++++----- 5 files changed, 94 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4607e5a6e4..4ef47a2990 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ To build the mainnet flavor for release run: just release ``` -`just release` builds the mainnet APK, Play Store AAB, and validates the native debug symbols archive. +`just release` builds the mainnet APK, Play Store AAB, resolves upstream native debug symbol artifacts, and validates the native debug symbols archive. Release artifacts: @@ -188,7 +188,7 @@ Release artifacts: - AAB: `app/build/outputs/bundle/mainnetRelease/` - Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip` -The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit a usable archive because native dependency metadata is already stripped, `just release` fails instead of creating a placeholder zip from stripped `.so` files. Stop the release and publish or consume native dependencies with usable debug metadata first. +The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. Native Rust dependencies publish stripped release AARs for app size and separate `native-debug-symbols` classifier artifacts for crash symbolication; `just release` merges those upstream symbol artifacts into the final archive and refuses placeholder symbols from stripped packaged `.so` files. For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 079272e6a9..daf71a8f97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,9 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.internal.tasks.FinalizeBundleTask import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Sync import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -318,6 +321,31 @@ composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") } +val nativeDebugSymbols by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +fun Provider.nativeDebugSymbolsArtifact(): String { + val dependency = get() + val version = dependency.versionConstraint.requiredVersion + .ifBlank { dependency.versionConstraint.preferredVersion } + + require(version.isNotBlank()) { + "Native debug symbols dependency '${dependency.module}' must declare an explicit version." + } + + return "${dependency.module.group}:${dependency.module.name}:$version:native-debug-symbols@zip" +} + +val syncNativeDebugSymbolArtifacts by tasks.registering(Sync::class) { + group = "build" + description = "Downloads native debug symbol archives for release native dependencies." + + from(nativeDebugSymbols) + into(layout.buildDirectory.dir("intermediates/native-debug-symbol-artifacts")) +} + dependencies { implementation(fileTree("libs") { include("*.aar", "*.jar") }) implementation(libs.jna) { artifact { type = "aar" } } @@ -343,6 +371,10 @@ dependencies { implementation(libs.bitkit.core) implementation(libs.paykit) implementation(libs.vss.client) + nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact()) + nativeDebugSymbols(libs.ldk.node.android.nativeDebugSymbolsArtifact()) + nativeDebugSymbols(libs.paykit.nativeDebugSymbolsArtifact()) + nativeDebugSymbols(libs.vss.client.nativeDebugSymbolsArtifact()) // Firebase implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index c698fb6927..7e736f84dc 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -38,6 +38,10 @@ class NativeReleaseConfigTest { justfile.contains("scripts/create-native-debug-symbols.sh"), "Release builds must create the native debug symbols archive before publishing.", ) + assertTrue( + justfile.contains(":app:syncNativeDebugSymbolArtifacts"), + "Release builds must resolve upstream native debug symbol artifacts before publishing.", + ) assertTrue( justfile.contains("Attach this exact file to GitHub releases"), "Release builds must tell the releaser to attach native debug symbols.", @@ -46,9 +50,9 @@ class NativeReleaseConfigTest { justfile.contains("upload it to Play Console for this release"), "Release builds must tell the releaser to upload native debug symbols to Play.", ) - assertFalse( - justfile.contains("download"), - "Release builds should keep native debug symbols in release storage.", + assertTrue( + justfile.contains("syncNativeDebugSymbolArtifacts"), + "Release builds should download native dependency symbols from release artifacts.", ) } @@ -71,8 +75,8 @@ class NativeReleaseConfigTest { "Release command should use current Play native symbol wording.", ) assertTrue( - releaseCommand.contains("fails instead of creating a placeholder zip from stripped `.so` files"), - "Release command must fail instead of publishing fake native debug symbols.", + releaseCommand.contains("resolves upstream native debug symbol artifacts"), + "Release command must document upstream native debug symbol artifact resolution.", ) assertTrue( releaseCommand.contains("Play Console may only show delete/replace controls"), @@ -90,6 +94,10 @@ class NativeReleaseConfigTest { ), "Native debug symbols script must write the canonical archive path.", ) + assertTrue( + symbolsScript.contains("native-debug-symbol-artifacts"), + "Native debug symbols script must use upstream native dependency symbol archives.", + ) assertTrue( symbolsScript.contains("arm64-v8a armeabi-v7a"), "Native debug symbols script must archive Play release ABIs.", @@ -130,5 +138,9 @@ class NativeReleaseConfigTest { symbolsScript.contains("Refusing to create '${'$'}output' from stripped native libraries."), "Native debug symbols script must refuse placeholder archives.", ) + assertTrue( + symbolsScript.contains("syncNativeDebugSymbolArtifacts"), + "Native debug symbols script must point to the Gradle task that resolves symbol artifacts.", + ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 30b329ada3..946c7f6c78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,11 +88,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -<<<<<<< HEAD vss-client = { module = "com.synonym:vss-client-android", version = "0.5.18" } -======= -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.17" } ->>>>>>> 8f40ef543 (fix: consume native symbol packages) work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index 846a743f3e..cbe7e72a13 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -8,6 +8,7 @@ cd "$repo_root" variant="mainnetRelease" output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" output_dir=$(dirname "$output") +dependency_symbols_dir="app/build/intermediates/native-debug-symbol-artifacts" required_libs="libbitkitcore.so libldk_node.so libpaykit.so libvss_rust_client_ffi.so" archive_symbol_suffixes=".dbg .sym" @@ -139,6 +140,47 @@ validate_output_zip() { validate_symbol_tree "$tmp_dir" } +create_output_zip_from_tree() { + root="$1" + + validate_symbol_tree "$root" + + mkdir -p "$output_dir" + rm -f "$output" + + ( + cd "$root" + zip -qr "$repo_root/$output" arm64-v8a armeabi-v7a + ) + + zip -T "$output" >/dev/null + echo "Native debug symbols: $output" + ls -lh "$output" +} + +if [ -d "$dependency_symbols_dir" ]; then + tmp_dir=$(make_tmp_dir) + found_archive=false + + for archive in "$dependency_symbols_dir"/*.zip; do + if [ ! -f "$archive" ]; then + continue + fi + + found_archive=true + unzip -q "$archive" -d "$tmp_dir" + done + + if [ "$found_archive" = false ]; then + echo "No native debug symbol archives found in '$dependency_symbols_dir'." >&2 + echo "Run './gradlew :app:syncNativeDebugSymbolArtifacts' before creating release symbols." >&2 + exit 1 + fi + + create_output_zip_from_tree "$tmp_dir" + exit 0 +fi + if [ -f "$output" ]; then validate_output_zip "$output" echo "Native debug symbols: $output" @@ -183,16 +225,4 @@ for abi in arm64-v8a armeabi-v7a; do fi done -validate_symbol_tree "$tmp_dir" - -mkdir -p "$output_dir" -rm -f "$output" - -( - cd "$tmp_dir" - zip -qr "$repo_root/$output" arm64-v8a armeabi-v7a -) - -zip -T "$output" >/dev/null -echo "Native debug symbols: $output" -ls -lh "$output" +create_output_zip_from_tree "$tmp_dir" From fef6566acef32c648b594b54a09eee377ecc60ea Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 16 Jun 2026 18:15:51 +0200 Subject: [PATCH 15/22] fix: address native symbol review --- .../bitkit/repositories/PrivatePaykitRepo.kt | 19 +++++-- .../bitkit/build/NativeReleaseConfigTest.kt | 9 ++++ .../repositories/PrivatePaykitRepoTest.kt | 49 +++++++++++++++++++ scripts/create-native-debug-symbols.sh | 29 ++++++++++- 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt b/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt index 2884534465..31a4cd8fb5 100644 --- a/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt @@ -1106,14 +1106,27 @@ class PrivatePaykitRepo @Inject constructor( val remotePayload = pubkyService.getPrivatePayments(linkId) ensureCurrentGeneration(generation) recordLinkSuccess(publicKey) - persistLinkSnapshot(linkId, publicKey, linkWasReplaced = false, generation = generation).getOrThrow() - ensureCurrentGeneration(generation) - if (remotePayload == null) return@runCatching 0 + if (remotePayload == null) { + persistLinkSnapshot( + linkId = linkId, + publicKey = publicKey, + linkWasReplaced = false, + generation = generation, + ).getOrThrow() + return@runCatching 0 + } val remoteEntries = remotePayload.paymentEndpoints val contactState = ensureState().contacts.getOrPut(publicKey) { ContactState() } contactState.remoteEndpoints = remoteEntries.map { it.toStoredPaymentEntry() } persistState(markWalletBackup = true) + ensureCurrentGeneration(generation) + persistLinkSnapshot( + linkId = linkId, + publicKey = publicKey, + linkWasReplaced = false, + generation = generation, + ).getOrThrow() remoteEntries.count() } } diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 7e736f84dc..91320d9a8d 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -116,6 +116,7 @@ class NativeReleaseConfigTest { symbolsScript.contains("""archive_symbol_suffixes=".dbg .sym""""), "Native debug symbols script must accept AGP native debug symbol entry suffixes.", ) + assertDependencyArchiveEntriesAreNormalized(symbolsScript) assertTrue( symbolsScript.contains("""grep -Eq '\.debug_info'"""), "Native debug symbols script must validate full DWARF debug metadata before zipping.", @@ -143,4 +144,12 @@ class NativeReleaseConfigTest { "Native debug symbols script must point to the Gradle task that resolves symbol artifacts.", ) } + + private fun assertDependencyArchiveEntriesAreNormalized(symbolsScript: String) { + assertTrue( + symbolsScript.contains("copy_archive_symbols") && + symbolsScript.contains("""mv "${'$'}tmp_dir/${'$'}entry" "${'$'}tmp_dir/${'$'}abi/${'$'}lib_name""""), + "Native debug symbols script must normalize suffixed dependency archive entries before validation.", + ) + } } diff --git a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt index ec99548843..1ef294534b 100644 --- a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt @@ -28,6 +28,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.eq +import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -1333,6 +1334,54 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).getPrivatePayments(retryLinkId) } + @Test + fun `beginSavedContactPayment persists remote endpoints before advancing link snapshot`() = test { + cacheData.value = PrivatePaykitCacheData( + contacts = mapOf( + CONTACT_KEY to PrivatePaykitContactCacheData( + lastLocalPayloadHash = LOCAL_PAYLOAD_HASH, + linkCompletedAt = NOW_SECONDS - 60, + ), + ), + ) + whenever(keychain.loadString(Keychain.Key.PRIVATE_PAYKIT_SECRET_STATE.name)) + .thenReturn(secretStateJson()) + whenever(keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name)).thenReturn(SECRET_KEY_HEX) + whenever(pubkyService.currentPublicKey()).thenReturn(OWN_KEY) + whenever(pubkyService.encryptedLinkSnapshotRecipient(LINK_SNAPSHOT)).thenReturn(CONTACT_KEY) + whenever(pubkyService.restoreEncryptedLink(SECRET_KEY_HEX, LINK_SNAPSHOT)).thenReturn(LINK_ID) + whenever(pubkyService.getPrivatePayments(LINK_ID)).thenReturn( + privatePaymentsPayload( + listOf( + FfiPaymentEndpoint( + paymentEndpointIdentifier = MethodId.P2wpkh.rawValue, + paymentEndpointPayload = PublicPaykitRepo.serializePayload("bcrt1qprivate"), + ), + ), + ), + ) + whenever(pubkyService.serializeEncryptedLink(LINK_ID)).thenReturn(UPDATED_LINK_SNAPSHOT) + whenever(publicPaykitRepo.payableEndpoints(any())).thenAnswer { it.getArgument>(0) } + whenever(coreService.isAddressUsed("bcrt1qprivate")).thenReturn(false) + whenever(lightningRepo.getPayments()).thenReturn(Result.success(emptyList())) + rememberSavedContact() + + val result = sut.beginSavedContactPayment(CONTACT_KEY).getOrThrow() + val snapshot = sut.backupSnapshot().getOrThrow()?.get(CONTACT_KEY) + + assertTrue(result is PublicPaykitPaymentResult.Opened) + assertNotNull(snapshot) + assertEquals( + mapOf(MethodId.P2wpkh.rawValue to PublicPaykitRepo.serializePayload("bcrt1qprivate")), + snapshot.remoteEndpoints, + ) + + val order = inOrder(pubkyService, cacheStore) + order.verify(pubkyService).getPrivatePayments(LINK_ID) + order.verify(cacheStore).update(any()) + order.verify(pubkyService).serializeEncryptedLink(LINK_ID) + } + @Test fun `isDuplicatePaymentError detects wrapped duplicate payment messages`() { val error = PrivatePaykitTestError("service queue failed", cause = AppError("Duplicate payment.")) diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index cbe7e72a13..381da903ef 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -126,6 +126,33 @@ extract_archive_lib() { exit 1 } +copy_archive_symbols() { + archive="$1" + tmp_dir="$2" + + for abi in arm64-v8a armeabi-v7a; do + mkdir -p "$tmp_dir/$abi" + for lib_name in $required_libs; do + copied=false + entry="$abi/$lib_name" + if unzip -qo "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + copied=true + fi + + if [ "$copied" = false ]; then + for suffix in $archive_symbol_suffixes; do + entry="$abi/$lib_name$suffix" + if unzip -qo "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + mv "$tmp_dir/$entry" "$tmp_dir/$abi/$lib_name" + copied=true + break + fi + done + fi + done + done +} + validate_output_zip() { archive="$1" zip -T "$archive" >/dev/null @@ -168,7 +195,7 @@ if [ -d "$dependency_symbols_dir" ]; then fi found_archive=true - unzip -q "$archive" -d "$tmp_dir" + copy_archive_symbols "$archive" "$tmp_dir" done if [ "$found_archive" = false ]; then From 5bc4558a8f5245d123d46818c75e4e646f414bce Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 16 Jun 2026 18:28:21 +0200 Subject: [PATCH 16/22] fix: preserve legacy private paykit --- .../services/PrivatePaykitMessageParser.kt | 48 +++++++++++++ .../java/to/bitkit/services/PubkyService.kt | 3 +- .../PrivatePaykitMessageParserTest.kt | 67 +++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt create mode 100644 app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt diff --git a/app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt b/app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt new file mode 100644 index 0000000000..0d2fe02d5e --- /dev/null +++ b/app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt @@ -0,0 +1,48 @@ +package to.bitkit.services + +import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPrivatePaymentList +import com.synonym.paykit.paykitParsePrivatePaymentListJson +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import to.bitkit.di.json + +internal object PrivatePaykitMessageParser { + private const val LEGACY_PRIVATE_PAYMENTS_KIND = "paykit.private_payments" + + private val parserJson = Json(json) { + prettyPrint = false + encodeDefaults = false + } + + fun parsePrivatePaymentListJson( + rawJson: String, + parsePaymentList: (String) -> FfiPrivatePaymentList = ::paykitParsePrivatePaymentListJson, + ): FfiPrivatePaymentList? = + runCatching { parsePaymentList(rawJson) } + .getOrElse { parseLegacyPrivatePayments(rawJson) } + + private fun parseLegacyPrivatePayments(rawJson: String): FfiPrivatePaymentList? = + runCatching { + val envelope = parserJson.decodeFromString(rawJson) + if (envelope.kind != LEGACY_PRIVATE_PAYMENTS_KIND) return null + + FfiPrivatePaymentList( + paymentEndpoints = envelope.entries.map { + FfiPaymentEndpoint( + paymentEndpointIdentifier = it.key, + paymentEndpointPayload = it.value, + ) + }, + ) + }.getOrNull() +} + +@Serializable +private data class LegacyPrivatePaymentsEnvelope( + val kind: String, + @SerialName("entries") + val entries: Map = emptyMap(), +) diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt index 55d0a22f47..6768066f14 100644 --- a/app/src/main/java/to/bitkit/services/PubkyService.kt +++ b/app/src/main/java/to/bitkit/services/PubkyService.kt @@ -38,7 +38,6 @@ import com.synonym.paykit.paykitImportSession import com.synonym.paykit.paykitInitialize import com.synonym.paykit.paykitInitiateEncryptedLink import com.synonym.paykit.paykitIsAuthenticated -import com.synonym.paykit.paykitParsePrivatePaymentListJson import com.synonym.paykit.paykitReceivePrivateApplicationMessages import com.synonym.paykit.paykitRemovePaymentEndpoint import com.synonym.paykit.paykitRestoreEncryptedLink @@ -202,7 +201,7 @@ class PubkyService @Inject constructor( isSetup.await() paykitReceivePrivateApplicationMessages(linkId) .asReversed() - .firstNotNullOfOrNull { runCatching { paykitParsePrivatePaymentListJson(it.rawJson) }.getOrNull() } + .firstNotNullOfOrNull { PrivatePaykitMessageParser.parsePrivatePaymentListJson(it.rawJson) } } // endregion diff --git a/app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt b/app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt new file mode 100644 index 0000000000..5e46316219 --- /dev/null +++ b/app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt @@ -0,0 +1,67 @@ +package to.bitkit.services + +import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPrivatePaymentList +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PrivatePaykitMessageParserTest { + @Test + fun `parsePrivatePaymentListJson keeps current Paykit parser result`() { + val parsed = FfiPrivatePaymentList( + paymentEndpoints = listOf( + FfiPaymentEndpoint( + paymentEndpointIdentifier = "bolt11-invoice-0", + paymentEndpointPayload = "lnbcrt1current", + ), + ), + ) + + val result = PrivatePaykitMessageParser.parsePrivatePaymentListJson("{}") { parsed } + + assertEquals(parsed, result) + } + + @Test + fun `parsePrivatePaymentListJson falls back to legacy private payments envelope`() { + val legacyJson = """ + { + "version": 1, + "kind": "paykit.private_payments", + "reference": "550e8400-e29b-41d4-a716-446655440000", + "entries": { + "bolt11-invoice-0": "lnbcrt1legacy", + "bip21-p2wpkh-0": "bcrt1qlegacy" + } + } + """.trimIndent() + + val result = PrivatePaykitMessageParser.parsePrivatePaymentListJson(legacyJson) { + throw IllegalArgumentException("unsupported legacy envelope") + } + + assertEquals( + listOf( + FfiPaymentEndpoint( + paymentEndpointIdentifier = "bolt11-invoice-0", + paymentEndpointPayload = "lnbcrt1legacy", + ), + FfiPaymentEndpoint( + paymentEndpointIdentifier = "bip21-p2wpkh-0", + paymentEndpointPayload = "bcrt1qlegacy", + ), + ), + result?.paymentEndpoints, + ) + } + + @Test + fun `parsePrivatePaymentListJson ignores unknown legacy envelopes`() { + val result = PrivatePaykitMessageParser.parsePrivatePaymentListJson("""{"kind":"other"}""") { + throw IllegalArgumentException("unsupported envelope") + } + + assertNull(result) + } +} From ace5abc927b00c5fbac3b40816ccbf1c448a802e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 16 Jun 2026 20:20:59 +0200 Subject: [PATCH 17/22] ci: upload native symbols --- .github/workflows/release-internal.yml | 13 +++++++++++-- .github/workflows/release.yml | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-internal.yml b/.github/workflows/release-internal.yml index 4f419bd702..dd7ec94169 100644 --- a/.github/workflows/release-internal.yml +++ b/.github/workflows/release-internal.yml @@ -73,7 +73,11 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.INTERNAL_KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.INTERNAL_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.INTERNAL_KEY_PASSWORD }} - run: ./gradlew assembleMainnetRelease --no-daemon --stacktrace + run: | + set -euo pipefail + ./gradlew assembleMainnetRelease --no-daemon --stacktrace + ./gradlew :app:syncNativeDebugSymbolArtifacts --no-daemon --stacktrace + scripts/create-native-debug-symbols.sh - name: Verify native libraries are stripped run: | @@ -124,18 +128,23 @@ jobs: artifact_dir="$RUNNER_TEMP/internal-release" mkdir -p "$artifact_dir" mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + mapfile -d '' symbol_paths < <(find app/build/outputs/native-debug-symbols/mainnetRelease -name 'native-debug-symbols-*.zip' -print0) test "${#apk_paths[@]}" -gt 0 + test "${#symbol_paths[@]}" -gt 0 for apk_path in "${apk_paths[@]}"; do cp "$apk_path" "$artifact_dir/" done + for symbol_path in "${symbol_paths[@]}"; do + cp "$symbol_path" "$artifact_dir/" + done apk_file="$(basename "${apk_paths[0]}")" build_number="${apk_file#bitkit-mainnet-release-}" build_number="${build_number%%-*}" [[ "$build_number" =~ ^[0-9]+$ ]] - (cd "$artifact_dir" && sha256sum -- *.apk > SHA256SUMS.txt) + (cd "$artifact_dir" && sha256sum -- *.apk native-debug-symbols-*.zip > SHA256SUMS.txt) echo "artifact_name=bitkit-release-internal-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bde387774c..b7af685206 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,11 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.BITKIT_KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.BITKIT_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.BITKIT_KEY_PASSWORD }} - run: ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace + run: | + set -euo pipefail + ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace + ./gradlew :app:syncNativeDebugSymbolArtifacts --no-daemon --stacktrace + scripts/create-native-debug-symbols.sh - name: Verify native libraries are stripped run: | @@ -148,8 +152,10 @@ jobs: mkdir -p "$artifact_dir" mapfile -d '' bundle_paths < <(find app/build/outputs/bundle/mainnetRelease -name 'bitkit-mainnet-release-*.aab' -print0) mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + mapfile -d '' symbol_paths < <(find app/build/outputs/native-debug-symbols/mainnetRelease -name 'native-debug-symbols-*.zip' -print0) test "${#bundle_paths[@]}" -gt 0 test "${#apk_paths[@]}" -gt 0 + test "${#symbol_paths[@]}" -gt 0 for bundle_path in "${bundle_paths[@]}"; do cp "$bundle_path" "$artifact_dir/" @@ -157,13 +163,16 @@ jobs: for apk_path in "${apk_paths[@]}"; do cp "$apk_path" "$artifact_dir/" done + for symbol_path in "${symbol_paths[@]}"; do + cp "$symbol_path" "$artifact_dir/" + done apk_file="$(basename "${apk_paths[0]}")" build_number="${apk_file#bitkit-mainnet-release-}" build_number="${build_number%%-*}" [[ "$build_number" =~ ^[0-9]+$ ]] - (cd "$artifact_dir" && sha256sum -- *.aab *.apk > SHA256SUMS.txt) + (cd "$artifact_dir" && sha256sum -- *.aab *.apk native-debug-symbols-*.zip > SHA256SUMS.txt) echo "artifact_name=bitkit-release-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" From f41d4c270c95ea6dadbbe7791a7c080531aa83fa Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 16 Jun 2026 20:46:13 +0200 Subject: [PATCH 18/22] fix: version native symbol archive --- README.md | 10 +++---- .../bitkit/build/NativeReleaseConfigTest.kt | 26 ++++++++++++------- scripts/create-native-debug-symbols.sh | 19 ++++++++++++-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4ef47a2990..b4c4a17829 100644 --- a/README.md +++ b/README.md @@ -186,15 +186,15 @@ Release artifacts: - APK: `app/build/outputs/apk/mainnet/release/` - AAB: `app/build/outputs/bundle/mainnetRelease/` -- Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip` +- Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{versionCode}.zip` -The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. Native Rust dependencies publish stripped release AARs for app size and separate `native-debug-symbols` classifier artifacts for crash symbolication; `just release` merges those upstream symbol artifacts into the final archive and refuses placeholder symbols from stripped packaged `.so` files. +The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the build-numbered filename, e.g. `native-debug-symbols-182.zip`, so it matches the APK/AAB build number. Native Rust dependencies publish stripped release AARs for app size and separate `native-debug-symbols` classifier artifacts for crash symbolication; `just release` merges those upstream symbol artifacts into the final archive and refuses placeholder symbols from stripped packaged `.so` files. -For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. +For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols-{versionCode}.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. -Keep the release-built `native-debug-symbols.zip` in GitHub releases or internal release storage. Play Console may only show delete/replace controls after upload, which is enough for release verification. +Keep the release-built `native-debug-symbols-{versionCode}.zip` in GitHub releases or internal release storage. Play Console may only show delete/replace controls after upload, which is enough for release verification. -For GitHub releases, attach `native-debug-symbols.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. +For GitHub releases, attach `native-debug-symbols-{versionCode}.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. #### Android App Bundle (AAB) diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index 91320d9a8d..fea97b60c8 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -30,7 +30,7 @@ class NativeReleaseConfigTest { assertTrue( justfile.contains( - """rm -f "${'$'}symbols"""", + """rm -f "${'$'}symbols_dir"/native-debug-symbols*.zip""", ), "Release builds must remove stale native debug symbols before rebuilding.", ) @@ -62,12 +62,12 @@ class NativeReleaseConfigTest { assertTrue( releaseCommand.contains( - "app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip", + "app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip", ), "Release command must include the native debug symbols archive path.", ) assertTrue( - releaseCommand.contains("Native debug symbols uploaded: native-debug-symbols.zip"), + releaseCommand.contains("Native debug symbols uploaded: native-debug-symbols-{newVersionCode}.zip"), "Release command summary must report the native debug symbols archive.", ) assertFalse( @@ -88,12 +88,7 @@ class NativeReleaseConfigTest { fun `native debug symbols script rejects stripped release libraries`() { val symbolsScript = repoRoot.resolve("scripts/create-native-debug-symbols.sh").readText() - assertTrue( - symbolsScript.contains( - "app/build/outputs/native-debug-symbols/${'$'}variant/native-debug-symbols.zip", - ), - "Native debug symbols script must write the canonical archive path.", - ) + assertBuildNumberedArchiveOutput(symbolsScript) assertTrue( symbolsScript.contains("native-debug-symbol-artifacts"), "Native debug symbols script must use upstream native dependency symbol archives.", @@ -145,6 +140,19 @@ class NativeReleaseConfigTest { ) } + private fun assertBuildNumberedArchiveOutput(symbolsScript: String) { + assertTrue( + symbolsScript.contains( + "app/build/outputs/native-debug-symbols/${'$'}variant/native-debug-symbols-${'$'}build_number.zip", + ), + "Native debug symbols script must write the build-numbered archive path.", + ) + assertTrue( + symbolsScript.contains("""rm -f "${'$'}output_dir"/native-debug-symbols*.zip"""), + "Native debug symbols script must clear stale build-numbered archives before writing.", + ) + } + private fun assertDependencyArchiveEntriesAreNormalized(symbolsScript: String) { assertTrue( symbolsScript.contains("copy_archive_symbols") && diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index 381da903ef..144abc163f 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -6,7 +6,22 @@ repo_root=$(cd "$script_dir/.." && pwd) cd "$repo_root" variant="mainnetRelease" -output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols.zip" +build_number=$( + awk -F= ' + /^[[:space:]]*versionCode[[:space:]]*=/ { + value = $2 + gsub(/[[:space:]]/, "", value) + print value + exit + } + ' app/build.gradle.kts +) +if [ -z "$build_number" ]; then + echo "Unable to read versionCode from app/build.gradle.kts." >&2 + exit 1 +fi + +output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols-$build_number.zip" output_dir=$(dirname "$output") dependency_symbols_dir="app/build/intermediates/native-debug-symbol-artifacts" required_libs="libbitkitcore.so libldk_node.so libpaykit.so libvss_rust_client_ffi.so" @@ -173,7 +188,7 @@ create_output_zip_from_tree() { validate_symbol_tree "$root" mkdir -p "$output_dir" - rm -f "$output" + rm -f "$output_dir"/native-debug-symbols*.zip ( cd "$root" From 2a4d0a17bdf2a5ded7bae2020814503305c35b49 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 17 Jun 2026 11:36:18 +0200 Subject: [PATCH 19/22] fix: consume final symbol packages --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 946c7f6c78..e4d5584034 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.73" } -paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc18" } +paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc19" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } @@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.49" } +ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.50" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } @@ -88,7 +88,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.18" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.19" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } From 934591a6ef8bab0c5f1c7f67e6091f1fd03973e6 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 17 Jun 2026 11:49:49 +0200 Subject: [PATCH 20/22] fix: defer paykit symbol integration --- app/build.gradle.kts | 1 - .../repositories/PrivatePaykitPayloads.kt | 22 +-- .../bitkit/repositories/PrivatePaykitRepo.kt | 39 +---- .../java/to/bitkit/repositories/PubkyRepo.kt | 6 +- .../bitkit/repositories/PublicPaykitRepo.kt | 4 +- .../services/PrivatePaykitMessageParser.kt | 48 ------ .../java/to/bitkit/services/PubkyService.kt | 25 ++-- .../bitkit/build/NativeReleaseConfigTest.kt | 2 +- .../repositories/PrivatePaykitRepoTest.kt | 138 ++++-------------- .../to/bitkit/repositories/PubkyRepoTest.kt | 8 +- .../repositories/PublicPaykitRepoTest.kt | 8 +- .../PrivatePaykitMessageParserTest.kt | 67 --------- gradle/libs.versions.toml | 2 +- scripts/create-native-debug-symbols.sh | 2 +- 14 files changed, 78 insertions(+), 294 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt delete mode 100644 app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index daf71a8f97..e5e7f31133 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -373,7 +373,6 @@ dependencies { implementation(libs.vss.client) nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact()) nativeDebugSymbols(libs.ldk.node.android.nativeDebugSymbolsArtifact()) - nativeDebugSymbols(libs.paykit.nativeDebugSymbolsArtifact()) nativeDebugSymbols(libs.vss.client.nativeDebugSymbolsArtifact()) // Firebase implementation(platform(libs.firebase.bom)) diff --git a/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt b/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt index 07b927e009..ec22395822 100644 --- a/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt +++ b/app/src/main/java/to/bitkit/repositories/PrivatePaykitPayloads.kt @@ -1,7 +1,5 @@ package to.bitkit.repositories -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import to.bitkit.di.json @@ -10,7 +8,8 @@ import java.security.MessageDigest internal object PrivatePaykitPayloads { private const val MAX_NOISE_PAYLOAD_BYTES = 1000 private const val PRIVATE_ENDPOINT_REMOVAL_PAYLOAD = """{"value":""}""" - private const val PRIVATE_PAYMENT_LIST_KIND = "paykit.private_payment_list" + private const val PRIVATE_PAYMENTS_ENVELOPE_KIND = "paykit.private_payments" + private const val PRIVATE_PAYMENTS_REFERENCE_PLACEHOLDER = "550e8400-e29b-41d4-a716-446655440000" private val noisePayloadJson = Json(json) { prettyPrint = false @@ -53,22 +52,23 @@ internal object PrivatePaykitPayloads { endpoints.toSortedMap().map { StoredPaymentEntry(it.key, it.value) } private fun isNoisePayloadWithinLimit(entries: List): Boolean { - val paymentEndpoints = entries.associate { it.methodId to it.endpointData } - val envelope = PrivatePaymentListEnvelope( + val payload = entries.associate { it.methodId to it.endpointData } + val envelope = PrivatePaymentsEnvelope( version = 1, - kind = PRIVATE_PAYMENT_LIST_KIND, - paymentEndpoints = paymentEndpoints, + kind = PRIVATE_PAYMENTS_ENVELOPE_KIND, + reference = PRIVATE_PAYMENTS_REFERENCE_PLACEHOLDER, + entries = payload, ) return noisePayloadJson.encodeToString(envelope).encodeToByteArray().size <= MAX_NOISE_PAYLOAD_BYTES } } -@Serializable -private data class PrivatePaymentListEnvelope( +@kotlinx.serialization.Serializable +private data class PrivatePaymentsEnvelope( val version: Int, val kind: String, - @SerialName("payment_endpoints") - val paymentEndpoints: Map, + val reference: String, + val entries: Map, ) internal data class PrivatePaykitPayloadSelection( diff --git a/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt b/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt index 31a4cd8fb5..ff828e8e91 100644 --- a/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt @@ -1,7 +1,7 @@ package to.bitkit.repositories import com.synonym.bitkitcore.Scanner -import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPaymentEntry import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,16 +47,6 @@ private data class PrivatePaymentAttempt( val shouldDeferPublicFallback: Boolean, ) -private fun StoredPaymentEntry.toFfiPaymentEndpoint() = FfiPaymentEndpoint( - paymentEndpointIdentifier = methodId, - paymentEndpointPayload = endpointData, -) - -private fun FfiPaymentEndpoint.toStoredPaymentEntry() = StoredPaymentEntry( - methodId = paymentEndpointIdentifier, - endpointData = paymentEndpointPayload, -) - @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) @Singleton @Suppress("TooManyFunctions", "LongParameterList", "LargeClass") @@ -965,7 +955,7 @@ class PrivatePaykitRepo @Inject constructor( ensureCurrentGeneration(generation) if (!canPublishPrivateEndpoints() || knownSavedContact(publicKey) == null) return@withLock - pubkyService.setPrivatePayments(linkId, entries.map { it.toFfiPaymentEndpoint() }) + pubkyService.setPrivatePayments(linkId, entries.map { FfiPaymentEntry(it.methodId, it.endpointData) }) ensureCurrentGeneration(generation) persistLinkSnapshot(linkId, publicKey, linkWasReplaced = false, generation = generation).getOrThrow() contactState.lastLocalPayloadHash = payloadHash @@ -1106,27 +1096,14 @@ class PrivatePaykitRepo @Inject constructor( val remotePayload = pubkyService.getPrivatePayments(linkId) ensureCurrentGeneration(generation) recordLinkSuccess(publicKey) - if (remotePayload == null) { - persistLinkSnapshot( - linkId = linkId, - publicKey = publicKey, - linkWasReplaced = false, - generation = generation, - ).getOrThrow() - return@runCatching 0 - } + persistLinkSnapshot(linkId, publicKey, linkWasReplaced = false, generation = generation).getOrThrow() + ensureCurrentGeneration(generation) + if (remotePayload == null) return@runCatching 0 - val remoteEntries = remotePayload.paymentEndpoints + val remoteEntries = remotePayload.entries val contactState = ensureState().contacts.getOrPut(publicKey) { ContactState() } - contactState.remoteEndpoints = remoteEntries.map { it.toStoredPaymentEntry() } + contactState.remoteEndpoints = remoteEntries.map { StoredPaymentEntry(it.methodId, it.endpointData) } persistState(markWalletBackup = true) - ensureCurrentGeneration(generation) - persistLinkSnapshot( - linkId = linkId, - publicKey = publicKey, - linkWasReplaced = false, - generation = generation, - ).getOrThrow() remoteEntries.count() } } @@ -1637,7 +1614,7 @@ class PrivatePaykitRepo @Inject constructor( PrivatePaykitPayloads.validateNoisePayload(entries) pubkyService.setPrivatePayments( linkId, - entries.map { it.toFfiPaymentEndpoint() }, + entries.map { FfiPaymentEntry(it.methodId, it.endpointData) }, ) ensureCurrentGeneration(generation) ensureState().contacts[publicKey]?.lastLocalPayloadHash = null diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt index f1120b3063..48b61bdeb2 100644 --- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt @@ -3,7 +3,7 @@ package to.bitkit.repositories import android.graphics.Bitmap import android.graphics.BitmapFactory import coil3.ImageLoader -import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPaymentEntry import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.post @@ -389,7 +389,7 @@ class PubkyRepo @Inject constructor( // region Payment endpoints - suspend fun getPaymentList(publicKey: String): Result> = withContext(ioDispatcher) { + suspend fun getPaymentList(publicKey: String): Result> = withContext(ioDispatcher) { runCatching { pubkyService.getPaymentList(publicKey.ensurePubkyPrefix()) } @@ -417,7 +417,7 @@ class PubkyRepo @Inject constructor( .toSet() pubkyService.getPaymentList(currentPublicKey) - .map { it.paymentEndpointIdentifier } + .map { it.methodId } .filter { it in managedMethodIds } .forEach { pubkyService.removePaymentEndpoint(it) } } diff --git a/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt b/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt index 4a71a96ffe..3e7e41ff0c 100644 --- a/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt @@ -220,7 +220,7 @@ class PublicPaykitRepo @Inject constructor( runCatching { val normalizedKey = PubkyPublicKeyFormat.normalized(publicKey) ?: publicKey pubkyRepo.getPaymentList(normalizedKey).getOrThrow() - .mapNotNull { parseEndpoint(it.paymentEndpointIdentifier, it.paymentEndpointPayload) } + .mapNotNull { parseEndpoint(it.methodId, it.endpointData) } .associateBy { it.methodId } .values .sortedBy { endpoint -> payablePreferenceOrder.indexOf(endpoint.methodId) } @@ -255,7 +255,7 @@ class PublicPaykitRepo @Inject constructor( private suspend fun currentPublishedMethodIds(): Set { return pubkyRepo.getPaymentList(requireCurrentPublicKey()).getOrThrow() - .map { it.paymentEndpointIdentifier } + .map { it.methodId } .toSet() } diff --git a/app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt b/app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt deleted file mode 100644 index 0d2fe02d5e..0000000000 --- a/app/src/main/java/to/bitkit/services/PrivatePaykitMessageParser.kt +++ /dev/null @@ -1,48 +0,0 @@ -package to.bitkit.services - -import com.synonym.paykit.FfiPaymentEndpoint -import com.synonym.paykit.FfiPrivatePaymentList -import com.synonym.paykit.paykitParsePrivatePaymentListJson -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import to.bitkit.di.json - -internal object PrivatePaykitMessageParser { - private const val LEGACY_PRIVATE_PAYMENTS_KIND = "paykit.private_payments" - - private val parserJson = Json(json) { - prettyPrint = false - encodeDefaults = false - } - - fun parsePrivatePaymentListJson( - rawJson: String, - parsePaymentList: (String) -> FfiPrivatePaymentList = ::paykitParsePrivatePaymentListJson, - ): FfiPrivatePaymentList? = - runCatching { parsePaymentList(rawJson) } - .getOrElse { parseLegacyPrivatePayments(rawJson) } - - private fun parseLegacyPrivatePayments(rawJson: String): FfiPrivatePaymentList? = - runCatching { - val envelope = parserJson.decodeFromString(rawJson) - if (envelope.kind != LEGACY_PRIVATE_PAYMENTS_KIND) return null - - FfiPrivatePaymentList( - paymentEndpoints = envelope.entries.map { - FfiPaymentEndpoint( - paymentEndpointIdentifier = it.key, - paymentEndpointPayload = it.value, - ) - }, - ) - }.getOrNull() -} - -@Serializable -private data class LegacyPrivatePaymentsEnvelope( - val kind: String, - @SerialName("entries") - val entries: Map = emptyMap(), -) diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt index 6768066f14..d7dcbedf6b 100644 --- a/app/src/main/java/to/bitkit/services/PubkyService.kt +++ b/app/src/main/java/to/bitkit/services/PubkyService.kt @@ -20,8 +20,8 @@ import com.synonym.bitkitcore.pubkySignIn import com.synonym.bitkitcore.pubkySignUp import com.synonym.bitkitcore.startPubkyAuth import com.synonym.paykit.FfiHandshakeProgress -import com.synonym.paykit.FfiPaymentEndpoint -import com.synonym.paykit.FfiPrivatePaymentList +import com.synonym.paykit.FfiPaymentEntry +import com.synonym.paykit.FfiPrivatePaymentsPayload import com.synonym.paykit.PaykitAndroid import com.synonym.paykit.paykitAcceptEncryptedLink import com.synonym.paykit.paykitAdvanceHandshake @@ -31,21 +31,22 @@ import com.synonym.paykit.paykitEncryptedLinkHandshakeSnapshotRecipient import com.synonym.paykit.paykitEncryptedLinkSnapshotRecipient import com.synonym.paykit.paykitExportSession import com.synonym.paykit.paykitForceSignOut +import com.synonym.paykit.paykitGeneratePaymentReference import com.synonym.paykit.paykitGetCurrentPublicKey import com.synonym.paykit.paykitGetPaymentEndpoint import com.synonym.paykit.paykitGetPaymentList +import com.synonym.paykit.paykitGetPrivatePayments import com.synonym.paykit.paykitImportSession import com.synonym.paykit.paykitInitialize import com.synonym.paykit.paykitInitiateEncryptedLink import com.synonym.paykit.paykitIsAuthenticated -import com.synonym.paykit.paykitReceivePrivateApplicationMessages import com.synonym.paykit.paykitRemovePaymentEndpoint import com.synonym.paykit.paykitRestoreEncryptedLink import com.synonym.paykit.paykitRestoreEncryptedLinkHandshake import com.synonym.paykit.paykitSerializeEncryptedLink import com.synonym.paykit.paykitSerializeEncryptedLinkHandshake import com.synonym.paykit.paykitSetPaymentEndpoint -import com.synonym.paykit.paykitSetPrivatePaymentList +import com.synonym.paykit.paykitSetPrivatePayments import com.synonym.paykit.paykitSignOut import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CompletableDeferred @@ -107,7 +108,7 @@ class PubkyService @Inject constructor( // region Payment endpoints - suspend fun getPaymentList(publicKey: String): List = ServiceQueue.CORE.background { + suspend fun getPaymentList(publicKey: String): List = ServiceQueue.CORE.background { isSetup.await() paykitGetPaymentList(publicKey) } @@ -191,17 +192,19 @@ class PubkyService @Inject constructor( paykitDropEncryptedLinkHandshake(handshakeId) } - suspend fun setPrivatePayments(linkId: String, entries: List) = + suspend fun setPrivatePayments(linkId: String, entries: List) = ServiceQueue.CORE.background { isSetup.await() - paykitSetPrivatePaymentList(linkId, FfiPrivatePaymentList(paymentEndpoints = entries)) + val payload = FfiPrivatePaymentsPayload( + reference = paykitGeneratePaymentReference(), + entries = entries, + ) + paykitSetPrivatePayments(linkId, payload) } - suspend fun getPrivatePayments(linkId: String): FfiPrivatePaymentList? = ServiceQueue.CORE.background { + suspend fun getPrivatePayments(linkId: String): FfiPrivatePaymentsPayload? = ServiceQueue.CORE.background { isSetup.await() - paykitReceivePrivateApplicationMessages(linkId) - .asReversed() - .firstNotNullOfOrNull { PrivatePaykitMessageParser.parsePrivatePaymentListJson(it.rawJson) } + paykitGetPrivatePayments(linkId) } // endregion diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt index fea97b60c8..2ebe63e639 100644 --- a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -103,7 +103,7 @@ class NativeReleaseConfigTest { ) assertTrue( symbolsScript.contains( - """required_libs="libbitkitcore.so libldk_node.so libpaykit.so libvss_rust_client_ffi.so"""", + """required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so"""", ), "Native debug symbols script must validate release-critical native libraries.", ) diff --git a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt index 1ef294534b..cb01195c09 100644 --- a/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PrivatePaykitRepoTest.kt @@ -4,8 +4,8 @@ import android.app.Activity import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.NetworkType import com.synonym.bitkitcore.Scanner -import com.synonym.paykit.FfiPaymentEndpoint -import com.synonym.paykit.FfiPrivatePaymentList +import com.synonym.paykit.FfiPaymentEntry +import com.synonym.paykit.FfiPrivatePaymentsPayload import com.synonym.paykit.PaykitFfiException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -14,9 +14,6 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject import org.junit.After import org.junit.Before import org.junit.Test @@ -28,7 +25,6 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.eq -import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -407,8 +403,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - isNotEmpty() && all { it.paymentEndpointPayload == TOMBSTONE_PAYLOAD } + argThat> { + isNotEmpty() && all { it.endpointData == TOMBSTONE_PAYLOAD } }, ) verify(addressReservationRepo).clearContactAssignment(CONTACT_KEY) @@ -465,8 +461,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(publicPaykitRepo, never()).syncPublishedEndpoints(false) verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - isNotEmpty() && all { it.paymentEndpointPayload == TOMBSTONE_PAYLOAD } + argThat> { + isNotEmpty() && all { it.endpointData == TOMBSTONE_PAYLOAD } }, ) verify(addressReservationRepo).clearContactAssignment(CONTACT_KEY) @@ -675,8 +671,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).getPrivatePayments(LINK_ID) verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - any { it.paymentEndpointPayload == PublicPaykitRepo.serializePayload("bcrt1qprivate") } + argThat> { + any { it.endpointData == PublicPaykitRepo.serializePayload("bcrt1qprivate") } }, ) } @@ -823,21 +819,19 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { + argThat> { any { - it.paymentEndpointIdentifier == MethodId.Bolt11.rawValue && - it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(bolt11) + it.methodId == MethodId.Bolt11.rawValue && + it.endpointData == PublicPaykitRepo.serializePayload(bolt11) } }, ) } @Test - fun `prepareSavedContacts keeps lightning when new private payment list payload fits`() = test { - val bolt11 = "l".repeat(830) + fun `prepareSavedContacts drops lightning when raw endpoint map fits but envelope is too large`() = test { + val bolt11 = "l".repeat(850) val address = "bcrt1qprivate" - assertTrue(privatePaymentListPayloadSize(bolt11, address) <= 1_000) - assertTrue(legacyPrivatePaymentsPayloadSize(bolt11, address) > 1_000) val entriesOnlyPayload = mapOf( MethodId.Bolt11.rawValue to PublicPaykitRepo.serializePayload(bolt11), MethodId.P2wpkh.rawValue to PublicPaykitRepo.serializePayload(address), @@ -870,14 +864,11 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).setPrivatePayments( eq(LINK_ID), - argThat> { - any { - it.paymentEndpointIdentifier == MethodId.Bolt11.rawValue && - it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(bolt11) - } && + argThat> { + none { it.methodId == MethodId.Bolt11.rawValue } && any { - it.paymentEndpointIdentifier == MethodId.P2wpkh.rawValue && - it.paymentEndpointPayload == PublicPaykitRepo.serializePayload(address) + it.methodId == MethodId.P2wpkh.rawValue && + it.endpointData == PublicPaykitRepo.serializePayload(address) } }, ) @@ -1069,14 +1060,8 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { .thenReturn( privatePaymentsPayload( listOf( - FfiPaymentEndpoint( - paymentEndpointIdentifier = MethodId.Bolt11.rawValue, - paymentEndpointPayload = TOMBSTONE_PAYLOAD, - ), - FfiPaymentEndpoint( - paymentEndpointIdentifier = MethodId.P2wpkh.rawValue, - paymentEndpointPayload = TOMBSTONE_PAYLOAD, - ), + FfiPaymentEntry(MethodId.Bolt11.rawValue, TOMBSTONE_PAYLOAD), + FfiPaymentEntry(MethodId.P2wpkh.rawValue, TOMBSTONE_PAYLOAD), ), ), ) @@ -1116,9 +1101,9 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { .thenReturn( privatePaymentsPayload( listOf( - FfiPaymentEndpoint( - paymentEndpointIdentifier = MethodId.Bolt11.rawValue, - paymentEndpointPayload = PublicPaykitRepo.serializePayload(PRIVATE_BOLT11), + FfiPaymentEntry( + MethodId.Bolt11.rawValue, + PublicPaykitRepo.serializePayload(PRIVATE_BOLT11), ), ), ), @@ -1308,9 +1293,9 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { whenever(pubkyService.getPrivatePayments(retryLinkId)).thenReturn( privatePaymentsPayload( listOf( - FfiPaymentEndpoint( - paymentEndpointIdentifier = MethodId.P2wpkh.rawValue, - paymentEndpointPayload = PublicPaykitRepo.serializePayload("bcrt1qprivate"), + FfiPaymentEntry( + methodId = MethodId.P2wpkh.rawValue, + endpointData = PublicPaykitRepo.serializePayload("bcrt1qprivate"), ), ), ), @@ -1334,54 +1319,6 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { verify(pubkyService).getPrivatePayments(retryLinkId) } - @Test - fun `beginSavedContactPayment persists remote endpoints before advancing link snapshot`() = test { - cacheData.value = PrivatePaykitCacheData( - contacts = mapOf( - CONTACT_KEY to PrivatePaykitContactCacheData( - lastLocalPayloadHash = LOCAL_PAYLOAD_HASH, - linkCompletedAt = NOW_SECONDS - 60, - ), - ), - ) - whenever(keychain.loadString(Keychain.Key.PRIVATE_PAYKIT_SECRET_STATE.name)) - .thenReturn(secretStateJson()) - whenever(keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name)).thenReturn(SECRET_KEY_HEX) - whenever(pubkyService.currentPublicKey()).thenReturn(OWN_KEY) - whenever(pubkyService.encryptedLinkSnapshotRecipient(LINK_SNAPSHOT)).thenReturn(CONTACT_KEY) - whenever(pubkyService.restoreEncryptedLink(SECRET_KEY_HEX, LINK_SNAPSHOT)).thenReturn(LINK_ID) - whenever(pubkyService.getPrivatePayments(LINK_ID)).thenReturn( - privatePaymentsPayload( - listOf( - FfiPaymentEndpoint( - paymentEndpointIdentifier = MethodId.P2wpkh.rawValue, - paymentEndpointPayload = PublicPaykitRepo.serializePayload("bcrt1qprivate"), - ), - ), - ), - ) - whenever(pubkyService.serializeEncryptedLink(LINK_ID)).thenReturn(UPDATED_LINK_SNAPSHOT) - whenever(publicPaykitRepo.payableEndpoints(any())).thenAnswer { it.getArgument>(0) } - whenever(coreService.isAddressUsed("bcrt1qprivate")).thenReturn(false) - whenever(lightningRepo.getPayments()).thenReturn(Result.success(emptyList())) - rememberSavedContact() - - val result = sut.beginSavedContactPayment(CONTACT_KEY).getOrThrow() - val snapshot = sut.backupSnapshot().getOrThrow()?.get(CONTACT_KEY) - - assertTrue(result is PublicPaykitPaymentResult.Opened) - assertNotNull(snapshot) - assertEquals( - mapOf(MethodId.P2wpkh.rawValue to PublicPaykitRepo.serializePayload("bcrt1qprivate")), - snapshot.remoteEndpoints, - ) - - val order = inOrder(pubkyService, cacheStore) - order.verify(pubkyService).getPrivatePayments(LINK_ID) - order.verify(cacheStore).update(any()) - order.verify(pubkyService).serializeEncryptedLink(LINK_ID) - } - @Test fun `isDuplicatePaymentError detects wrapped duplicate payment messages`() { val error = PrivatePaykitTestError("service queue failed", cause = AppError("Duplicate payment.")) @@ -1529,27 +1466,10 @@ class PrivatePaykitRepoTest : BaseUnitTest(StandardTestDispatcher()) { payeeNodeId = null, ) - private fun privatePaymentListPayloadSize(bolt11: String, address: String) = buildJsonObject { - put("version", 1) - put("kind", "paykit.private_payment_list") - putJsonObject("payment_endpoints") { - put(MethodId.Bolt11.rawValue, PublicPaykitRepo.serializePayload(bolt11)) - put(MethodId.P2wpkh.rawValue, PublicPaykitRepo.serializePayload(address)) - } - }.toString().encodeToByteArray().size - - private fun legacyPrivatePaymentsPayloadSize(bolt11: String, address: String) = buildJsonObject { - put("version", 1) - put("kind", "paykit.private_payments") - put("reference", "550e8400-e29b-41d4-a716-446655440000") - putJsonObject("entries") { - put(MethodId.Bolt11.rawValue, PublicPaykitRepo.serializePayload(bolt11)) - put(MethodId.P2wpkh.rawValue, PublicPaykitRepo.serializePayload(address)) - } - }.toString().encodeToByteArray().size - - private fun privatePaymentsPayload(entries: List) = - FfiPrivatePaymentList(paymentEndpoints = entries) + private fun privatePaymentsPayload(entries: List) = FfiPrivatePaymentsPayload( + reference = "550e8400-e29b-41d4-a716-446655440000", + entries = entries, + ) private fun secretStateJson( linkSnapshotHex: String? = LINK_SNAPSHOT, diff --git a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt index e0ede78a20..63b5a9f66c 100644 --- a/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PubkyRepoTest.kt @@ -4,7 +4,7 @@ import app.cash.turbine.test import coil3.ImageLoader import coil3.disk.DiskCache import coil3.memory.MemoryCache -import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPaymentEntry import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking @@ -1209,9 +1209,9 @@ class PubkyRepoTest : BaseUnitTest() { sut.completeAuthentication() } - private fun paymentEntry(methodId: MethodId) = FfiPaymentEndpoint( - paymentEndpointIdentifier = methodId.rawValue, - paymentEndpointPayload = """{"value":"value"}""", + private fun paymentEntry(methodId: MethodId) = FfiPaymentEntry( + methodId = methodId.rawValue, + endpointData = """{"value":"value"}""", ) private suspend fun startAuthForTesting(authUri: String = "auth_uri") { diff --git a/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt b/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt index ffadd9e7f0..ff072126c4 100644 --- a/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PublicPaykitRepoTest.kt @@ -3,7 +3,7 @@ package to.bitkit.repositories import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.NetworkType import com.synonym.bitkitcore.Scanner -import com.synonym.paykit.FfiPaymentEndpoint +import com.synonym.paykit.FfiPaymentEntry import kotlinx.coroutines.flow.MutableStateFlow import org.junit.After import org.junit.Before @@ -446,9 +446,9 @@ class PublicPaykitRepoTest : BaseUnitTest() { clock = clock, ) - private fun paymentEntry(methodId: MethodId, value: String) = FfiPaymentEndpoint( - paymentEndpointIdentifier = methodId.rawValue, - paymentEndpointPayload = """{"value":"$value"}""", + private fun paymentEntry(methodId: MethodId, value: String) = FfiPaymentEntry( + methodId = methodId.rawValue, + endpointData = """{"value":"$value"}""", ) private fun setSettings(settings: SettingsData) { diff --git a/app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt b/app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt deleted file mode 100644 index 5e46316219..0000000000 --- a/app/src/test/java/to/bitkit/services/PrivatePaykitMessageParserTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package to.bitkit.services - -import com.synonym.paykit.FfiPaymentEndpoint -import com.synonym.paykit.FfiPrivatePaymentList -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class PrivatePaykitMessageParserTest { - @Test - fun `parsePrivatePaymentListJson keeps current Paykit parser result`() { - val parsed = FfiPrivatePaymentList( - paymentEndpoints = listOf( - FfiPaymentEndpoint( - paymentEndpointIdentifier = "bolt11-invoice-0", - paymentEndpointPayload = "lnbcrt1current", - ), - ), - ) - - val result = PrivatePaykitMessageParser.parsePrivatePaymentListJson("{}") { parsed } - - assertEquals(parsed, result) - } - - @Test - fun `parsePrivatePaymentListJson falls back to legacy private payments envelope`() { - val legacyJson = """ - { - "version": 1, - "kind": "paykit.private_payments", - "reference": "550e8400-e29b-41d4-a716-446655440000", - "entries": { - "bolt11-invoice-0": "lnbcrt1legacy", - "bip21-p2wpkh-0": "bcrt1qlegacy" - } - } - """.trimIndent() - - val result = PrivatePaykitMessageParser.parsePrivatePaymentListJson(legacyJson) { - throw IllegalArgumentException("unsupported legacy envelope") - } - - assertEquals( - listOf( - FfiPaymentEndpoint( - paymentEndpointIdentifier = "bolt11-invoice-0", - paymentEndpointPayload = "lnbcrt1legacy", - ), - FfiPaymentEndpoint( - paymentEndpointIdentifier = "bip21-p2wpkh-0", - paymentEndpointPayload = "bcrt1qlegacy", - ), - ), - result?.paymentEndpoints, - ) - } - - @Test - fun `parsePrivatePaymentListJson ignores unknown legacy envelopes`() { - val result = PrivatePaykitMessageParser.parsePrivatePaymentListJson("""{"kind":"other"}""") { - throw IllegalArgumentException("unsupported envelope") - } - - assertNull(result) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4d5584034..803c42bdce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.73" } -paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc19" } +paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc8" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index 144abc163f..514f2ade33 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -24,7 +24,7 @@ fi output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols-$build_number.zip" output_dir=$(dirname "$output") dependency_symbols_dir="app/build/intermediates/native-debug-symbol-artifacts" -required_libs="libbitkitcore.so libldk_node.so libpaykit.so libvss_rust_client_ffi.so" +required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so" archive_symbol_suffixes=".dbg .sym" tmp_dirs="" From b6ba37b8c477bded17efd323a8ea336a866d1a9a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 17 Jun 2026 18:57:04 +0200 Subject: [PATCH 21/22] fix: consume final symbol deps --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 803c42bdce..40ca00380e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.50" } +ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.51" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } @@ -88,7 +88,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.19" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.20" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } From 4cd95d1002e9d365d70ec3254c4542ee90e0e9a8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 17 Jun 2026 19:03:34 +0200 Subject: [PATCH 22/22] fix: harden symbol archive script --- scripts/create-native-debug-symbols.sh | 39 +++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh index 514f2ade33..db33ce798c 100755 --- a/scripts/create-native-debug-symbols.sh +++ b/scripts/create-native-debug-symbols.sh @@ -16,10 +16,12 @@ build_number=$( } ' app/build.gradle.kts ) -if [ -z "$build_number" ]; then - echo "Unable to read versionCode from app/build.gradle.kts." >&2 - exit 1 -fi +case "$build_number" in + ''|*[!0-9]*) + echo "Unable to read numeric versionCode from app/build.gradle.kts." >&2 + exit 1 + ;; +esac output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols-$build_number.zip" output_dir=$(dirname "$output") @@ -150,15 +152,14 @@ copy_archive_symbols() { for lib_name in $required_libs; do copied=false entry="$abi/$lib_name" - if unzip -qo "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + if copy_archive_entry "$archive" "$tmp_dir" "$abi" "$lib_name" "$entry"; then copied=true fi if [ "$copied" = false ]; then for suffix in $archive_symbol_suffixes; do entry="$abi/$lib_name$suffix" - if unzip -qo "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then - mv "$tmp_dir/$entry" "$tmp_dir/$abi/$lib_name" + if copy_archive_entry "$archive" "$tmp_dir" "$abi" "$lib_name" "$entry"; then copied=true break fi @@ -168,6 +169,30 @@ copy_archive_symbols() { done } +copy_archive_entry() { + archive="$1" + tmp_dir="$2" + abi="$3" + lib_name="$4" + entry="$5" + output_lib="$tmp_dir/$abi/$lib_name" + + if ! unzip -Z -1 "$archive" "$entry" >/dev/null 2>&1; then + return 1 + fi + + if [ -f "$output_lib" ]; then + echo "Duplicate native debug symbol entry '$abi/$lib_name' found while reading '$archive'." >&2 + echo "Refusing to overwrite symbol metadata from an earlier archive." >&2 + exit 1 + fi + + unzip -q "$archive" "$entry" -d "$tmp_dir" + if [ "$entry" != "$abi/$lib_name" ]; then + mv "$tmp_dir/$entry" "$output_lib" + fi +} + validate_output_zip() { archive="$1" zip -T "$archive" >/dev/null