cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

commit 5d804199aaaf1308e818c3b9ca49139eedb42da4
parent 477d3f5f949db3e57fd8aa8dd59c766cb94ddcfb
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Mon, 29 Apr 2024 23:09:46 +0200

chore: save commit

Diffstat:
Mc2ec/api-terminals.go | 2++
Mc2ec/api-wire-gateway.go | 6++++--
Mc2ec/c2ec-config.conf | 6++++++
Mc2ec/c2ec-config.yaml | 1+
Mc2ec/config.go | 9+++++++--
Mc2ec/install/installation_notes.md | 29+++++++++++++++++++++++++++--
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt | 46++++++++++++++++++++++++++++------------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalWithdrawalSetup.kt | 1+
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt | 21++++++++++++++++++++-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt | 5++++-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt | 4++--
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 60++++++++++++++++++++++++++++++++++++++----------------------
12 files changed, 140 insertions(+), 50 deletions(-)

diff --git a/c2ec/api-terminals.go b/c2ec/api-terminals.go @@ -16,6 +16,7 @@ type TerminalConfig struct { Name string `json:"name"` Version string `json:"version"` ProviderName string `json:"provider_name"` + Currency string `json:"currency"` WireType string `json:"wire_type"` } @@ -58,6 +59,7 @@ func handleTerminalConfig(res http.ResponseWriter, req *http.Request) { Name: "taler-terminal", Version: "0:0:0", ProviderName: p.Name, + Currency: CONFIG.Server.Currency, WireType: p.PaytoTargetType, }) if err != nil { diff --git a/c2ec/api-wire-gateway.go b/c2ec/api-wire-gateway.go @@ -128,8 +128,10 @@ func NewOutgoingBankTransaction(tr *Transfer) *OutgoingBankTransaction { func wireGatewayConfig(res http.ResponseWriter, req *http.Request) { cfg := WireConfig{ - Name: "taler-wire-gateway", - Version: "0:0:1", + Name: "taler-wire-gateway", + Currency: CONFIG.Server.Currency, + Version: "0:0:1", + Implementation: "", } serializedCfg, err := NewJsonCodec[WireConfig]().EncodeToBytes(&cfg) diff --git a/c2ec/c2ec-config.conf b/c2ec/c2ec-config.conf @@ -30,6 +30,12 @@ FAIL_ON_MISSING_ATTESTORS = false # backend. EXCHANGE_ACCOUNT = payto://iban/CH50030202099498 +# The currency supported by this C2EC instance +# The terminals must accept payments in this currency +# and the Exchange creating the reserve must create +# reserves with the specified currency. +CURRENCY = CHF + # How many retries shall be triggered, when the attestation # of a transaction fails MAX_RETRIES = 3 diff --git a/c2ec/c2ec-config.yaml b/c2ec/c2ec-config.yaml @@ -7,6 +7,7 @@ c2ec: unix-path-mode: 660 fail-on-missing-attestors: false # forced if prod=true credit-account: "payto://iban/CH50030202099498" # this account must be specified at the providers backends as well + currency: "CHF" max-retries: 3 retry-delay-ms: 1000 wire-gateway: diff --git a/c2ec/config.go b/c2ec/config.go @@ -24,6 +24,7 @@ type C2ECServerConfig struct { UnixPathMode int `yaml:"unix-path-mode"` StrictAttestors bool `yaml:"fail-on-missing-attestors"` CreditAccount string `yaml:"credit-account"` + Currency string `yaml:"currency"` MaxRetries int32 `yaml:"max-retries"` RetryDelayMs int `yaml:"retry-delay-ms"` WireGateway C2ECWireGatewayConfig `yaml:"wire-gateway"` @@ -159,7 +160,6 @@ func ParseIni(content []byte) (*C2ECConfig, error) { if err != nil { return nil, err } - cfg.Server.StrictAttestors, err = value.Bool() if err != nil { return nil, err @@ -169,9 +169,14 @@ func ParseIni(content []byte) (*C2ECConfig, error) { if err != nil { return nil, err } - cfg.Server.CreditAccount = value.String() + value, err = s.GetKey("CURRENCY") + if err != nil { + return nil, err + } + cfg.Server.Currency = value.String() + value, err = s.GetKey("MAX_RETRIES") if err != nil { return nil, err diff --git a/c2ec/install/installation_notes.md b/c2ec/install/installation_notes.md @@ -30,4 +30,29 @@ To allow the withdrawal of Taler, I will need following binaries: `sudo apt install taler-exchange` -## Configure -\ No newline at end of file +## Configure + + +### Preparing the database + +`sudo passwd postgres` + +`su postgres` + +`psql` + +SQL `CREATE DATABASE c2ec;` + +SQL `CREATE USER c2ec_admin WITH ENCRYPTED PASSWORD [..]` -> keepass + +SQL `GRANT ALL PRIVILEGES ON DATABASE c2ec TO c2ec_admin;` + +`` + +For CLI (managing terminals and providers): +SQL `CREATE USER c2ec_operator WITH ENCRYPTED PASSWORD [..]` -> keepass + + + +For the API (handling withdrawals): +SQL `CREATE USER c2ec_api WITH ENCRYPTED PASSWORD [..]` -> keepass +\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt @@ -14,6 +14,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.util.Optional @@ -34,10 +35,6 @@ class TerminalClient( private fun baseUrlBuilder() = HttpUrl.Builder() .encodedPath(config.terminalApiBaseUrl) - private fun exchangeConfigUrl() = baseUrlBuilder() - .addPathSegment("config") - .build() - private fun terminalsConfigUrl() = baseUrlBuilder() .addPathSegment("config") .build() @@ -46,24 +43,24 @@ class TerminalClient( .addPathSegment("withdrawals") .build() - private fun withdrawalByWopid(encodedWopid: String) = baseUrlBuilder() + private fun withdrawalsByWopid(encodedWopid: String) = baseUrlBuilder() .addPathSegment("withdrawals") .addPathSegment(encodedWopid) .build() private fun <T> serializer(clazz: Class<T>) = Moshi.Builder().build().adapter(clazz) - private fun withdrawalConfirm(encodedWopid: String) = withdrawalByWopid(encodedWopid) + private fun withdrawalsConfirm(encodedWopid: String) = withdrawalsByWopid(encodedWopid) .newBuilder() .addPathSegment("check") .build() - private fun withdrawalAbort(encodedWopid: String) = withdrawalByWopid(encodedWopid) + + private fun withdrawalsAbort(encodedWopid: String) = withdrawalsByWopid(encodedWopid) .newBuilder() .addPathSegment("abort") .build() - fun retrieveTerminalApiConfig(): Optional<TerminalApiConfig> { - + fun terminalsConfig(): Optional<TerminalApiConfig> { val req = Request.Builder() .get() .url(terminalsConfigUrl()) @@ -74,10 +71,9 @@ class TerminalClient( fun setupWithdrawal(setupReq: TerminalWithdrawalSetup): Optional<TerminalWithdrawalSetupResponse> { - val reqBody = RequestBody.create( - "application/json".toMediaType(), - serializer(TerminalWithdrawalSetup::class.java).toJson(setupReq) - ) + val reqBody = serializer(TerminalWithdrawalSetup::class.java) + .toJson(setupReq) + .toRequestBody("application/json".toMediaType()) val req = Request.Builder() .post(reqBody) .url(setupWithdrawalUrl()) @@ -94,7 +90,7 @@ class TerminalClient( val req = Request.Builder() .get() - .url(withdrawalByWopid(wopid) + .url(withdrawalsByWopid(wopid) .newBuilder() .addQueryParameter("long_poll_ms", longPollMs.toString()) .addQueryParameter("old_state", oldState.value) @@ -105,13 +101,27 @@ class TerminalClient( return parseOrEmpty(response) } - fun sendConfirmationRequest(confirmationRequest: TerminalWithdrawalConfirmationRequest) { + fun sendConfirmationRequest(encodedWopid: String, confirmationRequest: TerminalWithdrawalConfirmationRequest) { - println("sending payment notification...") + val reqBody = serializer(TerminalWithdrawalConfirmationRequest::class.java) + .toJson(confirmationRequest) + .toRequestBody("application/json".toMediaType()) + val req = Request.Builder() + .post(reqBody) + .url(withdrawalsConfirm(encodedWopid)) + .build() + val response = client.newCall(req).execute() + if (response.code != 204) { + abortWithdrawal(encodedWopid) + } } - fun abortWithdrawal(wopid: String) { - println("aborting withdrawal") + fun abortWithdrawal(encodedWopid: String) { + val req = Request.Builder() + .delete() + .url(withdrawalsAbort(encodedWopid)) + .build() + client.newCall(req).execute() } private inline fun <reified T: Any> parseOrEmpty(response: Response): Optional<T> { diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalWithdrawalSetup.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalWithdrawalSetup.kt @@ -6,6 +6,7 @@ data class TerminalApiConfig( val name: String, val version: String, val providerName: String, + val currency: String, val wireType: String ) diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt @@ -1,10 +1,15 @@ package ch.bfh.habej2.wallee_c2ec.client.wallee import ch.bfh.habej2.wallee_c2ec.withdrawal.WithdrawalActivity +import ch.bfh.habej2.wallee_c2ec.withdrawal.WithdrawalViewModel import com.wallee.android.till.sdk.ResponseHandler +import com.wallee.android.till.sdk.data.TransactionCompletionResponse import com.wallee.android.till.sdk.data.TransactionResponse -class WalleeResponseHandler(private val activity: WithdrawalActivity) : ResponseHandler() { +class WalleeResponseHandler( + private val activity: WithdrawalActivity, + private val model: WithdrawalViewModel +) : ResponseHandler() { override fun authorizeTransactionReply(response: TransactionResponse?) { @@ -15,9 +20,23 @@ class WalleeResponseHandler(private val activity: WithdrawalActivity) : Response return } + response.transaction.metaData.forEach{ + println("${it.key}=${it.value}") + } response.transaction.metaData.get("id") } + override fun completeTransactionReply(response: TransactionCompletionResponse?) { + + if (response == null) { + model.withdrawalOperationFailed() + activity.finish() + return + } + + model.updateWalleeTransaction(response) + } + override fun checkApiServiceCompatibilityReply( isCompatible: Boolean?, apiServiceVersion: String? diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt @@ -29,7 +29,10 @@ fun ExchangeSelectionScreen( val ctx = LocalContext.current Button(onClick = { - model.exchangeUpdated(TalerTerminalConfig("","","","")) + model.exchangeUpdated( + activity, + TalerTerminalConfig("","","","") + ) onNavigateToWithdrawal() }) { Text(text = "withdraw") diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt @@ -16,13 +16,13 @@ class WithdrawalActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - walleeClient = ApiClient(WalleeResponseHandler(this)) + val model = WithdrawalViewModel() + walleeClient = ApiClient(WalleeResponseHandler(this, model)) walleeClient.bind(this) walleeClient.checkApiServiceCompatibility() setContent { - val model = WithdrawalViewModel() val navController = rememberNavController() NavHost(navController = navController, startDestination = "chooseExchangeScreen") { diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt @@ -1,25 +1,24 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal +import android.app.Activity import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ch.bfh.habej2.wallee_c2ec.client.taler.TerminalClient import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig -import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.Base32Encode import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmationRequest import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup +import com.wallee.android.till.sdk.data.State import com.wallee.android.till.sdk.data.TransactionCompletionResponse import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import java.io.Closeable import java.math.BigDecimal -import java.security.SecureRandom import java.util.Optional import java.util.UUID @@ -59,10 +58,16 @@ class WithdrawalViewModel( private val _uiState = MutableStateFlow(MutableWithdrawalOperationState()) val uiState: StateFlow<WithdrawalOperationState> = _uiState - fun exchangeUpdated(cfg: TalerTerminalConfig) { + fun exchangeUpdated(activity: Activity, cfg: TalerTerminalConfig) { + terminalClient = TerminalClient(cfg) _uiState.value = MutableWithdrawalOperationState() // reset withdrawal operation - updateCurrency("CHF") + val optionalApiCfg = terminalClient!!.terminalsConfig() + if (!optionalApiCfg.isPresent) { + println("no config") + activity.finish() + } + updateCurrency(optionalApiCfg.get().currency) } fun setupWithdrawal() { @@ -84,12 +89,21 @@ class WithdrawalViewModel( _uiState.value.amount = parseAmount(amount).orElse(null) } - fun updateCurrency(currency: String) { + private fun updateCurrency(currency: String) { _uiState.value.currency = currency } fun updateWalleeTransaction(completion: TransactionCompletionResponse) { _uiState.value.transaction = completion + + if (completion.state == State.FAILED) { + withdrawalOperationFailed() + } + + completion.transactionCompletion.lineItems[0].id + completion.state.name + + confirmationRequest() } fun startAuthorizationWhenReadyOrAbort( @@ -97,29 +111,29 @@ class WithdrawalViewModel( onFailure: () -> Unit ) { - return - viewModelScope.launch { - onSuccess() // TODO -// val result = bankIntegrationClient!!.retrieveWithdrawalStatus(uiState.value.encodedWopid, 30000) -// if (result.isPresent) { -// onSuccess() -// } else { -// withdrawalOperationFailed() -// onFailure() -// } + val result = terminalClient!!.retrieveWithdrawalStatus(uiState.value.encodedWopid, 30000) + if (result.isPresent) { + onSuccess() + } else { + withdrawalOperationFailed() + onFailure() + } } } - fun withdrawalOperationFailed() { - viewModelScope.launch { - terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) + fun confirmationRequest() { + viewModelScope.launch{ + terminalClient!!.sendConfirmationRequest( + _uiState.value.encodedWopid, + TerminalWithdrawalConfirmationRequest("", Amount(0,0)) + ) } } - fun confirmPayment() { - viewModelScope.launch{ - terminalClient!!.sendConfirmationRequest(TerminalWithdrawalConfirmationRequest("", Amount(0,0))) + fun withdrawalOperationFailed() { + viewModelScope.launch { + terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) } } @@ -141,6 +155,7 @@ class WithdrawalViewModel( val frac = fracStr.toInt() Optional.of(Amount(value, frac)) } catch (ex: NumberFormatException) { + println(ex.message) Optional.empty() } } @@ -149,6 +164,7 @@ class WithdrawalViewModel( val value = inp.toInt() Optional.of(Amount(value, 0)) } catch (ex: NumberFormatException) { + println(ex.message) Optional.empty() } }