diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 9fdfca3cd7..0b76964de9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -2,6 +2,8 @@ package com.tangem.tap.domain import androidx.activity.ComponentActivity import com.tangem.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.WalletManager import com.tangem.commands.CommandResponse import com.tangem.common.CompletionResult import com.tangem.common.extensions.CardType @@ -9,6 +11,7 @@ import com.tangem.tangem_sdk_new.extensions.init import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.domain.tasks.ScanNoteTask +import com.tangem.tap.domain.tasks.SendTask import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.* @@ -28,6 +31,21 @@ class TangemSdkManager(val activity: ComponentActivity) { return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask()) } + suspend fun send( + walletManager: WalletManager, + recipientAddress: String, + amountToSend: Amount, + feeAmount: Amount + ): CompletionResult { + return withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + tangemSdk.startSessionWithRunnable(SendTask(walletManager, recipientAddress, amountToSend, feeAmount)) { + continuation.resume(it) + } + } + } + } + private suspend fun runTaskAsync( runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null ): CompletionResult = diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 1141048a49..29147d93c6 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -9,4 +9,6 @@ sealed class TapError(@StringRes val localizedMessage: Int): Throwable() { object PayIdEmptyField: TapError(R.string.wallet_create_payid_empty) object UnknownBlockchain: TapError(R.string.wallet_unknown_blockchain) object NoInternetConnection: TapError(R.string.notification_no_internet) + object InsufficientBalance: TapError(R.string.error_insufficient_balance) + object BlockchainInternalError: TapError(R.string.error_insufficient_balance) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt new file mode 100644 index 0000000000..e5cdbf959a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt @@ -0,0 +1,114 @@ +package com.tangem.tap.domain.tasks + +import com.tangem.CardSession +import com.tangem.CardSessionRunnable +import com.tangem.TangemError +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.commands.CommandResponse +import com.tangem.commands.SignCommand +import com.tangem.commands.SignResponse +import com.tangem.common.CompletionResult +import com.tangem.tap.scope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import kotlin.coroutines.resume + +class SendTask( + private val walletManager: WalletManager, + private val recipientAddress: String, + private val amountToSend: Amount, + private val feeAmount: Amount, +) : CardSessionRunnable { + + override val requiresPin2: Boolean = false + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + val txSender = walletManager as TransactionSender + + val verifyResult = walletManager.validateTransaction(amountToSend, feeAmount) + if (verifyResult.isNotEmpty()) { + callback(CompletionResult.Failure(InsufficientBalance())) + return + } + val txData = walletManager.createTransaction(amountToSend, feeAmount, recipientAddress) + + scope.launch { + when (val result = txSender.send(txData, SessionTransactionSigner(session))) { + is SimpleResult.Success -> callback(CompletionResult.Success(SendResponse())) + is SimpleResult.Failure -> { + callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(result.error))) + } + } + } + } +} + +class SendResponse : CommandResponse + +class SessionTransactionSigner( + private val session: CardSession +) : TransactionSigner { + override suspend fun sign(hashes: Array, cardId: String): CompletionResult = + suspendCancellableCoroutine { continuation -> + Timber.d("sign transaction...") + SignCommand(hashes).run(session) { + if (continuation.isActive) { + continuation.resume(it) + } + } + } +} + +abstract class SendError : TangemError { +} + +class UnknownError : SendError() { + override val code: Int = 1000 + override var customMessage: String = "Unknown error" +} + +open class ThrowableError(throwable: Throwable?) : SendError() { + override val code: Int = 1001 + override var customMessage: String = throwable?.localizedMessage ?: "Unknown exception" +} + +class InsufficientBalance( + override var customMessage: String = "Insufficient balance" +) : SendError() { + override val code: Int = 1021 +} + +class BlockchainInternalError( + override var customMessage: String +) : SendError() { + override val code: Int = 2000 +} + +class BlockchainInternalErrorConverter { + companion object { + + private val stellarInternalErrors = mapOf( + "tx_bad_seq" to "Sequence number does not match source account", + "tx_too_late" to "The ledger closeTime was after the maxTime", + "tx_failedop_no_destination" to "The destination account does not exist", + "tx_no_source_account" to "Source account not found" + ) + + fun convert(throwable: Throwable?): TangemError { + val message = throwable?.message ?: return ThrowableError(throwable) + + val customMessage = getInternalBlockchainErrorMessage(message) + return if (customMessage == null) ThrowableError(throwable) + else BlockchainInternalError(customMessage) + } + + private fun getInternalBlockchainErrorMessage(message: String): String? { + return stellarInternalErrors[message] + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 7c7c5da116..1f76fa2399 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.send.redux import com.tangem.blockchain.common.Amount +import com.tangem.tap.common.redux.ErrorAction +import com.tangem.tap.domain.TapError import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import org.rekotlin.Action @@ -94,4 +96,23 @@ sealed class FeeAction : SendScreenAction { sealed class ReceiptAction : SendScreenAction { object RefreshReceipt : ReceiptAction() +} + +sealed class SendActionUi : SendScreenActionUi { + object SendAmountToRecipient : SendScreenActionUi +} + +sealed class SendAction : SendScreenAction { + enum class Error { + INSUFFICIENT_BALANCE, BLOCKCHAIN_INTERNAL + } + + object SendSuccess : SendAction() + + data class SendError(val sendError: Error) : SendAction(), ErrorAction { + override val error: TapError = when (sendError) { + Error.INSUFFICIENT_BALANCE -> TapError.InsufficientBalance + Error.BLOCKCHAIN_INTERNAL -> TapError.BlockchainInternalError + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index aff69f3f5a..03645a9444 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -1,9 +1,17 @@ package com.tangem.tap.features.send.redux.middlewares +import com.tangem.blockchain.common.Amount +import com.tangem.common.CompletionResult import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.send.redux.AddressPayIdActionUi.ChangeAddressOrPayId import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend import com.tangem.tap.features.send.redux.FeeAction.RequestFee +import com.tangem.tap.features.send.redux.SendAction +import com.tangem.tap.features.send.redux.SendActionUi +import com.tangem.tap.scope +import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.launch +import org.rekotlin.Action import org.rekotlin.Middleware /** @@ -16,8 +24,34 @@ val sendMiddleware: Middleware = { dispatch, appState -> is ChangeAddressOrPayId -> AddressPayIdMiddleware().handle(action.data, appState(), dispatch) is CheckAmountToSend -> AmountMiddleware().handle(action.data, appState(), dispatch) is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch) + is SendActionUi.SendAmountToRecipient -> verifyAndSendTransaction(appState(), dispatch) } nextDispatch(action) } } -} \ No newline at end of file +} + +private fun verifyAndSendTransaction(appState: AppState?, dispatch: (Action) -> Unit) { + val sendState = appState?.sendState ?: return + val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return + + val blockchain = walletManager.wallet.blockchain + val recipientAddress = sendState.addressPayIdState.recipientWalletAddress!! + + val feeAmount = Amount(sendState.feeState.getCurrentFee(), blockchain) + val amountToSend = Amount(sendState.amountState.amountToSendCrypto, blockchain, recipientAddress) + + scope.launch { + when (val sendResult = tangemSdkManager.send(walletManager, recipientAddress, amountToSend, feeAmount)) { + is CompletionResult.Success -> dispatch(SendAction.SendSuccess) + is CompletionResult.Failure -> { + when (sendResult.error.code) { + 1021 -> dispatch(SendAction.SendError(SendAction.Error.INSUFFICIENT_BALANCE)) + 1001, 2000 -> dispatch(SendAction.SendError(SendAction.Error.BLOCKCHAIN_INTERNAL)) + } + } + } + } + +} + diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index e11aa250df..8a23874e94 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -17,7 +17,7 @@ import java.math.BigDecimal data class SendState( val amount: Amount? = null, val walletManager: WalletManager? = null, - val currencyConverter: CurrencyConverter = CurrencyConverter(BigDecimal.ZERO), + val currencyConverter: CurrencyConverter = CurrencyConverter(BigDecimal.ONE), val lastChangedStateType: StateType = NoneState(), val addressPayIdState: AddressPayIdState = AddressPayIdState(), val amountState: AmountState = AmountState(), diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index db93595f5f..1fd7e7664d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -23,6 +23,7 @@ import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.FeeActionUi.* import com.tangem.tap.features.send.redux.ReceiptAction import com.tangem.tap.features.send.redux.ReleaseSendState +import com.tangem.tap.features.send.redux.SendActionUi import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber @@ -31,6 +32,7 @@ import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.btn_paste.* import kotlinx.android.synthetic.main.btn_qr_code.* +import kotlinx.android.synthetic.main.fragment_send.* import kotlinx.android.synthetic.main.layout_send_address_payid.* import kotlinx.android.synthetic.main.layout_send_amount.* import kotlinx.android.synthetic.main.layout_send_fee.* @@ -52,6 +54,10 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { setupAddressOrPayIdLayout() setupAmountLayout() setupFeeLayout() + + btnSend.setOnClickListener { + store.dispatch(SendActionUi.SendAmountToRecipient) + } } private fun setupAddressOrPayIdLayout() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14fd7dcb8c..a871a0485b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,8 +51,10 @@ Address is invalid or unsupported by blockchain Address is the same as wallet address Network fee request is failed - FEE_GREATER_THAN_AMOUNT - AMOUNT_WITH_FEE_GREATER_THAN_BALANCE + Fee greater than amount + Amount with fee greater than balance + Insufficient balance + Blockchain internal error Send