cashless2ecash

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

commit 0c79ae99713e6d4d6d31bd621a949617ebfe503a
parent 10d064835e8ed272a9ff09bf74ac371e19e2675b
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Sat, 23 Mar 2024 14:33:16 +0100

code: impl routing

Diffstat:
Ac2ec/auth.go | 22++++++++++++++++++++++
Ac2ec/bank-integration.go | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/c2ec-config.yaml | 10++++++++++
Ac2ec/common/amount.go | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rnonce2ecash/pkg/common/amount_test.go -> c2ec/common/amount_test.go | 0
Rnonce2ecash/pkg/common/codec.go -> c2ec/common/codec.go | 0
Ac2ec/common/codec_test.go | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/common/http-util.go | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/common/http-util_test.go | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/common/model.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/config.go | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/db.go | 48++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/go.mod | 21+++++++++++++++++++++
Ac2ec/go.sum | 35+++++++++++++++++++++++++++++++++++
Ac2ec/main.go | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/postgres.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Ac2ec/wire-gateway.go | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adata/c2ec_schema.sql | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddata/nonce2ecash_schema.sql | 107-------------------------------------------------------------------------------
Mdocs/content/architecture/c2ec.tex | 16++++++++++------
Mdocs/thesis.pdf | 0
Dnonce2ecash/go.mod | 18------------------
Dnonce2ecash/go.sum | 29-----------------------------
Dnonce2ecash/pkg/common/amount.go | 153-------------------------------------------------------------------------------
Dnonce2ecash/pkg/common/codec_test.go | 62--------------------------------------------------------------
Dnonce2ecash/pkg/common/http-util.go | 198-------------------------------------------------------------------------------
Dnonce2ecash/pkg/common/http-util_test.go | 62--------------------------------------------------------------
Dnonce2ecash/pkg/common/model.go | 31-------------------------------
Dnonce2ecash/pkg/db/db.go | 66------------------------------------------------------------------
Dnonce2ecash/pkg/db/provider.go | 23-----------------------
Dnonce2ecash/pkg/db/withdrawal.go | 29-----------------------------
Dnonce2ecash/pkg/handler.go | 70----------------------------------------------------------------------
Dnonce2ecash/pkg/main.go | 33---------------------------------
Dnonce2ecash/pkg/model.go | 25-------------------------
Dnonce2ecash/pkg/pkg | 0
Dnonce2ecash/pkg/taler-bank-integration/client.go | 163-------------------------------------------------------------------------------
Dnonce2ecash/pkg/taler-bank-integration/model.go | 50--------------------------------------------------
Dnonce2ecash/pkg/taler-wirewatch-gateway/client.go | 226-------------------------------------------------------------------------------
Dnonce2ecash/pkg/taler-wirewatch-gateway/model.go | 93-------------------------------------------------------------------------------
Dspecs/api-c2ec.rst | 181-------------------------------------------------------------------------------
40 files changed, 1448 insertions(+), 1625 deletions(-)

diff --git a/c2ec/auth.go b/c2ec/auth.go @@ -0,0 +1,22 @@ +package main + +import ( + "net/http" +) + +const AUTHORIZATION_HEADER = "Authorization" +const BEARER_TOKEN_PREFIX = "Bearer" + +func isAllowed(req *http.Request) bool { + + return true + + // auth := req.Header.Get(AUTHORIZATION_HEADER) + // token, found := strings.CutPrefix(auth, AUTHORIZATION_HEADER) + // if !found { + // // invalid token prefix + // return false + // } + + // return strings.EqualFold(token, "") +} diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go @@ -0,0 +1,182 @@ +package main + +import ( + "bytes" + "c2ec/common" + "log" + http "net/http" +) + +const BANK_INTEGRATION_CONFIG_ENDPOINT = "/config" +const WITHDRAWAL_OPERATION = "/withdrawal-operation" + +const WOPID_PARAMETER = "wopid" +const BANK_INTEGRATION_CONFIG_PATTERN = BANK_INTEGRATION_CONFIG_ENDPOINT +const WITHDRAWAL_OPERATION_PATTERN = WITHDRAWAL_OPERATION +const WITHDRAWAL_OPERATION_BY_WOPID_PATTERN = WITHDRAWAL_OPERATION + "/{" + WOPID_PARAMETER + "}" +const WITHDRAWAL_OPERATION_ABORTION_PATTERN = WITHDRAWAL_OPERATION_BY_WOPID_PATTERN + "/abort" + +// https://docs.taler.net/core/api-exchange.html#tsref-type-CurrencySpecification +type CurrencySpecification struct { + Name string `json:"name"` + Currency string `json:"currency"` + NumFractionalInputDigits int `json:"num_fractional_input_digits"` + NumFractionalNormalDigits int `json:"num_fractional_normal_digits"` + NumFractionalTrailingZeroDigits int `json:"num_fractional_trailing_zero_digits"` + AltUnitNames string `json:"alt_unit_names"` +} + +// https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankIntegrationConfig +type BankIntegrationConfig struct { + Name string `json:"name"` + Version string `json:"version"` + Implementation string `json:"implementation"` + Currency string `json:"currency"` + CurrencySpecification CurrencySpecification `json:"currency_specification"` +} + +type C2ECWithdrawRegistration struct { + Wopid common.WithdrawalIdentifier `json:"wopid"` + ReservePubKey common.EddsaPublicKey `json:"reserve_pub_key"` + Amount common.Amount `json:"amount"` + ProviderId uint64 `json:"provider_id"` +} + +type C2ECWithdrawalStatus struct { + Status common.WithdrawalOperationStatus `json:"status"` + Amount common.Amount `json:"amount"` + SenderWire string `json:"sender_wire"` + WireTypes []string `json:"wire_types"` + ReservePubKey common.EddsaPublicKey `json:"selected_reserve_pub"` +} + +type C2ECPaymentNotification struct { + ProviderTransactionId string `json:"provider_transaction_id"` + Amount common.Amount `json:"amount"` + Fees common.Amount `json:"fees"` +} + +func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) { + + cfg := BankIntegrationConfig{ + Name: "taler-bank-integration", + Version: "0:0:1", + } + + serializedCfg, err := common.NewJsonCodec[BankIntegrationConfig]().EncodeToBytes(&cfg) + if err != nil { + log.Default().Printf("failed serializing config: %s", err.Error()) + res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR) + return + } + + res.WriteHeader(common.HTTP_OK) + res.Write(serializedCfg) +} + +func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) { + + jsonCodec := common.NewJsonCodec[C2ECWithdrawRegistration]() + registration, err := common.ReadStructFromBody[C2ECWithdrawRegistration](req, jsonCodec) + if err != nil { + + err := common.WriteProblem(res, common.HTTP_BAD_REQUEST, &common.RFC9457Problem{ + TypeUri: common.TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_INVALID_REQ", + Title: "invalid request", + Detail: "the registration request for the withdrawal is malformed (error: " + err.Error() + ")", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + err = DB.RegisterWithdrawal(registration) + if err != nil { + + err := common.WriteProblem(res, common.HTTP_INTERNAL_SERVER_ERROR, &common.RFC9457Problem{ + TypeUri: common.TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_DB_FAILURE", + Title: "databse failure", + Detail: "the registration of the withdrawal failed due to db failure (error:" + err.Error() + ")", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + res.WriteHeader(common.HTTP_NO_CONTENT) +} + +// Get status of withdrawal associated with the given WOPID +// +// Parameters: +// - long_poll_ms (optional): +// milliseconds to wait for state to change +// given old_state until responding +// - old_state (optional): +// Default is 'pending' +// - terminal_provider_id (optional): +// The terminal provider requesting for status update. +func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { + + wopid := req.PathValue(WOPID_PARAMETER) + if wopid == "" { + res.WriteHeader(common.HTTP_BAD_REQUEST) + return + } + + res.WriteHeader(common.HTTP_OK) + res.Write(bytes.NewBufferString("retrieved withdrawal status request for wopid=" + wopid).Bytes()) +} + +func handlePaymentNotification(res http.ResponseWriter, req *http.Request) { + + wopid := req.PathValue(WOPID_PARAMETER) + if wopid == "" { + res.WriteHeader(common.HTTP_BAD_REQUEST) + return + } + + res.WriteHeader(common.HTTP_OK) + res.Write(bytes.NewBufferString("retrieved payment notification for wopid=" + wopid).Bytes()) +} + +func handleWithdrawalAbort(res http.ResponseWriter, req *http.Request) { + + res.WriteHeader(common.HTTP_OK) + res.Write(bytes.NewBufferString("retrieved withdrawal operation abortion request").Bytes()) +} + +// ---------------------- +// OFFICIAL MODELS +// ---------------------- +// 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/c2ec/c2ec-config.yaml b/c2ec/c2ec-config.yaml @@ -0,0 +1,10 @@ +c2ec: + host: "localhost" + port: 8081 + unix-domain-socket: true + unix-socket-path: "c2ec.sock" +db: + host: "localhost" + port: 5432 + username: "user" + password: "password" diff --git a/c2ec/common/amount.go b/c2ec/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 `json:"currency"` + + // The value (before the ".") + Value uint64 `json:"value"` + + // The fraction (after the ".", optional) + Fraction uint64 `json:"fraction"` +} + +// 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/c2ec/common/amount_test.go diff --git a/nonce2ecash/pkg/common/codec.go b/c2ec/common/codec.go diff --git a/c2ec/common/codec_test.go b/c2ec/common/codec_test.go @@ -0,0 +1,63 @@ +package common_test + +import ( + "bytes" + "c2ec/common" + "fmt" + + "testing" + + "gotest.tools/v3/assert" +) + +func TestJsonCodecRoundTrip(t *testing.T) { + + type TestStruct struct { + A string + B int + C []string + D byte + E []byte + F *TestStruct + } + + testObj := TestStruct{ + "TestA", + 1, + []string{"first", "second"}, + 'A', + []byte{0xdf, 0x01, 0x34}, + &TestStruct{ + "TestAA", + 2, + []string{"third", "fourth", "fifth"}, + 'B', + []byte{0xdf, 0x01, 0x34}, + nil, + }, + } + + jsonCodec := new(common.JsonCodec[TestStruct]) + + encodedTestObj, err := jsonCodec.Encode(&testObj) + if err != nil { + fmt.Println("error happened while encoding test obj", err.Error()) + t.FailNow() + } + + encodedTestObjBytes := make([]byte, 200) + _, err = encodedTestObj.Read(encodedTestObjBytes) + if err != nil { + fmt.Println("error happened while encoding test obj to byte array", err.Error()) + t.FailNow() + } + + encodedTestObjReader := bytes.NewReader(encodedTestObjBytes) + decodedTestObj, err := jsonCodec.Decode(encodedTestObjReader) + if err != nil { + fmt.Println("error happened while encoding test obj to byte array", err.Error()) + t.FailNow() + } + + assert.DeepEqual(t, &testObj, decodedTestObj) +} diff --git a/c2ec/common/http-util.go b/c2ec/common/http-util.go @@ -0,0 +1,256 @@ +package common + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strings" +) + +const HTTP_OK = 200 +const HTTP_NO_CONTENT = 204 +const HTTP_BAD_REQUEST = 400 +const HTTP_UNAUTHORIZED = 401 +const HTTP_NOT_FOUND = 404 +const HTTP_METHOD_NOT_ALLOWED = 405 +const HTTP_CONFLICT = 409 +const HTTP_INTERNAL_SERVER_ERROR = 500 + +const TALER_URI_PROBLEM_PREFIX = "taler://problem" + +type RFC9457Problem struct { + TypeUri string `json:"type"` + Title string `json:"title"` + Detail string `json:"detail"` + Instance string `json:"instance"` +} + +// Writes a problem as specified by RFC 9457 to +// the response. The problem is always serialized +// as JSON. +func WriteProblem(res http.ResponseWriter, status int, problem *RFC9457Problem) error { + + c := NewJsonCodec[RFC9457Problem]() + problm, err := c.EncodeToBytes(problem) + if err != nil { + return err + } + + res.Write(problm) + res.WriteHeader(status) + return nil +} + +// Reads a generic argument struct from the requests +// body. It takes the codec as argument which is used to +// decode the struct from the request. If an error occurs +// nil and the error are returned. +func ReadStructFromBody[T any](req *http.Request, codec Codec[T]) (*T, error) { + + bodyBytes, err := ReadBody(req) + if err != nil { + return nil, err + } + + return codec.Decode(bytes.NewReader(bodyBytes)) +} + +// Reads the body of a request into a byte array. +// If the body is empty, an empty array is returned. +// If an error occurs while reading the body, nil and +// the respective error is returned. +func ReadBody(req *http.Request) ([]byte, error) { + + if req.ContentLength < 0 { + return make([]byte, 0), nil + } + + body := make([]byte, req.ContentLength) + _, err := req.Body.Read(body) + if err != nil { + return nil, err + } + return body, nil +} + +// execute a GET request and parse body or retrieve error +func HttpGet2[T any]( + req string, + codec Codec[T], +) (*T, int, error) { + + return HttpGet( + req, + nil, + nil, + codec, + ) +} + +// execute a GET request and parse body or retrieve error +// path- and query-parameters can be set to add query and path parameters +func HttpGet[T any]( + req string, + pathParams map[string]string, + queryParams map[string]string, + codec Codec[T], +) (*T, int, error) { + + url := formatUrl(req, pathParams, queryParams) + fmt.Println("GET:", url) + + res, err := http.Get(url) + if err != nil { + return nil, -1, err + } + + if codec == nil { + return nil, res.StatusCode, err + } else { + resBody, err := codec.Decode(res.Body) + return resBody, res.StatusCode, err + } +} + +// execute a POST request and parse response or retrieve error +func HttpPost2[T any, R any]( + req string, + body *T, + requestCodec Codec[T], + responseCodec Codec[R], +) (*R, int, error) { + + return HttpPost( + req, + nil, + nil, + body, + requestCodec, + responseCodec, + ) +} + +// execute a POST request and parse response or retrieve error +// path- and query-parameters can be set to add query and path parameters +func HttpPost[T any, R any]( + req string, + pathParams map[string]string, + queryParams map[string]string, + body *T, + requestCodec Codec[T], + responseCodec Codec[R], +) (*R, int, error) { + + url := formatUrl(req, pathParams, queryParams) + fmt.Println("POST:", url) + + var res *http.Response + if body == nil { + if requestCodec == nil { + res, err := http.Post( + url, + "", + nil, + ) + + if err != nil { + return nil, -1, err + } + + return nil, res.StatusCode, nil + } else { + return nil, -1, errors.New("invalid arguments - body was not present but codec was defined") + } + } else { + if requestCodec == nil { + return nil, -1, errors.New("invalid arguments - body was present but no codec was defined") + } else { + + encodedBody, err := requestCodec.Encode(body) + if err != nil { + return nil, -1, err + } + + res, err = http.Post( + url, + requestCodec.HttpApplicationContentHeader(), + encodedBody, + ) + + if err != nil { + return nil, -1, err + } + } + } + + if responseCodec == nil { + return nil, res.StatusCode, nil + } + + resBody, err := responseCodec.Decode(res.Body) + if err != nil { + return nil, -1, err + } + + return resBody, res.StatusCode, err +} + +// builds request URL containing the path and query +// parameters of the respective parameter map. +func formatUrl( + req string, + pathParams map[string]string, + queryParams map[string]string, +) string { + + return setUrlQuery(setUrlPath(req, pathParams), queryParams) +} + +// Sets the parameters which are part of the url. +// The function expects each parameter in the path to be prefixed +// using a ':'. The function handles url as follows: +// +// /some/:param/tobereplaced -> ':param' will be replaced with value. +// +// For replacements, the pathParams map must be supplied. The map contains +// the name of the parameter with the value mapped to it. +// The names MUST not contain the prefix ':'! +func setUrlPath( + req string, + pathParams map[string]string, +) string { + + if pathParams == nil || len(pathParams) < 1 { + return req + } + + var url = req + for k, v := range pathParams { + + if !strings.HasPrefix(k, "/") { + // prevent scheme postfix replacements + url = strings.Replace(url, ":"+k, v, 1) + } + } + return url +} + +func setUrlQuery( + req string, + queryParams map[string]string, +) string { + + if queryParams == nil || len(queryParams) < 1 { + return req + } + + var url = req + "?" + for k, v := range queryParams { + + url = strings.Join([]string{url, k, "=", v, "&"}, "") + } + + url, _ = strings.CutSuffix(url, "&") + return url +} diff --git a/c2ec/common/http-util_test.go b/c2ec/common/http-util_test.go @@ -0,0 +1,62 @@ +package common_test + +import ( + "c2ec/common" + "fmt" + "testing" +) + +const URL_GET = "https://jsonplaceholder.typicode.com/todos/:id" +const URL_POST = "https://jsonplaceholder.typicode.com/posts" + +type TestStruct struct { + UserId int `json:"userId"` + Id int `json:"id"` + Title string `json:"title"` + Completed bool `json:"completed"` +} + +func TestGET(t *testing.T) { + + res, status, err := common.HttpGet( + URL_GET, + map[string]string{ + "id": "1", + }, + map[string]string{}, + common.NewJsonCodec[TestStruct](), + ) + + if err != nil { + t.Errorf("%s", err.Error()) + t.FailNow() + } + + fmt.Println("res:", res, ", status:", status) +} + +func TestPOST(t *testing.T) { + + res, status, err := common.HttpPost( + URL_POST, + map[string]string{ + "id": "1", + }, + map[string]string{}, + &TestStruct{ + UserId: 1, + Id: 1, + Title: "TEST", + Completed: false, + }, + common.NewJsonCodec[TestStruct](), + common.NewJsonCodec[TestStruct](), + ) + + if err != nil { + t.Errorf("%s", err.Error()) + t.FailNow() + } + + fmt.Println("res:", res, ", status:", status) +} diff --git a/c2ec/common/model.go b/c2ec/common/model.go @@ -0,0 +1,72 @@ +package common + +// https://docs.taler.net/core/api-common.html#hash-codes +type WithdrawalIdentifier string + +// https://docs.taler.net/core/api-common.html#cryptographic-primitives +type EddsaPublicKey string + +// https://docs.taler.net/core/api-common.html#hash-codes +type HashCode string + +// https://docs.taler.net/core/api-common.html#hash-codes +type ShortHashCode string + +// https://docs.taler.net/core/api-common.html#timestamps +type Timestamp struct { + Ts int `json:"t_s"` +} + +// https://docs.taler.net/core/api-common.html#wadid +type WadId [6]uint32 + +// according to https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationStatus +type WithdrawalOperationStatus string + +const ( + PENDING WithdrawalOperationStatus = "pending" + SELECTED WithdrawalOperationStatus = "selected" + ABORTED WithdrawalOperationStatus = "aborted" + CONFIRMED WithdrawalOperationStatus = "confirmed" +) + +type ErrorDetail struct { + + // Numeric error code unique to the condition. + // The other arguments are specific to the error value reported here. + Code int `json:"code"` + + // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... + // Should give a human-readable hint about the error's nature. Optional, may change without notice! + Hint string `json:"hint"` + + // Optional detail about the specific input value that failed. May change without notice! + Detail string `json:"detail"` + + // Name of the parameter that was bogus (if applicable). + Parameter string `json:"parameter"` + + // Path to the argument that was bogus (if applicable). + Path string `json:"path"` + + // Offset of the argument that was bogus (if applicable). + Offset string `json:"offset"` + + // Index of the argument that was bogus (if applicable). + Index string `json:"index"` + + // Name of the object that was bogus (if applicable). + Object string `json:"object"` + + // Name of the currency that was problematic (if applicable). + Currency string `json:"currency"` + + // Expected type (if applicable). + TypeExpected string `json:"type_expected"` + + // Type that was provided instead (if applicable). + TypeActual string `json:"type_actual"` + + // Extra information that doesn't fit into the above (if applicable). + Extra []byte `json:"extra"` +} diff --git a/c2ec/config.go b/c2ec/config.go @@ -0,0 +1,76 @@ +package main + +import ( + "os" + "strconv" + "strings" + + "gopkg.in/yaml.v3" +) + +const POSTGRESQL_SCHEME = "postgres://" +const NONCE2ECASH_DATABASE = "nonce2ecash" + +type C2ECConfig struct { + Server C2ECServerConfig `yaml:"c2ec"` + Database C2ECDatabseConfig `yaml:"db"` +} + +type C2ECServerConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + UseUnixDomainSocket bool `yaml:"unix-domain-socket"` + UnixSocketPath string `yaml:"unix-socket-path"` +} + +type C2ECDatabseConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +func Parse(path string) (*C2ECConfig, error) { + + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return nil, err + } + + content := make([]byte, stat.Size()) + _, err = f.Read(content) + if err != nil { + return nil, err + } + + cfg := new(C2ECConfig) + err = yaml.Unmarshal(content, cfg) + if err != nil { + return nil, err + } + + return cfg, nil +} + +func DBConnectionString(cfg *C2ECDatabseConfig) string { + + // format: postgres://username:password@hostname:port/database_name + return strings.Join([]string{ + POSTGRESQL_SCHEME, + cfg.Username, + ":", + cfg.Password, + "@", + cfg.Host, + ":", + strconv.FormatInt(int64(cfg.Port), 10), + "/", + NONCE2ECASH_DATABASE, + }, "") +} diff --git a/c2ec/db.go b/c2ec/db.go @@ -0,0 +1,48 @@ +package main + +import "c2ec/common" + +type TerminalProvider struct { + ProviderTerminalID int64 + Name string + BackendBaseURL string + BackendCredentials string +} + +type Terminal struct { + TerminalID int64 + AccessToken []byte + Active bool + ProviderID int64 + Provider TerminalProvider + Description string +} + +type Withdrawal struct { + WithdrawalId []byte + ReservePubKey []byte + RegistrationTs int64 + Amount TalerAmountCurrency + Fees TalerAmountCurrency + WithdrawalStatus common.WithdrawalOperationStatus + TerminalId int64 + ProviderTransactionId string + LastRetryTs int64 + RetryCounter int32 + CompletionProof []byte +} + +type TalerAmountCurrency struct { + Val int64 + Frac int32 + Curr string +} + +type C2ECDatabase interface { + RegisterWithdrawal(r *C2ECWithdrawRegistration) error + GetWithdrawalByWopid(wopid string) (*Withdrawal, error) + ConfirmPayment(c *C2ECPaymentNotification) error + GetUnconfirmedWithdrawals() ([]*Withdrawal, error) + GetTerminalProviderById(id int) (*TerminalProvider, error) + GetTerminalById(id int) (*Terminal, error) +} diff --git a/c2ec/go.mod b/c2ec/go.mod @@ -0,0 +1,21 @@ +module c2ec + +go 1.22.0 + +require gotest.tools/v3 v3.5.1 + +require ( + github.com/google/go-cmp v0.5.9 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx v3.6.2+incompatible // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/gorm v1.25.7 // indirect +) diff --git a/c2ec/go.sum b/c2ec/go.sum @@ -0,0 +1,35 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= +github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/c2ec/main.go b/c2ec/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "fmt" + "net" + http "net/http" + "os" + "os/signal" + "syscall" +) + +const GET = "GET " +const POST = "POST " + +const BANK_INTEGRATION_API = "/c2ec" +const WIRE_GATEWAY_API = "/wire" + +const DEFAULT_C2EC_CONFIG_PATH = "c2ec-config.yaml" + +var DB C2ECDatabase + +// Starts the c2ec process. +// The program takes following arguments (ordered): +// 1. path to configuration file (.yaml) (optional) +// +// The startup follows these steps: +// 1. load configuration or panic +// 2. setup database or panic +// 3. setup routes for the bank-integration-api +// 4. setup routes for the wire-gateway-api +// 5. listen for incoming requests (as specified in config) +func main() { + + cfgPath := DEFAULT_C2EC_CONFIG_PATH + if len(os.Args) > 1 && os.Args[1] != "" { + cfgPath = os.Args[1] + } + cfg, err := Parse(cfgPath) + if err != nil { + panic("unable to load config: " + err.Error()) + } + + DB, err = setupDatabase() + if err != nil { + panic("unable initialize datatbase: " + err.Error()) + } + + router := http.NewServeMux() + + setupBankIntegrationRoutes(router) + + setupWireGatewayRoutes(router) + + server := http.Server{ + Handler: router, + } + + if cfg.Server.UseUnixDomainSocket { + + socket, err := net.Listen("unix", cfg.Server.UnixSocketPath) + if err != nil { + panic("failed listening on socket: " + err.Error()) + } + + // cleans up socket when process fails and is shutdown. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + os.Remove(cfg.Server.UnixSocketPath) + os.Exit(1) + }() + + if err = server.Serve(socket); err != nil { + panic(err.Error()) + } + } else { + + server.Addr = fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) + if err = server.ListenAndServe(); err != nil { + panic(err.Error()) + } + } +} + +func setupDatabase() (C2ECDatabase, error) { + + return nil, nil +} + +func setupBankIntegrationRoutes(router *http.ServeMux) { + + router.HandleFunc( + GET+BANK_INTEGRATION_API+BANK_INTEGRATION_CONFIG_PATTERN, + bankIntegrationConfig, + ) + + router.HandleFunc( + POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_PATTERN, + handleWithdrawalRegistration, + ) + + router.HandleFunc( + GET+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN, + handleWithdrawalStatus, + ) + + router.HandleFunc( + POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN, + handlePaymentNotification, + ) + + router.HandleFunc( + POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_ABORTION_PATTERN, + handleWithdrawalAbort, + ) +} + +func setupWireGatewayRoutes(router *http.ServeMux) { + + router.HandleFunc( + GET+WIRE_GATEWAY_API+WIRE_GATEWAY_CONFIG_PATTERN, + wireGatewayConfig, + ) + + router.HandleFunc( + POST+WIRE_GATEWAY_API+WIRE_TRANSFER_PATTERN, + transfer, + ) + + router.HandleFunc( + GET+WIRE_GATEWAY_API+WIRE_HISTORY_INCOMING_PATTERN, + historyIncoming, + ) + + router.HandleFunc( + GET+WIRE_GATEWAY_API+WIRE_HISTORY_OUTGOING_PATTERN, + historyOutgoing, + ) + + router.HandleFunc( + POST+WIRE_GATEWAY_API+WIRE_ADMIN_ADD_INCOMING_PATTERN, + adminAddIncoming, + ) +} diff --git a/c2ec/postgres.go b/c2ec/postgres.go @@ -0,0 +1,52 @@ +package main + +import ( + "errors" + "time" + + pgx "github.com/jackc/pgx" +) + +const PS_INSERT_WITHDRAWAL = "INSERT INTO withdrawal " + + "(wopid, reserve_pub_key, registration_ts, amount, terminal_id)" + + " VALUES ($1, $2, $3, $4, $5)" + +const PS_GET_WITHDRAWAL_BY_ID = "" + +// Postgres implementation of the C2ECDatabase +type C2ECPostgres struct { + C2ECDatabase + + ConnPool *pgx.ConnPool +} + +func (db *C2ECPostgres) RegisterWithdrawal(r *C2ECWithdrawRegistration) error { + + ts := time.Now() + res, err := db.ConnPool.Query( + PS_INSERT_WITHDRAWAL, + r.Wopid, + r.ReservePubKey, + ts, + r.Amount, + r.ProviderId, + ) + defer res.Close() + + return err +} + +func (db *C2ECPostgres) GetWithdrawalByWopid(wopid string) (*Withdrawal, error) { + + return nil, errors.New("not yet implemented") +} + +func (db *C2ECPostgres) ConfirmPayment(c *C2ECPaymentNotification) error { + + return errors.New("not yet implemented") +} + +func (db *C2ECPostgres) GetUnconfirmedWithdrawals() ([]*Withdrawal, error) { + + return nil, errors.New("not yet implemented") +} diff --git a/c2ec/wire-gateway.go b/c2ec/wire-gateway.go @@ -0,0 +1,130 @@ +package main + +import ( + "bytes" + "c2ec/common" + "log" + http "net/http" +) + +const WIRE_GATEWAY_CONFIG_ENDPOINT = "/config" +const WIRE_GATEWAY_HISTORY_ENDPOINT = "/history" + +const WIRE_GATEWAY_CONFIG_PATTERN = WIRE_GATEWAY_CONFIG_ENDPOINT +const WIRE_TRANSFER_PATTERN = "/transfer" +const WIRE_HISTORY_INCOMING_PATTERN = WIRE_GATEWAY_HISTORY_ENDPOINT + "/incoming" +const WIRE_HISTORY_OUTGOING_PATTERN = WIRE_GATEWAY_HISTORY_ENDPOINT + "/outgoing" +const WIRE_ADMIN_ADD_INCOMING_PATTERN = "/admin/add-incoming" + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig +type WireConfig struct { + Name string `json:"name"` + Version string `json:"version"` + Currency string `json:"currency"` + Implementation string `json:"implementation"` +} + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest +type TransferRequest struct { + RequestUid common.HashCode `json:"request_uid"` + Amount common.Amount `json:"amount"` + ExchangeBaseUrl string `json:"exchange_base_url"` + Wtid common.ShortHashCode `json:"wtid"` + CreditAccount string `json:"credit_account"` +} + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferResponse +type TransferResponse struct { + Timestamp common.Timestamp `json:"timestamp"` + RowId int `json:"row_id"` +} + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory +type IncomingHistory struct { + IncomingTransactions []IncomingReserveTransaction `json:"incoming_transactions"` + CreditAccount string `json:"credit_account"` +} + +// type RESERVE | https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingReserveTransaction +type IncomingReserveTransaction struct { + Type string `json:"type"` + RowId int `json:"row_id"` + Date common.Timestamp `json:"date"` + Amount common.Amount `json:"amount"` + DebitAccount string `json:"debit_account"` + ReservePub common.EddsaPublicKey `json:"reserve_pub"` +} + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory +type OutgoingHistory struct { + OutgoingBankTransaction []OutgoingBankTransaction `json:"outgoing_bank_transaction"` + DebitAccount string `json:"debit_account"` +} + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction +type OutgoingBankTransaction struct { + RowId int `json:"row_id"` + Date common.Timestamp `json:"date"` + Amount common.Amount `json:"amount"` + CreditAccount string `json:"credit_account"` + Wtid common.ShortHashCode `json:"wtid"` + ExchangeBaseUrl string `json:"exchange_base_url"` +} + +func wireGatewayConfig(res http.ResponseWriter, req *http.Request) { + + cfg := WireConfig{ + Name: "taler-wire-gateway", + Version: "0:0:1", + } + + serializedCfg, err := common.NewJsonCodec[WireConfig]().EncodeToBytes(&cfg) + if err != nil { + log.Default().Printf("failed serializing config: %s", err.Error()) + res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR) + return + } + + res.WriteHeader(common.HTTP_OK) + res.Write(serializedCfg) +} + +func transfer(res http.ResponseWriter, req *http.Request) { + + res.WriteHeader(common.HTTP_OK) + res.Write(bytes.NewBufferString("retrieved transfer request").Bytes()) +} + +func historyIncoming(res http.ResponseWriter, req *http.Request) { + + res.WriteHeader(common.HTTP_OK) + res.Write(bytes.NewBufferString("retrieved history incoming request").Bytes()) +} + +// This method is currently dead and implemented for API conformance +func historyOutgoing(res http.ResponseWriter, req *http.Request) { + + res.WriteHeader(common.HTTP_BAD_REQUEST) +} + +// --------------------- +// TESTING (ONLY ADMINS) +// --------------------- + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest +type AddIncomingRequest struct { + Amount common.Amount `json:"amount"` + ReservcePub common.EddsaPublicKey `json:"reserve_pub"` + DebitAccount string `json:"debit_account"` +} + +// https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse +type AddIncomingResponse struct { + Timestamp common.Timestamp `json:"timestamp"` +} + +// This method is currently dead and implemented for API conformance +func adminAddIncoming(res http.ResponseWriter, req *http.Request) { + + res.WriteHeader(common.HTTP_BAD_REQUEST) +} diff --git a/data/c2ec_schema.sql b/data/c2ec_schema.sql @@ -0,0 +1,111 @@ +-- => proper versioning.sql nehmen (siehe exchange.git), +DROP SCHEMA IF EXISTS c2ec CASCADE; + +CREATE SCHEMA c2ec; +COMMENT ON SCHEMA c2ec + IS 'Schema containing all tables and types related to c2ec (cashless2ecash)'; + +SET search_path TO c2ec; + +CREATE TYPE withdrawal_operation_status AS ENUM ( + 'pending', + 'selected', + 'aborted', + 'confirmed' +); +COMMENT ON TYPE withdrawal_operation_status + IS 'Enumerates the states of a withdrawal operation. + The states are the same as in the bank-integration API: + pending : the operation is pending parameters selection (exchange and reserve public key) + selected : the operations has been selected and is pending confirmation + aborted : the operation has been aborted + confirmed: the transfer has been confirmed and registered by the bank'; + + +CREATE TYPE taler_amount_currency + AS (val INT8, frac INT4 , curr VARCHAR(12)); +COMMENT ON TYPE taler_amount_currency + IS 'Stores an amount, fraction is in units of 1/100000000 of the base value. + copied from https://git.taler.net/merchant.git/tree/src/backenddb/merchant-0001.sql'; + + +CREATE TABLE IF NOT EXISTS terminal_provider ( + provider_terminal_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + backend_base_url TEXT NOT NULL, + backend_credentials TEXT NOT NULL +); +COMMENT ON TABLE terminal_provider + IS 'Table describing providers of c2ec terminal'; +COMMENT ON COLUMN terminal_provider.provider_terminal_id + IS 'Uniquely identifies a provider'; +COMMENT ON COLUMN terminal_provider.name + IS 'Name of the provider, used for selection in transaction proofing'; +COMMENT ON COLUMN terminal_provider.backend_base_url + IS 'URL of the provider backend for transaction proofing'; +COMMENT ON COLUMN terminal_provider.backend_credentials + IS 'Credentials used to access the backend of the provider'; + + +CREATE TABLE IF NOT EXISTS terminal ( + terminal_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + access_token BYTEA CHECK (LENGTH(access_token)=32) NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, + description TEXT, + provider_id INT8 NOT NULL REFERENCES terminal_provider(provider_terminal_id) +); +COMMENT ON TABLE terminal + IS 'Table containing information about terminals of providers'; +COMMENT ON COLUMN terminal.terminal_id + IS 'Uniquely identifies a terminal'; +COMMENT ON COLUMN terminal.access_token + IS 'The access token of the terminal used for authentication against the c2ec API'; +COMMENT ON COLUMN terminal.active + IS 'Indicates if the terminal is active or deactivated'; +COMMENT ON COLUMN terminal.description + IS 'Description to help identify the terminal. This may include the location and an identifier of the terminal.'; +COMMENT ON COLUMN terminal.provider_id + IS 'Indicates the terminal provider to which the terminal belongs'; + + +CREATE TABLE IF NOT EXISTS withdrawal ( + withdrawal_id INT8 GENERATED VALUE BY DEFAULT AS IDENTITY PRIMARY KEY, + wopid BYTEA CHECK (LENGTH(wopid)=32) NOT NULL, + reserve_pub_key BYTEA CHECK (LENGTH(reserve_pub_key)=32) NOT NULL, + registration_ts INT8 NOT NULL, + amount taler_amount_currency NOT NULL, + fees taler_amount_currency, + withdrawal_status withdrawal_operation_status NOT NULL DEFAULT 'pending', + terminal_id INT8 NOT NULL REFERENCES terminal(terminal_id), + provider_transaction_id TEXT, + last_retry_ts INT8, + retry_counter INT4 NOT NULL DEFAULT 0, + completion_proof BLOB +); +COMMENT ON TABLE withdrawal + IS 'Table representing withdrawal processes initiated by terminals'; +COMMENT ON COLUMN withdrawal.withdrawal_id + IS 'The withdrawal id is used a technical id used by the wire gateway to sequentially select new transactions'; +COMMENT ON COLUMN withdrawal.wopid + IS 'The wopid (withdrawal operation id) is a nonce generated by the terminal requesting a withdrawal. '+ + 'The wopid identifies a specific withdrawal spawning all involved systems.'; +COMMENT ON COLUMN withdrawal.reserve_pub_key + IS 'Reserve public key for the reserve which will hold the withdrawal amount after completion'; +COMMENT ON COLUMN withdrawal.registration_ts + IS 'Timestamp of when the withdrawal request was registered'; +COMMENT ON COLUMN withdrawal.amount + IS 'Effective amount to be put into the reserve after completion'; +COMMENT ON COLUMN withdrawal.fees + IS 'Fees associated with the withdrawal, including exchange and provider fees'; +COMMENT ON COLUMN withdrawal.withdrawal_status + IS 'Status of the withdrawal process'; +COMMENT ON COLUMN withdrawal.terminal_id + IS 'ID of the terminal that initiated the withdrawal'; +COMMENT ON COLUMN withdrawal.provider_transaction_id + IS 'Transaction identifier supplied by the provider for backend request'; +COMMENT ON COLUMN withdrawal.last_retry_ts + IS 'Timestamp of the last retry attempt'; +COMMENT ON COLUMN withdrawal.retry_counter + IS 'Number of retry attempts'; +COMMENT ON COLUMN withdrawal.completion_proof + IS 'Proof of transaction upon final completion delivered by the providers system'; diff --git a/data/nonce2ecash_schema.sql b/data/nonce2ecash_schema.sql @@ -1,107 +0,0 @@ --- => proper versioning.sql nehmen (siehe exchange.git), -DROP SCHEMA IF EXISTS nonce2ecash CASCADE; - -CREATE SCHEMA nonce2ecash; -COMMENT ON SCHEMA nonce2ecash - IS 'Schema containing all tables and types related to nonce2ecash'; - -SET search_path TO nonce2ecash; - -CREATE TYPE withdrawal_operation_status AS ENUM ( - 'pending', - 'selected', - 'aborted', - 'confirmed' -); -COMMENT ON TYPE withdrawal_operation_status - IS 'Enumerates the states of a withdrawal operation. - The states are the same as in the bank-integration API: - pending : the operation is pending parameters selection (exchange and reserve public key) - selected : the operations has been selected and is pending confirmation - aborted : the operation has been aborted - confirmed: the transfer has been confirmed and registered by the bank'; - - -CREATE TYPE taler_amount_currency - AS (val INT8, frac INT4 , curr VARCHAR(12)); -COMMENT ON TYPE taler_amount_currency - IS 'Stores an amount, fraction is in units of 1/100000000 of the base value. - copied from https://git.taler.net/merchant.git/tree/src/backenddb/merchant-0001.sql'; - - -CREATE TABLE IF NOT EXISTS terminal_provider ( - provider_terminal_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - backend_base_url TEXT NOT NULL, - backend_credentials TEXT NOT NULL -); -COMMENT ON TABLE terminal_provider - IS 'Table describing providers of nonce2ecash terminal'; -COMMENT ON COLUMN terminal_provider.provider_terminal_id - IS 'Uniquely identifies a provider'; -COMMENT ON COLUMN terminal_provider.name - IS 'Name of the provider, used for selection in transaction proofing'; -COMMENT ON COLUMN terminal_provider.backend_base_url - IS 'URL of the provider backend for transaction proofing'; -COMMENT ON COLUMN terminal_provider.backend_credentials - IS 'Credentials used to access the backend of the provider'; - - -CREATE TABLE IF NOT EXISTS terminal ( - terminal_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - access_token BYTEA CHECK (LENGTH(access_token)=32) NOT NULL, - active BOOLEAN NOT NULL DEFAULT TRUE, - description TEXT, - provider_id INT8 NOT NULL REFERENCES terminal_provider(provider_terminal_id) -); -COMMENT ON TABLE terminal - IS 'Table containing information about terminals of providers'; -COMMENT ON COLUMN terminal.terminal_id - IS 'Uniquely identifies a terminal'; -COMMENT ON COLUMN terminal.access_token - IS 'The access token of the terminal used for authentication against the nonce2ecash API'; -COMMENT ON COLUMN terminal.active - IS 'Indicates if the terminal is active or deactivated'; -COMMENT ON COLUMN terminal.description - IS 'Description to help identify the terminal. This may include the location and an identifier of the terminal.'; -COMMENT ON COLUMN terminal.provider_id - IS 'Indicates the terminal provider to which the terminal belongs'; - - -CREATE TABLE IF NOT EXISTS withdrawal ( - withdrawal_id BYTEA PRIMARY KEY CHECK (LENGTH(withdrawal_id)=32), - reserve_pub_key BYTEA CHECK (LENGTH(reserve_pub_key)=32) NOT NULL, - registration_ts INT8 NOT NULL, - amount taler_amount_currency NOT NULL, - fees taler_amount_currency NOT NULL, - withdrawal_status withdrawal_operation_status NOT NULL, - terminal_id INT8 NOT NULL REFERENCES terminal(terminal_id), - provider_transaction_id TEXT, - last_retry_ts INT8, - retry_counter INT4 NOT NULL DEFAULT 0, - completion_proof BLOB -); -COMMENT ON TABLE withdrawal - IS 'Table representing withdrawal processes initiated by terminals'; -COMMENT ON COLUMN withdrawal.withdrawal_id - IS 'The withdrawal id is a nonce generated by the terminal requesting a withdrawal'; -COMMENT ON COLUMN withdrawal.reserve_pub_key - IS 'Reserve public key for the reserve which will hold the withdrawal amount after completion'; -COMMENT ON COLUMN withdrawal.registration_ts - IS 'Timestamp of when the withdrawal request was registered'; -COMMENT ON COLUMN withdrawal.amount - IS 'Effective amount to be put into the reserve after completion'; -COMMENT ON COLUMN withdrawal.fees - IS 'Fees associated with the withdrawal, including exchange and provider fees'; -COMMENT ON COLUMN withdrawal.withdrawal_status - IS 'Status of the withdrawal process'; -COMMENT ON COLUMN withdrawal.terminal_id - IS 'ID of the terminal that initiated the withdrawal'; -COMMENT ON COLUMN withdrawal.provider_transaction_id - IS 'Transaction identifier supplied by the provider for backend request'; -COMMENT ON COLUMN withdrawal.last_retry_ts - IS 'Timestamp of the last retry attempt'; -COMMENT ON COLUMN withdrawal.retry_counter - IS 'Number of retry attempts'; -COMMENT ON COLUMN withdrawal.completion_proof - IS 'Proof of transaction upon final completion delivered by the providers system'; diff --git a/docs/content/architecture/c2ec.tex b/docs/content/architecture/c2ec.tex @@ -10,7 +10,7 @@ A specification of the API in yaml format can be found in \autoref*{appendix-api Terminals which authenticate against the C2EC API must provide their respective access token. Therefore, they provide a \texttt{Authorization: Bearer \$ACCESS\_TOKEN} header, where \texttt{\$ACCESS\_TOKEN} is a secret authentication token configured by the exchange and must begin with the prefix specified in RFC 8959 \cite{rfc8959}: \textit{secret-token}. -\subsubsection{Configuration of C2EC} +\subsubsection{Configuration of C2EC Bank Integration API} \begin{itemize} \item \textbf{Method:} GET @@ -19,9 +19,9 @@ Terminals which authenticate against the C2EC API must provide their respective \item \textbf{Response:} HTTP status code 200 OK. The exchange responds with a \texttt{C2ECConfig} object. \end{itemize} -\subsubsection{Withdrawing using C2EC} +\subsubsection{Taler Bank Integration API} -Withdrawals with a C2EC are based on withdrawal operations which register a withdrawal identifier (nonce) at the C2EC component. The provider must first create a unique identifier for the withdrawal operation (the \texttt{WITHDRAWAL\_ID}) to interact with the withdrawal operation and eventually withdraw using the wallet. +Withdrawals with a C2EC are based on withdrawal operations which register a withdrawal identifier (nonce) at the C2EC component. The provider must first create a unique identifier for the withdrawal operation (the \texttt{WITHDRAWAL\_ID}) to interact with the withdrawal operation and eventually withdraw using the wallet. The withdrawal operation API is an implementation of the Bank Integration API \cite{taler-bank-integration-api}. \textbf{POST - withdrawal-operation} \begin{itemize} @@ -52,10 +52,14 @@ Withdrawals with a C2EC are based on withdrawal operations which register a with \subsection{Taler Wirewatch Gateway API} The Taler Wirewatch Gateway \cite{taler-wirewatch-gateway-api} must be implemented in order to capture incoming transactions and allow the withdrawal of money. The specification of the Taler Wirewatch Gateway can be found in the official Taler documentation \cite{taler-wirewatch-gateway-api}. -The wirewatch gateway helps the Exchange communicate with the C2EC component using a the API. It helps the Exchange to get guarantees, that a certain transaction went through and that the reserve can be created and withdrawn. This will help C2EC to capture the transaction of the Terminal Backend to the Exchange's account and therefore allow the withdrawal by the customer. Therefore the wirewatch gateway API is used in C2EC. When the wirewatch gateway can get the proof, that a transaction was successfully processed, it will create a reserve with the corresponding reserve public key. +The wirewatch gateway helps the Exchange communicate with the C2EC component using a the API. It helps the Exchange to fetch guarantees, that a certain transaction went through and that the reserve can be created and withdrawn. This will help C2EC to capture the transaction of the Terminal Backend to the Exchange's account and therefore allow the withdrawal by the customer. Therefore the wirewatch gateway API is used in C2EC. When the wirewatch gateway can get the proof, that a transaction was successfully processed, the exchange will create a reserve with the corresponding reserve public key. + +For C2EC not all endpoints of the Wire Gateway API are needed. Therefore the endoints which are not needed will be implemented but always return http status code 400 with explanatory error details as specified by the specification. + +\subsubsection{Configuration of C2EC Wire Gateway API} + +\subsubsection{Taler Bank Integration API} -\subsection{Taler Bank Integration API} -The Taler Bank Integration \cite{taler-bank-integration-api} must be implemented in order to allow the Wallet to withdraw money using already implemented flows. The wallet will use the implementation of the Bank Integration API to withdraw. The documentation for the Bank Integration API can also be found in the official Taler documentation. \subsection{The C2EC database} diff --git a/docs/thesis.pdf b/docs/thesis.pdf Binary files differ. diff --git a/nonce2ecash/go.mod b/nonce2ecash/go.mod @@ -1,18 +0,0 @@ -module nonce2ecash - -go 1.22.0 - -require gotest.tools/v3 v3.5.1 - -require ( - github.com/google/go-cmp v0.5.9 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/lib/pq v1.10.9 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - gorm.io/gorm v1.25.7 // indirect -) diff --git a/nonce2ecash/go.sum b/nonce2ecash/go.sum @@ -1,29 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= -gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/nonce2ecash/pkg/common/amount.go b/nonce2ecash/pkg/common/amount.go @@ -1,153 +0,0 @@ -// 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/codec_test.go b/nonce2ecash/pkg/common/codec_test.go @@ -1,62 +0,0 @@ -package common_test - -import ( - "bytes" - "fmt" - "nonce2ecash/pkg/common" - "testing" - - "gotest.tools/v3/assert" -) - -func TestJsonCodecRoundTrip(t *testing.T) { - - type TestStruct struct { - A string - B int - C []string - D byte - E []byte - F *TestStruct - } - - testObj := TestStruct{ - "TestA", - 1, - []string{"first", "second"}, - 'A', - []byte{0xdf, 0x01, 0x34}, - &TestStruct{ - "TestAA", - 2, - []string{"third", "fourth", "fifth"}, - 'B', - []byte{0xdf, 0x01, 0x34}, - nil, - }, - } - - jsonCodec := new(common.JsonCodec[TestStruct]) - - encodedTestObj, err := jsonCodec.Encode(&testObj) - if err != nil { - fmt.Println("error happened while encoding test obj", err.Error()) - t.FailNow() - } - - encodedTestObjBytes := make([]byte, 200) - _, err = encodedTestObj.Read(encodedTestObjBytes) - if err != nil { - fmt.Println("error happened while encoding test obj to byte array", err.Error()) - t.FailNow() - } - - encodedTestObjReader := bytes.NewReader(encodedTestObjBytes) - decodedTestObj, err := jsonCodec.Decode(encodedTestObjReader) - if err != nil { - fmt.Println("error happened while encoding test obj to byte array", err.Error()) - t.FailNow() - } - - assert.DeepEqual(t, &testObj, decodedTestObj) -} diff --git a/nonce2ecash/pkg/common/http-util.go b/nonce2ecash/pkg/common/http-util.go @@ -1,198 +0,0 @@ -package common - -import ( - "errors" - "fmt" - "net/http" - "strings" -) - -const HTTP_OK = 200 -const HTTP_NO_CONTENT = 204 -const HTTP_BAD_REQUEST = 400 -const HTTP_UNAUTHORIZED = 401 -const HTTP_NOT_FOUND = 404 -const HTTP_METHOD_NOT_ALLOWED = 405 -const HTTP_CONFLICT = 409 -const HTTP_INTERNAL_SERVER_ERROR = 500 - -// execute a GET request and parse body or retrieve error -func HttpGet2[T any]( - req string, - codec Codec[T], -) (*T, int, error) { - - return HttpGet( - req, - nil, - nil, - codec, - ) -} - -// execute a GET request and parse body or retrieve error -// path- and query-parameters can be set to add query and path parameters -func HttpGet[T any]( - req string, - pathParams map[string]string, - queryParams map[string]string, - codec Codec[T], -) (*T, int, error) { - - url := formatUrl(req, pathParams, queryParams) - fmt.Println("GET:", url) - - res, err := http.Get(url) - if err != nil { - return nil, -1, err - } - - if codec == nil { - return nil, res.StatusCode, err - } else { - resBody, err := codec.Decode(res.Body) - return resBody, res.StatusCode, err - } -} - -// execute a POST request and parse response or retrieve error -func HttpPost2[T any, R any]( - req string, - body *T, - requestCodec Codec[T], - responseCodec Codec[R], -) (*R, int, error) { - - return HttpPost( - req, - nil, - nil, - body, - requestCodec, - responseCodec, - ) -} - -// execute a POST request and parse response or retrieve error -// path- and query-parameters can be set to add query and path parameters -func HttpPost[T any, R any]( - req string, - pathParams map[string]string, - queryParams map[string]string, - body *T, - requestCodec Codec[T], - responseCodec Codec[R], -) (*R, int, error) { - - url := formatUrl(req, pathParams, queryParams) - fmt.Println("POST:", url) - - var res *http.Response - if body == nil { - if requestCodec == nil { - res, err := http.Post( - url, - "", - nil, - ) - - if err != nil { - return nil, -1, err - } - - return nil, res.StatusCode, nil - } else { - return nil, -1, errors.New("invalid arguments - body was not present but codec was defined") - } - } else { - if requestCodec == nil { - return nil, -1, errors.New("invalid arguments - body was present but no codec was defined") - } else { - - encodedBody, err := requestCodec.Encode(body) - if err != nil { - return nil, -1, err - } - - res, err = http.Post( - url, - requestCodec.HttpApplicationContentHeader(), - encodedBody, - ) - - if err != nil { - return nil, -1, err - } - } - } - - if responseCodec == nil { - return nil, res.StatusCode, nil - } - - resBody, err := responseCodec.Decode(res.Body) - if err != nil { - return nil, -1, err - } - - return resBody, res.StatusCode, err -} - -// builds request URL containing the path and query -// parameters of the respective parameter map. -func formatUrl( - req string, - pathParams map[string]string, - queryParams map[string]string, -) string { - - return setUrlQuery(setUrlPath(req, pathParams), queryParams) -} - -// Sets the parameters which are part of the url. -// The function expects each parameter in the path to be prefixed -// using a ':'. The function handles url as follows: -// -// /some/:param/tobereplaced -> ':param' will be replaced with value. -// -// For replacements, the pathParams map must be supplied. The map contains -// the name of the parameter with the value mapped to it. -// The names MUST not contain the prefix ':'! -func setUrlPath( - req string, - pathParams map[string]string, -) string { - - if pathParams == nil || len(pathParams) < 1 { - return req - } - - var url = req - for k, v := range pathParams { - - if !strings.HasPrefix(k, "/") { - // prevent scheme postfix replacements - url = strings.Replace(url, ":"+k, v, 1) - } - } - return url -} - -func setUrlQuery( - req string, - queryParams map[string]string, -) string { - - if queryParams == nil || len(queryParams) < 1 { - return req - } - - var url = req + "?" - for k, v := range queryParams { - - url = strings.Join([]string{url, k, "=", v, "&"}, "") - } - - url, _ = strings.CutSuffix(url, "&") - return url -} diff --git a/nonce2ecash/pkg/common/http-util_test.go b/nonce2ecash/pkg/common/http-util_test.go @@ -1,62 +0,0 @@ -package common_test - -import ( - "fmt" - "nonce2ecash/pkg/common" - "testing" -) - -const URL_GET = "https://jsonplaceholder.typicode.com/todos/:id" -const URL_POST = "https://jsonplaceholder.typicode.com/posts" - -type TestStruct struct { - UserId int `json:"userId"` - Id int `json:"id"` - Title string `json:"title"` - Completed bool `json:"completed"` -} - -func TestGET(t *testing.T) { - - res, status, err := common.HttpGet( - URL_GET, - map[string]string{ - "id": "1", - }, - map[string]string{}, - common.NewJsonCodec[TestStruct](), - ) - - if err != nil { - t.Errorf("%s", err.Error()) - t.FailNow() - } - - fmt.Println("res:", res, ", status:", status) -} - -func TestPOST(t *testing.T) { - - res, status, err := common.HttpPost( - URL_POST, - map[string]string{ - "id": "1", - }, - map[string]string{}, - &TestStruct{ - UserId: 1, - Id: 1, - Title: "TEST", - Completed: false, - }, - common.NewJsonCodec[TestStruct](), - common.NewJsonCodec[TestStruct](), - ) - - if err != nil { - t.Errorf("%s", err.Error()) - t.FailNow() - } - - fmt.Println("res:", res, ", status:", status) -} diff --git a/nonce2ecash/pkg/common/model.go b/nonce2ecash/pkg/common/model.go @@ -1,31 +0,0 @@ -package common - -// https://docs.taler.net/core/api-common.html#hash-codes -type WithdrawalIdentifier string - -// https://docs.taler.net/core/api-common.html#cryptographic-primitives -type EddsaPublicKey string - -// https://docs.taler.net/core/api-common.html#hash-codes -type HashCode string - -// https://docs.taler.net/core/api-common.html#hash-codes -type ShortHashCode string - -// https://docs.taler.net/core/api-common.html#timestamps -type Timestamp struct { - Ts int `json:"t_s"` -} - -// https://docs.taler.net/core/api-common.html#wadid -type WadId [6]uint32 - -// according to https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationStatus -type WithdrawalOperationStatus string - -const ( - PENDING WithdrawalOperationStatus = "pending" - SELECTED WithdrawalOperationStatus = "selected" - ABORTED WithdrawalOperationStatus = "aborted" - CONFIRMED WithdrawalOperationStatus = "confirmed" -) diff --git a/nonce2ecash/pkg/db/db.go b/nonce2ecash/pkg/db/db.go @@ -1,66 +0,0 @@ -package db - -import ( - "context" - "strconv" - "strings" - - "github.com/jackc/pgx/v5" -) - -const POSTGRESQL_SCHEME = "postgres://" -const NONCE2ECASH_DATABASE = "nonce2ecash" - -type DatabaseConfig interface { - ConnectionString() string -} - -type PostgresNonce2ecashDatabaseConfig struct { - DatabaseConfig - - host string - username string - password string - port int -} - -func NewDbConf( - host string, - port int, - username string, - password string, -) DatabaseConfig { - - cfg := new(PostgresNonce2ecashDatabaseConfig) - cfg.host = host - cfg.port = port - cfg.username = username - cfg.password = password - - return cfg -} - -func NewDb(cfg DatabaseConfig) (*pgx.Conn, error) { - - return pgx.Connect( - context.Background(), - cfg.ConnectionString(), - ) -} - -func (cfg *PostgresNonce2ecashDatabaseConfig) ConnectionString() string { - - // format: postgres://username:password@hostname:port/database_name - return strings.Join([]string{ - POSTGRESQL_SCHEME, - cfg.username, - ":", - cfg.password, - "@", - cfg.host, - ":", - strconv.FormatInt(int64(cfg.port), 10), - "/", - NONCE2ECASH_DATABASE, - }, "") -} diff --git a/nonce2ecash/pkg/db/provider.go b/nonce2ecash/pkg/db/provider.go @@ -1,23 +0,0 @@ -package db - -import "gorm.io/gorm" - -type TerminalProvider struct { - gorm.Model - - ProviderTerminalID int64 `gorm:"primaryKey"` - Name string `gorm:"unique;not null"` - BackendBaseURL string `gorm:"not null"` - BackendCredentials string `gorm:"not null"` -} - -type Terminal struct { - gorm.Model - - TerminalID int64 `gorm:"primaryKey"` - AccessToken []byte `gorm:"type:bytea;not null;check:LENGTH(access_token)=32"` - Active bool `gorm:"not null;default:true"` - ProviderID int64 `gorm:"not null"` - Provider TerminalProvider `gorm:"foreignKey:ProviderID"` - Description string -} diff --git a/nonce2ecash/pkg/db/withdrawal.go b/nonce2ecash/pkg/db/withdrawal.go @@ -1,29 +0,0 @@ -package db - -import ( - "nonce2ecash/pkg/common" - - "gorm.io/gorm" -) - -type Withdrawal struct { - gorm.Model - - WithdrawalId []byte `gorm:"type:bytea;primaryKey"` - ReservePubKey []byte `gorm:"type:bytea"` - RegistrationTs int64 - Amount TalerAmountCurrency - Fees TalerAmountCurrency - WithdrawalStatus common.WithdrawalOperationStatus - TerminalId int64 - ProviderTransactionId string - LastRetryTs int64 - RetryCounter int32 `gorm:"default:0"` - CompletionProof []byte `gorm:"type:blob"` -} - -type TalerAmountCurrency struct { - Val int64 - Frac int32 - Curr string `gorm:"size:12"` -} diff --git a/nonce2ecash/pkg/handler.go b/nonce2ecash/pkg/handler.go @@ -1,70 +0,0 @@ -package main - -import ( - "bytes" - "log" - http "net/http" - "nonce2ecash/pkg/common" -) - -const CONFIG_ENDPOINT = "/config" -const WITHDRAWAL_OPERATION = "/withdrawal-operation" - -const WOPID_PARAMETER = "wopid" -const GET_CONFIG_PATTERN = GET + CONFIG_ENDPOINT -const POST_WITHDRAWAL_OPERATION_PATTERN = POST + WITHDRAWAL_OPERATION -const GET_WITHDRAWAL_OPERATION_BY_WOPID_PATTERN = GET + WITHDRAWAL_OPERATION + "/{" + WOPID_PARAMETER + "}" -const POST_WITHDRAWAL_OPERATION_BY_WOPID_PATTERN = POST + WITHDRAWAL_OPERATION + "/{" + WOPID_PARAMETER + "}" - -func config(res http.ResponseWriter, req *http.Request) { - - cfg := Nonce2ecashConfig{ - Name: "taler-nonce2ecash", - Version: "0:0:1", - } - - serializedCfg, err := common.NewJsonCodec[Nonce2ecashConfig]().EncodeToBytes(&cfg) - if err != nil { - log.Default().Printf("failed serializing config: %s", err.Error()) - res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR) - return - } - - res.WriteHeader(common.HTTP_OK) - res.Write(serializedCfg) -} - -func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) { - - res.WriteHeader(common.HTTP_OK) - res.Write(bytes.NewBufferString("retrieved withdrawal request").Bytes()) -} - -func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { - - wopid := parseWopId(req) - if wopid == "" { - res.WriteHeader(common.HTTP_BAD_REQUEST) - return - } - - res.WriteHeader(common.HTTP_OK) - res.Write(bytes.NewBufferString("retrieved withdrawal status request for wopid=" + wopid).Bytes()) -} - -func handlePaymentNotification(res http.ResponseWriter, req *http.Request) { - - wopid := parseWopId(req) - if wopid == "" { - res.WriteHeader(common.HTTP_BAD_REQUEST) - return - } - - res.WriteHeader(common.HTTP_OK) - res.Write(bytes.NewBufferString("retrieved payment notification for wopid=" + wopid).Bytes()) -} - -func parseWopId(req *http.Request) string { - - return req.PathValue(WOPID_PARAMETER) -} diff --git a/nonce2ecash/pkg/main.go b/nonce2ecash/pkg/main.go @@ -1,33 +0,0 @@ -package main - -import ( - http "net/http" -) - -const GET = "GET " -const POST = "POST " - -func main() { - - http.HandleFunc( - GET_CONFIG_PATTERN, - config, - ) - - http.HandleFunc( - POST_WITHDRAWAL_OPERATION_PATTERN, - handleWithdrawalRegistration, - ) - - http.HandleFunc( - GET_WITHDRAWAL_OPERATION_BY_WOPID_PATTERN, - handleWithdrawalStatus, - ) - - http.HandleFunc( - POST_WITHDRAWAL_OPERATION_BY_WOPID_PATTERN, - handlePaymentNotification, - ) - - http.ListenAndServe(":8080", nil) -} diff --git a/nonce2ecash/pkg/model.go b/nonce2ecash/pkg/model.go @@ -1,25 +0,0 @@ -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 Withdrawal struct { - ReservePubKey common.EddsaPublicKey `json:"reserve_pub_key"` -} - -type PaymentNotification struct { - ProviderTransactionId string `json:"provider_transaction_id"` - Amount common.Amount `json:"amount"` - Fees common.Amount `json:"fees"` -} diff --git a/nonce2ecash/pkg/pkg b/nonce2ecash/pkg/pkg Binary files differ. diff --git a/nonce2ecash/pkg/taler-bank-integration/client.go b/nonce2ecash/pkg/taler-bank-integration/client.go @@ -1,163 +0,0 @@ -package talerbankintegration - -import ( - "errors" - "fmt" - "nonce2ecash/pkg/common" -) - -const WITHDRAWAL_ID_PATH_PARAM_NAME = "withdrawal_id" - -const TALER_BANK_INTEGRATION_CONFIG_API = "/config" -const WITHDRAWAL_OPERATION_API = "/withdrawal-operation" -const WITHDRAWAL_OPERATION_BY_ID_API = WITHDRAWAL_OPERATION_API + "/:" + WITHDRAWAL_ID_PATH_PARAM_NAME -const WITHDRAWAL_OPERATION_ABORT_BY_ID_API = WITHDRAWAL_OPERATION_BY_ID_API + "/abort" - -type TalerBankIntegration interface { - init(string) - - BankIntegrationConfig() (*BankIntegrationConfig, error) - WithdrawalOperationStatus(common.WithdrawalIdentifier) (*BankWithdrawalOperationStatus, error) - WithdrawalOperationCreate(common.EddsaPublicKey, string) (*BankWithdrawalOperationPostResponse, error) - WithdrawalOperationAbort(common.WithdrawalIdentifier) error -} - -type TalerBankIntegrationImpl struct { - TalerBankIntegration - - exchangeBaseUrl string -} - -func (tbi *TalerBankIntegrationImpl) BankIntegrationConfig() (*BankIntegrationConfig, error) { - - cfg, status, err := common.HttpGet( - tbi.exchangeBaseUrl+TALER_BANK_INTEGRATION_CONFIG_API, - nil, - nil, - common.NewJsonCodec[BankIntegrationConfig](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return cfg, nil - } - - return nil, fmt.Errorf("HTTP %d - unexpected", status) -} - -// Initialize the taler bank integration implementation. -// The exchangeBaseUrl will be used as target by the impl. -func (tbi *TalerBankIntegrationImpl) init(exchangeBaseUrl string) { - - tbi.exchangeBaseUrl = exchangeBaseUrl -} - -// check status of withdrawal -func (tbi *TalerBankIntegrationImpl) WithdrawalOperationStatus( - id common.WithdrawalIdentifier, -) (*BankWithdrawalOperationStatus, error) { - - WithdrawalOperationStatus, status, err := common.HttpGet( - tbi.exchangeBaseUrl+WITHDRAWAL_OPERATION_BY_ID_API, - map[string]string{WITHDRAWAL_ID_PATH_PARAM_NAME: string(id)}, - nil, - common.NewJsonCodec[BankWithdrawalOperationStatus](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return WithdrawalOperationStatus, nil - } - - if status == common.HTTP_NOT_FOUND { - - return nil, errors.New("HTTP 404 - The operation was not found") - } - - return nil, fmt.Errorf("HTTP %d - unexpected", status) -} - -// send parameters for reserve to exchange core. -func (tbi *TalerBankIntegrationImpl) WithdrawalOperationCreate( - id common.WithdrawalIdentifier, - reservePubKey common.EddsaPublicKey, - exchangPayToAddress string, -) (*BankWithdrawalOperationPostResponse, error) { - - bankWithdrawalOperationPostResponse, status, err := common.HttpPost( - tbi.exchangeBaseUrl+WITHDRAWAL_OPERATION_BY_ID_API, - map[string]string{WITHDRAWAL_ID_PATH_PARAM_NAME: string(id)}, - nil, - &BankWithdrawalOperationPostRequest{ - string(reservePubKey), - exchangPayToAddress, - }, - common.NewJsonCodec[BankWithdrawalOperationPostRequest](), - common.NewJsonCodec[BankWithdrawalOperationPostResponse](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return bankWithdrawalOperationPostResponse, nil - } - - if status == common.HTTP_NOT_FOUND { - - return nil, errors.New("HTTP 404 - The operation was not found") - } - - if status == common.HTTP_CONFLICT { - - return nil, errors.New("HTTP 409 - conflict") - } - - return nil, fmt.Errorf("HTTP %d - unexpected", status) -} - -// abort withdrawal -func (tbi *TalerBankIntegrationImpl) WithdrawalOperationAbort( - id common.WithdrawalIdentifier, -) error { - - _, status, err := common.HttpPost[any, any]( - tbi.exchangeBaseUrl+WITHDRAWAL_OPERATION_BY_ID_API, - map[string]string{WITHDRAWAL_ID_PATH_PARAM_NAME: string(id)}, - nil, - nil, - nil, - nil, - ) - - if err != nil { - return err - } - - if status == common.HTTP_NO_CONTENT { - - return nil - } - - if status == common.HTTP_NOT_FOUND { - - return errors.New("HTTP 404 - The withdrawal operation was not found") - } - - if status == common.HTTP_CONFLICT { - - return errors.New("HTTP 409 - The withdrawal operation has been confirmed previously and can’t be aborted") - } - - return fmt.Errorf("HTTP %d - unexpected", status) -} diff --git a/nonce2ecash/pkg/taler-bank-integration/model.go b/nonce2ecash/pkg/taler-bank-integration/model.go @@ -1,50 +0,0 @@ -package talerbankintegration - -import "nonce2ecash/pkg/common" - -// https://docs.taler.net/core/api-exchange.html#tsref-type-CurrencySpecification -type CurrencySpecification struct { - Name string `json:"name"` - Currency string `json:"currency"` - NumFractionalInputDigits int `json:"num_fractional_input_digits"` - NumFractionalNormalDigits int `json:"num_fractional_normal_digits"` - NumFractionalTrailingZeroDigits int `json:"num_fractional_trailing_zero_digits"` - AltUnitNames string `json:"alt_unit_names"` -} - -// https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankIntegrationConfig -type BankIntegrationConfig struct { - Name string `json:"name"` - Version string `json:"version"` - Implementation string `json:"implementation"` - Currency string `json:"currency"` - CurrencySpecification CurrencySpecification `json:"currency_specification"` -} - -// 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 @@ -1,226 +0,0 @@ -package talerwirewatchgateway - -import ( - "errors" - "fmt" - "nonce2ecash/pkg/common" - "strconv" -) - -const WIRE_GATEWAY_START_QUERY = "start" -const WIRE_GATEWAY_DELTA_QUERY = "delta" -const WIRE_GATEWAY_LONGPOLL_QUERY = "long_poll_ms" - -const WIRE_GATEWAY_API = "" -const WIRE_GATEWAY_API_CONFIG = WIRE_GATEWAY_API + "/config" -const WIRE_GATEWAY_TRANSFER_API = WIRE_GATEWAY_API + "/transfer" -const WIRE_GATEWAY_HISTORY_INCOMING_API = WIRE_GATEWAY_API + "/history/incoming" -const WIRE_GATEWAY_HISTORY_OUTGOING_API = WIRE_GATEWAY_API + "/history/outgoing" - -type TalerWirewatchGateway interface { - Init(string, string) - - WirewatchGatewayConfig() (*WireConfig, error) - WirewatchGatewayTransfer(*TransferRequest) (*TransferResponse, error) - WirewatchGatewayHistoryIncoming(int, int, int) (*IncomingHistory, error) - WirewatchGatewayHistoryOutgoing(int, int, int) (*OutgoingHistory, error) -} - -type TalerWirewatchGatewayImpl struct { - TalerWirewatchGateway - - exchangeBaseUrl string - authToken string -} - -func NewWirewatchGateway(exchangeBaseUrl string, authToken string) TalerWirewatchGateway { - - twg := new(TalerWirewatchGatewayImpl) - twg.Init(exchangeBaseUrl, authToken) - return twg -} - -func (twg *TalerWirewatchGatewayImpl) Init(exchangeBaseUrl string, authToken string) { - - twg.exchangeBaseUrl = exchangeBaseUrl - twg.authToken = authToken -} - -// https://docs.taler.net/core/api-bank-wire.html#making-transactions -func (twg *TalerWirewatchGatewayImpl) WirewatchGatewayConfig() (*WireConfig, error) { - - res, status, err := common.HttpGet2( - WIRE_GATEWAY_API_CONFIG, - common.NewJsonCodec[WireConfig](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return res, nil - } - - return nil, fmt.Errorf("HTTP %d - unexpected", status) -} - -// https://docs.taler.net/core/api-bank-wire.html#making-transactions -func (twg *TalerWirewatchGatewayImpl) WirewatchGatewayTransfer( - transferRequest *TransferRequest, -) (*TransferResponse, error) { - - res, status, err := common.HttpPost2( - WIRE_GATEWAY_TRANSFER_API, - transferRequest, - common.NewJsonCodec[TransferRequest](), - common.NewJsonCodec[TransferResponse](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return res, nil - } - - if status == common.HTTP_BAD_REQUEST { - - return nil, errors.New("request malformed") - } - - if status == common.HTTP_UNAUTHORIZED { - - return nil, errors.New("authentication failed, likely the credentials are wrong") - } - - if status == common.HTTP_NOT_FOUND { - - return nil, errors.New("the endpoint is wrong or the user name is unknown") - } - - if status == common.HTTP_CONFLICT { - - return nil, errors.New("a transaction with the same request_uid but different transaction details has been submitted before") - } - - return nil, fmt.Errorf("HTTP %d - unexpected", status) -} - -// https://docs.taler.net/core/api-bank-wire.html#querying-the-transaction-history -func (twg *TalerWirewatchGatewayImpl) WirewatchGatewayHistoryIncoming( - optionalStartRow int, - deltaRows int, - optionalLongPollMsTimeout int, -) (*IncomingHistory, error) { - - res, status, err := common.HttpGet( - WIRE_GATEWAY_HISTORY_INCOMING_API, - nil, - buildHistoryQueryMap(optionalStartRow, deltaRows, optionalLongPollMsTimeout), - common.NewJsonCodec[IncomingHistory](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return res, nil - } - - if status == common.HTTP_BAD_REQUEST { - - return nil, errors.New("request malformed") - } - - if status == common.HTTP_UNAUTHORIZED { - - return nil, errors.New("authentication failed, likely the credentials are wrong") - } - - if status == common.HTTP_NOT_FOUND { - - return nil, errors.New("the endpoint is wrong or the user name is unknown") - } - - if status == common.HTTP_CONFLICT { - - return nil, errors.New("a transaction with the same request_uid but different transaction details has been submitted before") - } - - return nil, fmt.Errorf("HTTP %d - unexpected", 0) -} - -// https://docs.taler.net/core/api-bank-wire.html#querying-the-transaction-history -func (twg *TalerWirewatchGatewayImpl) WirewatchGatewayHistoryOutgoing( - optionalStartRow int, - deltaRows int, - optionalLongPollMsTimeout int, -) (*OutgoingHistory, error) { - - res, status, err := common.HttpGet( - WIRE_GATEWAY_HISTORY_OUTGOING_API, - nil, - buildHistoryQueryMap(optionalStartRow, deltaRows, optionalLongPollMsTimeout), - common.NewJsonCodec[OutgoingHistory](), - ) - - if err != nil { - return nil, err - } - - if status == common.HTTP_OK { - - return res, nil - } - - if status == common.HTTP_BAD_REQUEST { - - return nil, errors.New("request malformed") - } - - if status == common.HTTP_UNAUTHORIZED { - - return nil, errors.New("authentication failed, likely the credentials are wrong") - } - - if status == common.HTTP_NOT_FOUND { - - return nil, errors.New("the endpoint is wrong or the user name is unknown") - } - - if status == common.HTTP_CONFLICT { - - return nil, errors.New("a transaction with the same request_uid but different transaction details has been submitted before") - } - - return nil, fmt.Errorf("HTTP %d - unexpected", 0) -} - -func buildHistoryQueryMap( - optionalStartRow int, - deltaRows int, - optionalLongPollMsTimeout int, -) map[string]string { - - startStr := strconv.FormatInt(int64(optionalStartRow), 10) - deltaRowsStr := strconv.FormatInt(int64(deltaRows), 10) - longPollMsStr := strconv.FormatInt(int64(optionalLongPollMsTimeout), 10) - - queryParams := map[string]string{ - WIRE_GATEWAY_DELTA_QUERY: deltaRowsStr, - } - if optionalStartRow > 0 { - queryParams[WIRE_GATEWAY_START_QUERY] = startStr - } - if optionalLongPollMsTimeout > 0 { - queryParams[WIRE_GATEWAY_LONGPOLL_QUERY] = longPollMsStr - } - - return queryParams -} diff --git a/nonce2ecash/pkg/taler-wirewatch-gateway/model.go b/nonce2ecash/pkg/taler-wirewatch-gateway/model.go @@ -1,93 +0,0 @@ -package talerwirewatchgateway - -import "nonce2ecash/pkg/common" - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig -type WireConfig struct { - Name string `json:"name"` - Version string `json:"version"` - Currency string `json:"currency"` - Implementation string `json:"implementation"` -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest -type TransferRequest struct { - RequestUid common.HashCode `json:"request_uid"` - Amount common.Amount `json:"amount"` - ExchangeBaseUrl string `json:"exchange_base_url"` - Wtid common.ShortHashCode `json:"wtid"` - CreditAccount string `json:"credit_account"` -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferResponse -type TransferResponse struct { - Timestamp common.Timestamp `json:"timestamp"` - RowId int `json:"row_id"` -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingBankTransaction -// type IncomingBankTransaction = IncomingReserveTransaction | IncomingWadTransaction -type IncomingBankTransaction struct { - IncomingReserveTransaction - IncomingWadTransaction -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory -type IncomingHistory struct { - IncomingTransactions []IncomingBankTransaction `json:"incoming_transactions"` - CreditAccount string `json:"credit_account"` -} - -// type RESERVE | https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingReserveTransaction -type IncomingReserveTransaction struct { - Type string `json:"type"` - RowId int `json:"row_id"` - Date common.Timestamp `json:"date"` - Amount common.Amount `json:"amount"` - DebitAccount string `json:"debit_account"` - ReservePub common.EddsaPublicKey `json:"reserve_pub"` -} - -// type WAD | https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingWadTransaction -type IncomingWadTransaction struct { - Type string `json:"type"` - RowId int `json:"row_id"` - Date common.Timestamp `json:"date"` - Amount common.Amount `json:"amount"` - CreditAccount string `json:"credit_account"` - DebitAccount string `json:"debit_account"` - OriginExchangeUrl string `json:"origin_exchange_url"` - WadId common.WadId `json:"wad_id"` -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory -type OutgoingHistory struct { - OutgoingBankTransaction []OutgoingBankTransaction `json:"outgoing_bank_transaction"` - DebitAccount string `json:"debit_account"` -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction -type OutgoingBankTransaction struct { - RowId int `json:"row_id"` - Date common.Timestamp `json:"date"` - Amount common.Amount `json:"amount"` - CreditAccount string `json:"credit_account"` - Wtid common.ShortHashCode `json:"wtid"` - ExchangeBaseUrl string `json:"exchange_base_url"` -} - -// --------------------- -// TESTING (ONLY ADMINS) -// --------------------- - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest -type AddIncomingRequest struct { - Amount common.Amount `json:"amount"` - ReservcePub common.EddsaPublicKey `json:"reserve_pub"` - DebitAccount string `json:"debit_account"` -} - -// https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse -type AddIncomingResponse struct { - Timestamp common.Timestamp `json:"timestamp"` -} diff --git a/specs/api-c2ec.rst b/specs/api-c2ec.rst @@ -1,181 +0,0 @@ -.. - This file is part of GNU TALER. - - Copyright (C) 2014-2024 Taler Systems SA - - TALER 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 2.1, or (at your option) any later version. - - TALER 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - - @author Joel Häberli - -=========================== -The C2EC RESTful API -=========================== - -.. note:: - - **This API is experimental and not yet implemented** - -This chapter describe the APIs that third party providers need to integrate to allow -withdrawals through indirect payment channels like credit cards or ATM. - -.. contents:: Table of Contents - --------------- -Authentication --------------- - -Terminals which authenticate against the C2EC API must provide their respective -access token. Therefore they provide a ``Authorization: Bearer $ACCESS_TOKEN`` header, -where `$ACCESS_TOKEN`` is a secret authentication token configured by the exchange and -must begin with the RFC 8959 prefix. - ----------------------------- -Configuration of C2EC ----------------------------- - -.. http:get:: /config - - Return the protocol version and configuration information about the C2EC API. - - **Response:** - - :http:statuscode:`200 OK`: - The exchange responds with a `C2ECConfig` object. This request should - virtually always be successful. - - **Details:** - - .. ts:def:: C2ECConfig - - interface C2ECConfig { - // Name of the API. - name: "taler-c2ec"; - - // libtool-style representation of the C2EC protocol version, see - // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning - // The format is "current:revision:age". - version: string; - } - ------------------------------ -Withdrawing using C2EC ------------------------------ - -Withdrawals with a C2EC are based on withdrawal operations which register a withdrawal identifier -(nonce) at the C2EC component. The provider must first create a unique identifier for the withdrawal -operation (the ``WITHDRAWAL_ID``) to interact with the withdrawal operation and eventually withdraw using the wallet. - -.. http:post:: /withdrawal-operation - - Initiate the withdrawal operation, identified by the ``WITHDRAWAL_ID``. - - **Request:** - - .. ts:def:: C2ECWithdrawalOperationPostRequest - - interface WithdrawRegistration { - // Maps a nonce generated by the provider to a reserve public key generated by the wallet. - withdrawal_id: ShortHashCode; - - // Reserve public key generated by the wallet. - // According to TALER_ReservePublicKeyP (https://docs.taler.net/core/api-common.html#cryptographic-primitives) - reserve_pub_key: EddsaPublicKey; - - // Optional amount for the withdrawal. - amount?: Amount; - - // Id of the terminal of the provider requesting a withdrawal by nonce. - // Assigned by the exchange. - provider_terminal_id: SafeUint64; - } - - **Response:** - - :http:statuscode:`204 No content`: - The withdrawal was successfully registered. - :http:statuscode:`400 Bad request`: - The ``WithdrawRegistration`` request was malformed or contained invalid parameters. - :http:statuscode:`500 Internal Server error`: - The registration of the withdrawal failed due to server side issues. - -.. http:get:: /withdrawal-operation/$WITHDRAWAL_ID - - Query information about a withdrawal operation, identified by the ``WITHDRAWAL_ID``. - - **Response:** - - :http:statuscode:`200 Ok`: - The withdrawal was found and is returned in the response body as ``C2ECWithdrawalStatus``. - :http:statuscode:`404 Not found`: - C2EC does not have a withdrawal registered with the specified ``WITHDRAWAL_ID``. - - **Details** - - .. ts:def:: C2ECWithdrawalStatus - - interface C2ECWithdrawalStatus { - // Current status of the operation - // pending: the operation is pending parameters selection (exchange and reserve public key) - // selected: the operations has been selected and is pending confirmation - // aborted: the operation has been aborted - // confirmed: the transfer has been confirmed and registered by the bank - // Since protocol v1. - status: "pending" | "selected" | "aborted" | "confirmed"; - - // Amount that will be withdrawn with this operation - // (raw amount without fee considerations). - amount: Amount; - - // A refund address as ``payto`` URI. This address shall be used - // in case a refund must be done. Only not-null if the status - // is "confirmed" or "aborted" - refund_wire?: string; - - // Reserve public key selected by the exchange, - // only non-null if ``status`` is ``selected`` or ``confirmed``. - // Since protocol v1. - selected_reserve_pub?: string; - } - - -.. http:post:: /withdrawal-operation/$WITHDRAWAL_ID - - Notifies C2EC about an executed payment for a specific withdrawal. - - **Request:** - - .. ts:def:: C2ECPaymentNotification - - interface C2ECPaymentNotification { - - // Unique identifier of the provider transaction. - provider_transaction_id: string; - - // Specifies the amount which was payed to the provider (without fees). - // This amount shall be put into the reserve linked to by the withdrawal id. - amount: Amount; - - // Fees associated with the payment. - fees: Amount; - } - - **Response:** - - :http:statuscode:`204 No content`: - C2EC received the ``C2ECPaymentNotification`` successfully and will further process - the withdrawal. - :http:statuscode:`400 Bad request`: - The ``C2ECPaymentNotification`` request was malformed or contained invalid parameters. - :http:statuscode:`404 Not found`: - C2EC does not have a withdrawal registered with the specified ``WITHDRAWAL_ID``. - :http:statuscode:`500 Internal Server error`: - The ``C2ECPaymentNotification`` could not be processed due to server side issues.