Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c005917
feat: add public contact payments
ben-kaufman Apr 30, 2026
216319b
Merge branch 'master' into codex-public-payments-contacts
ben-kaufman Apr 30, 2026
aab781c
fix: allow contact replacement
ben-kaufman Apr 30, 2026
758ea69
fix: address review comments
ben-kaufman Apr 30, 2026
4219bae
fix: address claude comments
ben-kaufman Apr 30, 2026
fa64cb4
Update ContactActivityViewModel.kt
ben-kaufman Apr 30, 2026
266f923
fix: align add contact send
ben-kaufman May 4, 2026
2d7d9ef
fix: preload contact payments
ben-kaufman May 4, 2026
6f9aa72
fix: align onchain paykit
ben-kaufman May 4, 2026
263a297
fix: gate contact details loading
ben-kaufman May 4, 2026
2da3269
fix: preserve pubky session
ben-kaufman May 4, 2026
c1b8120
fix: load contacts after auth
ben-kaufman May 4, 2026
37eb079
fix: show contact activity titles
ben-kaufman May 4, 2026
98b4e0c
fix: expose immutable contacts
ben-kaufman May 4, 2026
0ad5fd3
fix: polish paykit contacts
ben-kaufman May 4, 2026
c401c84
fix: serialize paykit publishing
ben-kaufman May 4, 2026
2affee5
Merge branch 'master' into codex-public-payments-contacts
ben-kaufman May 4, 2026
b7e7791
fix: isolate paykit invoice
ben-kaufman May 4, 2026
185de8c
fix: harden contact paykit
ben-kaufman May 4, 2026
6a67b2c
fix: follow repo rules
ben-kaufman May 4, 2026
1a0d751
fix: manage paykit endpoint lifecycle
ben-kaufman May 4, 2026
7b030f1
fix: allow cleanup failure
ben-kaufman May 4, 2026
de2cee7
fix: tag pending contact payments
ben-kaufman May 4, 2026
9a25b62
fix: harden public contact payments
ben-kaufman May 4, 2026
21825ef
fix: clean up activity contact titles
ben-kaufman May 4, 2026
57d0de6
fix: encode paykit bip21 invoice
ben-kaufman May 4, 2026
e9a12c1
fix: align contact detail actions
ben-kaufman May 4, 2026
2920a97
fix: reset paykit state on signout
ben-kaufman May 4, 2026
25b823b
fix: refresh paykit on channel events
ben-kaufman May 4, 2026
70bde1a
fix: skip quickpay for contacts
ben-kaufman May 4, 2026
afeb1f4
fix: update paykit bindings
ben-kaufman May 5, 2026
94938d2
fix: address paykit review feedback
ovitrif May 5, 2026
d666ceb
fix: restore paykit node gate
ovitrif May 5, 2026
b54ba49
fix: type paykit endpoint payload
ovitrif May 5, 2026
774f154
fix: align bolt11 expiry
ovitrif May 5, 2026
1e4dd83
fix: restore paykit invoice expiry
ovitrif May 5, 2026
8a5c573
Merge pull request #930 from synonymdev/codex/pr924-review-fixes
ovitrif May 5, 2026
50e1552
fix: harden paykit endpoints
ben-kaufman May 5, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Support public Paykit contact payments #924
Comment thread
ben-kaufman marked this conversation as resolved.
Outdated

### Changed
- Improve Pubky profile restore, contact editing, and contact routing flows #905

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ data class SettingsData(
val hasSeenShopIntro: Boolean = false,
val hasSeenProfileIntro: Boolean = false,
val hasSeenContactsIntro: Boolean = false,
val hasConfirmedPublicPaykitEndpoints: Boolean = false,
Comment thread
ovitrif marked this conversation as resolved.
val sharesPublicPaykitEndpoints: Boolean = false,
val quickPayIntroSeen: Boolean = false,
val bgPaymentsIntroSeen: Boolean = false,
val isQuickPayEnabled: Boolean = false,
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ internal object Env {
Network.BITCOIN -> ""
else -> "staging."
}
return "/pub/$pubkyDomain/:rw,/pub/${prefix}pubky.app/:r,/pub/${prefix}paykit/v0/:rw"
return "/pub/$pubkyDomain/:rw,/pub/${prefix}pubky.app/:r,/pub/paykit/v0/:rw"
}

val homegateUrl: String
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/to/bitkit/ext/Activities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ fun LightningActivity.Companion.create(
fee: ULong = 0u,
message: String = "",
preimage: String? = null,
contact: String? = null,
createdAt: ULong? = timestamp,
updatedAt: ULong? = createdAt,
seenAt: ULong? = null,
Expand All @@ -105,6 +106,7 @@ fun LightningActivity.Companion.create(
message = message,
timestamp = timestamp,
preimage = preimage,
contact = contact,
createdAt = createdAt,
updatedAt = updatedAt,
seenAt = seenAt,
Expand All @@ -128,6 +130,7 @@ fun OnchainActivity.Companion.create(
confirmTimestamp: ULong? = null,
channelId: String? = null,
transferTxId: String? = null,
contact: String? = null,
createdAt: ULong? = timestamp,
updatedAt: ULong? = createdAt,
seenAt: ULong? = null,
Expand All @@ -148,6 +151,7 @@ fun OnchainActivity.Companion.create(
confirmTimestamp = confirmTimestamp,
channelId = channelId,
transferTxId = transferTxId,
contact = contact,
createdAt = createdAt,
updatedAt = updatedAt,
seenAt = seenAt,
Expand Down
77 changes: 77 additions & 0 deletions app/src/main/java/to/bitkit/repositories/ActivityRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ import org.lightningdevkit.ldknode.TransactionDetails
import to.bitkit.data.CacheStore
import to.bitkit.data.dto.PendingBoostActivity
import to.bitkit.di.BgDispatcher
import to.bitkit.di.IoDispatcher
import to.bitkit.ext.amountOnClose
import to.bitkit.ext.matchesPaymentId
import to.bitkit.ext.nowMillis
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.rawId
import to.bitkit.models.ActivityBackupV1
import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.services.CoreService
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
Expand All @@ -55,6 +57,7 @@ private const val MS_SYNC_TIMEOUT = 40_000L
@Singleton
class ActivityRepo @Inject constructor(
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val coreService: CoreService,
private val lightningRepo: LightningRepo,
private val blocktankRepo: BlocktankRepo,
Expand Down Expand Up @@ -338,6 +341,79 @@ class ActivityRepo @Inject constructor(
}
}

suspend fun contactActivities(publicKey: String): Result<List<Activity>> = withContext(ioDispatcher) {
runCatching {
val normalizedKey = PubkyPublicKeyFormat.normalized(publicKey) ?: publicKey
getActivities(
filter = ActivityFilter.ALL,
sortDirection = SortDirection.DESC,
).getOrThrow().filter { activity ->
when (activity) {
is Activity.Lightning -> PubkyPublicKeyFormat.matches(activity.v1.contact, normalizedKey)
is Activity.Onchain -> PubkyPublicKeyFormat.matches(activity.v1.contact, normalizedKey)
}
Comment thread
ovitrif marked this conversation as resolved.
Outdated
}
}.onFailure {
Logger.error("Failed to load contact activities for '$publicKey'", it, context = TAG)
}
}

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

val normalizedKey = PubkyPublicKeyFormat.normalized(contactPublicKey) ?: contactPublicKey
val activity = findActivityForPaymentId(forPaymentId, syncLdkPayments)
if (activity == null) {
Logger.warn(
"Skipped setting contact for payment '$forPaymentId' because activity was not found",
context = TAG,
)
return@runCatching
}
if (PubkyPublicKeyFormat.matches(activity.contact(), normalizedKey)) {
return@runCatching
}

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

private suspend fun findActivityForPaymentId(forPaymentId: String, syncLdkPayments: Boolean): Activity? {
val activity = getActivityByPaymentId(forPaymentId)
if (activity != null) return activity
if (!syncLdkPayments) return null

syncActivities().getOrThrow()
return getActivityByPaymentId(forPaymentId)
}

private suspend fun getActivityByPaymentId(forPaymentId: String): Activity? =
coreService.activity.getActivity(forPaymentId)
?: getOnchainActivityByTxId(forPaymentId)?.let { Activity.Onchain(it) }

private fun Activity.contact(): String? = when (this) {
is Activity.Lightning -> v1.contact
is Activity.Onchain -> v1.contact
}

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))
}

suspend fun getClosedChannels(
sortDirection: SortDirection = SortDirection.ASC,
): Result<List<ClosedChannelDetails>> = withContext(bgDispatcher) {
Expand Down Expand Up @@ -515,6 +591,7 @@ class ActivityRepo @Inject constructor(
message = "",
timestamp = now,
preimage = null,
contact = null,
createdAt = now,
updatedAt = null,
seenAt = null,
Expand Down
44 changes: 40 additions & 4 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package to.bitkit.repositories
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import coil3.ImageLoader
import com.synonym.paykit.FfiPaymentEntry
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.post
Expand Down Expand Up @@ -46,6 +47,7 @@ import kotlin.math.min
enum class PubkyAuthState { Idle, Authenticating, Authenticated }

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

// endregion

// region Payment endpoints

suspend fun getPaymentList(publicKey: String): Result<List<FfiPaymentEntry>> = withContext(ioDispatcher) {
runCatching {
pubkyService.getPaymentList(publicKey.ensurePubkyPrefix())
}
}

suspend fun setPaymentEndpoint(methodId: String, endpointData: String): Result<Unit> = withContext(ioDispatcher) {
runCatching {
pubkyService.setPaymentEndpoint(methodId, endpointData)
}
}

suspend fun removePaymentEndpoint(methodId: String): Result<Unit> = withContext(ioDispatcher) {
runCatching {
pubkyService.removePaymentEndpoint(methodId)
}
}

suspend fun currentPublicKey(): Result<String?> = withContext(ioDispatcher) {
runCatching {
pubkyService.currentPublicKey()?.ensurePubkyPrefix()
}
}

// endregion

// region Profile loading

suspend fun loadProfile() {
Expand Down Expand Up @@ -918,11 +948,17 @@ class PubkyRepo @Inject constructor(

private fun requireAddableContactPublicKey(publicKey: String): String {
val prefixedKey = PubkyPublicKeyFormat.normalized(publicKey)
?: throw PubkyContactError.InvalidFormat
if (_publicKey.value == prefixedKey) {
throw PubkyContactError.CannotAddSelf
contactValidationError(prefixedKey)?.let { throw it }
return checkNotNull(prefixedKey) { "Normalized pubky key is required" }
}

private fun contactValidationError(prefixedKey: String?): PubkyContactError? {
if (prefixedKey == null) return PubkyContactError.InvalidFormat
if (_publicKey.value == prefixedKey) return PubkyContactError.CannotAddSelf
if (_contacts.value.any { PubkyPublicKeyFormat.matches(it.publicKey, prefixedKey) }) {
return PubkyContactError.AlreadyExists
}
return prefixedKey
return null
}

private fun String.ensurePubkyPrefix(): String =
Expand Down
Loading
Loading