diff --git a/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt b/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt index ce0425ed80..91731972c4 100644 --- a/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt +++ b/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt @@ -43,4 +43,8 @@ interface WalletDataProvider { fun sendCoins(address: Address, amount: Coin): LiveData> fun startSendCoinsForResult(activity: Activity, requestCode: Int, address: Address, amount: Coin?) -} \ No newline at end of file + + fun getWalletBalance(): Coin + + fun createSentDashAddress(address: String): Address +} diff --git a/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt b/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt new file mode 100644 index 0000000000..d6740258b9 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt @@ -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 . + */ + +package org.dash.wallet.common.services + +import org.bitcoinj.core.Address +import org.bitcoinj.core.Coin +import org.bitcoinj.core.Transaction + +interface SendPaymentService { + suspend fun sendCoins( + address: Address, + amount: Coin, + constrainInputsTo: Address? = null, + emptyWallet: Boolean = false + ): Transaction +} diff --git a/common/src/main/java/org/dash/wallet/common/transactions/ByAddressCoinSelector.kt b/common/src/main/java/org/dash/wallet/common/transactions/ByAddressCoinSelector.kt new file mode 100644 index 0000000000..12c5d07279 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/transactions/ByAddressCoinSelector.kt @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +package org.dash.wallet.common.transactions + +import org.bitcoinj.core.Address +import org.bitcoinj.core.Coin +import org.bitcoinj.core.TransactionOutput +import org.bitcoinj.script.ScriptPattern +import org.bitcoinj.wallet.CoinSelection +import org.bitcoinj.wallet.CoinSelector +import org.bitcoinj.wallet.ZeroConfCoinSelector + +class ByAddressCoinSelector(private val address: Address) : CoinSelector { + private val selector = ZeroConfCoinSelector.get() + + override fun select( + target: Coin, + candidates: MutableList + ): CoinSelection { + val filtered = candidates.filter { output -> + val script = output.scriptPubKey + (ScriptPattern.isP2PKH(script) || ScriptPattern.isP2SH(script)) && + script.getToAddress(address.parameters) == address + } + + return selector.select(target, filtered) + } +} diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Constants.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Constants.kt index e5b1d0f407..04907a286a 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Constants.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Constants.kt @@ -17,6 +17,7 @@ package org.dash.wallet.integration.coinbase_integration const val CB_VERSION_KEY = "CB-VERSION" +const val CB_2FA_TOKEN_KEY = "CB-2FA-TOKEN" const val CB_VERSION_VALUE = "2021-09-07" const val TRANSACTION_TYPE_SEND = "send" const val TRANSACTION_STATUS_PENDING = "pending" diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Mapper.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Mapper.kt index 6d6bddeb57..e2c23344db 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Mapper.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/Mapper.kt @@ -72,3 +72,14 @@ class CommitBuyOrderMapper : Mapper { } } } + +class CoinbaseAddressMapper : Mapper { + override fun map(input: CoinBaseAccountAddressResponse?): String { + return if (input == null) + "" + else { + input.data?.mapNotNull { it?.address } + ?.firstOrNull { it.isNotEmpty() } ?: "" + } + } +} diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/di/CoinBaseModule.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/di/CoinBaseModule.kt index 0a1430269a..cb7bd78eb2 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/di/CoinBaseModule.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/di/CoinBaseModule.kt @@ -22,6 +22,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.dash.wallet.common.Configuration +import org.dash.wallet.integration.coinbase_integration.CoinbaseAddressMapper import org.dash.wallet.integration.coinbase_integration.CommitBuyOrderMapper import org.dash.wallet.integration.coinbase_integration.PlaceBuyOrderMapper import org.dash.wallet.integration.coinbase_integration.SwapTradeMapper @@ -67,6 +68,8 @@ object CoinBaseModule { fun provideSwapTradeMapper(): SwapTradeMapper = SwapTradeMapper() @Provides fun provideReceiver(): CloseCoinbasePortalBroadcaster = CloseCoinbasePortalBroadcaster() + @Provides + fun provideCoinbaseAddressMapper(): CoinbaseAddressMapper = CoinbaseAddressMapper() } @Module diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/model/AddressesResponse.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/model/AddressesResponse.kt new file mode 100644 index 0000000000..375846ee84 --- /dev/null +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/model/AddressesResponse.kt @@ -0,0 +1,56 @@ +package org.dash.wallet.integration.coinbase_integration.model + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class AddressesResponse( + + @field:SerializedName("data") + val addresses: Addresses? = null +) : Parcelable + +@Parcelize +data class Addresses( + + @field:SerializedName("deposit_uri") + val depositUri: String? = null, + + @field:SerializedName("address_info") + val addressInfo: AddressInfo? = null, + + @field:SerializedName("address") + val address: String? = null, + + @field:SerializedName("resource") + val resource: String? = null, + + @field:SerializedName("warnings") + val warnings: List? = null, + + @field:SerializedName("created_at") + val createdAt: String? = null, + + @field:SerializedName("uri_scheme") + val uriScheme: String? = null, + + @field:SerializedName("network") + val network: String? = null, + + @field:SerializedName("callback_url") + val callbackUrl: String? = null, + + @field:SerializedName("updated_at") + val updatedAt: String? = null, + + @field:SerializedName("resource_path") + val resourcePath: String? = null, + + @field:SerializedName("name") + val name: String? = null, + + @field:SerializedName("id") + val id: String? = null +) : Parcelable + diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/model/CoinBaseAccountAddressResponse.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/model/CoinBaseAccountAddressResponse.kt new file mode 100644 index 0000000000..19b383acb5 --- /dev/null +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/model/CoinBaseAccountAddressResponse.kt @@ -0,0 +1,96 @@ +package org.dash.wallet.integration.coinbase_integration.model + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class CoinBaseAccountAddressResponse( + + @field:SerializedName("pagination") + val pagination: Pagination? = null, + + @field:SerializedName("data") + val data: List? = null +) : Parcelable + +@Parcelize +data class CoinBaseAccountAddressInfo( + @field:SerializedName("address") + val address: String? = null +) : Parcelable + +@Parcelize +data class WarningsItem( + + @field:SerializedName("image_url") + val imageUrl: String? = null, + + @field:SerializedName("options") + val options: List? = null, + + @field:SerializedName("details") + val details: String? = null, + + @field:SerializedName("type") + val type: String? = null, + + @field:SerializedName("title") + val title: String? = null +) : Parcelable + +@Parcelize +data class OptionsItem( + + @field:SerializedName("style") + val style: String? = null, + + @field:SerializedName("text") + val text: String? = null, + + @field:SerializedName("id") + val id: String? = null +) : Parcelable + +@Parcelize +data class DataItem( + + @field:SerializedName("deposit_uri") + val depositUri: String? = null, + + @field:SerializedName("address_info") + val addressInfo: CoinBaseAccountAddressInfo? = null, + + @field:SerializedName("address") + val address: String? = null, + + @field:SerializedName("resource") + val resource: String? = null, + + @field:SerializedName("warnings") + val warnings: List? = null, + + @field:SerializedName("created_at") + val createdAt: String? = null, + + @field:SerializedName("uri_scheme") + val uriScheme: String? = null, + + @field:SerializedName("network") + val network: String? = null, + + @field:SerializedName("callback_url") + val callbackUrl: String? = null, + + @field:SerializedName("updated_at") + val updatedAt: String? = null, + + @field:SerializedName("resource_path") + val resourcePath: String? = null, + + @field:SerializedName("name") + val name: String? = null, + + @field:SerializedName("id") + val id: String? = null +) : Parcelable diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/repository/CoinBaseRepository.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/repository/CoinBaseRepository.kt index b49a70e9e0..dc6b70f8ef 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/repository/CoinBaseRepository.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/repository/CoinBaseRepository.kt @@ -31,7 +31,9 @@ class CoinBaseRepository @Inject constructor( private val userPreferences: Configuration, private val placeBuyOrderMapper: PlaceBuyOrderMapper, private val swapTradeMapper: SwapTradeMapper, - private val commitBuyOrderMapper: CommitBuyOrderMapper) : CoinBaseRepositoryInt { + private val commitBuyOrderMapper: CommitBuyOrderMapper, + private val coinbaseAddressMapper: CoinbaseAddressMapper +) : CoinBaseRepositoryInt { override suspend fun getUserAccount() = safeApiCall { val apiResponse = servicesApi.getUserAccounts() val userAccountData = apiResponse?.data?.firstOrNull { @@ -104,13 +106,19 @@ class CoinBaseRepository @Inject constructor( placeBuyOrderMapper.map(apiResult?.data) } + + override suspend fun getUserAccountAddress(): ResponseResource = safeApiCall { + val apiResult = servicesApi.getUserAccountAddress(accountId = userPreferences.coinbaseUserAccountId) + coinbaseAddressMapper.map(apiResult) + } + override suspend fun commitBuyOrder(buyOrderId: String) = safeApiCall { val commitBuyResult = servicesApi.commitBuyOrder(accountId = userPreferences.coinbaseUserAccountId, buyOrderId = buyOrderId) commitBuyOrderMapper.map(commitBuyResult?.data) } - override suspend fun sendFundsToWallet(sendTransactionToWalletParams: SendTransactionToWalletParams) = safeApiCall { - servicesApi.sendCoinsToWallet(accountId = userPreferences.coinbaseUserAccountId, sendTransactionToWalletParams = sendTransactionToWalletParams) + override suspend fun sendFundsToWallet(sendTransactionToWalletParams: SendTransactionToWalletParams, api2FATokenVersion: String) = safeApiCall { + servicesApi.sendCoinsToWallet(accountId = userPreferences.coinbaseUserAccountId, sendTransactionToWalletParams = sendTransactionToWalletParams, api2FATokenVersion = api2FATokenVersion) } override fun getUserLastCoinbaseBalance(): String = userPreferences.lastCoinbaseBalance ?: "" @@ -136,6 +144,10 @@ class CoinBaseRepository @Inject constructor( } WithdrawalLimitUIModel(userPreferences.coinbaseUserWithdrawalLimitAmount, userPreferences.coinbaseSendLimitCurrency) } + + override suspend fun createAddress(): ResponseResource = safeApiCall { + return@safeApiCall servicesApi.createAddress(accountId = userPreferences.coinbaseUserAccountId)?.addresses?.address + } } interface CoinBaseRepositoryInt { @@ -146,10 +158,12 @@ interface CoinBaseRepositoryInt { suspend fun disconnectCoinbaseAccount() fun saveLastCoinbaseDashAccountBalance(amount: String?) fun saveUserAccountId(accountId: String?) + suspend fun createAddress(): ResponseResource + suspend fun getUserAccountAddress(): ResponseResource suspend fun getActivePaymentMethods(): ResponseResource> suspend fun placeBuyOrder(placeBuyOrderParams: PlaceBuyOrderParams): ResponseResource suspend fun commitBuyOrder(buyOrderId: String): ResponseResource - suspend fun sendFundsToWallet(sendTransactionToWalletParams: SendTransactionToWalletParams): ResponseResource + suspend fun sendFundsToWallet(sendTransactionToWalletParams: SendTransactionToWalletParams, api2FATokenVersion: String): ResponseResource fun getUserLastCoinbaseBalance(): String fun isUserConnected(): Boolean suspend fun swapTrade(tradesRequest: TradesRequest): ResponseResource @@ -161,4 +175,4 @@ interface CoinBaseRepositoryInt { data class WithdrawalLimitUIModel( val amount: String?, val currency: String -) \ No newline at end of file +) diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/service/CoinBaseServicesApi.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/service/CoinBaseServicesApi.kt index ba541ff16c..62746ca561 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/service/CoinBaseServicesApi.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/service/CoinBaseServicesApi.kt @@ -16,6 +16,7 @@ */ package org.dash.wallet.integration.coinbase_integration.service +import org.dash.wallet.integration.coinbase_integration.CB_2FA_TOKEN_KEY import org.dash.wallet.integration.coinbase_integration.CB_VERSION_KEY import org.dash.wallet.integration.coinbase_integration.CB_VERSION_VALUE import org.dash.wallet.integration.coinbase_integration.DASH_CURRENCY @@ -57,6 +58,7 @@ interface CoinBaseServicesApi { @POST("v2/accounts/{account_id}/transactions") suspend fun sendCoinsToWallet( @Header(CB_VERSION_KEY) apiVersion: String = CB_VERSION_VALUE, + @Header(CB_2FA_TOKEN_KEY) api2FATokenVersion: String, @Path("account_id") accountId: String, @Body sendTransactionToWalletParams: SendTransactionToWalletParams ): SendTransactionToWalletResponse? @@ -83,4 +85,16 @@ interface CoinBaseServicesApi { suspend fun getAuthorizationInformation( @Header(CB_VERSION_KEY) apiVersion: String = CB_VERSION_VALUE ): UserAuthorizationInfoResponse? + + @GET("v2/accounts/{account_id}/addresses") + suspend fun getUserAccountAddress( + @Path("account_id") accountId: String, + @Header(CB_VERSION_KEY) apiVersion: String = CB_VERSION_VALUE, + ): CoinBaseAccountAddressResponse + + @POST("v2/accounts/{account_id}/addresses") + suspend fun createAddress( + @Header(CB_VERSION_KEY) apiVersion: String = CB_VERSION_VALUE, + @Path("account_id") accountId: String + ): AddressesResponse } diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseBuyDashOrderReviewFragment.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseBuyDashOrderReviewFragment.kt index 39f93fe917..09024ff6bd 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseBuyDashOrderReviewFragment.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseBuyDashOrderReviewFragment.kt @@ -16,9 +16,13 @@ */ package org.dash.wallet.integration.coinbase_integration.ui +import android.app.AlertDialog +import android.content.DialogInterface import android.os.Bundle import android.os.CountDownTimer +import android.text.InputType import android.view.View +import android.widget.EditText import androidx.activity.addCallback import androidx.annotation.ColorRes import androidx.annotation.StyleRes @@ -82,7 +86,7 @@ class CoinbaseBuyDashOrderReviewFragment : Fragment(R.layout.fragment_coinbase_b override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner){ + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { analyticsService.logEvent(AnalyticsConstants.Coinbase.BOTTOM_BACK_TO_ENTER_AMOUNT, bundleOf()) findNavController().popBackStack() } @@ -120,14 +124,20 @@ class CoinbaseBuyDashOrderReviewFragment : Fragment(R.layout.fragment_coinbase_b analyticsService.logEvent(AnalyticsConstants.Coinbase.CONFIRM_DASH_PURCHASE, bundleOf()) countDownTimer.cancel() if (isRetrying) { - viewModel.onRefreshOrderClicked(amountViewModel.onContinueEvent.value?.second, - selectedPaymentMethodId) + viewModel.onRefreshOrderClicked( + amountViewModel.onContinueEvent.value?.second, + selectedPaymentMethodId + ) isRetrying = false } else { newBuyOrderId?.let { buyOrderId -> viewModel.commitBuyOrder(buyOrderId) } } } + + viewModel.commitBuyOrderSuccessCallback.observe(viewLifecycleOwner) { + show2FADialog() + } viewModel.showLoading.observe(viewLifecycleOwner) { showLoading -> if (showLoading) { showProgress(R.string.loading) @@ -136,14 +146,17 @@ class CoinbaseBuyDashOrderReviewFragment : Fragment(R.layout.fragment_coinbase_b } - viewModel.commitBuyOrderFailedCallback.observe(viewLifecycleOwner){ + viewModel.commitBuyOrderFailedCallback.observe(viewLifecycleOwner) { showBuyOrderDialog(CoinBaseBuyDashDialog.Type.PURCHASE_ERROR, null) } - viewModel.transactionCompleted.observe(viewLifecycleOwner){ transactionStatus -> - showBuyOrderDialog(if (transactionStatus.isTransactionSuccessful) - CoinBaseBuyDashDialog.Type.TRANSFER_SUCCESS else CoinBaseBuyDashDialog.Type.TRANSFER_ERROR, transactionStatus.responseMessage) + viewModel.transactionCompleted.observe(viewLifecycleOwner) { transactionStatus -> + showBuyOrderDialog( + if (transactionStatus.isTransactionSuccessful) + CoinBaseBuyDashDialog.Type.TRANSFER_SUCCESS else CoinBaseBuyDashDialog.Type.TRANSFER_ERROR, + transactionStatus.responseMessage + ) } binding.contentOrderReview.coinbaseFeeInfoContainer.setOnClickListener { @@ -151,17 +164,17 @@ class CoinbaseBuyDashOrderReviewFragment : Fragment(R.layout.fragment_coinbase_b safeNavigate(CoinbaseBuyDashOrderReviewFragmentDirections.orderReviewToFeeInfo()) } - viewModel.placeBuyOrderFailedCallback.observe(viewLifecycleOwner){ + viewModel.placeBuyOrderFailedCallback.observe(viewLifecycleOwner) { val placeBuyOrderError = CoinbaseGenericErrorUIModel( R.string.something_wrong_title, getString(R.string.retry_later_message), R.drawable.ic_info_red, - negativeButtonText= R.string.close + negativeButtonText = R.string.close ) safeNavigate(CoinbaseBuyDashOrderReviewFragmentDirections.coinbaseBuyDashOrderReviewToError(placeBuyOrderError)) } - viewModel.placeBuyOrder.observe(viewLifecycleOwner){ + viewModel.placeBuyOrder.observe(viewLifecycleOwner) { it.updateOrderReviewUI() countDownTimer.start() } @@ -226,7 +239,7 @@ class CoinbaseBuyDashOrderReviewFragment : Fragment(R.layout.fragment_coinbase_b findNavController().popBackStack() } CoinBaseBuyDashDialog.Type.TRANSFER_ERROR -> { - viewModel.retry() + show2FADialog() } CoinBaseBuyDashDialog.Type.TRANSFER_SUCCESS -> { dismiss() @@ -251,6 +264,29 @@ class CoinbaseBuyDashOrderReviewFragment : Fragment(R.layout.fragment_coinbase_b super.onPause() } + private fun show2FADialog() { + val builder: AlertDialog.Builder = android.app.AlertDialog.Builder(requireContext()) + builder.setTitle("Title") + + val input = EditText(requireContext()) + input.setHint("Enter Code") + input.inputType = InputType.TYPE_CLASS_NUMBER + builder.setView(input) + + + builder.setPositiveButton( + "OK", + DialogInterface.OnClickListener { dialog, which -> + // Here you get get input text from the Edittext + var m_Text = input.text.toString() + viewModel.sendDash(m_Text) + } + ) + builder.setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, which -> dialog.cancel() }) + + builder.show() + } + private fun setNetworkState(hasInternet: Boolean){ binding.networkStatusContainer.isVisible = !hasInternet binding.previewOfflineGroup.isVisible = hasInternet diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConversionPreviewFragment.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConversionPreviewFragment.kt index 97ef2c01f4..afde32ae50 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConversionPreviewFragment.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConversionPreviewFragment.kt @@ -16,9 +16,13 @@ */ package org.dash.wallet.integration.coinbase_integration.ui +import android.app.AlertDialog +import android.content.DialogInterface import android.os.Bundle import android.os.CountDownTimer +import android.text.InputType import android.view.View +import android.widget.EditText import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController @@ -51,6 +55,7 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co private var transactionStateDialog: CoinBaseBuyDashDialog? = null private var newSwapOrderId: String? = null + private val countDownTimer by lazy { object : CountDownTimer(10000, 1000) { @@ -74,7 +79,7 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co } binding.cancelBtn.setOnClickListener { - safeNavigate(CoinbaseBuyDashOrderReviewFragmentDirections.confirmCancelBuyDashTransaction()) + safeNavigate(CoinbaseConversionPreviewFragmentDirections.confirmCancelBuyDashTransaction()) } arguments?.let { @@ -91,7 +96,9 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co viewModel.onRefreshOrderClicked(swapTradeUIModel) isRetrying = false } else { - newSwapOrderId?.let { buyOrderId -> viewModel.commitSwapTrade(buyOrderId) } + swapTradeUIModel?.let { + viewModel.commitSwapTrade(it) + } } } @@ -107,14 +114,15 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co } viewModel.transactionCompleted.observe(viewLifecycleOwner) { transactionStatus -> - showBuyOrderDialog(if (transactionStatus.isTransactionSuccessful) + showBuyOrderDialog( + if (transactionStatus.isTransactionSuccessful) CoinBaseBuyDashDialog.Type.CONVERSION_SUCCESS else CoinBaseBuyDashDialog.Type.TRANSFER_ERROR, transactionStatus.responseMessage ) } binding.contentOrderReview.coinbaseFeeInfoContainer.setOnClickListener { - safeNavigate(CoinbaseBuyDashOrderReviewFragmentDirections.orderReviewToFeeInfo()) + safeNavigate(CoinbaseConversionPreviewFragmentDirections.orderReviewToFeeInfo()) } viewModel.swapTradeFailedCallback.observe(viewLifecycleOwner) { @@ -124,12 +132,16 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co R.drawable.ic_info_red, negativeButtonText = R.string.close ) - safeNavigate(CoinbaseBuyDashOrderReviewFragmentDirections.coinbaseServicesToError(placeBuyOrderError)) + safeNavigate(CoinbaseConversionPreviewFragmentDirections.coinbaseServicesToError(placeBuyOrderError)) } viewModel.swapTradeOrder.observe(viewLifecycleOwner) { countDownTimer.start() } + + viewModel.commitBuyOrderSuccessCallback.observe(viewLifecycleOwner) { + show2FADialog() + } } private fun SwapTradeUIModel.updateConversionPreviewUI() { @@ -147,11 +159,11 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co binding.contentOrderReview.convertOutputSubtitle.text = this.outputCurrency if (this.inputCurrency == DASH_CURRENCY) { + binding.contentOrderReview.inputAccountHintLabel.setText(R.string.from_dash_wallet_on_this_device) + binding.contentOrderReview.outputAccountHintLabel.setText(R.string.to_your_coinbase_account) + } else { binding.contentOrderReview.inputAccountHintLabel.setText(R.string.from_your_coinbase_account) binding.contentOrderReview.outputAccountHintLabel.setText(R.string.to_dash_wallet_on_this_device) - } else { - binding.contentOrderReview.outputAccountHintLabel.setText(R.string.from_your_coinbase_account) - binding.contentOrderReview.inputAccountHintLabel.setText(R.string.to_dash_wallet_on_this_device) } binding.contentOrderReview.inputAccount.text = getString( @@ -224,7 +236,7 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co findNavController().popBackStack() } CoinBaseBuyDashDialog.Type.TRANSFER_ERROR -> { - viewModel.retry() + show2FADialog() } CoinBaseBuyDashDialog.Type.CONVERSION_SUCCESS -> { dismiss() @@ -247,4 +259,27 @@ class CoinbaseConversionPreviewFragment : Fragment(R.layout.fragment_coinbase_co countDownTimer.cancel() super.onPause() } + + private fun show2FADialog() { + val builder: AlertDialog.Builder = android.app.AlertDialog.Builder(requireContext()) + builder.setTitle("Title") + + val input = EditText(requireContext()) + input.setHint("Enter Code") + input.inputType = InputType.TYPE_CLASS_NUMBER + builder.setView(input) + + + builder.setPositiveButton( + "OK", + DialogInterface.OnClickListener { dialog, which -> + // Here you get get input text from the Edittext + var m_Text = input.text.toString() + viewModel.sendDash(m_Text) + } + ) + builder.setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, which -> dialog.cancel() }) + + builder.show() + } } diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConvertCryptoFragment.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConvertCryptoFragment.kt index 1b6ddcf2f5..4a76883032 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConvertCryptoFragment.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/CoinbaseConvertCryptoFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.util.TypedValue import android.view.View import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -43,10 +44,10 @@ import org.dash.wallet.integration.coinbase_integration.model.CoinbaseGenericErr import org.dash.wallet.integration.coinbase_integration.model.getCoinBaseExchangeRateConversion import org.dash.wallet.integration.coinbase_integration.ui.convert_currency.ConvertViewFragment import org.dash.wallet.integration.coinbase_integration.ui.convert_currency.model.ServiceWallet +import org.dash.wallet.integration.coinbase_integration.ui.convert_currency.model.SwapValueErrorType import org.dash.wallet.integration.coinbase_integration.ui.dialogs.crypto_wallets.CryptoWalletsDialog import org.dash.wallet.integration.coinbase_integration.viewmodels.CoinbaseConvertCryptoViewModel import org.dash.wallet.integration.coinbase_integration.viewmodels.ConvertViewViewModel -import java.util.* @AndroidEntryPoint @@ -58,7 +59,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver private var selectedCoinBaseAccount: CoinBaseUserAccountDataUIModel? = null private var cryptoWalletsDialog: CryptoWalletsDialog? = null private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) - .noCode().minDecimals(6).optionalDecimals() + .noCode().minDecimals(8).optionalDecimals() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -100,8 +101,25 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver } convertViewModel.onContinueEvent.observe(viewLifecycleOwner) { pair -> - if (!pair.first && selectedCoinBaseAccount?.coinBaseUserAccountData?.currency?.code != DASH_CURRENCY) { - selectedCoinBaseAccount?.let { viewModel.swapTrade(pair.second, it) } + val swapValueErrorType = convertViewModel.checkEnteredAmountValue() + if (swapValueErrorType == SwapValueErrorType.NOError) { + if (!pair.first && selectedCoinBaseAccount?.coinBaseUserAccountData?.currency?.code != DASH_CURRENCY) { + selectedCoinBaseAccount?.let { + pair.second?.first?.let { fait -> + viewModel.swapTrade(fait, it, pair.first) + } + } + } else { + pair.second?.second?.let { coin -> + selectedCoinBaseAccount?.let { + pair.second?.first?.let { fait -> + viewModel.sellDashToCoinBase(coin, fait, it) + } + } + } + } + } else { + showSwapValueErrorView(swapValueErrorType) } } @@ -115,6 +133,50 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver } ) + + viewModel.getUserAccountAddressFailedCallback.observe(viewLifecycleOwner) { + val placeBuyOrderError = CoinbaseGenericErrorUIModel( + R.string.error, + getString(R.string.error), + R.drawable.ic_info_red, + negativeButtonText = R.string.close + ) + safeNavigate( + CoinbaseServicesFragmentDirections.coinbaseServicesToError( + placeBuyOrderError + ) + ) + } + + + viewModel.onInsufficientMoneyCallback.observe(viewLifecycleOwner) { + val placeBuyOrderError = CoinbaseGenericErrorUIModel( + R.string.insufficient_money_title, + getString(R.string.insufficient_money_msg), + R.drawable.ic_info_red, + negativeButtonText = R.string.close + ) + safeNavigate( + CoinbaseServicesFragmentDirections.coinbaseServicesToError( + placeBuyOrderError + ) + ) + } + + viewModel.onFailure.observe(viewLifecycleOwner) { + val placeBuyOrderError = CoinbaseGenericErrorUIModel( + R.string.send_coins_error_msg, + getString(R.string.insufficient_money_msg), + R.drawable.ic_info_red, + negativeButtonText = R.string.close + ) + safeNavigate( + CoinbaseServicesFragmentDirections.coinbaseServicesToError( + placeBuyOrderError + ) + ) + } + viewModel.swapTradeFailedCallback.observe(viewLifecycleOwner) { val placeBuyOrderError = CoinbaseGenericErrorUIModel( R.string.error, @@ -122,7 +184,11 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver R.drawable.ic_info_red, negativeButtonText = R.string.close ) - safeNavigate(CoinbaseServicesFragmentDirections.coinbaseServicesToError(placeBuyOrderError)) + safeNavigate( + CoinbaseServicesFragmentDirections.coinbaseServicesToError( + placeBuyOrderError + ) + ) } viewModel.userAccountError.observe(viewLifecycleOwner) { @@ -133,7 +199,26 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver R.string.buy_crypto_on_coinbase, negativeButtonText = R.string.close ) - safeNavigate(CoinbaseServicesFragmentDirections.coinbaseServicesToError(placeBuyOrderError)) + safeNavigate( + CoinbaseServicesFragmentDirections.coinbaseServicesToError( + placeBuyOrderError + ) + ) + } + + convertViewModel.userDashAccountEmptyError.observe(viewLifecycleOwner) { + if (it) { + val dashAccountEmptyError = CoinbaseGenericErrorUIModel( + title = R.string.dont_have_any_dash, + image = R.drawable.ic_info_red, + negativeButtonText = R.string.close + ) + safeNavigate( + CoinbaseServicesFragmentDirections.coinbaseServicesToError( + dashAccountEmptyError + ) + ) + } } binding.convertView.setOnCurrencyChooserClicked { @@ -141,9 +226,10 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver } binding.convertView.setOnSwapClicked { - convertViewModel.setOnSwapDashFromToCryptoClicked( it) + convertViewModel.setOnSwapDashFromToCryptoClicked(it) } + convertViewModel.selectedLocalExchangeRate.observe(viewLifecycleOwner) { binding.convertView.exchangeRate = ExchangeRate(Coin.COIN, it.fiat) setConvertViewInput() @@ -177,9 +263,31 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver binding.youWillReceiveLabel.isVisible = hasBalance binding.youWillReceiveValue.isVisible = hasBalance if (hasBalance) { - binding.youWillReceiveValue.text = context?.getString(R.string.you_will_receive_dash, dashFormat.format(balance).toString()) + binding.youWillReceiveValue.text = context?.getString( + R.string.you_will_receive_dash, + dashFormat.format(balance).toString() + ) } } + + viewModel.dashWalletBalance.observe( + viewLifecycleOwner + ) { + + binding.convertView.dashInput = it + } + + convertViewModel.validSwapValue.observe(viewLifecycleOwner) { + binding.limitDesc.isGone = true + } + } + + private fun showSwapValueErrorView(swapValueErrorType: SwapValueErrorType) { + binding.limitDesc.isGone = swapValueErrorType == SwapValueErrorType.NOError + when (swapValueErrorType) { + SwapValueErrorType.LessThanMin -> binding.limitDesc.setText(R.string.entered_amount_is_too_low) + SwapValueErrorType.MoreThanMax -> binding.limitDesc.setText(R.string.entered_amount_is_too_high) + } } private fun setConvertViewInput() { @@ -192,6 +300,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver } else { null } + convertViewModel.selectedLocalExchangeRate.value?.let { rate -> binding.convertView.input = ServiceWallet( it.coinBaseUserAccountData.currency?.name ?: "", diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertView.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertView.kt index 0e7c3198be..f83507e33f 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertView.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertView.kt @@ -50,6 +50,13 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont updateAmount() } + private var _dashInput: Coin? = null + var dashInput: Coin? + get() = _dashInput + set(value) { + _dashInput = value + } + var exchangeRate: ExchangeRate? = null set(value) { field = value @@ -67,11 +74,16 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont binding.convertFromDashBalance.isVisible = input != null updateUiWithSwap() + binding.swapBtn.setOnClickListener { - dashToCrypto = !dashToCrypto + onSwapClicked?.invoke(!dashToCrypto) + if (dashInput?.isZero == true && !dashToCrypto) { + return@setOnClickListener + } updateUiWithSwap() - onSwapClicked?.invoke(dashToCrypto) + dashToCrypto = !dashToCrypto } + binding.convertFromBtn.convertItemClickListener = object : CryptoConvertItem.ConvertItemClickListener { override fun onConvertItemClickListener() { @@ -108,6 +120,7 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont } } + private fun setConvertToBtnData() { binding.convertToBtn.setCryptoItemArrowVisibility(dashToCrypto) if (!dashToCrypto) { @@ -131,7 +144,7 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont binding.convertFromBtn.setConvertItemTitle(it.cryptoWalletName) binding.convertFromBtn.setConvertItemIcon(it.icon) - exchangeRate?.let { currentExchangeRate -> + exchangeRate?.let { _ -> val balance = it.balance.toBigDecimal().setScale(8, RoundingMode.HALF_UP).toString() val coin = try { @@ -148,13 +161,27 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont } } + @SuppressLint("SetTextI18n") private fun setToBtnData() { binding.convertToBtn.setCryptoItemGroupVisibility(input != null) + binding.convertFromDashBalance.isVisible = (dashInput != null) + binding.convertFromDashFiatAmount.isVisible = (dashInput != null) input?.let { binding.convertToBtn.setConvertItemServiceName(it.cryptoWalletService) binding.convertToBtn.setConvertItemTitle(it.cryptoWalletName) binding.convertToBtn.setConvertItemIcon(it.icon) } + + exchangeRate?.let { currentExchangeRate -> + dashInput?.let { dash -> + val currencyRate = ExchangeRate(Coin.COIN, currentExchangeRate.fiat) + val fiatAmount = GenericUtils.fiatToString(currencyRate.coinToFiat(dash)) + binding.convertFromDashBalance.text = "${context.getString(R.string.balance)} ${dashFormat.minDecimals(0) + .optionalDecimals(0,8).format(dash)} Dash" + + binding.convertFromDashFiatAmount.text = "${Constants.PREFIX_ALMOST_EQUAL_TO} $fiatAmount" + } + } } fun setOnCurrencyChooserClicked(listener: () -> Unit) { diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertViewFragment.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertViewFragment.kt index 56c08cc1a5..20712eeedf 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertViewFragment.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/ConvertViewFragment.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import org.bitcoinj.core.Coin import org.bitcoinj.utils.ExchangeRate import org.bitcoinj.utils.Fiat +import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Constants import org.dash.wallet.common.ui.enter_amount.NumericKeyboardView import org.dash.wallet.common.ui.viewBinding @@ -44,6 +45,7 @@ import org.dash.wallet.integration.coinbase_integration.model.CoinBaseUserAccoun import org.dash.wallet.integration.coinbase_integration.viewmodels.ConvertViewViewModel import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.DecimalFormatSymbols @AndroidEntryPoint @@ -52,6 +54,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { companion object { private const val ARG_DASH_TO_FIAT = "dash_to_fiat" private const val DECIMAL_SEPARATOR = '.' + @JvmStatic fun newInstance( dashToCrypto: Boolean = false, @@ -67,11 +70,13 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { private val binding by viewBinding(FragmentConvertCurrencyBinding::bind) private val viewModel by activityViewModels() private val format = Constants.SEND_PAYMENT_LOCAL_FORMAT.noCode() - private val decimalSeparator = DecimalFormatSymbols.getInstance(GenericUtils.getDeviceLocale()).decimalSeparator + private val decimalSeparator = + DecimalFormatSymbols.getInstance(GenericUtils.getDeviceLocale()).decimalSeparator private var maxAmountSelected: Boolean = false var selectedCurrencyCodeExchangeRate: ExchangeRate? = null var currencyConversionOptionList: List = emptyList() - + private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + .noCode().minDecimals(6).optionalDecimals() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -83,7 +88,10 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { binding.keyboardView.onKeyboardActionListener = keyboardActionListener binding.continueBtn.isEnabled = false binding.continueBtn.setOnClickListener { - getFaitAmount(viewModel.enteredConvertAmount, binding.currencyOptions.pickedOption)?.let { + getFaitAmount( + viewModel.enteredConvertAmount, + binding.currencyOptions.pickedOption + )?.let { viewModel.onContinueEvent.value = Pair( viewModel.dashToCrypto.value ?: false, it @@ -106,29 +114,35 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { resetViewSelection(it) } } + + binding.bottomCard.isVisible = false binding.currencyOptions.pickedOptionIndex = 0 + binding.maxButton.setOnClickListener { viewModel.selectedCryptoCurrencyAccount.value?.let { userAccountData -> - if (viewModel.selectedPickerCurrencyCode == userAccountData.coinBaseUserAccountData.balance?.currency - ) { - applyNewValue(viewModel.maxAmount, viewModel.selectedPickerCurrencyCode) - } else { - val cleanedValue = if (viewModel.selectedPickerCurrencyCode == viewModel.selectedLocalCurrencyCode) { - - viewModel.maxAmount.toBigDecimal() / - userAccountData.currencyToCryptoCurrencyExchangeRate.toBigDecimal() + getMaxAmount()?.let { maxAmount -> + if (viewModel.selectedPickerCurrencyCode == userAccountData.coinBaseUserAccountData.balance?.currency + ) { + applyNewValue(maxAmount, viewModel.selectedPickerCurrencyCode) } else { + val cleanedValue = + if (viewModel.selectedPickerCurrencyCode == viewModel.selectedLocalCurrencyCode) { - viewModel.maxAmount.toBigDecimal() * - userAccountData.cryptoCurrencyToDashExchangeRate.toBigDecimal() - }.setScale(8, RoundingMode.HALF_UP).toString() + maxAmount.toBigDecimal() / + userAccountData.currencyToCryptoCurrencyExchangeRate.toBigDecimal() + } else { - applyNewValue(cleanedValue, viewModel.selectedPickerCurrencyCode) - } + maxAmount.toBigDecimal() * + userAccountData.cryptoCurrencyToDashExchangeRate.toBigDecimal() + }.setScale(8, RoundingMode.HALF_UP).toString() + + applyNewValue(cleanedValue, viewModel.selectedPickerCurrencyCode) + } - maxAmountSelected = true + maxAmountSelected = true + } } } @@ -138,6 +152,21 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } } + private fun getMaxAmount(): String? { + + if (viewModel.dashToCrypto.value == true) { + viewModel.selectedCryptoCurrencyAccount.value?.let { account -> + val cleanedValue = + viewModel.maxForDashWalletAmount.toBigDecimal() / + account.cryptoCurrencyToDashExchangeRate.toBigDecimal() + return cleanedValue.setScale(8, RoundingMode.HALF_UP).toString() + } + } else { + return viewModel.maxCoinBaseAccountAmount + } + return null + } + private fun resetViewSelection(it: CoinBaseUserAccountDataUIModel?) { it?.coinBaseUserAccountData?.balance?.currency?.let { currencyCode -> currencyConversionOptionList = if (viewModel.dashToCrypto.value == true) @@ -159,50 +188,86 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } private fun setAmountValue(pickedCurrencyOption: String, valueToBind: String) { - val userAccountData = viewModel.selectedCryptoCurrencyAccount.value - - val cleanedValue = - if (viewModel.selectedPickerCurrencyCode !== pickedCurrencyOption && - (viewModel.enteredConvertAmount.toBigDecimalOrNull() ?: BigDecimal.ZERO) > BigDecimal.ZERO - ) { - when { - (userAccountData?.coinBaseUserAccountData?.balance?.currency == viewModel.selectedPickerCurrencyCode) -> { - if (pickedCurrencyOption == viewModel.selectedLocalCurrencyCode) { - - valueToBind.toBigDecimal() / - userAccountData.currencyToCryptoCurrencyExchangeRate.toBigDecimal() - } else { - - valueToBind.toBigDecimal() * - userAccountData.cryptoCurrencyToDashExchangeRate.toBigDecimal() + viewModel.selectedCryptoCurrencyAccount.value?.let { userAccountData -> + + + val cleanedValue = + if (viewModel.selectedPickerCurrencyCode !== pickedCurrencyOption && + ( + viewModel.enteredConvertAmount.toBigDecimalOrNull() + ?: BigDecimal.ZERO + ) > BigDecimal.ZERO + ) { + val convertedValue = when { + (userAccountData.coinBaseUserAccountData.balance?.currency == viewModel.selectedPickerCurrencyCode) -> { + if (pickedCurrencyOption == viewModel.selectedLocalCurrencyCode) { + + ( + valueToBind.toBigDecimal() / + userAccountData.currencyToCryptoCurrencyExchangeRate.toBigDecimal() + ) + .setScale(8, RoundingMode.HALF_UP).toString() + } else { + + val bd = toDashValue(valueToBind, userAccountData, true) + val coin = try { + Coin.parseCoin(bd.toString()) + } catch (x: Exception) { + Coin.ZERO + } + if (coin.isZero) { + 0.toBigDecimal() + } else { + bd + } + } } - } - (viewModel.selectedLocalCurrencyCode == viewModel.selectedPickerCurrencyCode) -> { - if (pickedCurrencyOption == userAccountData?.coinBaseUserAccountData?.balance?.currency) { - valueToBind.toBigDecimal() * - userAccountData.currencyToCryptoCurrencyExchangeRate.toBigDecimal() - } else { - valueToBind.toBigDecimal() * - userAccountData?.currencyToDashExchangeRate?.toBigDecimal()!! + (viewModel.selectedLocalCurrencyCode == viewModel.selectedPickerCurrencyCode) -> { + if (pickedCurrencyOption == userAccountData.coinBaseUserAccountData.balance?.currency) { + ( + valueToBind.toBigDecimal() * + userAccountData.currencyToCryptoCurrencyExchangeRate.toBigDecimal() + ) + .setScale(8, RoundingMode.HALF_UP).toString() + } else { + val bd = toDashValue(valueToBind, userAccountData) + val coin = try { + Coin.parseCoin(bd.toString()) + } catch (x: Exception) { + Coin.ZERO + } + if (coin.isZero) { + 0.toBigDecimal() + } else { + bd + } + } } - } - else -> { - if (pickedCurrencyOption == userAccountData?.coinBaseUserAccountData?.balance?.currency) { - valueToBind.toBigDecimal() / - userAccountData.cryptoCurrencyToDashExchangeRate.toBigDecimal() - } else { - - valueToBind.toBigDecimal() / - userAccountData?.currencyToDashExchangeRate?.toBigDecimal()!! + else -> { + if (pickedCurrencyOption == userAccountData.coinBaseUserAccountData.balance?.currency) { + ( + valueToBind.toBigDecimal() / + userAccountData.cryptoCurrencyToDashExchangeRate.toBigDecimal() + ) + .setScale(8, RoundingMode.HALF_UP).toString() + } else { + + ( + valueToBind.toBigDecimal() / + userAccountData.currencyToDashExchangeRate.toBigDecimal() + ) + .setScale(8, RoundingMode.HALF_UP).toString() + } } } - }.setScale(8, RoundingMode.HALF_UP).toString() - } else { - valueToBind - } + convertedValue.toString() + } else { + valueToBind + } - applyNewValue(cleanedValue, pickedCurrencyOption) + applyNewValue(cleanedValue, pickedCurrencyOption) + } } fun setViewDetails(continueText: String, keyboardHeader: View?) { @@ -220,14 +285,16 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { fun refreshValue() { value.clear() - val inputValue = if (viewModel.selectedLocalCurrencyCode == binding.currencyOptions.pickedOption) { - val localCurrencySymbol = GenericUtils.getLocalCurrencySymbol(viewModel.selectedLocalCurrencyCode) - binding.inputAmount.text.split(" ") - .first { it != localCurrencySymbol } - } else { - binding.inputAmount.text.split(" ") - .first { it != binding.currencyOptions.pickedOption } - } + val inputValue = + if (viewModel.selectedLocalCurrencyCode == binding.currencyOptions.pickedOption) { + val localCurrencySymbol = + GenericUtils.getLocalCurrencySymbol(viewModel.selectedLocalCurrencyCode) + binding.inputAmount.text.split(" ") + .first { it != localCurrencySymbol } + } else { + binding.inputAmount.text.split(" ") + .first { it != binding.currencyOptions.pickedOption } + } if (inputValue != "0") value.append(inputValue) } @@ -242,8 +309,10 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val isFraction = value.toString().indexOf(decimalSeparator) > -1 if (isFraction) { - val lengthOfDecimalPart = value.toString().length - value.toString().indexOf(decimalSeparator) - val decimalsThreshold = if (viewModel.selectedLocalCurrencyCode == binding.currencyOptions.pickedOption) 2 else 8 + val lengthOfDecimalPart = + value.toString().length - value.toString().indexOf(decimalSeparator) + val decimalsThreshold = + if (viewModel.selectedLocalCurrencyCode == binding.currencyOptions.pickedOption) 2 else 8 if (lengthOfDecimalPart > decimalsThreshold) { return @@ -268,6 +337,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { value.clear() } else if (value.isNotEmpty()) { value.deleteCharAt(value.length - 1) + viewModel.resetSwapValueError() } applyNewValue(value.toString(), binding.currencyOptions.pickedOption) maxAmountSelected = false @@ -308,7 +378,8 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val spannableString = if (viewModel.selectedLocalCurrencyCode == currencyCode) { val cleanedValue = GenericUtils.formatFiatWithoutComma(balance) val fiatAmount = Fiat.parseFiat(viewModel.selectedLocalCurrencyCode, cleanedValue) - val localCurrencySymbol = GenericUtils.getLocalCurrencySymbol(viewModel.selectedLocalCurrencyCode) + val localCurrencySymbol = + GenericUtils.getLocalCurrencySymbol(viewModel.selectedLocalCurrencyCode) val faitBalance = if (isFraction && lengthOfDecimalPart > 2) { format.format(fiatAmount).toString() @@ -321,16 +392,24 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { "$faitBalance $localCurrencySymbol" } SpannableString(text).apply { - if (GenericUtils.isCurrencyFirst(fiatAmount) && text.length - faitBalance.length> 0) { + if (GenericUtils.isCurrencyFirst(fiatAmount) && text.length - faitBalance.length > 0) { setAmountFormat(this, 0, text.length - faitBalance.length) } else { setAmountFormat(this, balance.length, text.length) } } } else { - val text = "$balance $currencyCode" + + val formattedValue = if (balance.contains("E")) { + DecimalFormat("########.########").format(balance.toDouble()) + } else { + balance + } + + val text = "$formattedValue $currencyCode" + SpannableString(text).apply { - setAmountFormat(this, balance.length, text.length) + setAmountFormat(this, formattedValue.length, text.length) } } @@ -339,7 +418,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val hasBalance = balance.isNotEmpty() && (balance.toBigDecimalOrNull() ?: BigDecimal.ZERO) > BigDecimal.ZERO - binding.continueBtn.isEnabled = hasBalance + if (hasBalance) { @@ -348,10 +427,8 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val dashAmount = when { (it.coinBaseUserAccountData.balance?.currency == currencyCode && it.coinBaseUserAccountData.balance.currency != DASH_CURRENCY) -> { - val cleanedValue = - balance.toBigDecimal() * - it.cryptoCurrencyToDashExchangeRate.toBigDecimal() - val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) + val bd = + toDashValue(balance, it, true) try { Coin.parseCoin(bd.toString()) } catch (x: Exception) { @@ -360,10 +437,8 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } (viewModel.selectedLocalCurrencyCode == currencyCode && it.coinBaseUserAccountData.balance?.currency != DASH_CURRENCY) -> { - val cleanedValue = - balance.toBigDecimal() * - it.currencyToDashExchangeRate.toBigDecimal() - val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) + val bd = + toDashValue(balance, it) try { Coin.parseCoin(bd.toString()) } catch (x: Exception) { @@ -387,8 +462,12 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } else { viewModel.setEnteredConvertDashAmount(Coin.ZERO) } + + checkTheUserEnteredValue(hasBalance) } + + private fun setAmountFormat( spannable: Spannable, from: Int, @@ -409,10 +488,10 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } } - private fun getFaitAmount(balance: String, currencyCode: String): Fiat? { + private fun getFaitAmount(balance: String, currencyCode: String): Pair? { viewModel.selectedCryptoCurrencyAccount.value?.let { selectedCurrencyCodeExchangeRate?.let { rate -> - return when { + val fiatAmount = when { (it.coinBaseUserAccountData.balance?.currency == currencyCode && it.coinBaseUserAccountData.balance.currency != DASH_CURRENCY) -> { val cleanedValue = balance.toBigDecimal() / @@ -435,8 +514,36 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { Fiat.parseFiat(rate.fiat.currencyCode, bd.toString()) } } + + val bd = + toDashValue(balance, it) + val coin = try { + Coin.parseCoin(bd.toString()) + } catch (x: Exception) { + Coin.ZERO + } + return Pair(fiatAmount, coin) } } return null } + + private fun toDashValue( + valueToBind: String, + userAccountData: CoinBaseUserAccountDataUIModel, + fromCrypto: Boolean = false + ): BigDecimal { + val convertedValue = if (fromCrypto) { + valueToBind.toBigDecimal() * + userAccountData.cryptoCurrencyToDashExchangeRate.toBigDecimal() + } else { + valueToBind.toBigDecimal() * + userAccountData.currencyToDashExchangeRate.toBigDecimal() + }.setScale(8, RoundingMode.HALF_UP) + return convertedValue + } + + private fun checkTheUserEnteredValue(hasBalance: Boolean) { + binding.continueBtn.isEnabled = hasBalance + } } diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/model/SwapValueErrorType.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/model/SwapValueErrorType.kt new file mode 100644 index 0000000000..d26cb38a02 --- /dev/null +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/ui/convert_currency/model/SwapValueErrorType.kt @@ -0,0 +1,8 @@ +package org.dash.wallet.integration.coinbase_integration.ui.convert_currency.model + +enum class SwapValueErrorType { + LessThanMin, + MoreThanMax, + UnAuthorizedValue, + NOError +} diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseBuyDashOrderReviewViewModel.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseBuyDashOrderReviewViewModel.kt index 17ad640257..d033956b50 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseBuyDashOrderReviewViewModel.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseBuyDashOrderReviewViewModel.kt @@ -55,6 +55,10 @@ class CoinbaseBuyDashOrderReviewViewModel @Inject constructor( val placeBuyOrder: LiveData get() = _placeBuyOrder + private val _commitBuyOrderSuccessCallback: MutableLiveData = MutableLiveData() + val commitBuyOrderSuccessCallback: LiveData + get() = _commitBuyOrderSuccessCallback + fun commitBuyOrder(params: String) = viewModelScope.launch(Dispatchers.Main) { _showLoading.value = true when (val result = coinBaseRepository.commitBuyOrder(params)) { @@ -70,7 +74,7 @@ class CoinbaseBuyDashOrderReviewViewModel @Inject constructor( to = walletDataProvider.freshReceiveAddress().toBase58(), type = result.value.transactionType ).apply { - sendDashToWallet(this) + _commitBuyOrderSuccessCallback.value = this } } } @@ -81,13 +85,18 @@ class CoinbaseBuyDashOrderReviewViewModel @Inject constructor( } } - private fun sendDashToWallet(params: SendTransactionToWalletParams) = viewModelScope.launch(Dispatchers.Main) { + fun sendDash(api2FATokenVersion: String) = viewModelScope.launch(Dispatchers.Main) { + sendFundToWalletParams?.apply { + sendDashToWallet(this, api2FATokenVersion) + } + } + private fun sendDashToWallet(params: SendTransactionToWalletParams, api2FATokenVersion: String) = viewModelScope.launch(Dispatchers.Main) { if (_showLoading.value == false) _showLoading.value = true - when (val result = coinBaseRepository.sendFundsToWallet(params)) { + when (val result = coinBaseRepository.sendFundsToWallet(params, api2FATokenVersion)) { is ResponseResource.Success -> { _showLoading.value = false - if (result.value == null){ + if (result.value == null) { _transactionCompleted.value = TransactionState(false, null) } else { _transactionCompleted.value = TransactionState(true, null) @@ -96,7 +105,7 @@ class CoinbaseBuyDashOrderReviewViewModel @Inject constructor( is ResponseResource.Failure -> { _showLoading.value = false val error = result.errorBody?.string() - if (result.errorCode == 400){ + if (result.errorCode == 400) { error?.let { val message = CoinbaseErrorResponse.getErrorMessage(it) _transactionCompleted.value = TransactionState(false, message) @@ -108,12 +117,6 @@ class CoinbaseBuyDashOrderReviewViewModel @Inject constructor( } } - fun retry() { - sendFundToWalletParams?.let { - sendDashToWallet(it) - } - } - private fun placeBuyOrder(params: PlaceBuyOrderParams) = viewModelScope.launch(Dispatchers.Main) { _showLoading.value = true when (val result = coinBaseRepository.placeBuyOrder(params)) { diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConversionPreviewViewModel.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConversionPreviewViewModel.kt index 3c46408b27..b9d0f76ba7 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConversionPreviewViewModel.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConversionPreviewViewModel.kt @@ -47,28 +47,37 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( get() = _transactionCompleted var sendFundToWalletParams: SendTransactionToWalletParams? = null + private val _swapTradeOrder: MutableLiveData = MutableLiveData() val swapTradeOrder: LiveData get() = _swapTradeOrder + private val _commitBuyOrderSuccessCallback: MutableLiveData = MutableLiveData() + val commitBuyOrderSuccessCallback: LiveData + get() = _commitBuyOrderSuccessCallback + val swapTradeFailedCallback = SingleLiveEvent() - fun commitSwapTrade(params: String) = viewModelScope.launch(Dispatchers.Main) { + fun commitSwapTrade(params: SwapTradeUIModel) = viewModelScope.launch(Dispatchers.Main) { _showLoading.value = true - when (val result = coinBaseRepository.commitSwapTrade(params)) { + when (val result = coinBaseRepository.commitSwapTrade(params.swapTradeId)) { is ResponseResource.Success -> { if (result.value == SwapTradeResponse.EMPTY_SWAP_TRADE) { _showLoading.value = false commitBuyOrderFailedCallback.call() } else { - sendFundToWalletParams = SendTransactionToWalletParams( - amount = result.value.displayInputAmount, - currency = result.value.displayInputCurrency, - idem = UUID.randomUUID().toString(), - to = walletDataProvider.freshReceiveAddress().toBase58(), - type = "send" - ).apply { - sendDashToWallet(this) + if (params.inputCurrencyName.lowercase() == "dash") { + _transactionCompleted.value = TransactionState(true, null) + } else { + sendFundToWalletParams = SendTransactionToWalletParams( + amount = result.value.displayInputAmount, + currency = result.value.displayInputCurrency, + idem = UUID.randomUUID().toString(), + to = walletDataProvider.freshReceiveAddress().toBase58(), + type = "send" + ).apply { + _commitBuyOrderSuccessCallback.value = this + } } } } @@ -79,13 +88,19 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( } } - fun sendDashToWallet(params: SendTransactionToWalletParams) = viewModelScope.launch(Dispatchers.Main) { + fun sendDash(api2FATokenVersion: String) = viewModelScope.launch(Dispatchers.Main) { + commitBuyOrderSuccessCallback.value?.let { + sendDashToWallet(it, api2FATokenVersion) + } + } + + fun sendDashToWallet(params: SendTransactionToWalletParams, api2FATokenVersion: String) = viewModelScope.launch(Dispatchers.Main) { if (_showLoading.value == false) _showLoading.value = true - when (val result = coinBaseRepository.sendFundsToWallet(params)) { + when (val result = coinBaseRepository.sendFundsToWallet(params, api2FATokenVersion)) { is ResponseResource.Success -> { _showLoading.value = false - if (result.value == null){ + if (result.value == null) { _transactionCompleted.value = TransactionState(false, null) } else { _transactionCompleted.value = TransactionState(true, null) @@ -94,7 +109,7 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( is ResponseResource.Failure -> { _showLoading.value = false val error = result.errorBody?.string() - if (result.errorCode == 400){ + if (result.errorCode == 400) { error?.let { val message = CoinbaseErrorResponse.getErrorMessage(it) _transactionCompleted.value = TransactionState(false, message) @@ -106,11 +121,6 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( } } - fun retry() { - sendFundToWalletParams?.let { - sendDashToWallet(it) - } - } fun swapTrade(swapTradeUIModel: SwapTradeUIModel) = viewModelScope.launch(Dispatchers.Main) { _showLoading.value = true diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConvertCryptoViewModel.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConvertCryptoViewModel.kt index 400646e2ef..ae534de023 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConvertCryptoViewModel.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/CoinbaseConvertCryptoViewModel.kt @@ -20,21 +20,28 @@ import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.bitcoinj.core.Coin +import org.bitcoinj.core.InsufficientMoneyException import org.bitcoinj.utils.Fiat import org.dash.wallet.common.Configuration +import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.livedata.Event +import org.dash.wallet.common.services.SendPaymentService import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.integration.coinbase_integration.DASH_CURRENCY import org.dash.wallet.integration.coinbase_integration.model.* import org.dash.wallet.integration.coinbase_integration.network.ResponseResource import org.dash.wallet.integration.coinbase_integration.repository.CoinBaseRepositoryInt +import java.util.* import javax.inject.Inject @HiltViewModel class CoinbaseConvertCryptoViewModel @Inject constructor( private val coinBaseRepository: CoinBaseRepositoryInt, - val config: Configuration + val config: Configuration, + private val walletDataProvider: WalletDataProvider, + private val sendPaymentService: SendPaymentService ) : ViewModel() { private val _userAccountsInfo: MutableLiveData> = MutableLiveData() val userAccountsInfo: LiveData> @@ -54,7 +61,6 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( val swapTradeFailedCallback = SingleLiveEvent() - private val _userAccountsWithBalance: MutableLiveData>> = MutableLiveData() val userAccountsWithBalance: LiveData>> get() = _userAccountsWithBalance @@ -63,8 +69,16 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( val userAccountError: LiveData get() = _userAccountError + private val _dashWalletBalance = MutableLiveData() + val dashWalletBalance: LiveData + get() = this._dashWalletBalance + val getUserAccountAddressFailedCallback = SingleLiveEvent() + val onFailure = SingleLiveEvent() + val onInsufficientMoneyCallback = SingleLiveEvent() + val sendDashToCoinBaseFailed = SingleLiveEvent() init { + setDashWalletBalance() getUserAccountInfo() getBaseIdForUSDModel() } @@ -95,12 +109,59 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( } } - fun swapTrade(valueToConvert: Fiat, selectedCoinBaseAccount: CoinBaseUserAccountDataUIModel) = viewModelScope.launch(Dispatchers.Main) { + + fun sellDashToCoinBase(coin: Coin, valueToConvert: Fiat, selectedCoinBaseAccount: CoinBaseUserAccountDataUIModel) = viewModelScope.launch(Dispatchers.Main) { + _showLoading.value = true + + + when (val result = coinBaseRepository.createAddress()) { + is ResponseResource.Success -> { + if (result.value?.isEmpty() == true) { + _showLoading.value = false + getUserAccountAddressFailedCallback.call() + } else { + result.value?.let { + if (sendDashToCoinbase(coin, result.value)) { + swapTrade(valueToConvert, selectedCoinBaseAccount, true) + } else { + _showLoading.value = false + sendDashToCoinBaseFailed.call() + } + } + _showLoading.value = false + } + } + is ResponseResource.Failure -> { + _showLoading.value = false + getUserAccountAddressFailedCallback.call() + } + } + } + + private suspend fun sendDashToCoinbase(coin: Coin, addressInfo: String): Boolean { + val address = walletDataProvider.createSentDashAddress(addressInfo) + try { + val transaction = sendPaymentService.sendCoins(address, coin) + return transaction.isPending + } catch (x: InsufficientMoneyException) { + onInsufficientMoneyCallback.call() + x.printStackTrace() + return false + } catch (ex: Exception) { + onFailure.value = ex.message + ex.printStackTrace() + return false + } + } + + fun swapTrade(valueToConvert: Fiat, selectedCoinBaseAccount: CoinBaseUserAccountDataUIModel, dashToCrypt: Boolean) = viewModelScope.launch(Dispatchers.Main) { _showLoading.value = true val source_asset = - _baseIdForUSDModelCoinBase.value?.firstOrNull { it.base == selectedCoinBaseAccount.coinBaseUserAccountData.currency?.code }?.base_id ?: "" - val target_asset = + if (dashToCrypt)_baseIdForUSDModelCoinBase.value?.firstOrNull { it.base == DASH_CURRENCY }?.base_id ?: "" + else _baseIdForUSDModelCoinBase.value?.firstOrNull { it.base == selectedCoinBaseAccount.coinBaseUserAccountData.currency?.code }?.base_id ?: "" + val target_asset = if (dashToCrypt)_baseIdForUSDModelCoinBase.value?.firstOrNull { it.base == selectedCoinBaseAccount.coinBaseUserAccountData.currency?.code }?.base_id ?: "" + else _baseIdForUSDModelCoinBase.value?.firstOrNull { it.base == DASH_CURRENCY }?.base_id ?: "" val tradesRequest = TradesRequest( @@ -121,9 +182,12 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( result.value.apply { this.assetsBaseID = Pair(source_asset, target_asset) - this.inputCurrencyName = + this.inputCurrencyName = if (dashToCrypt)"Dash" + else selectedCoinBaseAccount.coinBaseUserAccountData.currency?.name ?: "" - this.outputCurrencyName = "Dash" + this.outputCurrencyName = if (dashToCrypt) selectedCoinBaseAccount.coinBaseUserAccountData.currency?.name ?: "" + else + "Dash" _swapTradeOrder.value = Event(this) } } @@ -170,6 +234,11 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( ) = ( it.coinBaseUserAccountData.balance?.amount?.toDouble() != null && !it.coinBaseUserAccountData.balance.amount.toDouble().isNaN() && + it.coinBaseUserAccountData.type != "fiat" && it.coinBaseUserAccountData.balance.currency != DASH_CURRENCY ) + + private fun setDashWalletBalance() { + _dashWalletBalance.value = walletDataProvider.getWalletBalance() + } } diff --git a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/ConvertViewViewModel.kt b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/ConvertViewViewModel.kt index a08280de7d..bdf7a4505b 100644 --- a/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/ConvertViewViewModel.kt +++ b/integrations/coinbase-integration/src/main/java/org/dash/wallet/integration/coinbase_integration/viewmodels/ConvertViewViewModel.kt @@ -26,11 +26,17 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.bitcoinj.core.Coin import org.bitcoinj.utils.Fiat +import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration +import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ExchangeRate import org.dash.wallet.common.data.SingleLiveEvent +import org.dash.wallet.common.livedata.Event import org.dash.wallet.common.services.ExchangeRatesProvider +import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.integration.coinbase_integration.model.CoinBaseUserAccountDataUIModel +import org.dash.wallet.integration.coinbase_integration.ui.convert_currency.model.SwapValueErrorType +import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject @@ -38,16 +44,27 @@ import javax.inject.Inject @HiltViewModel class ConvertViewViewModel @Inject constructor( var exchangeRates: ExchangeRatesProvider, - var configuration: Configuration + var configuration: Configuration, + private val walletDataProvider: WalletDataProvider ) : ViewModel() { + val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + .noCode().minDecimals(6).optionalDecimals() + private val _dashToCrypto = MutableLiveData() val dashToCrypto: LiveData get() = this._dashToCrypto var enteredConvertAmount = "0" - var maxAmount: String = "0" - val onContinueEvent = SingleLiveEvent>() + var maxCoinBaseAccountAmount: String = "0" + + var minAllowedSwapAmount: String = "2" + + var maxForDashWalletAmount: String = "0" + val onContinueEvent = SingleLiveEvent?>>() + + var minAllowedSwapDashCoin: Coin = Coin.ZERO + private var maxForDashCoinBaseAccount: Coin = Coin.ZERO private val _selectedCryptoCurrencyAccount = MutableLiveData() val selectedCryptoCurrencyAccount: LiveData @@ -75,7 +92,18 @@ class ConvertViewViewModel @Inject constructor( val selectedLocalExchangeRate: LiveData get() = _selectedLocalExchangeRate + private val _dashWalletBalance = MutableLiveData>() + val dashWalletBalance: LiveData> + get() = this._dashWalletBalance + + private val _userDashAccountEmptyError: SingleLiveEvent = SingleLiveEvent() + val userDashAccountEmptyError: LiveData + get() = _userDashAccountEmptyError + + val validSwapValue = SingleLiveEvent() + init { + setDashWalletBalance() _selectedLocalCurrencyCode.flatMapLatest { code -> exchangeRates.observeExchangeRate(code) }.onEach(_selectedLocalExchangeRate::postValue) @@ -84,7 +112,8 @@ class ConvertViewViewModel @Inject constructor( fun setSelectedCryptoCurrency(account: CoinBaseUserAccountDataUIModel) { - maxAmount = account.coinBaseUserAccountData.balance?.amount ?: "0" + maxCoinBaseAccountAmount = account.coinBaseUserAccountData.balance?.amount ?: "0" + this._selectedLocalExchangeRate.value = selectedLocalExchangeRate.value?.currencyCode?.let { val cleanedValue = 1.toBigDecimal() / @@ -95,17 +124,84 @@ class ConvertViewViewModel @Inject constructor( bd.toString() ) } - this._selectedCryptoCurrencyAccount.value = account + + val cleanedValue: BigDecimal = + minAllowedSwapAmount.toBigDecimal() * account.currencyToDashExchangeRate.toBigDecimal() + val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) + + val coin = try { + Coin.parseCoin(bd.toString()) + } catch (x: Exception) { + Coin.ZERO + } + + minAllowedSwapDashCoin = coin + + val value = + (maxCoinBaseAccountAmount.toBigDecimal() * account.currencyToDashExchangeRate.toBigDecimal()) + .setScale(8, RoundingMode.HALF_UP) + + val maxCoinValue = try { + Coin.parseCoin(value.toString()) + } catch (x: Exception) { + Coin.ZERO + } + + maxForDashCoinBaseAccount = maxCoinValue } fun setEnteredConvertDashAmount(value: Coin) { _enteredConvertDashAmount.value = value + if (value.isZero) + resetSwapValueError() + } + + fun resetSwapValueError() { + validSwapValue.call() + } + + + fun checkEnteredAmountValue(): SwapValueErrorType { + val coin = try { + if (dashToCrypto.value == true) { + Coin.parseCoin(maxForDashWalletAmount) + } else { + maxForDashCoinBaseAccount + } + } catch (x: Exception) { + Coin.ZERO + } + + _enteredConvertDashAmount.value?.let { + return when { + it.isZero -> SwapValueErrorType.NOError + it.isLessThan(minAllowedSwapDashCoin) -> SwapValueErrorType.LessThanMin + it.isGreaterThan(coin) -> SwapValueErrorType.MoreThanMax + else -> SwapValueErrorType.NOError + } + } + return SwapValueErrorType.NOError } fun setOnSwapDashFromToCryptoClicked(dashToCrypto: Boolean) { + if (dashToCrypto) { + if (walletDataProvider.getWalletBalance().isZero) { + _userDashAccountEmptyError.value = true + return + } + } _dashToCrypto.value = dashToCrypto } fun clear() { _selectedCryptoCurrencyAccount.value = null } + + private fun setDashWalletBalance() { + val balance = walletDataProvider.getWalletBalance() + _dashWalletBalance.value = Event(balance) + + maxForDashWalletAmount = dashFormat.minDecimals(0) + .optionalDecimals(0, 8).format(balance).toString() + } } + diff --git a/integrations/coinbase-integration/src/main/res/layout/fragment_coinbase_convert_crypto.xml b/integrations/coinbase-integration/src/main/res/layout/fragment_coinbase_convert_crypto.xml index c356ac2fae..b21a59f200 100644 --- a/integrations/coinbase-integration/src/main/res/layout/fragment_coinbase_convert_crypto.xml +++ b/integrations/coinbase-integration/src/main/res/layout/fragment_coinbase_convert_crypto.xml @@ -84,24 +84,39 @@ style="@style/Caption" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/you_will_receive" - android:layout_marginTop="10dp" android:layout_marginStart="30dp" + android:layout_marginTop="10dp" + android:text="@string/you_will_receive" android:visibility="gone" - tools:visibility="visible" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/convert_view"/> + app:layout_constraintTop_toBottomOf="@+id/convert_view" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@+id/convert_view" + tools:text="111" + tools:visibility="visible" /> + + \ No newline at end of file diff --git a/integrations/coinbase-integration/src/main/res/layout/fragment_convert_currency.xml b/integrations/coinbase-integration/src/main/res/layout/fragment_convert_currency.xml index b9f98892a9..de4ae08f9a 100644 --- a/integrations/coinbase-integration/src/main/res/layout/fragment_convert_currency.xml +++ b/integrations/coinbase-integration/src/main/res/layout/fragment_convert_currency.xml @@ -33,9 +33,6 @@ tools:layout="@layout/vertical_segmented_picker" tools:visibility="visible" /> - - - From your Coinbase account you receive To Dash wallet on this device + to your Coinbase account + From Dash wallet on this device You will receive %s DASH We didn’t find any assets on your Coinbase account. @@ -82,5 +84,13 @@ You can change it anytime by logging out of your Coinbase account in Dash Wallet and logging back in. I got it Cannot send this amount without going over application limit + You don\'t have any Dash in your Dash Wallet. Disconnected + + Not enough coins + The amount of coins in the wallet is too small for sweeping. + Problem sending coins! + Entered amount is too low + Entered amount is too high + \ No newline at end of file diff --git a/integrations/coinbase-integration/src/test/java/org/dash/wallet/integration/coinbase_integration/CoinBaseRepositoryTest.kt b/integrations/coinbase-integration/src/test/java/org/dash/wallet/integration/coinbase_integration/CoinBaseRepositoryTest.kt index b26da2e60e..2807a579a5 100644 --- a/integrations/coinbase-integration/src/test/java/org/dash/wallet/integration/coinbase_integration/CoinBaseRepositoryTest.kt +++ b/integrations/coinbase-integration/src/test/java/org/dash/wallet/integration/coinbase_integration/CoinBaseRepositoryTest.kt @@ -42,6 +42,7 @@ class CoinBaseRepositoryTest { @MockK lateinit var placeBuyOrderMapper: PlaceBuyOrderMapper @MockK lateinit var swapTradeMapper: SwapTradeMapper @MockK lateinit var commitBuyOrderMapper: CommitBuyOrderMapper + @MockK lateinit var coinbaseAddressMapper: CoinbaseAddressMapper private lateinit var coinBaseRepository: CoinBaseRepository private val accountId = "423095d3-bb89-5cef-b1bc-d1dfe6e13857" @@ -54,7 +55,8 @@ class CoinBaseRepositoryTest { configuration, placeBuyOrderMapper, swapTradeMapper, - commitBuyOrderMapper + commitBuyOrderMapper, + coinbaseAddressMapper ) coEvery { configuration.coinbaseUserAccountId } returns accountId } @@ -93,9 +95,9 @@ class CoinBaseRepositoryTest { fun `when sending funds to dash wallet, repository returns success response `() { val params = SendTransactionToWalletParams("0.5", "usd", "9316dd16-0c05", "XfVe4NAHTp6NwWuM3PGpmUSwuZuWWE9qY3", "send") val expectedSendFundsToWalletResponse = TestUtils.sendFundsToWalletApiResponse() - coEvery { coinBaseServicesApi.sendCoinsToWallet(accountId = accountId, sendTransactionToWalletParams = params) } returns expectedSendFundsToWalletResponse + coEvery { coinBaseServicesApi.sendCoinsToWallet(api2FATokenVersion ="2345",accountId = accountId, sendTransactionToWalletParams = params) } returns expectedSendFundsToWalletResponse - runBlocking { coinBaseRepository.sendFundsToWallet(params) } - coVerify { coinBaseServicesApi.sendCoinsToWallet(accountId = accountId, sendTransactionToWalletParams = params) } + runBlocking { coinBaseRepository.sendFundsToWallet(params,"2345") } + coVerify { coinBaseServicesApi.sendCoinsToWallet(api2FATokenVersion = "2345",accountId = accountId, sendTransactionToWalletParams = params) } } } diff --git a/wallet/src/de/schildbach/wallet/BaseWalletApplication.kt b/wallet/src/de/schildbach/wallet/BaseWalletApplication.kt index 8e597eb5b4..828eac093b 100644 --- a/wallet/src/de/schildbach/wallet/BaseWalletApplication.kt +++ b/wallet/src/de/schildbach/wallet/BaseWalletApplication.kt @@ -98,6 +98,10 @@ abstract class BaseWalletApplication : MultiDexApplication(), WalletDataProvider return SendCoinsTask.sendCoins(wallet, sendRequest, scryptIterationsTarget) } + override fun createSentDashAddress(address: String): Address { + return Address.fromString(Constants.NETWORK_PARAMETERS, address.toString().trim { it <= ' ' }) + } + private fun createSendRequest(address: Address, amount: Coin): SendRequest { return SendRequest.to(address, amount).apply { coinSelector = ZeroConfCoinSelector.get() @@ -107,6 +111,10 @@ abstract class BaseWalletApplication : MultiDexApplication(), WalletDataProvider } } + override fun getWalletBalance(): Coin { + return walletApplication.wallet.getBalance(Wallet.BalanceType.ESTIMATED) + } + private fun checkWalletCreated() { if (getWalletData() == null) { throw RuntimeException("this method cant't be used before creating the wallet") diff --git a/wallet/src/de/schildbach/wallet/Constants.java b/wallet/src/de/schildbach/wallet/Constants.java index 20f17af1a6..9ffd067650 100644 --- a/wallet/src/de/schildbach/wallet/Constants.java +++ b/wallet/src/de/schildbach/wallet/Constants.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; +import org.bitcoinj.core.Coin; import org.bitcoinj.core.CoinDefinition; import org.bitcoinj.core.Context; import org.bitcoinj.core.NetworkParameters; @@ -53,6 +54,8 @@ public final class Constants { /** Network this wallet is on (e.g. testnet or mainnet). */ public static final NetworkParameters NETWORK_PARAMETERS; + public static final Coin ECONOMIC_FEE = Coin.valueOf(1000); + private static String FILENAME_NETWORK_SUFFIX; /** Currency code for the wallet name resolver. */ diff --git a/wallet/src/de/schildbach/wallet/di/AppModule.kt b/wallet/src/de/schildbach/wallet/di/AppModule.kt index 826c70c0d1..5f5716eb8a 100644 --- a/wallet/src/de/schildbach/wallet/di/AppModule.kt +++ b/wallet/src/de/schildbach/wallet/di/AppModule.kt @@ -17,12 +17,17 @@ package de.schildbach.wallet.di +import android.content.Context import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.payments.SendCoinsTaskRunner import org.dash.wallet.common.services.LockScreenBroadcaster +import org.dash.wallet.common.services.SendPaymentService import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.services.analytics.FirebaseAnalyticsServiceImpl import javax.inject.Singleton @@ -31,6 +36,11 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) abstract class AppModule { companion object { + @Provides + fun provideApplication( + @ApplicationContext context: Context + ): WalletApplication = context as WalletApplication + @Singleton @Provides fun provideLockScreenBroadcaster(): LockScreenBroadcaster = LockScreenBroadcaster() @@ -40,4 +50,9 @@ abstract class AppModule { abstract fun bindAnalyticsService( analyticsService: FirebaseAnalyticsServiceImpl ): AnalyticsService + + @Binds + abstract fun bindSendPaymentService( + sendCoinsTaskRunner: SendCoinsTaskRunner + ): SendPaymentService } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt b/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt new file mode 100644 index 0000000000..be2537ce7e --- /dev/null +++ b/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt @@ -0,0 +1,152 @@ +/* + * 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 . + */ +package de.schildbach.wallet.payments + +import com.google.common.base.Preconditions +import de.schildbach.wallet.Constants +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.ui.security.SecurityGuard +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.bitcoinj.core.* +import org.bitcoinj.crypto.KeyCrypterException +import org.bitcoinj.crypto.KeyCrypterScrypt +import org.bitcoinj.utils.ExchangeRate +import org.bitcoinj.wallet.SendRequest +import org.bitcoinj.wallet.Wallet +import org.bitcoinj.wallet.ZeroConfCoinSelector +import org.bouncycastle.crypto.params.KeyParameter +import org.dash.wallet.common.services.SendPaymentService +import org.dash.wallet.common.transactions.ByAddressCoinSelector +import org.slf4j.LoggerFactory +import javax.inject.Inject + + +class SendCoinsTaskRunner @Inject constructor( + private val walletApplication: WalletApplication +) : SendPaymentService { + private val log = LoggerFactory.getLogger(SendCoinsTaskRunner::class.java) + + override suspend fun sendCoins( + address: Address, + amount: Coin, + constrainInputsTo: Address?, + emptyWallet: Boolean + ): Transaction { + val wallet = walletApplication.wallet ?: throw RuntimeException("this method can't be used before creating the wallet") + Context.propagate(wallet.context) + val sendRequest = createSendRequest(address, amount, constrainInputsTo, emptyWallet) + val scryptIterationsTarget = walletApplication.scryptIterationsTarget() + + return sendCoins(wallet, sendRequest, scryptIterationsTarget) + } + + private fun createSendRequest( + address: Address, + amount: Coin, + constrainInputsTo: Address? = null, + emptyWallet: Boolean = false + ): SendRequest { + return SendRequest.to(address, amount).apply { + coinSelector = ZeroConfCoinSelector.get() + coinSelector = if (constrainInputsTo == null) ZeroConfCoinSelector.get() else ByAddressCoinSelector(constrainInputsTo) + feePerKb = Constants.ECONOMIC_FEE + ensureMinRequiredFee = true + changeAddress = constrainInputsTo + this.emptyWallet = emptyWallet + } + } + + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun sendCoins( + wallet: Wallet, + sendRequest: SendRequest, + scryptIterationsTarget: Int, + exchangeRate: ExchangeRate? = null, + txCompleted: Boolean = false + ): Transaction = withContext(Dispatchers.IO) { + Context.propagate(wallet.context) + + val securityGuard = SecurityGuard() + val password = securityGuard.retrievePassword() + val encryptionKey = deriveKey(wallet, password, scryptIterationsTarget) + + sendRequest.aesKey = encryptionKey + sendRequest.exchangeRate = exchangeRate + + try { + log.info("sending: {}", sendRequest) + + if (txCompleted) { + wallet.commitTx(sendRequest.tx) + } else { + wallet.sendCoinsOffline(sendRequest) + } + + val transaction = sendRequest.tx + log.info("send successful, transaction committed: {}", transaction.txId.toString()) + walletApplication.broadcastTransaction(transaction) + transaction + } catch (ex: Exception) { + when (ex) { + is InsufficientMoneyException -> ex.missing?.run { + log.info("send failed, {} missing", toFriendlyString()) + } ?: log.info("send failed, insufficient coins") + is ECKey.KeyIsEncryptedException -> log.info("send failed, key is encrypted: {}", ex.message) + is KeyCrypterException -> log.info("send failed, key crypter exception: {}", ex.message) + is Wallet.CouldNotAdjustDownwards -> log.info("send failed, could not adjust downwards: {}", ex.message) + is Wallet.CompletionException -> log.info("send failed, cannot complete: {}", ex.message) + } + throw ex + } + } + + @Throws(KeyCrypterException::class) + private fun deriveKey(wallet: Wallet, password: String, scryptIterationsTarget: Int): KeyParameter { + Preconditions.checkState(wallet.isEncrypted) + val keyCrypter = wallet.keyCrypter!! + + // Key derivation takes time. + var key = keyCrypter.deriveKey(password) + + // If the key isn't derived using the desired parameters, derive a new key. + if (keyCrypter is KeyCrypterScrypt) { + val scryptIterations = keyCrypter.scryptParameters.n + + if (scryptIterations != scryptIterationsTarget.toLong()) { + log.info( + "upgrading scrypt iterations from {} to {}; re-encrypting wallet", + scryptIterations, scryptIterationsTarget + ) + val newKeyCrypter = KeyCrypterScrypt(scryptIterationsTarget) + val newKey: KeyParameter = newKeyCrypter.deriveKey(password) + + // Re-encrypt wallet with new key. + try { + wallet.changeEncryptionKey(newKeyCrypter, key, newKey) + key = newKey + log.info("scrypt upgrade succeeded") + } catch (x: KeyCrypterException) { + log.info("scrypt upgrade failed: {}", x.message) + } + } + } + + // Hand back the (possibly changed) encryption key. + return key + } +} diff --git a/wallet/src/de/schildbach/wallet/ui/coinbase/CoinBaseWebClientActivity.kt b/wallet/src/de/schildbach/wallet/ui/coinbase/CoinBaseWebClientActivity.kt index dc0187cae4..f54c1adf4c 100644 --- a/wallet/src/de/schildbach/wallet/ui/coinbase/CoinBaseWebClientActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/coinbase/CoinBaseWebClientActivity.kt @@ -69,11 +69,12 @@ class CoinBaseWebClientActivity : InteractionAwareActivity() { "https://www.coinbase.com/oauth/authorize?client_id=1ca2946d789bf6d986f26df03f4a52a8c6f1" + "fe80e469eb1d3477e7c90768559a&redirect_uri=https://coin.base.test/callback&response_type" + "=code&scope=wallet:accounts:read,wallet:user:read,wallet:payment-methods:read," + - "wallet:buys:read,wallet:buys:create,wallet:transactions:transfer,wallet:" + - "transactions:request,wallet:transactions:read,wallet:trades:create,wallet:supported-assets:read,wallet:transactions:" + - "send&meta[send_limit_amount]=1&" + - "meta[send_limit_currency]=USD&" + - "meta[send_limit_period]=month" + + "wallet:buys:read,wallet:buys:create,wallet:transactions:transfer," + + "wallet:sells:create,wallet:sells:read," + + "wallet:transactions:request,wallet:transactions:read,wallet:trades:create," + + "wallet:supported-assets:read,wallet:transactions:send," + "wallet:addresses:read" + "&meta[send_limit_amount]=1" + + "&meta[send_limit_currency]=USD" + + "&meta[send_limit_period]=month" + "&account=all" binding.webView.loadUrl(loginUrl)