feat: added rest of services

This commit is contained in:
darwincereska
2025-12-18 09:22:54 -05:00
parent 6d360df21d
commit 1c8fe77a43
4 changed files with 428 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configuration-cache=true
# Application
ccoin.version=1.0.0

View File

@@ -25,7 +25,7 @@ data class BlockResponse(
val timestamp: Long,
val difficulty: Int,
val nonce: Long,
val minerAddress: String
val minerAddress: String,
val reward: Double,
val height: Int,
val transactionCount: Int,
@@ -51,3 +51,11 @@ data class MiningStatsResponse(
val lastBlockMined: Long?,
val currentDifficulty: Int
)
@Serializable
data class BlockRangeResponse(
val blocks: List<BlockResponse>,
val totalCount: Int,
val fromHeight: Int,
val toHeight: Int
)

View File

@@ -0,0 +1,218 @@
package org.ccoin.services
import org.ccoin.database.Tables
import org.ccoin.models.BlockResponse
import org.ccoin.models.BlockRangeResponse
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object BlockService {
/** Gets block by hash */
fun getBlock(hash: String): BlockResponse? = transaction {
Tables.Blocks.selectAll().where { Tables.Blocks.hash eq hash }
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}.singleOrNull()
}
/** Gets block by height */
fun getBlockByHeight(height: Int): BlockResponse? = transaction {
Tables.Blocks.selectAll().where { Tables.Blocks.height eq height }
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}.singleOrNull()
}
/** Gets latest blocks */
fun getLatestBlocks(limit: Int = 10): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(limit)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
/** Gets blocks in height range */
fun getBlocksInRange(fromHeight: Int, toHeight: Int): BlockRangeResponse = transaction {
val blocks = Tables.Blocks.selectAll()
.where { Tables.Blocks.height.between(fromHeight, toHeight) }
.orderBy(Tables.Blocks.height, SortOrder.ASC)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
BlockRangeResponse(blocks, blocks.size, fromHeight, toHeight)
}
/** Gets blocks mined by specific address */
fun getBlocksByMiner(minerAddress: String, limit: Int = 50, offset: Int = 0): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.where { Tables.Blocks.minerAddress eq minerAddress }
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(limit)
.offset(offset.toLong())
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
/** Gets total block count */
fun getTotalBlockCount(): Long = transaction {
Tables.Blocks.selectAll().count()
}
/** Gets latest block height */
fun getLatestBlockHeight(): Int = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.height] }
.singleOrNull() ?: 0
}
/** Gets latest block hash */
fun getLatestBlockHash(): String = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.hash] }
.singleOrNull() ?: "0".repeat(64)
}
/** Checks if block exists */
fun blockExists(hash: String): Boolean = transaction {
Tables.Blocks.selectAll().where { Tables.Blocks.hash eq hash }.count() > 0
}
/** Gets blocks by timestamp range */
fun getBlocksByTimeRange(fromTime: Long, toTime: Long): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.where { Tables.Blocks.timestamp.between(fromTime, toTime) }
.orderBy(Tables.Blocks.timestamp, SortOrder.ASC)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
/** Updates block confirmations */
fun updateBlockConfirmations(hash: String, confirmations: Int): Boolean = transaction {
val updated = Tables.Blocks.update({ Tables.Blocks.hash eq hash }) {
it[Tables.Blocks.confirmations] = confirmations
}
updated > 0
}
/** Gets average block time over last N blocks */
fun getAverageBlockTime(blockCount: Int = 100): Long = transaction {
val blocks = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
.limit(blockCount)
.map { it[Tables.Blocks.timestamp] }
if (blocks.size < 2) return@transaction 0L
val timeDiffs = blocks.zipWithNext { newer, older -> newer - older }
timeDiffs.average().toLong()
}
/** Gets total rewards distributed */
fun getTotalRewardsDistributed(): Double = transaction {
Tables.Blocks.select(Tables.Blocks.reward.sum())
.single()[Tables.Blocks.reward.sum()]?.toDouble() ?: 0.0
}
/** Gets blocks with specific difficulty */
fun getBlocksByDifficulty(difficulty: Int): List<BlockResponse> = transaction {
Tables.Blocks.selectAll()
.where { Tables.Blocks.difficulty eq difficulty }
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.map {
BlockResponse(
it[Tables.Blocks.hash],
it[Tables.Blocks.previousHash],
it[Tables.Blocks.merkleRoot],
it[Tables.Blocks.timestamp],
it[Tables.Blocks.difficulty],
it[Tables.Blocks.nonce],
it[Tables.Blocks.minerAddress],
it[Tables.Blocks.reward].toDouble(),
it[Tables.Blocks.height],
it[Tables.Blocks.transactionCount],
it[Tables.Blocks.confirmations]
)
}
}
}

View File

@@ -0,0 +1,200 @@
package org.ccoin.services
import org.ccoin.config.ServerConfig
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.utils.CryptoUtils
import org.ccoin.utils.HashUtils
object ValidationService {
/** Validates wallet address format */
fun validateWalletAddress(address: String): Boolean = CryptoUtils.isValidAddress(address)
/** Validates transaction amount */
fun validateTransactionAmount(amount: Double): Boolean = amount > 0 && amount <= Double.MAX_VALUE && !amount.isNaN() && !amount.isInfinite()
/** Validates transaction fee */
fun validateTransactionFee(fee: Double): Boolean = fee >= 0 && fee <= Double.MAX_VALUE && !fee.isNaN() && !fee.isInfinite()
/** Validates memo length */
fun validateMemo(memo: String?): Boolean = memo == null || memo.length <= ServerConfig.maxMemoLength
/** Validates transaction hash format */
fun validateTransactionHash(hash: String): Boolean = HashUtils.isValidSha256Hash(hash)
/** Validates block hash format */
fun validateBlockHash(hash: String): Boolean = HashUtils.isValidSha256Hash(hash)
/** Validates mining difficulty */
fun validateMiningDifficulty(difficulty: Int): Boolean = difficulty in 1..32
/** Validates mining nonce */
fun validateMiningNonce(nonce: Long): Boolean = nonce >= 0
/** Validates timestamp */
fun validateTimestamp(timestamp: Long): Boolean {
val now = System.currentTimeMillis() / 1000
val oneHourAgo = now - 3600
val oneHourFromNow = now + 3600
return timestamp in oneHourAgo..oneHourFromNow
}
/** Validates wallet label */
fun validateWalletLabel(label: String?): Boolean = label == null || (label.isNotBlank() && label.length <= 255)
/** Validates page number for pagination */
fun validatePageNumber(page: Int): Boolean = page >= 1
/** Validates page size for pagination */
fun validatePageSize(pageSize: Int): Boolean = pageSize in 1..ServerConfig.maxPageSize
/** Validates mining hash meets difficulty requirement */
fun validateMiningHash(hash: String, difficulty: Int): Boolean = validateBlockHash(hash) && CryptoUtils.isValidHash(hash, difficulty)
/** Validates complete transaction data */
fun validateTransaction(
fromAddress: String?,
toAddress: String,
amount: Double,
fee: Double,
memo: String?
): ValidationResult {
val errors = mutableListOf<String>()
// Validate addresses
if (fromAddress != null && !validateWalletAddress(fromAddress)) {
errors.add("Invalid from address format")
}
if (!validateWalletAddress(toAddress)) {
errors.add("Invalid to address format")
}
if (fromAddress == toAddress) {
errors.add("Cannot send to same address")
}
// Validate amounts
if (!validateTransactionAmount(amount)) {
errors.add("Invalid transaction amount")
}
if (!validateTransactionFee(fee)) {
errors.add("Invalid transaction fee")
}
// Validate memo
if (!validateMemo(memo)) {
errors.add("Memo too long (max ${ServerConfig.maxMemoLength} characters)")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Validates complete block data */
fun validateBlock(
hash: String,
previousHash: String?,
merkleRoot: String,
timestamp: Long,
difficulty: Int,
nonce: Long,
minerAddress: String
): ValidationResult {
val errors = mutableListOf<String>()
// Validate hash
if (!validateBlockHash(hash)) {
errors.add("Invalid block hash format")
}
// Validate previous hash
if (previousHash != null && !validateBlockHash(previousHash)) {
errors.add("Invalid previous hash format")
}
// Validate merkle root
if (!validateBlockHash(merkleRoot)) {
errors.add("Invalid merkle root format")
}
// Validate timestamp
if (!validateTimestamp(timestamp)) {
errors.add("Invalid timestamp (must be within 1 hour of current time)")
}
// Validate difficulty
if (!validateMiningDifficulty(difficulty)) {
errors.add("Invalid mining difficulty (must be between 1 and 32)")
}
// Validate nonce
if (!validateMiningNonce(nonce)) {
errors.add("Invalid nonce (must be non-negative)")
}
// Validate miner address
if (!validateWalletAddress(minerAddress)) {
errors.add("Invalid miner address format")
}
// Validate hash meets difficulty
if (!validateMiningHash(hash, difficulty)) {
errors.add("Hash does not meet difficulty requirements")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Validates wallet creation data */
fun validateWalletCreation(label: String?): ValidationResult {
val errors = mutableListOf<String>()
if (!validateWalletLabel(label)) {
errors.add("Invalid wallet label")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Validates pagination parameters */
fun validatePagination(page: Int, pageSize: Int): ValidationResult {
val errors = mutableListOf<String>()
if (!validatePageNumber(page)) {
errors.add("Page number must be >= 1")
}
if (!validatePageSize(pageSize)) {
errors.add("Page size must be between 1 and ${ServerConfig.maxPageSize}")
}
return ValidationResult(errors.isEmpty(), errors)
}
/** Sanitizes user input */
fun sanitizeInput(input: String): String {
return input.trim()
.replace(Regex("[\\r\\n\\t]"), " ")
.replace(Regex("\\s+"), " ")
}
/** Validates hex string */
fun validateHexString(hex: String, expectedLength: Int? = null): Boolean {
if (!HashUtils.isValidHex(hex)) return false
return expectedLength == null || hex.length == expectedLength
}
}
/** Result of validation with success status and error messages */
data class ValidationResult(
val isValid: Boolean,
val errors: List<String> = emptyList()
) {
fun getErrorMessage(): String = errors.joinToString(", ")
fun throwIfInvalid() {
if (!isValid) {
throw InvalidTransactionException(getErrorMessage())
}
}
}