diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 76093dbf..f7559712 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -554,7 +554,8 @@ "Bash(\"/c/Users/dong/go/bin/go1.22.10.exe\" mod tidy)", "Bash(adb devices:*)", "Bash(adb logcat:*)", - "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(android\\): add 5-minute polling timeout mechanism for keygen/sign\n\nImplements Electron''s checkAndTriggerKeygen\\(\\) polling fallback:\n- Adds polling every 2 seconds with 5-minute timeout\n- Triggers keygen/sign via synthetic session_started event on in_progress status\n- Handles gRPC stream disconnection when app goes to background\n- Shows timeout error in UI via existing error mechanism\n\nπŸ€– Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(android\\): add 5-minute polling timeout mechanism for keygen/sign\n\nImplements Electron''s checkAndTriggerKeygen\\(\\) polling fallback:\n- Adds polling every 2 seconds with 5-minute timeout\n- Triggers keygen/sign via synthetic session_started event on in_progress status\n- Handles gRPC stream disconnection when app goes to background\n- Shows timeout error in UI via existing error mechanism\n\nπŸ€– Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(go list:*)" ], "deny": [], "ask": [] 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 0ca8303e..af74fce7 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 @@ -2015,6 +2015,11 @@ class TssRepository @Inject constructor( ) } + // CRITICAL: Set pendingSessionId BEFORE any async operations to avoid race condition + // This ensures session events can be matched even if they arrive before other state is set + pendingSessionId = sessionId + android.util.Log.d("TssRepository", "[CO-SIGN] Set pendingSessionId=$sessionId for event matching (sign initiator)") + // Update session state with complete participant list // Use original keygen thresholdN (matching Electron's share.threshold_n) val session = TssSession( @@ -2074,7 +2079,8 @@ class TssRepository @Inject constructor( Result.success(SignSessionResult( sessionId = sessionId, - inviteCode = inviteCode + inviteCode = inviteCode, + sessionAlreadyInProgress = sessionAlreadyInProgress )) } catch (e: Exception) { android.util.Log.e("TssRepository", "[CO-SIGN] Create sign session failed", e) @@ -2386,7 +2392,8 @@ data class CreateKeygenSessionResult( */ data class SignSessionResult( val sessionId: String, - val inviteCode: String + val inviteCode: String, + val sessionAlreadyInProgress: Boolean = false ) /** 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 0215d073..32cac7ff 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 @@ -332,10 +332,10 @@ class MainViewModel @Inject constructor( when (event.eventType) { "session_started" -> { - // Check if this is for initiator (CreateWallet) + // Check if this is for keygen initiator (CreateWallet) val currentSessionId = _currentSessionId.value if (currentSessionId != null && event.sessionId == currentSessionId) { - android.util.Log.d("MainViewModel", "Session started event for initiator, triggering keygen") + android.util.Log.d("MainViewModel", "Session started event for keygen initiator, triggering keygen") viewModelScope.launch { startKeygenAsInitiator( sessionId = currentSessionId, @@ -359,6 +359,13 @@ class MainViewModel @Inject constructor( android.util.Log.d("MainViewModel", "Session started event for sign joiner, triggering sign") startSignAsJoiner() } + + // Check if this is for sign initiator (TransferScreen - 发衷签名) + val signSessionId = _signSessionId.value + if (signSessionId != null && event.sessionId == signSessionId) { + android.util.Log.d("MainViewModel", "Session started event for sign initiator, triggering sign") + startSignAsInitiator(event.selectedParties) + } } "party_joined", "participant_joined" -> { android.util.Log.d("MainViewModel", "Processing participant_joined event...") @@ -395,6 +402,18 @@ class MainViewModel @Inject constructor( current + newParticipant } } + + // Update participant count for sign initiator's TransferScreen (SigningScreen) + val signSessionId = _signSessionId.value + android.util.Log.d("MainViewModel", " Checking for sign initiator: signSessionId=$signSessionId, eventSessionId=${event.sessionId}") + if (signSessionId != null && event.sessionId == signSessionId) { + android.util.Log.d("MainViewModel", " β†’ Matched sign initiator session! Updating _signParticipants") + _signParticipants.update { current -> + val newParticipant = "ε‚δΈŽζ–Ή ${current.size + 1}" + android.util.Log.d("MainViewModel", " β†’ Adding participant: $newParticipant, total now: ${current.size + 1}") + current + newParticipant + } + } } "all_joined" -> { android.util.Log.d("MainViewModel", "All parties joined, starting session status polling as fallback") @@ -405,6 +424,7 @@ class MainViewModel @Inject constructor( val currentSessionId = _currentSessionId.value val joinKeygenInfo = pendingJoinKeygenInfo val joinSignInfo = pendingJoinSignInfo + val signSessionId = _signSessionId.value when { // Initiator keygen session @@ -417,11 +437,16 @@ class MainViewModel @Inject constructor( android.util.Log.d("MainViewModel", "Starting polling for joiner keygen session: ${joinKeygenInfo.sessionId}") repository.startSessionStatusPolling(joinKeygenInfo.sessionId, "keygen") } - // Sign session + // Sign joiner session joinSignInfo != null && event.sessionId == joinSignInfo.sessionId -> { - android.util.Log.d("MainViewModel", "Starting polling for sign session: ${joinSignInfo.sessionId}") + android.util.Log.d("MainViewModel", "Starting polling for sign joiner session: ${joinSignInfo.sessionId}") repository.startSessionStatusPolling(joinSignInfo.sessionId, "sign") } + // Sign initiator session + signSessionId != null && event.sessionId == signSessionId -> { + android.util.Log.d("MainViewModel", "Starting polling for sign initiator session: $signSessionId") + repository.startSessionStatusPolling(signSessionId, "sign") + } } } } @@ -1118,8 +1143,13 @@ class MainViewModel @Inject constructor( } } + // Store pending sign initiator info for when session_started event arrives + private var pendingSignInitiatorInfo: PendingSignInitiatorInfo? = null + /** - * Create sign session and start signing + * Create sign session and wait for other participants + * Matches Electron's cosign:createSession - does NOT start signing immediately + * Signing is triggered when session_started event is received (via startSignAsInitiator) */ fun initiateSignSession(shareId: Long, password: String, initiatorName: String = "发衷者") { viewModelScope.launch { @@ -1145,8 +1175,22 @@ class MainViewModel @Inject constructor( _signParticipants.value = listOf(initiatorName) _uiState.update { it.copy(isLoading = false) } - // Start signing process - startSigningProcess(sessionResult.sessionId, shareId, password) + // Store pending info for when session_started event arrives + // Matching Electron's behavior: don't start signing until session_started + pendingSignInitiatorInfo = PendingSignInitiatorInfo( + sessionId = sessionResult.sessionId, + shareId = shareId, + password = password + ) + + android.util.Log.d("MainViewModel", "Sign session created, waiting for participants. sessionId=${sessionResult.sessionId}") + + // Check if session already in_progress (all parties already joined) + // This matches Electron's immediate trigger when status === 'in_progress' + if (sessionResult.sessionAlreadyInProgress) { + android.util.Log.d("MainViewModel", "Session already in_progress, triggering sign immediately") + startSigningProcess(sessionResult.sessionId, shareId, password) + } }, onFailure = { e -> _uiState.update { it.copy(isLoading = false, error = e.message) } @@ -1155,6 +1199,21 @@ class MainViewModel @Inject constructor( } } + /** + * Start sign as initiator (called when session_started event is received) + * Matches Electron's handleCoSignStart for initiator + */ + private fun startSignAsInitiator(selectedParties: List) { + val info = pendingSignInitiatorInfo + if (info == null) { + android.util.Log.w("MainViewModel", "startSignAsInitiator called but no pending info") + return + } + + android.util.Log.d("MainViewModel", "Starting sign as initiator: sessionId=${info.sessionId}, selectedParties=$selectedParties") + startSigningProcess(info.sessionId, info.shareId, info.password) + } + /** * Start the TSS signing process */ @@ -1223,6 +1282,9 @@ class MainViewModel @Inject constructor( _signCurrentRound.value = 0 _signature.value = null _txHash.value = null + pendingSignInitiatorInfo = null + // Reset session status to WAITING for fresh start + repository.resetSessionStatus() } /** @@ -1336,3 +1398,13 @@ data class PendingJoinSignInfo( val shareId: Long, val password: String ) + +/** + * Pending sign initiator info (stored when creating sign session, waiting for session_started) + * Matches Electron's activeCoSignSession for initiator + */ +data class PendingSignInitiatorInfo( + val sessionId: String, + val shareId: Long, + val password: String +)