From 3f4349c9d219f4267aa694ece5dd274a0fbaab59 Mon Sep 17 00:00:00 2001 From: darwincereska Date: Fri, 19 Dec 2025 10:14:05 -0500 Subject: [PATCH] feat: first release --- .../main/kotlin/org/ccoin/database/Tables.kt | 1 + .../kotlin/org/ccoin/models/Transaction.kt | 3 ++- .../main/kotlin/org/ccoin/models/Wallet.kt | 4 +++- .../org/ccoin/routes/TransactionRoutes.kt | 23 ++++++++++++++----- .../kotlin/org/ccoin/routes/WalletRoutes.kt | 9 ++------ .../org/ccoin/services/TransactionService.kt | 16 ++++++++++--- .../org/ccoin/services/ValidationService.kt | 1 + .../org/ccoin/services/WalletService.kt | 16 +++++++++++-- .../kotlin/org/ccoin/utils/CryptoUtils.kt | 6 +++++ 9 files changed, 59 insertions(+), 20 deletions(-) diff --git a/server/src/main/kotlin/org/ccoin/database/Tables.kt b/server/src/main/kotlin/org/ccoin/database/Tables.kt index f1596aa..a632fea 100644 --- a/server/src/main/kotlin/org/ccoin/database/Tables.kt +++ b/server/src/main/kotlin/org/ccoin/database/Tables.kt @@ -9,6 +9,7 @@ object Tables { val address = varchar("address", 64) // Format random_word:random_6_digits val balance = decimal("balance", 20, 8).default(BigDecimal.ZERO) val label = varchar("label", 255).nullable() + val passwordHash = varchar("password_hash", 64).nullable() val createdAt = long("created_at") val lastActivity = long("last_activity").nullable() diff --git a/server/src/main/kotlin/org/ccoin/models/Transaction.kt b/server/src/main/kotlin/org/ccoin/models/Transaction.kt index a2d84a7..25d2122 100644 --- a/server/src/main/kotlin/org/ccoin/models/Transaction.kt +++ b/server/src/main/kotlin/org/ccoin/models/Transaction.kt @@ -8,7 +8,8 @@ data class SendTransactionRequest( val toAddress: String, // Format random_word:random_6_digits val amount: Double, val fee: Double = 0.0, - val memo: String? = null + val memo: String? = null, + val password: String ) @Serializable diff --git a/server/src/main/kotlin/org/ccoin/models/Wallet.kt b/server/src/main/kotlin/org/ccoin/models/Wallet.kt index cae2ec2..93b2a0f 100644 --- a/server/src/main/kotlin/org/ccoin/models/Wallet.kt +++ b/server/src/main/kotlin/org/ccoin/models/Wallet.kt @@ -4,7 +4,8 @@ import kotlinx.serialization.Serializable @Serializable data class CreateWalletRequest( - val label: String? = null + val label: String? = null, + val password: String ) @Serializable @@ -12,6 +13,7 @@ data class WalletResponse( val address: String, // Format random_word:random_6_digits (e.g. "phoenix:123456") val balance: Double, val label: String?, + val passwordHash: String, val createdAt: Long, val lastActivity: Long? ) diff --git a/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt index fcbf900..effeb5d 100644 --- a/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/TransactionRoutes.kt @@ -24,6 +24,7 @@ fun Route.transactionRoutes() { request.fromAddress, request.toAddress, request.amount, + request.password, request.fee, request.memo ) @@ -37,8 +38,9 @@ fun Route.transactionRoutes() { request.fromAddress, request.toAddress, request.amount, + request.password, request.fee, - request.memo + request.memo, ) call.respond(HttpStatusCode.Created, transaction) @@ -204,11 +206,13 @@ fun Route.transactionRoutes() { val totalCount = TransactionService.getTotalTransactionCount() val pendingCount = TransactionService.getPendingTransactions().size - call.respond(mapOf( - "totalTransactions" to totalCount, - "pendingTransactions" to pendingCount, - "confirmedTransactions" to (totalCount - pendingCount) - )) + call.respond( + TransactionStatsResponse( + totalTransactions = totalCount, + pendingTransactions = pendingCount, + confirmedTransactions = (totalCount - pendingCount) + ) + ) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction statistics"))) @@ -229,3 +233,10 @@ data class TransactionPendingResponse( val transactions: List, val count: Int ) + +@Serializable +data class TransactionStatsResponse( + val totalTransactions: Long, + val pendingTransactions: Int, + val confirmedTransactions: Long +) diff --git a/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt b/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt index 220f86a..bf49873 100644 --- a/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt +++ b/server/src/main/kotlin/org/ccoin/routes/WalletRoutes.kt @@ -18,12 +18,7 @@ fun Route.walletRoutes() { /** Create a new wallet */ post("/create") { try { - val request = try { - call.receive() - } catch (e: Exception) { - // If no body or invalid JSON, create with null label - CreateWalletRequest(null) - } + val request = call.receive() // Validate input val validation = ValidationService.validateWalletCreation(request.label) @@ -32,7 +27,7 @@ fun Route.walletRoutes() { return@post } - val wallet = WalletService.createWallet(request.label) + val wallet = WalletService.createWallet(request.label, request.password) call.respond(HttpStatusCode.Created, wallet) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to create wallet"))) diff --git a/server/src/main/kotlin/org/ccoin/services/TransactionService.kt b/server/src/main/kotlin/org/ccoin/services/TransactionService.kt index c911799..b7ab140 100644 --- a/server/src/main/kotlin/org/ccoin/services/TransactionService.kt +++ b/server/src/main/kotlin/org/ccoin/services/TransactionService.kt @@ -19,6 +19,7 @@ object TransactionService { fromAddress: String, toAddress: String, amount: Double, + password: String, fee: Double = 0.0, memo: String? = null ): TransactionResponse { @@ -27,11 +28,20 @@ object TransactionService { val totalAmount = BigDecimal.valueOf(amount + fee) return transaction { - // Check if sender wallet exists and has sufficient balance - val fromBalance = Tables.Wallets.selectAll() + // Check if sender wallet exists + val wallet = Tables.Wallets.selectAll() .where { Tables.Wallets.address eq fromAddress } - .map { it[Tables.Wallets.balance] } .singleOrNull() ?: throw WalletNotFoundException(fromAddress) + + val storedHash = wallet[Tables.Wallets.passwordHash] + ?: throw InvalidTransactionException("Wallet has no password set") + + if (!CryptoUtils.verifyPassword(password, storedHash)) { + throw InvalidTransactionException("Invalid password") + } + + // Check if sender wallet exists and has sufficient balance + val fromBalance = wallet[Tables.Wallets.balance] if (fromBalance < totalAmount) { throw InsufficientFundsException(fromAddress, amount + fee, fromBalance.toDouble()) diff --git a/server/src/main/kotlin/org/ccoin/services/ValidationService.kt b/server/src/main/kotlin/org/ccoin/services/ValidationService.kt index f6a2384..f4766e8 100644 --- a/server/src/main/kotlin/org/ccoin/services/ValidationService.kt +++ b/server/src/main/kotlin/org/ccoin/services/ValidationService.kt @@ -55,6 +55,7 @@ object ValidationService { fromAddress: String?, toAddress: String, amount: Double, + password: String, fee: Double, memo: String? ): ValidationResult { diff --git a/server/src/main/kotlin/org/ccoin/services/WalletService.kt b/server/src/main/kotlin/org/ccoin/services/WalletService.kt index 0cf303d..6954403 100644 --- a/server/src/main/kotlin/org/ccoin/services/WalletService.kt +++ b/server/src/main/kotlin/org/ccoin/services/WalletService.kt @@ -12,18 +12,27 @@ import java.time.Instant object WalletService { /** Creates a new wallet with optional label */ - fun createWallet(label: String? = null): WalletResponse { + fun createWallet(label: String? = null, password: String): WalletResponse { val address = CryptoUtils.generateWalletAddress() val timestamp = Instant.now().epochSecond + val passwordHash = CryptoUtils.hashPassword(password) return transaction { Tables.Wallets.insert { it[Tables.Wallets.address] = address it[Tables.Wallets.label] = label + it[Tables.Wallets.passwordHash] = passwordHash it[createdAt] = timestamp } - WalletResponse(address, 0.0, label, timestamp, null) + WalletResponse( + address = address, + balance = 0.0, + label = label, + passwordHash = passwordHash, + createdAt = timestamp, + lastActivity = null + ) } } @@ -35,6 +44,7 @@ object WalletService { it[Tables.Wallets.address], it[Tables.Wallets.balance].toDouble(), it[Tables.Wallets.label], + it[Tables.Wallets.passwordHash].toString(), it[Tables.Wallets.createdAt], it[Tables.Wallets.lastActivity] ) @@ -95,6 +105,7 @@ object WalletService { it[Tables.Wallets.address], it[Tables.Wallets.balance].toDouble(), it[Tables.Wallets.label], + it[Tables.Wallets.passwordHash].toString(), it[Tables.Wallets.createdAt], it[Tables.Wallets.lastActivity] ) @@ -115,6 +126,7 @@ object WalletService { it[Tables.Wallets.address], it[Tables.Wallets.balance].toDouble(), it[Tables.Wallets.label], + it[Tables.Wallets.passwordHash].toString(), it[Tables.Wallets.createdAt], it[Tables.Wallets.lastActivity] ) diff --git a/server/src/main/kotlin/org/ccoin/utils/CryptoUtils.kt b/server/src/main/kotlin/org/ccoin/utils/CryptoUtils.kt index d6578b2..271724f 100644 --- a/server/src/main/kotlin/org/ccoin/utils/CryptoUtils.kt +++ b/server/src/main/kotlin/org/ccoin/utils/CryptoUtils.kt @@ -52,6 +52,9 @@ object CryptoUtils { .joinToString("") { "%02x".format(it) } } + /** Hashes password */ + fun hashPassword(password: String): String = sha256("ccoin_password_$password") + /** Generates a transaction hash */ fun generateTransactionHash( fromAddress: String?, @@ -120,4 +123,7 @@ object CryptoUtils { /** Validates block has format */ fun isValidBlockHash(hash: String): Boolean = hash.length == 64 && hash.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' } + + /** Verifies password hash */ + fun verifyPassword(password: String, storedHash: String): Boolean = hashPassword(password) == storedHash }