diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md deleted file mode 100644 index d5b507c1c..000000000 --- a/.claude/commands/pr.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -description: Create a PR on GitHub for the current branch -argument_hint: "[branch] [--dry] [--draft]" -allowed_tools: Bash, Read, Glob, Grep, Write, AskUserQuestion, mcp__github__create_pull_request, mcp__github__list_pull_requests, mcp__github__get_file_contents, mcp__github__issue_read ---- - -Create a PR on GitHub using the `gh` CLI for the currently checked-out branch. - -**Examples:** -- `/pr` - Interactive mode, prompts for PR type -- `/pr master` - Interactive with explicit base branch -- `/pr --dry` - Generate description only, save to `.ai/` -- `/pr --draft` - Create as draft PR -- `/pr develop --draft` - Draft PR against non-default branch - -## Steps - -### 1. Check for Existing PR -Run `gh pr view --json number,url 2>/dev/null` to check if a PR already exists for this branch. -- If PR exists: Output `PR already exists: [URL]` and stop -- If no PR: Continue - -### 2. Parse Arguments -- `--dry`: Skip PR creation, only generate and save description -- `--draft`: Create PR as draft -- First non-flag argument: base branch (default: auto-detected, see Step 2.5) -- **If no flags provided**: Use `AskUserQuestion` to prompt user: - - Open PR (create and publish) - - Draft PR (create as draft) - - Dry run (save locally only) - -### 2.5. Determine Base Branch -If no base branch argument provided, detect the repo's default branch: -- Run: `git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'` -- Use result as default (typically `main` or `master`) -- If command fails, fall back to `master` - -### 3. Gather Context -- Get current branch name: `git branch --show-current` -- Extract repo identifier: `git remote get-url origin | sed 's/\.git$//' | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#'` (e.g., `synonymdev/bitkit-android`) -- Read PR template from `.github/pull_request_template.md` -- Fetch 10 most recent PRs (open or closed) from the extracted repo for writing style reference -- Run `git log $base..HEAD --oneline` for commit messages -- Run `git diff $base...HEAD --stat` for understanding scope of changes - -### 4. Extract Linked Issues -Scan commits for issue references: -- Pattern to match: `#123` (just the issue number reference) -- Extract unique issue numbers: `git log $base..HEAD --oneline | grep -oE "#[0-9]+" | sort -u` -- Fetch each issue title: `gh api "repos/$REPO/issues/NUMBER" --jq '.title'` (using repo from Step 3) -- These will be used to start the PR description with linking keywords (see Step 6) - -### 5. Identify Suggested Reviewers -Find potential reviewers based on: -- `.github/CODEOWNERS` file patterns (if exists) -- Recent contributors to changed files: `git log --format='%an' -- $(git diff $base..HEAD --name-only) | sort | uniq -c | sort -rn | head -3` -- Exclude the current user from suggestions - -### 6. Generate PR Description -Starting from the template in `.github/pull_request_template.md`: - -**Title Rules:** -- Format: `prefix: title` (e.g., `feat: add user settings screen`) -- Keep under 50 characters -- Use branch name as concept inspiration -- Prefixes: `feat`, `fix`, `chore`, `refactor`, `docs`, `test` - -**Issue Linking (at the very start):** -If linked issues were found in commit messages, begin the PR description with linking keywords: -- Use `Fixes #123` for bug fixes -- Use `Closes #123` for features/enhancements -- One per line, before the "This PR..." opening separated by one empty line -- Reference: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests - -Example: -``` -Fixes #528 -Closes #418 - -This PR adds support for... -``` - -**Opening Format:** -- Single change: Start with "This PR [verb]s..." as a complete sentence - - Example: `This PR adds a Claude Code /pr command for generating PRs.` -- Multiple changes: Start with "This PR:" followed by a numbered list - - Example: - ``` - This PR: - 1. Adds a Claude Code /pr command for generating PRs - 2. Fixes issue preventing Claude Code reviews to be added as PR comments - 3. Updates reviews workflow to minimize older review comments - ``` -- Each list item should start with a verb (Adds, Fixes, Updates, Removes, Refactors, etc.) - -**Description Rules:** -- Base content around all commit messages in the branch -- Use branch name as the conceptual anchor -- Match writing style of recent PRs -- Focus on functionality over technical details -- Avoid excessive bold formatting like `**this:** that` -- Minimize code and file references like `TheClassName` or `someFunctionName`, `thisFileName.ext` -- Exception: for refactoring PRs (1:10 ratio of functionality to code changes), more technical detail is ok - -**QA Notes / Testing Scenarios:** -- Structure with numbered headings and steps -- Make steps easily referenceable -- Be specific about what to test and expected outcomes - -**For library repos (has `bindings/` directory or `Cargo.toml`):** -Structure QA Notes around testing and integration: - -Example: -``` -### QA Notes - -#### Testing -- [ ] `cargo test` passes -- [ ] `cargo clippy` clean -- [ ] Android bindings: `./build_android.sh` -- [ ] iOS bindings: `./build_ios.sh` - -#### Integration -- Tested in: [bitkit-android#XXX](link) -- Or N/A if internal refactor with no API changes -``` - -**Preview Section (conditional):** -Only include if the PR template (`.github/pull_request_template.md`) contains a `### Preview` heading: -- Create placeholders for media: `IMAGE_1`, `VIDEO_2`, etc. -- Add code comment under each placeholder describing what it should show -- Example: `` - -### 7. Save PR Description -Before creating the PR: -- Get next PR number: `gh api "repos/$REPO/issues?per_page=1&state=all&sort=created&direction=desc" --jq '.[0].number'` then add 1 (using repo from Step 3) -- Create `.ai/` directory if it doesn't exist -- Save to `.ai/pr_NN.md` where `NN` is the predicted PR number - -### 8. Create the PR (unless --dry) -If not dry run: -```bash -gh pr create --base $base --title "..." --body "..." [--draft] -``` -- Add `--draft` flag if draft mode selected -- If actual PR number differs from predicted, rename the saved file - -### 9. Output Summary - -**If PR created:** -``` -PR Created: [PR URL] -Saved: .ai/pr_NN.md - -Suggested reviewers: -- @username1 (X files modified recently) -- @username2 (CODEOWNER) -``` - -**If dry run:** -``` -Dry run complete -Saved: .ai/pr_NN.md - -To create PR: /pr [--draft] - -Suggested reviewers: -- @username1 (X files modified recently) -- @username2 (CODEOWNER) -``` - -**Media TODOs (only if Preview section was included):** -If the PR description includes a Preview section with media placeholders, append: -``` -## TODOs -- [ ] IMAGE_1: [description] -- [ ] VIDEO_2: [description] -``` -List all media placeholders as TODOs with their descriptions. diff --git a/AGENTS.md b/AGENTS.md index e70f3c16a..ed2897729 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,6 +206,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS use template in `.github/pull_request_template.md` for PR descriptions - ALWAYS wrap `ULong` numbers with `USat` in arithmetic operations, to guard against overflows - PREFER to use one-liners with `run { }` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }` +- ALWAYS add imports instead of inline fully-qualified names ### Architecture Guidelines diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4450eab5b..b6ae8ab52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,8 +46,8 @@ android { applicationId = "to.bitkit" minSdk = 28 targetSdk = 36 - versionCode = 162 - versionName = "0.0.17" + versionCode = 163 + versionName = "0.0.18" testInstrumentationRunner = "to.bitkit.test.HiltTestRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/libs/LDK-release.aar b/app/libs/LDK-release.aar deleted file mode 100644 index e0b883249..000000000 Binary files a/app/libs/LDK-release.aar and /dev/null differ diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index 7aa91d0d5..af0aad5b4 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -1,26 +1,13 @@ package to.bitkit.ext -import java.security.MessageDigest import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -// region hex -@OptIn(ExperimentalStdlibApi::class) fun ByteArray.toHex(): String = this.toHexString() -@OptIn(ExperimentalStdlibApi::class) fun String.fromHex(): ByteArray = this.hexToByteArray() -// endregion -@OptIn(ExperimentalEncodingApi::class) fun ByteArray.toBase64(): String = Base64.encode(this) -@OptIn(ExperimentalEncodingApi::class) fun String.fromBase64(): ByteArray = Base64.decode(this) val String.uByteList get() = this.toByteArray().map { it.toUByte() } - -fun ByteArray.toSha256(): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(this) -} diff --git a/app/src/main/java/to/bitkit/services/RNBackupClient.kt b/app/src/main/java/to/bitkit/services/RNBackupClient.kt index fc1062e6c..63e62b27b 100644 --- a/app/src/main/java/to/bitkit/services/RNBackupClient.kt +++ b/app/src/main/java/to/bitkit/services/RNBackupClient.kt @@ -17,15 +17,13 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import org.bouncycastle.crypto.digests.SHA512Digest -import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator -import org.bouncycastle.crypto.params.KeyParameter -import org.ldk.structs.KeysManager import org.lightningdevkit.ldknode.Network +import org.lightningdevkit.ldknode.deriveNodeSecretFromMnemonic import to.bitkit.data.keychain.Keychain import to.bitkit.di.IoDispatcher import to.bitkit.di.json import to.bitkit.env.Env +import to.bitkit.ext.toHex import to.bitkit.utils.AppError import to.bitkit.utils.Crypto import to.bitkit.utils.Logger @@ -275,44 +273,11 @@ class RNBackupClient @Inject constructor( return crypto.sign(fullMessage, privateKey) } - private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray { - val bip39Seed = deriveSeed(mnemonic, passphrase) - val bip32Seed = deriveMasterKey(bip39Seed) - val seconds = System.currentTimeMillis() / 1000L - val nanoSeconds = ((System.currentTimeMillis() % 1000) * 1_000_000).toInt() - - return runCatching { - val keysManager = KeysManager.of(bip32Seed, seconds, nanoSeconds) - val method = keysManager.javaClass.getMethod("get_node_secret_key") - when (val nodeSecretKey = method.invoke(keysManager)) { - is ByteArray -> nodeSecretKey - is List<*> -> nodeSecretKey.map { (it as UByte).toByte() }.toByteArray() - else -> throw ClassCastException("Unexpected type: ${nodeSecretKey?.javaClass?.name}") - } - }.getOrElse { bip32Seed } - } + private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray = + deriveNodeSecretFromMnemonic(mnemonic, passphrase).map { it.toByte() }.toByteArray() - private fun deriveEncryptionKey(mnemonic: String, passphrase: String?): ByteArray { - // Match iOS: use the same node secret key as signing key for encryption - // iOS uses SymmetricKey(data: secretKey) where secretKey is the node secret key - return deriveSigningKey(mnemonic, passphrase) - } - - private fun deriveSeed(mnemonic: String, passphrase: String?): ByteArray { - val mnemonicBytes = mnemonic.toByteArray(Charsets.UTF_8) - val salt = ("mnemonic" + (passphrase ?: "")).toByteArray(Charsets.UTF_8) - val generator = PKCS5S2ParametersGenerator(SHA512Digest()) - generator.init(mnemonicBytes, salt, 2048) - return (generator.generateDerivedParameters(512) as KeyParameter).key - } - - private fun deriveMasterKey(seed: ByteArray): ByteArray { - val hmac = javax.crypto.Mac.getInstance("HmacSHA512") - val keySpec = javax.crypto.spec.SecretKeySpec("Bitcoin seed".toByteArray(), "HmacSHA512") - hmac.init(keySpec) - val i = hmac.doFinal(seed) - return i.sliceArray(0 until 32) - } + private fun deriveEncryptionKey(mnemonic: String, passphrase: String?): ByteArray = + deriveSigningKey(mnemonic, passphrase) private fun decrypt(blob: ByteArray, encryptionKey: ByteArray): ByteArray { if (blob.size < GCM_IV_LENGTH + GCM_TAG_LENGTH) { @@ -331,10 +296,6 @@ class RNBackupClient @Inject constructor( return cipher.doFinal(ciphertext + tag) } - - private fun ByteArray.toHex(): String { - return this.joinToString("") { "%02x".format(it) } - } } @Serializable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a95f687e5..1ae369060 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,17 +15,17 @@ haze = "1.7.1" [libraries] accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version = "0.36.0" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" } -activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.1" } +activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.2" } 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-alpha04" } +biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.33" } 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" } camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" } # https://developer.android.com/develop/ui/compose/bom/bom-mapping -compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.00" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.01" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -39,8 +39,8 @@ core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" } core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.1" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.6.0" } +detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.3" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.7.0" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -57,7 +57,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.github.synonymdev:ldk-node", version = "v0.7.0-rc.2" } # fork | local: remove `v` +ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.7.0-rc.3" } # fork | local: remove `v` 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" }