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
4 changes: 2 additions & 2 deletions common/src/main/java/org/dash/wallet/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ public class Constants {
public static final char CURRENCY_MINUS_SIGN = '\uff0d';
public static final String PREFIX_ALMOST_EQUAL_TO = Character.toString(CHAR_ALMOST_EQUAL_TO) + CHAR_THIN_SPACE;

public static Coin MAX_MONEY = MainNetParams.get().getMaxMoney();

public static final int REQUEST_CODE_BUY_SELL = 100;
public static final int USER_BUY_SELL_DASH = 101;
public static final int RESULT_CODE_GO_HOME = 100;

public static Coin MAX_MONEY = MainNetParams.get().getMaxMoney();
public static final Coin ECONOMIC_FEE = Coin.valueOf(1000);
public static final MonetaryFormat SEND_PAYMENT_LOCAL_FORMAT = new MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()).minDecimals(2).optionalDecimals();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.bitcoinj.core.Address
import org.bitcoinj.core.Transaction
import org.bitcoinj.script.ScriptPattern

class IgnoreAddressTxFilter(private val ignoreAddress: Address): TransactionFilter {
class NotFromAddressTxFilter(private val ignoreAddress: Address): TransactionFilter {
override fun matches(tx: Transaction): Boolean {
val networkParameters = ignoreAddress.parameters

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,17 @@ class AdaptiveDialog(@LayoutRes private val layout: Int): DialogFragment() {
)
}

suspend fun withProgress(message: String, activity: FragmentActivity, action: suspend () -> Unit) {
suspend fun <T> withProgress(
message: String,
activity: FragmentActivity,
action: suspend () -> T
): T {
val dialog = progress(message)
dialog.show(activity) { }
action.invoke()
val result = action.invoke()
dialog.dismiss()

return result
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class EnterAmountViewModel @Inject constructor(
val dashToFiatDirection: LiveData<Boolean>
get() = _dashToFiatDirection

private val _minAmount = MutableLiveData(Coin.ZERO)
val minAmount: LiveData<Coin>
get() = _minAmount

private val _maxAmount = MutableLiveData(Coin.ZERO)
val maxAmount: LiveData<Coin>
get() = _maxAmount
Expand All @@ -64,20 +68,19 @@ class EnterAmountViewModel @Inject constructor(
get() = _amount

val canContinue: LiveData<Boolean>
get() = MediatorLiveData<Boolean>().also { liveData ->
fun getValue(amount: Coin, maxAmount: Coin): Boolean {
return amount > Coin.ZERO && (maxAmount == Coin.ZERO || amount <= maxAmount)
}
get() = MediatorLiveData<Boolean>().apply {
fun canContinue(): Boolean {
val amount = _amount.value ?: Coin.ZERO
val minAmount = _minAmount.value ?: Coin.ZERO
val maxAmount = _maxAmount.value ?: Coin.ZERO

liveData.addSource(_amount) {
liveData.value = getValue(it, _maxAmount.value ?: Coin.ZERO)
}
liveData.addSource(_maxAmount) {
liveData.value = getValue(_amount.value ?: Coin.ZERO, it)
}
liveData.addSource(_dashToFiatDirection) {
liveData.value = getValue(_amount.value ?: Coin.ZERO, _maxAmount.value ?: Coin.ZERO)
return amount > minAmount && (maxAmount == Coin.ZERO || amount <= maxAmount)
}

addSource(_amount) { value = canContinue() }
addSource(_minAmount) { value = canContinue() }
addSource(_maxAmount) { value = canContinue() }
addSource(_dashToFiatDirection) { value = canContinue() }
}

init {
Expand All @@ -90,4 +93,8 @@ class EnterAmountViewModel @Inject constructor(
fun setMaxAmount(coin: Coin) {
_maxAmount.value = coin
}

fun setMinAmount(coin: Coin) {
_minAmount.value = coin
}
}
2 changes: 2 additions & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@
<string name="max">Max</string>
<string name="select_currency">Select Currency</string>
<string name="exchange_rate_template">%1$s DASH = %2$s</string>
<string name="send_coins_error_dusty_send">The amount is too small to send</string>
<string name="send_coins_error_insufficient_money">Insufficient funds</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ import kotlinx.coroutines.flow.first
import org.bitcoinj.core.Address
import org.bitcoinj.core.Coin
import org.bitcoinj.core.Transaction
import org.dash.wallet.common.Constants
import org.dash.wallet.common.WalletDataProvider
import org.dash.wallet.common.data.Resource
import org.dash.wallet.common.services.NotificationService
import org.dash.wallet.common.services.SendPaymentService
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.common.transactions.LockedTransaction
import org.dash.wallet.integrations.crowdnode.R
import org.dash.wallet.integrations.crowdnode.model.ApiCode
import org.dash.wallet.integrations.crowdnode.transactions.*
import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants
import org.dash.wallet.integrations.crowdnode.utils.ModuleConfiguration
Expand All @@ -49,6 +51,7 @@ import java.math.RoundingMode
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.min

enum class SignUpStatus {
NotStarted,
Expand All @@ -69,7 +72,8 @@ interface CrowdNodeApi {

fun persistentSignUp(accountAddress: Address)
suspend fun signUp(accountAddress: Address)
suspend fun deposit(accountAddress: Address, amount: Coin): Boolean
suspend fun deposit(amount: Coin): Boolean
suspend fun withdraw(amount: Coin): Boolean
fun refreshBalance(retries: Int = 0)
suspend fun reset()
}
Expand Down Expand Up @@ -151,7 +155,7 @@ class CrowdNodeBlockchainApi @Inject constructor(

notifyIfNeeded(appContext.getString(R.string.crowdnode_account_ready), "crowdnode_ready")
} catch (ex: Exception) {
log.info("CrowdNode error: $ex")
log.error("CrowdNode error: $ex")
analyticsService.logError(ex, "status: ${signUpStatus.value}")

apiError.value = ex
Expand All @@ -161,13 +165,16 @@ class CrowdNodeBlockchainApi @Inject constructor(
}
}

override suspend fun deposit(accountAddress: Address, amount: Coin): Boolean {
override suspend fun deposit(amount: Coin): Boolean {
val accountAddress = this.accountAddress
requireNotNull(accountAddress) { "Account address is null, make sure to sign up" }

return try {
apiError.value = null
val topUpTx = topUpAddress(accountAddress, amount)
val topUpTx = topUpAddress(accountAddress, amount + Constants.ECONOMIC_FEE)
log.info("topUpTx id: ${topUpTx.txId}")
val crowdNodeAddress = CrowdNodeConstants.getCrowdNodeAddress(params)
val depositTx = paymentService.sendCoins(crowdNodeAddress, amount, accountAddress, true)
val depositTx = paymentService.sendCoins(crowdNodeAddress, amount, accountAddress)
log.info("depositTx id: ${depositTx.txId}")

responseScope.launch {
Expand All @@ -193,34 +200,76 @@ class CrowdNodeBlockchainApi @Inject constructor(
}
}

override suspend fun withdraw(amount: Coin): Boolean {
val accountAddress = this.accountAddress
requireNotNull(accountAddress) { "Account address is null, make sure to sign up" }

val balance = this.balance.value.data ?: Coin.ZERO
require(amount <= balance) { "Amount is larger than CrowdNode balance" }

return try {
apiError.value = null

val maxPermil = ApiCode.WithdrawAll.code
val requestPermil = min(amount.value * maxPermil / balance.value, maxPermil)
Comment on lines +213 to +214

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "mil" 1000th or 0.1%?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One per mil is 1/1000 or 0.1%: https://en.wikipedia.org/wiki/Per_mille

val requestValue = CrowdNodeConstants.API_OFFSET + Coin.valueOf(requestPermil)
val topUpTx = topUpAddress(accountAddress, requestValue + Constants.ECONOMIC_FEE)
log.info("topUpTx id: ${topUpTx.txId}")

val crowdNodeAddress = CrowdNodeConstants.getCrowdNodeAddress(params)
val withdrawTx = paymentService.sendCoins(crowdNodeAddress, requestValue, accountAddress)
log.info("withdrawTx id: ${withdrawTx.txId}")

responseScope.launch {
val errorResponse = CrowdNodeErrorResponse(params, requestValue)
val tx = walletDataProvider.observeTransactions(
CrowdNodeWithdrawalQueueResponse(params),
errorResponse
).first()
log.info("got withdrawal queue response: ${tx.txId}")

if (errorResponse.matches(tx)) {
val ex = CrowdNodeException("Withdraw error")
handleError(ex, appContext.getString(R.string.crowdnode_withdraw_error))
}
}

return true
} catch (ex: Exception) {
handleError(ex, appContext.getString(R.string.crowdnode_withdraw_error))
false
}
}

override fun refreshBalance(retries: Int) {
responseScope.launch {
val lastBalance = config.lastBalance.first()
var currentBalance = Resource.loading(Coin.valueOf(lastBalance))
balance.value = currentBalance

for (i in 0..retries) {
if (i != 0) {
delay(TimeUnit.SECONDS.toMillis(pow(5, i)))
}

balance.value = Resource.loading()
val newBalance = resolveBalance()
balance.value = newBalance
currentBalance = resolveBalance()

if (lastBalance != newBalance.data?.value) {
if (lastBalance != currentBalance.data?.value) {
// balance changed, no need to retry anymore
break
}
}

balance.value = currentBalance
}
}


override suspend fun reset() {
log.info("reset is triggered")
signUpStatus.value = SignUpStatus.NotStarted
accountAddress = null
apiError.value = null
config.setCrowdNodeError("")
config.clearAll()
}

private fun restoreStatus() {
Expand Down Expand Up @@ -308,6 +357,7 @@ class CrowdNodeBlockchainApi @Inject constructor(
} catch (ex: HttpException) {
Resource.error(ex)
} catch (ex: Exception) {
log.error("Error while resolving balance: $ex")
analyticsService.logError(ex)
Resource.error(ex)
}
Expand All @@ -316,19 +366,17 @@ class CrowdNodeBlockchainApi @Inject constructor(
}
}


private suspend fun fetchBalance(address: String): String {
val response = crowdNodeWebApi.getTransactions(address)
var total = BigDecimal.ZERO
response.body()?.value?.forEach { tx ->
total += BigDecimal.valueOf(tx.amount)
}
return total.setScale(8, RoundingMode.HALF_UP).toString()
val response = crowdNodeWebApi.getBalance(address)
val balance = BigDecimal.valueOf(response.body()?.totalBalance ?: 0.0)

return balance.setScale(8, RoundingMode.HALF_UP).toString()
}

private fun handleError(ex: Exception, error: String) {
apiError.value = ex
notifyIfNeeded(error, "crowdnode_error")
log.error("$error: $ex")
analyticsService.logError(ex)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@

package org.dash.wallet.integrations.crowdnode.api

import org.dash.wallet.integrations.crowdnode.model.CrowdNodeResponse
import org.dash.wallet.integrations.crowdnode.model.CrowdNodeBalance
import org.dash.wallet.integrations.crowdnode.model.CrowdNodeTx
import retrofit2.Response
import retrofit2.http.*

interface CrowdNodeWebApi {
@GET("odata/apifundings/GetFunds(address='{address}')")
suspend fun getTransactions(
@Path("address") address: String
): Response<CrowdNodeResponse>
): Response<List<CrowdNodeTx>>

@GET("odata/apifundings/GetBalance(address='{address}')")
suspend fun getBalance(
@Path("address") address: String
): Response<CrowdNodeBalance>
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.dash.wallet.integrations.crowdnode.api.*
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
@ExperimentalCoroutinesApi
abstract class CrowdNodeModule {
companion object {
@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2022 Dash Core Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.dash.wallet.integrations.crowdnode.model

enum class ApiCode(val code: Long, val isRequest: Boolean = false) {
PleaseAcceptTerms(2, true),
WelcomeToApi(4, true),
DepositReceived(8, true),
WithdrawalQueue(16, true),
WithdrawAll(1000, false),
SignUp(131072, false),
AcceptTerms(65536, false),
MaxCode(131072, false)
}
Comment on lines +20 to +29

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to see all these constants in one place.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2022 Dash Core Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.dash.wallet.integrations.crowdnode.model

import com.google.gson.annotations.SerializedName

data class CrowdNodeBalance (
@SerializedName("DashAddress")
val dashAddress : String,
@SerializedName("TotalBalance")
val totalBalance : Double,
@SerializedName("TotalActiveBalance")
val totalActiveBalance : Double,
@SerializedName("TotalDividend")
val totalDividend : Double
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,6 @@ package org.dash.wallet.integrations.crowdnode.model

import com.google.gson.annotations.SerializedName

data class CrowdNodeResponse (
@SerializedName("@odata.context")
val dataContext : String,
@SerializedName("value")
val value : List<CrowdNodeTx>
)

data class CrowdNodeTx (
@SerializedName("FundingType")
val fundingType : String,
Expand Down
Loading