Skip to content

Commit 717f943

Browse files
authored
Merge pull request #536 from synonymdev/mainnet
feat: Setup mainnet config
2 parents e70592c + 3f2245f commit 717f943

File tree

32 files changed

+1215
-251
lines changed

32 files changed

+1215
-251
lines changed

.idea/codeStyles/Project.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AGENTS.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ GEO=false E2E=true ./gradlew assembleDevRelease
4545
## Architecture Overview
4646

4747
### Tech Stack
48+
4849
- **Language**: Kotlin
4950
- **UI Framework**: Jetpack Compose with Material3
5051
- **Architecture**: MVVM with Hilt dependency injection
@@ -57,6 +58,7 @@ GEO=false E2E=true ./gradlew assembleDevRelease
5758
- **Storage**: DataStore with json files
5859

5960
### Project Structure
61+
6062
- **app/src/main/java/to/bitkit/**
6163
- **App.kt**: Application class with Hilt setup
6264
- **ui/**: All UI components
@@ -75,34 +77,42 @@ GEO=false E2E=true ./gradlew assembleDevRelease
7577
- **usecases/**: Domain layer: use cases
7678

7779
### Key Architecture Patterns
80+
7881
1. **Single Activity Architecture**: MainActivity hosts all screens via Compose Navigation
7982
2. **Repository Pattern**: Repositories abstract data sources from ViewModels
8083
3. **Service Layer**: Core business logic in services (LightningService, WalletService)
8184
4. **Reactive State Management**: ViewModels expose UI state via StateFlow
8285
5. **Coroutine-based Async**: All async operations use Kotlin coroutines
8386

8487
### Build Variants
88+
8589
- **dev**: Regtest network for development
8690
- **tnet**: Testnet network
8791
- **mainnet**: Production (currently commented out)
8892

8993
## Common Pitfalls
9094

9195
### ❌ DON'T
96+
9297
```kotlin
9398
GlobalScope.launch { } // Use viewModelScope
9499
val result = nullable!!.doSomething() // Use safe calls
95100
Text("Send Payment") // Use string resources
96101
class Service(@Inject val vm: ViewModel) // Never inject VMs
102+
97103
suspend fun getData() = runBlocking { } // Use withContext
98104
```
99105

100106
### ✅ DO
107+
101108
```kotlin
102109
viewModelScope.launch { }
103110
val result = nullable?.doSomething() ?: default
104111
Text(stringResource(R.string.send_payment))
105-
class Service { fun process(data: Data) }
112+
class Service {
113+
fun process(data: Data)
114+
}
115+
106116
suspend fun getData() = withContext(Dispatchers.IO) { }
107117
```
108118

@@ -117,29 +127,32 @@ suspend fun getData() = withContext(Dispatchers.IO) { }
117127
## Common Patterns
118128

119129
### ViewModel State
130+
120131
```kotlin
121132
private val _uiState = MutableStateFlow(InitialState)
122133
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
123134

124135
fun updateState(action: Action) {
125-
viewModelScope.launch {
126-
_uiState.update { it.copy(/* fields */) }
127-
}
136+
viewModelScope.launch {
137+
_uiState.update { it.copy(/* fields */) }
138+
}
128139
}
129140
```
130141

131142
### Repository
143+
132144
```kotlin
133145
suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
134-
runCatching {
135-
Result.success(apiService.fetchData())
136-
}.onFailure { e ->
137-
Logger.error("Failed", e = e, context = TAG)
138-
}
146+
runCatching {
147+
Result.success(apiService.fetchData())
148+
}.onFailure { e ->
149+
Logger.error("Failed", e = e, context = TAG)
150+
}
139151
}
140152
```
141153

142154
### Rules
155+
143156
- USE coding rules from `.cursor/default.rules.mdc`
144157
- ALWAYS run `./gradlew compileDevDebugKotlin` after code changes to verify code compiles
145158
- ALWAYS run `./gradlew testDevDebugUnitTest` after code changes to verify tests succeed and fix accordingly
@@ -150,7 +163,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
150163
- USE `git diff HEAD sourceFilePath` to diff an uncommitted file against the last commit
151164
- ALWAYS check existing code patterns before implementing new features
152165
- USE existing extensions and utilities rather than creating new ones
153-
- ALWAYS consider applying YAGNI (You Aren't Gonna Need It) principle for new code
166+
- ALWAYS consider applying YAGNI (You Aren't Gonna Need It) principle for new code
154167
- ALWAYS reuse existing constants
155168
- ALWAYS ensure a method exist before calling it
156169
- ALWAYS remove unused code after refactors
@@ -161,6 +174,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
161174
- ALWAYS pass the TAG as context to `Logger` calls, e.g. `Logger.debug("message", context = TAG)`
162175
- ALWAYS use the Result API instead of try-catch
163176
- NEVER wrap methods returning `Result<T>` in try-catch
177+
- PREFER to use `it` instead of explicit named parameters in lambdas e.g. `fn().onSuccess { log(it) }.onFailure { log(it) }`
164178
- NEVER inject ViewModels as dependencies - Only android activities and composable functions can use viewmodels
165179
- NEVER hardcode strings and always preserve string resources
166180
- ALWAYS localize in ViewModels using injected `@ApplicationContext`, e.g. `context.getString()`
@@ -179,15 +193,19 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
179193
- ALWAYS split screen composables into parent accepting viewmodel + inner private child accepting state and callbacks `Content()`
180194
- ALWAYS name lambda parameters in a composable function using present tense, NEVER use past tense
181195
- ALWAYS list 3 suggested commit messages after implementation work
182-
- NEVER use `wheneverBlocking` when in an unit test where you're using expression body and already wrapping the test with a `= test {}` lambda.
196+
- NEVER use `wheneverBlocking` in unit test expression body functions wrapped in a `= test {}` lambda
197+
- ALWAYS wrap unit tests `setUp` methods mocking suspending calls with `runBlocking`, e.g `setUp() = runBlocking { }`
183198
- ALWAYS add business logic to Repository layer via methods returning `Result<T>` and use it in ViewModels
184199
- ALWAYS use services to wrap RUST code exposed via bindings
185200
- ALWAYS order upstream architectural data flow this way: `UI -> ViewModel -> Repository -> RUST` and vice-versa for downstream
186-
- ALWAYS add new string string resources in alphabetical order in `strings.xml`
201+
- ALWAYS add new localizable string string resources in alphabetical order in `strings.xml`
202+
- NEVER add string resources for strings used only in dev settings screens and previews and never localize acronyms
187203
- ALWAYS use template in `.github/pull_request_template.md` for PR descriptions
188204
- ALWAYS wrap `ULong` numbers with `USat` in arithmetic operations, to guard against overflows
205+
- PREFER to use one-liners with `run { }` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }`
189206

190207
### Architecture Guidelines
208+
191209
- Use `LightningNodeService` to manage background notifications while the node is running
192210
- Use `LightningService` to wrap node's RUST APIs and manage the inner lifecycle of the node
193211
- Use `LightningRepo` to defining the business logic for the node operations, usually delegating to `LightningService`

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ This repository contains a **new native Android app** which is **not ready for p
1515

1616
### Prerequisites
1717

18-
#### 1. Download `google-services.json` to `app/` from FCM Console.
18+
#### 1. Firebase Configuration
19+
20+
Download `google-services.json` from the Firebase Console for each build flavor:
21+
- **Dev/Testnet**: Place in `app/` (default location)
22+
- **Mainnet**: Place in `app/src/mainnet/google-services.json`
23+
24+
> **Note**: Each flavor requires its own Firebase project configuration. The mainnet flavor will fail to build without its dedicated `google-services.json` file.
1925
2026
#### 2. GitHub Packages setup
2127

@@ -100,6 +106,17 @@ The build config supports building 3 different apps for the 3 bitcoin networks (
100106
- `mainnet` flavour = mainnet
101107
- `tnet` flavour = testnet
102108

109+
### Build for Mainnet
110+
111+
To build the mainnet flavor:
112+
113+
```sh
114+
./gradlew assembleMainnetDebug # debug build
115+
./gradlew assembleMainnetRelease # release build (requires signing config)
116+
```
117+
118+
> **Important**: Ensure `app/src/mainnet/google-services.json` exists before building. See [Firebase Configuration](#1-firebase-configuration).
119+
103120
### Build for E2E Testing
104121

105122
Simply pass `E2E=true` as environment variable and build any flavor.

app/build.gradle.kts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ android {
6666
manifestPlaceholders["app_icon"] = "@mipmap/ic_launcher_regtest"
6767
manifestPlaceholders["app_icon_round"] = "@mipmap/ic_launcher_regtest_round"
6868
}
69-
// create("mainnet") {
70-
// dimension = "network"
71-
// applicationIdSuffix = ""
72-
// buildConfigField("String", "NETWORK", "\"BITCOIN\"")
73-
// resValue("string", "app_name", "Bitkit")
74-
// manifestPlaceholders["app_icon"] = "@mipmap/ic_launcher_orange"
75-
// manifestPlaceholders["app_icon_round"] = "@mipmap/ic_launcher_orange_round"
76-
// }
69+
create("mainnet") {
70+
dimension = "network"
71+
applicationIdSuffix = ""
72+
buildConfigField("String", "NETWORK", "\"BITCOIN\"")
73+
resValue("string", "app_name", "Bitkit")
74+
manifestPlaceholders["app_icon"] = "@mipmap/ic_launcher_orange"
75+
manifestPlaceholders["app_icon_round"] = "@mipmap/ic_launcher_orange_round"
76+
}
7777
create("tnet") {
7878
dimension = "network"
7979
applicationIdSuffix = ".tnet"

app/src/main/java/to/bitkit/App.kt

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,17 @@ internal open class App : Application(), Configuration.Provider {
3333
}
3434
}
3535

36-
// region currentActivity
3736
class CurrentActivity : ActivityLifecycleCallbacks {
3837
var value: Activity? = null
3938
private set
4039

4140
override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit
42-
43-
override fun onActivityStarted(activity: Activity) {
44-
this.value = activity
45-
}
46-
47-
override fun onActivityResumed(activity: Activity) {
48-
this.value = activity
49-
}
50-
51-
override fun onActivityPaused(activity: Activity) = Unit
52-
53-
override fun onActivityStopped(activity: Activity) {
54-
if (this.value == activity) this.value = null
55-
}
56-
41+
override fun onActivityStarted(activity: Activity) = run { this.value = activity }
42+
override fun onActivityResumed(activity: Activity) = run { this.value = activity }
43+
override fun onActivityPaused(activity: Activity) = clearIfCurrent(activity)
44+
override fun onActivityStopped(activity: Activity) = clearIfCurrent(activity)
5745
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit
46+
override fun onActivityDestroyed(activity: Activity) = clearIfCurrent(activity)
5847

59-
override fun onActivityDestroyed(activity: Activity) {
60-
if (this.value == activity) this.value = null
61-
}
48+
private fun clearIfCurrent(activity: Activity) = run { if (this.value == activity) this.value = null }
6249
}
63-
// endregion

app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import to.bitkit.models.NewTransactionSheetDetails
2323
import to.bitkit.models.NotificationDetails
2424
import to.bitkit.repositories.LightningRepo
2525
import to.bitkit.repositories.WalletRepo
26+
import to.bitkit.ui.ID_NOTIFICATION_NODE
2627
import to.bitkit.ui.MainActivity
2728
import to.bitkit.ui.pushNotification
2829
import to.bitkit.utils.Logger
@@ -52,7 +53,7 @@ class LightningNodeService : Service() {
5253

5354
override fun onCreate() {
5455
super.onCreate()
55-
startForeground(NOTIFICATION_ID, createNotification())
56+
startForeground(ID_NOTIFICATION_NODE, createNotification())
5657
setupService()
5758
}
5859

@@ -65,7 +66,7 @@ class LightningNodeService : Service() {
6566
}
6667
).onSuccess {
6768
val notification = createNotification()
68-
startForeground(NOTIFICATION_ID, notification)
69+
startForeground(ID_NOTIFICATION_NODE, notification)
6970

7071
walletRepo.setWalletExistsState()
7172
walletRepo.refreshBip21()
@@ -78,17 +79,22 @@ class LightningNodeService : Service() {
7879
if (event !is Event.PaymentReceived && event !is Event.OnchainTransactionReceived) return
7980
val command = NotifyPaymentReceived.Command.from(event, includeNotification = true) ?: return
8081

81-
notifyPaymentReceivedHandler(command).onSuccess { result ->
82-
if (result !is NotifyPaymentReceived.Result.ShowNotification) return
83-
showPaymentNotification(result.sheet, result.notification)
82+
notifyPaymentReceivedHandler(command).onSuccess {
83+
Logger.debug("Payment notification result: $it", context = TAG)
84+
if (it !is NotifyPaymentReceived.Result.ShowNotification) return
85+
showPaymentNotification(it.sheet, it.notification)
8486
}
8587
}
8688

8789
private fun showPaymentNotification(
8890
sheet: NewTransactionSheetDetails,
8991
notification: NotificationDetails,
9092
) {
91-
if (App.currentActivity?.value != null) return
93+
if (App.currentActivity?.value != null) {
94+
Logger.debug("Skipping payment notification: activity is active", context = TAG)
95+
return
96+
}
97+
Logger.debug("Showing payment notification: ${notification.title}", context = TAG)
9298
serviceScope.launch { cacheStore.setBackgroundReceive(sheet) }
9399
pushNotification(notification.title, notification.body)
94100
}
@@ -99,23 +105,13 @@ class LightningNodeService : Service() {
99105
val notificationIntent = Intent(this, MainActivity::class.java).apply {
100106
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
101107
}
102-
val pendingIntent = PendingIntent.getActivity(
103-
this,
104-
0,
105-
notificationIntent,
106-
PendingIntent.FLAG_IMMUTABLE
107-
)
108+
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
108109

109110
// Create stop action that will close both service and app
110111
val stopIntent = Intent(this, LightningNodeService::class.java).apply {
111112
action = ACTION_STOP_SERVICE_AND_APP
112113
}
113-
val stopPendingIntent = PendingIntent.getService(
114-
this,
115-
0,
116-
stopIntent,
117-
PendingIntent.FLAG_IMMUTABLE
118-
)
114+
val stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
119115

120116
return NotificationCompat.Builder(this, CHANNEL_ID_NODE)
121117
.setContentTitle(getString(R.string.app_name))
@@ -130,7 +126,6 @@ class LightningNodeService : Service() {
130126
.build()
131127
}
132128

133-
// Update the onStartCommand method
134129
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
135130
Logger.debug("onStartCommand", context = TAG)
136131
when (intent?.action) {
@@ -159,7 +154,6 @@ class LightningNodeService : Service() {
159154
override fun onBind(intent: Intent?): IBinder? = null
160155

161156
companion object {
162-
private const val NOTIFICATION_ID = 1
163157
const val CHANNEL_ID_NODE = "bitkit_notification_channel_node"
164158
const val TAG = "LightningNodeService"
165159
const val ACTION_STOP_SERVICE_AND_APP = "to.bitkit.androidServices.action.STOP_SERVICE_AND_APP"

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ data class SettingsData(
117117
val balanceWarningTimes: Int = 0,
118118
val coinSelectAuto: Boolean = true,
119119
val coinSelectPreference: CoinSelectionPreference = CoinSelectionPreference.BranchAndBound,
120-
val electrumServer: String = Env.defaultElectrumServer,
120+
val electrumServer: String = Env.electrumServerUrl,
121121
val rgsServerUrl: String? = Env.ldkRgsServerUrl,
122122
)
123123

0 commit comments

Comments
 (0)