From 92fea0fd9a8b5ea49ada1fd848d5b4909e0b9081 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Aug 2024 16:16:00 +0200 Subject: [PATCH 01/13] refactor: Move ServiceQueue to .async --- app/src/main/java/to/bitkit/{di => async}/ServiceQueue.kt | 4 ++-- app/src/main/java/to/bitkit/bdk/BitcoinService.kt | 2 +- app/src/main/java/to/bitkit/ldk/LightningService.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename app/src/main/java/to/bitkit/{di => async}/ServiceQueue.kt (96%) diff --git a/app/src/main/java/to/bitkit/di/ServiceQueue.kt b/app/src/main/java/to/bitkit/async/ServiceQueue.kt similarity index 96% rename from app/src/main/java/to/bitkit/di/ServiceQueue.kt rename to app/src/main/java/to/bitkit/async/ServiceQueue.kt index be333b28d..b8dd2f3fd 100644 --- a/app/src/main/java/to/bitkit/di/ServiceQueue.kt +++ b/app/src/main/java/to/bitkit/async/ServiceQueue.kt @@ -1,4 +1,4 @@ -package to.bitkit.di +package to.bitkit.async import android.util.Log import kotlinx.coroutines.CoroutineScope @@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory import kotlin.coroutines.CoroutineContext enum class ServiceQueue { - LDK, BDK, MIGRATION; + LDK, BDK, LSP, MIGRATION; private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) } diff --git a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt index f2574ff91..35516b154 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/bdk/BitcoinService.kt @@ -15,7 +15,7 @@ import to.bitkit.SEED import to.bitkit.Tag.BDK import to.bitkit.async.BaseCoroutineScope import to.bitkit.di.BgDispatcher -import to.bitkit.di.ServiceQueue +import to.bitkit.async.ServiceQueue import javax.inject.Inject import kotlin.io.path.Path import kotlin.io.path.pathString diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/ldk/LightningService.kt index c26e37776..bee6126ed 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/ldk/LightningService.kt @@ -17,7 +17,7 @@ import to.bitkit.Tag.LDK import to.bitkit.async.BaseCoroutineScope import to.bitkit.bdk.BitcoinService import to.bitkit.di.BgDispatcher -import to.bitkit.di.ServiceQueue +import to.bitkit.async.ServiceQueue import javax.inject.Inject class LightningService @Inject constructor( From 0d72831a1930662117ea255dd1d002bd0f034096 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Aug 2024 17:04:38 +0200 Subject: [PATCH 02/13] feat: SharedViewModel scaffold --- .../main/java/to/bitkit/LauncherActivity.kt | 10 +++++--- .../main/java/to/bitkit/di/ViewModelModule.kt | 23 +++++++++++++++++ .../main/java/to/bitkit/ui/MainActivity.kt | 2 ++ .../main/java/to/bitkit/ui/SharedViewModel.kt | 25 +++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/to/bitkit/di/ViewModelModule.kt create mode 100644 app/src/main/java/to/bitkit/ui/SharedViewModel.kt diff --git a/app/src/main/java/to/bitkit/LauncherActivity.kt b/app/src/main/java/to/bitkit/LauncherActivity.kt index 894aa91b8..9421f855f 100644 --- a/app/src/main/java/to/bitkit/LauncherActivity.kt +++ b/app/src/main/java/to/bitkit/LauncherActivity.kt @@ -2,22 +2,24 @@ package to.bitkit import android.content.Intent import android.os.Bundle +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.runBlocking -import to.bitkit.ldk.warmupNode import to.bitkit.ui.MainActivity +import to.bitkit.ui.SharedViewModel import to.bitkit.ui.initNotificationChannel import to.bitkit.ui.logFcmToken @AndroidEntryPoint class LauncherActivity : AppCompatActivity() { + private val sharedViewModel: SharedViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + initNotificationChannel() logFcmToken() - // TODO share mainViewModel in both activities, move warmupNode to it & call it suspending - runBlocking { warmupNode() } + sharedViewModel.warmupNode() startActivity(Intent(this, MainActivity::class.java)) } } diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt new file mode 100644 index 000000000..3e27ffbd6 --- /dev/null +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -0,0 +1,23 @@ +package to.bitkit.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import to.bitkit.ui.SharedViewModel +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ViewModelModule { + @Singleton + @Provides + fun provideSharedViewModel( + @BgDispatcher bgDispatcher: CoroutineDispatcher, + ): SharedViewModel { + return SharedViewModel( + bgDispatcher, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 4729ba7b0..49dcf0ac2 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -55,10 +55,12 @@ import to.bitkit.ui.theme.AppThemeSurface @AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel by viewModels() + private val sharedViewModel: SharedViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + sharedViewModel.logInstanceHashCode() setContent { enableEdgeToEdge() AppThemeSurface { diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt new file mode 100644 index 000000000..8707a6ed2 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -0,0 +1,25 @@ +package to.bitkit.ui + +import android.util.Log +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import to.bitkit.Tag.DEV +import to.bitkit.di.BgDispatcher +import javax.inject.Inject + +@HiltViewModel +class SharedViewModel @Inject constructor( + @BgDispatcher bgDispatcher: CoroutineDispatcher, +) : ViewModel() { + fun warmupNode() { + // TODO make it concurrent, and wait for all to finish before trying to access `lightningService.node`, etc… + logInstanceHashCode() + runBlocking { to.bitkit.ldk.warmupNode() } + } + + fun logInstanceHashCode() { + Log.d(DEV, "${this::class.java.simpleName} hashCode: ${hashCode()}") + } +} From ad6345b3098fd9854834bfe9cd12cdaeb44818ec Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Aug 2024 18:25:36 +0200 Subject: [PATCH 03/13] refactor: Move models to RestApi file --- app/src/main/java/to/bitkit/data/Models.kt | 34 --------------------- app/src/main/java/to/bitkit/data/RestApi.kt | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 34 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/data/Models.kt diff --git a/app/src/main/java/to/bitkit/data/Models.kt b/app/src/main/java/to/bitkit/data/Models.kt deleted file mode 100644 index cbdbd4306..000000000 --- a/app/src/main/java/to/bitkit/data/Models.kt +++ /dev/null @@ -1,34 +0,0 @@ -package to.bitkit.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Tx( - val txid: String, - val status: TxStatus, -) - -@Serializable -data class TxStatus( - @SerialName("confirmed") - val isConfirmed: Boolean, - @SerialName("block_height") - val blockHeight: Int? = null, - @SerialName("block_hash") - val blockHash: String? = null, -) - -@Serializable -data class OutputSpent( - val spent: Boolean, -) - -@Serializable -data class MerkleProof( - @SerialName("block_height") - val blockHeight: Int, - @Suppress("ArrayInDataClass") - val merkle: Array, - val pos: Int, -) diff --git a/app/src/main/java/to/bitkit/data/RestApi.kt b/app/src/main/java/to/bitkit/data/RestApi.kt index c64072b06..3a3be5f1a 100644 --- a/app/src/main/java/to/bitkit/data/RestApi.kt +++ b/app/src/main/java/to/bitkit/data/RestApi.kt @@ -6,6 +6,8 @@ import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import to.bitkit.REST import to.bitkit.ext.toHex import javax.inject.Inject @@ -62,3 +64,33 @@ class EsploraApi @Inject constructor( return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body() } } + +@Serializable +data class Tx( + val txid: String, + val status: TxStatus, +) + +@Serializable +data class TxStatus( + @SerialName("confirmed") + val isConfirmed: Boolean, + @SerialName("block_height") + val blockHeight: Int? = null, + @SerialName("block_hash") + val blockHash: String? = null, +) + +@Serializable +data class OutputSpent( + val spent: Boolean, +) + +@Serializable +data class MerkleProof( + @SerialName("block_height") + val blockHeight: Int, + @Suppress("ArrayInDataClass") + val merkle: Array, + val pos: Int, +) From 4002d8cd1f05cedfae66a099c839549dfa0b5d49 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Aug 2024 20:33:04 +0200 Subject: [PATCH 04/13] feat: Use LnPeer instead of LDK's PeerDetail --- app/src/main/java/to/bitkit/Constants.kt | 23 ++++++++++++++++++- .../{ldk => services}/LightningService.kt | 6 ++--- .../main/java/to/bitkit/ui/MainViewModel.kt | 9 ++++---- .../main/java/to/bitkit/ui/shared/Peers.kt | 6 ++--- 4 files changed, 32 insertions(+), 12 deletions(-) rename app/src/main/java/to/bitkit/{ldk => services}/LightningService.kt (97%) diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index eeb9546fb..362520f4c 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -3,6 +3,7 @@ package to.bitkit import android.util.Log +import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.Tag.APP import to.bitkit.env.Network import to.bitkit.ext.ensureDir @@ -81,13 +82,33 @@ data class LnPeer( val nodeId: String, val host: String, val port: String, + val isConnected: Boolean = false, + val isPersisted: Boolean = false, ) { - constructor(nodeId: String, address: String) : this( + constructor( + nodeId: String, + address: String, + isConnected: Boolean = false, + isPersisted: Boolean = false, + ) : this( nodeId, address.substringBefore(":"), address.substringAfter(":"), + isConnected, + isPersisted, ) val address get() = "$host:$port" override fun toString() = "$nodeId@${address}" + + companion object { + fun PeerDetails.toLnPeer(): LnPeer { + return LnPeer( + nodeId = nodeId, + address = address, + isConnected = isConnected, + isPersisted = isPersisted, + ) + } + } } diff --git a/app/src/main/java/to/bitkit/ldk/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt similarity index 97% rename from app/src/main/java/to/bitkit/ldk/LightningService.kt rename to app/src/main/java/to/bitkit/services/LightningService.kt index bee6126ed..691008ec7 100644 --- a/app/src/main/java/to/bitkit/ldk/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -11,6 +11,7 @@ import org.lightningdevkit.ldknode.Node import org.lightningdevkit.ldknode.defaultConfig import to.bitkit.Env import to.bitkit.LnPeer +import to.bitkit.LnPeer.Companion.toLnPeer import to.bitkit.REST import to.bitkit.SEED import to.bitkit.Tag.LDK @@ -109,7 +110,7 @@ class LightningService @Inject constructor( val nodeId: String get() = node.nodeId() val balances get() = node.listBalances() val status get() = node.status() - val peers get() = node.listPeers() + val peers get() = node.listPeers().map { it.toLnPeer() } val channels get() = node.listChannels() val payments get() = node.listPayments() // endregion @@ -126,8 +127,7 @@ internal fun LightningService.connectPeer(peer: LnPeer) { // endregion // region channels -internal suspend fun LightningService.openChannel() { - val peer = peers.first() +internal suspend fun LightningService.openChannel(peer: LnPeer) { // sendToAddress // mine 6 blocks & wait for esplora to pick up block diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index 102762ee2..9e3fff994 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.ChannelDetails -import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.LnPeer import to.bitkit.SEED import to.bitkit.Tag.DEV @@ -41,7 +40,7 @@ class MainViewModel @Inject constructor( val btcBalance = mutableStateOf("Loading…") val mnemonic = mutableStateOf(SEED) - val peers = mutableStateListOf() + val peers = mutableStateListOf() val channels = mutableStateListOf() private val node = lightningService.node @@ -94,7 +93,7 @@ class MainViewModel @Inject constructor( fun openChannel() { viewModelScope.launch(bgDispatcher) { - lightningService.openChannel() + lightningService.openChannel(peers.first()) sync() } } @@ -148,5 +147,5 @@ class MainViewModel @Inject constructor( } } -fun MainViewModel.togglePeerConnection(peer: PeerDetails) = - if (peer.isConnected) disconnectPeer(peer.nodeId) else connectPeer(LnPeer(peer.nodeId, peer.address)) +fun MainViewModel.togglePeerConnection(peer: LnPeer) = + if (peer.isConnected) disconnectPeer(peer.nodeId) else connectPeer(peer) diff --git a/app/src/main/java/to/bitkit/ui/shared/Peers.kt b/app/src/main/java/to/bitkit/ui/shared/Peers.kt index dc26210ea..151ab1be8 100644 --- a/app/src/main/java/to/bitkit/ui/shared/Peers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/Peers.kt @@ -23,13 +23,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.lightningdevkit.ldknode.PeerDetails +import to.bitkit.LnPeer import to.bitkit.R @Composable internal fun Peers( - peers: SnapshotStateList, - onToggle: (PeerDetails) -> Unit, + peers: SnapshotStateList, + onToggle: (LnPeer) -> Unit, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), From 7f72b7b105fcfbc5abfc5e64f0bd90ad2f322394 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Aug 2024 20:34:47 +0200 Subject: [PATCH 05/13] refactor: Move services to their own package --- .../{ldk => services}/LdkMigrationTest.kt | 2 +- .../main/java/to/bitkit/di/ServicesModule.kt | 4 +-- .../main/java/to/bitkit/fcm/Wake2PayWorker.kt | 6 ++-- .../{bdk => services}/BitcoinService.kt | 2 +- .../to/bitkit/services/LightningService.kt | 27 ++++++++++-------- .../{ldk => services}/MigrationService.kt | 28 +++++++++++-------- .../main/java/to/bitkit/ui/MainViewModel.kt | 14 +++++----- .../main/java/to/bitkit/ui/SharedViewModel.kt | 2 +- 8 files changed, 46 insertions(+), 39 deletions(-) rename app/src/androidTest/java/to/bitkit/{ldk => services}/LdkMigrationTest.kt (98%) rename app/src/main/java/to/bitkit/{bdk => services}/BitcoinService.kt (99%) rename app/src/main/java/to/bitkit/{ldk => services}/MigrationService.kt (81%) diff --git a/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt b/app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt similarity index 98% rename from app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt rename to app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt index 13bbdc172..3c0573229 100644 --- a/app/src/androidTest/java/to/bitkit/ldk/LdkMigrationTest.kt +++ b/app/src/androidTest/java/to/bitkit/services/LdkMigrationTest.kt @@ -1,4 +1,4 @@ -package to.bitkit.ldk +package to.bitkit.services import android.content.Context import androidx.test.core.app.ApplicationProvider diff --git a/app/src/main/java/to/bitkit/di/ServicesModule.kt b/app/src/main/java/to/bitkit/di/ServicesModule.kt index e1df6815b..654d8458e 100644 --- a/app/src/main/java/to/bitkit/di/ServicesModule.kt +++ b/app/src/main/java/to/bitkit/di/ServicesModule.kt @@ -7,8 +7,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher -import to.bitkit.bdk.BitcoinService -import to.bitkit.ldk.LightningService +import to.bitkit.services.BitcoinService +import to.bitkit.services.LightningService @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt index d25b3a05a..ff5973e34 100644 --- a/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt +++ b/app/src/main/java/to/bitkit/fcm/Wake2PayWorker.kt @@ -9,9 +9,9 @@ import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject import to.bitkit.Tag.FCM -import to.bitkit.ldk.LightningService -import to.bitkit.ldk.payInvoice -import to.bitkit.ldk.warmupNode +import to.bitkit.services.LightningService +import to.bitkit.services.payInvoice +import to.bitkit.services.warmupNode @HiltWorker class Wake2PayWorker @AssistedInject constructor( diff --git a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt b/app/src/main/java/to/bitkit/services/BitcoinService.kt similarity index 99% rename from app/src/main/java/to/bitkit/bdk/BitcoinService.kt rename to app/src/main/java/to/bitkit/services/BitcoinService.kt index 35516b154..67fbd081a 100644 --- a/app/src/main/java/to/bitkit/bdk/BitcoinService.kt +++ b/app/src/main/java/to/bitkit/services/BitcoinService.kt @@ -1,4 +1,4 @@ -package to.bitkit.bdk +package to.bitkit.services import android.util.Log import kotlinx.coroutines.CoroutineDispatcher diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index 691008ec7..aee35c03c 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -1,4 +1,4 @@ -package to.bitkit.ldk +package to.bitkit.services import android.util.Log import kotlinx.coroutines.CoroutineDispatcher @@ -16,9 +16,9 @@ import to.bitkit.REST import to.bitkit.SEED import to.bitkit.Tag.LDK import to.bitkit.async.BaseCoroutineScope -import to.bitkit.bdk.BitcoinService -import to.bitkit.di.BgDispatcher import to.bitkit.async.ServiceQueue +import to.bitkit.di.BgDispatcher +import to.bitkit.ext.uByteList import javax.inject.Inject class LightningService @Inject constructor( @@ -67,7 +67,7 @@ class LightningService @Inject constructor( } suspend fun start() { - check(::node.isInitialized) { "LDK node is not initialised" } + assertNodeIsInitialised() Log.d(LDK, "Starting node…") @@ -134,14 +134,17 @@ internal suspend fun LightningService.openChannel(peer: LnPeer) { // wait for esplora to pick up tx sync() - node.connectOpenChannel( - nodeId = peer.nodeId, - address = peer.address, - channelAmountSats = 50000u, - pushToCounterpartyMsat = null, - channelConfig = null, - announceChannel = true, - ) + ServiceQueue.LDK.background { + node.connectOpenChannel( + nodeId = peer.nodeId, + address = peer.address, + channelAmountSats = 50000u, + pushToCounterpartyMsat = null, + channelConfig = null, + announceChannel = true, + ) + } + sync() val pendingEvent = node.nextEventAsync() diff --git a/app/src/main/java/to/bitkit/ldk/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt similarity index 81% rename from app/src/main/java/to/bitkit/ldk/MigrationService.kt rename to app/src/main/java/to/bitkit/services/MigrationService.kt index f70e3288a..c2dcb6474 100644 --- a/app/src/main/java/to/bitkit/ldk/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -1,4 +1,4 @@ -package to.bitkit.ldk +package to.bitkit.services import android.content.ContentValues import android.content.Context @@ -7,32 +7,32 @@ import android.database.sqlite.SQLiteOpenHelper import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext import org.ldk.structs.KeysManager +import org.ldk.structs.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ +import org.ldk.structs.UtilMethods import to.bitkit.Env -import to.bitkit.Tag.LDK +import to.bitkit.Tag import to.bitkit.ext.hex import java.io.File import javax.inject.Inject import kotlin.io.path.Path -import org.ldk.structs.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK as ChannelMonitorDecodeResultTuple -import org.ldk.structs.UtilMethods.C2Tuple_ThirtyTwoBytesChannelMonitorZ_read as read32BytesChannelMonitor class MigrationService @Inject constructor( @ApplicationContext private val context: Context, ) { fun migrate(seed: ByteArray, manager: ByteArray, monitors: List) { - Log.d(LDK, "Migrating LDK backup…") + Log.d(Tag.LDK, "Migrating LDK backup…") val file = Path(Env.Storage.ldk, LDK_DB_NAME).toFile() // Skip if db already exists if (file.exists()) { - Log.d(LDK, "Migration skipped: ldk-node db exists at: $file") + Log.d(Tag.LDK, "Migration skipped: ldk-node db exists at: $file") return } val path = file.path - Log.d(LDK, "Creating ldk-node db at: $path") - Log.d(LDK, "Seeding ldk-node db with LDK backup data…") + Log.d(Tag.LDK, "Creating ldk-node db at: $path") + Log.d(Tag.LDK, "Seeding ldk-node db with LDK backup data…") LdkNodeDataDbHelper(context, path).writableDatabase.use { it.beginTransaction() @@ -49,7 +49,7 @@ class MigrationService @Inject constructor( File("$path-journal").delete() - Log.i(LDK, "Migrated LDK backup to ldk-node db at: $path") + Log.i(Tag.LDK, "Migrated LDK backup to ldk-node db at: $path") } private fun SQLiteDatabase.insertManager(manager: ByteArray) { @@ -70,8 +70,12 @@ class MigrationService @Inject constructor( val (entropySource, signerProvider) = keysManager.as_EntropySource() to keysManager.as_SignerProvider() for (monitor in monitors) { - val channelMonitor = read32BytesChannelMonitor(monitor, entropySource, signerProvider).takeIf { it.is_ok } - ?.let { it as? ChannelMonitorDecodeResultTuple }?.res?._b + val channelMonitor = UtilMethods.C2Tuple_ThirtyTwoBytesChannelMonitorZ_read( + monitor, + entropySource, + signerProvider + ).takeIf { it.is_ok } + ?.let { it as? Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK }?.res?._b ?: throw Error("Could not read channel monitor using read32BytesChannelMonitor") val fundingTx = channelMonitor._funding_txo._a._txid?.reversedArray()?.hex ?: throw Error("Could not read txid from funding tx OutPoint of channel monitor") @@ -86,7 +90,7 @@ class MigrationService @Inject constructor( } insert(LDK_NODE_DATA, null, values) - Log.d(LDK, "Inserted monitor: $key") + Log.d(Tag.LDK, "Inserted monitor: $key") } } diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index 9e3fff994..691f74643 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -13,17 +13,17 @@ import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.LnPeer import to.bitkit.SEED import to.bitkit.Tag.DEV -import to.bitkit.bdk.BitcoinService +import to.bitkit.services.BitcoinService import to.bitkit.data.AppDb import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher import to.bitkit.ext.syncTo -import to.bitkit.ldk.LightningService -import to.bitkit.ldk.closeChannel -import to.bitkit.ldk.connectPeer -import to.bitkit.ldk.createInvoice -import to.bitkit.ldk.openChannel -import to.bitkit.ldk.payInvoice +import to.bitkit.services.LightningService +import to.bitkit.services.closeChannel +import to.bitkit.services.connectPeer +import to.bitkit.services.createInvoice +import to.bitkit.services.openChannel +import to.bitkit.services.payInvoice import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 8707a6ed2..1c0f0aec8 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -16,7 +16,7 @@ class SharedViewModel @Inject constructor( fun warmupNode() { // TODO make it concurrent, and wait for all to finish before trying to access `lightningService.node`, etc… logInstanceHashCode() - runBlocking { to.bitkit.ldk.warmupNode() } + runBlocking { to.bitkit.services.warmupNode() } } fun logInstanceHashCode() { From 22043553b76910498c4bd505c2d72e3d1b498861 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Aug 2024 20:36:53 +0200 Subject: [PATCH 06/13] feat: BT register device for notifications --- app/src/main/java/to/bitkit/Constants.kt | 1 + app/src/main/java/to/bitkit/data/LspApi.kt | 36 ++++++++++++++ .../bitkit/data/keychain/AndroidKeyStore.kt | 2 +- app/src/main/java/to/bitkit/di/HttpModule.kt | 8 +++ app/src/main/java/to/bitkit/ext/ByteArray.kt | 2 + .../to/bitkit/services/BlocktankService.kt | 49 +++++++++++++++++++ .../to/bitkit/services/LightningService.kt | 10 ++++ 7 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/to/bitkit/data/LspApi.kt create mode 100644 app/src/main/java/to/bitkit/services/BlocktankService.kt diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index 362520f4c..1ef189c96 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -14,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork internal object Tag { const val FCM = "FCM" const val LDK = "LDK" + const val LSP = "LSP" const val BDK = "BDK" const val DEV = "DEV" const val APP = "APP" diff --git a/app/src/main/java/to/bitkit/data/LspApi.kt b/app/src/main/java/to/bitkit/data/LspApi.kt new file mode 100644 index 000000000..2160290c4 --- /dev/null +++ b/app/src/main/java/to/bitkit/data/LspApi.kt @@ -0,0 +1,36 @@ +package to.bitkit.data + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import kotlinx.serialization.Serializable +import javax.inject.Inject + +interface LspApi { + suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest): String +} + +class BlocktankApi @Inject constructor( + private val client: HttpClient, +) : LspApi { + private val baseUrl = "https://api.stag.blocktank.to" + + override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest): String { + val response = client.post("$baseUrl/notifications/api/device") { + setBody(payload) + } + + return response.body().decodeToString() + } +} + +@Serializable +data class RegisterDeviceRequest( + val deviceToken: String, + val publicKey: String, + val features: List, + val nodeId: String, + val isoTimestamp: String, + val signature: String, +) diff --git a/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt b/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt index 74059f110..74ccc213f 100644 --- a/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt +++ b/app/src/main/java/to/bitkit/data/keychain/AndroidKeyStore.kt @@ -77,6 +77,6 @@ class AndroidKeyStore( val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) } val decryptedDataBytes = cipher.doFinal(actualEncryptedData) - return decryptedDataBytes.toString(Charsets.UTF_8) + return decryptedDataBytes.decodeToString() } } diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index 1db00918b..d8be64af2 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -12,7 +12,9 @@ import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import to.bitkit.data.BlocktankApi import to.bitkit.data.EsploraApi +import to.bitkit.data.LspApi import to.bitkit.data.RestApi import javax.inject.Singleton @@ -43,6 +45,12 @@ object HttpModule { } } + @Provides + @Singleton + fun provideLspApi(blocktankApi: BlocktankApi): LspApi { + return blocktankApi + } + @Provides @Singleton fun provideRestApi(esploraApi: EsploraApi): RestApi { diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index 11923631e..dfdfc1afc 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray { fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags) fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags) + +val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() } diff --git a/app/src/main/java/to/bitkit/services/BlocktankService.kt b/app/src/main/java/to/bitkit/services/BlocktankService.kt new file mode 100644 index 000000000..8b4011bb5 --- /dev/null +++ b/app/src/main/java/to/bitkit/services/BlocktankService.kt @@ -0,0 +1,49 @@ +package to.bitkit.services + +import android.util.Log +import kotlinx.coroutines.CoroutineDispatcher +import to.bitkit.Tag.LSP +import to.bitkit.async.BaseCoroutineScope +import to.bitkit.async.ServiceQueue +import to.bitkit.data.LspApi +import to.bitkit.data.RegisterDeviceRequest +import to.bitkit.di.BgDispatcher +import java.time.Instant +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import javax.inject.Inject + +class BlocktankService @Inject constructor( + @BgDispatcher bgDispatcher: CoroutineDispatcher, + private val lspApi: LspApi, + private val lightningService: LightningService, +) : BaseCoroutineScope(bgDispatcher) { + + suspend fun registerDevice(deviceToken: String) { + // UserDefaults.standard.setValue(deviceToken, forKey: "deviceToken") + val nodeId = requireNotNull(lightningService.nodeId) { "Node not started" } + + Log.d(LSP, "Registering device for notifications") + + val isoTimestamp = DateTimeFormatter.ISO_INSTANT.format(Instant.now().truncatedTo(ChronoUnit.SECONDS)) + val messageToSign = "bitkit-notifications$deviceToken$isoTimestamp" + + val signature = lightningService.sign(messageToSign) + + // TODO: Use real public key to enable decryption of the push notification payload + val publicKey = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + + val payload = RegisterDeviceRequest( + deviceToken = deviceToken, + publicKey = publicKey, + features = listOf("blocktank.incomingHtlc"), + nodeId = nodeId, + isoTimestamp = isoTimestamp, + signature = signature, + ) + + ServiceQueue.LSP.background { + lspApi.registerDeviceForNotifications(payload) + } + } +} diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index aee35c03c..9954d00ee 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -106,6 +106,16 @@ class LightningService @Inject constructor( Log.i(LDK, "Node synced") } + suspend fun sign(message: String): String { + assertNodeIsInitialised() + + return ServiceQueue.LDK.background { + node.signMessage(message.uByteList) + } + } + + private fun assertNodeIsInitialised() = check(::node.isInitialized) { "LDK node is not initialised" } + // region state val nodeId: String get() = node.nodeId() val balances get() = node.listBalances() From 678c8bf35d2f6ef8ba1eb720967e81bc2b47e9de Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Aug 2024 20:38:48 +0200 Subject: [PATCH 07/13] feat: UI to test BT register device --- .../main/java/to/bitkit/LauncherActivity.kt | 2 +- app/src/main/java/to/bitkit/di/HttpModule.kt | 6 +++ .../main/java/to/bitkit/di/ViewModelModule.kt | 3 ++ .../main/java/to/bitkit/ui/MainActivity.kt | 39 ++++++++++++++++--- .../main/java/to/bitkit/ui/SharedViewModel.kt | 22 ++++++++++- app/src/main/java/to/bitkit/ui/theme/Theme.kt | 6 ++- 6 files changed, 69 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/LauncherActivity.kt b/app/src/main/java/to/bitkit/LauncherActivity.kt index 9421f855f..20c3271f4 100644 --- a/app/src/main/java/to/bitkit/LauncherActivity.kt +++ b/app/src/main/java/to/bitkit/LauncherActivity.kt @@ -12,7 +12,7 @@ import to.bitkit.ui.logFcmToken @AndroidEntryPoint class LauncherActivity : AppCompatActivity() { - private val sharedViewModel: SharedViewModel by viewModels() + private val sharedViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index d8be64af2..20b924eca 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -6,10 +6,13 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.ANDROID import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging +import io.ktor.http.ContentType +import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import to.bitkit.data.BlocktankApi @@ -42,6 +45,9 @@ object HttpModule { install(ContentNegotiation) { json(json = json) } + defaultRequest { // Set default request properties + contentType(ContentType.Application.Json) + } } } diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index 3e27ffbd6..4b5931975 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher +import to.bitkit.services.BlocktankService import to.bitkit.ui.SharedViewModel import javax.inject.Singleton @@ -15,9 +16,11 @@ object ViewModelModule { @Provides fun provideSharedViewModel( @BgDispatcher bgDispatcher: CoroutineDispatcher, + blocktankService: BlocktankService, ): SharedViewModel { return SharedViewModel( bgDispatcher, + blocktankService, ) } } diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 49dcf0ac2..01fde991d 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -21,10 +22,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.NotificationAdd import androidx.compose.material.icons.filled.NotificationsNone import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Card import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold @@ -37,6 +40,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -55,7 +59,7 @@ import to.bitkit.ui.theme.AppThemeSurface @AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel by viewModels() - private val sharedViewModel: SharedViewModel by viewModels() + private val sharedViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -66,10 +70,35 @@ class MainActivity : ComponentActivity() { AppThemeSurface { MainScreen(viewModel) { WalletScreen(viewModel) { - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - TextButton(onClick = viewModel::debugDb) { Text(text = "Debug DB") } - TextButton(onClick = viewModel::debugKeychain) { Text(text = "Debug Keychain") } - TextButton(onClick = viewModel::debugWipeBdk) { Text(text = "Wipe BDK") } + + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Debug", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), + ) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier.fillMaxWidth(), + ) { + TextButton(viewModel::debugDb) { Text("Debug DB") } + TextButton(viewModel::debugKeychain) { Text("Debug Keychain") } + TextButton(viewModel::debugWipeBdk) { Text("Wipe BDK") } + } + } + + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Notifications", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), + ) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier.fillMaxWidth(), + ) { + TextButton(sharedViewModel::registerForNotifications) { Text("Register Device") } + } } Peers(viewModel.peers, viewModel::togglePeerConnection) diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 1c0f0aec8..93f75008b 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -2,16 +2,23 @@ package to.bitkit.ui import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.tasks.await import to.bitkit.Tag.DEV +import to.bitkit.Tag.LSP import to.bitkit.di.BgDispatcher +import to.bitkit.services.BlocktankService import javax.inject.Inject @HiltViewModel class SharedViewModel @Inject constructor( - @BgDispatcher bgDispatcher: CoroutineDispatcher, + @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val blocktankService: BlocktankService, ) : ViewModel() { fun warmupNode() { // TODO make it concurrent, and wait for all to finish before trying to access `lightningService.node`, etc… @@ -22,4 +29,17 @@ class SharedViewModel @Inject constructor( fun logInstanceHashCode() { Log.d(DEV, "${this::class.java.simpleName} hashCode: ${hashCode()}") } + + fun registerForNotifications() { + viewModelScope.launch(bgDispatcher) { + val token = runCatching { FirebaseMessaging.getInstance().token.await() }.getOrNull() + requireNotNull(token) { "FCM token read error" } + + runCatching { + blocktankService.registerDevice(token) + }.onFailure { + Log.e(LSP, "Failed to register device with LSP", it) + } + } + } } diff --git a/app/src/main/java/to/bitkit/ui/theme/Theme.kt b/app/src/main/java/to/bitkit/ui/theme/Theme.kt index 453fbaf32..d2a458b78 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Theme.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Theme.kt @@ -10,7 +10,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color -val Brand100 = Color(0xFFFFF1EE) +val Gray100 = Color(0xFFF4F4F4) +val Brand50 = Color(0xFFFFF1EE) val Brand500 = Color(0xFFEC5428) val Teal200 = Color(0xFF03DAC5) @@ -18,9 +19,10 @@ private object ColorPalette { @Stable val Light = lightColorScheme( primary = Brand500, - primaryContainer = Brand100, + primaryContainer = Brand50, secondary = Teal200, background = Color.White, + surfaceVariant = Gray100, /* // Other default colors to override surface = Color.White, onPrimary = Color.White, From 165207e757ca0d6ecde3cadad837af6200a0bcb3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Aug 2024 20:49:37 +0200 Subject: [PATCH 08/13] refactor: Cleanup MigrationService after move --- .../to/bitkit/services/MigrationService.kt | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index c2dcb6474..0feab329f 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -7,32 +7,32 @@ import android.database.sqlite.SQLiteOpenHelper import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext import org.ldk.structs.KeysManager -import org.ldk.structs.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ -import org.ldk.structs.UtilMethods import to.bitkit.Env -import to.bitkit.Tag +import to.bitkit.Tag.LDK import to.bitkit.ext.hex import java.io.File import javax.inject.Inject import kotlin.io.path.Path +import org.ldk.structs.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK as ChannelMonitorDecodeResultTuple +import org.ldk.structs.UtilMethods.C2Tuple_ThirtyTwoBytesChannelMonitorZ_read as read32BytesChannelMonitor class MigrationService @Inject constructor( @ApplicationContext private val context: Context, ) { fun migrate(seed: ByteArray, manager: ByteArray, monitors: List) { - Log.d(Tag.LDK, "Migrating LDK backup…") + Log.d(LDK, "Migrating LDK backup…") val file = Path(Env.Storage.ldk, LDK_DB_NAME).toFile() // Skip if db already exists if (file.exists()) { - Log.d(Tag.LDK, "Migration skipped: ldk-node db exists at: $file") + Log.d(LDK, "Migration skipped: ldk-node db exists at: $file") return } val path = file.path - Log.d(Tag.LDK, "Creating ldk-node db at: $path") - Log.d(Tag.LDK, "Seeding ldk-node db with LDK backup data…") + Log.d(LDK, "Creating ldk-node db at: $path") + Log.d(LDK, "Seeding ldk-node db with LDK backup data…") LdkNodeDataDbHelper(context, path).writableDatabase.use { it.beginTransaction() @@ -49,7 +49,7 @@ class MigrationService @Inject constructor( File("$path-journal").delete() - Log.i(Tag.LDK, "Migrated LDK backup to ldk-node db at: $path") + Log.i(LDK, "Migrated LDK backup to ldk-node db at: $path") } private fun SQLiteDatabase.insertManager(manager: ByteArray) { @@ -70,12 +70,8 @@ class MigrationService @Inject constructor( val (entropySource, signerProvider) = keysManager.as_EntropySource() to keysManager.as_SignerProvider() for (monitor in monitors) { - val channelMonitor = UtilMethods.C2Tuple_ThirtyTwoBytesChannelMonitorZ_read( - monitor, - entropySource, - signerProvider - ).takeIf { it.is_ok } - ?.let { it as? Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK }?.res?._b + val channelMonitor = read32BytesChannelMonitor(monitor, entropySource, signerProvider).takeIf { it.is_ok } + ?.let { it as? ChannelMonitorDecodeResultTuple }?.res?._b ?: throw Error("Could not read channel monitor using read32BytesChannelMonitor") val fundingTx = channelMonitor._funding_txo._a._txid?.reversedArray()?.hex ?: throw Error("Could not read txid from funding tx OutPoint of channel monitor") @@ -90,23 +86,13 @@ class MigrationService @Inject constructor( } insert(LDK_NODE_DATA, null, values) - Log.d(Tag.LDK, "Inserted monitor: $key") + Log.d(LDK, "Inserted monitor: $key") } } - companion object { - private const val LDK_NODE_DATA = "ldk_node_data" - private const val PRIMARY_NAMESPACE = "primary_namespace" - private const val SECONDARY_NAMESPACE = "secondary_namespace" - private const val KEY = "key" - private const val VALUE = "value" - private const val LDK_DB_NAME = "$LDK_NODE_DATA.sqlite" - } -} - -private class LdkNodeDataDbHelper(context: Context, name: String) : SQLiteOpenHelper(context, name, null, VERSION) { - override fun onCreate(db: SQLiteDatabase) { - val query = """ + class LdkNodeDataDbHelper(context: Context, name: String) : SQLiteOpenHelper(context, name, null, LDK_DB_VERSION) { + override fun onCreate(db: SQLiteDatabase) { + val query = """ |CREATE TABLE ldk_node_data ( | primary_namespace TEXT NOT NULL, | secondary_namespace TEXT DEFAULT "" NOT NULL, @@ -115,12 +101,19 @@ private class LdkNodeDataDbHelper(context: Context, name: String) : SQLiteOpenHe | PRIMARY KEY (primary_namespace, secondary_namespace, `key`) |); """.trimMargin() - db.execSQL(query) - } + db.execSQL(query) + } - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit + } companion object { - const val VERSION = 2 + private const val LDK_NODE_DATA = "ldk_node_data" + private const val PRIMARY_NAMESPACE = "primary_namespace" + private const val SECONDARY_NAMESPACE = "secondary_namespace" + private const val KEY = "key" + private const val VALUE = "value" + private const val LDK_DB_NAME = "$LDK_NODE_DATA.sqlite" + private const val LDK_DB_VERSION = 2 // TODO: check on each ldk-node version update } } From 94c894d818032539b062710ae55a2f858c42246e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Aug 2024 21:08:06 +0200 Subject: [PATCH 09/13] feat: Add todo for next steps on BT device token registration --- app/src/main/java/to/bitkit/fcm/FcmService.kt | 1 + app/src/main/java/to/bitkit/ui/Notifications.kt | 1 + app/src/main/java/to/bitkit/ui/SharedViewModel.kt | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index e45b99e00..f590e4a91 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -70,5 +70,6 @@ internal class FcmService : FirebaseMessagingService() { override fun onNewToken(token: String) { this.token = token Log.d(FCM, "FCM registration token refreshed: $token") + // TODO call sharedViewModel.registerForNotifications(token) } } diff --git a/app/src/main/java/to/bitkit/ui/Notifications.kt b/app/src/main/java/to/bitkit/ui/Notifications.kt index f63e0319d..1d463dd19 100644 --- a/app/src/main/java/to/bitkit/ui/Notifications.kt +++ b/app/src/main/java/to/bitkit/ui/Notifications.kt @@ -89,6 +89,7 @@ fun logFcmToken() { return@OnCompleteListener } val token = task.result + // TODO call sharedViewModel.registerForNotifications(token) and move the listener body to there Log.d(FCM, "FCM registration token: $token") }) } diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 93f75008b..0e4859cac 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -30,9 +30,9 @@ class SharedViewModel @Inject constructor( Log.d(DEV, "${this::class.java.simpleName} hashCode: ${hashCode()}") } - fun registerForNotifications() { + fun registerForNotifications(fcmToken: String? = null) { viewModelScope.launch(bgDispatcher) { - val token = runCatching { FirebaseMessaging.getInstance().token.await() }.getOrNull() + val token = fcmToken ?: runCatching { FirebaseMessaging.getInstance().token.await() }.getOrNull() requireNotNull(token) { "FCM token read error" } runCatching { From 6a1fa1c38cb97529d70cf6a68b06fe623e3dc028 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Aug 2024 21:24:53 +0200 Subject: [PATCH 10/13] fix: Back navigation from main activity --- app/src/main/java/to/bitkit/LauncherActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/LauncherActivity.kt b/app/src/main/java/to/bitkit/LauncherActivity.kt index 20c3271f4..36ce1447d 100644 --- a/app/src/main/java/to/bitkit/LauncherActivity.kt +++ b/app/src/main/java/to/bitkit/LauncherActivity.kt @@ -20,6 +20,8 @@ class LauncherActivity : AppCompatActivity() { initNotificationChannel() logFcmToken() sharedViewModel.warmupNode() - startActivity(Intent(this, MainActivity::class.java)) + startActivity(Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + }) } } From 346a51602e026ab0a3cae0412708e87737fb2701 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Aug 2024 23:11:22 +0200 Subject: [PATCH 11/13] feat: UI notification for FCM message --- app/src/main/java/to/bitkit/ext/Context.kt | 2 +- app/src/main/java/to/bitkit/fcm/FcmService.kt | 13 ++-- .../main/java/to/bitkit/ui/MainActivity.kt | 9 --- .../main/java/to/bitkit/ui/Notifications.kt | 68 +++++++++++-------- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index d2318bc91..57eb28d62 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -37,7 +37,7 @@ internal fun toast( text: String, duration: Int = Toast.LENGTH_SHORT, ) { - with(currentActivity()) { + currentActivity().run { Toast.makeText(this, text, duration).show() } } diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index f590e4a91..763a0bfad 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -1,12 +1,15 @@ package to.bitkit.fcm +import android.os.Bundle import android.util.Log +import androidx.core.os.toPersistableBundle import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import to.bitkit.Tag.FCM +import to.bitkit.ui.pushNotification import java.util.Date internal class FcmService : FirebaseMessagingService() { @@ -15,10 +18,7 @@ internal class FcmService : FirebaseMessagingService() { /** * Act on received messages * - * To generate notifications as a result of a received FCM message, see: - * [MyFirebaseMessagingService.sendNotification](https://github.com/firebase/snippets-android/blob/ae9bd6ff8eccfb3eeba863d41eaca2b0e77eaa01/messaging/app/src/main/java/com/google/firebase/example/messaging/kotlin/MyFirebaseMessagingService.kt#L89-L124) - * - * [Debug messages not received](https://goo.gl/39bRNJ) + * [Debug](https://goo.gl/39bRNJ) */ override fun onMessageReceived(message: RemoteMessage) { Log.d(FCM, "New FCM at: ${Date(message.sentTime)}") @@ -26,6 +26,7 @@ internal class FcmService : FirebaseMessagingService() { message.notification?.run { Log.d(FCM, "FCM title: $title") Log.d(FCM, "FCM body: $body") + sendNotification(title, body, Bundle(message.data.toPersistableBundle())) } if (message.data.isNotEmpty()) { @@ -39,6 +40,10 @@ internal class FcmService : FirebaseMessagingService() { } } + private fun sendNotification(title: String?, body: String?, extras: Bundle) { + pushNotification(title, body, extras) + } + /** * Handle message within 10 seconds. */ diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 01fde991d..2ae496c39 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -1,6 +1,5 @@ package to.bitkit.ui -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult @@ -40,7 +39,6 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -110,13 +108,6 @@ class MainActivity : ComponentActivity() { } } -private val notificationPermission - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - android.Manifest.permission.POST_NOTIFICATIONS - } else { - TODO("Cant request 'POST_NOTIFICATIONS' permissions on SDK < 33") - } - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainScreen( diff --git a/app/src/main/java/to/bitkit/ui/Notifications.kt b/app/src/main/java/to/bitkit/ui/Notifications.kt index 1d463dd19..f5876e2c2 100644 --- a/app/src/main/java/to/bitkit/ui/Notifications.kt +++ b/app/src/main/java/to/bitkit/ui/Notifications.kt @@ -2,13 +2,17 @@ package to.bitkit.ui import android.Manifest import android.annotation.SuppressLint -import android.app.Activity import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_ONE_SHOT import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP +import android.media.RingtoneManager +import android.os.Build +import android.os.Bundle import android.util.Log import androidx.core.app.NotificationCompat import com.google.android.gms.tasks.OnCompleteListener @@ -34,54 +38,58 @@ fun Context.initNotificationChannel( } internal fun Context.notificationBuilder( + extra: Bundle? = null, channelId: String = CHANNEL_MAIN, ): NotificationCompat.Builder { - val activityIntent = Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + val intent = Intent(this, MainActivity::class.java).apply { + flags = FLAG_ACTIVITY_CLEAR_TOP + extra?.let { putExtras(it) } } - val pendingIntent = PendingIntent.getActivity(this, 0, activityIntent, FLAG_IMMUTABLE) + val flags = FLAG_IMMUTABLE or FLAG_ONE_SHOT + val pendingIntent = PendingIntent.getActivity(this, 0, intent, flags) return NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setContentIntent(pendingIntent) // fired on tap .setAutoCancel(true) // remove on tap } +@SuppressLint("MissingPermission") internal fun pushNotification( - title: String, - text: String, - bigText: String, + title: String?, + text: String?, + extras: Bundle? = null, + bigText: String? = null, + id: Int = Random.nextInt(), ): Int { - val id = Random.nextInt() - with(currentActivity()) { - pushNotification( - title = title, - text = text, - bigText = bigText, - id = id, - ) + currentActivity().withPermission(notificationPermission) { + val builder = notificationBuilder(extras) + .setContentTitle(title) + .setContentText(text) + .apply { + bigText?.let { + setStyle(NotificationCompat.BigTextStyle().bigText(bigText)) + } + } + notificationManagerCompat.notify(id, builder.build()) } return id } -@SuppressLint("MissingPermission") // Handled by custom guard -internal fun Activity.pushNotification( - title: String, - text: String, - bigText: String, - id: Int, -) { - if (requiresPermission(Manifest.permission.POST_NOTIFICATIONS)) return - - val builder = notificationBuilder() - .setContentTitle(title) - .setContentText(text) - .setStyle(NotificationCompat.BigTextStyle().bigText(bigText)) - - notificationManagerCompat.notify(id, builder.build()) +inline fun Context.withPermission(permission: String, block: Context.() -> T) { + if (requiresPermission(permission)) return + block() } +val notificationPermission + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.POST_NOTIFICATIONS + } else { + TODO("Cant request 'POST_NOTIFICATIONS' permissions on SDK < 33") + } + fun logFcmToken() { FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> if (!task.isSuccessful) { From 6db003eb4a761a819f43c15d141e12cb9eac4ee5 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 21 Aug 2024 00:25:34 +0200 Subject: [PATCH 12/13] feat: Test LSP notification to self --- app/src/main/java/to/bitkit/data/LspApi.kt | 35 ++++++++++++--- app/src/main/java/to/bitkit/di/HttpModule.kt | 12 ++--- app/src/main/java/to/bitkit/fcm/FcmService.kt | 40 ++++++++++++++++- .../to/bitkit/services/BlocktankService.kt | 22 ++++++++-- .../main/java/to/bitkit/ui/MainActivity.kt | 1 + .../main/java/to/bitkit/ui/MainViewModel.kt | 44 ++++++++++++------- .../main/java/to/bitkit/ui/SharedViewModel.kt | 3 +- 7 files changed, 123 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/LspApi.kt b/app/src/main/java/to/bitkit/data/LspApi.kt index 2160290c4..976a8a8c4 100644 --- a/app/src/main/java/to/bitkit/data/LspApi.kt +++ b/app/src/main/java/to/bitkit/data/LspApi.kt @@ -1,28 +1,32 @@ package to.bitkit.data import io.ktor.client.HttpClient -import io.ktor.client.call.body import io.ktor.client.request.post import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse import kotlinx.serialization.Serializable import javax.inject.Inject interface LspApi { - suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest): String + suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) + suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse } class BlocktankApi @Inject constructor( private val client: HttpClient, ) : LspApi { private val baseUrl = "https://api.stag.blocktank.to" + private val notificationsApi = "$baseUrl/notifications/api/device" - override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest): String { - val response = client.post("$baseUrl/notifications/api/device") { - setBody(payload) - } + override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) { + post(notificationsApi, payload) + } - return response.body().decodeToString() + override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse { + return post("$notificationsApi/$deviceToken/test-notification", payload) } + + private suspend inline fun post(url: String, payload: T) = client.post(url) { setBody(payload) } } @Serializable @@ -34,3 +38,20 @@ data class RegisterDeviceRequest( val isoTimestamp: String, val signature: String, ) + +@Serializable +data class TestNotificationRequest( + val data: Data, +) { + @Serializable + data class Data( + val source: String, + val type: String, + val payload: Payload, + ) { + @Serializable + data class Payload( + val secretMessage: String, + ) + } +} diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index 20b924eca..e0bcdd697 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -21,17 +21,19 @@ import to.bitkit.data.LspApi import to.bitkit.data.RestApi import javax.inject.Singleton +val json = Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true +} + @Module @InstallIn(SingletonComponent::class) object HttpModule { @Provides @Singleton fun provideJson(): Json { - return Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - } + return json } @Provides diff --git a/app/src/main/java/to/bitkit/fcm/FcmService.kt b/app/src/main/java/to/bitkit/fcm/FcmService.kt index 763a0bfad..84dfd386a 100644 --- a/app/src/main/java/to/bitkit/fcm/FcmService.kt +++ b/app/src/main/java/to/bitkit/fcm/FcmService.kt @@ -2,13 +2,18 @@ package to.bitkit.fcm import android.os.Bundle import android.util.Log +import androidx.core.os.bundleOf import androidx.core.os.toPersistableBundle import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString import to.bitkit.Tag.FCM +import to.bitkit.di.json import to.bitkit.ui.pushNotification import java.util.Date @@ -48,7 +53,29 @@ internal class FcmService : FirebaseMessagingService() { * Handle message within 10 seconds. */ private fun handleNow(data: Map) { - Log.e(FCM, "FCM handler not implemented for: $data") + val isHandled = data.runAs { + val extras = bundleOf( + "tag" to tag, + "sound" to sound, + "publicKey" to publicKey, + ) + + sendNotification(title, message, extras) + } + if (!isHandled) { + Log.e(FCM, "FCM handler not implemented for: $data") + } + } + + private inline fun Map.runAs(block: T.() -> Unit): Boolean { + val encoded = json.encodeToString(this) + return try { + val decoded = json.decodeFromString(encoded) + block(decoded) + true + } catch (e: SerializationException) { + false + } } /** @@ -78,3 +105,14 @@ internal class FcmService : FirebaseMessagingService() { // TODO call sharedViewModel.registerForNotifications(token) } } + +@Serializable +data class EncryptedNotification( + val cipher: String, + val iv: String, + val tag: String, + val sound: String, + val title: String, + val message: String, + val publicKey: String, +) diff --git a/app/src/main/java/to/bitkit/services/BlocktankService.kt b/app/src/main/java/to/bitkit/services/BlocktankService.kt index 8b4011bb5..db1b42ae6 100644 --- a/app/src/main/java/to/bitkit/services/BlocktankService.kt +++ b/app/src/main/java/to/bitkit/services/BlocktankService.kt @@ -7,6 +7,7 @@ import to.bitkit.async.BaseCoroutineScope import to.bitkit.async.ServiceQueue import to.bitkit.data.LspApi import to.bitkit.data.RegisterDeviceRequest +import to.bitkit.data.TestNotificationRequest import to.bitkit.di.BgDispatcher import java.time.Instant import java.time.format.DateTimeFormatter @@ -20,17 +21,16 @@ class BlocktankService @Inject constructor( ) : BaseCoroutineScope(bgDispatcher) { suspend fun registerDevice(deviceToken: String) { - // UserDefaults.standard.setValue(deviceToken, forKey: "deviceToken") val nodeId = requireNotNull(lightningService.nodeId) { "Node not started" } - Log.d(LSP, "Registering device for notifications") + Log.d(LSP, "Registering device for notifications…") val isoTimestamp = DateTimeFormatter.ISO_INSTANT.format(Instant.now().truncatedTo(ChronoUnit.SECONDS)) val messageToSign = "bitkit-notifications$deviceToken$isoTimestamp" val signature = lightningService.sign(messageToSign) - // TODO: Use real public key to enable decryption of the push notification payload + // TODO: Use actual public key to enable decryption of the push notification payload val publicKey = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" val payload = RegisterDeviceRequest( @@ -46,4 +46,20 @@ class BlocktankService @Inject constructor( lspApi.registerDeviceForNotifications(payload) } } + + suspend fun testNotification(deviceToken: String) { + Log.d(LSP, "Sending test notification to self…") + + val payload = TestNotificationRequest( + data = TestNotificationRequest.Data( + source = "blocktank", + type = "incomingHtlc", + payload = TestNotificationRequest.Data.Payload(secretMessage = "hello") + ) + ) + + ServiceQueue.LSP.background { + lspApi.testNotification(deviceToken, payload) + } + } } diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 2ae496c39..5898b4961 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -96,6 +96,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxWidth(), ) { TextButton(sharedViewModel::registerForNotifications) { Text("Register Device") } + TextButton(viewModel::debugLspNotifications) { Text("LSP Notification") } } } diff --git a/app/src/main/java/to/bitkit/ui/MainViewModel.kt b/app/src/main/java/to/bitkit/ui/MainViewModel.kt index 691f74643..30fb887e3 100644 --- a/app/src/main/java/to/bitkit/ui/MainViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/MainViewModel.kt @@ -5,19 +5,22 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.LnPeer import to.bitkit.SEED import to.bitkit.Tag.DEV -import to.bitkit.services.BitcoinService import to.bitkit.data.AppDb import to.bitkit.data.keychain.KeychainStore import to.bitkit.di.BgDispatcher import to.bitkit.ext.syncTo +import to.bitkit.services.BitcoinService +import to.bitkit.services.BlocktankService import to.bitkit.services.LightningService import to.bitkit.services.closeChannel import to.bitkit.services.connectPeer @@ -31,6 +34,7 @@ class MainViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val bitcoinService: BitcoinService, private val lightningService: LightningService, + private val blocktankService: BlocktankService, private val keychain: KeychainStore, private val appDb: AppDb, ) : ViewModel() { @@ -105,6 +109,23 @@ class MainViewModel @Inject constructor( } } + fun refresh() { + viewModelScope.launch { + "Refreshing…".also { + ldkNodeId.value = it + ldkBalance.value = it + btcAddress.value = it + btcBalance.value = it + } + peers.clear() + channels.clear() + + delay(50) + lightningService.sync() + sync() + } + } + // region debug fun debugDb() { viewModelScope.launch { @@ -127,24 +148,15 @@ class MainViewModel @Inject constructor( fun debugWipeBdk() { bitcoinService.wipeStorage() } - // endregion - - fun refresh() { - viewModelScope.launch { - "Refreshing…".also { - ldkNodeId.value = it - ldkBalance.value = it - btcAddress.value = it - btcBalance.value = it - } - peers.clear() - channels.clear() - delay(50) - lightningService.sync() - sync() + fun debugLspNotifications() { + viewModelScope.launch(bgDispatcher) { + val token = FirebaseMessaging.getInstance().token.await() + blocktankService.testNotification(token) } } + + // endregion } fun MainViewModel.togglePeerConnection(peer: LnPeer) = diff --git a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt index 0e4859cac..8aa401dcd 100644 --- a/app/src/main/java/to/bitkit/ui/SharedViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/SharedViewModel.kt @@ -32,8 +32,7 @@ class SharedViewModel @Inject constructor( fun registerForNotifications(fcmToken: String? = null) { viewModelScope.launch(bgDispatcher) { - val token = fcmToken ?: runCatching { FirebaseMessaging.getInstance().token.await() }.getOrNull() - requireNotNull(token) { "FCM token read error" } + val token = fcmToken ?: FirebaseMessaging.getInstance().token.await() runCatching { blocktankService.registerDevice(token) From 90fb275f0834011fca530c317e352307e15c7cdc Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Aug 2024 14:22:43 +0200 Subject: [PATCH 13/13] chore: Cleanup code --- app/src/main/java/to/bitkit/Constants.kt | 14 ++++++-------- app/src/main/java/to/bitkit/data/LspApi.kt | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/Constants.kt b/app/src/main/java/to/bitkit/Constants.kt index 1ef189c96..88fc03663 100644 --- a/app/src/main/java/to/bitkit/Constants.kt +++ b/app/src/main/java/to/bitkit/Constants.kt @@ -103,13 +103,11 @@ data class LnPeer( override fun toString() = "$nodeId@${address}" companion object { - fun PeerDetails.toLnPeer(): LnPeer { - return LnPeer( - nodeId = nodeId, - address = address, - isConnected = isConnected, - isPersisted = isPersisted, - ) - } + fun PeerDetails.toLnPeer() = LnPeer( + nodeId = nodeId, + address = address, + isConnected = isConnected, + isPersisted = isPersisted, + ) } } diff --git a/app/src/main/java/to/bitkit/data/LspApi.kt b/app/src/main/java/to/bitkit/data/LspApi.kt index 976a8a8c4..0dff0fb8f 100644 --- a/app/src/main/java/to/bitkit/data/LspApi.kt +++ b/app/src/main/java/to/bitkit/data/LspApi.kt @@ -9,7 +9,7 @@ import javax.inject.Inject interface LspApi { suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) - suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse + suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest) } class BlocktankApi @Inject constructor( @@ -22,8 +22,8 @@ class BlocktankApi @Inject constructor( post(notificationsApi, payload) } - override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse { - return post("$notificationsApi/$deviceToken/test-notification", payload) + override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest) { + post("$notificationsApi/$deviceToken/test-notification", payload) } private suspend inline fun post(url: String, payload: T) = client.post(url) { setBody(payload) }