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 bf9abbd4..e6eaa4a5 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 @@ -3509,44 +3509,210 @@ data class ParticipantStatusInfo( * 同步所有类型的历史交易 * 首次导入钱包时调用 */ + /** + * 使用 BlockScout API 同步交易历史(更快的方式) + * 一次性获取地址的所有交易,无需分批扫描区块 + */ + suspend fun syncTransactionHistoryViaBlockScout( + shareId: Long, + address: String, + networkType: NetworkType = NetworkType.MAINNET + ): Result { + return withContext(Dispatchers.IO) { + try { + android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Syncing via BlockScout API for $address") + + val baseUrl = if (networkType == NetworkType.TESTNET) { + "https://explorer.evm-alpha.kava.io" + } else { + "https://kavascan.com" + } + + val client = okhttp3.OkHttpClient.Builder() + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .build() + + var totalSynced = 0 + var nextPageParams: String? = null + + // 代币合约地址映射 + val contractToTokenType = mapOf( + GreenPointsToken.CONTRACT_ADDRESS.lowercase() to TokenType.GREEN_POINTS, + EnergyPointsToken.CONTRACT_ADDRESS.lowercase() to TokenType.ENERGY_POINTS, + FuturePointsToken.CONTRACT_ADDRESS.lowercase() to TokenType.FUTURE_POINTS + ) + + // 分页获取所有交易 + do { + val url = if (nextPageParams != null) { + "$baseUrl/api/v2/addresses/$address/transactions?$nextPageParams" + } else { + "$baseUrl/api/v2/addresses/$address/transactions" + } + + android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Fetching: $url") + + val request = okhttp3.Request.Builder() + .url(url) + .get() + .build() + + val response = client.newCall(request).execute() + val responseBody = response.body?.string() + + if (responseBody == null || response.code != 200) { + android.util.Log.e("TssRepository", "[SYNC-BLOCKSCOUT] Request failed: code=${response.code}") + break + } + + android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Response preview: ${responseBody.take(500)}") + + val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject + val items = json.getAsJsonArray("items") + val nextPageParamsObj = json.getAsJsonObject("next_page_params") + + if (items != null) { + android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Processing ${items.size()} transactions") + + for (item in items) { + try { + val txObj = item.asJsonObject + val txHash = txObj.get("hash")?.asString ?: continue + + // 检查是否已存在 + val existing = transactionRecordDao.getRecordByTxHash(txHash) + if (existing != null) continue + + val fromAddr = txObj.get("from")?.asJsonObject?.get("hash")?.asString ?: continue + val toAddr = txObj.get("to")?.asJsonObject?.get("hash")?.asString + val valueStr = txObj.get("value")?.asString ?: "0" + val gasPrice = txObj.get("gas_price")?.asString ?: "0" + val gasUsed = txObj.get("gas_used")?.asString ?: "0" + val blockNumber = txObj.get("block")?.asLong + val timestamp = txObj.get("timestamp")?.asString + val status = txObj.get("status")?.asString + val tokenTransfers = txObj.getAsJsonArray("token_transfers") + + // 统一地址格式 + val normalizedFrom = fromAddr.lowercase() + val normalizedTo = toAddr?.lowercase() + val normalizedAddress = address.lowercase() + + // 处理代币转账 + if (tokenTransfers != null && tokenTransfers.size() > 0) { + for (transfer in tokenTransfers) { + val transferObj = transfer.asJsonObject + val tokenAddr = transferObj.get("token")?.asJsonObject?.get("address")?.asString?.lowercase() + val tokenType = contractToTokenType[tokenAddr] ?: continue + + val transferFrom = transferObj.get("from")?.asJsonObject?.get("hash")?.asString?.lowercase() + val transferTo = transferObj.get("to")?.asJsonObject?.get("hash")?.asString?.lowercase() + val transferValue = transferObj.get("total")?.asJsonObject?.get("value")?.asString ?: "0" + val decimals = transferObj.get("total")?.asJsonObject?.get("decimals")?.asInt ?: 18 + + val amount = java.math.BigInteger(transferValue) + .toBigDecimal() + .divide(java.math.BigDecimal.TEN.pow(decimals)) + .stripTrailingZeros() + .toPlainString() + + val direction = if (transferFrom == normalizedAddress) "SENT" else "RECEIVED" + + val timestampMs = try { + java.time.Instant.parse(timestamp).toEpochMilli() + } catch (e: Exception) { + System.currentTimeMillis() + } + + val entity = TransactionRecordEntity( + shareId = shareId, + fromAddress = transferFrom ?: "", + toAddress = transferTo ?: "", + amount = amount, + tokenType = tokenType.name, + txHash = txHash, + gasPrice = gasPrice, + gasUsed = gasUsed, + txFee = "", + status = if (status == "ok") "CONFIRMED" else "FAILED", + direction = direction, + blockNumber = blockNumber, + createdAt = timestampMs, + confirmedAt = timestampMs + ) + transactionRecordDao.insertRecord(entity) + totalSynced++ + } + } else if (normalizedTo != null && (normalizedFrom == normalizedAddress || normalizedTo == normalizedAddress)) { + // 原生 KAVA 转账 + val valueBig = java.math.BigInteger(valueStr.removePrefix("0x").ifEmpty { "0" }, 16) + val valueKava = valueBig.toBigDecimal() + .divide(java.math.BigDecimal("1000000000000000000")) + .stripTrailingZeros() + .toPlainString() + + if (valueBig > java.math.BigInteger.ZERO) { + val direction = if (normalizedFrom == normalizedAddress) "SENT" else "RECEIVED" + + val timestampMs = try { + java.time.Instant.parse(timestamp).toEpochMilli() + } catch (e: Exception) { + System.currentTimeMillis() + } + + val entity = TransactionRecordEntity( + shareId = shareId, + fromAddress = normalizedFrom, + toAddress = normalizedTo, + amount = valueKava, + tokenType = TokenType.KAVA.name, + txHash = txHash, + gasPrice = gasPrice, + gasUsed = gasUsed, + txFee = "", + status = if (status == "ok") "CONFIRMED" else "FAILED", + direction = direction, + blockNumber = blockNumber, + createdAt = timestampMs, + confirmedAt = timestampMs + ) + transactionRecordDao.insertRecord(entity) + totalSynced++ + } + } + } catch (e: Exception) { + android.util.Log.w("TssRepository", "[SYNC-BLOCKSCOUT] Error parsing tx: ${e.message}") + } + } + } + + // 获取下一页参数 + nextPageParams = if (nextPageParamsObj != null && !nextPageParamsObj.isJsonNull) { + nextPageParamsObj.entrySet().joinToString("&") { "${it.key}=${it.value.asString}" } + } else { + null + } + + } while (nextPageParams != null) + + android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Completed: synced $totalSynced transactions") + Result.success(totalSynced) + } catch (e: Exception) { + android.util.Log.e("TssRepository", "[SYNC-BLOCKSCOUT] Error: ${e.message}", e) + Result.failure(e) + } + } + } + suspend fun syncAllTransactionHistory( shareId: Long, address: String, rpcUrl: String, networkType: NetworkType = NetworkType.MAINNET ): Result { - return withContext(Dispatchers.IO) { - android.util.Log.d("TssRepository", "[SYNC-ALL] Starting full transaction history sync for $address") - var totalSynced = 0 - - // 同步 ERC-20 代币 - val tokenTypes = listOf( - TokenType.GREEN_POINTS to GreenPointsToken.CONTRACT_ADDRESS, - TokenType.ENERGY_POINTS to EnergyPointsToken.CONTRACT_ADDRESS, - TokenType.FUTURE_POINTS to FuturePointsToken.CONTRACT_ADDRESS - ) - - for ((tokenType, contractAddress) in tokenTypes) { - syncERC20TransactionHistory(shareId, address, contractAddress, tokenType, rpcUrl) - .onSuccess { count -> totalSynced += count } - .onFailure { e -> - android.util.Log.w("TssRepository", "[SYNC-ALL] Failed to sync ${tokenType.name}: ${e.message}") - } - } - - // 暂时跳过原生 KAVA 同步 - KavaScan API 格式需要验证 - android.util.Log.d("TssRepository", "[SYNC-ALL] Skipping native KAVA sync (KavaScan API needs verification)") - /* 暂时禁用原生交易同步 - syncNativeTransactionHistory(shareId, address, networkType) - .onSuccess { count -> totalSynced += count } - .onFailure { e -> - android.util.Log.w("TssRepository", "[SYNC-ALL] Failed to sync native KAVA: ${e.message}") - } - */ - - android.util.Log.d("TssRepository", "[SYNC-ALL] Completed: synced $totalSynced ERC-20 transactions (native KAVA sync disabled)") - Result.success(totalSynced) - } + // 使用 BlockScout API 替代慢速的 eth_getLogs 扫描 + return syncTransactionHistoryViaBlockScout(shareId, address, networkType) } }