summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt')
-rw-r--r--wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt267
1 files changed, 227 insertions, 40 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index 5afb125..e308b2a 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -16,35 +16,47 @@
package net.taler.wallet.withdraw
+import android.net.Uri
import android.util.Log
import androidx.annotation.UiThread
-import androidx.lifecycle.LiveData
+import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.common.Bech32
import net.taler.common.Event
import net.taler.common.toEvent
-import net.taler.lib.common.Amount
import net.taler.wallet.TAG
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.exchanges.ExchangeFees
import net.taler.wallet.exchanges.ExchangeItem
+import net.taler.wallet.transactions.WithdrawalExchangeAccountDetails
import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
sealed class WithdrawStatus {
data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus()
- data class NeedsExchange(val exchangeSelection: Event<ExchangeSelection>) : WithdrawStatus()
+
+ data class NeedsExchange(
+ val talerWithdrawUri: String,
+ val amount: Amount,
+ val possibleExchanges: List<ExchangeItem>,
+ ) : WithdrawStatus()
data class TosReviewRequired(
val talerWithdrawUri: String? = null,
val exchangeBaseUrl: String,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+ val ageRestrictionOptions: List<Int>? = null,
val tosText: String,
val tosEtag: String,
val showImmediately: Event<Boolean>,
+ val possibleExchanges: List<ExchangeItem> = emptyList(),
) : WithdrawStatus()
data class ReceivedDetails(
@@ -52,13 +64,62 @@ sealed class WithdrawStatus {
val exchangeBaseUrl: String,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+ val ageRestrictionOptions: List<Int>? = null,
+ val possibleExchanges: List<ExchangeItem> = emptyList(),
+ ) : WithdrawStatus()
+
+ data object Withdrawing : WithdrawStatus()
+
+ data class Success(val currency: String, val transactionId: String) : WithdrawStatus()
+
+ class ManualTransferRequired(
+ val transactionId: String?,
+ val transactionAmountRaw: Amount,
+ val transactionAmountEffective: Amount,
+ val exchangeBaseUrl: String,
+ val withdrawalTransfers: List<TransferData>,
) : WithdrawStatus()
- object Withdrawing : WithdrawStatus()
- data class Success(val currency: String) : WithdrawStatus()
data class Error(val message: String?) : WithdrawStatus()
}
+sealed class TransferData {
+ abstract val subject: String
+ abstract val amountRaw: Amount
+ abstract val amountEffective: Amount
+ abstract val withdrawalAccount: WithdrawalExchangeAccountDetails
+
+ val currency get() = withdrawalAccount.transferAmount?.currency
+
+ data class Taler(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val receiverName: String? = null,
+ val account: String,
+ ): TransferData()
+
+ data class IBAN(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val receiverName: String? = null,
+ val iban: String,
+ ): TransferData()
+
+ data class Bitcoin(
+ override val subject: String,
+ override val amountRaw: Amount,
+ override val amountEffective: Amount,
+ override val withdrawalAccount: WithdrawalExchangeAccountDetails,
+ val account: String,
+ val segwitAddresses: List<String>,
+ ): TransferData()
+}
+
sealed class WithdrawTestStatus {
object Withdrawing : WithdrawTestStatus()
object Success : WithdrawTestStatus()
@@ -73,15 +134,31 @@ data class WithdrawalDetailsForUri(
)
@Serializable
-data class WithdrawalDetails(
+data class WithdrawExchangeResponse(
+ val exchangeBaseUrl: String,
+ val amount: Amount? = null,
+)
+
+@Serializable
+data class ManualWithdrawalDetails(
val tosAccepted: Boolean,
val amountRaw: Amount,
val amountEffective: Amount,
+ val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
+ val ageRestrictionOptions: List<Int>? = null,
+ val scopeInfo: ScopeInfo,
)
-data class ExchangeSelection(
- val amount: Amount,
- val talerWithdrawUri: String,
+@Serializable
+data class AcceptWithdrawalResponse(
+ val transactionId: String,
+)
+
+@Serializable
+data class AcceptManualWithdrawalResponse(
+ val reservePub: String,
+ val withdrawalAccountsList: List<WithdrawalExchangeAccountDetails>,
+ val transactionId: String,
)
class WithdrawManager(
@@ -92,8 +169,6 @@ class WithdrawManager(
val withdrawStatus = MutableLiveData<WithdrawStatus>()
val testWithdrawalStatus = MutableLiveData<WithdrawTestStatus>()
- private val _exchangeSelection = MutableLiveData<Event<ExchangeSelection>>()
- val exchangeSelection: LiveData<Event<ExchangeSelection>> = _exchangeSelection
var exchangeFees: ExchangeFees? = null
private set
@@ -106,11 +181,6 @@ class WithdrawManager(
}
}
- @UiThread
- fun selectExchange(selection: ExchangeSelection) {
- _exchangeSelection.value = selection.toEvent()
- }
-
fun getWithdrawalDetails(uri: String) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
api.request("getWithdrawalDetailsForUri", WithdrawalDetailsForUri.serializer()) {
@@ -119,11 +189,18 @@ class WithdrawManager(
handleError("getWithdrawalDetailsForUri", error)
}.onSuccess { details ->
if (details.defaultExchangeBaseUrl == null) {
- val exchangeSelection = ExchangeSelection(details.amount, uri)
- withdrawStatus.value = WithdrawStatus.NeedsExchange(exchangeSelection.toEvent())
- } else {
- getWithdrawalDetails(details.defaultExchangeBaseUrl, details.amount, false, uri)
- }
+ withdrawStatus.value = WithdrawStatus.NeedsExchange(
+ talerWithdrawUri = uri,
+ amount = details.amount,
+ possibleExchanges = details.possibleExchanges,
+ )
+ } else getWithdrawalDetails(
+ exchangeBaseUrl = details.defaultExchangeBaseUrl,
+ amount = details.amount,
+ showTosImmediately = false,
+ uri = uri,
+ possibleExchanges = details.possibleExchanges,
+ )
}
}
@@ -132,9 +209,10 @@ class WithdrawManager(
amount: Amount,
showTosImmediately: Boolean = false,
uri: String? = null,
+ possibleExchanges: List<ExchangeItem> = emptyList(),
) = scope.launch {
withdrawStatus.value = WithdrawStatus.Loading(uri)
- api.request("getWithdrawalDetailsForAmount", WithdrawalDetails.serializer()) {
+ api.request("getWithdrawalDetailsForAmount", ManualWithdrawalDetails.serializer()) {
put("exchangeBaseUrl", exchangeBaseUrl)
put("amount", amount.toJSONString())
}.onError { error ->
@@ -146,16 +224,34 @@ class WithdrawManager(
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
+ withdrawalAccountList = details.withdrawalAccountsList,
+ ageRestrictionOptions = details.ageRestrictionOptions,
+ possibleExchanges = possibleExchanges,
)
- } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri)
+ } else getExchangeTos(exchangeBaseUrl, details, showTosImmediately, uri, possibleExchanges)
}
}
+ @WorkerThread
+ suspend fun prepareManualWithdrawal(uri: String): WithdrawExchangeResponse? {
+ withdrawStatus.postValue(WithdrawStatus.Loading(uri))
+ var response: WithdrawExchangeResponse? = null
+ api.request("prepareWithdrawExchange", WithdrawExchangeResponse.serializer()) {
+ put("talerUri", uri)
+ }.onError {
+ handleError("prepareWithdrawExchange", it)
+ }.onSuccess {
+ response = it
+ }
+ return response
+ }
+
private fun getExchangeTos(
exchangeBaseUrl: String,
- details: WithdrawalDetails,
+ details: ManualWithdrawalDetails,
showImmediately: Boolean,
uri: String?,
+ possibleExchanges: List<ExchangeItem>,
) = scope.launch {
api.request("getExchangeTos", TosResponse.serializer()) {
put("exchangeBaseUrl", exchangeBaseUrl)
@@ -167,9 +263,12 @@ class WithdrawManager(
exchangeBaseUrl = exchangeBaseUrl,
amountRaw = details.amountRaw,
amountEffective = details.amountEffective,
- tosText = it.tos,
+ withdrawalAccountList = details.withdrawalAccountsList,
+ ageRestrictionOptions = details.ageRestrictionOptions,
+ tosText = it.content,
tosEtag = it.currentEtag,
showImmediately = showImmediately.toEvent(),
+ possibleExchanges = possibleExchanges,
)
}
}
@@ -190,38 +289,126 @@ class WithdrawManager(
exchangeBaseUrl = s.exchangeBaseUrl,
amountRaw = s.amountRaw,
amountEffective = s.amountEffective,
+ withdrawalAccountList = s.withdrawalAccountList,
+ ageRestrictionOptions = s.ageRestrictionOptions,
+ possibleExchanges = s.possibleExchanges,
)
}
}
@UiThread
- fun acceptWithdrawal() = scope.launch {
+ fun acceptWithdrawal(restrictAge: Int? = null) = scope.launch {
val status = withdrawStatus.value as ReceivedDetails
- val operation = if (status.talerWithdrawUri == null) {
- "acceptManualWithdrawal"
+ withdrawStatus.value = WithdrawStatus.Withdrawing
+ if (status.talerWithdrawUri == null) {
+ acceptManualWithdrawal(status, restrictAge)
} else {
- "acceptBankIntegratedWithdrawal"
+ acceptBankIntegratedWithdrawal(status, restrictAge)
}
- withdrawStatus.value = WithdrawStatus.Withdrawing
+ }
- api.request<Unit>(operation) {
+ private suspend fun acceptBankIntegratedWithdrawal(
+ status: ReceivedDetails,
+ restrictAge: Int? = null,
+ ) {
+ api.request("acceptBankIntegratedWithdrawal", AcceptWithdrawalResponse.serializer()) {
+ restrictAge?.let { put("restrictAge", restrictAge) }
put("exchangeBaseUrl", status.exchangeBaseUrl)
- if (status.talerWithdrawUri == null) {
- put("amount", status.amountRaw.toJSONString())
- } else {
- put("talerWithdrawUri", status.talerWithdrawUri)
- }
+ put("talerWithdrawUri", status.talerWithdrawUri)
}.onError {
- handleError(operation, it)
+ handleError("acceptBankIntegratedWithdrawal", it)
}.onSuccess {
- withdrawStatus.value = WithdrawStatus.Success(status.amountRaw.currency)
+ withdrawStatus.value =
+ WithdrawStatus.Success(status.amountRaw.currency, it.transactionId)
+ }
+ }
+
+ private suspend fun acceptManualWithdrawal(status: ReceivedDetails, restrictAge: Int? = null) {
+ api.request("acceptManualWithdrawal", AcceptManualWithdrawalResponse.serializer()) {
+ restrictAge?.let { put("restrictAge", restrictAge) }
+ put("exchangeBaseUrl", status.exchangeBaseUrl)
+ put("amount", status.amountRaw.toJSONString())
+ }.onError {
+ handleError("acceptManualWithdrawal", it)
+ }.onSuccess { response ->
+ withdrawStatus.value = createManualTransferRequired(
+ status = status,
+ response = response,
+ )
}
}
- @UiThread
private fun handleError(operation: String, error: TalerErrorInfo) {
Log.e(TAG, "Error $operation $error")
- withdrawStatus.value = WithdrawStatus.Error(error.userFacingMsg)
+ withdrawStatus.postValue(WithdrawStatus.Error(error.userFacingMsg))
+ }
+
+ /**
+ * A hack to be able to view bank details for manual withdrawal with the same logic.
+ * Don't call this from ongoing withdrawal processes as it destroys state.
+ */
+ fun viewManualWithdrawal(status: WithdrawStatus.ManualTransferRequired) {
+ require(status.transactionId != null) { "No transaction ID given" }
+ withdrawStatus.value = status
}
}
+
+fun createManualTransferRequired(
+ transactionId: String,
+ exchangeBaseUrl: String,
+ amountRaw: Amount,
+ amountEffective: Amount,
+ withdrawalAccountList: List<WithdrawalExchangeAccountDetails>,
+) = WithdrawStatus.ManualTransferRequired(
+ transactionId = transactionId,
+ transactionAmountRaw = amountRaw,
+ transactionAmountEffective = amountEffective,
+ exchangeBaseUrl = exchangeBaseUrl,
+ withdrawalTransfers = withdrawalAccountList.mapNotNull {
+ val uri = Uri.parse(it.paytoUri.replace("receiver-name=", "receiver_name="))
+ if ("bitcoin".equals(uri.authority, true)) {
+ val msg = uri.getQueryParameter("message").orEmpty()
+ val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg)
+ val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
+ val segwitAddresses = Bech32.generateFakeSegwitAddress(reserve, uri.pathSegments.first())
+ TransferData.Bitcoin(
+ account = uri.lastPathSegment!!,
+ segwitAddresses = segwitAddresses,
+ subject = reserve,
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString())
+ )
+ } else if (uri.authority.equals("x-taler-bank", true)) {
+ TransferData.Taler(
+ account = uri.lastPathSegment!!,
+ receiverName = uri.getQueryParameter("receiver_name"),
+ subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString()),
+ )
+ } else if (uri.authority.equals("iban", true)) {
+ TransferData.IBAN(
+ iban = uri.lastPathSegment!!,
+ receiverName = uri.getQueryParameter("receiver_name"),
+ subject = uri.getQueryParameter("message") ?: "Error: No message in URI",
+ amountRaw = amountRaw,
+ amountEffective = amountEffective,
+ withdrawalAccount = it.copy(paytoUri = uri.toString()),
+ )
+ } else null
+ },
+)
+
+fun createManualTransferRequired(
+ status: ReceivedDetails,
+ response: AcceptManualWithdrawalResponse,
+): WithdrawStatus.ManualTransferRequired = createManualTransferRequired(
+ transactionId = response.transactionId,
+ exchangeBaseUrl = status.exchangeBaseUrl,
+ amountRaw = status.amountRaw,
+ amountEffective = status.amountEffective,
+ withdrawalAccountList = response.withdrawalAccountsList,
+) \ No newline at end of file