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 7ca2fb0c..2ebf2e17 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 @@ -46,6 +46,10 @@ class TssRepository @Inject constructor( private var messageCollectionJob: Job? = null private var sessionEventJob: Job? = null + // Pre-registered session ID for event matching (set before joinSession to avoid race condition) + // This allows session_started events to be matched even if _currentSession is not yet set + private var pendingSessionId: String? = null + /** * Get the current party ID */ @@ -161,12 +165,18 @@ class TssRepository @Inject constructor( android.util.Log.d("TssRepository", " sessionId: ${event.sessionId}") android.util.Log.d("TssRepository", " selectedParties: ${event.selectedParties}") - // Check if this event is for our active session + // Check if this event is for our active session OR pending session + // (pendingSessionId handles race condition where session_started arrives before _currentSession is set) val activeSession = _currentSession.value + val pendingId = pendingSessionId android.util.Log.d("TssRepository", " activeSession: ${activeSession?.sessionId ?: "null"}") + android.util.Log.d("TssRepository", " pendingSessionId: ${pendingId ?: "null"}") - if (activeSession != null && event.sessionId == activeSession.sessionId) { - android.util.Log.d("TssRepository", " → Event matches active session!") + val matchesActiveSession = activeSession != null && event.sessionId == activeSession.sessionId + val matchesPendingSession = pendingId != null && event.sessionId == pendingId + + if (matchesActiveSession || matchesPendingSession) { + android.util.Log.d("TssRepository", " → Event matches session! (active=$matchesActiveSession, pending=$matchesPendingSession)") when (event.eventType) { "session_started" -> { android.util.Log.d("TssRepository", " → Processing session_started event") @@ -186,10 +196,10 @@ class TssRepository @Inject constructor( } } } else { - android.util.Log.d("TssRepository", " → Event does NOT match active session (ignored)") - if (activeSession == null) { - android.util.Log.d("TssRepository", " Reason: activeSession is null") - } else { + android.util.Log.d("TssRepository", " → Event does NOT match any session (ignored)") + if (activeSession == null && pendingId == null) { + android.util.Log.d("TssRepository", " Reason: both activeSession and pendingSessionId are null") + } else if (activeSession != null) { android.util.Log.d("TssRepository", " Reason: sessionId mismatch (event: ${event.sessionId}, active: ${activeSession.sessionId})") } } @@ -290,6 +300,12 @@ class TssRepository @Inject constructor( android.util.Log.d("TssRepository", "Session created: sessionId=$sessionId, inviteCode=$inviteCode, joinToken length=${joinToken.length}") + // CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition + // This ensures session_started events can be matched even if they arrive + // before _currentSession is set + pendingSessionId = sessionId + android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching") + // Auto-join session via gRPC (matching Electron behavior) var partyIndex = 0 var sessionAlreadyInProgress = false @@ -585,6 +601,12 @@ class TssRepository @Inject constructor( try { android.util.Log.d("TssRepository", "Joining keygen session via gRPC: sessionId=$sessionId, joinToken length=${joinToken.length}") + // CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition + // This ensures session_started events can be matched even if they arrive + // before _currentSession is set + pendingSessionId = sessionId + android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (joiner)") + // Join session via gRPC (matching Electron's grpcClient.joinSession) val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken) if (joinResult.isFailure) { @@ -738,6 +760,7 @@ class TssRepository @Inject constructor( grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes) _sessionStatus.value = SessionStatus.COMPLETED + pendingSessionId = null // Clear pending session ID on completion android.util.Log.d("TssRepository", "Keygen as joiner completed: address=$address, partyIndex=$actualPartyIndex") @@ -746,6 +769,7 @@ class TssRepository @Inject constructor( } catch (e: Exception) { android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e) _sessionStatus.value = SessionStatus.FAILED + pendingSessionId = null // Clear pending session ID on failure Result.failure(e) } } @@ -783,6 +807,12 @@ class TssRepository @Inject constructor( // Note: Password is verified during actual sign execution, same as Electron + // CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition + // This ensures session_started events can be matched even if they arrive + // before _currentSession is set + pendingSessionId = sessionId + android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (sign joiner)") + // Join session via gRPC (matching Electron's grpcClient.joinSession) val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken) if (joinResult.isFailure) { @@ -906,6 +936,7 @@ class TssRepository @Inject constructor( grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes) _sessionStatus.value = SessionStatus.COMPLETED + pendingSessionId = null // Clear pending session ID on completion messageCollectionJob?.cancel() android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...") @@ -915,6 +946,7 @@ class TssRepository @Inject constructor( } catch (e: Exception) { android.util.Log.e("TssRepository", "Execute sign as joiner failed", e) _sessionStatus.value = SessionStatus.FAILED + pendingSessionId = null // Clear pending session ID on failure Result.failure(e) } } @@ -1437,6 +1469,7 @@ class TssRepository @Inject constructor( grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes) _sessionStatus.value = SessionStatus.COMPLETED + pendingSessionId = null // Clear pending session ID on completion sessionEventJob?.cancel() Result.success(shareEntity.copy(id = id).toShareRecord()) @@ -1444,6 +1477,7 @@ class TssRepository @Inject constructor( } catch (e: Exception) { android.util.Log.e("TssRepository", "Start keygen as initiator failed", e) _sessionStatus.value = SessionStatus.FAILED + pendingSessionId = null // Clear pending session ID on failure Result.failure(e) } } @@ -1458,6 +1492,9 @@ class TssRepository @Inject constructor( _currentSession.value = null _sessionStatus.value = SessionStatus.WAITING + // Clear pending session ID (race condition prevention) + pendingSessionId = null + // Clear reconnection recovery params currentMessageRoutingSessionId = null currentMessageRoutingPartyIndex = null