feat: added transaction service
This commit is contained in:
@@ -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<TransactionResponse> = 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<TransactionResponse> = 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user