Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,30 @@ class OnboardingPreferences @Inject constructor(
prefs.edit().putBoolean(KEY_ONBOARDING_COMPLETED, completed).apply()
}

/**
* Check if a DCA plan was created during onboarding.
*/
fun isPlanCreatedDuringOnboarding(): Boolean {
return prefs.getBoolean(KEY_PLAN_CREATED, false)
}

fun setPlanCreatedDuringOnboarding(created: Boolean) {
prefs.edit().putBoolean(KEY_PLAN_CREATED, created).apply()
}

/**
* Reset onboarding state (for testing or re-onboarding).
*/
fun resetOnboarding() {
prefs.edit().putBoolean(KEY_ONBOARDING_COMPLETED, false).apply()
prefs.edit()
.putBoolean(KEY_ONBOARDING_COMPLETED, false)
.remove(KEY_PLAN_CREATED)
.apply()
}

companion object {
private const val PREFS_NAME = "accbot_onboarding"
private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed"
private const val KEY_PLAN_CREATED = "plan_created_during_onboarding"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class MarketDataService @Inject constructor(
"EUR" to "eur",
"GBP" to "gbp",
"CZK" to "czk",
"USDT" to "usd" // Treat USDT as USD
"USDT" to "usd", // Treat USDT as USD
"USDC" to "usd" // Treat USDC as USD
)
}

Expand Down Expand Up @@ -247,7 +248,7 @@ class MarketDataService @Inject constructor(
limit: Int
): List<Pair<LocalDate, BigDecimal>>? = withContext(Dispatchers.IO) {
val fsym = crypto.uppercase()
val tsym = if (fiat.uppercase() == "USDT") "USD" else fiat.uppercase()
val tsym = if (fiat.uppercase() in listOf("USDT", "USDC")) "USD" else fiat.uppercase()

try {
val toUnix = toDate.plusDays(1).atStartOfDay(ZoneOffset.UTC).toEpochSecond()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ object ExchangeInstructionsProvider {
R.string.exchange_instructions_binance_2,
R.string.exchange_instructions_binance_3,
R.string.exchange_instructions_binance_4,
R.string.exchange_instructions_binance_5,
R.string.exchange_instructions_binance_6
),
url = "https://www.binance.com/en/my/settings/api-management",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ enum class Exchange(
BINANCE(
displayName = "Binance",
logoRes = R.drawable.ic_exchange_binance,
supportedFiats = listOf("EUR", "USDT"),
supportedCryptos = listOf("BTC", "ETH", "SOL", "ADA", "DOT"),
minOrderSize = mapOf("EUR" to BigDecimal("10"), "USDT" to BigDecimal("10")),
supportedFiats = listOf("EUR", "USDC"),
supportedCryptos = listOf("BTC", "ETH", "BNB", "SOL", "ADA", "DOT"),
minOrderSize = mapOf("EUR" to BigDecimal("5"), "USDC" to BigDecimal("5")),
sandboxSupport = SandboxSupport.FULL
),
KRAKEN(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.accbot.dca.domain.usecase

import com.accbot.dca.data.local.DcaPlanDao
import com.accbot.dca.data.local.DcaPlanEntity
import com.accbot.dca.data.local.UserPreferences
import com.accbot.dca.domain.model.DcaFrequency
import com.accbot.dca.domain.model.DcaStrategy
import com.accbot.dca.domain.model.Exchange
import com.accbot.dca.domain.util.CronUtils
import java.math.BigDecimal
import java.time.Duration
import java.time.Instant
import javax.inject.Inject

class CreateDcaPlanUseCase @Inject constructor(
private val dcaPlanDao: DcaPlanDao,
private val userPreferences: UserPreferences
) {
suspend fun execute(
exchange: Exchange,
crypto: String,
fiat: String,
amount: BigDecimal,
frequency: DcaFrequency,
cronExpression: String?,
strategy: DcaStrategy,
withdrawalEnabled: Boolean = false,
withdrawalAddress: String? = null,
targetAmount: BigDecimal? = null
) {
val now = Instant.now()
val nextExecution = if (frequency == DcaFrequency.CUSTOM && cronExpression != null) {
CronUtils.getNextExecution(cronExpression, now)
?: now.plus(Duration.ofMinutes(1440))
} else {
now.plus(Duration.ofMinutes(frequency.intervalMinutes))
}

val plan = DcaPlanEntity(
exchange = exchange,
crypto = crypto,
fiat = fiat,
amount = amount,
frequency = frequency,
cronExpression = if (frequency == DcaFrequency.CUSTOM) cronExpression else null,
strategy = strategy,
isEnabled = true,
withdrawalEnabled = withdrawalEnabled,
withdrawalAddress = withdrawalAddress,
createdAt = now,
nextExecutionAt = nextExecution,
targetAmount = targetAmount
)

dcaPlanDao.insertPlan(plan)

// Auto-enable Market Pulse when creating a plan with market-aware strategy
if (strategy is DcaStrategy.AthBased || strategy is DcaStrategy.FearAndGreed) {
userPreferences.setMarketPulseEnabled(true)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.accbot.dca.domain.util

/**
* Basic validation for cryptocurrency wallet addresses.
* Simplified validation — actual address format depends on the crypto.
*/
object CryptoAddressValidator {

fun isValid(crypto: String, address: String): Boolean {
if (address.isBlank()) return false
val trimmed = address.trim()
return when (crypto.uppercase()) {
"BTC" -> isValidBtcAddress(trimmed)
"ETH", "SOL", "ADA", "DOT" -> isValidGenericAddress(trimmed, minLength = 26, maxLength = 128)
"LTC" -> isValidLtcAddress(trimmed)
else -> isValidGenericAddress(trimmed, minLength = 20, maxLength = 128)
}
}

private fun isValidBtcAddress(address: String): Boolean {
return when {
address.startsWith("1") || address.startsWith("3") ->
address.length in 25..34 && address.all { it.isLetterOrDigit() }
address.startsWith("bc1") ->
address.length in 42..62 && address.all { it.isLetterOrDigit() }
else -> false
}
}

private fun isValidLtcAddress(address: String): Boolean {
return when {
address.startsWith("L") || address.startsWith("M") ->
address.length in 25..34 && address.all { it.isLetterOrDigit() }
address.startsWith("ltc1") ->
address.length in 42..62 && address.all { it.isLetterOrDigit() }
else -> false
}
}

private fun isValidGenericAddress(address: String, minLength: Int, maxLength: Int): Boolean {
return address.length in minLength..maxLength &&
address.all { it.isLetterOrDigit() || it == '_' }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,27 @@ class KuCoinApi(

private val baseUrl = ExchangeConfig.getBaseUrl(Exchange.KUCOIN, isSandbox)

private fun signedRequest(method: String, endpoint: String, body: String? = null): Request.Builder {
val timestamp = System.currentTimeMillis().toString()
val preSign = "$timestamp$method$endpoint${body ?: ""}"
val signature = CryptoUtils.hmacSha256Base64Secret(preSign, credentials.apiSecret)
val passphrase = credentials.passphrase?.let {
CryptoUtils.hmacSha256Base64Secret(it, credentials.apiSecret)
} ?: ""

return Request.Builder()
.url("$baseUrl$endpoint")
.header("KC-API-KEY", credentials.apiKey)
.header("KC-API-SIGN", signature)
.header("KC-API-TIMESTAMP", timestamp)
.header("KC-API-PASSPHRASE", passphrase)
.header("KC-API-KEY-VERSION", "2")
}

override suspend fun marketBuy(crypto: String, fiat: String, fiatAmount: BigDecimal): DcaResult =
withContext(Dispatchers.IO) {
try {
val symbol = "$crypto-$fiat"
val timestamp = System.currentTimeMillis().toString()
val clientOid = java.util.UUID.randomUUID().toString()

val body = JSONObject().apply {
Expand All @@ -449,19 +465,7 @@ class KuCoinApi(
put("funds", fiatAmount.setScale(2, RoundingMode.DOWN).toPlainString())
}.toString()

val preSign = "${timestamp}POST/api/v1/orders$body"
val signature = CryptoUtils.hmacSha256Base64Secret(preSign, credentials.apiSecret)
val passphrase = credentials.passphrase?.let {
CryptoUtils.hmacSha256Base64Secret(it, credentials.apiSecret)
} ?: ""

val request = Request.Builder()
.url("$baseUrl/api/v1/orders")
.header("KC-API-KEY", credentials.apiKey)
.header("KC-API-SIGN", signature)
.header("KC-API-TIMESTAMP", timestamp)
.header("KC-API-PASSPHRASE", passphrase)
.header("KC-API-KEY-VERSION", "2")
val request = signedRequest("POST", "/api/v1/orders", body)
.header("Content-Type", "application/json")
.post(body.toRequestBody())
.build()
Expand Down Expand Up @@ -508,20 +512,7 @@ class KuCoinApi(

override suspend fun validateCredentials(): Boolean = withContext(Dispatchers.IO) {
try {
val timestamp = System.currentTimeMillis().toString()
val preSign = "${timestamp}GET/api/v1/accounts"
val signature = CryptoUtils.hmacSha256Base64Secret(preSign, credentials.apiSecret)
val passphrase = credentials.passphrase?.let {
CryptoUtils.hmacSha256Base64Secret(it, credentials.apiSecret)
} ?: ""

val request = Request.Builder()
.url("$baseUrl/api/v1/accounts")
.header("KC-API-KEY", credentials.apiKey)
.header("KC-API-SIGN", signature)
.header("KC-API-TIMESTAMP", timestamp)
.header("KC-API-PASSPHRASE", passphrase)
.header("KC-API-KEY-VERSION", "2")
val request = signedRequest("GET", "/api/v1/accounts")
.get()
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ package com.accbot.dca.presentation.changelog

object ChangelogData {
val entries: List<ChangelogEntry> = listOf(
ChangelogEntry(
versionCode = 26100,
version = "2.6.1",
titles = mapOf(
"cs" to "DRY refaktoring a Binance USDC",
"en" to "DRY Refactor & Binance USDC",
),
features = mapOf(
"cs" to listOf(
"Sdílený CredentialFormDelegate — méně duplicitního kódu napříč 4 ViewModely",
"Sdílený dialog výsledku API importu na 3 obrazovkách",
"Jednotný AccBotTopAppBar na 11 obrazovkách",
"Binance: přechod z USDT na USDC, minimální objednávka snížena na 5",
"Rychlé částky: 5, 10, 25, 50, 100 (dříve 25–500)",
"Výchozí částka DCA plánu nastavena na minimum burzy",
"Oprava zobrazení minimální částky — bez zbytečných nul",
"Extrakce KuCoin signed-request helperu a ROI výpočtu",
),
"en" to listOf(
"Shared CredentialFormDelegate — less duplicate code across 4 ViewModels",
"Shared API import result dialog across 3 screens",
"Unified AccBotTopAppBar across 11 screens",
"Binance: switch from USDT to USDC, min order lowered to 5",
"Quick amounts: 5, 10, 25, 50, 100 (was 25–500)",
"Default DCA plan amount set to exchange minimum",
"Fix min order size display — strip trailing zeros",
"Extract KuCoin signed-request helper and ROI calculation",
),
)
),
ChangelogEntry(
versionCode = 25200,
version = "2.5.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.accbot.dca.presentation.components

import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.accbot.dca.R
import com.accbot.dca.domain.usecase.ApiImportResultState

@Composable
fun ApiImportResultDialog(
result: ApiImportResultState,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
when (result) {
is ApiImportResultState.Success -> stringResource(R.string.import_api_success_title)
is ApiImportResultState.Error -> stringResource(R.string.import_api_error_title)
}
)
},
text = {
Text(
when (result) {
is ApiImportResultState.Success -> {
if (result.imported == 0) {
stringResource(R.string.import_api_no_new)
} else {
stringResource(R.string.import_api_success_message, result.imported, result.skipped)
}
}
is ApiImportResultState.Error -> result.message
}
)
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.common_done))
}
}
)
}
Loading