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
8 changes: 8 additions & 0 deletions app/src/main/java/to/bitkit/models/TransactionSpeed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package to.bitkit.models

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.synonym.bitkitcore.FeeRates
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
Expand All @@ -25,6 +26,13 @@ sealed class TransactionSpeed {
is Custom -> "custom_$satsPerVByte"
}

fun getFeeRate(feeRates: FeeRates): UInt = when (this) {
is Fast -> feeRates.fast
is Medium -> feeRates.mid
is Slow -> feeRates.slow
is Custom -> satsPerVByte
}

companion object {
fun default(): TransactionSpeed = Medium

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

import com.synonym.bitkitcore.FeeRates
import com.synonym.bitkitcore.broadcastSweepTransaction
import com.synonym.bitkitcore.checkSweepableBalances
import com.synonym.bitkitcore.prepareSweepTransaction
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import to.bitkit.async.ServiceQueue
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.BgDispatcher
import to.bitkit.env.Env
import to.bitkit.models.toCoreNetwork
import to.bitkit.services.CoreService
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import to.bitkit.viewmodels.SweepResult
import to.bitkit.viewmodels.SweepTransactionPreview
import to.bitkit.viewmodels.SweepableBalances
import javax.inject.Inject
import javax.inject.Singleton
import com.synonym.bitkitcore.SweepResult as BitkitCoreSweepResult
import com.synonym.bitkitcore.SweepTransactionPreview as BitkitCoreSweepTransactionPreview
import com.synonym.bitkitcore.SweepableBalances as BitkitCoreSweepableBalances

@Singleton
class SweepRepo @Inject constructor(
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
private val keychain: Keychain,
private val coreService: CoreService,
) {
suspend fun checkSweepableBalances(): Result<SweepableBalances> = withContext(bgDispatcher) {
runCatching {
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
?: throw ServiceError.MnemonicNotFound
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)

Logger.debug("Checking sweepable balances...", context = TAG)

val balances = ServiceQueue.CORE.background {
checkSweepableBalances(
mnemonicPhrase = mnemonic,
network = Env.network.toCoreNetwork(),
bip39Passphrase = passphrase,
electrumUrl = Env.electrumServerUrl,
)
}

balances.toSweepableBalances()
}
}

suspend fun prepareSweepTransaction(
destinationAddress: String,
feeRateSatsPerVbyte: UInt,
): Result<SweepTransactionPreview> = withContext(bgDispatcher) {
runCatching {
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
?: throw ServiceError.MnemonicNotFound
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)

Logger.debug("Preparing sweep transaction...", context = TAG)

val preview = ServiceQueue.CORE.background {
prepareSweepTransaction(
mnemonicPhrase = mnemonic,
network = Env.network.toCoreNetwork(),
bip39Passphrase = passphrase,
electrumUrl = Env.electrumServerUrl,
destinationAddress = destinationAddress,
feeRateSatsPerVbyte = feeRateSatsPerVbyte,
)
}

preview.toSweepTransactionPreview()
}
}

suspend fun broadcastSweepTransaction(psbt: String): Result<SweepResult> = withContext(bgDispatcher) {
runCatching {
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
?: throw ServiceError.MnemonicNotFound
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)

Logger.debug("Broadcasting sweep transaction...", context = TAG)

val result = ServiceQueue.CORE.background {
broadcastSweepTransaction(
psbt = psbt,
mnemonicPhrase = mnemonic,
network = Env.network.toCoreNetwork(),
bip39Passphrase = passphrase,
electrumUrl = Env.electrumServerUrl,
)
}

result.toSweepResult()
}
}

suspend fun getFeeRates(): Result<FeeRates> = coreService.blocktank.getFees()

suspend fun hasSweepableFunds(): Result<Boolean> = checkSweepableBalances().map { balances ->
val hasFunds = balances.totalBalance > 0u
if (hasFunds) {
Logger.info("Found ${balances.totalBalance} sats to sweep", context = TAG)
} else {
Logger.debug("No sweepable funds found", context = TAG)
}
hasFunds
}

companion object {
private const val TAG = "SweepRepo"
}
}

private fun BitkitCoreSweepableBalances.toSweepableBalances() = SweepableBalances(
legacyBalance = legacyBalance,
legacyUtxosCount = legacyUtxosCount,
p2shBalance = p2shBalance,
p2shUtxosCount = p2shUtxosCount,
taprootBalance = taprootBalance,
taprootUtxosCount = taprootUtxosCount,
)

private fun BitkitCoreSweepTransactionPreview.toSweepTransactionPreview() = SweepTransactionPreview(
psbt = psbt,
estimatedFee = estimatedFee,
amountAfterFees = amountAfterFees,
estimatedVsize = estimatedVsize,
)

private fun BitkitCoreSweepResult.toSweepResult() = SweepResult(
txid = txid,
amountSwept = amountSwept,
)
65 changes: 64 additions & 1 deletion app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ import to.bitkit.ui.settings.advanced.AddressViewerScreen
import to.bitkit.ui.settings.advanced.CoinSelectPreferenceScreen
import to.bitkit.ui.settings.advanced.ElectrumConfigScreen
import to.bitkit.ui.settings.advanced.RgsServerScreen
import to.bitkit.ui.settings.advanced.sweep.SweepConfirmScreen
import to.bitkit.ui.settings.advanced.sweep.SweepFeeCustomScreen
import to.bitkit.ui.settings.advanced.sweep.SweepFeeRateScreen
import to.bitkit.ui.settings.advanced.sweep.SweepSettingsScreen
import to.bitkit.ui.settings.advanced.sweep.SweepSuccessScreen
import to.bitkit.ui.settings.appStatus.AppStatusScreen
import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroScreen
import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsSettings
Expand Down Expand Up @@ -169,6 +174,7 @@ import to.bitkit.ui.sheets.LnurlAuthSheet
import to.bitkit.ui.sheets.PinSheet
import to.bitkit.ui.sheets.QuickPayIntroSheet
import to.bitkit.ui.sheets.SendSheet
import to.bitkit.ui.sheets.SweepPromptSheet
import to.bitkit.ui.sheets.UpdateSheet
import to.bitkit.ui.theme.TRANSITION_SHEET_MS
import to.bitkit.ui.utils.AutoReadClipboardHandler
Expand All @@ -185,6 +191,7 @@ import to.bitkit.viewmodels.CurrencyViewModel
import to.bitkit.viewmodels.MainScreenEffect
import to.bitkit.viewmodels.RestoreState
import to.bitkit.viewmodels.SettingsViewModel
import to.bitkit.viewmodels.SweepViewModel
import to.bitkit.viewmodels.TransferViewModel
import to.bitkit.viewmodels.WalletViewModel

Expand Down Expand Up @@ -333,7 +340,10 @@ fun ContentView(
return
} else if (restoreState is RestoreState.Completed) {
WalletRestoreSuccessView(
onContinue = { walletViewModel.onRestoreContinue() },
onContinue = {
walletViewModel.onRestoreContinue()
appViewModel.checkForSweepableFunds()
},
)
return
}
Expand Down Expand Up @@ -397,6 +407,13 @@ fun ContentView(
is Sheet.Backup -> BackupSheet(sheet, onDismiss = { appViewModel.hideSheet() })
is Sheet.LnurlAuth -> LnurlAuthSheet(sheet, appViewModel)
Sheet.ForceTransfer -> ForceTransferSheet(appViewModel, transferViewModel)
Sheet.SweepPrompt -> SweepPromptSheet(
onSweep = {
appViewModel.hideSheet()
navController.navigate(Routes.SweepNav)
},
onCancel = { appViewModel.hideSheet() },
)
is Sheet.Gift -> GiftSheet(sheet, appViewModel)
is Sheet.TimedSheet -> {
when (sheet.type) {
Expand Down Expand Up @@ -996,6 +1013,34 @@ private fun NavGraphBuilder.advancedSettings(navController: NavHostController) {
composableWithDefaultTransitions<Routes.AddressViewer> {
AddressViewerScreen(navController)
}
navigationWithDefaultTransitions<Routes.SweepNav>(
startDestination = Routes.Sweep,
) {
composableWithDefaultTransitions<Routes.Sweep> {
val parentEntry = remember(it) { navController.getBackStackEntry(Routes.SweepNav) }
val viewModel = hiltViewModel<SweepViewModel>(parentEntry)
SweepSettingsScreen(navController, viewModel)
}
composableWithDefaultTransitions<Routes.SweepConfirm> {
val parentEntry = remember(it) { navController.getBackStackEntry(Routes.SweepNav) }
val viewModel = hiltViewModel<SweepViewModel>(parentEntry)
SweepConfirmScreen(navController, viewModel)
}
composableWithDefaultTransitions<Routes.SweepFeeRate> {
val parentEntry = remember(it) { navController.getBackStackEntry(Routes.SweepNav) }
val viewModel = hiltViewModel<SweepViewModel>(parentEntry)
SweepFeeRateScreen(navController, viewModel)
}
composableWithDefaultTransitions<Routes.SweepFeeCustom> {
val parentEntry = remember(it) { navController.getBackStackEntry(Routes.SweepNav) }
val viewModel = hiltViewModel<SweepViewModel>(parentEntry)
SweepFeeCustomScreen(navController, viewModel)
}
composableWithDefaultTransitions<Routes.SweepSuccess> {
val route = it.toRoute<Routes.SweepSuccess>()
SweepSuccessScreen(navController, amountSats = route.amountSats)
}
}
composableWithDefaultTransitions<Routes.NodeInfo> {
NodeInfoScreen(navController)
}
Expand Down Expand Up @@ -1689,6 +1734,24 @@ sealed interface Routes {
@Serializable
data object AddressViewer : Routes

@Serializable
data object SweepNav : Routes

@Serializable
data object Sweep : Routes

@Serializable
data object SweepConfirm : Routes

@Serializable
data object SweepFeeRate : Routes

@Serializable
data object SweepFeeCustom : Routes

@Serializable
data class SweepSuccess(val amountSats: Long) : Routes

@Serializable
data object AboutSettings : Routes

Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/ui/components/SheetHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ sealed interface Sheet {
data class LnurlAuth(val domain: String, val lnurl: String, val k1: String) : Sheet
data object ForceTransfer : Sheet
data class Gift(val code: String, val amount: ULong) : Sheet
data object SweepPrompt : Sheet

data class TimedSheet(val type: TimedSheetType) : Sheet
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -54,7 +55,9 @@ fun InitializingWalletView(
isRestoring: Boolean = false,
) {
BoxWithConstraints(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.keepScreenOn(),
contentAlignment = Alignment.Center,
) {
val percentage = remember { Animatable(0f) }
Expand Down
Loading
Loading