cashless2ecash

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

commit 8055aaf68b52b5d4fd2578152e6f07f0b0e102b3
parent 2eeaf9f1c11caa407bcfd474e3fc61521e591f36
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Tue, 21 May 2024 22:59:32 +0200

feat: exponential backoff for retries

Diffstat:
Mc2ec/amount.go | 20++++++++++++++++++--
Mc2ec/amount_test.go | 22++++++++++++++++++++++
Mc2ec/proc-attestor.go | 14++++++++++++++
Mc2ec/proc-retrier.go | 10++++++++--
Mc2ec/provider.go | 1+
Mc2ec/simulation-client.go | 5+++++
Mc2ec/wallee-client.go | 26+++++++++++++++++++++++---
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt | 15++++++++++-----
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt | 2+-
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt | 10++++------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 13++++++++++---
11 files changed, 116 insertions(+), 22 deletions(-)

diff --git a/c2ec/amount.go b/c2ec/amount.go @@ -165,13 +165,15 @@ func ParseAmount(s string) (*Amount, error) { return nil, fmt.Errorf("invalid amount: %s", s) } - fraction := 0 + fraction := 0.0 if len(valueAndFraction) == 2 { - fraction, err = strconv.Atoi(valueAndFraction[1]) + divider := leadingZerosDivider(valueAndFraction[1]) + fractionInt, err := strconv.Atoi(valueAndFraction[1]) if err != nil { LogError("amount", err) return nil, fmt.Errorf("invalid amount: %s", s) } + fraction = float64(fractionInt) / float64(divider) } a := NewAmount(currency, uint64(value), uint64(fraction)) @@ -194,3 +196,17 @@ func (a *Amount) String() string { } return fmt.Sprintf("%s:%s", a.Currency, v) } + +func leadingZerosDivider(s string) int { + + rs := strings.Split(s, "") + leadingZeros := 0 + for _, r := range rs { + if r == "0" { + leadingZeros += 1 + } else { + break + } + } + return int(math.Pow10(leadingZeros)) +} diff --git a/c2ec/amount_test.go b/c2ec/amount_test.go @@ -194,3 +194,25 @@ func TestFormatAmountInvalid(t *testing.T) { } } } + +func TestParseFloat(t *testing.T) { + + floats := []float64{ + 0.5, + 0.3, + 0.0004, + 1234.9923, + } + + for _, f := range floats { + + formatted := fmt.Sprintf("CHF:%.8f", f) + fmt.Println("formatted:", formatted) + amount, err := parseAmount(formatted) + if err != nil { + fmt.Println("failed!", err) + } + + fmt.Println(FormatAmount(&amount)) + } +} diff --git a/c2ec/proc-attestor.go b/c2ec/proc-attestor.go @@ -86,6 +86,20 @@ func finaliseOrSetRetry( return } + if w, err := DB.GetWithdrawalById(withdrawalRowId); err != nil { + LogError("proc-attestor", err) + errs <- err + prepareRetryOrAbort(withdrawalRowId, errs) + return + } else { + if err := transaction.Attest(w); err != nil { + LogError("proc-attestor", err) + errs <- err + prepareRetryOrAbort(withdrawalRowId, errs) + return + } + } + completionProof := transaction.Bytes() if len(completionProof) > 0 { // only allow finalization operation, when the completion diff --git a/c2ec/proc-retrier.go b/c2ec/proc-retrier.go @@ -22,7 +22,7 @@ func RunRetrier(ctx context.Context, errs chan error) { go func() { for { - time.Sleep(time.Duration(CONFIG.Server.RetryDelayMs) * time.Millisecond) + time.Sleep(time.Duration(1000 * time.Millisecond)) //LogInfo("proc-retrier", "attesting selected withdrawals...") withdrawals, err := DB.GetAttestableWithdrawals() if err != nil { @@ -31,7 +31,13 @@ func RunRetrier(ctx context.Context, errs chan error) { continue } for _, w := range withdrawals { - attest(w, errs) + var lastRetryTs int64 = 0 + if w.LastRetryTs != nil { + lastRetryTs = *w.LastRetryTs + } + if ShouldStartRetry(time.Unix(lastRetryTs, 0), int(w.RetryCounter), CONFIG.Server.RetryDelayMs) { + attest(w, errs) + } } } }() diff --git a/c2ec/provider.go b/c2ec/provider.go @@ -3,6 +3,7 @@ package main type ProviderTransaction interface { AllowWithdrawal() bool AbortWithdrawal() bool + Attest(w *Withdrawal) error Bytes() []byte } diff --git a/c2ec/simulation-client.go b/c2ec/simulation-client.go @@ -32,6 +32,11 @@ func (st *SimulationTransaction) AbortWithdrawal() bool { return false } +func (st *SimulationTransaction) Attest(w *Withdrawal) error { + + return nil +} + func (st *SimulationTransaction) Bytes() []byte { return bytes.NewBufferString("this is a simulated transaction and therefore has no content.").Bytes() diff --git a/c2ec/wallee-client.go b/c2ec/wallee-client.go @@ -54,10 +54,26 @@ func (wt *WalleeTransaction) AbortWithdrawal() bool { strings.EqualFold(string(wt.State), string(StateDecline)) } -// func (wt *WalleeTransaction) FormatPayto() string { +func (wt *WalleeTransaction) Attest(w *Withdrawal) error { -// return fmt.Sprintf("payto://wallee-transaction/%d", wt.Id) -// } + amount, err := parseAmount(fmt.Sprintf("%s:%f", CONFIG.Server.Currency, wt.CompletedAmount)) + if err != nil { + LogError("wallee-client", err) + return err + } + + if amount.Value != uint64(w.Amount.Val) /*|| amount.Fraction != uint64(w.Amount.Frac)*/ { + + return errors.New("amount does not match the withdrawal") + } + + if wt.MerchantReference != *w.ProviderTransactionId { + + return errors.New("the merchant reference does not match the withdrawal") + } + + return nil +} func (wt *WalleeTransaction) Bytes() []byte { @@ -97,6 +113,10 @@ func (w *WalleeClient) SetupClient(p *Provider) error { func (w *WalleeClient) GetTransaction(transactionId string) (ProviderTransaction, error) { + if transactionId == "" { + return nil, errors.New("transaction id must be specified but was blank") + } + call := fmt.Sprintf("%s%s", w.baseUrl, WALLEE_SEARCH_TRANSACTION_API) queryParams := map[string]string{ WALLEE_API_SPACEID_PARAM_NAME: strconv.Itoa(w.credentials.SpaceId), diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt @@ -95,12 +95,17 @@ class TerminalClientImplementation ( .post(reqBody) .url(url) .build() - client.newCall(req).execute().use { - if (it.isSuccessful) { - callback(parseOrEmpty(it)) - } else { - callback(Optional.empty()) + try { + client.newCall(req).execute().use { + if (it.isSuccessful) { + callback(parseOrEmpty(it)) + } else { + callback(Optional.empty()) + } } + } catch (ex: Exception) { + println("exception occured: $ex") + callback(Optional.empty()) } } diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt @@ -44,7 +44,7 @@ fun AuthorizePaymentScreen(model: WithdrawalViewModel, activity: Activity, clien val transaction = Transaction.Builder(withdrawalAmount) .setCurrency(Currency.getInstance(uiState.currency)) .setInvoiceReference(uiState.encodedWopid) - .setMerchantReference(uiState.transactionId) + .setMerchantReference(uiState.encodedWopid) .setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY) .build() diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import kotlinx.coroutines.launch +import java.util.concurrent.Executors import kotlin.coroutines.coroutineContext @SuppressLint("StateFlowValueCalledInComposition") @@ -66,12 +67,9 @@ fun RegisterParametersScreen( Text(text = "simulate registration") } } else { - val s = rememberCoroutineScope() - LaunchedEffect(key1 = uiState) { - s.launch { - model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { - activity.finish() - } + Executors.newSingleThreadExecutor().execute { + model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { + activity.finish() } } } 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 @@ -237,7 +237,12 @@ class WithdrawalViewModel( signals.doOnError(onFailure) println("registered callbacks for parameter selection") println("executing parameter selection long-polling request") - recursiveRetries(uiState.value.encodedWopid) + try { + recursiveRetries(uiState.value.encodedWopid) + } catch (ex: Exception) { + println("error occured: $ex") + signals.setError() + } println("awaiting response for parameter selection") signals.block() } @@ -247,7 +252,7 @@ class WithdrawalViewModel( terminalClient!!.sendConfirmationRequest( _uiState.value.encodedWopid, TerminalWithdrawalConfirmationRequest( - _uiState.value.transactionId, + _uiState.value.encodedWopid, Amount(0, 0) ) ) { @@ -283,11 +288,13 @@ class WithdrawalViewModel( fun withdrawalOperationFailed() { viewModelScope.launch { - terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) {} + if (_uiState.value.transactionState == TransactionState.AUTHORIZATION_PENDING) { + terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) {} // SummaryActivity.summary.success = false // val activity = WithdrawalActivity() // val intent = Intent(activity, SummaryActivity::class.java) // activity.startActivity(intent) + } } }