feat: first release

This commit is contained in:
darwincereska
2025-12-19 10:14:05 -05:00
parent 194fd7357c
commit 3f4349c9d2
9 changed files with 59 additions and 20 deletions

View File

@@ -9,6 +9,7 @@ object Tables {
val address = varchar("address", 64) // Format random_word:random_6_digits val address = varchar("address", 64) // Format random_word:random_6_digits
val balance = decimal("balance", 20, 8).default(BigDecimal.ZERO) val balance = decimal("balance", 20, 8).default(BigDecimal.ZERO)
val label = varchar("label", 255).nullable() val label = varchar("label", 255).nullable()
val passwordHash = varchar("password_hash", 64).nullable()
val createdAt = long("created_at") val createdAt = long("created_at")
val lastActivity = long("last_activity").nullable() val lastActivity = long("last_activity").nullable()

View File

@@ -8,7 +8,8 @@ data class SendTransactionRequest(
val toAddress: String, // Format random_word:random_6_digits val toAddress: String, // Format random_word:random_6_digits
val amount: Double, val amount: Double,
val fee: Double = 0.0, val fee: Double = 0.0,
val memo: String? = null val memo: String? = null,
val password: String
) )
@Serializable @Serializable

View File

@@ -4,7 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class CreateWalletRequest( data class CreateWalletRequest(
val label: String? = null val label: String? = null,
val password: String
) )
@Serializable @Serializable
@@ -12,6 +13,7 @@ data class WalletResponse(
val address: String, // Format random_word:random_6_digits (e.g. "phoenix:123456") val address: String, // Format random_word:random_6_digits (e.g. "phoenix:123456")
val balance: Double, val balance: Double,
val label: String?, val label: String?,
val passwordHash: String,
val createdAt: Long, val createdAt: Long,
val lastActivity: Long? val lastActivity: Long?
) )

View File

@@ -24,6 +24,7 @@ fun Route.transactionRoutes() {
request.fromAddress, request.fromAddress,
request.toAddress, request.toAddress,
request.amount, request.amount,
request.password,
request.fee, request.fee,
request.memo request.memo
) )
@@ -37,8 +38,9 @@ fun Route.transactionRoutes() {
request.fromAddress, request.fromAddress,
request.toAddress, request.toAddress,
request.amount, request.amount,
request.password,
request.fee, request.fee,
request.memo request.memo,
) )
call.respond(HttpStatusCode.Created, transaction) call.respond(HttpStatusCode.Created, transaction)
@@ -204,11 +206,13 @@ fun Route.transactionRoutes() {
val totalCount = TransactionService.getTotalTransactionCount() val totalCount = TransactionService.getTotalTransactionCount()
val pendingCount = TransactionService.getPendingTransactions().size val pendingCount = TransactionService.getPendingTransactions().size
call.respond(mapOf( call.respond(
"totalTransactions" to totalCount, TransactionStatsResponse(
"pendingTransactions" to pendingCount, totalTransactions = totalCount,
"confirmedTransactions" to (totalCount - pendingCount) pendingTransactions = pendingCount,
)) confirmedTransactions = (totalCount - pendingCount)
)
)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction statistics"))) call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to get transaction statistics")))
@@ -229,3 +233,10 @@ data class TransactionPendingResponse(
val transactions: List<TransactionResponse>, val transactions: List<TransactionResponse>,
val count: Int val count: Int
) )
@Serializable
data class TransactionStatsResponse(
val totalTransactions: Long,
val pendingTransactions: Int,
val confirmedTransactions: Long
)

View File

@@ -18,12 +18,7 @@ fun Route.walletRoutes() {
/** Create a new wallet */ /** Create a new wallet */
post("/create") { post("/create") {
try { try {
val request = try { val request = call.receive<CreateWalletRequest>()
call.receive<CreateWalletRequest>()
} catch (e: Exception) {
// If no body or invalid JSON, create with null label
CreateWalletRequest(null)
}
// Validate input // Validate input
val validation = ValidationService.validateWalletCreation(request.label) val validation = ValidationService.validateWalletCreation(request.label)
@@ -32,7 +27,7 @@ fun Route.walletRoutes() {
return@post return@post
} }
val wallet = WalletService.createWallet(request.label) val wallet = WalletService.createWallet(request.label, request.password)
call.respond(HttpStatusCode.Created, wallet) call.respond(HttpStatusCode.Created, wallet)
} catch (e: Exception) { } catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to create wallet"))) call.respond(HttpStatusCode.InternalServerError, mapOf("error" to (e.message ?: "Failed to create wallet")))

View File

@@ -19,6 +19,7 @@ object TransactionService {
fromAddress: String, fromAddress: String,
toAddress: String, toAddress: String,
amount: Double, amount: Double,
password: String,
fee: Double = 0.0, fee: Double = 0.0,
memo: String? = null memo: String? = null
): TransactionResponse { ): TransactionResponse {
@@ -27,12 +28,21 @@ object TransactionService {
val totalAmount = BigDecimal.valueOf(amount + fee) val totalAmount = BigDecimal.valueOf(amount + fee)
return transaction { return transaction {
// Check if sender wallet exists and has sufficient balance // Check if sender wallet exists
val fromBalance = Tables.Wallets.selectAll() val wallet = Tables.Wallets.selectAll()
.where { Tables.Wallets.address eq fromAddress } .where { Tables.Wallets.address eq fromAddress }
.map { it[Tables.Wallets.balance] }
.singleOrNull() ?: throw WalletNotFoundException(fromAddress) .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) { if (fromBalance < totalAmount) {
throw InsufficientFundsException(fromAddress, amount + fee, fromBalance.toDouble()) throw InsufficientFundsException(fromAddress, amount + fee, fromBalance.toDouble())
} }

View File

@@ -55,6 +55,7 @@ object ValidationService {
fromAddress: String?, fromAddress: String?,
toAddress: String, toAddress: String,
amount: Double, amount: Double,
password: String,
fee: Double, fee: Double,
memo: String? memo: String?
): ValidationResult { ): ValidationResult {

View File

@@ -12,18 +12,27 @@ import java.time.Instant
object WalletService { object WalletService {
/** Creates a new wallet with optional label */ /** 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 address = CryptoUtils.generateWalletAddress()
val timestamp = Instant.now().epochSecond val timestamp = Instant.now().epochSecond
val passwordHash = CryptoUtils.hashPassword(password)
return transaction { return transaction {
Tables.Wallets.insert { Tables.Wallets.insert {
it[Tables.Wallets.address] = address it[Tables.Wallets.address] = address
it[Tables.Wallets.label] = label it[Tables.Wallets.label] = label
it[Tables.Wallets.passwordHash] = passwordHash
it[createdAt] = timestamp 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.address],
it[Tables.Wallets.balance].toDouble(), it[Tables.Wallets.balance].toDouble(),
it[Tables.Wallets.label], it[Tables.Wallets.label],
it[Tables.Wallets.passwordHash].toString(),
it[Tables.Wallets.createdAt], it[Tables.Wallets.createdAt],
it[Tables.Wallets.lastActivity] it[Tables.Wallets.lastActivity]
) )
@@ -95,6 +105,7 @@ object WalletService {
it[Tables.Wallets.address], it[Tables.Wallets.address],
it[Tables.Wallets.balance].toDouble(), it[Tables.Wallets.balance].toDouble(),
it[Tables.Wallets.label], it[Tables.Wallets.label],
it[Tables.Wallets.passwordHash].toString(),
it[Tables.Wallets.createdAt], it[Tables.Wallets.createdAt],
it[Tables.Wallets.lastActivity] it[Tables.Wallets.lastActivity]
) )
@@ -115,6 +126,7 @@ object WalletService {
it[Tables.Wallets.address], it[Tables.Wallets.address],
it[Tables.Wallets.balance].toDouble(), it[Tables.Wallets.balance].toDouble(),
it[Tables.Wallets.label], it[Tables.Wallets.label],
it[Tables.Wallets.passwordHash].toString(),
it[Tables.Wallets.createdAt], it[Tables.Wallets.createdAt],
it[Tables.Wallets.lastActivity] it[Tables.Wallets.lastActivity]
) )

View File

@@ -52,6 +52,9 @@ object CryptoUtils {
.joinToString("") { "%02x".format(it) } .joinToString("") { "%02x".format(it) }
} }
/** Hashes password */
fun hashPassword(password: String): String = sha256("ccoin_password_$password")
/** Generates a transaction hash */ /** Generates a transaction hash */
fun generateTransactionHash( fun generateTransactionHash(
fromAddress: String?, fromAddress: String?,
@@ -120,4 +123,7 @@ object CryptoUtils {
/** Validates block has format */ /** Validates block has format */
fun isValidBlockHash(hash: String): Boolean = hash.length == 64 && hash.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' } 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
} }