fix(android): sign initiator event handling to match Electron flow

Changes:
- Add sign initiator handling for participant_joined events in MainViewModel
- Add sign initiator handling for session_started events in MainViewModel
- Add sign initiator handling for all_joined events in MainViewModel
- Set pendingSessionId in TssRepository.createSignSession for event matching
- Refactor initiateSignSession to wait for session_started instead of starting immediately
- Add PendingSignInitiatorInfo data class to store pending sign info
- Add sessionAlreadyInProgress flag to SignSessionResult for immediate trigger case

This fixes the issue where sign initiator couldn't detect when other parties
joined the signing session, making Android flow 100% consistent with Electron.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-01 20:14:32 -08:00
parent fd56de5c00
commit 16e1e9159c
3 changed files with 90 additions and 10 deletions

View File

@ -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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(go list:*)"
],
"deny": [],
"ask": []

View File

@ -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
)
/**

View File

@ -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<String>) {
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
)