Compare commits
2 Commits
abc8ee7553
...
6d360df21d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d360df21d | ||
|
|
a6021f9523 |
@@ -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] }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user