diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt index 924c238c..0ca8303e 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt @@ -67,6 +67,13 @@ class TssRepository @Inject constructor( // Called every second with remaining seconds for UI countdown display private var countdownTickCallback: ((Long) -> Unit)? = null + // Progress callback (set by ViewModel) + // Called when TSS protocol progress updates (round/totalRounds) + private var progressCallback: ((Int, Int) -> Unit)? = null + + // Job for collecting progress from native bridge + private var progressCollectionJob: Job? = null + // Repository-level CoroutineScope for background tasks // Uses SupervisorJob so individual task failures don't cancel other tasks private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -294,6 +301,45 @@ class TssRepository @Inject constructor( countdownTickCallback = callback } + /** + * Set progress callback (called by ViewModel) + * This callback is invoked when the TSS protocol progresses through rounds + * @param callback receives (currentRound, totalRounds) + */ + fun setProgressCallback(callback: (Int, Int) -> Unit) { + progressCallback = callback + } + + /** + * Start collecting progress from native bridge + * Called when keygen/sign session starts + */ + private fun startProgressCollection() { + // Cancel any existing progress collection + progressCollectionJob?.cancel() + + android.util.Log.d("TssRepository", "[PROGRESS] Starting progress collection from native bridge") + + progressCollectionJob = repositoryScope.launch { + tssNativeBridge.progress.collect { (round, totalRounds) -> + android.util.Log.d("TssRepository", "[PROGRESS] Round $round / $totalRounds") + withContext(Dispatchers.Main) { + progressCallback?.invoke(round, totalRounds) + } + } + } + } + + /** + * Stop collecting progress from native bridge + * Called when session ends or is cancelled + */ + private fun stopProgressCollection() { + progressCollectionJob?.cancel() + progressCollectionJob = null + android.util.Log.d("TssRepository", "[PROGRESS] Progress collection stopped") + } + /** * Start polling session status as fallback mechanism * This matches Electron's checkAndTriggerKeygen() polling behavior: @@ -961,6 +1007,9 @@ class TssRepository @Inject constructor( return@coroutineScope Result.failure(startResult.exceptionOrNull()!!) } + // Start collecting progress from native bridge + startProgressCollection() + _sessionStatus.value = SessionStatus.IN_PROGRESS // Mark ready @@ -969,6 +1018,7 @@ class TssRepository @Inject constructor( // Wait for keygen result val keygenResult = tssNativeBridge.waitForKeygenResult(password) if (keygenResult.isFailure) { + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!) } @@ -994,6 +1044,7 @@ class TssRepository @Inject constructor( // Report completion grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes) + stopProgressCollection() _sessionStatus.value = SessionStatus.COMPLETED pendingSessionId = null // Clear pending session ID on completion @@ -1003,6 +1054,7 @@ class TssRepository @Inject constructor( } catch (e: Exception) { android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e) + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED pendingSessionId = null // Clear pending session ID on failure Result.failure(e) @@ -1156,12 +1208,16 @@ class TssRepository @Inject constructor( return@coroutineScope Result.failure(startResult.exceptionOrNull()!!) } + // Start collecting progress from native bridge + startProgressCollection() + // Mark ready grpcClient.markPartyReady(sessionId, partyId) // Wait for sign result val signResult = tssNativeBridge.waitForSignResult() if (signResult.isFailure) { + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED return@coroutineScope Result.failure(signResult.exceptionOrNull()!!) } @@ -1172,6 +1228,7 @@ class TssRepository @Inject constructor( val signatureBytes = android.util.Base64.decode(result.signature, android.util.Base64.NO_WRAP) grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes) + stopProgressCollection() _sessionStatus.value = SessionStatus.COMPLETED pendingSessionId = null // Clear pending session ID on completion messageCollectionJob?.cancel() @@ -1182,6 +1239,7 @@ class TssRepository @Inject constructor( } catch (e: Exception) { android.util.Log.e("TssRepository", "Execute sign as joiner failed", e) + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED pendingSessionId = null // Clear pending session ID on failure Result.failure(e) @@ -1672,6 +1730,9 @@ class TssRepository @Inject constructor( return@coroutineScope Result.failure(startResult.exceptionOrNull()!!) } + // Start collecting progress from native bridge + startProgressCollection() + _sessionStatus.value = SessionStatus.IN_PROGRESS // Mark ready @@ -1680,6 +1741,7 @@ class TssRepository @Inject constructor( // Wait for keygen result val keygenResult = tssNativeBridge.waitForKeygenResult(password) if (keygenResult.isFailure) { + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!) } @@ -1705,6 +1767,7 @@ class TssRepository @Inject constructor( // Report completion grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes) + stopProgressCollection() _sessionStatus.value = SessionStatus.COMPLETED pendingSessionId = null // Clear pending session ID on completion sessionEventJob?.cancel() @@ -1713,6 +1776,7 @@ class TssRepository @Inject constructor( } catch (e: Exception) { android.util.Log.e("TssRepository", "Start keygen as initiator failed", e) + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED pendingSessionId = null // Clear pending session ID on failure Result.failure(e) @@ -2055,6 +2119,9 @@ class TssRepository @Inject constructor( return@withContext Result.failure(startResult.exceptionOrNull()!!) } + // Start collecting progress from native bridge + startProgressCollection() + _sessionStatus.value = SessionStatus.IN_PROGRESS // Note: Message routing is already started in createSignSession after auto-join @@ -2068,6 +2135,7 @@ class TssRepository @Inject constructor( Result.success(Unit) } catch (e: Exception) { + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED Result.failure(e) } @@ -2082,6 +2150,7 @@ class TssRepository @Inject constructor( try { val signResult = tssNativeBridge.waitForSignResult() if (signResult.isFailure) { + stopProgressCollection() _sessionStatus.value = SessionStatus.FAILED return@withContext Result.failure(signResult.exceptionOrNull()!!) } @@ -2095,6 +2164,7 @@ class TssRepository @Inject constructor( grpcClient.reportCompletion(session.sessionId, partyId, signature = signatureBytes) } + stopProgressCollection() _sessionStatus.value = SessionStatus.COMPLETED messageCollectionJob?.cancel() diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt index a995bf91..0215d073 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt @@ -298,6 +298,30 @@ class MainViewModel @Inject constructor( _uiState.update { it.copy(countdownSeconds = remainingSeconds) } } + // Setup progress callback for real-time round updates from native TSS bridge + repository.setProgressCallback { round, totalRounds -> + android.util.Log.d("MainViewModel", "Progress update: $round / $totalRounds") + // Update the appropriate round state based on which session type is active + when { + // Initiator keygen (CreateWallet) + _currentSessionId.value != null && pendingJoinKeygenInfo == null && pendingJoinSignInfo == null -> { + _currentRound.value = round + } + // Joiner keygen (JoinKeygen) + pendingJoinKeygenInfo != null -> { + _joinKeygenRound.value = round + } + // Joiner sign (CoSign/参与签名) + pendingJoinSignInfo != null -> { + _coSignRound.value = round + } + // Initiator sign (Transfer) + _signSessionId.value != null -> { + _signCurrentRound.value = round + } + } + } + repository.setSessionEventCallback { event -> android.util.Log.d("MainViewModel", "=== MainViewModel received session event ===") android.util.Log.d("MainViewModel", " eventType: ${event.eventType}")