Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a746d3d
Merge pull request #53 from gruntsoftware/release/v4.4.1
kcw-grunt Mar 30, 2025
ba8ea45
Revert "Updated the APIManager"
kcw-grunt Mar 31, 2025
638ce3a
Reverted the Kotlin APIManager.
kcw-grunt Mar 31, 2025
5344a96
Add version and code to welcome screen
kcw-grunt Mar 31, 2025
ebc3bb9
reset padding
kcw-grunt Mar 31, 2025
03c68b8
Merge pull request #56 from gruntsoftware/feature/add-version-issue-108
kcw-grunt Apr 1, 2025
c770f01
Revert "Updated the APIManager"
kcw-grunt Mar 31, 2025
f8c1bd5
Reverted the Kotlin APIManager.
kcw-grunt Mar 31, 2025
ff58b57
fix: fix crash when FragmentSignal dismissed
andhikayuana Apr 4, 2025
d64ea93
fix: fix sync after wipe
andhikayuana Apr 7, 2025
277f387
Merge branch 'hotfix/api-manager-fix' of https://github.com/brainwall…
kcw-grunt Apr 7, 2025
9a8a20f
fix: fix wrong lifecycle to trigger callback at FragmentSignal
andhikayuana Apr 7, 2025
832fa36
Update issue templates
kcw-grunt Apr 8, 2025
b294cd6
updated core changes
kcw-grunt Apr 8, 2025
4ed8af3
Merge pull request #57 from gruntsoftware/hotfix/api-manager-fix
kcw-grunt Apr 8, 2025
089995a
Rename .java to .kt
andhikayuana Apr 11, 2025
7e85987
chore: refactor BRApiManager & APIClient
andhikayuana Apr 11, 2025
0aa41e7
chore: remove unused part at APIClient
andhikayuana Apr 14, 2025
0cc29cb
feat: wip new peer discovery
andhikayuana Apr 14, 2025
54d2d9e
feat: implement selected peer ip address from cache (fetched from API)
andhikayuana Apr 14, 2025
c6923b1
feat: implement selected peer ip address from cache (fetched from API)
andhikayuana Apr 14, 2025
ef4e6d2
feat: filter out peers with NODE_NETWORK, NODE_BLOOM
andhikayuana Apr 15, 2025
2336939
fix: race condition when clear shared prefs values after wipeAll
andhikayuana Apr 16, 2025
51a4d9a
Merge pull request #62 from gruntsoftware/hotfix/prefs-race-condition
kcw-grunt Apr 16, 2025
62afb9d
Merge pull request #61 from gruntsoftware/feat/new-peer-discovery
kcw-grunt Apr 16, 2025
391e319
Updating the core library
kcw-grunt Apr 16, 2025
836b6f0
Chore/revert pre peer discovery (Android) (#69)
andhikayuana Apr 25, 2025
3caa1a7
feat: remove unused activity (ImportActivity) at AndroidManifest.xml …
andhikayuana Apr 29, 2025
a324444
fix: fix crash IllegalStateException: cannot make a new request becau…
andhikayuana May 2, 2025
1f9c5d0
code bump
kcw-grunt May 2, 2025
7ca17d3
Feat/move tx fee (#74)
andhikayuana May 5, 2025
e7fb2b7
fix: fix dismiss allow state loss (#76)
andhikayuana May 7, 2025
3d8ef98
fix: fix typo at strings.xml (#75)
andhikayuana May 7, 2025
25be1a8
feat: new UI for receive and topup flow (moonpay integration) (#72)
andhikayuana May 12, 2025
18eda46
version and code bump
kcw-grunt May 12, 2025
e439e88
Merge branch 'main' into release/v4.5.0
kcw-grunt May 12, 2025
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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ android {
applicationId = "ltd.grunt.brainwallet"
minSdk = 29
targetSdk = 34
versionCode = 202504251
versionName = "v4.4.7"
versionCode = 202505121
versionName = "v4.5.0"

multiDexEnabled = true
base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})")
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,6 @@
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".presenter.activities.settings.ImportActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".presenter.activities.ReEnterPinActivity"
android:exported="true"
Expand Down
58 changes: 52 additions & 6 deletions app/src/main/java/com/brainwallet/data/model/Fee.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package com.brainwallet.data.model

import android.annotation.SuppressLint
import com.brainwallet.R
import com.brainwallet.tools.manager.FeeManager.ECONOMY
import com.brainwallet.tools.manager.FeeManager.FeeType
import com.brainwallet.tools.manager.FeeManager.LUXURY
import com.brainwallet.tools.manager.FeeManager.REGULAR
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.math.ceil
import kotlin.math.round

@Serializable
data class Fee(
@JvmField
@SerialName("fee_per_kb")
@SerialName("fee_per_kb_luxury")
var luxury: Long,
@JvmField
@SerialName("fee_per_kb_economy")
@SerialName("fee_per_kb")
var regular: Long,
@JvmField
@SerialName("fee_per_kb_luxury")
@SerialName("fee_per_kb_economy")
var economy: Long,
var timestamp: Long
) {
companion object {
//from legacy
Expand All @@ -25,13 +32,52 @@ data class Fee(
private const val defaultLuxuryFeePerKB: Long = 66746L
private const val defaultTimestamp: Long = 1583015199122L

// {"fee_per_kb":5289,"fee_per_kb_economy":2645,"fee_per_kb_luxury":10578}

@JvmStatic
val Default = Fee(
defaultLuxuryFeePerKB,
defaultRegularFeePerKB,
defaultEconomyFeePerKB,
defaultTimestamp
)

}
}


data class FeeOption(
@FeeType
val type: String,
val feePerKb: Long,
val labelStringId: Int,
)

fun Fee.toFeeOptions(): List<FeeOption> = listOf(
FeeOption(
type = ECONOMY,
feePerKb = economy,
labelStringId = R.string.network_fee_options_low
),
FeeOption(
type = REGULAR,
feePerKb = regular,
labelStringId = R.string.network_fee_options_medium
),
FeeOption(
type = LUXURY,
feePerKb = luxury,
labelStringId = R.string.network_fee_options_top
),
)

fun FeeOption.getFiat(currencyEntity: CurrencyEntity): Float {
val satoshisPerLtc = 100_000_000.0
val feeInLtc = feePerKb / satoshisPerLtc
return (feeInLtc * currencyEntity.rate).toFloat()
}

@SuppressLint("DefaultLocale")
fun FeeOption.getFiatFormatted(currencyEntity: CurrencyEntity): String {
val fiatValue = getFiat(currencyEntity)
val formatted = String.format("%.3f", fiatValue)
return "${currencyEntity.symbol}$formatted"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.brainwallet.data.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MoonpayCurrencyLimit(
@SerialName("data") val data: Data = Data()
) {
@Serializable
data class Data(
@SerialName("paymentMethod") val paymentMethod: String = "",
@SerialName("quoteCurrency") val quoteCurrency: CurrencyLimit = CurrencyLimit(),
@SerialName("baseCurrency") val baseCurrency: CurrencyLimit = CurrencyLimit(),
@SerialName("areFeesIncluded") val areFeesIncluded: Boolean = false
)

@Serializable
data class CurrencyLimit(
val code: String = "usd",
@SerialName("minBuyAmount") val min: Float = 21f,
@SerialName("maxBuyAmount") val max: Float = 29849f
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.brainwallet.data.model


data class QuickFiatAmountOption(
val symbol: String = "custom",
val value: Float = -1f
)

fun QuickFiatAmountOption.isCustom(): Boolean = symbol == "custom" && value == -1f

fun QuickFiatAmountOption.getFormattedText(): String = "${symbol}${value.toInt()}"
59 changes: 52 additions & 7 deletions app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.brainwallet.data.repository

import android.content.Context
import android.content.SharedPreferences
import androidx.core.net.toUri
import com.brainwallet.BuildConfig
import com.brainwallet.data.model.CurrencyEntity
import com.brainwallet.data.model.Fee
import com.brainwallet.data.model.MoonpayCurrencyLimit
import com.brainwallet.data.source.RemoteApiSource
import com.brainwallet.data.source.fetchWithCache
import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse
import com.brainwallet.tools.manager.BRSharedPrefs
import com.brainwallet.tools.manager.FeeManager
import com.brainwallet.tools.sqlite.CurrencyDataSource
Expand All @@ -13,10 +19,17 @@ interface LtcRepository {

suspend fun fetchFeePerKb(): Fee

suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit

suspend fun fetchBuyQuote(params: Map<String, String>): GetMoonpayBuyQuoteResponse

suspend fun fetchMoonpaySignedUrl(params: Map<String, String>): String

class Impl(
private val context: Context,
private val remoteApiSource: RemoteApiSource,
private val currencyDataSource: CurrencyDataSource
private val currencyDataSource: CurrencyDataSource,
private val sharedPreferences: SharedPreferences,
) : LtcRepository {

//todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies
Expand All @@ -42,18 +55,50 @@ interface LtcRepository {
}

override suspend fun fetchFeePerKb(): Fee {
return runCatching {
val fee = remoteApiSource.getFeePerKb()
return sharedPreferences.fetchWithCache(
key = PREF_KEY_NETWORK_FEE_PER_KB,
cachedAtKey = PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT,
cacheTimeMs = 6 * 60 * 60 * 1000,
fetchData = {
remoteApiSource.getFeePerKb()
},
defaultValue = Fee.Default
)
}

//todo: cache
override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit {
return sharedPreferences.fetchWithCache(
key = "${PREF_KEY_BUY_LIMITS_PREFIX}${baseCurrencyCode.lowercase()}",
cachedAtKey = "${PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT}${baseCurrencyCode.lowercase()}",
cacheTimeMs = 5 * 60 * 1000, //5 minutes
fetchData = {
remoteApiSource.getMoonpayCurrencyLimit(baseCurrencyCode)
}
)
}

override suspend fun fetchBuyQuote(params: Map<String, String>): GetMoonpayBuyQuoteResponse =
remoteApiSource.getBuyQuote(params)

return fee
}.getOrElse { Fee.Default }
override suspend fun fetchMoonpaySignedUrl(params: Map<String, String>): String {
return remoteApiSource.getMoonpaySignedUrl(params)
.signedUrl.toUri()
.buildUpon()
.apply {
if (BuildConfig.DEBUG) {
authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com
}
}
.build()
.toString()
}

}

companion object {

const val PREF_KEY_NETWORK_FEE_PER_KB = "network_fee_per_kb"
const val PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT = "${PREF_KEY_NETWORK_FEE_PER_KB}_cached_at"
const val PREF_KEY_BUY_LIMITS_PREFIX = "buy_limits:" //e.g. buy_limits:usd
const val PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT = "buy_limits_cached_at:"
}
}
45 changes: 45 additions & 0 deletions app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.brainwallet.data.source

import android.content.SharedPreferences
import androidx.core.content.edit
import com.brainwallet.di.json
import kotlinx.serialization.encodeToString

/**
* Generic function to handle caching data in shared preferences
* @param key The key to store the data under
* @param cachedAtKey The key to store the timestamp under
* @param cacheTimeMs How long the cache should be valid in milliseconds
* @param fetchData Suspending function to fetch new data
* @return The data, either from cache or freshly fetched
*/
suspend inline fun <reified T> SharedPreferences.fetchWithCache(
key: String,
cachedAtKey: String,
cacheTimeMs: Long,
crossinline fetchData: suspend () -> T,
defaultValue: T? = null
): T {
val lastUpdateTime = getLong(cachedAtKey, 0)
val currentTime = System.currentTimeMillis()
val cached = getString(key, null)
?.let { runCatching { json.decodeFromString<T>(it) }.getOrNull() }

// Return cached value if it exists and is not expired
if (cached != null && (currentTime - lastUpdateTime) < cacheTimeMs) {
return cached
}

return runCatching {
// Fetch fresh data
val result = fetchData()
// Save to cache
edit {
putString(key, json.encodeToString(result))
putLong(cachedAtKey, currentTime)
}
result
}.getOrElse {
cached ?: (defaultValue ?: throw it)
}
}
22 changes: 20 additions & 2 deletions app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ package com.brainwallet.data.source

import com.brainwallet.data.model.CurrencyEntity
import com.brainwallet.data.model.Fee
import com.brainwallet.data.model.MoonpayCurrencyLimit
import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse
import com.brainwallet.data.source.response.GetMoonpaySignUrlResponse
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap

//TODO
interface RemoteApiSource {
Expand All @@ -13,6 +18,19 @@ interface RemoteApiSource {
@GET("v1/fee-per-kb")
suspend fun getFeePerKb(): Fee

// https://prod.apigsltd.net/moonpay/buy?address=ltc1qjnsg3p9rt4r4vy7ncgvrywdykl0zwhkhcp8ue0&code=USD&idate=1742331930290&uid=ec51fa950b271ff3
// suspend fun getMoonPayBuy()
@GET("v1/moonpay/ltc-to-fiat-limits")
suspend fun getMoonpayCurrencyLimit(
@Query("baseCurrencyCode") baseCurrencyCode: String
): MoonpayCurrencyLimit

@GET("v1/moonpay/sign-url")
suspend fun getMoonpaySignedUrl(
@QueryMap params: Map<String, String>
): GetMoonpaySignUrlResponse

@GET("v1/moonpay/buy-quote")
suspend fun getBuyQuote(
@QueryMap params: Map<String, String>
): GetMoonpayBuyQuoteResponse

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.brainwallet.data.source.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GetMoonpayBuyQuoteResponse(
@SerialName("data") val data: Data = Data()

) {
@Serializable
data class Data(
val totalAmount: Float = 0f,
val baseCurrencyCode: String = "usd",
val quoteCurrencyAmount: Float = 0f,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.brainwallet.data.source.response

import kotlinx.serialization.Serializable

@Serializable
data class GetMoonpaySignUrlResponse(
val signedUrl: String
)
Loading