Skip to content

Commit c33bf74

Browse files
authored
Merge pull request #129 from synonymdev/feat/fee-rates
feat: Custom fee rates
2 parents 319a7c9 + d0ff09c commit c33bf74

34 files changed

+787
-117
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import androidx.datastore.preferences.preferencesDataStore
99
import dagger.hilt.android.qualifiers.ApplicationContext
1010
import kotlinx.coroutines.flow.Flow
1111
import kotlinx.coroutines.flow.map
12+
import to.bitkit.di.json
1213
import to.bitkit.ext.enumValueOfOrNull
1314
import to.bitkit.models.BitcoinDisplayUnit
1415
import to.bitkit.models.PrimaryDisplay
16+
import to.bitkit.models.TransactionSpeed
1517
import to.bitkit.utils.Logger
1618
import javax.inject.Inject
1719
import javax.inject.Singleton
@@ -48,6 +50,20 @@ class SettingsStore @Inject constructor(
4850
store.edit { it[SELECTED_CURRENCY_KEY] = currency }
4951
}
5052

53+
val defaultTransactionSpeed: Flow<TransactionSpeed> = store.data
54+
.map {
55+
it[DEFAULT_TX_SPEED]?.let { x -> runCatching { json.decodeFromString<TransactionSpeed>(x) }.getOrNull() }
56+
?: TransactionSpeed.Medium
57+
}
58+
59+
suspend fun setDefaultTransactionSpeed(speed: TransactionSpeed) {
60+
val encoded = runCatching { json.encodeToString(speed) }.getOrElse {
61+
Logger.error("Failed to encode default transaction speed: $it")
62+
return
63+
}
64+
store.edit { it[DEFAULT_TX_SPEED] = encoded }
65+
}
66+
5167
val showEmptyState: Flow<Boolean> = store.data.map { it[SHOW_EMPTY_STATE] == true }
5268
suspend fun setShowEmptyState(show: Boolean) {
5369
store.edit { it[SHOW_EMPTY_STATE] = show }
@@ -92,6 +108,7 @@ class SettingsStore @Inject constructor(
92108
private val PRIMARY_DISPLAY_UNIT_KEY = stringPreferencesKey("primary_display_unit")
93109
private val BTC_DISPLAY_UNIT_KEY = stringPreferencesKey("btc_display_unit")
94110
private val SELECTED_CURRENCY_KEY = stringPreferencesKey("selected_currency")
111+
private val DEFAULT_TX_SPEED = stringPreferencesKey("default_tx_speed")
95112
private val SHOW_EMPTY_STATE = booleanPreferencesKey("show_empty_state")
96113
private val HAS_SEEN_SPENDING_INTRO = booleanPreferencesKey("has_seen_spending_intro")
97114
private val HAS_SEEN_SAVINGS_INTRO = booleanPreferencesKey("has_seen_savings_intro")

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.lightningdevkit.ldknode.Network
55
import to.bitkit.BuildConfig
66
import to.bitkit.ext.ensureDir
77
import to.bitkit.models.LnPeer
8-
import to.bitkit.models.blocktank.BlocktankNotificationType
8+
import to.bitkit.models.BlocktankNotificationType
99
import to.bitkit.utils.Logger
1010
import java.io.File
1111
import kotlin.io.path.Path
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package to.bitkit.ext
2+
3+
import to.bitkit.models.TransactionSpeed
4+
import uniffi.bitkitcore.FeeRates
5+
6+
fun FeeRates.getSatsPerVByteFor(speed: TransactionSpeed): UInt {
7+
return when (speed) {
8+
is TransactionSpeed.Fast -> fast
9+
is TransactionSpeed.Medium -> mid
10+
is TransactionSpeed.Slow -> slow
11+
is TransactionSpeed.Custom -> speed.satsPerVByte
12+
}
13+
}

app/src/main/java/to/bitkit/fcm/FcmService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import to.bitkit.di.json
1717
import to.bitkit.env.Env.DERIVATION_NAME
1818
import to.bitkit.ext.fromBase64
1919
import to.bitkit.ext.fromHex
20-
import to.bitkit.models.blocktank.BlocktankNotificationType
20+
import to.bitkit.models.BlocktankNotificationType
2121
import to.bitkit.ui.pushNotification
2222
import to.bitkit.utils.Crypto
2323
import to.bitkit.utils.Logger

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import to.bitkit.di.json
1818
import to.bitkit.models.NewTransactionSheetDetails
1919
import to.bitkit.models.NewTransactionSheetDirection
2020
import to.bitkit.models.NewTransactionSheetType
21-
import to.bitkit.models.blocktank.BlocktankNotificationType
22-
import to.bitkit.models.blocktank.BlocktankNotificationType.cjitPaymentArrived
23-
import to.bitkit.models.blocktank.BlocktankNotificationType.incomingHtlc
24-
import to.bitkit.models.blocktank.BlocktankNotificationType.mutualClose
25-
import to.bitkit.models.blocktank.BlocktankNotificationType.orderPaymentConfirmed
26-
import to.bitkit.models.blocktank.BlocktankNotificationType.wakeToTimeout
21+
import to.bitkit.models.BlocktankNotificationType
22+
import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived
23+
import to.bitkit.models.BlocktankNotificationType.incomingHtlc
24+
import to.bitkit.models.BlocktankNotificationType.mutualClose
25+
import to.bitkit.models.BlocktankNotificationType.orderPaymentConfirmed
26+
import to.bitkit.models.BlocktankNotificationType.wakeToTimeout
2727
import to.bitkit.repositories.LightningRepo
2828
import to.bitkit.services.CoreService
2929
import to.bitkit.ui.pushNotification

app/src/main/java/to/bitkit/models/blocktank/BlocktankNotificationType.kt renamed to app/src/main/java/to/bitkit/models/BlocktankNotificationType.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package to.bitkit.models.blocktank
1+
package to.bitkit.models
22

33
import kotlinx.serialization.Serializable
44

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package to.bitkit.models
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.descriptors.PrimitiveKind
6+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7+
import kotlinx.serialization.descriptors.SerialDescriptor
8+
import kotlinx.serialization.encoding.Decoder
9+
import kotlinx.serialization.encoding.Encoder
10+
11+
@Serializable(with = TransactionSpeedSerializer::class)
12+
sealed class TransactionSpeed {
13+
object Fast : TransactionSpeed()
14+
object Medium : TransactionSpeed()
15+
object Slow : TransactionSpeed()
16+
data class Custom(val satsPerVByte: UInt) : TransactionSpeed()
17+
18+
fun serialized(): String = when (this) {
19+
is Fast -> "fast"
20+
is Medium -> "medium"
21+
is Slow -> "slow"
22+
is Custom -> "custom_$satsPerVByte"
23+
}
24+
25+
companion object {
26+
fun fromString(value: String): TransactionSpeed = when {
27+
value == "fast" -> Fast
28+
value == "medium" -> Medium
29+
value == "slow" -> Slow
30+
value.matches(Regex("custom_\\d+")) -> {
31+
value.substringAfter("custom_")
32+
.toUIntOrNull()
33+
?.let { Custom(it) }
34+
?: Medium
35+
}
36+
37+
else -> Medium
38+
}
39+
}
40+
}
41+
42+
private object TransactionSpeedSerializer : KSerializer<TransactionSpeed> {
43+
override val descriptor: SerialDescriptor =
44+
PrimitiveSerialDescriptor("TransactionSpeed", PrimitiveKind.STRING)
45+
46+
override fun serialize(encoder: Encoder, value: TransactionSpeed) {
47+
encoder.encodeString(value.serialized())
48+
}
49+
50+
override fun deserialize(decoder: Decoder): TransactionSpeed {
51+
return TransactionSpeed.fromString(decoder.decodeString())
52+
}
53+
}

app/src/main/java/to/bitkit/repositories/LightningRepo.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import org.lightningdevkit.ldknode.PaymentDetails
1616
import org.lightningdevkit.ldknode.PaymentId
1717
import org.lightningdevkit.ldknode.Txid
1818
import org.lightningdevkit.ldknode.UserChannelId
19+
import to.bitkit.data.SettingsStore
1920
import to.bitkit.di.BgDispatcher
21+
import to.bitkit.ext.getSatsPerVByteFor
2022
import to.bitkit.models.LnPeer
2123
import to.bitkit.models.NodeLifecycleState
24+
import to.bitkit.models.TransactionSpeed
25+
import to.bitkit.services.CoreService
2226
import to.bitkit.services.LdkNodeEventBus
2327
import to.bitkit.services.LightningService
2428
import to.bitkit.services.NodeEventHandler
@@ -34,7 +38,9 @@ class LightningRepo @Inject constructor(
3438
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
3539
private val lightningService: LightningService,
3640
private val ldkNodeEventBus: LdkNodeEventBus,
37-
private val addressChecker: AddressChecker
41+
private val addressChecker: AddressChecker,
42+
private val settingsStore: SettingsStore,
43+
private val coreService: CoreService,
3844
) {
3945
private val _nodeLifecycleState: MutableStateFlow<NodeLifecycleState> = MutableStateFlow(NodeLifecycleState.Stopped)
4046
val nodeLifecycleState = _nodeLifecycleState.asStateFlow()
@@ -221,9 +227,23 @@ class LightningRepo @Inject constructor(
221227
Result.success(paymentId)
222228
}
223229

224-
suspend fun sendOnChain(address: Address, sats: ULong): Result<Txid> =
230+
/**
231+
* Sends bitcoin to an on-chain address
232+
*
233+
* @param address The bitcoin address to send to
234+
* @param sats The amount in satoshis to send
235+
* @param speed The desired transaction speed determining the fee rate. If null, the user's default speed is used.
236+
* @return A `Result` with the `Txid` of sent transaction, or an error if the transaction fails
237+
* or the fee rate cannot be retrieved.
238+
*/
239+
suspend fun sendOnChain(address: Address, sats: ULong, speed: TransactionSpeed? = null): Result<Txid> =
225240
executeWhenNodeRunning("Send on-chain") {
226-
val txId = lightningService.send(address = address, sats = sats)
241+
val transactionSpeed = speed ?: settingsStore.defaultTransactionSpeed.first()
242+
243+
var fees = coreService.blocktank.getFees().getOrThrow()
244+
var satsPerVByte = fees.getSatsPerVByteFor(transactionSpeed)
245+
246+
val txId = lightningService.send(address = address, sats = sats, satsPerVByte = satsPerVByte)
227247
Result.success(txId)
228248
}
229249

app/src/main/java/to/bitkit/services/CoreService.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import uniffi.bitkitcore.BtOrderState2
2121
import uniffi.bitkitcore.CJitStateEnum
2222
import uniffi.bitkitcore.CreateCjitOptions
2323
import uniffi.bitkitcore.CreateOrderOptions
24+
import uniffi.bitkitcore.FeeRates
2425
import uniffi.bitkitcore.IBtEstimateFeeResponse2
2526
import uniffi.bitkitcore.IBtInfo
2627
import uniffi.bitkitcore.IBtOrder
@@ -435,12 +436,29 @@ class BlocktankService(
435436
private val coreService: CoreService,
436437
private val lightningService: LightningService,
437438
) {
438-
suspend fun info(refresh: Boolean = false): IBtInfo? {
439+
suspend fun info(refresh: Boolean = true): IBtInfo? {
439440
return ServiceQueue.CORE.background {
440441
getInfo(refresh = refresh)
441442
}
442443
}
443444

445+
private suspend fun fees(refresh: Boolean = true) : FeeRates? {
446+
return info(refresh)?.onchain?.feeRates
447+
}
448+
449+
suspend fun getFees() : Result<FeeRates> {
450+
var fees = fees(refresh = true)
451+
if (fees == null) {
452+
Logger.warn("Failed to fetch fresh fee rate, using cached rate.")
453+
fees = fees(refresh = false)
454+
}
455+
if (fees == null) {
456+
return Result.failure(AppError("Fees unavailable from bitkit-core"))
457+
}
458+
459+
return Result.success(fees)
460+
}
461+
444462
suspend fun createCjit(
445463
channelSizeSat: ULong,
446464
invoiceSat: ULong,

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,17 +361,16 @@ class LightningService @Inject constructor(
361361
return true
362362
}
363363

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

368-
Logger.info("Sending $sats sats to $address")
367+
Logger.info("Sending $sats sats to $address with satsPerVByte=$satsPerVByte")
369368

370369
return ServiceQueue.LDK.background {
371370
node.onchainPayment().sendToAddress(
372371
address = address,
373372
amountSats = sats,
374-
feeRate = FeeRate.fromSatPerKwu(satKwu)
373+
feeRate = convertVByteToKwu(satsPerVByte)
375374
)
376375
}
377376
}
@@ -508,4 +507,11 @@ fun List<ChannelDetails>.filterOpen(): List<ChannelDetails> {
508507
return this.filter { it.isChannelReady }
509508
}
510509

510+
511+
private fun convertVByteToKwu(satsPerVByte: UInt): FeeRate {
512+
// 1 vbyte = 4 weight units, so 1 sats/vbyte = 250 sats/kwu
513+
val satPerKwu = satsPerVByte.toULong() * 250u
514+
// Ensure we're above the minimum relay fee
515+
return FeeRate.fromSatPerKwu(maxOf(satPerKwu, 253u)) // FEERATE_FLOOR_SATS_PER_KW is 253 in LDK
516+
}
511517
// endregion

0 commit comments

Comments
 (0)