Skip to content
Open
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
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/data/PrivatePaykitStores.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ data class PrivatePaykitCacheData(
@Serializable
data class PrivatePaykitContactCacheData(
val remoteEndpoints: List<PrivatePaykitStoredPaymentEntryData> = emptyList(),
val localInvoice: PrivatePaykitStoredInvoiceData? = null,
val localInvoicesByReceiverPath: Map<String, PrivatePaykitStoredInvoiceData> = emptyMap(),
val receivedInvoicePaymentHashes: List<String> = emptyList(),
val hasPublishedPrivatePaymentList: Boolean = false,
val publishedPrivatePaymentReceiverPaths: Set<String> = emptySet(),
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import to.bitkit.data.PrivatePaykitReservationStore
import to.bitkit.data.PrivatePaykitStoredAssignmentData
import to.bitkit.data.SettingsStore
import to.bitkit.di.IoDispatcher
import to.bitkit.ext.runSuspendCatching
import to.bitkit.models.DEFAULT_ADDRESS_TYPE
import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.models.addressTypeFromAddress
import to.bitkit.models.toAddressType
import to.bitkit.models.toSettingsString
import to.bitkit.services.CoreService
import to.bitkit.services.PaykitReceiverPaths
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
import javax.inject.Inject
Expand Down Expand Up @@ -80,18 +82,21 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
}
}

suspend fun currentOrRotatedAddress(publicKey: String): Result<String> = withContext(ioDispatcher) {
runCatching {
val normalizedKey = normalizedPublicKey(publicKey)
val current = locked { it.contactAssignments[normalizedKey] }
suspend fun currentOrRotatedAddress(
publicKey: String,
receiverPath: String,
): Result<String> = withContext(ioDispatcher) {
runSuspendCatching {
val assignmentKey = contactAssignmentKey(publicKey, receiverPath)
val current = locked { it.contactAssignments[assignmentKey] }
if (current != null && isAddressTypeMonitored(current.addressType)) {
val address = resolvedAddress(current).getOrThrow()
if (!isReservedAddressUsed(address)) return@runCatching address
if (!isReservedAddressUsed(address)) return@runSuspendCatching address
} else if (current != null) {
clearCurrentAssignment(normalizedKey)
clearCurrentAssignment(assignmentKey)
}

allocateAddress(normalizedKey).getOrThrow()
allocateAddress(assignmentKey).getOrThrow()
}.onFailure {
Logger.warn(
"Failed to get private Paykit address for '${redacted(publicKey)}'",
Expand All @@ -101,18 +106,6 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
}
}

suspend fun rotateAddress(publicKey: String): Result<String> = withContext(ioDispatcher) {
runCatching {
allocateAddress(normalizedPublicKey(publicKey)).getOrThrow()
}.onFailure {
Logger.warn(
"Failed to rotate private Paykit address for '${redacted(publicKey)}'",
it,
context = TAG,
)
}
}

suspend fun nextReusableReceiveAddress(): Result<String> =
nextReusableReceiveAddress(selectedAddressType())

Expand Down Expand Up @@ -163,7 +156,7 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
assignments.firstOrNull { (_, assignment) ->
assignment.addressType == addressType &&
(assignment.address == address || resolvedAddress(assignment).getOrNull() == address)
}?.first
}?.first?.publicKeyFromAssignmentKey()
}

suspend fun currentContactPublicKeyForReservedAddress(address: String): String? = withContext(ioDispatcher) {
Expand All @@ -173,24 +166,25 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
assignments.firstOrNull { (_, assignment) ->
assignment.addressType == addressType &&
(assignment.address == address || resolvedAddress(assignment).getOrNull() == address)
}?.first
}?.first?.publicKeyFromAssignmentKey()
}

suspend fun contactsWithUsedReservedAddresses(): List<String> = withContext(ioDispatcher) {
val assignments = locked { it.contactAssignments.map { entry -> entry.key to entry.value } }
assignments.mapNotNull { (publicKey, assignment) ->
val address = resolvedAddress(assignment).getOrNull() ?: return@mapNotNull null
val isUsed = runCatching { isReservedAddressUsed(address) }
val isUsed = runSuspendCatching { isReservedAddressUsed(address) }
.onFailure {
Logger.warn(
"Failed to check private Paykit address usage for '${redacted(publicKey)}'",
"Failed to check private Paykit address usage for " +
"'${redacted(publicKey.publicKeyFromAssignmentKey())}'",
it,
context = TAG,
)
}
.getOrDefault(false)
publicKey.takeIf { isUsed }
}
publicKey.publicKeyFromAssignmentKey().takeIf { isUsed }
}.distinct()
}

private suspend fun isReservedAddressUsed(address: String): Boolean {
Expand All @@ -199,20 +193,23 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
return lightningRepo.getAddressBalance(address).getOrDefault(0u) > 0u
}

suspend fun hasContactAssignment(publicKey: String): Boolean = withContext(ioDispatcher) {
val normalizedKey = normalizedPublicKey(publicKey)
locked { it.contactAssignments.containsKey(normalizedKey) }
}

suspend fun clearContactAssignment(publicKey: String) = withContext(ioDispatcher) {
val normalizedKey = normalizedPublicKey(publicKey)
locked { current ->
val hadAssignment = normalizedKey in current.contactAssignments
val hadHistory = normalizedKey in current.contactAssignmentHistory
val hadAssignment = current.contactAssignments.keys.any {
it.publicKeyFromAssignmentKey() == normalizedKey
}
val hadHistory = current.contactAssignmentHistory.keys.any {
it.publicKeyFromAssignmentKey() == normalizedKey
}
if (!hadAssignment && !hadHistory) return@locked
val next = current.copy(
contactAssignments = current.contactAssignments - normalizedKey,
contactAssignmentHistory = current.contactAssignmentHistory - normalizedKey,
contactAssignments = current.contactAssignments.filterKeys {
it.publicKeyFromAssignmentKey() != normalizedKey
},
contactAssignmentHistory = current.contactAssignmentHistory.filterKeys {
it.publicKeyFromAssignmentKey() != normalizedKey
},
)
ledger = next
persist(next)
Expand All @@ -224,8 +221,12 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
val savedKeys = excludingPublicKeys.mapNotNull { normalizedPublicKeyOrNull(it) }.toSet()
locked { current ->
val next = current.copy(
contactAssignments = current.contactAssignments.filterKeys { it in savedKeys },
contactAssignmentHistory = current.contactAssignmentHistory.filterKeys { it in savedKeys },
contactAssignments = current.contactAssignments.filterKeys {
it.publicKeyFromAssignmentKey() in savedKeys
},
contactAssignmentHistory = current.contactAssignmentHistory.filterKeys {
it.publicKeyFromAssignmentKey() in savedKeys
},
)
if (next == current) return@locked
ledger = next
Expand All @@ -242,8 +243,8 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
}
}

private suspend fun allocateAddress(publicKey: String): Result<String> = withContext(ioDispatcher) {
runCatching {
private suspend fun allocateAddress(assignmentKey: String): Result<String> = withContext(ioDispatcher) {
runSuspendCatching {
val addressType = selectedAddressType()
val addressTypeKey = addressType.toSettingsString()

Expand All @@ -259,13 +260,13 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
)
locked { current ->
val reserved = current.reservedReceiveIndexesByAddressType[addressTypeKey].orEmpty() + addressInfo.index
val history = current.contactAssignmentHistory[publicKey].orEmpty()
val history = current.contactAssignmentHistory[assignmentKey].orEmpty()
.let { if (assignment in it) it else it + assignment }
val next = current.copy(
reservedReceiveIndexesByAddressType = current.reservedReceiveIndexesByAddressType +
(addressTypeKey to reserved),
contactAssignments = current.contactAssignments + (publicKey to assignment),
contactAssignmentHistory = current.contactAssignmentHistory + (publicKey to history),
contactAssignments = current.contactAssignments + (assignmentKey to assignment),
contactAssignmentHistory = current.contactAssignmentHistory + (assignmentKey to history),
)
ledger = next
persist(next)
Expand Down Expand Up @@ -317,9 +318,9 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
}
}

private suspend fun clearCurrentAssignment(publicKey: String) {
private suspend fun clearCurrentAssignment(assignmentKey: String) {
locked { current ->
val next = current.copy(contactAssignments = current.contactAssignments - publicKey)
val next = current.copy(contactAssignments = current.contactAssignments - assignmentKey)
ledger = next
persist(next)
notifyBackupStateChanged()
Expand Down Expand Up @@ -377,6 +378,13 @@ class PrivatePaykitAddressReservationRepo @Inject constructor(
private fun normalizedPublicKeyOrNull(publicKey: String): String? =
PubkyPublicKeyFormat.normalized(publicKey)

private fun contactAssignmentKey(publicKey: String, receiverPath: String): String {
val normalizedKey = normalizedPublicKey(publicKey)
return if (receiverPath == PaykitReceiverPaths.WALLET) normalizedKey else "$normalizedKey#$receiverPath"
}

private fun String.publicKeyFromAssignmentKey(): String = substringBefore("#")

private fun redacted(publicKey: String): String =
PubkyPublicKeyFormat.redacted(publicKey)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class PrivatePaykitContactResolver @Inject constructor(
if (paymentHash.isBlank()) return@withContext null
cacheStore.data.first().contacts.firstNotNullOfOrNull { (publicKey, contactState) ->
publicKey.takeIf {
contactState.localInvoice?.paymentHash == paymentHash ||
contactState.localInvoicesByReceiverPath.values.any { invoice ->
invoice.paymentHash == paymentHash
} ||
paymentHash in contactState.receivedInvoicePaymentHashes
}
}
Expand Down
20 changes: 12 additions & 8 deletions app/src/main/java/to/bitkit/repositories/PrivatePaykitModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,32 @@ internal data class PrivatePaykitState(

internal data class ContactState(
var remoteEndpoints: List<StoredPaymentEntry> = emptyList(),
var localInvoice: StoredInvoice? = null,
var localInvoicesByReceiverPath: Map<String, StoredInvoice> = emptyMap(),
var receivedInvoicePaymentHashes: List<String> = emptyList(),
var hasPublishedPrivatePaymentList: Boolean = false,
var publishedPrivatePaymentReceiverPaths: Set<String> = emptySet(),
) {
constructor(cache: PrivatePaykitContactCacheData) : this(
remoteEndpoints = cache.remoteEndpoints.map { StoredPaymentEntry(it.methodId, it.endpointData) },
localInvoice = cache.localInvoice?.let { StoredInvoice(it.bolt11, it.paymentHash, it.expiresAt) },
localInvoicesByReceiverPath = cache.localInvoicesByReceiverPath.mapValues { (_, invoice) ->
StoredInvoice(invoice.bolt11, invoice.paymentHash, invoice.expiresAt)
},
receivedInvoicePaymentHashes = cache.receivedInvoicePaymentHashes,
hasPublishedPrivatePaymentList = cache.hasPublishedPrivatePaymentList,
publishedPrivatePaymentReceiverPaths = cache.publishedPrivatePaymentReceiverPaths,
)

val hasCacheState: Boolean
get() = hasPublishedPrivatePaymentList ||
get() = publishedPrivatePaymentReceiverPaths.isNotEmpty() ||
remoteEndpoints.isNotEmpty() ||
localInvoice != null ||
localInvoicesByReceiverPath.isNotEmpty() ||
receivedInvoicePaymentHashes.isNotEmpty()

fun cacheState() = PrivatePaykitContactCacheData(
remoteEndpoints = remoteEndpoints.map { PrivatePaykitStoredPaymentEntryData(it.methodId, it.endpointData) },
localInvoice = localInvoice?.let { PrivatePaykitStoredInvoiceData(it.bolt11, it.paymentHash, it.expiresAt) },
localInvoicesByReceiverPath = localInvoicesByReceiverPath.mapValues { (_, invoice) ->
PrivatePaykitStoredInvoiceData(invoice.bolt11, invoice.paymentHash, invoice.expiresAt)
},
receivedInvoicePaymentHashes = receivedInvoicePaymentHashes,
hasPublishedPrivatePaymentList = hasPublishedPrivatePaymentList,
publishedPrivatePaymentReceiverPaths = publishedPrivatePaymentReceiverPaths,
)
}

Expand Down
Loading
Loading