Skip to content

Commit 0a2aea4

Browse files
committed
feat: add public contact payments
1 parent 1573ac7 commit 0a2aea4

31 files changed

Lines changed: 1371 additions & 40 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Support public Paykit contact payments.
12+
1013
### Changed
1114
- Improve Pubky profile restore, contact editing, and contact routing flows #905
1215

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ data class SettingsData(
100100
val hasSeenShopIntro: Boolean = false,
101101
val hasSeenProfileIntro: Boolean = false,
102102
val hasSeenContactsIntro: Boolean = false,
103+
val hasConfirmedPublicPaykitEndpoints: Boolean = false,
104+
val sharesPublicPaykitEndpoints: Boolean = false,
103105
val quickPayIntroSeen: Boolean = false,
104106
val bgPaymentsIntroSeen: Boolean = false,
105107
val isQuickPayEnabled: Boolean = false,

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ internal object Env {
169169
Network.BITCOIN -> ""
170170
else -> "staging."
171171
}
172-
return "/pub/$pubkyDomain/:rw,/pub/${prefix}pubky.app/:r,/pub/${prefix}paykit/v0/:rw"
172+
return "/pub/$pubkyDomain/:rw,/pub/${prefix}pubky.app/:r,/pub/paykit/v0/:rw"
173173
}
174174

175175
val homegateUrl: String

app/src/main/java/to/bitkit/ext/Activities.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ fun LightningActivity.Companion.create(
9292
fee: ULong = 0u,
9393
message: String = "",
9494
preimage: String? = null,
95+
contact: String? = null,
9596
createdAt: ULong? = timestamp,
9697
updatedAt: ULong? = createdAt,
9798
seenAt: ULong? = null,
@@ -105,6 +106,7 @@ fun LightningActivity.Companion.create(
105106
message = message,
106107
timestamp = timestamp,
107108
preimage = preimage,
109+
contact = contact,
108110
createdAt = createdAt,
109111
updatedAt = updatedAt,
110112
seenAt = seenAt,
@@ -128,6 +130,7 @@ fun OnchainActivity.Companion.create(
128130
confirmTimestamp: ULong? = null,
129131
channelId: String? = null,
130132
transferTxId: String? = null,
133+
contact: String? = null,
131134
createdAt: ULong? = timestamp,
132135
updatedAt: ULong? = createdAt,
133136
seenAt: ULong? = null,
@@ -148,6 +151,7 @@ fun OnchainActivity.Companion.create(
148151
confirmTimestamp = confirmTimestamp,
149152
channelId = channelId,
150153
transferTxId = transferTxId,
154+
contact = contact,
151155
createdAt = createdAt,
152156
updatedAt = updatedAt,
153157
seenAt = seenAt,

app/src/main/java/to/bitkit/repositories/ActivityRepo.kt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import org.lightningdevkit.ldknode.TransactionDetails
3333
import to.bitkit.data.CacheStore
3434
import to.bitkit.data.dto.PendingBoostActivity
3535
import to.bitkit.di.BgDispatcher
36+
import to.bitkit.di.IoDispatcher
3637
import to.bitkit.ext.amountOnClose
3738
import to.bitkit.ext.matchesPaymentId
3839
import to.bitkit.ext.nowMillis
3940
import to.bitkit.ext.nowTimestamp
4041
import to.bitkit.ext.rawId
4142
import to.bitkit.models.ActivityBackupV1
43+
import to.bitkit.models.PubkyPublicKeyFormat
4244
import to.bitkit.services.CoreService
4345
import to.bitkit.utils.AppError
4446
import to.bitkit.utils.Logger
@@ -55,6 +57,7 @@ private const val MS_SYNC_TIMEOUT = 40_000L
5557
@Singleton
5658
class ActivityRepo @Inject constructor(
5759
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
60+
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
5861
private val coreService: CoreService,
5962
private val lightningRepo: LightningRepo,
6063
private val blocktankRepo: BlocktankRepo,
@@ -338,6 +341,79 @@ class ActivityRepo @Inject constructor(
338341
}
339342
}
340343

344+
suspend fun contactActivities(publicKey: String): Result<List<Activity>> = withContext(ioDispatcher) {
345+
runCatching {
346+
val normalizedKey = PubkyPublicKeyFormat.normalized(publicKey) ?: publicKey
347+
getActivities(
348+
filter = ActivityFilter.ALL,
349+
sortDirection = SortDirection.DESC,
350+
).getOrThrow().filter { activity ->
351+
when (activity) {
352+
is Activity.Lightning -> PubkyPublicKeyFormat.matches(activity.v1.contact, normalizedKey)
353+
is Activity.Onchain -> PubkyPublicKeyFormat.matches(activity.v1.contact, normalizedKey)
354+
}
355+
}
356+
}.onFailure {
357+
Logger.error("Failed to load contact activities for '$publicKey'", it, context = TAG)
358+
}
359+
}
360+
361+
suspend fun setContact(
362+
contactPublicKey: String,
363+
forPaymentId: String,
364+
syncLdkPayments: Boolean = true,
365+
): Result<Unit> = withContext(ioDispatcher) {
366+
runCatching {
367+
if (syncLdkPayments) {
368+
lightningRepo.getPayments().onSuccess {
369+
syncLdkNodePayments(it).getOrThrow()
370+
}.getOrThrow()
371+
}
372+
373+
val normalizedKey = PubkyPublicKeyFormat.normalized(contactPublicKey) ?: contactPublicKey
374+
val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments)
375+
if (activity == null) {
376+
Logger.warn(
377+
"Skipped setting contact for payment '$forPaymentId' because activity was not found",
378+
context = TAG,
379+
)
380+
return@runCatching
381+
}
382+
if (PubkyPublicKeyFormat.matches(activity.contact(), normalizedKey)) {
383+
return@runCatching
384+
}
385+
386+
val updatedAt = nowTimestamp().epochSecond.toULong()
387+
val updatedActivity = activity.withContact(normalizedKey, updatedAt)
388+
updateActivity(updatedActivity.rawId(), updatedActivity).getOrThrow()
389+
}.onFailure {
390+
Logger.error("Failed to set contact for payment '$forPaymentId'", it, context = TAG)
391+
}
392+
}
393+
394+
private suspend fun findActivityForPaymentId(forPaymentId: String, syncLdkPayments: Boolean): Activity? {
395+
val activity = getActivityByPaymentId(forPaymentId)
396+
if (activity != null) return activity
397+
if (!syncLdkPayments) return null
398+
399+
syncActivities().getOrThrow()
400+
return getActivityByPaymentId(forPaymentId)
401+
}
402+
403+
private suspend fun getActivityByPaymentId(forPaymentId: String): Activity? =
404+
coreService.activity.getActivity(forPaymentId)
405+
?: getOnchainActivityByTxId(forPaymentId)?.let { Activity.Onchain(it) }
406+
407+
private fun Activity.contact(): String? = when (this) {
408+
is Activity.Lightning -> v1.contact
409+
is Activity.Onchain -> v1.contact
410+
}
411+
412+
private fun Activity.withContact(normalizedKey: String, updatedAt: ULong): Activity = when (this) {
413+
is Activity.Lightning -> Activity.Lightning(v1.copy(contact = normalizedKey, updatedAt = updatedAt))
414+
is Activity.Onchain -> Activity.Onchain(v1.copy(contact = normalizedKey, updatedAt = updatedAt))
415+
}
416+
341417
suspend fun getClosedChannels(
342418
sortDirection: SortDirection = SortDirection.ASC,
343419
): Result<List<ClosedChannelDetails>> = withContext(bgDispatcher) {
@@ -515,6 +591,7 @@ class ActivityRepo @Inject constructor(
515591
message = "",
516592
timestamp = now,
517593
preimage = null,
594+
contact = null,
518595
createdAt = now,
519596
updatedAt = null,
520597
seenAt = null,

app/src/main/java/to/bitkit/repositories/PubkyRepo.kt

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package to.bitkit.repositories
33
import android.graphics.Bitmap
44
import android.graphics.BitmapFactory
55
import coil3.ImageLoader
6+
import com.synonym.paykit.FfiPaymentEntry
67
import io.ktor.client.HttpClient
78
import io.ktor.client.call.body
89
import io.ktor.client.request.post
@@ -46,6 +47,7 @@ import kotlin.math.min
4647
enum class PubkyAuthState { Idle, Authenticating, Authenticated }
4748

4849
sealed class PubkyContactError(message: String) : AppError(message) {
50+
data object AlreadyExists : PubkyContactError("Contact already exists")
4951
data object CannotAddSelf : PubkyContactError("Cannot add your own pubky as a contact")
5052
data object InvalidFormat : PubkyContactError("Invalid pubky key format")
5153
}
@@ -280,6 +282,34 @@ class PubkyRepo @Inject constructor(
280282

281283
// endregion
282284

285+
// region Payment endpoints
286+
287+
suspend fun getPaymentList(publicKey: String): Result<List<FfiPaymentEntry>> = withContext(ioDispatcher) {
288+
runCatching {
289+
pubkyService.getPaymentList(publicKey.ensurePubkyPrefix())
290+
}
291+
}
292+
293+
suspend fun setPaymentEndpoint(methodId: String, endpointData: String): Result<Unit> = withContext(ioDispatcher) {
294+
runCatching {
295+
pubkyService.setPaymentEndpoint(methodId, endpointData)
296+
}
297+
}
298+
299+
suspend fun removePaymentEndpoint(methodId: String): Result<Unit> = withContext(ioDispatcher) {
300+
runCatching {
301+
pubkyService.removePaymentEndpoint(methodId)
302+
}
303+
}
304+
305+
suspend fun currentPublicKey(): Result<String?> = withContext(ioDispatcher) {
306+
runCatching {
307+
pubkyService.currentPublicKey()?.ensurePubkyPrefix()
308+
}
309+
}
310+
311+
// endregion
312+
283313
// region Profile loading
284314

285315
suspend fun loadProfile() {
@@ -918,11 +948,17 @@ class PubkyRepo @Inject constructor(
918948

919949
private fun requireAddableContactPublicKey(publicKey: String): String {
920950
val prefixedKey = PubkyPublicKeyFormat.normalized(publicKey)
921-
?: throw PubkyContactError.InvalidFormat
922-
if (_publicKey.value == prefixedKey) {
923-
throw PubkyContactError.CannotAddSelf
951+
contactValidationError(prefixedKey)?.let { throw it }
952+
return checkNotNull(prefixedKey) { "Normalized pubky key is required" }
953+
}
954+
955+
private fun contactValidationError(prefixedKey: String?): PubkyContactError? {
956+
if (prefixedKey == null) return PubkyContactError.InvalidFormat
957+
if (_publicKey.value == prefixedKey) return PubkyContactError.CannotAddSelf
958+
if (_contacts.value.any { PubkyPublicKeyFormat.matches(it.publicKey, prefixedKey) }) {
959+
return PubkyContactError.AlreadyExists
924960
}
925-
return prefixedKey
961+
return null
926962
}
927963

928964
private fun String.ensurePubkyPrefix(): String =

0 commit comments

Comments
 (0)