feat: first release
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
|
||||
@@ -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<TransactionResponse>,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TransactionStatsResponse(
|
||||
val totalTransactions: Long,
|
||||
val pendingTransactions: Int,
|
||||
val confirmedTransactions: Long
|
||||
)
|
||||
|
||||
@@ -18,12 +18,7 @@ fun Route.walletRoutes() {
|
||||
/** Create a new wallet */
|
||||
post("/create") {
|
||||
try {
|
||||
val request = try {
|
||||
call.receive<CreateWalletRequest>()
|
||||
} catch (e: Exception) {
|
||||
// If no body or invalid JSON, create with null label
|
||||
CreateWalletRequest(null)
|
||||
}
|
||||
val request = call.receive<CreateWalletRequest>()
|
||||
|
||||
// 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")))
|
||||
|
||||
@@ -19,6 +19,7 @@ object TransactionService {
|
||||
fromAddress: String,
|
||||
toAddress: String,
|
||||
amount: Double,
|
||||
password: String,
|
||||
fee: Double = 0.0,
|
||||
memo: String? = null
|
||||
): TransactionResponse {
|
||||
@@ -27,12 +28,21 @@ 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())
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ object ValidationService {
|
||||
fromAddress: String?,
|
||||
toAddress: String,
|
||||
amount: Double,
|
||||
password: String,
|
||||
fee: Double,
|
||||
memo: String?
|
||||
): ValidationResult {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user