Skip to content
Merged
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
17 changes: 17 additions & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import to.bitkit.di.json
import to.bitkit.ext.enumValueOfOrNull
import to.bitkit.models.BitcoinDisplayUnit
import to.bitkit.models.PrimaryDisplay
import to.bitkit.models.TransactionSpeed
import to.bitkit.utils.Logger
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -48,6 +50,20 @@ class SettingsStore @Inject constructor(
store.edit { it[SELECTED_CURRENCY_KEY] = currency }
}

val defaultTransactionSpeed: Flow<TransactionSpeed> = store.data
.map {
it[DEFAULT_TX_SPEED]?.let { x -> runCatching { json.decodeFromString<TransactionSpeed>(x) }.getOrNull() }
?: TransactionSpeed.Medium
}

suspend fun setDefaultTransactionSpeed(speed: TransactionSpeed) {
val encoded = runCatching { json.encodeToString(speed) }.getOrElse {
Logger.error("Failed to encode default transaction speed: $it")
return
}
store.edit { it[DEFAULT_TX_SPEED] = encoded }
}

val showEmptyState: Flow<Boolean> = store.data.map { it[SHOW_EMPTY_STATE] == true }
suspend fun setShowEmptyState(show: Boolean) {
store.edit { it[SHOW_EMPTY_STATE] = show }
Expand Down Expand Up @@ -92,6 +108,7 @@ class SettingsStore @Inject constructor(
private val PRIMARY_DISPLAY_UNIT_KEY = stringPreferencesKey("primary_display_unit")
private val BTC_DISPLAY_UNIT_KEY = stringPreferencesKey("btc_display_unit")
private val SELECTED_CURRENCY_KEY = stringPreferencesKey("selected_currency")
private val DEFAULT_TX_SPEED = stringPreferencesKey("default_tx_speed")
private val SHOW_EMPTY_STATE = booleanPreferencesKey("show_empty_state")
private val HAS_SEEN_SPENDING_INTRO = booleanPreferencesKey("has_seen_spending_intro")
private val HAS_SEEN_SAVINGS_INTRO = booleanPreferencesKey("has_seen_savings_intro")
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.lightningdevkit.ldknode.Network
import to.bitkit.BuildConfig
import to.bitkit.ext.ensureDir
import to.bitkit.models.LnPeer
import to.bitkit.models.blocktank.BlocktankNotificationType
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.utils.Logger
import java.io.File
import kotlin.io.path.Path
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/to/bitkit/ext/FeeRates.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package to.bitkit.ext

import to.bitkit.models.TransactionSpeed
import uniffi.bitkitcore.FeeRates

fun FeeRates.getSatsPerVByteFor(speed: TransactionSpeed): UInt {
return when (speed) {
is TransactionSpeed.Fast -> fast
is TransactionSpeed.Medium -> mid
is TransactionSpeed.Slow -> slow
is TransactionSpeed.Custom -> speed.satsPerVByte
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/fcm/FcmService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import to.bitkit.di.json
import to.bitkit.env.Env.DERIVATION_NAME
import to.bitkit.ext.fromBase64
import to.bitkit.ext.fromHex
import to.bitkit.models.blocktank.BlocktankNotificationType
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.ui.pushNotification
import to.bitkit.utils.Crypto
import to.bitkit.utils.Logger
Expand Down
12 changes: 6 additions & 6 deletions app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import to.bitkit.di.json
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.NewTransactionSheetDirection
import to.bitkit.models.NewTransactionSheetType
import to.bitkit.models.blocktank.BlocktankNotificationType
import to.bitkit.models.blocktank.BlocktankNotificationType.cjitPaymentArrived
import to.bitkit.models.blocktank.BlocktankNotificationType.incomingHtlc
import to.bitkit.models.blocktank.BlocktankNotificationType.mutualClose
import to.bitkit.models.blocktank.BlocktankNotificationType.orderPaymentConfirmed
import to.bitkit.models.blocktank.BlocktankNotificationType.wakeToTimeout
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived
import to.bitkit.models.BlocktankNotificationType.incomingHtlc
import to.bitkit.models.BlocktankNotificationType.mutualClose
import to.bitkit.models.BlocktankNotificationType.orderPaymentConfirmed
import to.bitkit.models.BlocktankNotificationType.wakeToTimeout
import to.bitkit.repositories.LightningRepo
import to.bitkit.services.CoreService
import to.bitkit.ui.pushNotification
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package to.bitkit.models.blocktank
package to.bitkit.models

import kotlinx.serialization.Serializable

Expand Down
53 changes: 53 additions & 0 deletions app/src/main/java/to/bitkit/models/TransactionSpeed.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package to.bitkit.models

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable(with = TransactionSpeedSerializer::class)
sealed class TransactionSpeed {
object Fast : TransactionSpeed()
object Medium : TransactionSpeed()
object Slow : TransactionSpeed()
data class Custom(val satsPerVByte: UInt) : TransactionSpeed()

fun serialized(): String = when (this) {
is Fast -> "fast"
is Medium -> "medium"
is Slow -> "slow"
is Custom -> "custom_$satsPerVByte"
}

companion object {
fun fromString(value: String): TransactionSpeed = when {
value == "fast" -> Fast
value == "medium" -> Medium
value == "slow" -> Slow
value.matches(Regex("custom_\\d+")) -> {
value.substringAfter("custom_")
.toUIntOrNull()
?.let { Custom(it) }
?: Medium
}

else -> Medium
}
}
}

private object TransactionSpeedSerializer : KSerializer<TransactionSpeed> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("TransactionSpeed", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: TransactionSpeed) {
encoder.encodeString(value.serialized())
}

override fun deserialize(decoder: Decoder): TransactionSpeed {
return TransactionSpeed.fromString(decoder.decodeString())
}
}
26 changes: 23 additions & 3 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import org.lightningdevkit.ldknode.PaymentDetails
import org.lightningdevkit.ldknode.PaymentId
import org.lightningdevkit.ldknode.Txid
import org.lightningdevkit.ldknode.UserChannelId
import to.bitkit.data.SettingsStore
import to.bitkit.di.BgDispatcher
import to.bitkit.ext.getSatsPerVByteFor
import to.bitkit.models.LnPeer
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.TransactionSpeed
import to.bitkit.services.CoreService
import to.bitkit.services.LdkNodeEventBus
import to.bitkit.services.LightningService
import to.bitkit.services.NodeEventHandler
Expand All @@ -34,7 +38,9 @@ class LightningRepo @Inject constructor(
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
private val lightningService: LightningService,
private val ldkNodeEventBus: LdkNodeEventBus,
private val addressChecker: AddressChecker
private val addressChecker: AddressChecker,
private val settingsStore: SettingsStore,
private val coreService: CoreService,
) {
private val _nodeLifecycleState: MutableStateFlow<NodeLifecycleState> = MutableStateFlow(NodeLifecycleState.Stopped)
val nodeLifecycleState = _nodeLifecycleState.asStateFlow()
Expand Down Expand Up @@ -221,9 +227,23 @@ class LightningRepo @Inject constructor(
Result.success(paymentId)
}

suspend fun sendOnChain(address: Address, sats: ULong): Result<Txid> =
/**
* Sends bitcoin to an on-chain address
*
* @param address The bitcoin address to send to
* @param sats The amount in satoshis to send
* @param speed The desired transaction speed determining the fee rate. If null, the user's default speed is used.
* @return A `Result` with the `Txid` of sent transaction, or an error if the transaction fails
* or the fee rate cannot be retrieved.
*/
suspend fun sendOnChain(address: Address, sats: ULong, speed: TransactionSpeed? = null): Result<Txid> =
executeWhenNodeRunning("Send on-chain") {
val txId = lightningService.send(address = address, sats = sats)
val transactionSpeed = speed ?: settingsStore.defaultTransactionSpeed.first()

var fees = coreService.blocktank.getFees().getOrThrow()
var satsPerVByte = fees.getSatsPerVByteFor(transactionSpeed)

val txId = lightningService.send(address = address, sats = sats, satsPerVByte = satsPerVByte)
Result.success(txId)
}

Expand Down
20 changes: 19 additions & 1 deletion app/src/main/java/to/bitkit/services/CoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import uniffi.bitkitcore.BtOrderState2
import uniffi.bitkitcore.CJitStateEnum
import uniffi.bitkitcore.CreateCjitOptions
import uniffi.bitkitcore.CreateOrderOptions
import uniffi.bitkitcore.FeeRates
import uniffi.bitkitcore.IBtEstimateFeeResponse2
import uniffi.bitkitcore.IBtInfo
import uniffi.bitkitcore.IBtOrder
Expand Down Expand Up @@ -435,12 +436,29 @@ class BlocktankService(
private val coreService: CoreService,
private val lightningService: LightningService,
) {
suspend fun info(refresh: Boolean = false): IBtInfo? {
suspend fun info(refresh: Boolean = true): IBtInfo? {
return ServiceQueue.CORE.background {
getInfo(refresh = refresh)
}
}

private suspend fun fees(refresh: Boolean = true) : FeeRates? {
return info(refresh)?.onchain?.feeRates
}

suspend fun getFees() : Result<FeeRates> {
var fees = fees(refresh = true)
if (fees == null) {
Logger.warn("Failed to fetch fresh fee rate, using cached rate.")
fees = fees(refresh = false)
}
if (fees == null) {
return Result.failure(AppError("Fees unavailable from bitkit-core"))
}

return Result.success(fees)
}

suspend fun createCjit(
channelSizeSat: ULong,
invoiceSat: ULong,
Expand Down
14 changes: 10 additions & 4 deletions app/src/main/java/to/bitkit/services/LightningService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -361,17 +361,16 @@ class LightningService @Inject constructor(
return true
}

// TODO: get feeRate from real source
suspend fun send(address: Address, sats: ULong, satKwu: ULong = 250uL * 5uL): Txid {
suspend fun send(address: Address, sats: ULong, satsPerVByte: UInt): Txid {
val node = this.node ?: throw ServiceError.NodeNotSetup

Logger.info("Sending $sats sats to $address")
Logger.info("Sending $sats sats to $address with satsPerVByte=$satsPerVByte")

return ServiceQueue.LDK.background {
node.onchainPayment().sendToAddress(
address = address,
amountSats = sats,
feeRate = FeeRate.fromSatPerKwu(satKwu)
feeRate = convertVByteToKwu(satsPerVByte)
)
}
}
Expand Down Expand Up @@ -508,4 +507,11 @@ fun List<ChannelDetails>.filterOpen(): List<ChannelDetails> {
return this.filter { it.isChannelReady }
}


private fun convertVByteToKwu(satsPerVByte: UInt): FeeRate {
// 1 vbyte = 4 weight units, so 1 sats/vbyte = 250 sats/kwu
val satPerKwu = satsPerVByte.toULong() * 250u
// Ensure we're above the minimum relay fee
return FeeRate.fromSatPerKwu(maxOf(satPerKwu, 253u)) // FEERATE_FLOOR_SATS_PER_KW is 253 in LDK
}
// endregion
30 changes: 27 additions & 3 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ import to.bitkit.ui.settings.LogsScreen
import to.bitkit.ui.settings.OrderDetailScreen
import to.bitkit.ui.settings.SecuritySettingsScreen
import to.bitkit.ui.settings.SettingsScreen
import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen
import to.bitkit.ui.settings.backups.BackupWalletScreen
import to.bitkit.ui.settings.backups.RestoreWalletScreen
import to.bitkit.ui.settings.pin.ChangePinConfirmScreen
import to.bitkit.ui.settings.pin.ChangePinNewScreen
import to.bitkit.ui.settings.pin.ChangePinResultScreen
import to.bitkit.ui.settings.pin.ChangePinScreen
import to.bitkit.ui.settings.pin.DisablePinScreen
import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen
import to.bitkit.ui.utils.screenScaleIn
import to.bitkit.ui.utils.screenScaleOut
import to.bitkit.ui.utils.screenSlideIn
Expand Down Expand Up @@ -243,6 +245,7 @@ fun ContentView(
settings(walletViewModel, navController)
nodeState(walletViewModel, navController)
generalSettings(navController)
transactionSpeedSettings(navController)
securitySettings(navController)
disablePin(navController)
changePin(navController)
Expand Down Expand Up @@ -495,11 +498,18 @@ private fun NavGraphBuilder.generalSettings(navController: NavHostController) {
}
}

private fun NavGraphBuilder.transactionSpeedSettings(navController: NavHostController) {
composableWithDefaultTransitions<Routes.TransactionSpeedSettings> {
TransactionSpeedSettingsScreen(navController)
}
composableWithDefaultTransitions<Routes.CustomFeeSettings> {
CustomFeeSettingsScreen(navController)
}
}

private fun NavGraphBuilder.securitySettings(navController: NavHostController) {
composableWithDefaultTransitions<Routes.SecuritySettings> {
SecuritySettingsScreen(
navController = navController,
)
SecuritySettingsScreen(navController = navController)
}
}

Expand Down Expand Up @@ -876,6 +886,14 @@ fun NavController.navigateToLogs() = navigate(
fun NavController.navigateToLogDetail(fileName: String) = navigate(
route = Routes.LogDetail(fileName),
)

fun NavController.navigateToTransactionSpeedSettings() = navigate(
route = Routes.TransactionSpeedSettings,
)

fun NavController.navigateToCustomFeeSettings() = navigate(
route = Routes.CustomFeeSettings,
)
// endregion

object Routes {
Expand All @@ -891,6 +909,12 @@ object Routes {
@Serializable
data object GeneralSettings

@Serializable
data object TransactionSpeedSettings

@Serializable
data object CustomFeeSettings

@Serializable
data object SecuritySettings

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ private fun PinPad(
pin = pin,
modifier = Modifier.padding(vertical = 16.dp),
)
PinNumberPad(
NumberPadSimple(
modifier = Modifier.height(310.dp),
onPress = { key ->
if (key == KEY_DELETE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private fun NumberButton(
}

@Composable
fun PinNumberPad(
fun NumberPadSimple(
onPress: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Expand Down Expand Up @@ -123,7 +123,7 @@ fun PinNumberPad(
@Composable
private fun Preview() {
AppThemeSurface {
PinNumberPad(
NumberPadSimple(
onPress = {},
modifier = Modifier.height(310.dp)
)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ui/components/Text.kt
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ fun Caption13Up(
fontWeight = FontWeight.Medium,
fontSize = 13.sp,
lineHeight = 18.sp,
letterSpacing = 0.8.sp,
letterSpacing = 0.4.sp,
fontFamily = InterFontFamily,
color = color,
textAlign = TextAlign.Start,
Expand Down
Loading