Skip to content

Commit f3c3e2e

Browse files
authored
Merge branch 'master' into feat/spend-unconfirmed
2 parents 6e770e0 + 589f652 commit f3c3e2e

File tree

11 files changed

+266
-103
lines changed

11 files changed

+266
-103
lines changed

app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,38 +36,13 @@ class NotifyPaymentReceivedHandler @Inject constructor(
3636
): Result<NotifyPaymentReceived.Result> = withContext(ioDispatcher) {
3737
runCatching {
3838
val shouldShow = when (command) {
39-
is NotifyPaymentReceived.Command.Lightning -> true
40-
is NotifyPaymentReceived.Command.Onchain -> {
41-
activityRepo.handleOnchainTransactionReceived(command.event.txid, command.event.details)
42-
if (command.event.details.amountSats > 0) {
43-
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
44-
activityRepo.shouldShowReceivedSheet(
45-
command.event.txid,
46-
command.event.details.amountSats.toULong()
47-
)
48-
} else {
49-
false
50-
}
51-
}
39+
is NotifyPaymentReceived.Command.Lightning -> shouldShowLightning(command)
40+
is NotifyPaymentReceived.Command.Onchain -> shouldShowOnchain(command)
5241
}
5342

5443
if (!shouldShow) return@runCatching NotifyPaymentReceived.Result.Skip
5544

56-
val details = NewTransactionSheetDetails(
57-
type = when (command) {
58-
is NotifyPaymentReceived.Command.Lightning -> NewTransactionSheetType.LIGHTNING
59-
is NotifyPaymentReceived.Command.Onchain -> NewTransactionSheetType.ONCHAIN
60-
},
61-
direction = NewTransactionSheetDirection.RECEIVED,
62-
paymentHashOrTxId = when (command) {
63-
is NotifyPaymentReceived.Command.Lightning -> command.event.paymentHash
64-
is NotifyPaymentReceived.Command.Onchain -> command.event.txid
65-
},
66-
sats = when (command) {
67-
is NotifyPaymentReceived.Command.Lightning -> (command.event.amountMsat / 1000u).toLong()
68-
is NotifyPaymentReceived.Command.Onchain -> command.event.details.amountSats
69-
},
70-
)
45+
val details = buildSheetDetails(command)
7146

7247
if (command.includeNotification) {
7348
val notification = buildNotificationContent(details.sats)
@@ -80,6 +55,45 @@ class NotifyPaymentReceivedHandler @Inject constructor(
8055
}
8156
}
8257

58+
private suspend fun shouldShowLightning(command: NotifyPaymentReceived.Command.Lightning): Boolean {
59+
val paymentId = command.event.paymentId ?: return false
60+
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
61+
if (activityRepo.isActivitySeen(paymentId)) return false
62+
activityRepo.markActivityAsSeen(paymentId)
63+
return true
64+
}
65+
66+
private suspend fun shouldShowOnchain(command: NotifyPaymentReceived.Command.Onchain): Boolean {
67+
activityRepo.handleOnchainTransactionReceived(command.event.txid, command.event.details)
68+
if (command.event.details.amountSats <= 0) return false
69+
70+
delay(DELAY_FOR_ACTIVITY_SYNC_MS)
71+
val shouldShowSheet = activityRepo.shouldShowReceivedSheet(
72+
command.event.txid,
73+
command.event.details.amountSats.toULong()
74+
)
75+
if (shouldShowSheet) {
76+
activityRepo.markOnchainActivityAsSeen(command.event.txid)
77+
}
78+
return shouldShowSheet
79+
}
80+
81+
private fun buildSheetDetails(command: NotifyPaymentReceived.Command) = NewTransactionSheetDetails(
82+
type = when (command) {
83+
is NotifyPaymentReceived.Command.Lightning -> NewTransactionSheetType.LIGHTNING
84+
is NotifyPaymentReceived.Command.Onchain -> NewTransactionSheetType.ONCHAIN
85+
},
86+
direction = NewTransactionSheetDirection.RECEIVED,
87+
paymentHashOrTxId = when (command) {
88+
is NotifyPaymentReceived.Command.Lightning -> command.event.paymentHash
89+
is NotifyPaymentReceived.Command.Onchain -> command.event.txid
90+
},
91+
sats = when (command) {
92+
is NotifyPaymentReceived.Command.Lightning -> (command.event.amountMsat / 1000u).toLong()
93+
is NotifyPaymentReceived.Command.Onchain -> command.event.details.amountSats
94+
},
95+
)
96+
8397
private suspend fun buildNotificationContent(sats: Long): NotificationDetails {
8498
val settings = settingsStore.data.first()
8599
val title = context.getString(R.string.notification_received_title)

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import javax.inject.Inject
4141
import javax.inject.Singleton
4242
import kotlin.time.Clock
4343
import kotlin.time.ExperimentalTime
44+
import com.synonym.bitkitcore.TransactionDetails as BitkitCoreTransactionDetails
4445

4546
private const val SYNC_TIMEOUT_MS = 40_000L
4647

@@ -232,6 +233,24 @@ class ActivityRepo @Inject constructor(
232233
return coreService.activity.shouldShowReceivedSheet(txid, value)
233234
}
234235

236+
suspend fun isActivitySeen(activityId: String): Boolean {
237+
return coreService.activity.isActivitySeen(activityId)
238+
}
239+
240+
suspend fun markActivityAsSeen(activityId: String) {
241+
coreService.activity.markActivityAsSeen(activityId)
242+
notifyActivitiesChanged()
243+
}
244+
245+
suspend fun markOnchainActivityAsSeen(txid: String) {
246+
coreService.activity.markOnchainActivityAsSeen(txid)
247+
notifyActivitiesChanged()
248+
}
249+
250+
suspend fun getTransactionDetails(txid: String): Result<BitkitCoreTransactionDetails?> = runCatching {
251+
coreService.activity.getTransactionDetails(txid)
252+
}
253+
235254
suspend fun getBoostTxDoesExist(boostTxIds: List<String>): Map<String, Boolean> {
236255
return coreService.activity.getBoostTxDoesExist(boostTxIds)
237256
}
@@ -529,7 +548,7 @@ class ActivityRepo @Inject constructor(
529548
preimage = null,
530549
createdAt = now,
531550
updatedAt = null,
532-
seenAt = null, // TODO implement synonymdev/bitkit-ios#270 changes
551+
seenAt = null,
533552
)
534553
)
535554
)

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -728,12 +728,6 @@ class LightningRepo @Inject constructor(
728728
Result.success(payments)
729729
}
730730

731-
suspend fun getTransactionDetails(txid: Txid): Result<TransactionDetails?> = executeWhenNodeRunning(
732-
"Get transaction details by txid"
733-
) {
734-
Result.success(lightningService.getTransactionDetails(txid))
735-
}
736-
737731
suspend fun getAddressBalance(address: String): Result<ULong> = executeWhenNodeRunning("Get address balance") {
738732
runCatching {
739733
lightningService.getAddressBalance(address)

app/src/main/java/to/bitkit/services/CoreService.kt

Lines changed: 118 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import com.synonym.bitkitcore.upsertCjitEntries
5050
import com.synonym.bitkitcore.upsertClosedChannels
5151
import com.synonym.bitkitcore.upsertInfo
5252
import com.synonym.bitkitcore.upsertOrders
53+
import com.synonym.bitkitcore.upsertTransactionDetails
5354
import com.synonym.bitkitcore.wipeAllDatabases
5455
import io.ktor.client.HttpClient
5556
import io.ktor.client.request.get
@@ -77,6 +78,10 @@ import to.bitkit.utils.ServiceError
7778
import javax.inject.Inject
7879
import javax.inject.Singleton
7980
import kotlin.random.Random
81+
import com.synonym.bitkitcore.TransactionDetails as BitkitCoreTransactionDetails
82+
import com.synonym.bitkitcore.TxInput as BitkitCoreTxInput
83+
import com.synonym.bitkitcore.TxOutput as BitkitCoreTxOutput
84+
import com.synonym.bitkitcore.getTransactionDetails as getBitkitCoreTransactionDetails
8085

8186
// region Core
8287

@@ -248,6 +253,48 @@ class ActivityService(
248253
upsertActivities(activities)
249254
}
250255

256+
private fun mapToCoreTransactionDetails(
257+
txid: String,
258+
details: TransactionDetails,
259+
): BitkitCoreTransactionDetails {
260+
val inputs = details.inputs.map { input ->
261+
BitkitCoreTxInput(
262+
txid = input.txid,
263+
vout = input.vout,
264+
scriptsig = input.scriptsig,
265+
witness = input.witness,
266+
sequence = input.sequence,
267+
)
268+
}
269+
val outputs = details.outputs.map { output ->
270+
BitkitCoreTxOutput(
271+
scriptpubkey = output.scriptpubkey,
272+
scriptpubkeyType = output.scriptpubkeyType,
273+
scriptpubkeyAddress = output.scriptpubkeyAddress,
274+
value = output.value,
275+
n = output.n,
276+
)
277+
}
278+
return BitkitCoreTransactionDetails(
279+
txId = txid,
280+
amountSats = details.amountSats,
281+
inputs = inputs,
282+
outputs = outputs,
283+
)
284+
}
285+
286+
suspend fun saveTransactionDetails(txid: String, details: TransactionDetails) {
287+
ServiceQueue.CORE.background {
288+
val coreDetails = mapToCoreTransactionDetails(txid, details)
289+
upsertTransactionDetails(listOf(coreDetails))
290+
}
291+
}
292+
293+
suspend fun getTransactionDetails(txid: String): BitkitCoreTransactionDetails? =
294+
ServiceQueue.CORE.background {
295+
getBitkitCoreTransactionDetails(txid)
296+
}
297+
251298
suspend fun getActivity(id: String): Activity? {
252299
return ServiceQueue.CORE.background {
253300
getActivityById(id)
@@ -484,7 +531,7 @@ class ActivityService(
484531
fee = (payment.feePaidMsat ?: 0u) / 1000u,
485532
message = kind.description.orEmpty(),
486533
preimage = kind.preimage,
487-
seenAt = null, // TODO implement synonymdev/bitkit-ios#270 changes
534+
seenAt = null,
488535
)
489536
}
490537

@@ -499,8 +546,15 @@ class ActivityService(
499546
* Check pre-activity metadata for addresses in the transaction
500547
* Returns the first address found in pre-activity metadata that matches a transaction output
501548
*/
549+
private suspend fun fetchTransactionDetails(txid: String): BitkitCoreTransactionDetails? =
550+
runCatching { getTransactionDetails(txid) }
551+
.onFailure { e ->
552+
Logger.warn("Failed to fetch stored transaction details for $txid: $e", context = TAG)
553+
}
554+
.getOrNull()
555+
502556
private suspend fun findAddressInPreActivityMetadata(
503-
details: TransactionDetails
557+
details: BitkitCoreTransactionDetails,
504558
): String? {
505559
for (output in details.outputs) {
506560
val address = output.scriptpubkeyAddress ?: continue
@@ -519,14 +573,14 @@ class ActivityService(
519573
kind: PaymentKind.Onchain,
520574
existingActivity: Activity?,
521575
payment: PaymentDetails,
522-
transactionDetails: TransactionDetails? = null,
576+
transactionDetails: BitkitCoreTransactionDetails? = null,
523577
): String? {
524578
if (existingActivity != null || payment.direction != PaymentDirection.INBOUND) {
525579
return null
526580
}
527581

528582
// Get transaction details if not provided
529-
val details = transactionDetails ?: lightningService.getTransactionDetails(kind.txid)
583+
val details = transactionDetails ?: fetchTransactionDetails(kind.txid)
530584
if (details == null) {
531585
Logger.verbose("Transaction details not available for txid: ${kind.txid}", context = TAG)
532586
return null
@@ -609,7 +663,7 @@ class ActivityService(
609663
isTransfer = isTransfer,
610664
confirmTimestamp = confirmationData.confirmedTimestamp,
611665
channelId = channelId,
612-
seenAt = null, // TODO implement synonymdev/bitkit-ios#270 changes
666+
seenAt = null,
613667
)
614668
}
615669

@@ -618,7 +672,7 @@ class ActivityService(
618672
payment: PaymentDetails,
619673
forceUpdate: Boolean,
620674
channelId: String? = null,
621-
transactionDetails: TransactionDetails? = null,
675+
transactionDetails: BitkitCoreTransactionDetails? = null,
622676
) {
623677
val timestamp = payment.latestUpdateTimestamp
624678
val confirmationData = getConfirmationStatus(kind, timestamp)
@@ -769,6 +823,9 @@ class ActivityService(
769823
suspend fun handleOnchainTransactionReceived(txid: String, details: TransactionDetails) {
770824
ServiceQueue.CORE.background {
771825
runCatching {
826+
val coreDetails = mapToCoreTransactionDetails(txid, details)
827+
upsertTransactionDetails(listOf(coreDetails))
828+
772829
val payments = lightningService.payments ?: run {
773830
Logger.warn("No payments available for transaction $txid", context = TAG)
774831
return@background
@@ -786,7 +843,7 @@ class ActivityService(
786843
payment = payment,
787844
forceUpdate = false,
788845
channelId = null,
789-
transactionDetails = details,
846+
transactionDetails = coreDetails,
790847
)
791848
}.onFailure { e ->
792849
Logger.error("Error handling onchain transaction received for $txid", e, context = TAG)
@@ -797,6 +854,9 @@ class ActivityService(
797854
suspend fun handleOnchainTransactionConfirmed(txid: String, details: TransactionDetails) {
798855
ServiceQueue.CORE.background {
799856
runCatching {
857+
val coreDetails = mapToCoreTransactionDetails(txid, details)
858+
upsertTransactionDetails(listOf(coreDetails))
859+
800860
val payments = lightningService.payments ?: run {
801861
Logger.warn("No payments available for transaction $txid", context = TAG)
802862
return@background
@@ -814,7 +874,7 @@ class ActivityService(
814874
payment = payment,
815875
forceUpdate = false,
816876
channelId = null,
817-
transactionDetails = details,
877+
transactionDetails = coreDetails,
818878
)
819879
}.onFailure { e ->
820880
Logger.error("Error handling onchain transaction confirmed for $txid", e, context = TAG)
@@ -1000,6 +1060,15 @@ class ActivityService(
10001060
runCatching {
10011061
val onchain = getOnchainActivityByTxId(txid) ?: return@background true
10021062

1063+
// Check if activity has already been seen
1064+
if (onchain.seenAt != null) {
1065+
Logger.info(
1066+
"Skipping received sheet for transaction $txid - already seen at ${onchain.seenAt}",
1067+
context = TAG
1068+
)
1069+
return@background false
1070+
}
1071+
10031072
if (onchain.boostTxIds.isEmpty()) {
10041073
return@background true
10051074
}
@@ -1023,6 +1092,40 @@ class ActivityService(
10231092
}
10241093
}
10251094

1095+
suspend fun isActivitySeen(activityId: String): Boolean = ServiceQueue.CORE.background {
1096+
val activity = getActivityById(activityId) ?: return@background false
1097+
return@background when (activity) {
1098+
is Activity.Lightning -> activity.v1.seenAt != null
1099+
is Activity.Onchain -> activity.v1.seenAt != null
1100+
}
1101+
}
1102+
1103+
suspend fun markActivityAsSeen(activityId: String, seenAt: ULong? = null) = ServiceQueue.CORE.background {
1104+
val activity = getActivityById(activityId) ?: run {
1105+
Logger.warn("Cannot mark activity as seen - activity not found: $activityId", context = TAG)
1106+
return@background
1107+
}
1108+
1109+
val timestamp = seenAt ?: (System.currentTimeMillis().toULong() / 1000u)
1110+
val updatedActivity = when (activity) {
1111+
is Activity.Lightning -> Activity.Lightning(activity.v1.copy(seenAt = timestamp))
1112+
is Activity.Onchain -> Activity.Onchain(activity.v1.copy(seenAt = timestamp))
1113+
}
1114+
1115+
updateActivity(activityId, updatedActivity)
1116+
Logger.info("Marked activity $activityId as seen at $timestamp", context = TAG)
1117+
}
1118+
1119+
suspend fun markOnchainActivityAsSeen(txid: String, seenAt: ULong? = null) {
1120+
val activity = ServiceQueue.CORE.background {
1121+
getOnchainActivityByTxId(txid)
1122+
} ?: run {
1123+
Logger.warn("Cannot mark onchain activity as seen - activity not found for txid: $txid", context = TAG)
1124+
return
1125+
}
1126+
markActivityAsSeen(activity.id, seenAt)
1127+
}
1128+
10261129
suspend fun getBoostTxDoesExist(boostTxIds: List<String>): Map<String, Boolean> {
10271130
return ServiceQueue.CORE.background {
10281131
val doesExistMap = mutableMapOf<String, Boolean>()
@@ -1071,7 +1174,7 @@ class ActivityService(
10711174
private suspend fun findChannelForTransaction(
10721175
txid: String,
10731176
direction: PaymentDirection,
1074-
transactionDetails: TransactionDetails? = null,
1177+
transactionDetails: BitkitCoreTransactionDetails? = null,
10751178
): String? {
10761179
return when (direction) {
10771180
PaymentDirection.INBOUND -> {
@@ -1123,15 +1226,18 @@ class ActivityService(
11231226
}.getOrNull()
11241227
}
11251228

1126-
suspend fun findClosedChannelForTransaction(txid: String, transactionDetails: TransactionDetails? = null): String? {
1229+
suspend fun findClosedChannelForTransaction(
1230+
txid: String,
1231+
transactionDetails: BitkitCoreTransactionDetails? = null,
1232+
): String? {
11271233
return runCatching {
11281234
val closedChannelsList = closedChannels(SortDirection.DESC)
11291235
if (closedChannelsList.isEmpty()) {
11301236
return null
11311237
}
11321238

1133-
// Use provided transaction details if available, otherwise try node
1134-
val details = transactionDetails ?: lightningService.getTransactionDetails(txid) ?: run {
1239+
// Use provided transaction details if available, otherwise fetch from bitkitcore
1240+
val details = transactionDetails ?: fetchTransactionDetails(txid) ?: run {
11351241
Logger.warn("Transaction details not available for $txid", context = TAG)
11361242
return null
11371243
}

0 commit comments

Comments
 (0)