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.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
|
||||||
# Application
|
# Application
|
||||||
ccoin.version=1.0.0
|
ccoin.version=1.0.0
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ data class BlockResponse(
|
|||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val difficulty: Int,
|
val difficulty: Int,
|
||||||
val nonce: Long,
|
val nonce: Long,
|
||||||
val minerAddress: String
|
val minerAddress: String,
|
||||||
val reward: Double,
|
val reward: Double,
|
||||||
val height: Int,
|
val height: Int,
|
||||||
val transactionCount: Int,
|
val transactionCount: Int,
|
||||||
@@ -51,3 +51,11 @@ data class MiningStatsResponse(
|
|||||||
val lastBlockMined: Long?,
|
val lastBlockMined: Long?,
|
||||||
val currentDifficulty: Int
|
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