Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d1f2086
refactor: remove color from pencil icon
ovitrif Aug 5, 2025
4bae300
fix: small money text color
ovitrif Aug 5, 2025
f94c54a
feat: send speed and fee screens scaffold & nav
ovitrif Aug 5, 2025
6eaf21c
feat: fee items ui
ovitrif Aug 5, 2025
7342337
fix: insets in previews
ovitrif Aug 5, 2025
281013f
feat: speed screen viewmodel
ovitrif Aug 5, 2025
618b8a8
fix: use selected utxos to estimate send fee
ovitrif Aug 5, 2025
952bfcb
feat: calculate send amount fee by rate
ovitrif Aug 5, 2025
0754427
fix: navigate back from speed screen
ovitrif Aug 5, 2025
432241c
fix: use the speed selected in settings for send
ovitrif Aug 5, 2025
b78bbfe
feat: reuse fetched fee rates
ovitrif Aug 5, 2025
06bd762
feat: use fee rate details in send confirm screen
ovitrif Aug 5, 2025
96c79e6
refactor: rename back to ic_pencil_simple
ovitrif Aug 5, 2025
efea810
chore: fix WithdrawErrorScreen caps
ovitrif Aug 6, 2025
6363daa
fix: remove divider transparency onclick
ovitrif Aug 6, 2025
6713a69
fix: resetSendState race conditions
ovitrif Aug 6, 2025
510ee36
refactor: extract FeeRate
ovitrif Aug 6, 2025
8be67a5
feat: fee amount on send review screen
ovitrif Aug 6, 2025
927619d
chore: fix lint
ovitrif Aug 6, 2025
c2c533c
feat: lower log level for overly frequent logs
ovitrif Aug 6, 2025
317ef86
feat: add utxos to logs for calculateTotalFee
ovitrif Aug 6, 2025
479c0b6
fix: dont validate amount for LN in onchain send
ovitrif Aug 6, 2025
8aeaa15
fix: preselect utxos for deterministic fee estimation
ovitrif Aug 6, 2025
bc69beb
chore: cleanup
ovitrif Aug 6, 2025
b509af0
fix: preload fee rates before each send flow
ovitrif Aug 6, 2025
ef05e09
refactor: rename to NavigateToConfirm
ovitrif Aug 6, 2025
29092ce
fix: refresh fee estimate on speed change
ovitrif Aug 6, 2025
3e15ec7
fix: pass feeRates to getFeeRateForSpeed
ovitrif Aug 6, 2025
12b8b3a
fix: only reset utxos if stsPerVByte changes
ovitrif Aug 6, 2025
b13ad37
chore: fix tests
ovitrif Aug 7, 2025
3fc00b0
feat: log error type
ovitrif Aug 7, 2025
deb5de7
feat: new lightning repo tests
ovitrif Aug 7, 2025
5a93201
chore: fix boost tests
ovitrif Aug 7, 2025
3e671b7
chore: update detekt baseline
ovitrif Aug 7, 2025
896db27
feat: lower log level for overly frequent logs
ovitrif Aug 7, 2025
d0245fb
chore: logger fixes
ovitrif Aug 7, 2025
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
54 changes: 7 additions & 47 deletions app/detekt-baseline.xml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/data/BlocktankHttpClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class BlocktankHttpClient @Inject constructor(
) {
suspend fun fetchLatestRates(): FxRateResponse {
val response = client.get(Env.btcRatesServer)
Logger.debug("Http call: $response")
Logger.verbose("Http call: $response")

return when (response.status.isSuccess()) {
true -> response.body()
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ data class SettingsData(
val primaryDisplay: PrimaryDisplay = PrimaryDisplay.BITCOIN,
val displayUnit: BitcoinDisplayUnit = BitcoinDisplayUnit.MODERN,
val selectedCurrency: String = "USD",
val defaultTransactionSpeed: TransactionSpeed = TransactionSpeed.Medium,
val defaultTransactionSpeed: TransactionSpeed = TransactionSpeed.default(),
val showEmptyBalanceView: Boolean = true,
val hasSeenSpendingIntro: Boolean = false,
val hasSeenWidgetsIntro: Boolean = false,
Expand Down
16 changes: 8 additions & 8 deletions app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class VssBackupClient @Inject constructor(
suspend fun setup() = withContext(bgDispatcher) {
try {
withTimeout(30.seconds) {
Logger.debug("VSS client setting up…", context = TAG)
Logger.verbose("VSS client setting up…", context = TAG)
vssNewClient(
baseUrl = Env.vssServerUrl,
storeId = vssStoreIdProvider.getVssStoreId(),
Expand All @@ -44,34 +44,34 @@ class VssBackupClient @Inject constructor(
data: ByteArray,
): Result<VssItem> = withContext(bgDispatcher) {
isSetup.await()
Logger.debug("VSS 'putObject' call for '$key'", context = TAG)
Logger.verbose("VSS 'putObject' call for '$key'", context = TAG)
runCatching {
vssStore(
key = key,
value = data,
)
}.onSuccess {
Logger.debug("VSS 'putObject' success for '$key' at version: ${it.version}", context = TAG)
Logger.verbose("VSS 'putObject' success for '$key' at version: ${it.version}", context = TAG)
}.onFailure { e ->
Logger.error("VSS 'putObject' error for '$key'", e = e, context = TAG)
Logger.verbose("VSS 'putObject' error for '$key'", e = e, context = TAG)
}
}

suspend fun getObject(key: String): Result<VssItem?> = withContext(bgDispatcher) {
isSetup.await()
Logger.debug("VSS 'getObject' call for '$key'", context = TAG)
Logger.verbose("VSS 'getObject' call for '$key'", context = TAG)
runCatching {
vssGet(
key = key,
)
}.onSuccess {
if (it == null) {
Logger.warn("VSS 'getObject' success null for '$key'", context = TAG)
Logger.verbose("VSS 'getObject' success null for '$key'", context = TAG)
} else {
Logger.debug("VSS 'getObject' success for '$key'", context = TAG)
Logger.verbose("VSS 'getObject' success for '$key'", context = TAG)
}
}.onFailure { e ->
Logger.error("VSS 'getObject' error for '$key'", e = e, context = TAG)
Logger.verbose("VSS 'getObject' error for '$key'", e = e, context = TAG)
}
}

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

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.Color
import to.bitkit.R
import to.bitkit.ui.theme.Colors

enum class FeeRate(
@StringRes val title: Int,
@StringRes val description: Int,
@DrawableRes val icon: Int,
val color: Color,
) {
FAST(
title = R.string.fee__fast__title,
description = R.string.fee__fast__description,
color = Colors.Brand,
icon = R.drawable.ic_speed_fast,
),
NORMAL(
title = R.string.fee__normal__title,
description = R.string.fee__normal__description,
color = Colors.Brand,
icon = R.drawable.ic_speed_normal,
),
SLOW(
title = R.string.fee__slow__title,
description = R.string.fee__slow__description,
color = Colors.Brand,
icon = R.drawable.ic_speed_slow,
),
CUSTOM(
title = R.string.fee__custom__title,
description = R.string.fee__custom__description,
color = Colors.White64,
icon = R.drawable.ic_settings,
);

fun toSpeed(): TransactionSpeed {
return when (this) {
FAST -> TransactionSpeed.Fast
NORMAL -> TransactionSpeed.Medium
SLOW -> TransactionSpeed.Slow
CUSTOM -> TransactionSpeed.Custom(0u)
}
}

companion object {
fun fromSpeed(speed: TransactionSpeed): FeeRate {
return when (speed) {
is TransactionSpeed.Fast -> FAST
is TransactionSpeed.Medium -> NORMAL
is TransactionSpeed.Slow -> SLOW
is TransactionSpeed.Custom -> CUSTOM
}
}
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/models/TransactionSpeed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ sealed class TransactionSpeed {
}

companion object {
fun default(): TransactionSpeed = Medium

fun fromString(value: String): TransactionSpeed = when {
value == "fast" -> Fast
value == "medium" -> Medium
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,15 @@ class BackupRepo @Inject constructor(
cacheStore.updateBackupStatus(category) {
it.copy(required = System.currentTimeMillis())
}
Logger.debug("Marked backup required for: '$category'", context = TAG)
Logger.verbose("Marked backup required for: '$category'", context = TAG)
}
}

private fun scheduleBackup(category: BackupCategory) {
// Cancel existing backup job for this category
backupJobs[category]?.cancel()

Logger.debug("Scheduling backup for: '$category'", context = TAG)
Logger.verbose("Scheduling backup for: '$category'", context = TAG)

backupJobs[category] = scope.launch {
delay(BACKUP_DEBOUNCE)
Expand Down
7 changes: 2 additions & 5 deletions app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class BlocktankRepo @Inject constructor(
isRefreshing = true

try {
Logger.debug("Refreshing blocktank orders…", context = TAG)
Logger.verbose("Refreshing blocktank orders…", context = TAG)

val paidOrderIds = cacheStore.data.first().paidOrders.keys

Expand All @@ -142,10 +142,7 @@ class BlocktankRepo @Inject constructor(
)
}

Logger.debug(
"Orders refreshed: ${orders.size} orders, ${cjitEntries.size} cjit entries",
context = TAG
)
Logger.debug("Orders refreshed: ${orders.size} orders, ${cjitEntries.size} cjit entries", context = TAG)
} catch (e: Throwable) {
Logger.error("Failed to refresh orders", e, context = TAG)
} finally {
Expand Down
47 changes: 22 additions & 25 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package to.bitkit.repositories

import com.google.firebase.messaging.FirebaseMessaging
import com.synonym.bitkitcore.FeeRates
import com.synonym.bitkitcore.LightningInvoice
import com.synonym.bitkitcore.Scanner
import com.synonym.bitkitcore.createWithdrawCallbackUrl
Expand Down Expand Up @@ -87,7 +88,7 @@ class LightningRepo @Inject constructor(
waitTimeout: Duration = 1.minutes,
operation: suspend () -> Result<T>,
): Result<T> = withContext(bgDispatcher) {
Logger.debug("Operation called: $operationName", context = TAG)
Logger.verbose("Operation called: $operationName", context = TAG)

if (_lightningState.value.nodeLifecycleState.isRunning()) {
return@withContext executeOperation(operationName, operation)
Expand All @@ -106,7 +107,7 @@ class LightningRepo @Inject constructor(
}

// Otherwise, wait for it to transition to running state
Logger.debug("Waiting for node runs to execute $operationName", context = TAG)
Logger.verbose("Waiting for node runs to execute $operationName", context = TAG)
_lightningState.first { it.nodeLifecycleState.isRunning() }
Logger.debug("Operation executed: $operationName", context = TAG)
true
Expand Down Expand Up @@ -480,28 +481,18 @@ class LightningRepo @Inject constructor(
Result.success(paymentId)
}

/**
* 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.
* @param utxosToSpend Manually specify UTXO's to spend if not null.
* @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,
utxosToSpend: List<SpendableUtxo>? = null,
feeRates: FeeRates? = null,
isTransfer: Boolean = false,
channelId: String? = null,
): Result<Txid> =
executeWhenNodeRunning("Send on-chain") {
val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed
val satsPerVByte = getFeeRateForSpeed(transactionSpeed).getOrThrow().toUInt()
val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow().toUInt()

// if utxos are manually specified, use them, otherwise run auto coin select if enabled
val finalUtxosToSpend = utxosToSpend ?: determineUtxosToSpend(
Expand Down Expand Up @@ -531,33 +522,35 @@ class LightningRepo @Inject constructor(
Result.success(txId)
}

private suspend fun determineUtxosToSpend(
suspend fun determineUtxosToSpend(
sats: ULong,
satsPerVByte: UInt,
): List<SpendableUtxo>? {
return runCatching {
): List<SpendableUtxo>? = withContext(bgDispatcher) {
return@withContext runCatching {
val settings = settingsStore.data.first()
if (settings.coinSelectAuto) {
val coinSelectionPreference = settings.coinSelectPreference

val allSpendableUtxos = lightningService.listSpendableOutputs().getOrThrow()

if (coinSelectionPreference == CoinSelectionPreference.Consolidate) {
Logger.info("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG)
return allSpendableUtxos
Logger.debug("Consolidating by spending all ${allSpendableUtxos.size} UTXOs", context = TAG)
return@withContext allSpendableUtxos
}

val coinSelectionAlgorithm = coinSelectionPreference.toCoinSelectAlgorithm().getOrThrow()

Logger.info("Selecting UTXOs with algorithm: $coinSelectionAlgorithm for sats: $sats", context = TAG)
Logger.debug("All spendable UTXOs: $allSpendableUtxos", context = TAG)
Logger.debug("Selecting UTXOs with algorithm: $coinSelectionAlgorithm for sats: $sats", context = TAG)
Logger.verbose("All spendable UTXOs: $allSpendableUtxos", context = TAG)

lightningService.selectUtxosWithAlgorithm(
targetAmountSats = sats,
algorithm = coinSelectionAlgorithm,
satsPerVByte = satsPerVByte,
utxos = allSpendableUtxos,
).getOrThrow()
).onSuccess {
Logger.debug("Selected ${it.size} UTXOs", context = TAG)
}.getOrThrow()
} else {
null // let ldk-node handle utxos
}
Expand All @@ -579,10 +572,11 @@ class LightningRepo @Inject constructor(
address: Address? = null,
speed: TransactionSpeed? = null,
utxosToSpend: List<SpendableUtxo>? = null,
feeRates: FeeRates? = null,
): Result<ULong> = withContext(bgDispatcher) {
return@withContext try {
val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed
val satsPerVByte = getFeeRateForSpeed(transactionSpeed).getOrThrow().toUInt()
val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow().toUInt()

val addressOrDefault = address ?: cacheStore.data.first().onchainAddress

Expand All @@ -600,9 +594,12 @@ class LightningRepo @Inject constructor(
}
}

suspend fun getFeeRateForSpeed(speed: TransactionSpeed): Result<ULong> = withContext(bgDispatcher) {
suspend fun getFeeRateForSpeed(
speed: TransactionSpeed,
feeRates: FeeRates? = null,
): Result<ULong> = withContext(bgDispatcher) {
return@withContext runCatching {
val fees = coreService.blocktank.getFees().getOrThrow()
val fees = feeRates ?: coreService.blocktank.getFees().getOrThrow()
val satsPerVByte = fees.getSatsPerVByteFor(speed)
satsPerVByte.toULong()
}.onFailure { e ->
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class WalletRepo @Inject constructor(
}

suspend fun syncNodeAndWallet(): Result<Unit> = withContext(bgDispatcher) {
Logger.debug("Refreshing node and wallet state…")
Logger.verbose("Refreshing node and wallet state…")
syncBalances()
lightningRepo.sync().onSuccess {
syncBalances()
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ class WidgetsRepo @Inject constructor(
service.fetchData()
.onSuccess { data ->
updateStore(data)
Logger.debug("Updated $widgetType widget successfully")
Logger.verbose("Updated $widgetType widget successfully")
}
.onFailure { error ->
Logger.warn(e = error, msg = "Failed to update $widgetType widget", context = TAG)
.onFailure { e ->
Logger.verbose("Failed to update $widgetType widget", e = e, context = TAG)
}

_refreshStates.update { it + (widgetType to false) }
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/java/to/bitkit/services/CoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,12 @@ class CoreService @Inject constructor(
/** Returns true if geo blocked */
suspend fun checkGeoStatus(): Boolean? {
return ServiceQueue.CORE.background {
Logger.info("Checking geo status…", context = "GeoCheck")
Logger.verbose("Checking geo status…", context = "GeoCheck")
val response = httpClient.get(Env.geoCheckUrl)
Logger.debug("Received geo status response: ${response.status.value}", context = "GeoCheck")

when (response.status.value) {
HttpStatusCode.OK.value -> {
Logger.info("Region allowed", context = "GeoCheck")
Logger.verbose("Region allowed", context = "GeoCheck")
false
}

Expand Down
8 changes: 5 additions & 3 deletions app/src/main/java/to/bitkit/services/LightningService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,12 @@ class LightningService @Inject constructor(
suspend fun sync() {
val node = this.node ?: throw ServiceError.NodeNotSetup

Logger.debug("Syncing LDK…")
Logger.verbose("Syncing LDK…")
ServiceQueue.LDK.background {
node.syncWallets()
// launch { setMaxDustHtlcExposureForCurrentChannels() }
}
Logger.info("LDK synced")
Logger.debug("LDK synced")
}

// private fun setMaxDustHtlcExposureForCurrentChannels() {
Expand Down Expand Up @@ -556,7 +556,9 @@ class LightningService @Inject constructor(
): ULong {
val node = this.node ?: throw ServiceError.NodeNotSetup

Logger.info("Calculating fee for $amountSats sats to $address, satsPerVByte=$satsPerVByte")
Logger.info(
"Calculating fee for $amountSats sats to $address, UTXOs=${utxosToSpend?.size}, satsPerVByte=$satsPerVByte"
)

return ServiceQueue.LDK.background {
return@background try {
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen
import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorSheet
import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet
import to.bitkit.ui.screens.wallets.receive.ReceiveSheet
import to.bitkit.ui.sheets.SendSheet
import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen
import to.bitkit.ui.screens.widgets.AddWidgetsScreen
import to.bitkit.ui.screens.widgets.WidgetsIntroScreen
Expand Down Expand Up @@ -137,6 +136,7 @@ import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen
import to.bitkit.ui.sheets.BackupSheet
import to.bitkit.ui.sheets.LnurlAuthSheet
import to.bitkit.ui.sheets.PinSheet
import to.bitkit.ui.sheets.SendSheet
import to.bitkit.ui.utils.AutoReadClipboardHandler
import to.bitkit.ui.utils.composableWithDefaultTransitions
import to.bitkit.ui.utils.screenSlideIn
Expand Down Expand Up @@ -326,7 +326,6 @@ fun ContentView(
walletViewModel = walletViewModel,
startDestination = sheet.route,
onComplete = { txSheet ->
appViewModel.resetSendState()
appViewModel.hideSheet()
appViewModel.clearClipboardForAutoRead()
txSheet?.let { appViewModel.showNewTransactionSheet(it) }
Expand Down
Loading