cashless2ecash

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

commit 82a397b5ad89183b87394f16148bf143428ea853
parent ffec58b70e4712ac416575b1c3139ee31eb5658f
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Wed,  6 Mar 2024 21:16:23 +0100

db: database schema

Diffstat:
Mdata/nonce2ecash_schema.sql | 5+----
Anonce2ecash/go.mod | 3+++
Anonce2ecash/pkg/common/amount.go | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anonce2ecash/pkg/common/amount_test.go | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anonce2ecash/pkg/common/model.go | 16++++++++++++++++
Anonce2ecash/pkg/handler.go | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anonce2ecash/pkg/main.go | 16++++++++++++++++
Anonce2ecash/pkg/model.go | 21+++++++++++++++++++++
Anonce2ecash/pkg/taler-bank-integration/client.go | 21+++++++++++++++++++++
Anonce2ecash/pkg/taler-bank-integration/model.go | 31+++++++++++++++++++++++++++++++
Anonce2ecash/pkg/taler-wirewatch-gateway/client.go | 1+
Anonce2ecash/pkg/taler-wirewatch-gateway/model.go | 1+
Mspecs/api-nonce2ecash.rst | 2+-
13 files changed, 393 insertions(+), 5 deletions(-)

diff --git a/data/nonce2ecash_schema.sql b/data/nonce2ecash_schema.sql @@ -1,6 +1,3 @@ - --- java -jar schemaspy-6.2.4.jar -t pgsql11 -dp ./ -hq -host localhost -port 5432 -db postgres -u local -p local -o ./nonce2ecash-erd - DROP TABLE IF EXISTS withdrawal; DROP TABLE IF EXISTS terminal; DROP TABLE IF EXISTS terminal_provider; @@ -9,7 +6,7 @@ DROP TYPE WITHDRAWAL_OPERATION_STATUS; CREATE TYPE WITHDRAWAL_OPERATION_STATUS AS ENUM ( 'pending', 'selected', - 'aborted', + 'aborted', 'confirmed' ); diff --git a/nonce2ecash/go.mod b/nonce2ecash/go.mod @@ -0,0 +1,3 @@ +module nonce2ecash + +go 1.22.0 diff --git a/nonce2ecash/pkg/common/amount.go b/nonce2ecash/pkg/common/amount.go @@ -0,0 +1,153 @@ +// This file is part of taler-go, the Taler Go implementation. +// Copyright (C) 2022 Martin Schanzenbach +// +// 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 +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// Taler Go is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package common + +import ( + "errors" + "fmt" + "math" + "regexp" + "strconv" + "strings" +) + +// The GNU Taler Amount object +type Amount struct { + + // The type of currency, e.g. EUR + Currency string + + // The value (before the ".") + Value uint64 + + // The fraction (after the ".", optional) + Fraction uint64 +} + +// The maximim length of a fraction (in digits) +const FractionalLength = 8 + +// The base of the fraction. +const FractionalBase = 1e8 + +// The maximum value +var MaxAmountValue = uint64(math.Pow(2, 52)) + +// Create a new amount from value and fraction in a currency +func NewAmount(currency string, value uint64, fraction uint64) Amount { + return Amount{ + Currency: currency, + Value: value, + Fraction: fraction, + } +} + +// 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) { + if a.Currency != b.Currency { + return nil, errors.New("Currency mismatch!") + } + v := a.Value + f := a.Fraction + if a.Fraction < b.Fraction { + v -= 1 + f += FractionalBase + } + f -= b.Fraction + if v < b.Value { + return nil, errors.New("Amount Overflow!") + } + v -= b.Value + r := Amount{ + Currency: a.Currency, + Value: v, + Fraction: f, + } + return &r, nil +} + +// Add b to a and return the result. +// Returns an error if the currencies do not match or the addition would +// cause an overflow of the value +func (a *Amount) Add(b Amount) (*Amount, error) { + if a.Currency != b.Currency { + return nil, errors.New("Currency mismatch!") + } + v := a.Value + + b.Value + + uint64(math.Floor((float64(a.Fraction)+float64(b.Fraction))/FractionalBase)) + + if v >= MaxAmountValue { + return nil, errors.New(fmt.Sprintf("Amount Overflow (%d > %d)!", v, MaxAmountValue)) + } + f := uint64((a.Fraction + b.Fraction) % FractionalBase) + r := Amount{ + Currency: a.Currency, + Value: v, + Fraction: f, + } + return &r, nil +} + +// Parses an amount string in the format <currency>:<value>[.<fraction>] +func ParseAmount(s string) (*Amount, error) { + re, err := regexp.Compile(`^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$`) + parsed := re.FindStringSubmatch(s) + + if nil != err { + return nil, errors.New(fmt.Sprintf("invalid amount: %s", s)) + } + tail := "0.0" + if len(parsed) >= 4 { + tail = "0." + parsed[3] + } + if len(tail) > FractionalLength+1 { + return nil, errors.New("fraction too long") + } + value, err := strconv.ParseUint(parsed[2], 10, 64) + if nil != err { + return nil, errors.New(fmt.Sprintf("Unable to parse value %s", parsed[2])) + } + fractionF, err := strconv.ParseFloat(tail, 64) + if nil != err { + return nil, errors.New(fmt.Sprintf("Unable to parse fraction %s", tail)) + } + fraction := uint64(math.Round(fractionF * FractionalBase)) + currency := parsed[1] + a := NewAmount(currency, value, fraction) + return &a, nil +} + +// Check if this amount is zero +func (a *Amount) IsZero() bool { + return (a.Value == 0) && (a.Fraction == 0) +} + +// Returns the string representation of the amount: <currency>:<value>[.<fraction>] +// Omits trailing zeroes. +func (a *Amount) String() string { + v := strconv.FormatUint(a.Value, 10) + if a.Fraction != 0 { + f := strconv.FormatUint(a.Fraction, 10) + f = strings.TrimRight(f, "0") + v = fmt.Sprintf("%s.%s", v, f) + } + return fmt.Sprintf("%s:%s", a.Currency, v) +} diff --git a/nonce2ecash/pkg/common/amount_test.go b/nonce2ecash/pkg/common/amount_test.go @@ -0,0 +1,69 @@ +// This file is part of taler-go, the Taler Go implementation. +// Copyright (C) 2022 Martin Schanzenbach +// +// 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 +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// Taler Go is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package common + +import ( + "fmt" + "testing" +) + +var a = Amount{ + Currency: "EUR", + Value: 1, + Fraction: 50000000, +} +var b = Amount{ + Currency: "EUR", + Value: 23, + Fraction: 70007000, +} +var c = Amount{ + Currency: "EUR", + Value: 25, + Fraction: 20007000, +} + +func TestAmountAdd(t *testing.T) { + d, err := a.Add(b) + if err != nil { + t.Errorf("Failed adding amount") + } + if c.String() != d.String() { + t.Errorf("Failed to add to correct amount") + } +} + +func TestAmountSub(t *testing.T) { + d, err := c.Sub(b) + if err != nil { + t.Errorf("Failed substracting amount") + } + if a.String() != d.String() { + t.Errorf("Failed to substract to correct amount") + } +} + +func TestAmountLarge(t *testing.T) { + x, err := ParseAmount("EUR:50") + _, err = x.Add(a) + if nil != err { + fmt.Println(err) + t.Errorf("Failed") + } +} diff --git a/nonce2ecash/pkg/common/model.go b/nonce2ecash/pkg/common/model.go @@ -0,0 +1,16 @@ +package common + +// https://docs.taler.net/core/api-common.html#hash-codes +type WithdrawalIdentifier [32]byte + +// https://docs.taler.net/core/api-common.html#cryptographic-primitives +type EddsaPublicKey [32]byte + +type WithdrawalOperationStatus int + +const ( + PENDING WithdrawalOperationStatus = iota + SELECTED + ABORTED + CONFIRMED +) diff --git a/nonce2ecash/pkg/handler.go b/nonce2ecash/pkg/handler.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + http "net/http" + "strings" +) + +const HTTP_OK = 200 +const HTTP_NO_CONTENT = 204 +const HTTP_METHOD_NOT_ALLOWED = 405 + +func config(writer http.ResponseWriter, req *http.Request) { + + if isGet(req) { + + writer.Write([]byte("{\n\"\":\"\",\n\"\":\"\"\n}")) + writer.WriteHeader(HTTP_OK) + } + + methodNotAllowed(writer) +} + +func withdrawalOperationDispatcher(writer http.ResponseWriter, req *http.Request) { + + fmt.Printf("req.URL.Fragment: %v\n", req.URL.Fragment) + fmt.Printf("req.URL.RawPath: %v\n", req.URL.RawPath) + fmt.Printf("req.URL.Path: %v\n", req.URL.Path) + + if strings.Compare("POST", strings.ToUpper(req.Method)) != 0 { + + writer.WriteHeader(405) + return + } + +} + +func handleWithdrawalRegistration(writer http.ResponseWriter, req *http.Request) { + +} + +func handlePaymentNotification(writer http.ResponseWriter, req *http.Request) { + +} + +func isGet(req *http.Request) bool { + + return strings.EqualFold("GET", strings.ToUpper(req.Method)) +} + +func isPost(req *http.Request) bool { + + return strings.EqualFold("POST", strings.ToUpper(req.Method)) +} + +func methodNotAllowed(writer http.ResponseWriter) { + + writer.WriteHeader(405) +} diff --git a/nonce2ecash/pkg/main.go b/nonce2ecash/pkg/main.go @@ -0,0 +1,16 @@ +package main + +import ( + http "net/http" +) + +const CONFIG_ENDPOINT = "/config" +const WITHDRAWAL_OPERATION = "/withdrawal-operation" + +func main() { + + http.HandleFunc(CONFIG_ENDPOINT, config) + http.HandleFunc(WITHDRAWAL_OPERATION, withdrawalOperationDispatcher) + + http.ListenAndServe(":8080", nil) +} diff --git a/nonce2ecash/pkg/model.go b/nonce2ecash/pkg/model.go @@ -0,0 +1,21 @@ +package main + +import "nonce2ecash/pkg/common" + +type Nonce2ecashConfig struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type WithdrawRegistration struct { + WithdrawalId common.WithdrawalIdentifier `json:"withdrawal_id"` + ReservePubKey common.EddsaPublicKey `json:"reserve_pub_key"` + Amount common.Amount `json:"amount"` + ProviderId uint64 `json:"provider_id"` +} + +type PaymentNotification struct { + ProviderTransactionId string `json:"provider_transaction_id"` + Amount common.Amount `json:"amount"` + Fees common.Amount `json:"fees"` +} diff --git a/nonce2ecash/pkg/taler-bank-integration/client.go b/nonce2ecash/pkg/taler-bank-integration/client.go @@ -0,0 +1,21 @@ +package talerbankintegration + +import ( + "errors" + "nonce2ecash/pkg/common" +) + +// check status of withdrawal +func withdrawalOperationStatus(id common.WithdrawalIdentifier) (*BankWithdrawalOperationStatus, error) { + return nil, errors.New("not implemented yet") +} + +// send parameters for reserve to exchange core. +func withdrawalOperationCreate(reservePubKey common.EddsaPublicKey, exchangeBaseUrl string) (*BankWithdrawalOperationPostResponse, error) { + return nil, errors.New("not implemented yet") +} + +// abort withdrawal +func withdrawalOperationAbort(id common.WithdrawalIdentifier) error { + return errors.New("not implemented yet") +} diff --git a/nonce2ecash/pkg/taler-bank-integration/model.go b/nonce2ecash/pkg/taler-bank-integration/model.go @@ -0,0 +1,31 @@ +package talerbankintegration + +import "nonce2ecash/pkg/common" + +// https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationPostRequest +type BankWithdrawalOperationPostRequest struct { + ReservePub string `json:"reserve_pub"` + SelectedExchange string `json:"selected_exchange"` +} + +// https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationPostResponse +type BankWithdrawalOperationPostResponse struct { + Status common.WithdrawalOperationStatus `json:"status"` + ConfirmTransferUrl string `json:"confirm_transfer_url"` + TransferDone bool `json:"transfer_done"` +} + +// https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationStatus +type BankWithdrawalOperationStatus struct { + Status common.WithdrawalOperationStatus `json:"status"` + Amount common.Amount `json:"amount"` + SenderWire string `json:"sender_wire"` + SuggestedExchange string `json:"suggested_exchange"` + ConfirmTransferUrl string `json:"confirm_transfer_url"` + WireTypes []string `json:"wire_types"` + SelectedReservePub string `json:"selected_reserve_pub"` + SelectedExchangeAccount string `json:"selected_exchange_account"` + Aborted bool `json:"aborted"` + SelectionDone bool `json:"selection_done"` + TransferDone bool `json:"transfer_done"` +} diff --git a/nonce2ecash/pkg/taler-wirewatch-gateway/client.go b/nonce2ecash/pkg/taler-wirewatch-gateway/client.go @@ -0,0 +1 @@ +package talerwirewatchgateway diff --git a/nonce2ecash/pkg/taler-wirewatch-gateway/model.go b/nonce2ecash/pkg/taler-wirewatch-gateway/model.go @@ -0,0 +1 @@ +package talerwirewatchgateway diff --git a/specs/api-nonce2ecash.rst b/specs/api-nonce2ecash.rst @@ -82,7 +82,7 @@ operation (the ``WITHDRAWAL_ID``) to interact with the withdrawal operation and amount?: Amount; // Id of the provider requesting a withdrawal by nonce. - providerId: SafeUint64; + provider_id: SafeUint64; } **Response:**