diff --git a/server/src/main/kotlin/org/ccoin/services/TransactionService.kt b/server/src/main/kotlin/org/ccoin/services/TransactionService.kt index e69de29..c911799 100644 --- a/server/src/main/kotlin/org/ccoin/services/TransactionService.kt +++ b/server/src/main/kotlin/org/ccoin/services/TransactionService.kt @@ -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 = 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 = 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 + ) + } + } +} +