Skip to content

Commit be45a5b

Browse files
committed
Merge branch 'master' into chore/update-ldk-node
# Conflicts: # app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt # app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt # gradle/libs.versions.toml
2 parents 4f6d3bf + 7ab5328 commit be45a5b

File tree

80 files changed

+3597
-1682
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+3597
-1682
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ dependencies {
195195
// Crypto
196196
implementation(libs.bouncycastle.provider.jdk)
197197
implementation(libs.ldk.node.android) { exclude(group = "net.java.dev.jna", module = "jna") }
198-
implementation(libs.bitkitcore)
198+
implementation(libs.bitkit.core)
199199
implementation(libs.vss)
200200
// Firebase
201201
implementation(platform(libs.firebase.bom))

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ class CurrentActivity : ActivityLifecycleCallbacks {
3838
var value: Activity? = null
3939
private set
4040

41-
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
42-
this.value = activity
43-
}
41+
override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit
4442

4543
override fun onActivityStarted(activity: Activity) {
4644
this.value = activity
@@ -51,8 +49,15 @@ class CurrentActivity : ActivityLifecycleCallbacks {
5149
}
5250

5351
override fun onActivityPaused(activity: Activity) = Unit
54-
override fun onActivityStopped(activity: Activity) = Unit
52+
53+
override fun onActivityStopped(activity: Activity) {
54+
if (this.value == activity) this.value = null
55+
}
56+
5557
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit
56-
override fun onActivityDestroyed(activity: Activity) = Unit
58+
59+
override fun onActivityDestroyed(activity: Activity) {
60+
if (this.value == activity) this.value = null
61+
}
5762
}
5863
// endregion

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

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,49 @@ import android.content.Intent
77
import android.os.IBinder
88
import androidx.core.app.NotificationCompat
99
import dagger.hilt.android.AndroidEntryPoint
10+
import kotlinx.coroutines.CoroutineDispatcher
1011
import kotlinx.coroutines.CoroutineScope
11-
import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.SupervisorJob
1313
import kotlinx.coroutines.cancel
1414
import kotlinx.coroutines.launch
15+
import org.lightningdevkit.ldknode.Event
1516
import to.bitkit.App
1617
import to.bitkit.R
18+
import to.bitkit.data.CacheStore
19+
import to.bitkit.di.UiDispatcher
20+
import to.bitkit.domain.commands.NotifyPaymentReceived
21+
import to.bitkit.domain.commands.NotifyPaymentReceivedHandler
22+
import to.bitkit.models.NewTransactionSheetDetails
23+
import to.bitkit.models.NotificationDetails
1724
import to.bitkit.repositories.LightningRepo
1825
import to.bitkit.repositories.WalletRepo
1926
import to.bitkit.ui.MainActivity
27+
import to.bitkit.ui.pushNotification
2028
import to.bitkit.utils.Logger
29+
import to.bitkit.utils.jsonLogOf
2130
import javax.inject.Inject
2231

2332
@AndroidEntryPoint
2433
class LightningNodeService : Service() {
2534

26-
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
35+
@Inject
36+
@UiDispatcher
37+
lateinit var uiDispatcher: CoroutineDispatcher
38+
39+
private val serviceScope by lazy { CoroutineScope(SupervisorJob() + uiDispatcher) }
2740

2841
@Inject
2942
lateinit var lightningRepo: LightningRepo
3043

3144
@Inject
3245
lateinit var walletRepo: WalletRepo
3346

47+
@Inject
48+
lateinit var notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler
49+
50+
@Inject
51+
lateinit var cacheStore: CacheStore
52+
3453
override fun onCreate() {
3554
super.onCreate()
3655
startForeground(NOTIFICATION_ID, createNotification())
@@ -39,26 +58,43 @@ class LightningNodeService : Service() {
3958

4059
private fun setupService() {
4160
serviceScope.launch {
42-
launch {
43-
lightningRepo.start(
44-
eventHandler = { event ->
45-
walletRepo.refreshBip21ForEvent(event)
46-
}
47-
).onSuccess {
48-
val notification = createNotification()
49-
startForeground(NOTIFICATION_ID, notification)
50-
51-
walletRepo.setWalletExistsState()
52-
walletRepo.refreshBip21()
53-
walletRepo.syncBalances()
61+
lightningRepo.start(
62+
eventHandler = { event ->
63+
Logger.debug("LDK-node event received in $TAG: ${jsonLogOf(event)}", context = TAG)
64+
handlePaymentReceived(event)
5465
}
66+
).onSuccess {
67+
val notification = createNotification()
68+
startForeground(NOTIFICATION_ID, notification)
69+
70+
walletRepo.setWalletExistsState()
71+
walletRepo.refreshBip21()
72+
walletRepo.syncBalances()
5573
}
5674
}
5775
}
5876

59-
// Update the createNotification method in LightningNodeService.kt
77+
private suspend fun handlePaymentReceived(event: Event) {
78+
if (event !is Event.PaymentReceived && event !is Event.OnchainTransactionReceived) return
79+
val command = NotifyPaymentReceived.Command.from(event, includeNotification = true) ?: return
80+
81+
notifyPaymentReceivedHandler(command).onSuccess { result ->
82+
if (result !is NotifyPaymentReceived.Result.ShowNotification) return
83+
showPaymentNotification(result.sheet, result.notification)
84+
}
85+
}
86+
87+
private fun showPaymentNotification(
88+
sheet: NewTransactionSheetDetails,
89+
notification: NotificationDetails,
90+
) {
91+
if (App.currentActivity?.value != null) return
92+
serviceScope.launch { cacheStore.setBackgroundReceive(sheet) }
93+
pushNotification(notification.title, notification.body)
94+
}
95+
6096
private fun createNotification(
61-
contentText: String = "Bitkit is running in background so you can receive Lightning payments"
97+
contentText: String = getString(R.string.notification_running_in_background),
6298
): Notification {
6399
val notificationIntent = Intent(this, MainActivity::class.java).apply {
64100
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
@@ -88,7 +124,7 @@ class LightningNodeService : Service() {
88124
.setContentIntent(pendingIntent)
89125
.addAction(
90126
R.drawable.ic_x,
91-
"Stop App", // TODO: Get from resources
127+
getString(R.string.notification_stop_app),
92128
stopPendingIntent
93129
)
94130
.build()

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import to.bitkit.models.BackupCategory
1414
import to.bitkit.models.BackupItemStatus
1515
import to.bitkit.models.BalanceState
1616
import to.bitkit.models.FxRate
17+
import to.bitkit.models.NewTransactionSheetDetails
1718
import to.bitkit.utils.Logger
1819
import javax.inject.Inject
1920
import javax.inject.Singleton
@@ -83,22 +84,6 @@ class CacheStore @Inject constructor(
8384
}
8485
}
8586

86-
suspend fun addActivityToPendingDelete(activityId: String) {
87-
if (activityId.isBlank()) return
88-
if (activityId in store.data.first().activitiesPendingDelete) return
89-
store.updateData {
90-
it.copy(activitiesPendingDelete = it.activitiesPendingDelete + activityId)
91-
}
92-
}
93-
94-
suspend fun removeActivityFromPendingDelete(activityId: String) {
95-
if (activityId.isBlank()) return
96-
if (activityId !in store.data.first().activitiesPendingDelete) return
97-
store.updateData {
98-
it.copy(activitiesPendingDelete = it.activitiesPendingDelete - activityId)
99-
}
100-
}
101-
10287
suspend fun addActivityToPendingBoost(pendingBoostActivity: PendingBoostActivity) {
10388
if (pendingBoostActivity in store.data.first().pendingBoostActivities) return
10489
store.updateData {
@@ -113,6 +98,18 @@ class CacheStore @Inject constructor(
11398
}
11499
}
115100

101+
suspend fun setLastLightningPayment(paymentId: String) {
102+
store.updateData { it.copy(lastLightningPaymentId = paymentId) }
103+
}
104+
105+
suspend fun setBackgroundReceive(details: NewTransactionSheetDetails) = store.updateData {
106+
it.copy(backgroundReceive = details)
107+
}
108+
109+
suspend fun clearBackgroundReceive() {
110+
store.updateData { it.copy(backgroundReceive = null) }
111+
}
112+
116113
suspend fun reset() {
117114
store.updateData { AppCacheData() }
118115
Logger.info("Deleted all app cached data.")
@@ -133,6 +130,9 @@ data class AppCacheData(
133130
val balance: BalanceState? = null,
134131
val backupStatuses: Map<BackupCategory, BackupItemStatus> = mapOf(),
135132
val deletedActivities: List<String> = listOf(),
136-
val activitiesPendingDelete: List<String> = listOf(),
133+
val lastLightningPaymentId: String? = null,
137134
val pendingBoostActivities: List<PendingBoostActivity> = listOf(),
138-
)
135+
val backgroundReceive: NewTransactionSheetDetails? = null,
136+
) {
137+
fun resetBip21() = copy(bip21 = "", bolt11 = "", onchainAddress = "")
138+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package to.bitkit.domain.commands
2+
3+
import org.lightningdevkit.ldknode.Event
4+
import to.bitkit.models.NewTransactionSheetDetails
5+
import to.bitkit.models.NotificationDetails
6+
7+
sealed interface NotifyPaymentReceived {
8+
9+
sealed interface Command : NotifyPaymentReceived {
10+
val includeNotification: Boolean
11+
12+
data class Lightning(
13+
val event: Event.PaymentReceived,
14+
override val includeNotification: Boolean = false,
15+
) : Command
16+
17+
data class Onchain(
18+
val event: Event.OnchainTransactionReceived,
19+
override val includeNotification: Boolean = false,
20+
) : Command
21+
22+
companion object {
23+
fun from(event: Event, includeNotification: Boolean = false): Command? =
24+
when (event) {
25+
is Event.PaymentReceived -> Lightning(
26+
event = event,
27+
includeNotification = includeNotification,
28+
)
29+
30+
is Event.OnchainTransactionReceived -> Onchain(
31+
event = event,
32+
includeNotification = includeNotification,
33+
)
34+
35+
else -> null
36+
}
37+
}
38+
}
39+
40+
sealed interface Result : NotifyPaymentReceived {
41+
data class ShowSheet(
42+
val sheet: NewTransactionSheetDetails,
43+
) : Result
44+
45+
data class ShowNotification(
46+
val sheet: NewTransactionSheetDetails,
47+
val notification: NotificationDetails,
48+
) : Result
49+
50+
data object Skip : Result
51+
}
52+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package to.bitkit.domain.commands
2+
3+
import android.content.Context
4+
import dagger.hilt.android.qualifiers.ApplicationContext
5+
import kotlinx.coroutines.CoroutineDispatcher
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.flow.first
8+
import kotlinx.coroutines.withContext
9+
import to.bitkit.R
10+
import to.bitkit.data.SettingsData
11+
import to.bitkit.data.SettingsStore
12+
import to.bitkit.di.IoDispatcher
13+
import to.bitkit.models.BITCOIN_SYMBOL
14+
import to.bitkit.models.NewTransactionSheetDetails
15+
import to.bitkit.models.NewTransactionSheetDirection
16+
import to.bitkit.models.NewTransactionSheetType
17+
import to.bitkit.models.NotificationDetails
18+
import to.bitkit.models.PrimaryDisplay
19+
import to.bitkit.models.formatToModernDisplay
20+
import to.bitkit.repositories.ActivityRepo
21+
import to.bitkit.repositories.CurrencyRepo
22+
import to.bitkit.utils.Logger
23+
import javax.inject.Inject
24+
import javax.inject.Singleton
25+
26+
@Singleton
27+
class NotifyPaymentReceivedHandler @Inject constructor(
28+
@param:ApplicationContext private val context: Context,
29+
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
30+
private val activityRepo: ActivityRepo,
31+
private val currencyRepo: CurrencyRepo,
32+
private val settingsStore: SettingsStore,
33+
) {
34+
suspend operator fun invoke(
35+
command: NotifyPaymentReceived.Command,
36+
): Result<NotifyPaymentReceived.Result> = withContext(ioDispatcher) {
37+
runCatching {
38+
val shouldShow = when (command) {
39+
is NotifyPaymentReceived.Command.Lightning -> true
40+
is NotifyPaymentReceived.Command.Onchain -> {
41+
activityRepo.handleOnchainTransactionReceived(command.event.txid, command.event.details)
42+
if (command.event.details.amountSats > 0) {
43+
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
44+
activityRepo.shouldShowReceivedSheet(
45+
command.event.txid,
46+
command.event.details.amountSats.toULong()
47+
)
48+
} else {
49+
false
50+
}
51+
}
52+
}
53+
54+
if (!shouldShow) return@runCatching NotifyPaymentReceived.Result.Skip
55+
56+
val details = NewTransactionSheetDetails(
57+
type = when (command) {
58+
is NotifyPaymentReceived.Command.Lightning -> NewTransactionSheetType.LIGHTNING
59+
is NotifyPaymentReceived.Command.Onchain -> NewTransactionSheetType.ONCHAIN
60+
},
61+
direction = NewTransactionSheetDirection.RECEIVED,
62+
paymentHashOrTxId = when (command) {
63+
is NotifyPaymentReceived.Command.Lightning -> command.event.paymentHash
64+
is NotifyPaymentReceived.Command.Onchain -> command.event.txid
65+
},
66+
sats = when (command) {
67+
is NotifyPaymentReceived.Command.Lightning -> (command.event.amountMsat / 1000u).toLong()
68+
is NotifyPaymentReceived.Command.Onchain -> command.event.details.amountSats
69+
},
70+
)
71+
72+
if (command.includeNotification) {
73+
val notification = buildNotificationContent(details.sats)
74+
NotifyPaymentReceived.Result.ShowNotification(details, notification)
75+
} else {
76+
NotifyPaymentReceived.Result.ShowSheet(details)
77+
}
78+
}.onFailure { e ->
79+
Logger.error("Failed to process payment notification", e, context = TAG)
80+
}
81+
}
82+
83+
private suspend fun buildNotificationContent(sats: Long): NotificationDetails {
84+
val settings = settingsStore.data.first()
85+
val title = context.getString(R.string.notification_received_title)
86+
val body = if (settings.showNotificationDetails) {
87+
formatNotificationAmount(sats, settings)
88+
} else {
89+
context.getString(R.string.notification_received_body_hidden)
90+
}
91+
return NotificationDetails(title, body)
92+
}
93+
94+
private fun formatNotificationAmount(sats: Long, settings: SettingsData): String {
95+
val converted = currencyRepo.convertSatsToFiat(sats).getOrNull()
96+
97+
val amountText = converted?.let {
98+
val btcDisplay = it.bitcoinDisplay(settings.displayUnit)
99+
if (settings.primaryDisplay == PrimaryDisplay.BITCOIN) {
100+
"${btcDisplay.symbol} ${btcDisplay.value} (${it.symbol}${it.formatted})"
101+
} else {
102+
"${it.symbol}${it.formatted} (${btcDisplay.symbol} ${btcDisplay.value})"
103+
}
104+
} ?: "$BITCOIN_SYMBOL ${sats.formatToModernDisplay()}"
105+
106+
return context.getString(R.string.notification_received_body_amount, amountText)
107+
}
108+
109+
companion object {
110+
const val TAG = "NotifyPaymentReceivedHandler"
111+
112+
/**
113+
* Delay after syncing onchain transaction to allow the database to fully process
114+
* the transaction before checking for RBF replacement or channel closure.
115+
*/
116+
private const val DELAY_FOR_ACTIVITY_SYNC_MS = 500L
117+
}
118+
}

0 commit comments

Comments
 (0)