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 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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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")))
|
||||||
|
|||||||
@@ -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,11 +28,20 @@ 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())
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user