feat: added rest of services
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user