fix(android): 为 MainViewModel 添加 safeLaunch 异常处理 [P2]
【架构安全修复 - ViewModel 层协程异常处理】 ## 问题背景 MainViewModel 使用的 viewModelScope 没有配置 CoroutineExceptionHandler: - 未捕获的异常会导致应用崩溃 - 用户操作触发的异常体验最差 - 有 29 处 viewModelScope.launch 调用都存在风险 ## 修复方案 ### 1. 添加 safeLaunch 辅助函数 创建一个扩展函数自动捕获异常: ### 2. 替换关键的 viewModelScope.launch 将 14 个最关键的用户交互点改为使用 safeLaunch: **已修复的函数:** 1. checkAllServices() - 服务初始化检查 2. connectToServer() - 连接服务器 3. createKeygenSession() - 创建密钥生成会话 4. validateInviteCode() - 验证邀请码 5. joinKeygen() - 加入密钥生成 6. joinSign() - 加入签名 7. initiateSignSession() - 发起签名会话 8. initiateSignSessionWithOptions() - 发起签名(带选项) 9. startSigningProcess() - 启动签名过程 10. prepareTransfer() - 准备转账 11. broadcastTransaction() - 广播交易 12. exportShareBackup() - 导出备份 13. importShareBackup() - 导入备份 14. confirmTransactionInBackground() - 后台确认交易 ## 修复的崩溃场景 ### 场景 1: 网络请求失败 - 原问题: 用户点击"创建钱包"时网络异常 - 修复前: 应用直接崩溃 ❌ - 修复后: 显示"网络错误"提示,应用继续运行 ✅ ### 场景 2: 参数验证失败 - 原问题: 邀请码格式错误抛出 IllegalArgumentException - 修复前: 应用崩溃 ❌ - 修复后: 显示"参数错误"提示 ✅ ### 场景 3: 状态不一致 - 原问题: 快速切换页面导致状态异常 - 修复前: 应用崩溃,用户丢失数据 ❌ - 修复后: 显示错误提示,状态可恢复 ✅ ### 场景 4: JSON 解析失败 - 原问题: 导入损坏的备份文件 - 修复前: 应用崩溃 ❌ - 修复后: 显示"导入失败"提示 ✅ ## 双重保护机制 现在有两层保护: 1. **内层 try-catch** - 函数内部的具体业务异常处理 2. **外层 safeLaunch** - 捕获所有未处理的异常,防止崩溃 示例: ## 异常分类处理 根据异常类型提供友好的错误提示: - SocketTimeoutException → "网络超时,请检查网络连接" - UnknownHostException → "无法连接到服务器,请检查网络设置" - IOException → "网络错误: {message}" - IllegalStateException → "状态错误: {message}" - IllegalArgumentException → "参数错误: {message}" - 其他异常 → "操作失败: {message}" ## 影响范围 ### 修改的代码位置 - MainViewModel.kt - 添加 safeLaunch 函数 - 14 个关键用户交互函数 - 替换 viewModelScope.launch 为 safeLaunch ### 行为变化 - BEFORE: 协程中未捕获异常导致应用崩溃 - AFTER: 异常被捕获,显示错误提示,应用继续运行 ### 完全向后兼容 - 所有现有的 try-catch 逻辑保持不变 - 仅在异常未被捕获时才触发 safeLaunch 的处理 - 不影响正常的业务流程 ## 测试验证 编译状态: ✅ BUILD SUCCESSFUL in 29s - 无编译错误 - 仅有警告 (unused parameters),不影响功能 ## 与 TssRepository 形成完整防护 现在有两层完整的异常保护: 1. **TssRepository 层** - 后台协程的异常处理 (CoroutineExceptionHandler) 2. **MainViewModel 层** - UI 交互的异常处理 (safeLaunch) 用户操作流程: 用户点击按钮 → MainViewModel.safeLaunch (外层保护) ↓ Repository 调用 → repositoryScope (后台保护) ↓ 双重保护,极大降低崩溃风险 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
704ee523c9
commit
62b2a87e90
|
|
@ -8,9 +8,8 @@ import com.durian.tssparty.domain.model.*
|
||||||
import com.durian.tssparty.util.AddressUtils
|
import com.durian.tssparty.util.AddressUtils
|
||||||
import com.durian.tssparty.util.TransactionUtils
|
import com.durian.tssparty.util.TransactionUtils
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -18,6 +17,59 @@ class MainViewModel @Inject constructor(
|
||||||
private val repository: TssRepository
|
private val repository: TssRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全启动协程 - 自动捕获异常防止应用崩溃
|
||||||
|
*
|
||||||
|
* 【架构安全修复 - ViewModel 层异常处理】
|
||||||
|
*
|
||||||
|
* 问题背景:
|
||||||
|
* - viewModelScope.launch 没有配置 CoroutineExceptionHandler
|
||||||
|
* - 未捕获的异常会导致应用崩溃
|
||||||
|
* - 用户操作触发的异常体验最差
|
||||||
|
*
|
||||||
|
* 解决方案:
|
||||||
|
* - 提供 safeLaunch 扩展函数,自动添加 try-catch
|
||||||
|
* - 捕获所有异常并更新 UI 错误状态
|
||||||
|
* - 记录详细日志用于调试
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* safeLaunch {
|
||||||
|
* // 业务逻辑,异常会被自动捕获
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private fun safeLaunch(
|
||||||
|
onError: ((Exception) -> Unit)? = null,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// 协程取消是正常行为,重新抛出
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 捕获所有其他异常
|
||||||
|
android.util.Log.e("MainViewModel", "Caught exception in safeLaunch", e)
|
||||||
|
|
||||||
|
// 根据异常类型进行分类处理
|
||||||
|
val errorMessage = when (e) {
|
||||||
|
is java.net.SocketTimeoutException -> "网络超时,请检查网络连接"
|
||||||
|
is java.net.UnknownHostException -> "无法连接到服务器,请检查网络设置"
|
||||||
|
is java.io.IOException -> "网络错误: ${e.message}"
|
||||||
|
is IllegalStateException -> "状态错误: ${e.message}"
|
||||||
|
is IllegalArgumentException -> "参数错误: ${e.message}"
|
||||||
|
else -> "操作失败: ${e.message ?: e.javaClass.simpleName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用自定义错误处理器(如果提供)
|
||||||
|
if (onError != null) {
|
||||||
|
onError(e)
|
||||||
|
} else {
|
||||||
|
// 默认更新 UI 错误状态
|
||||||
|
_uiState.update { it.copy(isLoading = false, error = errorMessage) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// App State (similar to Zustand store)
|
// App State (similar to Zustand store)
|
||||||
private val _appState = MutableStateFlow(AppState())
|
private val _appState = MutableStateFlow(AppState())
|
||||||
val appState: StateFlow<AppState> = _appState.asStateFlow()
|
val appState: StateFlow<AppState> = _appState.asStateFlow()
|
||||||
|
|
@ -57,7 +109,12 @@ class MainViewModel @Inject constructor(
|
||||||
* Check all services for startup
|
* Check all services for startup
|
||||||
*/
|
*/
|
||||||
fun checkAllServices() {
|
fun checkAllServices() {
|
||||||
viewModelScope.launch {
|
safeLaunch(
|
||||||
|
onError = { e ->
|
||||||
|
_appState.update { it.copy(appReady = AppReadyState.ERROR) }
|
||||||
|
android.util.Log.e("MainViewModel", "Service check failed", e)
|
||||||
|
}
|
||||||
|
) {
|
||||||
_appState.update { it.copy(appReady = AppReadyState.INITIALIZING) }
|
_appState.update { it.copy(appReady = AppReadyState.INITIALIZING) }
|
||||||
|
|
||||||
var hasError = false
|
var hasError = false
|
||||||
|
|
@ -178,7 +235,7 @@ class MainViewModel @Inject constructor(
|
||||||
* Connect to Message Router server
|
* Connect to Message Router server
|
||||||
*/
|
*/
|
||||||
fun connectToServer(serverUrl: String) {
|
fun connectToServer(serverUrl: String) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
try {
|
try {
|
||||||
val parts = serverUrl.split(":")
|
val parts = serverUrl.split(":")
|
||||||
val host = parts[0]
|
val host = parts[0]
|
||||||
|
|
@ -226,7 +283,7 @@ class MainViewModel @Inject constructor(
|
||||||
* Matches Electron behavior: creates session via API, then auto-joins via gRPC
|
* Matches Electron behavior: creates session via API, then auto-joins via gRPC
|
||||||
*/
|
*/
|
||||||
fun createKeygenSession(walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) {
|
fun createKeygenSession(walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
val result = repository.createKeygenSession(walletName, thresholdT, thresholdN, participantName)
|
val result = repository.createKeygenSession(walletName, thresholdT, thresholdN, participantName)
|
||||||
|
|
@ -550,7 +607,7 @@ class MainViewModel @Inject constructor(
|
||||||
* Matches Electron's grpc:validateInviteCode - returns sessionInfo + joinToken
|
* Matches Electron's grpc:validateInviteCode - returns sessionInfo + joinToken
|
||||||
*/
|
*/
|
||||||
fun validateInviteCode(inviteCode: String) {
|
fun validateInviteCode(inviteCode: String) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
pendingInviteCode = inviteCode
|
pendingInviteCode = inviteCode
|
||||||
|
|
||||||
|
|
@ -590,16 +647,16 @@ class MainViewModel @Inject constructor(
|
||||||
* 3. Waits for session_started event to trigger keygen
|
* 3. Waits for session_started event to trigger keygen
|
||||||
*/
|
*/
|
||||||
fun joinKeygen(inviteCode: String, password: String) {
|
fun joinKeygen(inviteCode: String, password: String) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
val sessionInfo = _joinSessionInfo.value
|
val sessionInfo = _joinSessionInfo.value
|
||||||
if (sessionInfo == null) {
|
if (sessionInfo == null) {
|
||||||
_uiState.update { it.copy(error = "会话信息不完整") }
|
_uiState.update { it.copy(error = "会话信息不完整") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingJoinToken.isEmpty()) {
|
if (pendingJoinToken.isEmpty()) {
|
||||||
_uiState.update { it.copy(error = "未获取到加入令牌,请重新验证邀请码") }
|
_uiState.update { it.copy(error = "未获取到加入令牌,请重新验证邀请码") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
@ -780,16 +837,16 @@ class MainViewModel @Inject constructor(
|
||||||
* 4. Otherwise waits for session_started event to trigger sign
|
* 4. Otherwise waits for session_started event to trigger sign
|
||||||
*/
|
*/
|
||||||
fun joinSign(inviteCode: String, shareId: Long, password: String) {
|
fun joinSign(inviteCode: String, shareId: Long, password: String) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
val sessionInfo = _coSignSessionInfo.value
|
val sessionInfo = _coSignSessionInfo.value
|
||||||
if (sessionInfo == null) {
|
if (sessionInfo == null) {
|
||||||
_uiState.update { it.copy(error = "会话信息不完整") }
|
_uiState.update { it.copy(error = "会话信息不完整") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingCoSignJoinToken.isEmpty()) {
|
if (pendingCoSignJoinToken.isEmpty()) {
|
||||||
_uiState.update { it.copy(error = "未获取到加入令牌,请重新验证邀请码") }
|
_uiState.update { it.copy(error = "未获取到加入令牌,请重新验证邀请码") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
@ -1010,7 +1067,11 @@ class MainViewModel @Inject constructor(
|
||||||
fun exportShareBackup(shareId: Long, onSuccess: (String) -> Unit) {
|
fun exportShareBackup(shareId: Long, onSuccess: (String) -> Unit) {
|
||||||
android.util.Log.d("MainViewModel", "[EXPORT] ========== exportShareBackup called ==========")
|
android.util.Log.d("MainViewModel", "[EXPORT] ========== exportShareBackup called ==========")
|
||||||
android.util.Log.d("MainViewModel", "[EXPORT] shareId: $shareId")
|
android.util.Log.d("MainViewModel", "[EXPORT] shareId: $shareId")
|
||||||
viewModelScope.launch {
|
safeLaunch(
|
||||||
|
onError = { e ->
|
||||||
|
_exportResult.value = ExportImportResult(isLoading = false, error = e.message)
|
||||||
|
}
|
||||||
|
) {
|
||||||
android.util.Log.d("MainViewModel", "[EXPORT] Setting loading state...")
|
android.util.Log.d("MainViewModel", "[EXPORT] Setting loading state...")
|
||||||
_exportResult.value = ExportImportResult(isLoading = true)
|
_exportResult.value = ExportImportResult(isLoading = true)
|
||||||
|
|
||||||
|
|
@ -1043,7 +1104,11 @@ class MainViewModel @Inject constructor(
|
||||||
android.util.Log.d("MainViewModel", "[IMPORT] ========== importShareBackup called ==========")
|
android.util.Log.d("MainViewModel", "[IMPORT] ========== importShareBackup called ==========")
|
||||||
android.util.Log.d("MainViewModel", "[IMPORT] JSON length: ${backupJson.length}")
|
android.util.Log.d("MainViewModel", "[IMPORT] JSON length: ${backupJson.length}")
|
||||||
android.util.Log.d("MainViewModel", "[IMPORT] JSON preview: ${backupJson.take(100)}...")
|
android.util.Log.d("MainViewModel", "[IMPORT] JSON preview: ${backupJson.take(100)}...")
|
||||||
viewModelScope.launch {
|
safeLaunch(
|
||||||
|
onError = { e ->
|
||||||
|
_importResult.value = ExportImportResult(isLoading = false, error = e.message)
|
||||||
|
}
|
||||||
|
) {
|
||||||
android.util.Log.d("MainViewModel", "[IMPORT] Setting loading state...")
|
android.util.Log.d("MainViewModel", "[IMPORT] Setting loading state...")
|
||||||
_importResult.value = ExportImportResult(isLoading = true)
|
_importResult.value = ExportImportResult(isLoading = true)
|
||||||
|
|
||||||
|
|
@ -1295,14 +1360,14 @@ class MainViewModel @Inject constructor(
|
||||||
* Prepare a transfer transaction
|
* Prepare a transfer transaction
|
||||||
*/
|
*/
|
||||||
fun prepareTransfer(shareId: Long, toAddress: String, amount: String, tokenType: TokenType = TokenType.KAVA) {
|
fun prepareTransfer(shareId: Long, toAddress: String, amount: String, tokenType: TokenType = TokenType.KAVA) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
_transferState.update { it.copy(shareId = shareId, toAddress = toAddress, amount = amount, tokenType = tokenType) }
|
_transferState.update { it.copy(shareId = shareId, toAddress = toAddress, amount = amount, tokenType = tokenType) }
|
||||||
|
|
||||||
val share = repository.getShareById(shareId)
|
val share = repository.getShareById(shareId)
|
||||||
if (share == null) {
|
if (share == null) {
|
||||||
_uiState.update { it.copy(isLoading = false, error = "钱包不存在") }
|
_uiState.update { it.copy(isLoading = false, error = "钱包不存在") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
val rpcUrl = _settings.value.kavaRpcUrl
|
val rpcUrl = _settings.value.kavaRpcUrl
|
||||||
|
|
@ -1342,13 +1407,13 @@ class MainViewModel @Inject constructor(
|
||||||
* Signing is triggered when session_started event is received (via startSignAsInitiator)
|
* Signing is triggered when session_started event is received (via startSignAsInitiator)
|
||||||
*/
|
*/
|
||||||
fun initiateSignSession(shareId: Long, password: String, initiatorName: String = "发起者") {
|
fun initiateSignSession(shareId: Long, password: String, initiatorName: String = "发起者") {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
val tx = _preparedTx.value
|
val tx = _preparedTx.value
|
||||||
if (tx == null) {
|
if (tx == null) {
|
||||||
_uiState.update { it.copy(isLoading = false, error = "交易未准备") }
|
_uiState.update { it.copy(isLoading = false, error = "交易未准备") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = repository.createSignSession(
|
val result = repository.createSignSession(
|
||||||
|
|
@ -1422,13 +1487,13 @@ class MainViewModel @Inject constructor(
|
||||||
initiatorName: String = "发起者",
|
initiatorName: String = "发起者",
|
||||||
includeServerBackup: Boolean = false // 新增参数
|
includeServerBackup: Boolean = false // 新增参数
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
val tx = _preparedTx.value
|
val tx = _preparedTx.value
|
||||||
if (tx == null) {
|
if (tx == null) {
|
||||||
_uiState.update { it.copy(isLoading = false, error = "交易未准备") }
|
_uiState.update { it.copy(isLoading = false, error = "交易未准备") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
android.util.Log.d("MainViewModel", "[SIGN-OPTIONS] Initiating sign session with includeServerBackup=$includeServerBackup")
|
android.util.Log.d("MainViewModel", "[SIGN-OPTIONS] Initiating sign session with includeServerBackup=$includeServerBackup")
|
||||||
|
|
@ -1502,7 +1567,7 @@ class MainViewModel @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun startSigningProcess(sessionId: String, shareId: Long, password: String) {
|
private fun startSigningProcess(sessionId: String, shareId: Long, password: String) {
|
||||||
android.util.Log.d("MainViewModel", "[SIGN] startSigningProcess called: sessionId=$sessionId, shareId=$shareId")
|
android.util.Log.d("MainViewModel", "[SIGN] startSigningProcess called: sessionId=$sessionId, shareId=$shareId")
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
android.util.Log.d("MainViewModel", "[SIGN] Calling repository.startSigning...")
|
android.util.Log.d("MainViewModel", "[SIGN] Calling repository.startSigning...")
|
||||||
val startResult = repository.startSigning(sessionId, shareId, password)
|
val startResult = repository.startSigning(sessionId, shareId, password)
|
||||||
android.util.Log.d("MainViewModel", "[SIGN] repository.startSigning returned: isSuccess=${startResult.isSuccess}")
|
android.util.Log.d("MainViewModel", "[SIGN] repository.startSigning returned: isSuccess=${startResult.isSuccess}")
|
||||||
|
|
@ -1510,7 +1575,7 @@ class MainViewModel @Inject constructor(
|
||||||
if (startResult.isFailure) {
|
if (startResult.isFailure) {
|
||||||
android.util.Log.e("MainViewModel", "[SIGN] startSigning FAILED: ${startResult.exceptionOrNull()?.message}")
|
android.util.Log.e("MainViewModel", "[SIGN] startSigning FAILED: ${startResult.exceptionOrNull()?.message}")
|
||||||
_uiState.update { it.copy(error = startResult.exceptionOrNull()?.message) }
|
_uiState.update { it.copy(error = startResult.exceptionOrNull()?.message) }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for signature
|
// Wait for signature
|
||||||
|
|
@ -1538,7 +1603,7 @@ class MainViewModel @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun broadcastTransaction() {
|
fun broadcastTransaction() {
|
||||||
android.util.Log.d("MainViewModel", "[BROADCAST] broadcastTransaction() called")
|
android.util.Log.d("MainViewModel", "[BROADCAST] broadcastTransaction() called")
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
android.util.Log.d("MainViewModel", "[BROADCAST] Starting broadcast...")
|
android.util.Log.d("MainViewModel", "[BROADCAST] Starting broadcast...")
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
|
|
@ -1551,7 +1616,7 @@ class MainViewModel @Inject constructor(
|
||||||
if (tx == null || sig == null) {
|
if (tx == null || sig == null) {
|
||||||
android.util.Log.e("MainViewModel", "[BROADCAST] Missing tx or signature! tx=$tx, sig=$sig")
|
android.util.Log.e("MainViewModel", "[BROADCAST] Missing tx or signature! tx=$tx, sig=$sig")
|
||||||
_uiState.update { it.copy(isLoading = false, error = "交易或签名缺失") }
|
_uiState.update { it.copy(isLoading = false, error = "交易或签名缺失") }
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
val rpcUrl = _settings.value.kavaRpcUrl
|
val rpcUrl = _settings.value.kavaRpcUrl
|
||||||
|
|
@ -1602,7 +1667,7 @@ class MainViewModel @Inject constructor(
|
||||||
* 每 3 秒轮询一次,最多尝试 60 次(3 分钟)
|
* 每 3 秒轮询一次,最多尝试 60 次(3 分钟)
|
||||||
*/
|
*/
|
||||||
private fun confirmTransactionInBackground(txHash: String, rpcUrl: String) {
|
private fun confirmTransactionInBackground(txHash: String, rpcUrl: String) {
|
||||||
viewModelScope.launch {
|
safeLaunch {
|
||||||
android.util.Log.d("MainViewModel", "[TX-CONFIRM] Starting background confirmation for $txHash")
|
android.util.Log.d("MainViewModel", "[TX-CONFIRM] Starting background confirmation for $txHash")
|
||||||
var attempts = 0
|
var attempts = 0
|
||||||
val maxAttempts = 60
|
val maxAttempts = 60
|
||||||
|
|
@ -1616,7 +1681,7 @@ class MainViewModel @Inject constructor(
|
||||||
onSuccess = { confirmed ->
|
onSuccess = { confirmed ->
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
android.util.Log.d("MainViewModel", "[TX-CONFIRM] Transaction confirmed after $attempts attempts")
|
android.util.Log.d("MainViewModel", "[TX-CONFIRM] Transaction confirmed after $attempts attempts")
|
||||||
return@launch
|
return@safeLaunch
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue