fix(android): co-sign flow consistency with Electron + state reset
Changes: - Fix Android state not resetting after successful keygen/join - Add resetSessionStatus() method in TssRepository - Call reset on success navigation in MainActivity - Make Android co-sign flow 100% consistent with Electron: - Get keygen session status for participants list - Filter out co-managed-party-* (server backup parties) - Auto-join via gRPC after creating sign session - Start message routing BEFORE signing (prepareForSign) - Use gRPC response partyIndex instead of local share - Use original keygen thresholdN instead of signingParties.size - Pass parties list in join sign flow - Update SignSessionInfoResponse to include parties array - Update validateSignInviteCode to parse parties from API response 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7346b3518a
commit
04eeadf7a7
|
|
@ -108,6 +108,11 @@ fun TssPartyApp(
|
||||||
navController.navigate(BottomNavItem.Wallets.route) {
|
navController.navigate(BottomNavItem.Wallets.route) {
|
||||||
popUpTo(BottomNavItem.Wallets.route) { inclusive = true }
|
popUpTo(BottomNavItem.Wallets.route) { inclusive = true }
|
||||||
}
|
}
|
||||||
|
// Reset all session states so next time user enters a fresh state
|
||||||
|
viewModel.resetSessionState()
|
||||||
|
viewModel.resetJoinKeygenState()
|
||||||
|
viewModel.resetCoSignState()
|
||||||
|
viewModel.resetTransferState()
|
||||||
viewModel.clearSuccess()
|
viewModel.clearSuccess()
|
||||||
viewModel.clearCreatedInviteCode()
|
viewModel.clearCreatedInviteCode()
|
||||||
}
|
}
|
||||||
|
|
@ -201,8 +206,11 @@ fun TssPartyApp(
|
||||||
onPrepareTransaction = { toAddress, amount ->
|
onPrepareTransaction = { toAddress, amount ->
|
||||||
viewModel.prepareTransfer(shareId, toAddress, amount)
|
viewModel.prepareTransfer(shareId, toAddress, amount)
|
||||||
},
|
},
|
||||||
onConfirmTransaction = { password ->
|
onConfirmTransaction = {
|
||||||
viewModel.initiateSignSession(shareId, password)
|
viewModel.initiateSignSession(shareId, "")
|
||||||
|
},
|
||||||
|
onScanQrCode = {
|
||||||
|
// TODO: Launch QR scanner for recipient address
|
||||||
},
|
},
|
||||||
onCopyInviteCode = {
|
onCopyInviteCode = {
|
||||||
signInviteCode?.let { onCopyToClipboard(it) }
|
signInviteCode?.let { onCopyToClipboard(it) }
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,20 @@ class TssRepository @Inject constructor(
|
||||||
val joinedCount = json.get("joined_count")?.asInt ?: 0
|
val joinedCount = json.get("joined_count")?.asInt ?: 0
|
||||||
val joinToken = json.get("join_token")?.asString ?: ""
|
val joinToken = json.get("join_token")?.asString ?: ""
|
||||||
|
|
||||||
|
// Parse parties array (matching Electron's sessionInfo.parties)
|
||||||
|
val partiesArray = json.get("parties")?.asJsonArray
|
||||||
|
val parties = mutableListOf<Participant>()
|
||||||
|
partiesArray?.forEach { elem ->
|
||||||
|
val p = elem.asJsonObject
|
||||||
|
parties.add(Participant(
|
||||||
|
partyId = p.get("party_id")?.asString ?: "",
|
||||||
|
partyIndex = p.get("party_index")?.asInt ?: 0,
|
||||||
|
name = "参与方 ${(p.get("party_index")?.asInt ?: 0) + 1}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] validateSignInviteCode: found ${parties.size} parties")
|
||||||
|
|
||||||
val signSessionInfo = SignSessionInfoResponse(
|
val signSessionInfo = SignSessionInfoResponse(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
keygenSessionId = keygenSessionId,
|
keygenSessionId = keygenSessionId,
|
||||||
|
|
@ -596,7 +610,8 @@ class TssRepository @Inject constructor(
|
||||||
messageHash = messageHash,
|
messageHash = messageHash,
|
||||||
thresholdT = thresholdT,
|
thresholdT = thresholdT,
|
||||||
thresholdN = thresholdN,
|
thresholdN = thresholdN,
|
||||||
currentParticipants = joinedCount
|
currentParticipants = joinedCount,
|
||||||
|
parties = parties
|
||||||
)
|
)
|
||||||
|
|
||||||
Result.success(ValidateSignInviteCodeResult(
|
Result.success(ValidateSignInviteCodeResult(
|
||||||
|
|
@ -851,9 +866,11 @@ class TssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionData = joinResult.getOrThrow()
|
val sessionData = joinResult.getOrThrow()
|
||||||
val myPartyIndex = shareEntity.partyIndex // Use party index from our share
|
// Use party_index from gRPC response (matching Electron's result.party_index)
|
||||||
|
// This is more authoritative than local share's partyIndex
|
||||||
|
val myPartyIndex = sessionData.partyIndex
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "gRPC sign join successful: partyIndex=$myPartyIndex, sessionStatus=${sessionData.sessionStatus}")
|
android.util.Log.d("TssRepository", "gRPC sign join successful: partyIndex=$myPartyIndex (from gRPC), localPartyIndex=${shareEntity.partyIndex}, sessionStatus=${sessionData.sessionStatus}")
|
||||||
|
|
||||||
// Build participants list (matching Electron's logic)
|
// Build participants list (matching Electron's logic)
|
||||||
// Prefer using parties from validateInviteCode (complete list)
|
// Prefer using parties from validateInviteCode (complete list)
|
||||||
|
|
@ -1530,6 +1547,14 @@ class TssRepository @Inject constructor(
|
||||||
currentMessageRoutingPartyIndex = null
|
currentMessageRoutingPartyIndex = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset session status to WAITING
|
||||||
|
* Called when navigating away from session screens to ensure fresh state
|
||||||
|
*/
|
||||||
|
fun resetSessionStatus() {
|
||||||
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a share record
|
* Delete a share record
|
||||||
*/
|
*/
|
||||||
|
|
@ -1605,7 +1630,13 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a sign session for a transaction (as initiator)
|
* Create a sign session for a transaction (as initiator)
|
||||||
* Calls account-service API: POST /api/v1/co-managed/sign
|
* Matches Electron's cosign:createSession flow exactly:
|
||||||
|
* 1. Get keygen session status to get all participants with party_index
|
||||||
|
* 2. Filter out co-managed-party-* (server backup parties)
|
||||||
|
* 3. Create sign session with filtered signing parties
|
||||||
|
* 4. Auto-join the session via gRPC
|
||||||
|
* 5. Subscribe to message stream (prepareForSign)
|
||||||
|
* 6. Check if session already in_progress and trigger sign immediately
|
||||||
*/
|
*/
|
||||||
suspend fun createSignSession(
|
suspend fun createSignSession(
|
||||||
shareId: Long,
|
shareId: Long,
|
||||||
|
|
@ -1619,20 +1650,54 @@ class TssRepository @Inject constructor(
|
||||||
val shareEntity = shareRecordDao.getShareById(shareId)
|
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||||
?: return@withContext Result.failure(Exception("Share not found"))
|
?: return@withContext Result.failure(Exception("Share not found"))
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] Creating sign session for share: ${shareEntity.sessionId}")
|
||||||
|
|
||||||
|
// Step 1: Get keygen session status to get all participants with party_index
|
||||||
|
// This matches Electron's: accountClient.getSessionStatus(share.session_id)
|
||||||
|
val keygenStatusResult = getSessionStatus(shareEntity.sessionId)
|
||||||
|
if (keygenStatusResult.isFailure) {
|
||||||
|
return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息: ${keygenStatusResult.exceptionOrNull()?.message}"))
|
||||||
|
}
|
||||||
|
val keygenStatus = keygenStatusResult.getOrThrow()
|
||||||
|
|
||||||
|
if (keygenStatus.participants.isEmpty()) {
|
||||||
|
return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Filter out co-managed-party-* (server backup parties)
|
||||||
|
// Only user-held signing parties should participate in signing
|
||||||
|
// This matches Electron's: filter(p => !p.party_id.startsWith('co-managed-party-'))
|
||||||
|
val signingParties = keygenStatus.participants
|
||||||
|
.filter { !it.partyId.startsWith("co-managed-party-") }
|
||||||
|
.map { Pair(it.partyId, it.partyIndex) }
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] Signing parties (excluding co-managed-party): ${signingParties.size} of ${keygenStatus.participants.size}")
|
||||||
|
signingParties.forEach { (id, index) ->
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] party_id=${id.take(16)}, party_index=$index")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signingParties.size < shareEntity.thresholdT) {
|
||||||
|
return@withContext Result.failure(Exception(
|
||||||
|
"签名参与方不足: 需要 ${shareEntity.thresholdT} 个,但只有 ${signingParties.size} 个用户方"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
// Build parties array - include initiator party info
|
// Build parties array with all signing parties (not just initiator)
|
||||||
val partiesArray = com.google.gson.JsonArray().apply {
|
val partiesArray = com.google.gson.JsonArray().apply {
|
||||||
add(com.google.gson.JsonObject().apply {
|
signingParties.forEach { (partyIdStr, partyIndex) ->
|
||||||
addProperty("party_id", partyId)
|
add(com.google.gson.JsonObject().apply {
|
||||||
addProperty("party_index", shareEntity.partyIndex)
|
addProperty("party_id", partyIdStr)
|
||||||
})
|
addProperty("party_index", partyIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build request body matching account-service API
|
// Build request body matching account-service API
|
||||||
val requestBody = com.google.gson.JsonObject().apply {
|
val requestBody = com.google.gson.JsonObject().apply {
|
||||||
addProperty("keygen_session_id", shareEntity.sessionId)
|
addProperty("keygen_session_id", shareEntity.sessionId)
|
||||||
addProperty("wallet_name", "Wallet") // Use a default name or could be passed as parameter
|
addProperty("wallet_name", shareEntity.walletName)
|
||||||
addProperty("message_hash", messageHash)
|
addProperty("message_hash", messageHash)
|
||||||
add("parties", partiesArray)
|
add("parties", partiesArray)
|
||||||
addProperty("threshold_t", shareEntity.thresholdT)
|
addProperty("threshold_t", shareEntity.thresholdT)
|
||||||
|
|
@ -1645,13 +1710,13 @@ class TssRepository @Inject constructor(
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "Creating sign session: $requestBody")
|
android.util.Log.d("TssRepository", "[CO-SIGN] Creating sign session: $requestBody")
|
||||||
|
|
||||||
val response = httpClient.newCall(request).execute()
|
val response = httpClient.newCall(request).execute()
|
||||||
val responseBody = response.body?.string()
|
val responseBody = response.body?.string()
|
||||||
?: return@withContext Result.failure(Exception("空响应"))
|
?: return@withContext Result.failure(Exception("空响应"))
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "Create sign session response: $responseBody")
|
android.util.Log.d("TssRepository", "[CO-SIGN] Create sign session response: $responseBody")
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorJson = try {
|
val errorJson = try {
|
||||||
|
|
@ -1669,13 +1734,31 @@ class TssRepository @Inject constructor(
|
||||||
val inviteCode = json.get("invite_code").asString
|
val inviteCode = json.get("invite_code").asString
|
||||||
val thresholdT = json.get("threshold_t")?.asInt ?: shareEntity.thresholdT
|
val thresholdT = json.get("threshold_t")?.asInt ?: shareEntity.thresholdT
|
||||||
|
|
||||||
// Update session state
|
// Get join token - support both new format (join_tokens map) and old format (join_token string)
|
||||||
|
val joinToken = if (json.has("join_tokens") && json.get("join_tokens").isJsonObject) {
|
||||||
|
val joinTokens = json.getAsJsonObject("join_tokens")
|
||||||
|
joinTokens.get(partyId)?.asString ?: joinTokens.get("*")?.asString
|
||||||
|
} else {
|
||||||
|
json.get("join_token")?.asString
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build participants list from signingParties
|
||||||
|
val participants = signingParties.map { (pId, pIndex) ->
|
||||||
|
Participant(
|
||||||
|
partyId = pId,
|
||||||
|
partyIndex = pIndex,
|
||||||
|
name = if (pId == partyId) initiatorName else "参与方 ${pIndex + 1}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session state with complete participant list
|
||||||
|
// Use original keygen thresholdN (matching Electron's share.threshold_n)
|
||||||
val session = TssSession(
|
val session = TssSession(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
sessionType = SessionType.SIGN,
|
sessionType = SessionType.SIGN,
|
||||||
thresholdT = thresholdT,
|
thresholdT = thresholdT,
|
||||||
thresholdN = shareEntity.thresholdN,
|
thresholdN = shareEntity.thresholdN, // Use original keygen N (matching Electron)
|
||||||
participants = listOf(Participant(partyId, shareEntity.partyIndex, initiatorName)),
|
participants = participants,
|
||||||
status = SessionStatus.WAITING,
|
status = SessionStatus.WAITING,
|
||||||
inviteCode = inviteCode,
|
inviteCode = inviteCode,
|
||||||
messageHash = messageHash
|
messageHash = messageHash
|
||||||
|
|
@ -1683,12 +1766,54 @@ class TssRepository @Inject constructor(
|
||||||
_currentSession.value = session
|
_currentSession.value = session
|
||||||
_sessionStatus.value = SessionStatus.WAITING
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
|
||||||
|
// Step 4: Initiator auto-join via gRPC (matching Electron behavior)
|
||||||
|
var sessionAlreadyInProgress = false
|
||||||
|
if (joinToken != null) {
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] Initiator auto-joining session...")
|
||||||
|
val myPartyIndex = signingParties.find { it.first == partyId }?.second ?: shareEntity.partyIndex
|
||||||
|
|
||||||
|
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
|
if (joinResult.isSuccess) {
|
||||||
|
val joinData = joinResult.getOrThrow()
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] Initiator joined: partyIndex=${joinData.partyIndex}, status=${joinData.sessionStatus}")
|
||||||
|
|
||||||
|
// Step 5: Start message routing (prepareForSign) BEFORE sign starts
|
||||||
|
startMessageRouting(sessionId, myPartyIndex)
|
||||||
|
|
||||||
|
// Step 6: Check if session already in_progress
|
||||||
|
if (joinData.sessionStatus == "in_progress") {
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] Session already in_progress, will trigger sign immediately")
|
||||||
|
sessionAlreadyInProgress = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("TssRepository", "[CO-SIGN] Initiator failed to join: ${joinResult.exceptionOrNull()?.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("TssRepository", "[CO-SIGN] No join token found for initiator")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session is already in_progress, notify callback immediately
|
||||||
|
if (sessionAlreadyInProgress) {
|
||||||
|
val selectedParties = signingParties.map { it.first }
|
||||||
|
val eventData = SessionEventData(
|
||||||
|
eventId = "immediate_sign_start",
|
||||||
|
eventType = "session_started",
|
||||||
|
sessionId = sessionId,
|
||||||
|
thresholdN = shareEntity.thresholdN, // Use original keygen N (matching Electron)
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
selectedParties = selectedParties,
|
||||||
|
joinTokens = emptyMap(),
|
||||||
|
messageHash = messageHash
|
||||||
|
)
|
||||||
|
sessionEventCallback?.invoke(eventData)
|
||||||
|
}
|
||||||
|
|
||||||
Result.success(SignSessionResult(
|
Result.success(SignSessionResult(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
inviteCode = inviteCode
|
inviteCode = inviteCode
|
||||||
))
|
))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("TssRepository", "Create sign session failed", e)
|
android.util.Log.e("TssRepository", "[CO-SIGN] Create sign session failed", e)
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1696,6 +1821,7 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the signing process after all parties have joined
|
* Start the signing process after all parties have joined
|
||||||
|
* Note: Message routing should already be started in createSignSession (after auto-join)
|
||||||
*/
|
*/
|
||||||
suspend fun startSigning(
|
suspend fun startSigning(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
|
|
@ -1710,6 +1836,8 @@ class TssRepository @Inject constructor(
|
||||||
val shareEntity = shareRecordDao.getShareById(shareId)
|
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||||
?: return@withContext Result.failure(Exception("Share not found"))
|
?: return@withContext Result.failure(Exception("Share not found"))
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: participants=${session.participants.size}")
|
||||||
|
|
||||||
// Start TSS sign
|
// Start TSS sign
|
||||||
val startResult = tssNativeBridge.startSign(
|
val startResult = tssNativeBridge.startSign(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
@ -1729,8 +1857,11 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
_sessionStatus.value = SessionStatus.IN_PROGRESS
|
_sessionStatus.value = SessionStatus.IN_PROGRESS
|
||||||
|
|
||||||
// Start message routing
|
// Note: Message routing is already started in createSignSession after auto-join
|
||||||
startMessageRouting(sessionId, shareEntity.partyIndex)
|
// Only start if not already running (for backward compatibility with old flow)
|
||||||
|
if (messageCollectionJob == null || messageCollectionJob?.isActive != true) {
|
||||||
|
startMessageRouting(sessionId, shareEntity.partyIndex)
|
||||||
|
}
|
||||||
|
|
||||||
// Mark ready
|
// Mark ready
|
||||||
grpcClient.markPartyReady(sessionId, partyId)
|
grpcClient.markPartyReady(sessionId, partyId)
|
||||||
|
|
@ -2027,7 +2158,8 @@ data class SignSessionInfoResponse(
|
||||||
val messageHash: String,
|
val messageHash: String,
|
||||||
val thresholdT: Int,
|
val thresholdT: Int,
|
||||||
val thresholdN: Int,
|
val thresholdN: Int,
|
||||||
val currentParticipants: Int
|
val currentParticipants: Int,
|
||||||
|
val parties: List<Participant> = emptyList() // Complete parties list from API
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.durian.tssparty.presentation.screens
|
package com.durian.tssparty.presentation.screens
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
|
@ -17,8 +20,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -60,16 +61,15 @@ fun TransferScreen(
|
||||||
networkType: NetworkType = NetworkType.MAINNET,
|
networkType: NetworkType = NetworkType.MAINNET,
|
||||||
rpcUrl: String = "https://evm.kava.io",
|
rpcUrl: String = "https://evm.kava.io",
|
||||||
onPrepareTransaction: (toAddress: String, amount: String) -> Unit,
|
onPrepareTransaction: (toAddress: String, amount: String) -> Unit,
|
||||||
onConfirmTransaction: (password: String) -> Unit,
|
onConfirmTransaction: () -> Unit,
|
||||||
onCopyInviteCode: () -> Unit,
|
onCopyInviteCode: () -> Unit,
|
||||||
onBroadcastTransaction: () -> Unit,
|
onBroadcastTransaction: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onBackToWallets: () -> Unit
|
onBackToWallets: () -> Unit,
|
||||||
|
onScanQrCode: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var toAddress by remember { mutableStateOf("") }
|
var toAddress by remember { mutableStateOf("") }
|
||||||
var amount by remember { mutableStateOf("") }
|
var amount by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
var showPassword by remember { mutableStateOf(false) }
|
|
||||||
var validationError by remember { mutableStateOf<String?>(null) }
|
var validationError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
// Determine current step
|
// Determine current step
|
||||||
|
|
@ -131,7 +131,8 @@ fun TransferScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancel = onCancel
|
onCancel = onCancel,
|
||||||
|
onScanQrCode = onScanQrCode
|
||||||
)
|
)
|
||||||
|
|
||||||
"preparing" -> PreparingScreen()
|
"preparing" -> PreparingScreen()
|
||||||
|
|
@ -141,18 +142,10 @@ fun TransferScreen(
|
||||||
preparedTx = preparedTx!!,
|
preparedTx = preparedTx!!,
|
||||||
toAddress = toAddress,
|
toAddress = toAddress,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
password = password,
|
|
||||||
onPasswordChange = { password = it },
|
|
||||||
showPassword = showPassword,
|
|
||||||
onTogglePassword = { showPassword = !showPassword },
|
|
||||||
error = error,
|
error = error,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
if (password.isBlank()) {
|
validationError = null
|
||||||
validationError = "请输入密码"
|
onConfirmTransaction()
|
||||||
} else {
|
|
||||||
validationError = null
|
|
||||||
onConfirmTransaction(password)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onBack = onCancel
|
onBack = onCancel
|
||||||
)
|
)
|
||||||
|
|
@ -201,7 +194,8 @@ private fun TransferInputScreen(
|
||||||
error: String?,
|
error: String?,
|
||||||
rpcUrl: String,
|
rpcUrl: String,
|
||||||
onSubmit: () -> Unit,
|
onSubmit: () -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit,
|
||||||
|
onScanQrCode: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var isCalculatingMax by remember { mutableStateOf(false) }
|
var isCalculatingMax by remember { mutableStateOf(false) }
|
||||||
|
|
@ -251,16 +245,25 @@ private fun TransferInputScreen(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Recipient address
|
// Recipient address with QR scan button
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = toAddress,
|
value = toAddress,
|
||||||
onValueChange = onToAddressChange,
|
onValueChange = onToAddressChange,
|
||||||
label = { Text("收款地址") },
|
label = { Text("收款地址") },
|
||||||
placeholder = { Text("0x...") },
|
placeholder = { Text("0x... 或点击扫码") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(Icons.Default.Person, contentDescription = null)
|
Icon(Icons.Default.Person, contentDescription = null)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = onScanQrCode) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.QrCodeScanner,
|
||||||
|
contentDescription = "扫描二维码",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -414,10 +417,6 @@ private fun TransferConfirmScreen(
|
||||||
preparedTx: TransactionUtils.PreparedTransaction,
|
preparedTx: TransactionUtils.PreparedTransaction,
|
||||||
toAddress: String,
|
toAddress: String,
|
||||||
amount: String,
|
amount: String,
|
||||||
password: String,
|
|
||||||
onPasswordChange: (String) -> Unit,
|
|
||||||
showPassword: Boolean,
|
|
||||||
onTogglePassword: () -> Unit,
|
|
||||||
error: String?,
|
error: String?,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
|
|
@ -501,30 +500,6 @@ private fun TransferConfirmScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Password input
|
|
||||||
OutlinedTextField(
|
|
||||||
value = password,
|
|
||||||
onValueChange = onPasswordChange,
|
|
||||||
label = { Text("钱包密码") },
|
|
||||||
placeholder = { Text("输入密码解锁密钥份额") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.Lock, contentDescription = null)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = onTogglePassword) {
|
|
||||||
Icon(
|
|
||||||
if (showPassword) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error display
|
// Error display
|
||||||
error?.let {
|
error?.let {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,8 @@ class MainViewModel @Inject constructor(
|
||||||
_publicKey.value = null
|
_publicKey.value = null
|
||||||
_createdInviteCode.value = null
|
_createdInviteCode.value = null
|
||||||
_hasEnteredSession.value = false
|
_hasEnteredSession.value = false
|
||||||
|
// Reset session status to WAITING for fresh start
|
||||||
|
repository.resetSessionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Join Keygen State ==========
|
// ========== Join Keygen State ==========
|
||||||
|
|
@ -612,6 +614,8 @@ class MainViewModel @Inject constructor(
|
||||||
pendingJoinToken = ""
|
pendingJoinToken = ""
|
||||||
pendingPassword = ""
|
pendingPassword = ""
|
||||||
pendingJoinKeygenInfo = null
|
pendingJoinKeygenInfo = null
|
||||||
|
// Reset session status to WAITING for fresh start
|
||||||
|
repository.resetSessionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== CoSign (Join Sign) State ==========
|
// ========== CoSign (Join Sign) State ==========
|
||||||
|
|
@ -635,7 +639,7 @@ class MainViewModel @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate sign session invite code
|
* Validate sign session invite code
|
||||||
* Matches Electron's cosign:validateInviteCode - returns sessionInfo + joinToken
|
* Matches Electron's cosign:validateInviteCode - returns sessionInfo + joinToken + parties
|
||||||
*/
|
*/
|
||||||
fun validateSignInviteCode(inviteCode: String) {
|
fun validateSignInviteCode(inviteCode: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -657,11 +661,12 @@ class MainViewModel @Inject constructor(
|
||||||
messageHash = info.messageHash,
|
messageHash = info.messageHash,
|
||||||
thresholdT = info.thresholdT,
|
thresholdT = info.thresholdT,
|
||||||
thresholdN = info.thresholdN,
|
thresholdN = info.thresholdN,
|
||||||
currentParticipants = info.currentParticipants
|
currentParticipants = info.currentParticipants,
|
||||||
|
parties = info.parties // Include complete parties list
|
||||||
)
|
)
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|
||||||
android.util.Log.d("MainViewModel", "Validate sign success: sessionId=${info.sessionId}, joinToken length=${pendingCoSignJoinToken.length}")
|
android.util.Log.d("MainViewModel", "Validate sign success: sessionId=${info.sessionId}, parties=${info.parties.size}, joinToken length=${pendingCoSignJoinToken.length}")
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
|
|
@ -693,9 +698,10 @@ class MainViewModel @Inject constructor(
|
||||||
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
android.util.Log.d("MainViewModel", "Joining sign session: sessionId=${sessionInfo.sessionId}, joinToken length=${pendingCoSignJoinToken.length}")
|
android.util.Log.d("MainViewModel", "Joining sign session: sessionId=${sessionInfo.sessionId}, parties=${sessionInfo.parties.size}, joinToken length=${pendingCoSignJoinToken.length}")
|
||||||
|
|
||||||
// Join session via gRPC (matching Electron's cosign:joinSession)
|
// Join session via gRPC (matching Electron's cosign:joinSession)
|
||||||
|
// Use complete parties list from validateInviteCode (matching Electron's behavior)
|
||||||
val result = repository.joinSignSessionViaGrpc(
|
val result = repository.joinSignSessionViaGrpc(
|
||||||
sessionId = sessionInfo.sessionId,
|
sessionId = sessionInfo.sessionId,
|
||||||
joinToken = pendingCoSignJoinToken,
|
joinToken = pendingCoSignJoinToken,
|
||||||
|
|
@ -705,7 +711,7 @@ class MainViewModel @Inject constructor(
|
||||||
messageHash = sessionInfo.messageHash,
|
messageHash = sessionInfo.messageHash,
|
||||||
thresholdT = sessionInfo.thresholdT,
|
thresholdT = sessionInfo.thresholdT,
|
||||||
thresholdN = sessionInfo.thresholdN,
|
thresholdN = sessionInfo.thresholdN,
|
||||||
parties = emptyList() // Will use other_parties from gRPC response
|
parties = sessionInfo.parties // Use complete parties list from validateInviteCode
|
||||||
)
|
)
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
|
|
@ -793,6 +799,8 @@ class MainViewModel @Inject constructor(
|
||||||
pendingCoSignInviteCode = ""
|
pendingCoSignInviteCode = ""
|
||||||
pendingCoSignJoinToken = ""
|
pendingCoSignJoinToken = ""
|
||||||
pendingJoinSignInfo = null
|
pendingJoinSignInfo = null
|
||||||
|
// Reset session status to WAITING for fresh start
|
||||||
|
repository.resetSessionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1212,7 +1220,8 @@ data class CoSignSessionInfo(
|
||||||
val messageHash: String,
|
val messageHash: String,
|
||||||
val thresholdT: Int,
|
val thresholdT: Int,
|
||||||
val thresholdN: Int,
|
val thresholdN: Int,
|
||||||
val currentParticipants: Int
|
val currentParticipants: Int,
|
||||||
|
val parties: List<com.durian.tssparty.data.repository.Participant> = emptyList() // Complete parties list from API
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue