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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
local.properties
# AI
.ai
.codex/
.claude/worktrees
*.local.*
!*.local.template*
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/data/CacheStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ data class AppCacheData(
val deletedActivities: List<String> = listOf(),
val pendingBoostActivities: List<PendingBoostActivity> = listOf(),
val backgroundReceive: NewTransactionSheetDetails? = null,
val addressSearchLastUsedReceiveIndexes: Map<String, Int> = mapOf(),
val addressSearchLastUsedChangeIndexes: Map<String, Int> = mapOf(),
) {
fun resetBip21() = copy(bip21 = "", bolt11 = "", onchainAddress = "")
}
148 changes: 148 additions & 0 deletions app/src/main/java/to/bitkit/data/PrivatePaykitStores.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package to.bitkit.data

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import to.bitkit.di.json
import to.bitkit.utils.Logger
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
import javax.inject.Singleton

private val Context.privatePaykitCacheDataStore: DataStore<PrivatePaykitCacheData> by dataStore(
fileName = "private_paykit_cache.json",
serializer = PrivatePaykitCacheSerializer,
)

private val Context.privatePaykitReservationDataStore: DataStore<PrivatePaykitReservationData> by dataStore(
fileName = "private_paykit_reservations.json",
serializer = PrivatePaykitReservationSerializer,
)

@Singleton
class PrivatePaykitCacheStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val store = context.privatePaykitCacheDataStore

val data: Flow<PrivatePaykitCacheData> = store.data

suspend fun update(transform: (PrivatePaykitCacheData) -> PrivatePaykitCacheData) {
store.updateData(transform)
}

suspend fun reset() {
store.updateData { PrivatePaykitCacheData() }
}
}

@Singleton
class PrivatePaykitReservationStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val store = context.privatePaykitReservationDataStore

val data: Flow<PrivatePaykitReservationData> = store.data

suspend fun update(transform: (PrivatePaykitReservationData) -> PrivatePaykitReservationData) {
store.updateData(transform)
}

suspend fun reset() {
store.updateData { PrivatePaykitReservationData() }
}
}

@Serializable
data class PrivatePaykitCacheData(
val contacts: Map<String, PrivatePaykitContactCacheData> = emptyMap(),
val cleanupPending: Boolean = false,
val deletedContactCleanupPendingPublicKeys: Set<String> = emptySet(),
val profileRecoveryPending: Boolean = false,
)

@Serializable
data class PrivatePaykitContactCacheData(
val remoteEndpoints: List<PrivatePaykitStoredPaymentEntryData> = emptyList(),
val localInvoice: PrivatePaykitStoredInvoiceData? = null,
val receivedInvoicePaymentHashes: List<String> = emptyList(),
val lastLocalPayloadHash: String? = null,
val linkCompletedAt: Long? = null,
val handshakeUpdatedAt: Long? = null,
val recoveryStartedAt: Long? = null,
val mainRecoveryAttemptId: String? = null,
val responderRecoveryAttemptId: String? = null,
val lastCompletedRecoveryAttemptId: String? = null,
val awaitingRecoveredRemoteEndpoints: Boolean = false,
val linkFailureCount: Int = 0,
)

@Serializable
data class PrivatePaykitStoredPaymentEntryData(
val methodId: String,
val endpointData: String,
)

@Serializable
data class PrivatePaykitStoredInvoiceData(
val bolt11: String,
val paymentHash: String,
val expiresAt: Long,
)

@Serializable
data class PrivatePaykitReservationData(
val version: Int = 1,
val reservedReceiveIndexesByAddressType: Map<String, Set<Int>> = emptyMap(),
val contactAssignments: Map<String, PrivatePaykitStoredAssignmentData> = emptyMap(),
val contactAssignmentHistory: Map<String, List<PrivatePaykitStoredAssignmentData>> = emptyMap(),
val restoredReservedReceiveIndexCeilingsByAddressType: Map<String, Int> = emptyMap(),
)

@Serializable
data class PrivatePaykitStoredAssignmentData(
val addressType: String,
val receiveIndex: Int,
val address: String = "",
)

private object PrivatePaykitCacheSerializer : Serializer<PrivatePaykitCacheData> {
private const val TAG = "PrivatePaykitCacheSerializer"

override val defaultValue: PrivatePaykitCacheData = PrivatePaykitCacheData()

override suspend fun readFrom(input: InputStream): PrivatePaykitCacheData =
runCatching {
json.decodeFromString<PrivatePaykitCacheData>(input.readBytes().decodeToString())
}.getOrElse {
Logger.error("Failed to deserialize PrivatePaykitCacheData", it, context = TAG)
defaultValue
}

override suspend fun writeTo(t: PrivatePaykitCacheData, output: OutputStream) {
output.write(json.encodeToString(t).encodeToByteArray())
}
}

private object PrivatePaykitReservationSerializer : Serializer<PrivatePaykitReservationData> {
private const val TAG = "PrivatePaykitReservationSerializer"

override val defaultValue: PrivatePaykitReservationData = PrivatePaykitReservationData()

override suspend fun readFrom(input: InputStream): PrivatePaykitReservationData =
runCatching {
json.decodeFromString<PrivatePaykitReservationData>(input.readBytes().decodeToString())
}.getOrElse {
Logger.error("Failed to deserialize PrivatePaykitReservationData", it, context = TAG)
defaultValue
}

override suspend fun writeTo(t: PrivatePaykitReservationData, output: OutputStream) {
output.write(json.encodeToString(t).encodeToByteArray())
}
}
3 changes: 3 additions & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ data class SettingsData(
val hasSeenContactsIntro: Boolean = false,
val hasConfirmedPublicPaykitEndpoints: Boolean = false,
val sharesPublicPaykitEndpoints: Boolean = false,
val sharesPrivatePaykitEndpoints: Boolean = false,
val publicPaykitLightningEnabled: Boolean = true,
val publicPaykitOnchainEnabled: Boolean = true,
val publicPaykitBolt11: String = "",
val publicPaykitBolt11PaymentHash: String = "",
val publicPaykitBolt11ExpiresAtMillis: Long = 0,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/data/keychain/Keychain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class Keychain @Inject constructor(
PIN,
PIN_ATTEMPTS_REMAINING,
PAYKIT_SESSION,
PRIVATE_PAYKIT_SECRET_STATE,
PUBKY_SECRET_KEY,
}
}
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/to/bitkit/models/BackupPayloads.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ data class WalletBackupV1(
val version: Int = 1,
val createdAt: Long,
val transfers: List<TransferEntity>,
val privatePaykitHighestReservedReceiveIndexByAddressType: Map<String, Int>? = null,
val privatePaykitContactLinks: Map<String, PrivatePaykitContactLinkBackupV1>? = null,
)

@Serializable
data class PrivatePaykitContactLinkBackupV1(
val publicKey: String,
val linkSnapshotHex: String? = null,
val handshakeSnapshotHex: String? = null,
val remoteEndpoints: Map<String, String> = emptyMap(),
val linkCompletedAt: Long? = null,
val handshakeUpdatedAt: Long? = null,
val recoveryStartedAt: Long? = null,
val mainRecoveryAttemptId: String? = null,
val responderRecoveryAttemptId: String? = null,
val awaitingRecoveredRemoteEndpoints: Boolean = false,
)

@Serializable
Expand Down
34 changes: 32 additions & 2 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,39 @@ class ActivityRepo @Inject constructor(
}
}

suspend fun clearContact(
forPaymentId: String,
syncLdkPayments: Boolean = true,
): Result<Unit> = withContext(ioDispatcher) {
runCatching {
if (syncLdkPayments) {
lightningRepo.getPayments().onSuccess {
syncLdkNodePayments(it).getOrThrow()
}.getOrThrow()
}

val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments)
if (activity == null) {
Logger.warn(
"Skipped clearing contact for payment '$forPaymentId' because activity was not found",
context = TAG,
)
return@runCatching
}
if (activity.contact() == null) return@runCatching

val updatedAt = nowTimestamp().epochSecond.toULong()
val updatedActivity = activity.withContact(null, updatedAt)
updateActivity(updatedActivity.rawId(), updatedActivity).getOrThrow()
updateReplacementContactIfNeeded(updatedActivity, null, updatedAt)
}.onFailure {
Logger.error("Failed to clear contact for payment '$forPaymentId'", it, context = TAG)
}
}

private suspend fun updateReplacementContactIfNeeded(
activity: Activity,
normalizedKey: String,
normalizedKey: String?,
updatedAt: ULong,
) {
if (activity !is Activity.Onchain || activity.v1.doesExist || activity.v1.txType != PaymentType.SENT) return
Expand Down Expand Up @@ -422,7 +452,7 @@ class ActivityRepo @Inject constructor(
coreService.activity.getActivity(forPaymentId)
?: getOnchainActivityByTxId(forPaymentId)?.let { Activity.Onchain(it) }

private fun Activity.withContact(normalizedKey: String, updatedAt: ULong): Activity = when (this) {
private fun Activity.withContact(normalizedKey: String?, updatedAt: ULong): Activity = when (this) {
is Activity.Lightning -> Activity.Lightning(v1.copy(contact = normalizedKey, updatedAt = updatedAt))
is Activity.Onchain -> Activity.Onchain(v1.copy(contact = normalizedKey, updatedAt = updatedAt))
}
Expand Down
Loading
Loading