@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
44import androidx.lifecycle.viewModelScope
55import com.synonym.bitkitcore.Activity
66import com.synonym.bitkitcore.ActivityFilter
7+ import com.synonym.bitkitcore.OnchainActivity
78import com.synonym.bitkitcore.PaymentType
89import dagger.hilt.android.lifecycle.HiltViewModel
910import kotlinx.coroutines.flow.MutableSharedFlow
@@ -260,90 +261,161 @@ class BoostTransactionViewModel @Inject constructor(
260261 }
261262 }
262263
263- /* *RBF: Update the current activity with boost data -> when the new transaction is created, se it as boosting and
264- * delete the old one
265- * CPFP: Just update the current activity*/
264+ /* *
265+ * Updates activity based on boost type:
266+ * - RBF: Updates current activity with boost data, then replaces with new transaction
267+ * - CPFP: Simply updates the current activity
268+ */
266269 private suspend fun updateActivity (newTxId : Txid , isRBF : Boolean ): Result <Unit > {
267- Logger .debug(" Updating activity for txId: $newTxId . isRBF:$isRBF " , context = TAG )
270+ Logger .debug(" Updating activity for txId: $newTxId . isRBF: $isRBF " , context = TAG )
268271
269- val currentActivity = activity?.v1 ? : return Result .failure(Exception (" Activity required" ))
272+ val currentActivity = activity?.v1
273+ ? : return Result .failure(Exception (" Activity required" ))
270274
271- // For CPFP, just update the activity
275+ return if (isRBF) {
276+ handleRBFUpdate(newTxId, currentActivity)
277+ } else {
278+ handleCPFPUpdate(currentActivity)
279+ }
280+ }
281+
282+ /* *
283+ * Handles CPFP (Child Pays For Parent) update by simply updating the current activity
284+ */
285+ private suspend fun handleCPFPUpdate (currentActivity : OnchainActivity ): Result <Unit > {
272286 val updatedActivity = Activity .Onchain (
273287 v1 = currentActivity.copy(
274288 isBoosted = true ,
275- feeRate = _uiState .value.feeRate,
276- fee = _uiState .value.totalFeeSats,
277289 updatedAt = nowTimestamp().toEpochMilli().toULong()
278290 )
279291 )
280292
281- val updateResult = activityRepo.updateActivity(
293+ return activityRepo.updateActivity(
282294 id = updatedActivity.v1.id,
283295 activity = updatedActivity
284296 )
297+ }
285298
286- return if (! isRBF) {
287- updateResult
288- } else {
289- // For RBF, update new activity and delete old one
290- activityRepo.findActivityByPaymentId(
291- paymentHashOrTxId = newTxId,
292- type = ActivityFilter .ONCHAIN ,
293- txType = PaymentType .SENT
294- ).fold(
295- onSuccess = { newActivity ->
296- Logger .debug(" Activity found: $newActivity " , context = TAG )
297-
298- val newOnChainActivity = newActivity as ? Activity .Onchain
299- ? : return Result .failure(Exception (" Activity is not onchain type" ))
300-
301- val updatedActivity = Activity .Onchain (
302- v1 = newOnChainActivity.v1.copy(
303- isBoosted = true ,
304- feeRate = _uiState .value.feeRate,
305- fee = _uiState .value.totalFeeSats,
306- updatedAt = nowTimestamp().toEpochMilli().toULong()
307- )
308- )
309-
310- activityRepo.replaceActivity(
311- id = updatedActivity.v1.id,
312- activityIdToDelete = activity?.v1?.id.orEmpty(),
313- activity = updatedActivity,
314- ).onFailure {
315- activityRepo.addActivityToPendingBoost(
316- PendingBoostActivity (
317- txId = newTxId,
318- feeRate = _uiState .value.feeRate,
319- fee = _uiState .value.totalFeeSats,
320- updatedAt = nowTimestamp().toEpochMilli().toULong(),
321- activityToDelete = activity?.v1?.id
322- )
323- )
324- }
325- },
326- onFailure = { error ->
327- Logger .error(
328- " Activity $newTxId not found. Caching data to try again on next sync" ,
329- e = error,
330- context = TAG
331- )
332- activityRepo.addActivityToPendingBoost(
333- PendingBoostActivity (
334- txId = newTxId,
335- feeRate = _uiState .value.feeRate,
336- fee = _uiState .value.totalFeeSats,
337- updatedAt = nowTimestamp().toEpochMilli().toULong(),
338- activityToDelete = activity?.v1?.id.takeIf { isRBF }
339- )
340- )
341- Result .failure(error)
342- }
299+ /* *
300+ * Handles RBF (Replace By Fee) update by updating current activity and replacing with new one
301+ */
302+ private suspend fun handleRBFUpdate (
303+ newTxId : Txid ,
304+ currentActivity : OnchainActivity ,
305+ ): Result <Unit > {
306+ // First update the current activity to show boost status
307+ val updatedCurrentActivity = Activity .Onchain (
308+ v1 = currentActivity.copy(
309+ isBoosted = true ,
310+ feeRate = _uiState .value.feeRate,
311+ updatedAt = nowTimestamp().toEpochMilli().toULong()
343312 )
313+ )
314+
315+ activityRepo.updateActivity(
316+ id = updatedCurrentActivity.v1.id,
317+ activity = updatedCurrentActivity
318+ )
319+
320+ // Then find and replace with the new activity
321+ return findAndReplaceWithNewActivity(newTxId, currentActivity.id)
322+ }
323+
324+ /* *
325+ * Creates a boosted version of the given activity with current fee data
326+ */
327+ private fun createBoostedActivity (currentActivity : OnchainActivity ): Activity .Onchain {
328+ return Activity .Onchain (
329+ v1 = currentActivity.copy(
330+ isBoosted = true ,
331+ feeRate = _uiState .value.feeRate,
332+ updatedAt = nowTimestamp().toEpochMilli().toULong()
333+ )
334+ )
335+ }
336+
337+ /* *
338+ * Finds the new activity and replaces the old one, handling failures gracefully
339+ */
340+ private suspend fun findAndReplaceWithNewActivity (
341+ newTxId : Txid ,
342+ oldActivityId : String ,
343+ ): Result <Unit > {
344+ return activityRepo.findActivityByPaymentId(
345+ paymentHashOrTxId = newTxId,
346+ type = ActivityFilter .ONCHAIN ,
347+ txType = PaymentType .SENT
348+ ).fold(
349+ onSuccess = { newActivity ->
350+ replaceActivityWithNewOne(newActivity, oldActivityId, newTxId)
351+ },
352+ onFailure = { error ->
353+ handleActivityNotFound(error, newTxId, oldActivityId)
354+ }
355+ )
356+ }
357+
358+ /* *
359+ * Replaces the old activity with the new boosted one
360+ */
361+ private suspend fun replaceActivityWithNewOne (
362+ newActivity : Activity ,
363+ oldActivityId : String ,
364+ newTxId : Txid ,
365+ ): Result <Unit > {
366+ Logger .debug(" Activity found: $newActivity " , context = TAG )
367+
368+ val newOnChainActivity = newActivity as ? Activity .Onchain
369+ ? : return Result .failure(Exception (" Activity is not onchain type" ))
370+
371+ val updatedNewActivity = Activity .Onchain (
372+ v1 = newOnChainActivity.v1.copy(
373+ isBoosted = true ,
374+ feeRate = _uiState .value.feeRate,
375+ updatedAt = nowTimestamp().toEpochMilli().toULong()
376+ )
377+ )
378+
379+ return activityRepo.replaceActivity(
380+ id = updatedNewActivity.v1.id,
381+ activityIdToDelete = oldActivityId,
382+ activity = updatedNewActivity,
383+ ).onFailure {
384+ cachePendingBoostActivity(newTxId, oldActivityId)
344385 }
345386 }
346387
388+ /* *
389+ * Handles the case when new activity is not found by caching for later retry
390+ */
391+ private suspend fun handleActivityNotFound (
392+ error : Throwable ,
393+ newTxId : Txid ,
394+ oldActivityId : String? ,
395+ ): Result <Unit > {
396+ Logger .error(
397+ " Activity $newTxId not found. Caching data to try again on next sync" ,
398+ e = error,
399+ context = TAG
400+ )
401+
402+ cachePendingBoostActivity(newTxId, oldActivityId)
403+ return Result .failure(error)
404+ }
405+
406+ /* *
407+ * Caches activity data for pending boost operation
408+ */
409+ private suspend fun cachePendingBoostActivity (newTxId : Txid , activityToDelete : String? ) {
410+ activityRepo.addActivityToPendingBoost(
411+ PendingBoostActivity (
412+ txId = newTxId,
413+ updatedAt = nowTimestamp().toEpochMilli().toULong(),
414+ activityToDelete = activityToDelete
415+ )
416+ )
417+ }
418+
347419 private fun handleError (message : String , error : Throwable ? = null) {
348420 Logger .error(message, error, context = TAG )
349421 _uiState .update {
0 commit comments