Compare commits

...

2 Commits

Author SHA1 Message Date
darwincereska
6d360df21d feat: added mining service 2025-12-17 10:32:53 -05:00
darwincereska
a6021f9523 feat: added transaction service 2025-12-17 10:26:56 -05:00
2 changed files with 418 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
package org.ccoin.services
import org.ccoin.config.ServerConfig
import org.ccoin.database.Tables
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.models.BlockResponse
import org.ccoin.models.MiningJobResponse
import org.ccoin.models.MiningStatsResponse
import org.ccoin.utils.CryptoUtils
import org.ccoin.utils.HashUtils
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.time.Instant
object MiningService {
/** Starts a mining job for a miner */
fun startMining(minerAddress: String, difficulty: Int? = null): MiningJobResponse {
val jobId = CryptoUtils.generateJobId()
val currentDifficulty = difficulty ?: getCurrentDifficulty()
val previousHash = getLatestBlockHash()
val height = getNextBlockHeight()
val timestamp = Instant.now().epochSecond
val expiresAt = timestamp + 300 // 5 minutes
val target = "0".repeat(currentDifficulty)
return MiningJobResponse(
jobId, target, currentDifficulty, previousHash, height, timestamp, expiresAt
)
}
/** Submits mining result and creates block if valid */
fun submitMiningResult(
minerAddress: String,
nonce: Long,
hash: String,
previousHash: String,
timestamp: Long = Instant.now().epochSecond
): BlockResponse {
val currentDifficulty = getCurrentDifficulty()
// Validate hash meets difficulty requirements
if (!CryptoUtils.isValidHash(hash, currentDifficulty)) {
throw InvalidTransactionException("Hash does not meet difficulty requirements")
}
// Validate previous hash matches latest block
val latestHash = getLatestBlockHash()
if (previousHash != latestHash) {
throw InvalidTransactionException("Previous hash does not match latest block")
}
val height = getNextBlockHeight()
val reward = ServerConfig.miningReward
val merkleRoot = calculateMerkleRoot(emptyList()) // No transactions for now
return transaction {
// Create block
Tables.Blocks.insert {
it[Tables.Blocks.hash] = hash
it[Tables.Blocks.previousHash] = if (previousHash == "0".repeat(64)) null else previousHash
it[Tables.Blocks.merkleRoot] = merkleRoot
it[Tables.Blocks.timestamp] = timestamp
it[Tables.Blocks.difficulty] = currentDifficulty
it[Tables.Blocks.nonce] = nonce
it[Tables.Blocks.minerAddress] = minerAddress
it[Tables.Blocks.reward] = BigDecimal.valueOf(reward)
it[transactionCount] = 0
}
// Reward miner with genesis transaction
TransactionService.createGenesisTransaction(minerAddress, reward)
BlockResponse(
hash, if (previousHash == "0".repeat(64)) null else previousHash, merkleRoot,
timestamp, currentDifficulty, nonce, minerAddress, reward, height, 0, 0
)
}
}
/** Gets current mining difficulty */
fun getCurrentDifficulty(): Int = ServerConfig.miningDifficulty
/** Gets mining statistics for a miner */
fun getMinerStats(minerAddress: String): MiningStatsResponse = transaction {
val blocks = Tables.Blocks.selectAll()
.where { Tables.Blocks.minerAddress eq minerAddress }
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
val totalBlocks = blocks.count().toInt()
val totalReward = blocks.sumOf { it[Tables.Blocks.reward] }.toDouble()
val lastBlockMined = blocks.firstOrNull()?.get(Tables.Blocks.timestamp)?.toLong()
MiningStatsResponse(
minerAddress, totalBlocks, totalReward, lastBlockMined, getCurrentDifficulty()
)
}
/** Gets latest block hash */
private fun getLatestBlockHash(): String = transaction {
Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.hash] }
.singleOrNull() ?: "0".repeat(64) // Genesis hash
}
/** Gets next block height */
private fun getNextBlockHeight(): Int = transaction {
val latestHeight = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.height, SortOrder.DESC)
.limit(1)
.map { it[Tables.Blocks.height] }
.singleOrNull() ?: 0
latestHeight + 1
}
/** Calculates merkle root for transactions */
private fun calculateMerkleRoot(transactionHashes: List<String>): String {
return if (transactionHashes.isEmpty()) {
HashUtils.sha256Hex("empty_block")
} else {
CryptoUtils.calculateMerkleRoot(transactionHashes)
}
}
/** Gets network hash rate estimate */
fun getNetworkHashRate(): Double = transaction {
val recentBlocks = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
.limit(100)
if (recentBlocks.count() < 2) return@transaction 0.0
val blocks = recentBlocks.toList()
val timeSpan = blocks.first()[Tables.Blocks.timestamp] - blocks.last()[Tables.Blocks.timestamp]
val difficulty = getCurrentDifficulty()
if (timeSpan <= 0) return@transaction 0.0
// Rough estimate: (blocks * 2^difficulty) / time_span
(blocks.size * Math.pow(2.0, difficulty.toDouble())) / timeSpan
}
/** Gets average block time */
fun getAverageBlockTime(): Long = transaction {
val recentBlocks = Tables.Blocks.selectAll()
.orderBy(Tables.Blocks.timestamp, SortOrder.DESC)
.limit(100)
.map { it[Tables.Blocks.timestamp] }
if (recentBlocks.size < 2) return@transaction 0L
val timeDiffs = recentBlocks.zipWithNext { a, b -> a - b }
timeDiffs.average().toLong()
}
/** Gets total number of active miners */
fun getActiveMinersCount(): Int = transaction {
val oneDayAgo = Instant.now().epochSecond - 86400
Tables.Blocks.selectAll()
.where { Tables.Blocks.timestamp greater oneDayAgo }
.groupBy(Tables.Blocks.minerAddress)
.count().toInt()
}
/** Validates mining job */
fun validateMiningJob(jobId: String, hash: String, nonce: Long): Boolean {
// Simple validation - in production you'd store job details
return CryptoUtils.isValidHash(hash, getCurrentDifficulty())
}
/** Gets pending transactions for mining */
fun getPendingTransactionsForMining(limit: Int = 100): List<String> = transaction {
Tables.Transactions.selectAll()
.where { Tables.Transactions.status eq "pending" }
.orderBy(Tables.Transactions.timestamp, SortOrder.ASC)
.limit(limit)
.map { it[Tables.Transactions.hash] }
}
}

View File

@@ -0,0 +1,233 @@
package org.ccoin.services
import org.ccoin.database.Tables
import org.ccoin.exceptions.InsufficientFundsException
import org.ccoin.exceptions.InvalidTransactionException
import org.ccoin.exceptions.WalletNotFoundException
import org.ccoin.models.TransactionResponse
import org.ccoin.models.TransactionStatus
import org.ccoin.utils.CryptoUtils
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.time.Instant
object TransactionService {
/** Sends a transaction between wallets */
fun sendTransaction(
fromAddress: String,
toAddress: String,
amount: Double,
fee: Double = 0.0,
memo: String? = null
): TransactionResponse {
val hash = CryptoUtils.generateTransactionHash(fromAddress, toAddress, amount, Instant.now().epochSecond)
val timestamp = Instant.now().epochSecond
val totalAmount = BigDecimal.valueOf(amount + fee)
return transaction {
// Check if sender wallet exists and has sufficient balance
val fromBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq fromAddress }
.map { it[Tables.Wallets.balance] }
.singleOrNull() ?: throw WalletNotFoundException(fromAddress)
if (fromBalance < totalAmount) {
throw InsufficientFundsException(fromAddress, amount + fee, fromBalance.toDouble())
}
// Check if recipient wallet exists
val recipientExists = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.count() > 0
if (!recipientExists) {
throw WalletNotFoundException(toAddress)
}
// Create transaction record
Tables.Transactions.insert {
it[Tables.Transactions.hash] = hash
it[Tables.Transactions.fromAddress] = fromAddress
it[Tables.Transactions.toAddress] = toAddress
it[Tables.Transactions.amount] = BigDecimal.valueOf(amount)
it[Tables.Transactions.fee] = BigDecimal.valueOf(fee)
it[Tables.Transactions.memo] = memo
it[Tables.Transactions.timestamp] = timestamp
it[status] = "confirmed"
}
// Update sender balance
val senderCurrentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq fromAddress }
.map { it[Tables.Wallets.balance] }
.single()
Tables.Wallets.update({ Tables.Wallets.address eq fromAddress }) {
it[balance] = senderCurrentBalance.subtract(totalAmount)
it[lastActivity] = timestamp
}
// Update recipient balance
val recipientCurrentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.map { it[Tables.Wallets.balance] }
.single()
Tables.Wallets.update({ Tables.Wallets.address eq toAddress }) {
it[balance] = recipientCurrentBalance.add(BigDecimal.valueOf(amount))
it[lastActivity] = timestamp
}
TransactionResponse(
hash, fromAddress, toAddress, amount, fee, memo, null, timestamp, TransactionStatus.CONFIRMED, 0
)
}
}
/** Gets transaction by hash */
fun getTransaction(hash: String): TransactionResponse? = transaction {
Tables.Transactions.selectAll().where { Tables.Transactions.hash eq hash }
.map {
TransactionResponse(
it[Tables.Transactions.hash],
it[Tables.Transactions.fromAddress],
it[Tables.Transactions.toAddress],
it[Tables.Transactions.amount].toDouble(),
it[Tables.Transactions.fee].toDouble(),
it[Tables.Transactions.memo],
it[Tables.Transactions.blockHash],
it[Tables.Transactions.timestamp],
TransactionStatus.valueOf(it[Tables.Transactions.status].uppercase()),
it[Tables.Transactions.confirmations]
)
}.singleOrNull()
}
/** Gets transaction history for an address */
fun getTransactionHistory(
address: String,
limit: Int = 50,
offset: Int = 0
): List<TransactionResponse> = transaction {
Tables.Transactions.selectAll()
.where {
(Tables.Transactions.fromAddress eq address) or (Tables.Transactions.toAddress eq address)
}
.orderBy(Tables.Transactions.timestamp, SortOrder.DESC)
.limit(limit)
.offset(offset.toLong())
.map {
TransactionResponse(
it[Tables.Transactions.hash],
it[Tables.Transactions.fromAddress],
it[Tables.Transactions.toAddress],
it[Tables.Transactions.amount].toDouble(),
it[Tables.Transactions.fee].toDouble(),
it[Tables.Transactions.memo],
it[Tables.Transactions.blockHash],
it[Tables.Transactions.timestamp],
TransactionStatus.valueOf(it[Tables.Transactions.status].uppercase()),
it[Tables.Transactions.confirmations]
)
}
}
/** Gets pending transactions */
fun getPendingTransactions(): List<TransactionResponse> = transaction {
Tables.Transactions.selectAll()
.where { Tables.Transactions.status eq "pending" }
.orderBy(Tables.Transactions.timestamp, SortOrder.ASC)
.map {
TransactionResponse(
it[Tables.Transactions.hash],
it[Tables.Transactions.fromAddress],
it[Tables.Transactions.toAddress],
it[Tables.Transactions.amount].toDouble(),
it[Tables.Transactions.fee].toDouble(),
it[Tables.Transactions.memo],
it[Tables.Transactions.blockHash],
it[Tables.Transactions.timestamp],
TransactionStatus.valueOf(it[Tables.Transactions.status].uppercase()),
it[Tables.Transactions.confirmations]
)
}
}
/** Updates transaction status */
fun updateTransactionStatus(hash: String, status: TransactionStatus): Boolean = transaction {
val updated = Tables.Transactions.update({ Tables.Transactions.hash eq hash }) {
it[Tables.Transactions.status] = status.name.lowercase()
}
updated > 0
}
/** Adds transaction to block */
fun addTransactionToBlock(transactionHash: String, blockHash: String): Boolean = transaction {
val updated = Tables.Transactions.update({ Tables.Transactions.hash eq transactionHash }) {
it[Tables.Transactions.blockHash] = blockHash
it[status] = "confirmed"
}
updated > 0
}
/** Gets total transaction count */
fun getTotalTransactionCount(): Long = transaction {
Tables.Transactions.selectAll().count()
}
/** Gets transaction count for address */
fun getTransactionCountForAddress(address: String): Long = transaction {
Tables.Transactions.selectAll()
.where {
(Tables.Transactions.fromAddress eq address) or (Tables.Transactions.toAddress eq address)
}
.count()
}
/** Creates a genesis transaction (mining reward) */
fun createGenesisTransaction(toAddress: String, amount: Double): TransactionResponse {
val hash = CryptoUtils.generateTransactionHash(null, toAddress, amount, Instant.now().epochSecond)
val timestamp = Instant.now().epochSecond
return transaction {
// Check if recipient wallet exists
val recipientExists = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.count() > 0
if (!recipientExists) {
throw WalletNotFoundException(toAddress)
}
// Create genesis transaction
Tables.Transactions.insert {
it[Tables.Transactions.hash] = hash
it[Tables.Transactions.fromAddress] = null
it[Tables.Transactions.toAddress] = toAddress
it[Tables.Transactions.amount] = BigDecimal.valueOf(amount)
it[Tables.Transactions.fee] = BigDecimal.ZERO
it[Tables.Transactions.memo] = "Mining reward"
it[Tables.Transactions.timestamp] = timestamp
it[status] = "confirmed"
}
// Update recipient balance
val recipientCurrentBalance = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq toAddress }
.map { it[Tables.Wallets.balance] }
.single()
Tables.Wallets.update({ Tables.Wallets.address eq toAddress }) {
it[balance] = recipientCurrentBalance.add(BigDecimal.valueOf(amount))
it[lastActivity] = timestamp
}
TransactionResponse(
hash, null, toAddress, amount, 0.0, "Mining reward", null, timestamp, TransactionStatus.CONFIRMED, 0
)
}
}
}