feat: added health and api routes
This commit is contained in:
231
server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt
Normal file
231
server/src/main/kotlin/org/ccoin/routes/ApiRoutes.kt
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package org.ccoin.routes
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Route.apiRoutes() {
|
||||||
|
route("/api") {
|
||||||
|
/** Get all available API routes */
|
||||||
|
get("/routes") {
|
||||||
|
try {
|
||||||
|
val routes = mapOf(
|
||||||
|
"wallet" to mapOf(
|
||||||
|
"POST /wallet/create" to "Create a new wallet",
|
||||||
|
"GET /wallet/{address}" to "Get wallet by address",
|
||||||
|
"GET /wallet/{address}/balance" to "Get wallet balance",
|
||||||
|
"PUT /wallet/{address}/label" to "Update wallet label",
|
||||||
|
"GET /wallet/list" to "Get all wallets with pagination",
|
||||||
|
"GET /wallet/rich" to "Get wallets with minimum balance",
|
||||||
|
"GET /wallet/{address}/exists" to "Check if wallet exists"
|
||||||
|
),
|
||||||
|
"transaction" to mapOf(
|
||||||
|
"POST /transaction/send" to "Send a transaction",
|
||||||
|
"GET /transaction/{hash}" to "Get transaction by hash",
|
||||||
|
"GET /transaction/history/{address}" to "Get transaction history for address",
|
||||||
|
"GET /transaction/pending" to "Get pending transactions",
|
||||||
|
"GET /transaction/count/{address}" to "Get transaction count for address",
|
||||||
|
"GET /transaction/list" to "Get all transactions with pagination",
|
||||||
|
"GET /transaction/stats" to "Get network transaction statistics"
|
||||||
|
),
|
||||||
|
"mining" to mapOf(
|
||||||
|
"POST /mining/start" to "Start a mining job",
|
||||||
|
"POST /mining/submit" to "Submit mining result",
|
||||||
|
"GET /mining/difficulty" to "Get current mining difficulty",
|
||||||
|
"GET /mining/stats/{address}" to "Get mining statistics for miner",
|
||||||
|
"GET /mining/network" to "Get network mining statistics",
|
||||||
|
"GET /mining/pending-transactions" to "Get pending transactions for mining",
|
||||||
|
"POST /mining/validate" to "Validate mining job",
|
||||||
|
"GET /mining/leaderboard" to "Get mining leaderboard"
|
||||||
|
),
|
||||||
|
"block" to mapOf(
|
||||||
|
"GET /block/{hash}" to "Get block by hash",
|
||||||
|
"GET /block/height/{height}" to "Get block by height",
|
||||||
|
"GET /block/{hash}/exists" to "Check if block exists",
|
||||||
|
"GET /blocks/latest" to "Get latest blocks",
|
||||||
|
"GET /blocks/range" to "Get blocks in height range",
|
||||||
|
"GET /blocks/miner/{address}" to "Get blocks by miner address",
|
||||||
|
"GET /blocks/time-range" to "Get blocks by timestamp range",
|
||||||
|
"GET /blocks/difficulty/{difficulty}" to "Get blocks by difficulty",
|
||||||
|
"GET /blocks/stats" to "Get blockchain statistics"
|
||||||
|
),
|
||||||
|
"health" to mapOf(
|
||||||
|
"GET /health" to "Basic health check",
|
||||||
|
"GET /health/detailed" to "Detailed health check with system metrics",
|
||||||
|
"GET /health/database" to "Database health check",
|
||||||
|
"GET /health/blockchain" to "Blockchain health check",
|
||||||
|
"GET /ready" to "Readiness probe (Kubernetes)",
|
||||||
|
"GET /live" to "Liveness probe (Kubernetes)",
|
||||||
|
"GET /version" to "Service version and build info"
|
||||||
|
),
|
||||||
|
"api" to mapOf(
|
||||||
|
"GET /api/routes" to "Get all available API routes",
|
||||||
|
"GET /api/docs" to "Get API documentation",
|
||||||
|
"GET /api/examples" to "Get API usage examples"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val summary = mapOf(
|
||||||
|
"totalEndpoints" to routes.values.sumOf { it.size },
|
||||||
|
"categories" to routes.keys.toList(),
|
||||||
|
"baseUrl" to "http://localhost:8080",
|
||||||
|
"documentation" to "https://github.com/your-repo/ccoin-server/docs",
|
||||||
|
"version" to "1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
call.respond(mapOf(
|
||||||
|
"summary" to summary,
|
||||||
|
"routes" to routes
|
||||||
|
))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.InternalServerError, mapOf(
|
||||||
|
"error" to (e.message ?: "Failed to get routes information")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get API documentation */
|
||||||
|
get("/docs") {
|
||||||
|
try {
|
||||||
|
val documentation = mapOf(
|
||||||
|
"title" to "CCoin API Documentation",
|
||||||
|
"version" to "1.0.0",
|
||||||
|
"description" to "REST API for CCoin cryptocurrency server",
|
||||||
|
"baseUrl" to "http://localhost:8080",
|
||||||
|
"authentication" to "None required",
|
||||||
|
"contentType" to "application/json",
|
||||||
|
"rateLimit" to "100 requests per minute",
|
||||||
|
"sections" to mapOf(
|
||||||
|
"wallet" to mapOf(
|
||||||
|
"description" to "Wallet management endpoints",
|
||||||
|
"addressFormat" to "random_word:random_6_digits (e.g., phoenix:123456)"
|
||||||
|
),
|
||||||
|
"transaction" to mapOf(
|
||||||
|
"description" to "Transaction management endpoints",
|
||||||
|
"fees" to "Optional, defaults to 0.0",
|
||||||
|
"memoMaxLength" to 256
|
||||||
|
),
|
||||||
|
"mining" to mapOf(
|
||||||
|
"description" to "Mining and block creation endpoints",
|
||||||
|
"defaultDifficulty" to 4,
|
||||||
|
"defaultReward" to 50.0
|
||||||
|
),
|
||||||
|
"block" to mapOf(
|
||||||
|
"description" to "Blockchain query endpoints",
|
||||||
|
"hashFormat" to "64-character hex string"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"errorCodes" to mapOf(
|
||||||
|
"400" to "Bad Request - Invalid parameters",
|
||||||
|
"404" to "Not Found - Resource not found",
|
||||||
|
"500" to "Internal Server Error - Server error"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
call.respond(documentation)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.InternalServerError, mapOf(
|
||||||
|
"error" to (e.message ?: "Failed to get API documentation")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get API usage examples */
|
||||||
|
get("/examples") {
|
||||||
|
try {
|
||||||
|
val examples = mapOf(
|
||||||
|
"createWallet" to mapOf(
|
||||||
|
"method" to "POST",
|
||||||
|
"url" to "/wallet/create",
|
||||||
|
"body" to mapOf(
|
||||||
|
"label" to "My Wallet"
|
||||||
|
),
|
||||||
|
"response" to mapOf(
|
||||||
|
"address" to "phoenix:123456",
|
||||||
|
"balance" to 0.0,
|
||||||
|
"label" to "My Wallet",
|
||||||
|
"createdAt" to 1703097600,
|
||||||
|
"lastActivity" to null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"sendTransaction" to mapOf(
|
||||||
|
"method" to "POST",
|
||||||
|
"url" to "/transaction/send",
|
||||||
|
"body" to mapOf(
|
||||||
|
"fromAddress" to "phoenix:123456",
|
||||||
|
"toAddress" to "dragon:789012",
|
||||||
|
"amount" to 10.5,
|
||||||
|
"fee" to 0.01,
|
||||||
|
"memo" to "Payment for services"
|
||||||
|
),
|
||||||
|
"response" to mapOf(
|
||||||
|
"hash" to "abc123...",
|
||||||
|
"fromAddress" to "phoenix:123456",
|
||||||
|
"toAddress" to "dragon:789012",
|
||||||
|
"amount" to 10.5,
|
||||||
|
"fee" to 0.01,
|
||||||
|
"memo" to "Payment for services",
|
||||||
|
"timestamp" to 1703097600,
|
||||||
|
"status" to "CONFIRMED"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"startMining" to mapOf(
|
||||||
|
"method" to "POST",
|
||||||
|
"url" to "/mining/start",
|
||||||
|
"body" to mapOf(
|
||||||
|
"minerAddress" to "tiger:456789",
|
||||||
|
"difficulty" to 4
|
||||||
|
),
|
||||||
|
"response" to mapOf(
|
||||||
|
"jobId" to "job123",
|
||||||
|
"target" to "0000",
|
||||||
|
"difficulty" to 4,
|
||||||
|
"previousHash" to "def456...",
|
||||||
|
"height" to 100,
|
||||||
|
"timestamp" to 1703097600,
|
||||||
|
"expiresAt" to 1703097900
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"getBlock" to mapOf(
|
||||||
|
"method" to "GET",
|
||||||
|
"url" to "/block/abc123...",
|
||||||
|
"response" to mapOf(
|
||||||
|
"hash" to "abc123...",
|
||||||
|
"previousHash" to "def456...",
|
||||||
|
"merkleRoot" to "ghi789...",
|
||||||
|
"timestamp" to 1703097600,
|
||||||
|
"difficulty" to 4,
|
||||||
|
"nonce" to 12345,
|
||||||
|
"minerAddress" to "tiger:456789",
|
||||||
|
"reward" to 50.0,
|
||||||
|
"height" to 100,
|
||||||
|
"transactionCount" to 0,
|
||||||
|
"confirmations" to 6
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
call.respond(mapOf(
|
||||||
|
"title" to "CCoin API Examples",
|
||||||
|
"description" to "Common usage examples for the CCoin API",
|
||||||
|
"examples" to examples,
|
||||||
|
"curlExamples" to mapOf(
|
||||||
|
"createWallet" to "curl -X POST http://localhost:8080/wallet/create -H 'Content-Type: application/json' -d '{\"label\":\"My Wallet\"}'",
|
||||||
|
"getBalance" to "curl http://localhost:8080/wallet/phoenix:123456/balance",
|
||||||
|
"sendTransaction" to "curl -X POST http://localhost:8080/transaction/send -H 'Content-Type: application/json' -d '{\"fromAddress\":\"phoenix:123456\",\"toAddress\":\"dragon:789012\",\"amount\":10.5}'",
|
||||||
|
"getLatestBlocks" to "curl http://localhost:8080/blocks/latest?limit=5"
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.InternalServerError, mapOf(
|
||||||
|
"error" to (e.message ?: "Failed to get API examples")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package org.ccoin.routes
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import org.ccoin.config.DatabaseConfig
|
||||||
|
import org.ccoin.config.ServerConfig
|
||||||
|
import org.ccoin.models.DatabaseHealth
|
||||||
|
import org.ccoin.models.BlockchainHealth
|
||||||
|
import org.ccoin.models.HealthResponse
|
||||||
|
import org.ccoin.services.BlockService
|
||||||
|
import org.ccoin.services.TransactionService
|
||||||
|
import org.ccoin.services.WalletService
|
||||||
|
import org.ccoin.services.MiningService
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.lang.management.ManagementFactory
|
||||||
|
|
||||||
|
fun Route.healthRoutes() {
|
||||||
|
/** Basic health check */
|
||||||
|
get("/health") {
|
||||||
|
try {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Check database connectivity
|
||||||
|
val dbHealth = checkDatabaseHealth()
|
||||||
|
|
||||||
|
// Check blockchain health
|
||||||
|
val blockchainHealth = checkBlockchainHealth()
|
||||||
|
|
||||||
|
val uptime = ManagementFactory.getRuntimeMXBean().uptime
|
||||||
|
|
||||||
|
val health = HealthResponse(
|
||||||
|
status = if (dbHealth.connected) "healthy" else "unhealthy",
|
||||||
|
version = "1.0.0",
|
||||||
|
uptime = uptime,
|
||||||
|
database = dbHealth,
|
||||||
|
blockchain = blockchainHealth
|
||||||
|
)
|
||||||
|
|
||||||
|
val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable
|
||||||
|
call.respond(statusCode, health)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||||
|
"status" to "unhealthy",
|
||||||
|
"error" to (e.message ?: "Health check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detailed health check */
|
||||||
|
get("/health/detailed") {
|
||||||
|
try {
|
||||||
|
val dbHealth = checkDatabaseHealth()
|
||||||
|
val blockchainHealth = checkBlockchainHealth()
|
||||||
|
|
||||||
|
// Additional checks
|
||||||
|
val memoryUsage = getMemoryUsage()
|
||||||
|
val diskSpace = getDiskSpace()
|
||||||
|
val networkStats = getNetworkStats()
|
||||||
|
|
||||||
|
call.respond(mapOf(
|
||||||
|
"status" to if (dbHealth.connected) "healthy" else "unhealthy",
|
||||||
|
"version" to "1.0.0",
|
||||||
|
"uptime" to ManagementFactory.getRuntimeMXBean().uptime,
|
||||||
|
"timestamp" to System.currentTimeMillis(),
|
||||||
|
"database" to dbHealth,
|
||||||
|
"blockchain" to blockchainHealth,
|
||||||
|
"system" to mapOf(
|
||||||
|
"memory" to memoryUsage,
|
||||||
|
"diskSpace" to diskSpace
|
||||||
|
),
|
||||||
|
"network" to networkStats,
|
||||||
|
"config" to ServerConfig.getServerInfo()
|
||||||
|
))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||||
|
"status" to "unhealthy",
|
||||||
|
"error" to (e.message ?: "Detailed health check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Database health check */
|
||||||
|
get("/health/database") {
|
||||||
|
try {
|
||||||
|
val dbHealth = checkDatabaseHealth()
|
||||||
|
val statusCode = if (dbHealth.connected) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable
|
||||||
|
call.respond(statusCode, dbHealth)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||||
|
"connected" to false,
|
||||||
|
"error" to (e.message ?: "Database health check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blockchain health check */
|
||||||
|
get("/health/blockchain") {
|
||||||
|
try {
|
||||||
|
val blockchainHealth = checkBlockchainHealth()
|
||||||
|
call.respond(blockchainHealth)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||||
|
"error" to (e.message ?: "Blockchain health check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Readiness probe (for Kubernetes) */
|
||||||
|
get("/ready") {
|
||||||
|
try {
|
||||||
|
val dbHealth = checkDatabaseHealth()
|
||||||
|
|
||||||
|
if (dbHealth.connected) {
|
||||||
|
call.respond(HttpStatusCode.OK, mapOf("status" to "ready"))
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf("status" to "not ready"))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||||
|
"status" to "not ready",
|
||||||
|
"error" to (e.message ?: "Readiness check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Liveness probe (for Kubernetes) */
|
||||||
|
get("/live") {
|
||||||
|
try {
|
||||||
|
// Simple liveness check - just return OK if the service is running
|
||||||
|
call.respond(HttpStatusCode.OK, mapOf("status" to "alive"))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.ServiceUnavailable, mapOf(
|
||||||
|
"status" to "dead",
|
||||||
|
"error" to (e.message ?: "Liveness check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Service version and build info */
|
||||||
|
get("/version") {
|
||||||
|
try {
|
||||||
|
call.respond(mapOf(
|
||||||
|
"version" to "1.0.0",
|
||||||
|
"buildTime" to System.getProperty("build.time", "unknown"),
|
||||||
|
"gitCommit" to System.getProperty("git.commit", "unknown"),
|
||||||
|
"javaVersion" to System.getProperty("java.version"),
|
||||||
|
"kotlinVersion" to "2.2.21"
|
||||||
|
))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(HttpStatusCode.InternalServerError, mapOf(
|
||||||
|
"error" to (e.message ?: "Version check failed")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check database health */
|
||||||
|
private fun checkDatabaseHealth(): DatabaseHealth {
|
||||||
|
return try {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
// Simple query to test connectivity
|
||||||
|
WalletService.getTotalWalletCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseTime = System.currentTimeMillis() - startTime
|
||||||
|
|
||||||
|
DatabaseHealth(
|
||||||
|
connected = true,
|
||||||
|
responseTime = responseTime,
|
||||||
|
activeConnections = 0, // Would need HikariCP integration to get real numbers
|
||||||
|
maxConnections = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DatabaseHealth(
|
||||||
|
connected = false,
|
||||||
|
responseTime = -1,
|
||||||
|
activeConnections = 0,
|
||||||
|
maxConnections = 20
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check blockchain health */
|
||||||
|
private fun checkBlockchainHealth(): BlockchainHealth {
|
||||||
|
return try {
|
||||||
|
val latestBlock = BlockService.getLatestBlockHeight()
|
||||||
|
val pendingTransactions = TransactionService.getPendingTransactions().size
|
||||||
|
val networkHashRate = MiningService.getNetworkHashRate()
|
||||||
|
val averageBlockTime = BlockService.getAverageBlockTime()
|
||||||
|
|
||||||
|
BlockchainHealth(
|
||||||
|
latestBlock = latestBlock,
|
||||||
|
pendingTransactions = pendingTransactions,
|
||||||
|
networkHashRate = networkHashRate,
|
||||||
|
averageBlockTime = averageBlockTime
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
BlockchainHealth(
|
||||||
|
latestBlock = 0,
|
||||||
|
pendingTransactions = 0,
|
||||||
|
networkHashRate = 0.0,
|
||||||
|
averageBlockTime = 0L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get memory usage information */
|
||||||
|
private fun getMemoryUsage(): Map<String, Any> {
|
||||||
|
val runtime = Runtime.getRuntime()
|
||||||
|
val maxMemory = runtime.maxMemory()
|
||||||
|
val totalMemory = runtime.totalMemory()
|
||||||
|
val freeMemory = runtime.freeMemory()
|
||||||
|
val usedMemory = totalMemory - freeMemory
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"maxMemory" to maxMemory,
|
||||||
|
"totalMemory" to totalMemory,
|
||||||
|
"usedMemory" to usedMemory,
|
||||||
|
"freeMemory" to freeMemory,
|
||||||
|
"usagePercentage" to ((usedMemory.toDouble() / maxMemory) * 100).toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get disk space information */
|
||||||
|
private fun getDiskSpace(): Map<String, Any> {
|
||||||
|
return try {
|
||||||
|
val file = java.io.File(".")
|
||||||
|
val totalSpace = file.totalSpace
|
||||||
|
val freeSpace = file.freeSpace
|
||||||
|
val usedSpace = totalSpace - freeSpace
|
||||||
|
|
||||||
|
mapOf(
|
||||||
|
"totalSpace" to totalSpace,
|
||||||
|
"freeSpace" to freeSpace,
|
||||||
|
"usedSpace" to usedSpace,
|
||||||
|
"usagePercentage" to ((usedSpace.toDouble() / totalSpace) * 100).toInt()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mapOf("error" to "Unable to get disk space information")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get network statistics */
|
||||||
|
private fun getNetworkStats(): Map<String, Any> {
|
||||||
|
return try {
|
||||||
|
val totalBlocks = BlockService.getTotalBlockCount()
|
||||||
|
val totalTransactions = TransactionService.getTotalTransactionCount()
|
||||||
|
val totalWallets = WalletService.getTotalWalletCount()
|
||||||
|
val totalSupply = WalletService.getTotalSupply()
|
||||||
|
|
||||||
|
mapOf(
|
||||||
|
"totalBlocks" to totalBlocks,
|
||||||
|
"totalTransactions" to totalTransactions,
|
||||||
|
"totalWallets" to totalWallets,
|
||||||
|
"totalSupply" to totalSupply
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mapOf("error" to "Unable to get network statistics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user