Skip to content

Commit

Permalink
feat: use CrowdNode API for fees (#1324)
Browse files Browse the repository at this point in the history
* feat: use CrowdNode API for fees

* tests: update MainViewModelTest
  • Loading branch information
HashEngineering authored Nov 20, 2024
1 parent 261c7de commit 92d361c
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import retrofit2.HttpException
import java.io.IOException
import java.net.UnknownHostException
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.pow
import kotlin.time.Duration
Expand All @@ -77,6 +78,7 @@ interface CrowdNodeApi {
suspend fun deposit(amount: Coin, emptyWallet: Boolean, checkBalanceConditions: Boolean): Boolean
suspend fun withdraw(amount: Coin): Boolean
suspend fun getWithdrawalLimit(period: WithdrawalLimitPeriod): Coin
suspend fun getFee(): Double
fun hasAnyDeposits(): Boolean
fun refreshBalance(retries: Int = 0, afterWithdrawal: Boolean = false)
fun trackLinkingAccount(address: Address)
Expand Down Expand Up @@ -342,6 +344,11 @@ class CrowdNodeApiAggregator @Inject constructor(
)
}

override suspend fun getFee(): Double {
refreshFees()
return config.get(CrowdNodeConfig.FEE_PERCENTAGE) ?: FeeInfo.DEFAULT_FEE
}

override fun hasAnyDeposits(): Boolean {
val accountAddress = this.accountAddress
requireNotNull(accountAddress) { "Account address is null, make sure to sign up" }
Expand Down Expand Up @@ -854,6 +861,22 @@ class CrowdNodeApiAggregator @Inject constructor(
}
}

/**
* obtains the current CrowdNode fee on masternode rewards once per 24 hours
*/
private suspend fun refreshFees() {
val lastFeeRequest = config.get(CrowdNodeConfig.LAST_FEE_REQUEST)
if (lastFeeRequest == null || (lastFeeRequest + TimeUnit.DAYS.toMillis(1)) < System.currentTimeMillis()) {
val feeInfo = webApi.getFees(accountAddress)
log.info("crowdnode feeInfo: {}", feeInfo)
val fee = feeInfo.getNormal()?.fee
fee?.let {
config.set(CrowdNodeConfig.FEE_PERCENTAGE, fee)
config.set(CrowdNodeConfig.LAST_FEE_REQUEST, System.currentTimeMillis())
}
}
}

private fun notifyIfNeeded(message: String, tag: String) {
if (showNotificationOnResult) {
notificationService.showNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ interface CrowdNodeEndpoint {
@Path("address") address: String
): Response<List<WithdrawalLimit>>

@GET("odata/apifundings/GetFeeJson(address='{address}')")
suspend fun getFees(
@Path("address") address: String
): Response<FeeInfo>

@GET("odata/apiaddresses/IsApiAddressInUse(address='{address}')")
suspend fun isAddressInUse(
@Path("address") address: String
Expand Down Expand Up @@ -269,6 +274,26 @@ open class CrowdNodeWebApi @Inject constructor(
}
}

open suspend fun getFees(address: Address?): FeeInfo {
return try {
val response = endpoint.getFees(address?.toBase58() ?: "")

return if (response.isSuccessful) {
response.body()!!
} else {
FeeInfo.default
}
} catch (ex: Exception) {
log.error("Error in getFees: $ex")

if (ex !is IOException) {
analyticsService.logError(ex)
}

FeeInfo.default
}
}

open suspend fun isApiAddressInUse(address: Address): Pair<Boolean, Address?> {
return try {
val result = endpoint.isAddressInUse(address.toBase58())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.dash.wallet.integrations.crowdnode.model

import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

data class FeeLadder(
val name: String,
val type: String,
val amount: Double,
val fee: Double
)

/*
[
{
"Key":"FeeLadder",
"Value":"[
{
\"name\":\"Up to 10 Dash and above\",
\"type\":\"Normal\",
\"amount\":10.0,\"fee\":35.0
},
{
\"name\":\"Trustless up to 100 Dash and above\",
\"type\":\"Trustless\",
\"amount\":100.0,
\"fee\":20.0
}
]"
}
]
*/
@Parcelize
data class FeeInfo(
@SerializedName("FeeLadder") val feeLadder: @RawValue List<FeeLadder>
) : Parcelable {
companion object {
const val DEFAULT_FEE = 35.0
const val DEFAULT_AMOUNT = 100.0
const val KEY_FEELADDER = "FeeLadder"
const val TYPE_NORMAL = "Normal"
const val TYPE_TRUSTLESS = "Trustless"
val default = FeeInfo(listOf(FeeLadder("", TYPE_NORMAL, DEFAULT_AMOUNT, DEFAULT_FEE)))
}

fun getNormal() = feeLadder.find { it.type == FeeInfo.TYPE_NORMAL }
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.dash.wallet.common.services.analytics.AnalyticsConstants
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.common.ui.BalanceUIState
import org.dash.wallet.integrations.crowdnode.api.CrowdNodeApi
import org.dash.wallet.integrations.crowdnode.model.FeeInfo
import org.dash.wallet.integrations.crowdnode.model.MessageStatusException
import org.dash.wallet.integrations.crowdnode.model.OnlineAccountStatus
import org.dash.wallet.integrations.crowdnode.model.SignUpStatus
Expand Down Expand Up @@ -118,6 +119,7 @@ class CrowdNodeViewModel @Inject constructor(
val crowdNodeBalance: LiveData<BalanceUIState>
get() = _crowdNodeBalance

private var crowdNodeFee: Double = FeeInfo.DEFAULT_FEE
val dashFormat: MonetaryFormat
get() = globalConfig.format.noCode()

Expand Down Expand Up @@ -164,6 +166,12 @@ class CrowdNodeViewModel @Inject constructor(
}
}
.launchIn(viewModelScope)

config.observe(CrowdNodeConfig.FEE_PERCENTAGE)
.onEach {
crowdNodeFee = it ?: FeeInfo.DEFAULT_FEE
}
.launchIn(viewModelScope)
}

fun backupPassphrase() {
Expand Down Expand Up @@ -413,6 +421,7 @@ class CrowdNodeViewModel @Inject constructor(
}

fun getCrowdNodeAPY(): Double {
return 0.85 * getMasternodeAPY()
val withoutFees = (100.0 - crowdNodeFee) / 100
return withoutFees * getMasternodeAPY()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.dash.wallet.integrations.crowdnode.utils

import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
Expand Down Expand Up @@ -47,5 +48,7 @@ open class CrowdNodeConfig @Inject constructor(
val WITHDRAWAL_LIMIT_PER_HOUR = longPreferencesKey("withdrawal_limit_per_hour")
val WITHDRAWAL_LIMIT_PER_DAY = longPreferencesKey("withdrawal_limit_per_day")
val LAST_WITHDRAWAL_BLOCK = intPreferencesKey("last_withdrawal_block")
val FEE_PERCENTAGE = doublePreferencesKey("fee_double")
val LAST_FEE_REQUEST = longPreferencesKey("last_fee_request")
}
}
11 changes: 8 additions & 3 deletions wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import org.dash.wallet.common.transactions.TransactionUtils.isEntirelySelf
import org.dash.wallet.common.transactions.TransactionWrapper
import org.dash.wallet.common.transactions.TransactionWrapperComparator
import org.dash.wallet.common.util.toBigDecimal
import org.dash.wallet.integrations.crowdnode.api.CrowdNodeApi
import org.dash.wallet.integrations.crowdnode.transactions.FullCrowdNodeSignUpTxSetFactory
import org.slf4j.LoggerFactory
import kotlin.math.abs
Expand Down Expand Up @@ -134,7 +135,8 @@ class MainViewModel @Inject constructor(
dashPayConfig: DashPayConfig,
dashPayContactRequestDao: DashPayContactRequestDao,
private val coinJoinConfig: CoinJoinConfig,
private val coinJoinService: CoinJoinService
private val coinJoinService: CoinJoinService,
private val crowdNodeApi: CrowdNodeApi
) : BaseContactsViewModel(blockchainIdentityDataDao, dashPayProfileDao, dashPayContactRequestDao) {
companion object {
private const val THROTTLE_DURATION = 500L
Expand Down Expand Up @@ -406,7 +408,9 @@ class MainViewModel @Inject constructor(

fun getLastStakingAPY() {
viewModelScope.launch(Dispatchers.IO) {
_stakingAPY.postValue(0.85 * blockchainStateProvider.getLastMasternodeAPY())
val withoutFees = (100.0 - crowdNodeApi.getFee()) / 100
log.info("fees: without $withoutFees")
_stakingAPY.postValue(withoutFees * blockchainStateProvider.getLastMasternodeAPY())
}
}

Expand Down Expand Up @@ -538,7 +542,8 @@ class MainViewModel @Inject constructor(

if (state.isSynced()) {
viewModelScope.launch(Dispatchers.IO) {
_stakingAPY.postValue(0.85 * blockchainStateProvider.getMasternodeAPY())
val withoutFees = (100.0 - crowdNodeApi.getFee()) / 100
_stakingAPY.postValue(withoutFees * blockchainStateProvider.getMasternodeAPY())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package de.schildbach.wallet.util.viewModels

import android.os.Looper
import android.telephony.TelephonyManager
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import androidx.work.WorkManager
Expand All @@ -32,10 +33,16 @@ import de.schildbach.wallet.database.dao.InvitationsDao
import de.schildbach.wallet.ui.dashpay.utils.DashPayConfig
import de.schildbach.wallet.transactions.TxFilterType
import androidx.datastore.preferences.core.Preferences
import de.schildbach.wallet.data.CoinJoinConfig
import de.schildbach.wallet.database.dao.DashPayContactRequestDao
import de.schildbach.wallet.database.dao.UserAlertDao
import de.schildbach.wallet.database.entity.BlockchainIdentityBaseData
import de.schildbach.wallet.database.entity.BlockchainIdentityConfig
import de.schildbach.wallet.database.entity.BlockchainIdentityData
import de.schildbach.wallet.security.BiometricHelper
import de.schildbach.wallet.service.CoinJoinService
import de.schildbach.wallet.service.platform.PlatformService
import de.schildbach.wallet.service.platform.PlatformSyncService
import de.schildbach.wallet.ui.main.MainViewModel
import io.mockk.*
import junit.framework.TestCase.assertEquals
Expand All @@ -60,6 +67,7 @@ import org.dash.wallet.common.services.ExchangeRatesProvider
import org.dash.wallet.common.services.RateRetrievalState
import org.dash.wallet.common.services.TransactionMetadataProvider
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.integrations.crowdnode.api.CrowdNodeApi
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
Expand Down Expand Up @@ -88,6 +96,8 @@ class MainViewModelTest {
every { applicationContext } returns mockk()
every { mainLooper } returns Looper.getMainLooper()
}
private val platformService = mockk<PlatformService>()
private val platformSyncService = mockk<PlatformSyncService>()
private val mockIdentityData = BlockchainIdentityBaseData(-1, BlockchainIdentityData.CreationState.NONE, null, null, null, false,null, false)
private val blockchainIdentityConfigMock = mockk<BlockchainIdentityConfig> {
coEvery { loadBase() } returns mockIdentityData
Expand All @@ -103,7 +113,7 @@ class MainViewModelTest {
every { dashPayProfileDao() } returns dashPayProfileDaoMock
every { dashPayContactRequestDao() } returns mockk()
every { invitationsDao() } returns invitationsDaoMock
every { userAlertDao() } returns mockk()
every { userAlertDao() } returns userAgentDaoMock
every { transactionMetadataDocumentDao() } returns mockk()
every { transactionMetadataCacheDao() } returns mockk()
}
Expand All @@ -127,7 +137,7 @@ class MainViewModelTest {
private val transactionMetadataMock = mockk<TransactionMetadataProvider> {
every { observePresentableMetadata() } returns MutableStateFlow(mapOf())
}

private val dashPayContactRequestDao = mockk<DashPayContactRequestDao>()
private val mockDashPayConfig = mockk<DashPayConfig> {
every { observe<Long>(any()) } returns MutableStateFlow(0L)
coEvery { areNotificationsDisabled() } returns false
Expand All @@ -144,6 +154,12 @@ class MainViewModelTest {

private val platformRepo = mockk<PlatformRepo>()

private val biometricHelper = mockk<BiometricHelper>()
private val telephonyManager = mockk<TelephonyManager>()
private val coinJoinConfig = mockk<CoinJoinConfig>()
private val coinJoinService = mockk<CoinJoinService>()
private val crowdNodeApi = mockk<CrowdNodeApi>()

@get:Rule
var rule: TestRule = InstantTaskExecutorRule()

Expand Down Expand Up @@ -200,16 +216,35 @@ class MainViewModelTest {
every { savedStateMock.set<TxFilterType>(any(), any()) } just runs
}

@Test
@Test(timeout = 1000)
fun observeBlockchainState_replaying_notSynced() {
every { blockchainStateMock.observeState() } returns MutableStateFlow(BlockchainState(replaying = true))

val viewModel = spyk(
MainViewModel(
analyticsService, configMock, uiConfigMock,
exchangeRatesMock, walletDataMock, walletApp, platformRepo,
mockk(), mockk(), blockchainIdentityConfigMock, savedStateMock, transactionMetadataMock,
blockchainStateMock, mockk(), mockk(), mockk(), userAgentDaoMock, mockk(), mockDashPayConfig, mockk(), mockk()
analyticsService,
configMock,
uiConfigMock,
exchangeRatesMock,
walletDataMock,
walletApp,
platformRepo,
platformService,
platformSyncService,
blockchainIdentityConfigMock,
savedStateMock,
transactionMetadataMock,
blockchainStateMock,
biometricHelper,
telephonyManager,
invitationsDaoMock,
userAgentDaoMock,
dashPayProfileDaoMock,
mockDashPayConfig,
dashPayContactRequestDao,
coinJoinConfig,
coinJoinService,
crowdNodeApi
)
)

Expand All @@ -226,10 +261,29 @@ class MainViewModelTest {
every { blockchainStateMock.observeState() } returns MutableStateFlow(state)
val viewModel = spyk(
MainViewModel(
analyticsService, configMock, uiConfigMock,
exchangeRatesMock, walletDataMock, walletApp, platformRepo,
mockk(), mockk(), blockchainIdentityConfigMock, savedStateMock, transactionMetadataMock,
blockchainStateMock, mockk(), mockk(), mockk(), mockk(), mockk(), mockDashPayConfig, mockk(), mockk()
analyticsService,
configMock,
uiConfigMock,
exchangeRatesMock,
walletDataMock,
walletApp,
platformRepo,
platformService,
platformSyncService,
blockchainIdentityConfigMock,
savedStateMock,
transactionMetadataMock,
blockchainStateMock,
biometricHelper,
telephonyManager,
invitationsDaoMock,
userAgentDaoMock,
dashPayProfileDaoMock,
mockDashPayConfig,
dashPayContactRequestDao,
coinJoinConfig,
coinJoinService,
crowdNodeApi
)
)

Expand Down

0 comments on commit 92d361c

Please sign in to comment.