cashless2ecash

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

commit 794b483c5004cffe21039b29061033e6fc485af4
parent 9cc426ecf7ca3e6378d8ee131298627a18bd6500
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Thu, 30 May 2024 11:18:36 +0200

fix: fractions in amount

Diffstat:
Mc2ec/amount.go | 61++++++++++++++++++++++++++++++++++++++-----------------------
Mc2ec/amount_test.go | 34++++++++++++++++++++--------------
Mc2ec/api-bank-integration.go | 2+-
Mc2ec/api-terminals.go | 4++--
Mc2ec/api-wire-gateway.go | 8++++----
Mc2ec/c2ec-config.conf | 2+-
6 files changed, 66 insertions(+), 45 deletions(-)

diff --git a/c2ec/amount.go b/c2ec/amount.go @@ -1,5 +1,6 @@ // This file is part of taler-go, the Taler Go implementation. // Copyright (C) 2022 Martin Schanzenbach +// Copyright (C) 2024 Joel Häberli // // Taler Go is free software: you can redistribute it and/or modify it // under the terms of the GNU Affero General Public License as published @@ -55,7 +56,7 @@ func ToAmount(amount *TalerAmountCurrency) (*Amount, error) { return a, nil } -func FormatAmount(amount *Amount) string { +func FormatAmount(amount *Amount, fractionalDigits int) string { if amount == nil { return "" @@ -69,7 +70,8 @@ func FormatAmount(amount *Amount) string { return fmt.Sprintf("%s:%d", amount.Currency, amount.Value) } - return fmt.Sprintf("%s:%d.%d", amount.Currency, amount.Value, amount.Fraction) + fractionStr := toFractionStr(int(amount.Fraction), fractionalDigits) + return fmt.Sprintf("%s:%d.%s", amount.Currency, amount.Value, fractionStr) } // The maximim length of a fraction (in digits) @@ -90,6 +92,31 @@ func NewAmount(currency string, value uint64, fraction uint64) Amount { } } +func toFractionStr(frac int, fractionalDigits int) string { + + if fractionalDigits > 8 { + return "" + } + + leadingZerosStr := "" + strLengthTens := int(math.Pow10(fractionalDigits - 1)) + strLength := int(math.Log10(float64(strLengthTens))) + leadingZeros := 0 + if strLengthTens > frac { + for i := 0; i < strLength; i++ { + if strLengthTens > frac { + leadingZeros++ + strLengthTens = strLengthTens / 10 + } + } + for i := 0; i < leadingZeros; i++ { + leadingZerosStr += "0" + } + } + + return leadingZerosStr + strconv.Itoa(frac) +} + // Subtract the amount b from a and return the result. // a and b must be of the same currency and a >= b func (a *Amount) Sub(b Amount) (*Amount, error) { @@ -139,7 +166,7 @@ func (a *Amount) Add(b Amount) (*Amount, error) { } // Parses an amount string in the format <currency>:<value>[.<fraction>] -func ParseAmount(s string) (*Amount, error) { +func ParseAmount(s string, fractionDigits int) (*Amount, error) { if s == "" { return &Amount{"", 0, 0}, nil @@ -157,23 +184,25 @@ func ParseAmount(s string) (*Amount, error) { currency := currencyAndAmount[0] valueAndFraction := strings.Split(currencyAndAmount[1], ".") if len(valueAndFraction) < 1 && len(valueAndFraction) > 2 { - return nil, fmt.Errorf("invalid amount: %s", s) + return nil, fmt.Errorf("invalid value and fraction part in amount %s", s) } value, err := strconv.Atoi(valueAndFraction[0]) if err != nil { LogError("amount", err) - return nil, fmt.Errorf("invalid amount: %s", s) + return nil, fmt.Errorf("invalid value in amount %s", s) } - fraction := 0.0 + fraction := 0 if len(valueAndFraction) == 2 { - divider := leadingZerosDivider(valueAndFraction[1]) + if len(valueAndFraction[1]) > fractionDigits { + return nil, fmt.Errorf("invalid amount: %s expected at max %d fractional digits", s, fractionDigits) + } fractionInt, err := strconv.Atoi(valueAndFraction[1]) if err != nil { LogError("amount", err) - return nil, fmt.Errorf("invalid amount: %s", s) + return nil, fmt.Errorf("invalid fraction in amount %s", s) } - fraction = float64(fractionInt) / float64(divider) + fraction = fractionInt } a := NewAmount(currency, uint64(value), uint64(fraction)) @@ -196,17 +225,3 @@ 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 @@ -60,7 +60,7 @@ func TestAmountSub(t *testing.T) { } func TestAmountLarge(t *testing.T) { - x, err := ParseAmount("EUR:50") + x, err := ParseAmount("EUR:50", 2) if err != nil { fmt.Println(err) t.Errorf("Failed") @@ -78,12 +78,12 @@ func TestParseValid(t *testing.T) { "CHF:30", "EUR:20.34", "CHF:23.99", - "CHF:50.3500000", + "CHF:50.350", "USD:109992332", } for _, a := range amnts { - _, err := ParseAmount(a) + _, err := ParseAmount(a, 2) if err != nil { fmt.Println("failed!", a) t.FailNow() @@ -97,13 +97,15 @@ func TestParseInvalid(t *testing.T) { "CHF", "EUR:.34", "CHF:23.", + "EUR:452:001", "USD:1099928583593859583332", + "CHF:4564:005", } for _, a := range amnts { - _, err := ParseAmount(a) + _, err := ParseAmount(a, 2) if err == nil { - fmt.Println("failed!", a) + fmt.Println("failed! (expected error)", a) t.FailNow() } } @@ -116,11 +118,13 @@ func TestFormatAmountValid(t *testing.T) { "EUR:20.34", "CHF:23.99", "USD:109992332", + "CHF:20.05", + "USD:109992332.01", "", } amntsParsed := make([]Amount, 0) for _, a := range amnts { - a, err := ParseAmount(a) + a, err := ParseAmount(a, 2) if err != nil { fmt.Println("failed!", err) t.FailNow() @@ -130,13 +134,13 @@ func TestFormatAmountValid(t *testing.T) { amntsFormatted := make([]string, 0) for _, a := range amntsParsed { - amntsFormatted = append(amntsFormatted, FormatAmount(&a)) + amntsFormatted = append(amntsFormatted, FormatAmount(&a, 2)) } for i, frmtd := range amntsFormatted { fmt.Println(frmtd) - expectation, err1 := ParseAmount(amnts[i]) - reality, err2 := ParseAmount(frmtd) + expectation, err1 := ParseAmount(amnts[i], 2) + reality, err2 := ParseAmount(frmtd, 2) if err1 != nil || err2 != nil { fmt.Println("failed!", err1, err2) t.FailNow() @@ -149,6 +153,8 @@ func TestFormatAmountValid(t *testing.T) { fmt.Println("failed!", amnts[i], frmtd) t.FailNow() } + + fmt.Println("success!", amnts[i], frmtd) } } @@ -163,7 +169,7 @@ func TestFormatAmountInvalid(t *testing.T) { } amntsParsed := make([]Amount, 0) for _, a := range amnts { - a, err := ParseAmount(a) + a, err := ParseAmount(a, 2) if err != nil { fmt.Println("failed!", err) t.FailNow() @@ -173,13 +179,13 @@ func TestFormatAmountInvalid(t *testing.T) { amntsFormatted := make([]string, 0) for _, a := range amntsParsed { - amntsFormatted = append(amntsFormatted, FormatAmount(&a)) + amntsFormatted = append(amntsFormatted, FormatAmount(&a, 2)) } for i, frmtd := range amntsFormatted { fmt.Println(frmtd) - expectation, err1 := ParseAmount(amnts[i]) - reality, err2 := ParseAmount(frmtd) + expectation, err1 := ParseAmount(amnts[i], 2) + reality, err2 := ParseAmount(frmtd, 2) if err1 != nil || err2 != nil { fmt.Println("failed!", err1, err2) t.FailNow() @@ -213,6 +219,6 @@ func TestParseFloat(t *testing.T) { fmt.Println("failed!", err) } - fmt.Println(FormatAmount(&amount)) + fmt.Println(FormatAmount(&amount, 2)) } } diff --git a/c2ec/api-bank-integration.go b/c2ec/api-bank-integration.go @@ -372,7 +372,7 @@ func formatWithdrawalOrErrorStatus(w *Withdrawal) ([]byte, int) { } else { withdrawalStatusBytes, err := NewJsonCodec[BankWithdrawalOperationStatus]().EncodeToBytes(&BankWithdrawalOperationStatus{ Status: w.WithdrawalStatus, - Amount: FormatAmount(amount), + Amount: FormatAmount(amount, CONFIG.Server.CurrencyFractionDigits), SenderWire: fmt.Sprintf("payto://%s/%d", operator.PaytoTargetType, w.ProviderTransactionId), WireTypes: []string{operator.PaytoTargetType}, ReservePubKey: EddsaPublicKey((encodeCrock(w.ReservePubKey))), diff --git a/c2ec/api-terminals.go b/c2ec/api-terminals.go @@ -225,7 +225,7 @@ func handleWithdrawalCheck(res http.ResponseWriter, req *http.Request) { return } - trmlFees, err := ParseAmount(paymentNotification.TerminalFees) + trmlFees, err := ParseAmount(paymentNotification.TerminalFees, CONFIG.Server.CurrencyFractionDigits) if err != nil { LogError("terminals-api", err) setLastResponseCodeForLogger(HTTP_BAD_REQUEST) @@ -277,7 +277,7 @@ func handleWithdrawalAbortTerminal(res http.ResponseWriter, req *http.Request) { func parseAmount(amountStr string) (Amount, error) { - a, err := ParseAmount(amountStr) + a, err := ParseAmount(amountStr, CONFIG.Server.CurrencyFractionDigits) if err != nil { return Amount{"", 0, 0}, err } diff --git a/c2ec/api-wire-gateway.go b/c2ec/api-wire-gateway.go @@ -97,7 +97,7 @@ func NewIncomingReserveTransaction(w *Withdrawal) *IncomingReserveTransaction { LogError("wire-gateway", err) return nil } - t.Amount = FormatAmount(a) + t.Amount = FormatAmount(a, CONFIG.Server.CurrencyFractionDigits) t.Date = Timestamp{ Ts: int(w.RegistrationTs), } @@ -115,7 +115,7 @@ func NewOutgoingBankTransaction(tr *Transfer) *OutgoingBankTransaction { LogError("wire-gateway", err) return nil } - t.Amount = FormatAmount(a) + t.Amount = FormatAmount(a, CONFIG.Server.CurrencyFractionDigits) t.Date = Timestamp{ Ts: int(tr.TransferTs), } @@ -207,7 +207,7 @@ func transfer(res http.ResponseWriter, req *http.Request) { if t == nil { // no transfer for this request_id -> generate new - amount, err := ParseAmount(transfer.Amount) + amount, err := ParseAmount(transfer.Amount, CONFIG.Server.CurrencyFractionDigits) if err != nil { LogError("wire-gateway-api", err) setLastResponseCodeForLogger(HTTP_INTERNAL_SERVER_ERROR) @@ -236,7 +236,7 @@ func transfer(res http.ResponseWriter, req *http.Request) { res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) return } - if transfer.Amount != FormatAmount(ta) || + if transfer.Amount != FormatAmount(ta, CONFIG.Server.CurrencyFractionDigits) || transfer.ExchangeBaseUrl != t.ExchangeBaseUrl || transfer.Wtid != ShortHashCode(t.Wtid) || transfer.CreditAccount != t.CreditAccount { diff --git a/c2ec/c2ec-config.conf b/c2ec/c2ec-config.conf @@ -27,7 +27,7 @@ FAIL_ON_MISSING_ATTESTORS = false # The account where the exchange receives payments # of the providers. Must be the same, in the providers -# backend. +# backend. EXCHANGE_ACCOUNT = payto://iban/CH50030202099498 # The currency supported by this C2EC instance