Skip to content

Commit

Permalink
feat(crowdnode): limit withdrawals to once per block (#1229)
Browse files Browse the repository at this point in the history
* feat(crowdnode): limit withdrawals to once per block

* fix: eliminate progress dialog after withdraw error (per block)

* refactor: remove LiveData from BlockchainStateDao

* tests: fix CrowdNodeApiAggregatorTest

* chore: replace hard coded text with string resources
  • Loading branch information
HashEngineering authored Nov 28, 2023
1 parent fdb5397 commit d9ba8e7
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import java.util.*
data class BlockchainState(var bestChainDate: Date?,
var bestChainHeight: Int,
var replaying: Boolean,
var impediments: Set<Impediment>,
var impediments: MutableSet<Impediment>,
var chainlockHeight: Int,
var mnlistHeight: Int,
var percentageSync: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.dash.wallet.common.data.Resource
import org.dash.wallet.common.data.ServiceName
import org.dash.wallet.common.data.Status
import org.dash.wallet.common.data.TaxCategory
import org.dash.wallet.common.services.BlockchainStateProvider
import org.dash.wallet.common.services.LeftoverBalanceException
import org.dash.wallet.common.services.NotificationService
import org.dash.wallet.common.services.TransactionMetadataProvider
Expand Down Expand Up @@ -94,6 +95,7 @@ class CrowdNodeApiAggregator @Inject constructor(
private val config: CrowdNodeConfig,
private val globalConfig: Configuration,
private val transactionMetadataProvider: TransactionMetadataProvider,
private val blockchainStateProvider: BlockchainStateProvider,
@ApplicationContext private val appContext: Context
) : CrowdNodeApi {
companion object {
Expand Down Expand Up @@ -299,6 +301,8 @@ class CrowdNodeApiAggregator @Inject constructor(
if (result.messageStatus.lowercase() == MESSAGE_RECEIVED_STATUS) {
log.info("Withdrawal request sent successfully")
refreshBalance(retries = 3, afterWithdrawal = true)
val currentBlockHeight = blockchainStateProvider.getState()?.bestChainHeight ?: -1
config.set(CrowdNodeConfig.LAST_WITHDRAWAL_BLOCK, currentBlockHeight)
true
} else {
log.info("Withdrawal request not received, status: ${result.messageStatus}. Result: ${result.result}")
Expand Down Expand Up @@ -331,6 +335,9 @@ class CrowdNodeApiAggregator @Inject constructor(
config.get(CrowdNodeConfig.WITHDRAWAL_LIMIT_PER_DAY)
?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_DAY.value
}
else -> {
0L
}
}
)
}
Expand Down Expand Up @@ -824,6 +831,11 @@ class CrowdNodeApiAggregator @Inject constructor(
if (withdrawalsLast24h.add(value) > perDayLimit) {
throw WithdrawalLimitsException(perDayLimit, WithdrawalLimitPeriod.PerDay)
}
val lastWithdrawBlock = config.get(CrowdNodeConfig.LAST_WITHDRAWAL_BLOCK)
val currentBlockHeight = blockchainStateProvider.getState()?.bestChainHeight ?: -1
if (lastWithdrawBlock != null && lastWithdrawBlock >= currentBlockHeight) {
throw WithdrawalLimitsException(value, WithdrawalLimitPeriod.PerBlock)
}
}

private fun refreshWithdrawalLimits() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ data class WithdrawalLimit(
enum class WithdrawalLimitPeriod {
PerTransaction,
PerHour,
PerDay
PerDay,
PerBlock
}

data class WithdrawalLimitsException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.dash.wallet.common.ui.viewBinding
import org.dash.wallet.integrations.crowdnode.R
import org.dash.wallet.integrations.crowdnode.databinding.DialogWithdrawalLimitsBinding
import org.dash.wallet.integrations.crowdnode.model.WithdrawalLimitPeriod
import java.lang.IllegalArgumentException

class WithdrawalLimitsInfoDialog(
private val limitPerTx: Coin,
Expand Down Expand Up @@ -79,6 +80,9 @@ class WithdrawalLimitsInfoDialog(
binding.perDayLimit.setTextColor(warningColor)
TextViewCompat.setCompoundDrawableTintList(binding.perDayLimit, colorStateLit)
}
else -> {
throw IllegalArgumentException("highlightedLimit $highlightedLimit not supported")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,30 +339,39 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) {
}

private suspend fun showWithdrawalLimitsError(period: WithdrawalLimitPeriod) {
val limits = viewModel.getWithdrawalLimits()
val okButtonText = if (period == WithdrawalLimitPeriod.PerTransaction) {
if (viewModel.onlineAccountStatus == OnlineAccountStatus.Done) {
getString(R.string.read_withdrawal_policy)
if (period == WithdrawalLimitPeriod.PerBlock) {
AdaptiveDialog.create(
R.drawable.ic_warning,
getString(R.string.crowdnode_withdrawal_limits_per_block_title),
getString(R.string.crowdnode_withdrawal_limits_per_block_message),
getString(R.string.button_okay)
).showAsync(requireActivity())
} else {
val limits = viewModel.getWithdrawalLimits()
val okButtonText = if (period == WithdrawalLimitPeriod.PerTransaction) {
if (viewModel.onlineAccountStatus == OnlineAccountStatus.Done) {
getString(R.string.read_withdrawal_policy)
} else {
getString(R.string.online_account_create)
}
} else {
getString(R.string.online_account_create)
""
}
} else {
""
}

val doAction = WithdrawalLimitsInfoDialog(
limits[0],
limits[1],
limits[2],
highlightedLimit = period,
okButtonText = okButtonText
).showAsync(requireActivity())

if (doAction == true) {
if (viewModel.onlineAccountStatus == OnlineAccountStatus.Done) {
openWithdrawalPolicy()
} else {
safeNavigate(TransferFragmentDirections.transferToOnlineAccountEmail())
val doAction = WithdrawalLimitsInfoDialog(
limits[0],
limits[1],
limits[2],
highlightedLimit = period,
okButtonText = okButtonText
).showAsync(requireActivity())

if (doAction == true) {
if (viewModel.onlineAccountStatus == OnlineAccountStatus.Done) {
openWithdrawalPolicy()
} else {
safeNavigate(TransferFragmentDirections.transferToOnlineAccountEmail())
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ open class CrowdNodeConfig @Inject constructor(
val WITHDRAWAL_LIMIT_PER_TX = longPreferencesKey("withdrawal_limit_per_tx")
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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@
<string name="crowdnode_withdrawal_limits_message">Due to CrowdNode’s terms of service users can withdraw no more than:</string>
<string name="crowdnode_withdraw_without_limits_online">Withdraw without limits with an online account on CrowdNode website.</string>
<string name="crowdnode_withdrawal_limits_error">This error is most likely due to exceeding CrowdNode withdrawal limits. Try again later.</string>
<string name="crowdnode_withdrawal_limits_per_block_title">Please wait before initiating the next withdrawal</string>
<string name="crowdnode_withdrawal_limits_per_block_message">Please wait 5 minutes before initiating another withdrawal.</string>
<string name="per_transaction">per transaction</string>
<string name="per_hour">per hour</string>
<string name="per_day">per 24 hours</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class CrowdNodeApiAggregatorTest {
localConfig.stub {
onBlocking { get(CrowdNodeConfig.ONLINE_ACCOUNT_STATUS) } doReturn OnlineAccountStatus.Linking.ordinal
}
val api = CrowdNodeApiAggregator(webApi, blockchainApi, walletData, mock(), mock(), localConfig, globalConfig, mock(), mock()) // ktlint-disable max-line-length
val api = CrowdNodeApiAggregator(webApi, blockchainApi, walletData, mock(), mock(), localConfig, globalConfig, mock(), mock(), mock()) // ktlint-disable max-line-length
api.restoreStatus()
api.stopTrackingLinked()

Expand All @@ -112,7 +112,7 @@ class CrowdNodeApiAggregatorTest {
}
val api = CrowdNodeApiAggregator(
mock(), blockchainApi, walletData, mock(), mock(),
localConfig, globalConfig, mock(), mock()
localConfig, globalConfig, mock(), mock(), mock()
)
api.restoreStatus()
api.stopTrackingLinked()
Expand All @@ -139,7 +139,7 @@ class CrowdNodeApiAggregatorTest {
blockchainApi.stub {
on { getFullSignUpTxSet() } doReturn mockFullSet
}
val api = CrowdNodeApiAggregator(webApi, blockchainApi, walletData, mock(), mock(), localConfig, globalConfig, mock(), mock()) // ktlint-disable max-line-length
val api = CrowdNodeApiAggregator(webApi, blockchainApi, walletData, mock(), mock(), localConfig, globalConfig, mock(), mock(), mock()) // ktlint-disable max-line-length
api.restoreStatus()
assertEquals(SignUpStatus.Finished, api.signUpStatus.value)
assertEquals(OnlineAccountStatus.None, api.onlineAccountStatus.value)
Expand All @@ -159,7 +159,7 @@ class CrowdNodeApiAggregatorTest {
}
val api = CrowdNodeApiAggregator(
webApi, blockchainApi, walletData, mock(), mock(),
localConfig, globalConfig, mock(), mock()
localConfig, globalConfig, mock(), mock(), mock()
)
api.restoreStatus()
api.refreshBalance()
Expand All @@ -177,7 +177,7 @@ class CrowdNodeApiAggregatorTest {
}
val api = CrowdNodeApiAggregator(
webApi, blockchainApi, walletData, mock(), mock(),
localConfig, globalConfig, mock(), mock()
localConfig, globalConfig, mock(), mock(), mock()
)
api.restoreStatus()
assertEquals(SignUpStatus.LinkedOnline, api.signUpStatus.value)
Expand All @@ -204,7 +204,7 @@ class CrowdNodeApiAggregatorTest {
}
val api = CrowdNodeApiAggregator(
webApi, blockchainApi, walletData, mock(), mock(),
localConfig, globalConfig, mock(), mock()
localConfig, globalConfig, mock(), mock(), mock()
)
api.restoreStatus()
assertEquals(SignUpStatus.Finished, api.signUpStatus.value)
Expand Down Expand Up @@ -238,7 +238,7 @@ class CrowdNodeApiAggregatorTest {

val api = CrowdNodeApiAggregator(
webApi, blockchainApi, walletData, mock(), mock(),
localConfig, globalConfig, mock(), mock()
localConfig, globalConfig, mock(), mock(), mock()
)
api.restoreStatus()

Expand Down
29 changes: 4 additions & 25 deletions wallet/src/de/schildbach/wallet/WalletApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteException;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build;
Expand All @@ -52,9 +51,6 @@
import androidx.work.WorkManager;

import com.google.common.base.Stopwatch;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
Expand All @@ -70,7 +66,6 @@
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.UnreadableWalletException;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.WalletExtension;
import org.bitcoinj.wallet.WalletProtobufSerializer;
import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension;
import org.bitcoinj.wallet.authentication.AuthenticationKeyUsage;
Expand All @@ -86,6 +81,7 @@
import org.dash.wallet.features.exploredash.utils.DashDirectConstants;
import org.dash.wallet.integrations.coinbase.service.CoinBaseClientConstants;

import de.schildbach.wallet.service.BlockchainStateDataProvider;
import de.schildbach.wallet.service.PackageInfoProvider;
import de.schildbach.wallet.service.WalletFactory;
import de.schildbach.wallet.transactions.MasternodeObserver;
Expand Down Expand Up @@ -123,8 +119,6 @@
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import dagger.hilt.android.HiltAndroidApp;
import org.dash.wallet.common.data.entity.BlockchainState;
import de.schildbach.wallet.database.dao.BlockchainStateDao;
import de.schildbach.wallet.service.BlockchainService;
import de.schildbach.wallet.service.BlockchainServiceImpl;
import de.schildbach.wallet.service.BlockchainSyncJobService;
Expand Down Expand Up @@ -186,7 +180,7 @@ public class WalletApplication extends MultiDexApplication
@Inject
HiltWorkerFactory workerFactory;
@Inject
BlockchainStateDao blockchainStateDao;
BlockchainStateDataProvider blockchainStateDataProvider;
@Inject
CrowdNodeConfig crowdNodeConfig;
@Inject
Expand Down Expand Up @@ -749,9 +743,7 @@ public void stopBlockchainService() {
}

public void resetBlockchainState() {
Executors.newSingleThreadExecutor().execute(() -> {
blockchainStateDao.save(new BlockchainState(true));
});
blockchainStateDataProvider.resetBlockchainState();
}

public void resetBlockchain() {
Expand All @@ -768,20 +760,7 @@ public void resetBlockchain() {
}

private void resetBlockchainSyncProgress() {
Executors.newSingleThreadExecutor().execute(() -> {
BlockchainState blockchainState;

try {
blockchainState = blockchainStateDao.loadSync();
} catch (SQLiteException ex) {
blockchainState = null;
}

if (blockchainState != null) {
blockchainState.setPercentageSync(0);
blockchainStateDao.save(blockchainState);
}
});
blockchainStateDataProvider.resetBlockchainSyncProgress();
}

public void replaceWallet(final Wallet newWallet) {
Expand Down
13 changes: 3 additions & 10 deletions wallet/src/de/schildbach/wallet/database/dao/BlockchainStateDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package de.schildbach.wallet.database.dao

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
Expand All @@ -32,23 +31,17 @@ import org.dash.wallet.common.data.entity.BlockchainState
abstract class BlockchainStateDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract fun insert(blockchainState: BlockchainState)
protected abstract suspend fun insert(blockchainState: BlockchainState)

fun save(blockchainState: BlockchainState) {
suspend fun saveState(blockchainState: BlockchainState) {
if (blockchainState.replaying && blockchainState.percentageSync == 100) {
blockchainState.replaying = false
}
insert(blockchainState)
}

@Query("SELECT * FROM blockchain_state LIMIT 1")
abstract fun load(): LiveData<BlockchainState?>

@Query("SELECT * FROM blockchain_state LIMIT 1")
abstract fun loadSync(): BlockchainState?

@Query("SELECT * FROM blockchain_state LIMIT 1")
abstract suspend fun get(): BlockchainState?
abstract suspend fun getState(): BlockchainState?

@Query("SELECT * FROM blockchain_state LIMIT 1")
abstract fun observeState(): Flow<BlockchainState?>
Expand Down
Loading

0 comments on commit d9ba8e7

Please sign in to comment.