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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ android {
buildConfigField("boolean", "TREZOR_BRIDGE", trezorBridgeEnv)
buildConfigField("String", "TREZOR_BRIDGE_URL", "\"$trezorBridgeUrlEnv\"")
buildConfigField("boolean", "GEO", System.getenv("GEO")?.toBoolean()?.toString() ?: "true")
buildConfigField("boolean", "FEATURE_PAYKIT_UI_DISABLED", System.getenv("PAYKIT_UI_DISABLED")?.toBoolean()?.toString() ?: "false")
buildConfigField("String", "LOCALES", "\"${bcp47Locales.joinToString(",")}\"")
}

Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ package to.bitkit.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
import to.bitkit.data.serializers.SettingsSerializer
import to.bitkit.env.Env
Expand All @@ -24,13 +29,17 @@ private val Context.settingsDataStore: DataStore<SettingsData> by dataStore(
serializer = SettingsSerializer,
)

private val Context.localSettingsDataStore: DataStore<Preferences> by preferencesDataStore("local_settings")

@Singleton
class SettingsStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val store = context.settingsDataStore
private val localStore = context.localSettingsDataStore

val data: Flow<SettingsData> = store.data
val isPaykitEnabled: Flow<Boolean> = localStore.data.map { it[PAYKIT_ENABLED_KEY] ?: false }

@Volatile
var restoredMonitoredTypesFromBackup: Boolean = false
Expand All @@ -53,6 +62,10 @@ class SettingsStore @Inject constructor(
store.updateData(transform)
}

suspend fun setIsPaykitEnabled(value: Boolean) {
localStore.edit { it[PAYKIT_ENABLED_KEY] = value }
}

suspend fun addLastUsedTag(newTag: String) {
store.updateData { currentSettings ->
val combinedTags = (listOf(newTag) + currentSettings.lastUsedTags).distinct()
Expand All @@ -76,13 +89,15 @@ class SettingsStore @Inject constructor(

suspend fun reset() {
store.updateData { SettingsData() }
localStore.edit { it.clear() }
restoredMonitoredTypesFromBackup = false
Logger.info("Deleted all user settings data.")
}

companion object {
private const val TAG = "SettingsStore"
private const val MAX_LAST_USED_TAGS = 10
private val PAYKIT_ENABLED_KEY = booleanPreferencesKey("paykit_enabled")
}
}

Expand All @@ -103,6 +118,7 @@ data class SettingsData(
val hasConfirmedPublicPaykitEndpoints: Boolean = false,
val sharesPublicPaykitEndpoints: Boolean = false,
val sharesPrivatePaykitEndpoints: Boolean = false,
val publicPaykitCleanupPending: Boolean = false,
val publicPaykitLightningEnabled: Boolean = true,
val publicPaykitOnchainEnabled: Boolean = true,
val publicPaykitBolt11: String = "",
Expand Down Expand Up @@ -148,3 +164,25 @@ fun SettingsData.resetPin() = this.copy(
isPinForPaymentsEnabled = false,
isBiometricEnabled = false,
)

fun SettingsData.hasPublicPaykitPublicationState(): Boolean =
hasConfirmedPublicPaykitEndpoints ||
sharesPublicPaykitEndpoints ||
publicPaykitCleanupPending ||
publicPaykitBolt11.isNotBlank() ||
publicPaykitBolt11PaymentHash.isNotBlank() ||
publicPaykitBolt11ExpiresAtMillis > 0L

fun SettingsData.hasPaykitState(): Boolean =
hasPublicPaykitPublicationState() ||
sharesPrivatePaykitEndpoints

fun SettingsData.paykitDisabled(markPublicCleanupPending: Boolean = false) = copy(
hasConfirmedPublicPaykitEndpoints = false,
sharesPublicPaykitEndpoints = false,
sharesPrivatePaykitEndpoints = false,
publicPaykitCleanupPending = publicPaykitCleanupPending || markPublicCleanupPending,
publicPaykitBolt11 = "",
publicPaykitBolt11PaymentHash = "",
publicPaykitBolt11ExpiresAtMillis = 0,
)
11 changes: 11 additions & 0 deletions app/src/main/java/to/bitkit/flags/PaykitFeatureFlags.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package to.bitkit.flags

import to.bitkit.BuildConfig

object PaykitFeatureFlags {
Comment thread
ben-kaufman marked this conversation as resolved.
const val isUiAvailable = !BuildConfig.FEATURE_PAYKIT_UI_DISABLED

fun isUiEnabled(localFlagEnabled: Boolean): Boolean {
return isUiAvailable && localFlagEnabled
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/to/bitkit/models/AddressType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ fun AddressType.toDerivationPath(
}
}

fun AddressType.toAccountDerivationPath(network: Network = Env.network): String {
val coinType = if (network == Network.BITCOIN) 0 else 1

return when (this) {
AddressType.P2TR -> "m/86'/$coinType'/0'"
AddressType.P2WPKH -> "m/84'/$coinType'/0'"
AddressType.P2SH -> "m/49'/$coinType'/0'"
AddressType.P2PKH -> "m/44'/$coinType'/0'"
else -> ""
}
}

fun AddressType.toSettingsString(): String = when (this) {
AddressType.P2TR -> "taproot"
AddressType.P2WPKH -> "nativeSegwit"
Expand Down
21 changes: 6 additions & 15 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ class BackupRepo @Inject constructor(
}
performRestore(BackupCategory.WALLET) { dataBytes ->
restoreWalletBackup(dataBytes)
}
}.getOrThrow()
performRestore(BackupCategory.BLOCKTANK) { dataBytes ->
val parsed = json.decodeFromString<BlocktankBackupV1>(String(dataBytes))
blocktankRepo.restoreFromBackup(parsed)
Expand Down Expand Up @@ -621,20 +621,11 @@ class BackupRepo @Inject constructor(
if (!parsed.privatePaykitHighestReservedReceiveIndexByAddressType.isNullOrEmpty()) {
cacheStore.update { it.copy(onchainAddress = "", bip21 = "") }
}
privatePaykitAddressReservationRepo.get()
.restoreBackup(parsed.privatePaykitHighestReservedReceiveIndexByAddressType)
.onFailure {
Logger.warn("Failed to restore private Paykit reservations", it, context = TAG)
}
privatePaykitRepo.get().restoreBackup(parsed.privatePaykitContactLinks)
.onFailure {
Logger.warn("Failed to restore private Paykit contact links", it, context = TAG)
}
privatePaykitAddressReservationRepo.get()
.reconcileReservedIndexesWithLdk()
.onFailure {
Logger.warn("Failed to reconcile restored private Paykit reservations", it, context = TAG)
}
val addressReservationRepo = privatePaykitAddressReservationRepo.get()
addressReservationRepo.restoreBackup(parsed.privatePaykitHighestReservedReceiveIndexByAddressType).getOrThrow()
val privateRepo = privatePaykitRepo.get()
privateRepo.restoreBackup(parsed.privatePaykitContactLinks).getOrThrow()
addressReservationRepo.reconcileReservedIndexesWithLdk().getOrThrow()
Logger.debug("Restored ${parsed.transfers.size} transfers", context = TAG)
return parsed.createdAt
}
Expand Down
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
Expand All @@ -30,6 +31,7 @@ import kotlinx.coroutines.withContext
import to.bitkit.data.PrivatePaykitCacheStore
import to.bitkit.data.PubkyStore
import to.bitkit.data.SettingsStore
import to.bitkit.data.hasPublicPaykitPublicationState
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.IoDispatcher
import to.bitkit.env.Env
Expand Down Expand Up @@ -983,7 +985,8 @@ class PubkyRepo @Inject constructor(
// region Sign out

suspend fun signOut(): Result<Unit> {
removeBitkitPaymentEndpoints()
val hadPublicPaykitState = settingsStore.data.first().hasPublicPaykitPublicationState()
val endpointCleanupResult = removeBitkitPaymentEndpoints()
.onFailure { Logger.warn("Failed to remove Bitkit payment endpoints", it, context = TAG) }

val result = runCatching {
Expand All @@ -993,7 +996,7 @@ class PubkyRepo @Inject constructor(
withContext(ioDispatcher) { pubkyService.forceSignOut() }
}

clearLocalState()
clearLocalState(publicPaykitCleanupPending = endpointCleanupResult.isFailure && hadPublicPaykitState)
return result
}

Expand Down Expand Up @@ -1074,23 +1077,24 @@ class PubkyRepo @Inject constructor(
_contactsLoadVersion.update { it + 1 }
}

private suspend fun clearLocalState() = withContext(ioDispatcher) {
private suspend fun clearLocalState(publicPaykitCleanupPending: Boolean = false) = withContext(ioDispatcher) {
runCatching { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) }
runCatching { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) }
runCatching { clearPublicPaykitSharingState() }
runCatching { clearPublicPaykitSharingState(publicPaykitCleanupPending) }
.onFailure { Logger.warn("Failed to clear public Paykit sharing state", it, context = TAG) }
notifyBackupStateChanged()
clearAuthenticatedState()
}

private suspend fun clearPublicPaykitSharingState() {
private suspend fun clearPublicPaykitSharingState(publicPaykitCleanupPending: Boolean) {
settingsStore.update {
it.copy(
hasConfirmedPublicPaykitEndpoints = false,
sharesPublicPaykitEndpoints = false,
publicPaykitBolt11 = "",
publicPaykitBolt11PaymentHash = "",
publicPaykitBolt11ExpiresAtMillis = 0,
publicPaykitCleanupPending = publicPaykitCleanupPending,
)
}
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,13 @@ class PublicPaykitRepo @Inject constructor(
runCatching {
if (!publish) {
removePublishedEndpoints()
settingsStore.update { it.copy(publicPaykitCleanupPending = false) }
return@runCatching
}

val desired = buildWalletEndpoints(refresh = true)
applyPublishedEndpoints(desired)
settingsStore.update { it.copy(publicPaykitCleanupPending = false) }
}
}

Expand All @@ -198,6 +200,7 @@ class PublicPaykitRepo @Inject constructor(
requireEndpoint = requireEndpoint,
)
applyPublishedEndpoints(desired)
settingsStore.update { it.copy(publicPaykitCleanupPending = false) }
}
}

Expand Down
40 changes: 39 additions & 1 deletion app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import to.bitkit.data.CacheStore
import to.bitkit.data.SettingsStore
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.BgDispatcher
import to.bitkit.env.Env
import to.bitkit.ext.filterOpen
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.toHex
Expand All @@ -33,12 +34,15 @@ import to.bitkit.models.AddressModel
import to.bitkit.models.BalanceState
import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING
import to.bitkit.models.msatFloorOf
import to.bitkit.models.toAccountDerivationPath
import to.bitkit.models.toDerivationPath
import to.bitkit.services.AddressDerivationInfo
import to.bitkit.services.CoreService
import to.bitkit.usecases.DeriveBalanceStateUseCase
import to.bitkit.usecases.WipeWalletUseCase
import to.bitkit.utils.Bip21Utils
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import to.bitkit.utils.measured
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -401,7 +405,14 @@ class WalletRepo @Inject constructor(
isChange = isChange,
startIndex = startIndex,
count = count,
).getOrThrow()
).getOrElse {
deriveAddressInfosFromMnemonic(
addressType = addressType,
isChange = isChange,
startIndex = startIndex,
count = count,
)
}

val addresses = result.map { address ->
AddressModel(
Expand All @@ -417,6 +428,33 @@ class WalletRepo @Inject constructor(
}
}

private suspend fun deriveAddressInfosFromMnemonic(
addressType: AddressType,
isChange: Boolean,
startIndex: Int,
count: Int,
): List<AddressDerivationInfo> {
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
?: throw ServiceError.MnemonicNotFound()
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
val baseDerivationPath = addressType.toAccountDerivationPath()

return coreService.onchain.deriveBitcoinAddresses(
mnemonicPhrase = mnemonic,
derivationPathStr = baseDerivationPath,
network = Env.network,
bip39Passphrase = passphrase,
isChange = isChange,
startIndex = startIndex.toUInt(),
count = count.toUInt(),
).addresses.mapIndexed { offset, address ->
AddressDerivationInfo(
address = address.address,
index = startIndex + offset,
)
}
}

fun getBolt11(): String = _walletState.value.bolt11

suspend fun setBolt11(bolt11: String) {
Expand Down
Loading
Loading