Skip to content
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/ext/Activities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.synonym.bitkitcore.LightningActivity
import com.synonym.bitkitcore.OnchainActivity
import com.synonym.bitkitcore.PaymentState
import com.synonym.bitkitcore.PaymentType
import to.bitkit.models.WalletScope

fun Activity.rawId(): String = when (this) {
is Activity.Lightning -> v1.id
Expand Down Expand Up @@ -94,6 +95,7 @@ enum class BoostType { RBF, CPFP }

@Suppress("LongParameterList")
fun LightningActivity.Companion.create(
walletId: String = WalletScope.default,
id: String,
txType: PaymentType,
status: PaymentState,
Expand All @@ -108,6 +110,7 @@ fun LightningActivity.Companion.create(
updatedAt: ULong? = createdAt,
seenAt: ULong? = null,
) = LightningActivity(
walletId = walletId,
id = id,
txType = txType,
status = status,
Expand All @@ -125,6 +128,7 @@ fun LightningActivity.Companion.create(

@Suppress("LongParameterList")
fun OnchainActivity.Companion.create(
walletId: String = WalletScope.default,
id: String,
txType: PaymentType,
txId: String,
Expand All @@ -146,6 +150,7 @@ fun OnchainActivity.Companion.create(
updatedAt: ULong? = createdAt,
seenAt: ULong? = null,
) = OnchainActivity(
walletId = walletId,
id = id,
txType = txType,
txId = txId,
Expand Down
45 changes: 45 additions & 0 deletions app/src/main/java/to/bitkit/ext/BackupRestoreCompat.kt

@jvsena42 jvsena42 Jul 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This compatibility layer should be handled by #1046 , gated by synonymdev/bitkit-core#113

I recommend drop this file and wait for Bitkit-core implementation

Summarizing, we need to implement wallet-scope backup migrations because of the new walletId. The logic must be in Bitkit-core for single source of true

@piotr-iohk piotr-iohk Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree long-term: wallet-scoped backup migration belongs in Core (#113) and the app integration in #1046.

BackupRestoreCompat is a minimal bridge added after Codex flagged legacy VSS restore failing on missing walletId once we bumped to 0.3.9 (P1 on this PR). It keeps existing hot-wallet cloud backup restore working until Core exposes migration APIs — it does not add Trezor/HW tag product scope for 2.4.0.

Happy to drop it as soon as synonymdev/bitkit-core#113 lands and wire the proper flow in #1046.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might also be patch-able by making the serializer more lenient on Kotlin's Json instance, maybe a derivation of our global di.json instance (if that wasn't already tried and deemed not applicable here); or perhaps by playing with an optional walletId on a domain replica of the original serialized model originating from core, which adds optional fields, given that the json instance is already configured with isLenient = true

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package to.bitkit.ext

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import to.bitkit.di.json
import to.bitkit.models.ActivityBackupV1
import to.bitkit.models.MetadataBackupV1
import to.bitkit.models.WalletScope

fun String.decodeActivityBackupV1Compat(): ActivityBackupV1 =
json.decodeFromString(normalizeLegacyWalletIdsInBackupJson(this))

fun String.decodeMetadataBackupV1Compat(): MetadataBackupV1 =
json.decodeFromString(normalizeLegacyWalletIdsInBackupJson(this))

private fun normalizeLegacyWalletIdsInBackupJson(
raw: String,
walletId: String = WalletScope.default,
): String {
val normalized = json.parseToJsonElement(raw).normalizeLegacyWalletIds(walletId)
return json.encodeToString(normalized)
}

private fun JsonElement.normalizeLegacyWalletIds(walletId: String): JsonElement = when (this) {
is JsonObject -> {
val patched = if (needsLegacyWalletId() && "walletId" !in this) {
this + ("walletId" to JsonPrimitive(walletId))
} else {
this
}
JsonObject(patched.mapValues { (_, value) -> value.normalizeLegacyWalletIds(walletId) })
}
is JsonArray -> JsonArray(map { it.normalizeLegacyWalletIds(walletId) })
else -> this
}

private fun JsonObject.needsLegacyWalletId(): Boolean = when {
"paymentId" in this && "tags" in this && "isReceive" in this -> true
"activityId" in this && "tags" in this && "paymentId" !in this -> true
"txId" in this && "txType" in this && "value" in this -> true
"invoice" in this && "status" in this && "txType" in this -> true
else -> false
}
17 changes: 17 additions & 0 deletions app/src/main/java/to/bitkit/ext/TrezorExceptionExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package to.bitkit.ext

import com.synonym.bitkitcore.TrezorException

fun Throwable.isTrezorUserCancellation(): Boolean {
var current: Throwable? = this
while (current != null) {
when (current) {
is TrezorException.UserCancelled,
is TrezorException.PinCancelled,
is TrezorException.PassphraseCancelled,
-> return true
}
current = current.cause
}
return false
}
10 changes: 10 additions & 0 deletions app/src/main/java/to/bitkit/models/HwWalletId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package to.bitkit.models

import com.synonym.bitkitcore.deriveWalletId

object HwWalletId {
fun derive(xpubs: Map<String, String>, deviceType: String = "trezor"): String {
require(xpubs.isNotEmpty()) { "xpubs must not be empty" }
return deriveWalletId(deviceType = deviceType, xpubs = xpubs.values.toList())
}
}
14 changes: 14 additions & 0 deletions app/src/main/java/to/bitkit/models/WalletScope.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package to.bitkit.models

import androidx.annotation.VisibleForTesting
import com.synonym.bitkitcore.getDefaultWalletId

object WalletScope {
@VisibleForTesting
internal var testOverride: String? = null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A missed tearDown or parallel run leaks a wallet id across test


val default: String
get() = testOverride ?: lazyDefault

private val lazyDefault: String by lazy { getDefaultWalletId() }
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
3 changes: 2 additions & 1 deletion app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import to.bitkit.di.BgDispatcher
import to.bitkit.di.IoDispatcher
import to.bitkit.ext.amountOnClose
import to.bitkit.ext.contact
import to.bitkit.ext.create
import to.bitkit.ext.isReplacedSentTransaction
import to.bitkit.ext.matchesPaymentId
import to.bitkit.ext.nowMillis
Expand Down Expand Up @@ -653,7 +654,7 @@ class ActivityRepo @Inject constructor(
val now = nowTimestamp().epochSecond.toULong()
insertActivity(
Activity.Lightning(
LightningActivity(
LightningActivity.create(
id = id,
txType = PaymentType.RECEIVED,
status = PaymentState.SUCCEEDED,
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend also revert the changes from this file, same reason https://github.com/synonymdev/bitkit-android/pull/1062/changes#r3535801605

Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import to.bitkit.data.backup.VssBackupClientLdk
import to.bitkit.data.resetPin
import to.bitkit.di.IoDispatcher
import to.bitkit.di.json
import to.bitkit.ext.decodeActivityBackupV1Compat
import to.bitkit.ext.decodeMetadataBackupV1Compat
import to.bitkit.ext.formatPlural
import to.bitkit.ext.nowMillis
import to.bitkit.models.ActivityBackupV1
Expand Down Expand Up @@ -581,7 +583,7 @@ class BackupRepo @Inject constructor(

val result = runCatching {
performRestore(BackupCategory.METADATA) { dataBytes ->
val parsed = json.decodeFromString<MetadataBackupV1>(String(dataBytes))
val parsed = String(dataBytes).decodeMetadataBackupV1Compat()
val cleanCache = parsed.cache.resetBip21() // Force address rotation
cacheStore.update { cleanCache }
Logger.debug("Restored caches: ${jsonLogOf(parsed.cache.copy(cachedRates = emptyList()))}", TAG)
Expand Down Expand Up @@ -613,7 +615,7 @@ class BackupRepo @Inject constructor(
parsed.createdAt
}
performRestore(BackupCategory.ACTIVITY) { dataBytes ->
val parsed = json.decodeFromString<ActivityBackupV1>(String(dataBytes))
val parsed = String(dataBytes).decodeActivityBackupV1Compat()
activityRepo.restoreFromBackup(parsed)
parsed.createdAt
}
Expand Down
Loading
Loading