From 656f75a4d11157c0176b477ed1c33ff1c298910c Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 26 Jan 2026 08:51:40 -0800 Subject: [PATCH] =?UTF-8?q?fix(android):=20=E4=BD=BF=E7=94=A8=20Kavascan?= =?UTF-8?q?=20Etherscan=20API=20=E5=90=8C=E6=AD=A5=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 替换之前的 BlockScout v2 API(返回404)为 Kavascan 的 Etherscan 兼容 API: - action=tokentx: 获取 ERC-20 代币交易 - action=txlist: 获取原生 KAVA 交易 优势: - 一次请求获取所有历史交易,无需分批扫描区块 - 速度快(<5秒 vs 之前的30-45秒) - API 稳定可靠 Co-Authored-By: Claude Sonnet 4.5 --- .claude/settings.local.json | 13 +- .../tssparty/data/repository/TssRepository.kt | 265 +++++++++--------- 2 files changed, 142 insertions(+), 136 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f2d0948a..bd87dc92 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -798,7 +798,18 @@ "Bash(ssh -o ProxyCommand=\"ssh -W %h:%p ceshi@103.39.231.231\" -o StrictHostKeyChecking=no ceshi@192.168.1.111 \"cat /home/ceshi/rwadurian/backend/services/mining-service/src/application/services/batch-mining.service.ts | head -250\")", "Bash(ssh -o ProxyCommand=\"ssh -W %h:%p ceshi@103.39.231.231\" -o StrictHostKeyChecking=no ceshi@192.168.1.111 \"docker logs rwa-mining-admin-service --tail 50 2>&1 | grep ''第一条数据\\\\|最后一条数据''\")", "Bash(npx xlsx-cli 挖矿.xlsx)", - "Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/mining_db?schema=public\" npx prisma migrate dev:*)" + "Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/mining_db?schema=public\" npx prisma migrate dev:*)", + "Bash(md-to-pdf:*)", + "Bash(dir \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\docs\\\\deployment\\\\*.pdf\")", + "Bash(./gradlew compileDebugKotlin:*)", + "Bash(cmd.exe /c \"cd /d c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\mpc-system\\\\services\\\\service-party-android && gradlew.bat :app:compileDebugKotlin --no-daemon\")", + "Bash(powershell -Command \"Set-Location 'c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\mpc-system\\\\services\\\\service-party-android'; .\\\\gradlew.bat :app:compileDebugKotlin --no-daemon 2>&1\":*)", + "Bash(powershell -Command \"Set-Location ''c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\mpc-system\\\\services\\\\service-party-android''; .\\\\gradlew.bat :app:compileDebugKotlin --no-daemon 2>&1 | Select-Object -Last 20\")", + "Bash(cmd.exe /c \"gradlew.bat installDebug && adb logcat -c && adb logcat | findstr /C:\"\"EXPORT\"\" /C:\"\"IMPORT\"\" /C:\"\"STATE\"\"\")", + "Bash(./gradlew:*)", + "Bash(adb shell \"run-as com.durian.tssparty sqlite3 /data/data/com.durian.tssparty/databases/tss_party.db ''SELECT id, tx_hash, from_address, to_address, amount, token_type, status, direction, created_at FROM transaction_records ORDER BY id DESC LIMIT 5;''\")", + "WebFetch(domain:docs.kava.io)", + "WebFetch(domain:kavascan.com)" ], "deny": [], "ask": [] 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 e6eaa4a5..c1d6745c 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 @@ -3510,7 +3510,7 @@ data class ParticipantStatusInfo( * 首次导入钱包时调用 */ /** - * 使用 BlockScout API 同步交易历史(更快的方式) + * 使用 Kavascan Etherscan 兼容 API 同步交易历史 * 一次性获取地址的所有交易,无需分批扫描区块 */ suspend fun syncTransactionHistoryViaBlockScout( @@ -3520,7 +3520,7 @@ data class ParticipantStatusInfo( ): Result { return withContext(Dispatchers.IO) { try { - android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Syncing via BlockScout API for $address") + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Syncing via Kavascan API for $address") val baseUrl = if (networkType == NetworkType.TESTNET) { "https://explorer.evm-alpha.kava.io" @@ -3534,7 +3534,6 @@ data class ParticipantStatusInfo( .build() var totalSynced = 0 - var nextPageParams: String? = null // 代币合约地址映射 val contractToTokenType = mapOf( @@ -3543,39 +3542,23 @@ data class ParticipantStatusInfo( 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" - } + // 1. 获取 ERC-20 代币交易 (tokentx) + val tokenUrl = "$baseUrl/api?module=account&action=tokentx&address=$address&startblock=0&endblock=99999999&sort=desc" + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Fetching token tx: $tokenUrl") - android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Fetching: $url") + val tokenRequest = okhttp3.Request.Builder().url(tokenUrl).get().build() + val tokenResponse = client.newCall(tokenRequest).execute() + val tokenBody = tokenResponse.body?.string() - val request = okhttp3.Request.Builder() - .url(url) - .get() - .build() + if (tokenBody != null && tokenResponse.code == 200) { + val tokenJson = com.google.gson.JsonParser.parseString(tokenBody).asJsonObject + val tokenStatus = tokenJson.get("status")?.asString + val tokenResult = tokenJson.getAsJsonArray("result") - val response = client.newCall(request).execute() - val responseBody = response.body?.string() + if (tokenStatus == "1" && tokenResult != null) { + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Processing ${tokenResult.size()} token transactions") - 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) { + for (item in tokenResult) { try { val txObj = item.asJsonObject val txHash = txObj.get("hash")?.asString ?: continue @@ -3584,122 +3567,134 @@ data class ParticipantStatusInfo( 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 contractAddr = txObj.get("contractAddress")?.asString?.lowercase() ?: continue + val tokenType = contractToTokenType[contractAddr] ?: continue - // 统一地址格式 - val normalizedFrom = fromAddr.lowercase() - val normalizedTo = toAddr?.lowercase() - val normalizedAddress = address.lowercase() + val fromAddr = txObj.get("from")?.asString?.lowercase() ?: continue + val toAddr = txObj.get("to")?.asString?.lowercase() ?: continue + val value = txObj.get("value")?.asString ?: "0" + val decimals = txObj.get("tokenDecimal")?.asString?.toIntOrNull() ?: 18 + val gasPrice = txObj.get("gasPrice")?.asString ?: "0" + val gasUsed = txObj.get("gasUsed")?.asString ?: "0" + val blockNumber = txObj.get("blockNumber")?.asString?.toLongOrNull() + val timestamp = txObj.get("timeStamp")?.asString?.toLongOrNull()?.times(1000) - // 处理代币转账 - 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 amount = java.math.BigInteger(value) + .toBigDecimal() + .divide(java.math.BigDecimal.TEN.pow(decimals)) + .stripTrailingZeros() + .toPlainString() - 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 direction = if (fromAddr == address.lowercase()) "SENT" else "RECEIVED" - 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++ - } - } + val entity = TransactionRecordEntity( + shareId = shareId, + fromAddress = fromAddr, + toAddress = toAddr, + amount = amount, + tokenType = tokenType.name, + txHash = txHash, + gasPrice = gasPrice, + gasUsed = gasUsed, + txFee = "", + status = "CONFIRMED", + direction = direction, + blockNumber = blockNumber, + createdAt = timestamp ?: System.currentTimeMillis(), + confirmedAt = timestamp ?: System.currentTimeMillis() + ) + transactionRecordDao.insertRecord(entity) + totalSynced++ } catch (e: Exception) { - android.util.Log.w("TssRepository", "[SYNC-BLOCKSCOUT] Error parsing tx: ${e.message}") + android.util.Log.w("TssRepository", "[SYNC-KAVASCAN] Error parsing token tx: ${e.message}") } } - } - - // 获取下一页参数 - nextPageParams = if (nextPageParamsObj != null && !nextPageParamsObj.isJsonNull) { - nextPageParamsObj.entrySet().joinToString("&") { "${it.key}=${it.value.asString}" } } else { - null + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Token tx response: status=$tokenStatus") } + } else { + android.util.Log.e("TssRepository", "[SYNC-KAVASCAN] Token tx request failed: code=${tokenResponse.code}") + } - } while (nextPageParams != null) + // 2. 获取原生 KAVA 交易 (txlist) + val txUrl = "$baseUrl/api?module=account&action=txlist&address=$address&startblock=0&endblock=99999999&sort=desc" + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Fetching native tx: $txUrl") - android.util.Log.d("TssRepository", "[SYNC-BLOCKSCOUT] Completed: synced $totalSynced transactions") + val txRequest = okhttp3.Request.Builder().url(txUrl).get().build() + val txResponse = client.newCall(txRequest).execute() + val txBody = txResponse.body?.string() + + if (txBody != null && txResponse.code == 200) { + val txJson = com.google.gson.JsonParser.parseString(txBody).asJsonObject + val txStatus = txJson.get("status")?.asString + val txResult = txJson.getAsJsonArray("result") + + if (txStatus == "1" && txResult != null) { + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Processing ${txResult.size()} native transactions") + + for (item in txResult) { + 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")?.asString?.lowercase() ?: continue + val toAddr = txObj.get("to")?.asString?.lowercase() ?: continue + val value = txObj.get("value")?.asString ?: "0" + val gasPrice = txObj.get("gasPrice")?.asString ?: "0" + val gasUsed = txObj.get("gasUsed")?.asString ?: "0" + val blockNumber = txObj.get("blockNumber")?.asString?.toLongOrNull() + val timestamp = txObj.get("timeStamp")?.asString?.toLongOrNull()?.times(1000) + val isError = txObj.get("isError")?.asString + + // 将 value 从 wei 转换为 KAVA + val amount = java.math.BigInteger(value) + .toBigDecimal() + .divide(java.math.BigDecimal("1000000000000000000")) + .stripTrailingZeros() + .toPlainString() + + // 只保存有 value 的交易 + if (java.math.BigInteger(value) > java.math.BigInteger.ZERO) { + val direction = if (fromAddr == address.lowercase()) "SENT" else "RECEIVED" + + val entity = TransactionRecordEntity( + shareId = shareId, + fromAddress = fromAddr, + toAddress = toAddr, + amount = amount, + tokenType = TokenType.KAVA.name, + txHash = txHash, + gasPrice = gasPrice, + gasUsed = gasUsed, + txFee = "", + status = if (isError == "0") "CONFIRMED" else "FAILED", + direction = direction, + blockNumber = blockNumber, + createdAt = timestamp ?: System.currentTimeMillis(), + confirmedAt = timestamp ?: System.currentTimeMillis() + ) + transactionRecordDao.insertRecord(entity) + totalSynced++ + } + } catch (e: Exception) { + android.util.Log.w("TssRepository", "[SYNC-KAVASCAN] Error parsing native tx: ${e.message}") + } + } + } else { + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Native tx response: status=$txStatus") + } + } else { + android.util.Log.e("TssRepository", "[SYNC-KAVASCAN] Native tx request failed: code=${txResponse.code}") + } + + android.util.Log.d("TssRepository", "[SYNC-KAVASCAN] Completed: synced $totalSynced transactions") Result.success(totalSynced) } catch (e: Exception) { - android.util.Log.e("TssRepository", "[SYNC-BLOCKSCOUT] Error: ${e.message}", e) + android.util.Log.e("TssRepository", "[SYNC-KAVASCAN] Error: ${e.message}", e) Result.failure(e) } }