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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ class FundingScreenTest {
assertEquals(0, manualClicks)
}

@Test
fun whenGeoBlocked_onlyManualSetupShouldInvoke() {
setContent(isGeoBlocked = true)

clickableTransferOption().performClick()
composeTestRule.onNodeWithTag("FundReceive").performClick()
composeTestRule.onNodeWithTag("FundManual").performClick()
composeTestRule.waitForIdle()

assertEquals(0, transferClicks)
assertEquals(0, fundClicks)
assertEquals(1, manualClicks)
}

@Test
fun whenFundableBalanceIsZero_transferShouldNotRouteToTheTransferFlow() {
setContent(fundableBalance = 0uL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import javax.inject.Singleton

/**
* Builds the user-facing "Payment Received" notification content, applying the same currency
* formatting rules everywhere a received payment is surfaced (foreground service, in-app handler,
* and the background [to.bitkit.fcm.WakeNodeWorker] push path).
* formatting rules everywhere a received payment is surfaced (foreground service and in-app handler).
*/
@Singleton
class ReceivedNotificationContent @Inject constructor(
Expand Down
43 changes: 1 addition & 42 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import android.os.Build
import com.synonym.bitkitcore.UsdtDestination
import org.lightningdevkit.ldknode.LogLevel
import org.lightningdevkit.ldknode.Network
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.BuildConfig
import to.bitkit.BuildConfig.VERSION_NAME
import to.bitkit.ext.ensureDir
import to.bitkit.ext.of
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.models.NodePeer
import to.bitkit.utils.Logger
import java.io.File
import kotlin.io.path.Path
Expand Down Expand Up @@ -52,14 +48,6 @@ internal object Env {

val ldkLogLevel = LogLevel.TRACE

val trustedLnPeers
get() = when (network) {
Network.BITCOIN -> listOf(Peers.lnd1, Peers.lnd3, Peers.lnd4)
Network.REGTEST -> listOf(Peers.stag)
Network.TESTNET -> listOf(Peers.stag)
else -> listOf()
}

/**
* Whether LN -> onchain swaps can reach a Boltz backend. Boltz only serves a public API on
* mainnet: its testnet deployment is deprecated and regtest resolves to a local backend that
Expand All @@ -71,9 +59,6 @@ internal object Env {
const val fxRateStaleThreshold = 10 * 60 * 1000L // 10 minutes in millis
const val lspOrdersRefreshInterval = 2 * 60 * 1000L // 2 minutes in millis

val pushNotificationFeatures = BlocktankNotificationType.entries
const val derivationName = "bitkit-notifications"

const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.fileprovider"
const val SUPPORT_EMAIL = "support@synonym.to"
const val PIN_LENGTH = 4
Expand Down Expand Up @@ -140,11 +125,6 @@ internal object Env {
Network.BITCOIN -> "$blocktankBaseUrl/api"
else -> "$blocktankBaseUrl/blocktank/api/v2"
}
val blocktankNotificationApiUrl
get() = when (network) {
Network.BITCOIN -> "$blocktankBaseUrl/api/notifications"
else -> "$blocktankBaseUrl/notifications/api"
}

val btcRatesServer
get() = when (network) {
Expand Down Expand Up @@ -177,6 +157,7 @@ internal object Env {
const val BITKIT_TELEGRAM = "https://t.me/bitkitchat"
const val BITKIT_GITHUB = "https://github.com/synonymdev"
const val BITKIT_HELP_CENTER = "https://help.bitkit.to"
const val BLOCKTANK_SUNSET_URL = "https://blog.bitkit.to/"
const val TERMS_OF_USE_URL = "https://bitkit.to/terms-of-use"
const val PRIVACY_POLICY_URL = "https://bitkit.to/privacy-policy"
const val STORING_BITCOINS_URL = "https://en.bitcoin.it/wiki/Storing_bitcoins"
Expand Down Expand Up @@ -283,28 +264,6 @@ object Defaults {
const val TAG_MAX_LENGTH = 20
}

object Peers {
val stag = PeerDetails.of("028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc@34.65.86.104:9400")
val lnd1 = PeerDetails.of("039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3@34.65.111.104:9735")
val lnd3 = PeerDetails.of("03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b@34.65.191.64:9735")
val lnd4 = PeerDetails.of("02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187@34.65.153.174:9735")

object Known {
val stag = NodePeer(Peers.stag, name = "Synonym-Own-Regtest-0")
val lnd1 = NodePeer(Peers.lnd1, name = "Blocktank-LND1")
val lnd3 = NodePeer(Peers.lnd3, name = "Blocktank-LND3")
val lnd4 = NodePeer(Peers.lnd4, name = "Blocktank-LND4")

fun find(peer: PeerDetails): NodePeer? = when (peer.nodeId) {
stag.peerDetails.nodeId -> stag
lnd1.peerDetails.nodeId -> lnd1
lnd3.peerDetails.nodeId -> lnd3
lnd4.peerDetails.nodeId -> lnd4
else -> null
}
}
}

private object ElectrumServers {
object MAINNET {
const val ESPLORA = "ssl://bitkit.to:9999"
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/to/bitkit/ext/LightningBalance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ fun LightningBalance.channelId(): String {
}
}

fun LightningBalance.counterpartyNodeId(): String {
return when (this) {
is LightningBalance.ClaimableOnChannelClose -> this.counterpartyNodeId
is LightningBalance.ClaimableAwaitingConfirmations -> this.counterpartyNodeId
is LightningBalance.ContentiousClaimable -> this.counterpartyNodeId
is LightningBalance.MaybeTimeoutClaimableHtlc -> this.counterpartyNodeId
is LightningBalance.MaybePreimageClaimableHtlc -> this.counterpartyNodeId
is LightningBalance.CounterpartyRevokedOutputClaimable -> this.counterpartyNodeId
}
}

fun LightningBalance.claimableAtHeight(): UInt? = when (this) {
is LightningBalance.ClaimableAwaitingConfirmations -> this.confirmationHeight
is LightningBalance.ContentiousClaimable -> this.timeoutHeight
Expand Down
138 changes: 0 additions & 138 deletions app/src/main/java/to/bitkit/fcm/FcmService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,19 @@ package to.bitkit.fcm

import android.os.Bundle
import androidx.core.os.toPersistableBundle
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonObject
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.json
import to.bitkit.env.Env.derivationName
import to.bitkit.ext.fromBase64
import to.bitkit.ext.fromHex
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.repositories.LightningRepo
import to.bitkit.ui.pushNotification
import to.bitkit.utils.Crypto
import to.bitkit.utils.Logger
import java.util.Date
import javax.inject.Inject

@AndroidEntryPoint
class FcmService : FirebaseMessagingService() {
companion object {
private const val TAG = "FcmService"
}

private var notificationType: BlocktankNotificationType? = null
private var notificationPayload: JsonObject? = null

@Inject
lateinit var crypto: Crypto

@Inject
lateinit var keychain: Keychain

@Inject
lateinit var lightningRepo: LightningRepo

/**
* Act on received messages. [Debug](https://goo.gl/39bRNJ)
*/
Expand All @@ -54,123 +26,13 @@ class FcmService : FirebaseMessagingService() {
Logger.debug("FCM body: $body", context = TAG)
sendNotification(title, body, Bundle(message.data.toPersistableBundle()))
}

if (message.data.isNotEmpty()) {
Logger.debug("FCM data: ${message.data}", context = TAG)

val shouldSchedule = runCatching {
val isEncryptedNotification = message.data.tryAs<EncryptedNotification> {
decryptPayload(it)
}
isEncryptedNotification
}.getOrElse {
Logger.error("Failed to read encrypted notification payload", it, context = TAG)
// Let the node to spin up and handle incoming events
true
}

when (shouldSchedule) {
true -> handleAsync()
else -> handleNow(message.data)
}
}
}

private fun handleAsync() {
val work = OneTimeWorkRequestBuilder<WakeNodeWorker>()
.setInputData(
workDataOf(
"type" to notificationType?.name,
"payload" to notificationPayload?.toString(),
)
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(this)
.beginWith(work)
.enqueue()
}

private fun handleNow(data: Map<String, String>) {
Logger.warn("FCM handler not implemented for: $data", context = TAG)
}

private fun decryptPayload(response: EncryptedNotification) {
val ciphertext = runCatching { response.cipher.fromBase64() }.getOrElse {
Logger.error("Failed to decode cipher", it, context = TAG)
return
}
val privateKey = runCatching { keychain.load(Keychain.Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)!! }.getOrElse {
Logger.error("Missing PUSH_NOTIFICATION_PRIVATE_KEY", it, context = TAG)
return
}
val password =
runCatching { crypto.generateSharedSecret(privateKey, response.publicKey, derivationName) }.getOrElse {
Logger.error("Failed to generate shared secret", it, context = TAG)
return
}

val decrypted = crypto.decrypt(
encryptedPayload = Crypto.EncryptedPayload(ciphertext, response.iv.fromHex(), response.tag.fromHex()),
secretKey = password,
)

val decoded = decrypted.decodeToString()
Logger.debug("Decrypted payload: $decoded", context = TAG)

val (payload, type) = runCatching { json.decodeFromString<DecryptedNotification>(decoded) }.getOrElse {
Logger.error("Failed to decode decrypted data", it, context = TAG)
return
}

if (payload == null) {
Logger.error("Missing payload", context = TAG)
return
}

if (type == null) {
Logger.error("Missing type", context = TAG)
return
}

notificationType = type
notificationPayload = payload
}

private fun sendNotification(title: String?, body: String?, extras: Bundle? = null) {
applicationContext.pushNotification(title, body, extras)
}

private inline fun <reified T> Map<String, String>.tryAs(block: (T) -> Unit): Boolean {
val encoded = json.encodeToString(this)
return try {
val decoded = json.decodeFromString<T>(encoded)
block(decoded)
true
} catch (_: SerializationException) {
false
}
}

override fun onNewToken(token: String) {
Logger.debug("Got new FCM token via FirebaseMessagingService.onNewToken: '$token'", context = TAG)
lightningRepo.registerForNotificationsAsync(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 = "",
)

@Serializable
data class DecryptedNotification(
val payload: JsonObject? = null,
val type: BlocktankNotificationType? = null,
)
Loading
Loading