commit 794b483c5004cffe21039b29061033e6fc485af4
parent 9cc426ecf7ca3e6378d8ee131298627a18bd6500
Author: Joel-Haeberli <haebu@rubigen.ch>
Date: Thu, 30 May 2024 11:18:36 +0200
fix: fractions in amount
Diffstat:
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