Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/img/detekt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- ALWAYS be mindful of thread safety when working with mutable lists & state
- ALWAYS split screen composables into parent accepting viewmodel + inner private child accepting state and callbacks `Content()`
- ALWAYS name lambda parameters in a composable function using present tense, NEVER use past tense
- ALWAYS list 3 suggested commit messages after implementation work for the entire set of uncommitted changes
- ALWAYS list 3 suggested commit messages after implementation work for ALL uncommitted changes
- NEVER use `wheneverBlocking` in unit test expression body functions wrapped in a `= test {}` lambda
- ALWAYS wrap unit tests `setUp` methods mocking suspending calls with `runBlocking`, e.g `setUp() = runBlocking { }`
- ALWAYS add business logic to Repository layer via methods returning `Result<T>` and use it in ViewModels
Expand Down
65 changes: 36 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ This repository contains a **new native Android app** which is **not ready for p

#### 1. Firebase Configuration

Download `google-services.json` from the Firebase Console for each build flavor:
- **Dev/Testnet**: Place in `app/` (default location)
- **Mainnet**: Place in `app/src/mainnet/google-services.json`
Download `google-services.json` from the Firebase Console for each of the following build flavor groups,:
- dev/tnet/mainnetDebug: Place in `app/google-services.json`
- mainnetRelease: Place in `app/src/mainnetRelease/google-services.json`

> **Note**: Each flavor requires its own Firebase project configuration. The mainnet flavor will fail to build without its dedicated `google-services.json` file.

Expand Down Expand Up @@ -57,9 +57,16 @@ See also:

This project uses detekt with default ktlint and compose-rules for android code linting.

Recommended Android Studio plugins:
- EditorConfig
- Detekt
### IDE Plugins
The following IDE plugins are recommended for development with Android Studio or IntelliJ IDEA:
- [Compose Color Preview](https://plugins.jetbrains.com/plugin/21298-compose-color-preview)
- [Compose Stability Analyzer](https://plugins.jetbrains.com/plugin/28767-compose-stability-analyzer)
- [detekt](https://plugins.jetbrains.com/plugin/10761-detekt)
<details>
<summary>See screenshot on how to setup the Detekt plugin after installation.</summary>

![Detekt plugin setup][img_detekt]
</details>

**Commands**
```sh
Expand Down Expand Up @@ -112,16 +119,32 @@ The build config supports building 3 different apps for the 3 bitcoin networks (
- `mainnet` flavour = mainnet
- `tnet` flavour = testnet

### Build for Mainnet
### Build for Internal Testing

To build the mainnet flavor:
**Prerequisites**
Setup the signing config:
- Add the keystore file to root dir (i.e. `internal.keystore`)
- Setup `keystore.properties` file in root dir (`cp keystore.properties.template keystore.properties`)

**Routine**

Increment `versionCode` and `versionName` in `app/build.gradle.kts`, then run:
```sh
./gradlew assembleMainnetDebug # debug build
./gradlew assembleMainnetRelease # release build (requires signing config)
./gradlew assembleDevRelease
#./gradlew assembleMainnetDebug # mainnet debug build
# ./gradlew assembleRelease # for all flavors
```

> **Important**: Ensure `app/src/mainnet/google-services.json` exists before building. See [Firebase Configuration](#1-firebase-configuration).
APK is generated in `app/build/outputs/apk/_flavor_/release`. (`_flavor_` can be any of 'dev', 'mainnet', 'tnet').
Example for dev: `app/build/outputs/apk/dev/release`

### Build for Release

To build the mainnet flavor for release run:

```sh
./gradlew assembleMainnetRelease
```

### Build for E2E Testing

Expand Down Expand Up @@ -152,24 +175,6 @@ By default, geoblocking checks via API are enabled. To disable at build time, us
GEO=false E2E=true ./gradlew assembleDevRelease
```

### Build for Release

**Prerequisites**
Setup the signing config:
- Add the keystore file to root dir (i.e. `release.keystore`)
- Setup `keystore.properties` file in root dir (`cp keystore.properties.template keystore.properties`)

**Routine**

Increment `versionCode` and `versionName` in `app/build.gradle.kts`, then run:
```sh
./gradlew assembleDevRelease
# ./gradlew assembleRelease # for all flavors
```

APK is generated in `app/build/outputs/apk/_flavor_/release`. (`_flavor_` can be any of 'dev', 'mainnet', 'tnet').
Example for dev: `app/build/outputs/apk/dev/release`

## Contributing

### AI Code Review with Claude
Expand Down Expand Up @@ -223,3 +228,5 @@ Destructive operations like `rm -rf`, `git commit`, and `git push` still require

This project is licensed under the MIT License.
See the [LICENSE](./LICENSE) file for more details.

[img_detekt]: .github/img/detekt.png
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.compose.stability.analyzer)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
Expand Down Expand Up @@ -219,6 +220,7 @@ dependencies {
androidTestImplementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.runtime.tracing)
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
Expand Down
62 changes: 0 additions & 62 deletions app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt

This file was deleted.

9 changes: 5 additions & 4 deletions app/src/androidTest/java/to/bitkit/services/TxBumpingTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ class TxBumpingTests {
assertEquals(depositAmount, totalBalance, "Balance should equal deposit amount")

// Send a transaction with a low fee rate
@Suppress("SpellCheckingInspection")
val destinationAddress = "bcrt1qs04g2ka4pr9s3mv73nu32tvfy7r3cxd27wkyu8"
val sendAmount = 10_000uL // Send 10,000 sats
val lowFeeRate = 1u // 1 sat/vbyte (very low)
val lowFeeRate = 1uL // 1 sat/vbyte (very low)

println("Sending $sendAmount sats to $destinationAddress with low fee rate of $lowFeeRate sat/vbyte")
val originalTxId = lightningService.send(
Expand All @@ -160,7 +161,7 @@ class TxBumpingTests {
println("Wait completed")

// Bump the fee using RBF with a higher fee rate
val highFeeRate = 10u // 10 sat/vbyte (much higher)
val highFeeRate = 10uL // 10 sat/vbyte (much higher)
println("Bumping fee for transaction $originalTxId to $highFeeRate sat/vbyte using RBF")

val replacementTxId = lightningService.bumpFeeByRbf(
Expand Down Expand Up @@ -261,7 +262,7 @@ class TxBumpingTests {

// Now use CPFP to spend from the incoming transaction with high fees
// This demonstrates using CPFP to quickly move received funds
val highFeeRate = 20u // 20 sat/vbyte (very high for fast confirmation)
val highFeeRate = 20uL // 20 sat/vbyte (very high for fast confirmation)
println("Using CPFP to quickly spend from incoming transaction $stuckIncomingTxId with $highFeeRate sat/vbyte")

// Generate a destination address for the CPFP transaction (where we'll send the funds)
Expand All @@ -272,7 +273,7 @@ class TxBumpingTests {
val childTxId = lightningService.accelerateByCpfp(
txid = stuckIncomingTxId,
satsPerVByte = highFeeRate,
destinationAddress = cpfpDestinationAddress,
toAddress = cpfpDestinationAddress,
)

assertFalse(childTxId.isEmpty(), "CPFP child transaction ID should not be empty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.PrimaryDisplay
import to.bitkit.repositories.CurrencyState
import to.bitkit.viewmodels.MainUiState
import to.bitkit.viewmodels.SendMethod
import to.bitkit.viewmodels.SendUiState
import to.bitkit.viewmodels.previewAmountInputViewModel
Expand All @@ -19,22 +16,20 @@ class SendAmountContentTest {
@get:Rule
val composeTestRule = createComposeRule()

private val testUiState = SendUiState(
private val uiState = SendUiState(
payMethod = SendMethod.LIGHTNING,
amount = 100u,
isUnified = true
)

private val testWalletState = MainUiState(
nodeLifecycleState = NodeLifecycleState.Running
)
private val nodeLifecycleState = NodeLifecycleState.Running

@Test
fun whenScreenLoaded_shouldShowAllComponents() {
composeTestRule.setContent {
SendAmountContent(
walletUiState = testWalletState,
uiState = testUiState,
nodeLifecycleState = nodeLifecycleState,
uiState = uiState,
amountInputViewModel = previewAmountInputViewModel(),
)
}
Expand All @@ -51,10 +46,8 @@ class SendAmountContentTest {
fun whenNodeNotRunning_shouldShowSyncView() {
composeTestRule.setContent {
SendAmountContent(
walletUiState = MainUiState(
nodeLifecycleState = NodeLifecycleState.Initializing
),
uiState = testUiState,
nodeLifecycleState = nodeLifecycleState,
uiState = uiState,
amountInputViewModel = previewAmountInputViewModel(),
)
}
Expand All @@ -68,15 +61,14 @@ class SendAmountContentTest {
var eventTriggered = false
composeTestRule.setContent {
SendAmountContent(
walletUiState = testWalletState,
uiState = testUiState,
nodeLifecycleState = nodeLifecycleState,
uiState = uiState,
amountInputViewModel = previewAmountInputViewModel(),
onClickPayMethod = { eventTriggered = true }
)
}

composeTestRule.onNodeWithTag("AssetButton-switch")
.performClick()
composeTestRule.onNodeWithTag("AssetButton-switch").performClick()

assert(eventTriggered)
}
Expand All @@ -86,8 +78,8 @@ class SendAmountContentTest {
var eventTriggered = false
composeTestRule.setContent {
SendAmountContent(
walletUiState = testWalletState,
uiState = testUiState,
nodeLifecycleState = nodeLifecycleState,
uiState = uiState,
amountInputViewModel = previewAmountInputViewModel(),
onContinue = { eventTriggered = true }
)
Expand All @@ -103,8 +95,8 @@ class SendAmountContentTest {
fun whenAmountInvalid_continueButtonShouldBeDisabled() {
composeTestRule.setContent {
SendAmountContent(
walletUiState = testWalletState,
uiState = testUiState.copy(amount = 0u),
nodeLifecycleState = nodeLifecycleState,
uiState = uiState.copy(amount = 0u),
amountInputViewModel = previewAmountInputViewModel(),
)
}
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/to/bitkit/data/AppDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import to.bitkit.data.dao.TransferDao
import to.bitkit.data.entities.ConfigEntity
import to.bitkit.data.entities.TransferEntity
import to.bitkit.data.typeConverters.StringListConverter
import to.bitkit.env.Env

@Database(
entities = [
Expand Down Expand Up @@ -53,7 +54,6 @@ abstract class AppDb : RoomDatabase() {
private fun buildDatabase(context: Context): AppDb {
return Room.databaseBuilder(context, AppDb::class.java, DB_NAME)
.setJournalMode(JournalMode.TRUNCATE)
.fallbackToDestructiveMigration() // TODO remove in prod
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Expand All @@ -65,6 +65,9 @@ abstract class AppDb : RoomDatabase() {
}
}
})
.apply {
if (Env.isDebug) fallbackToDestructiveMigration(dropAllTables = true)
}
.build()
}
}
Expand Down
Loading
Loading