# 2-of-3 服务器参与选项 - 纯新增实施方案

## 目标
允许 2-of-3 MPC 用户勾选"包含服务器备份"参与签名，以便在丢失一个设备时转出资产。

## 核心设计

### 安全限制
- **仅** 2-of-3 配置显示此选项
- 其他配置（3-of-5, 4-of-7等）不显示

### 实施范围
- ✅ 只修改 Android 客户端
- ❌ **不需要**修改后端（account-service, message-router）
- ✅ 纯新增代码，现有逻辑保持不变

## 修改文件清单

### 1. TssRepository.kt（2处新增）

#### 1.1 新增辅助方法（private）
```kotlin
// 位置：3712行之前（类内部末尾）
/**
 * 构建参与方列表（新增辅助方法）
 * @param participants 所有参与方
 * @param includeServerParties 是否包含服务器方（默认 false，保持现有行为）
 */
private fun buildSigningParticipantList(
    participants: List<ParticipantStatusInfo>,
    includeServerParties: Boolean = false
): List<Pair<String, Int>> {
    val filtered = if (includeServerParties) {
        // 包含所有参与方（含服务器）
        participants
    } else {
        // 过滤掉服务器方（现有行为）
        participants.filter { !it.partyId.startsWith("co-managed-party-") }
    }
    return filtered.map { Pair(it.partyId, it.partyIndex) }
}
```

#### 1.2 新增签名会话创建方法
```kotlin
// 位置：buildSigningParticipantList 之后
/**
 * 创建签名会话（支持选择是否包含服务器）
 * @param includeServerBackup 是否包含服务器备份参与方（仅 2-of-3 时使用）
 * 新增方法，不影响现有 createSignSession
 */
suspend fun createSignSessionWithOptions(
    shareId: Long,
    messageHash: String,
    password: String,
    initiatorName: String,
    includeServerBackup: Boolean = false  // 新增参数
): Result<SignSessionResult> {
    return withContext(Dispatchers.IO) {
        try {
            val shareEntity = shareRecordDao.getShareById(shareId)
                ?: return@withContext Result.failure(Exception("Share not found"))

            val signingPartyIdForEvents = shareEntity.partyId

            android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Creating sign session with includeServerBackup=$includeServerBackup")
            ensureSessionEventSubscriptionActive(signingPartyIdForEvents)

            val keygenStatusResult = getSessionStatus(shareEntity.sessionId)
            if (keygenStatusResult.isFailure) {
                return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息: ${keygenStatusResult.exceptionOrNull()?.message}"))
            }
            val keygenStatus = keygenStatusResult.getOrThrow()

            // 使用新的辅助方法构建参与方列表
            val signingParties = buildSigningParticipantList(
                keygenStatus.participants,
                includeServerBackup
            )

            android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Signing parties: ${signingParties.size} of ${keygenStatus.participants.size} (includeServer=$includeServerBackup)")
            signingParties.forEach { (id, index) ->
                android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS]   party_id=${id.take(16)}, party_index=$index")
            }

            if (signingParties.size < shareEntity.thresholdT) {
                return@withContext Result.failure(Exception(
                    "签名参与方不足: 需要 ${shareEntity.thresholdT} 个，但只有 ${signingParties.size} 个参与方"
                ))
            }

            // 后续逻辑与 createSignSession 相同
            // ... 构建请求、创建session、加入gRPC等
            // （复用现有 createSignSession 的代码）

            // 调用现有方法的内部逻辑（需要提取）
            createSignSessionInternal(
                shareEntity,
                signingParties,
                messageHash,
                password,
                initiatorName
            )
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}
```

### 2. MainViewModel.kt（1处新增）

```kotlin
// 位置：initiateSignSession 方法之后
/**
 * 创建签名会话（支持选择服务器参与）
 * 新增方法，不影响现有 initiateSignSession
 */
fun initiateSignSessionWithOptions(
    shareId: Long,
    password: String,
    initiatorName: String = "发起者",
    includeServerBackup: Boolean = false  // 新增参数
) {
    viewModelScope.launch {
        _uiState.update { it.copy(isLoading = true, error = null) }

        val tx = _preparedTx.value
        if (tx == null) {
            _uiState.update { it.copy(isLoading = false, error = "交易未准备") }
            return@launch
        }

        android.util.Log.d("MainViewModel", "[SIGN-OPTIONS] Initiating sign session with includeServerBackup=$includeServerBackup")

        val result = repository.createSignSessionWithOptions(
            shareId = shareId,
            messageHash = tx.signHash,
            password = password,
            initiatorName = initiatorName,
            includeServerBackup = includeServerBackup  // 传递参数
        )

        result.fold(
            onSuccess = { sessionResult ->
                _signSessionId.value = sessionResult.sessionId
                _signInviteCode.value = sessionResult.inviteCode
                _signParticipants.value = listOf(initiatorName)
                _uiState.update { it.copy(isLoading = false) }

                pendingSignInitiatorInfo = PendingSignInitiatorInfo(
                    sessionId = sessionResult.sessionId,
                    shareId = shareId,
                    password = password
                )

                if (sessionResult.sessionAlreadyInProgress) {
                    startSigningProcess(sessionResult.sessionId, shareId, password)
                }
            },
            onFailure = { e ->
                _uiState.update { it.copy(isLoading = false, error = e.message) }
            }
        )
    }
}
```

### 3. TransferScreen.kt（UI 新增）

```kotlin
// 在交易确认界面新增复选框（Step 2）
// 位置：密码输入框之后

// 仅在 2-of-3 时显示
if (wallet.thresholdT == 2 && wallet.thresholdN == 3) {
    Spacer(modifier = Modifier.height(16.dp))

    var includeServerBackup by remember { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = includeServerBackup,
            onCheckedChange = { includeServerBackup = it }
        )
        Spacer(modifier = Modifier.width(8.dp))
        Column {
            Text(
                text = "包含服务器备份参与签名",
                style = MaterialTheme.typography.bodyMedium
            )
            Text(
                text = "如果您丢失了一个设备，勾选此项以使用服务器备份完成签名",
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}
```

### 4. MainActivity.kt（传递参数）

```kotlin
// 修改 TransferScreen 的 onConfirmTransaction 回调
onConfirmTransaction = { includeServer ->
    viewModel.initiateSignSessionWithOptions(
        shareId = shareId,
        password = "",
        includeServerBackup = includeServer
    )
}
```

## 测试场景

### 场景1：2-of-3 正常使用（不勾选）
- 设备A + 设备B 签名 ✅
- 服务器被过滤（现有行为）

### 场景2：2-of-3 设备丢失（勾选）
- 设备A + 服务器 签名 ✅
- 用户明确勾选"包含服务器备份"

### 场景3：3-of-5 配置
- 不显示复选框 ✅
- 保持现有行为

## 优势

1. ✅ **零后端修改**：后端只接收 parties 数组
2. ✅ **完全向后兼容**：默认行为不变
3. ✅ **安全限制**：仅 2-of-3 可用
4. ✅ **纯新增**：不修改现有方法
5. ✅ **用户明确选择**：需要主动勾选

## 实施顺序

1. TssRepository：新增辅助方法
2. TssRepository：新增 createSignSessionWithOptions
3. MainViewModel：新增 initiateSignSessionWithOptions
4. TransferScreen：新增 UI 复选框
5. MainActivity：传递参数
6. 测试编译和功能
