commit 5770318d4a4f7a8e668afa7db088776191ccd191
parent eb8f6b2d75dec32f4a8be462484e21d0d1b349f2
Author: Joel-Haeberli <haebu@rubigen.ch>
Date: Thu, 28 Mar 2024 22:10:29 +0100
docs: add feedback
Diffstat:
47 files changed, 1864 insertions(+), 1026 deletions(-)
diff --git a/bruno/c2ec/(LOCAL-BIA) Abort Withdrawal.bru b/bruno/c2ec/(LOCAL-BIA) Abort Withdrawal.bru
@@ -0,0 +1,11 @@
+meta {
+ name: (LOCAL-BIA) Abort Withdrawal
+ type: http
+ seq: 5
+}
+
+post {
+ url: http://localhost:8081/c2ec/withdrawal-operation/WOPID/abort
+ body: none
+ auth: none
+}
diff --git a/bruno/c2ec/(LOCAL-BIA) Payment Confirmation.bru b/bruno/c2ec/(LOCAL-BIA) Payment Confirmation.bru
@@ -0,0 +1,11 @@
+meta {
+ name: (LOCAL-BIA) Payment Confirmation
+ type: http
+ seq: 4
+}
+
+post {
+ url: http://localhost:8081/c2ec/withdrawal-operation/WOPID
+ body: none
+ auth: none
+}
diff --git a/bruno/c2ec/(LOCAL-BIA) Register Withdrawal.bru b/bruno/c2ec/(LOCAL-BIA) Register Withdrawal.bru
@@ -0,0 +1,11 @@
+meta {
+ name: (LOCAL-BIA) Register Withdrawal
+ type: http
+ seq: 2
+}
+
+post {
+ url: http://localhost:8081/c2ec/withdrawal-operation
+ body: none
+ auth: none
+}
diff --git a/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru b/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru
@@ -0,0 +1,11 @@
+meta {
+ name: (LOCAL-BIA) Withdrawal Status
+ type: http
+ seq: 3
+}
+
+get {
+ url: http://localhost:8081/c2ec/withdrawal-operation/WOPID
+ body: none
+ auth: none
+}
diff --git a/bruno/c2ec/(LOCAL-WIRE) Transaction History Incoming.bru b/bruno/c2ec/(LOCAL-WIRE) Transaction History Incoming.bru
@@ -0,0 +1,11 @@
+meta {
+ name: (LOCAL-WIRE) Transaction History Incoming
+ type: http
+ seq: 7
+}
+
+get {
+ url: http://localhost:8081/wire/history/incoming
+ body: none
+ auth: none
+}
diff --git a/bruno/c2ec/(LOCAL-WIRE) Transfer.bru b/bruno/c2ec/(LOCAL-WIRE) Transfer.bru
@@ -0,0 +1,11 @@
+meta {
+ name: (LOCAL-WIRE) Transfer
+ type: http
+ seq: 6
+}
+
+post {
+ url: http://localhost:8081/wire/transfer
+ body: none
+ auth: none
+}
diff --git a/bruno/c2ec/bruno.json b/bruno/c2ec/bruno.json
@@ -0,0 +1,9 @@
+{
+ "version": "1",
+ "name": "c2ec",
+ "type": "collection",
+ "ignore": [
+ "node_modules",
+ ".git"
+ ]
+}
+\ No newline at end of file
diff --git a/bruno/wallee/bruno.json b/bruno/wallee/bruno.json
@@ -0,0 +1,9 @@
+{
+ "version": "1",
+ "name": "wallee",
+ "type": "collection",
+ "ignore": [
+ "node_modules",
+ ".git"
+ ]
+}
+\ No newline at end of file
diff --git a/c2ec/amount.go b/c2ec/amount.go
@@ -0,0 +1,162 @@
+// 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 main
+
+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"`
+}
+
+func ToAmount(amount TalerAmountCurrency) (*Amount, error) {
+
+ a := new(Amount)
+ a.Currency = amount.Curr
+ a.Value = uint64(amount.Val)
+ a.Fraction = uint64(amount.Frac)
+ return a, nil
+}
+
+// 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/c2ec/amount_test.go b/c2ec/amount_test.go
@@ -0,0 +1,69 @@
+// This file is part of taler-go, the Taler Go implementation.
+// Copyright (C) 2022 Martin Schanzenbach
+//
+// Taler Go is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// Taler Go is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package main
+
+import (
+ "fmt"
+ "testing"
+)
+
+var a = Amount{
+ Currency: "EUR",
+ Value: 1,
+ Fraction: 50000000,
+}
+var b = Amount{
+ Currency: "EUR",
+ Value: 23,
+ Fraction: 70007000,
+}
+var c = Amount{
+ Currency: "EUR",
+ Value: 25,
+ Fraction: 20007000,
+}
+
+func TestAmountAdd(t *testing.T) {
+ d, err := a.Add(b)
+ if err != nil {
+ t.Errorf("Failed adding amount")
+ }
+ if c.String() != d.String() {
+ t.Errorf("Failed to add to correct amount")
+ }
+}
+
+func TestAmountSub(t *testing.T) {
+ d, err := c.Sub(b)
+ if err != nil {
+ t.Errorf("Failed substracting amount")
+ }
+ if a.String() != d.String() {
+ t.Errorf("Failed to substract to correct amount")
+ }
+}
+
+func TestAmountLarge(t *testing.T) {
+ x, err := ParseAmount("EUR:50")
+ _, err = x.Add(a)
+ if nil != err {
+ fmt.Println(err)
+ t.Errorf("Failed")
+ }
+}
diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go
@@ -2,9 +2,12 @@ package main
import (
"bytes"
- "c2ec/common"
+ "fmt"
"log"
http "net/http"
+ "strconv"
+ "strings"
+ "time"
)
const BANK_INTEGRATION_CONFIG_ENDPOINT = "/config"
@@ -16,6 +19,9 @@ 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"
+const DEFAULT_LONG_POLL_MS = 1000
+const DEFAULT_OLD_STATE = PENDING
+
// https://docs.taler.net/core/api-exchange.html#tsref-type-CurrencySpecification
type CurrencySpecification struct {
Name string `json:"name"`
@@ -36,24 +42,24 @@ type BankIntegrationConfig struct {
}
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"`
+ Wopid WithdrawalIdentifier `json:"wopid"`
+ ReservePubKey EddsaPublicKey `json:"reserve_pub_key"`
+ Amount Amount `json:"amount"`
+ TerminalId uint64 `json:"terminal_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"`
+ Status WithdrawalOperationStatus `json:"status"`
+ Amount Amount `json:"amount"`
+ SenderWire string `json:"sender_wire"`
+ WireTypes []string `json:"wire_types"`
+ ReservePubKey EddsaPublicKey `json:"selected_reserve_pub"`
}
type C2ECPaymentNotification struct {
- ProviderTransactionId string `json:"provider_transaction_id"`
- Amount common.Amount `json:"amount"`
- Fees common.Amount `json:"fees"`
+ ProviderTransactionId string `json:"provider_transaction_id"`
+ Amount Amount `json:"amount"`
+ Fees Amount `json:"fees"`
}
func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) {
@@ -63,31 +69,31 @@ func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) {
Version: "0:0:1",
}
- serializedCfg, err := common.NewJsonCodec[BankIntegrationConfig]().EncodeToBytes(&cfg)
+ serializedCfg, err := NewJsonCodec[BankIntegrationConfig]().EncodeToBytes(&cfg)
if err != nil {
log.Default().Printf("failed serializing config: %s", err.Error())
- res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
- res.WriteHeader(common.HTTP_OK)
+ res.WriteHeader(HTTP_OK)
res.Write(serializedCfg)
}
func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) {
- jsonCodec := common.NewJsonCodec[C2ECWithdrawRegistration]()
- registration, err := common.ReadStructFromBody[C2ECWithdrawRegistration](req, jsonCodec)
+ jsonCodec := NewJsonCodec[C2ECWithdrawRegistration]()
+ registration, err := 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",
+ err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ TypeUri: 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)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
}
return
}
@@ -95,58 +101,192 @@ func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) {
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",
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_DB_FAILURE",
+ Title: "database 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)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
}
return
}
- res.WriteHeader(common.HTTP_NO_CONTENT)
+ res.WriteHeader(HTTP_NO_CONTENT)
}
// Get status of withdrawal associated with the given WOPID
//
+// # If the
+//
// 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):
+// - provider_id (optional, for c2ec use mandatory):
// The terminal provider requesting for status update.
func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
+ // read and validate request query parameters
+ longPollMilli := DEFAULT_LONG_POLL_MS
+ longPollMilliPtr, err := OptionalQueryParamOrError("long_poll_ms", req, strconv.Atoi)
+ if err != nil {
+ err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_INVALID_PARAMETER",
+ Title: "invalid request parameter",
+ Detail: "the withdrawal status request parameter 'long_poll_ms' is malformed (error: " + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+ if longPollMilliPtr != nil {
+ longPollMilli = *longPollMilliPtr
+ }
+
+ oldState := DEFAULT_OLD_STATE
+ oldStatePtr, err := OptionalQueryParamOrError("old_state", req, ToWithdrawalOpStatus)
+ if err != nil {
+ err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_INVALID_PARAMETER",
+ Title: "invalid request parameter",
+ Detail: "the withdrawal status request parameter 'old_state' is malformed (error: " + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+ if oldStatePtr != nil {
+ oldState = *oldStatePtr
+ }
+
+ // TODO is this needed ? I think there's a better solution
+ // providerId, err := OptionalQueryParamOrError("provider_id", req, strconv.Atoi)
+ // if err != nil {
+ // err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ // TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_INVALID_PARAMETER",
+ // Title: "invalid request parameter",
+ // Detail: "the withdrawal status request parameter 'provider_id' is malformed (error: " + err.Error() + ")",
+ // Instance: req.RequestURI,
+ // })
+ // if err != nil {
+ // res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ // }
+ // return
+ // }
+
+ // read and validate the wopid path parameter
wopid := req.PathValue(WOPID_PARAMETER)
if wopid == "" {
- res.WriteHeader(common.HTTP_BAD_REQUEST)
+ err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_INVALID_PARAMETER",
+ Title: "invalid request path parameter",
+ Detail: "the withdrawal status request path parameter 'wopid' is malformed (error: " + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ // read the withdrawal from the database
+ withdrawal, err := DB.GetWithdrawalByWopid(wopid)
+ if err != nil {
+
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_DB_FAILURE",
+ Title: "database failure",
+ Detail: "the registration of the withdrawal failed due to db failure (error:" + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
return
}
- res.WriteHeader(common.HTTP_OK)
- res.Write(bytes.NewBufferString("retrieved withdrawal status request for wopid=" + wopid).Bytes())
+ // if the old state was supplied and the current status of withdrawal is
+ // different, return the current withdrawal directly.
+ if oldStatePtr != nil {
+ // Only enter listening mode if the old state did not yet change
+ // compared to the current state.
+ if strings.EqualFold(string(oldState), string(withdrawal.WithdrawalStatus)) {
+ // Listen for change from old_state here for a maximal time of long_poll_ms
+ duration := time.Duration(longPollMilli) * time.Millisecond
+ withdrawal, err = DB.AwaitWithdrawalStatusChange(wopid, duration, oldState)
+ if err != nil {
+ err := WriteProblem(res, HTTP_NOT_FOUND, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_LISTEN_FOR_CHANGE_FAILED",
+ Title: fmt.Sprintf("failed while listening for change of status '%s' for withdrawal-operation with wopid=%s", oldState, wopid),
+ Detail: "listening for C2ECWithdrawalStatus object failed (error:" + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+ }
+ }
+
+ // return the C2ECWithdrawalStatus
+ if amount, err := ToAmount(withdrawal.Amount); err != nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_CONVERSION_FAILURE",
+ Title: "conversion failure",
+ Detail: "failed converting amount object (error:" + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ } else {
+ withdrawalStatusBytes, err := NewJsonCodec[C2ECWithdrawalStatus]().EncodeToBytes(&C2ECWithdrawalStatus{
+ Status: withdrawal.WithdrawalStatus,
+ Amount: *amount,
+ })
+ if err != nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_STATUS_CONVERSION_FAILURE",
+ Title: "conversion failure",
+ Detail: "failed converting C2ECWithdrawalStatus object (error:" + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+ res.WriteHeader(HTTP_OK)
+ res.Write(withdrawalStatusBytes)
+ return
+ }
}
func handlePaymentNotification(res http.ResponseWriter, req *http.Request) {
wopid := req.PathValue(WOPID_PARAMETER)
if wopid == "" {
- res.WriteHeader(common.HTTP_BAD_REQUEST)
+ res.WriteHeader(HTTP_BAD_REQUEST)
return
}
- res.WriteHeader(common.HTTP_OK)
+ res.WriteHeader(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.WriteHeader(HTTP_OK)
res.Write(bytes.NewBufferString("retrieved withdrawal operation abortion request").Bytes())
}
@@ -161,22 +301,22 @@ type BankWithdrawalOperationPostRequest struct {
// 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"`
+ Status 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"`
+ Status WithdrawalOperationStatus `json:"status"`
+ Amount 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
@@ -6,5 +6,5 @@ c2ec:
db:
host: "localhost"
port: 5432
- username: "user"
- password: "password"
+ username: "local"
+ password: "local"
diff --git a/c2ec/codec.go b/c2ec/codec.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+)
+
+type Codec[T any] interface {
+ HttpApplicationContentHeader() string
+ Encode(*T) (io.Reader, error)
+ EncodeToBytes(body *T) ([]byte, error)
+ Decode(io.Reader) (*T, error)
+}
+
+type JsonCodec[T any] struct {
+ Codec[T]
+}
+
+func NewJsonCodec[T any]() Codec[T] {
+
+ return new(JsonCodec[T])
+}
+
+func (*JsonCodec[T]) HttpApplicationContentHeader() string {
+ return "application/json"
+}
+
+func (*JsonCodec[T]) Encode(body *T) (io.Reader, error) {
+
+ encodedBytes, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+
+ return bytes.NewReader(encodedBytes), err
+}
+
+func (c *JsonCodec[T]) EncodeToBytes(body *T) ([]byte, error) {
+
+ reader, err := c.Encode(body)
+ if err != nil {
+ return make([]byte, 0), err
+ }
+ buf, err := io.ReadAll(reader)
+ if err != nil {
+ return make([]byte, 0), err
+ }
+ return buf, nil
+}
+
+func (*JsonCodec[T]) Decode(reader io.Reader) (*T, error) {
+
+ body := new(T)
+ err := json.NewDecoder(reader).Decode(body)
+ return body, err
+}
diff --git a/c2ec/codec_test.go b/c2ec/codec_test.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "bytes"
+ "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(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/amount.go b/c2ec/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 `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/c2ec/common/amount_test.go b/c2ec/common/amount_test.go
@@ -1,69 +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 (
- "fmt"
- "testing"
-)
-
-var a = Amount{
- Currency: "EUR",
- Value: 1,
- Fraction: 50000000,
-}
-var b = Amount{
- Currency: "EUR",
- Value: 23,
- Fraction: 70007000,
-}
-var c = Amount{
- Currency: "EUR",
- Value: 25,
- Fraction: 20007000,
-}
-
-func TestAmountAdd(t *testing.T) {
- d, err := a.Add(b)
- if err != nil {
- t.Errorf("Failed adding amount")
- }
- if c.String() != d.String() {
- t.Errorf("Failed to add to correct amount")
- }
-}
-
-func TestAmountSub(t *testing.T) {
- d, err := c.Sub(b)
- if err != nil {
- t.Errorf("Failed substracting amount")
- }
- if a.String() != d.String() {
- t.Errorf("Failed to substract to correct amount")
- }
-}
-
-func TestAmountLarge(t *testing.T) {
- x, err := ParseAmount("EUR:50")
- _, err = x.Add(a)
- if nil != err {
- fmt.Println(err)
- t.Errorf("Failed")
- }
-}
diff --git a/c2ec/common/codec.go b/c2ec/common/codec.go
@@ -1,57 +0,0 @@
-package common
-
-import (
- "bytes"
- "encoding/json"
- "io"
-)
-
-type Codec[T any] interface {
- HttpApplicationContentHeader() string
- Encode(*T) (io.Reader, error)
- EncodeToBytes(body *T) ([]byte, error)
- Decode(io.Reader) (*T, error)
-}
-
-type JsonCodec[T any] struct {
- Codec[T]
-}
-
-func NewJsonCodec[T any]() Codec[T] {
-
- return new(JsonCodec[T])
-}
-
-func (*JsonCodec[T]) HttpApplicationContentHeader() string {
- return "application/json"
-}
-
-func (*JsonCodec[T]) Encode(body *T) (io.Reader, error) {
-
- encodedBytes, err := json.Marshal(body)
- if err != nil {
- return nil, err
- }
-
- return bytes.NewReader(encodedBytes), err
-}
-
-func (c *JsonCodec[T]) EncodeToBytes(body *T) ([]byte, error) {
-
- reader, err := c.Encode(body)
- if err != nil {
- return make([]byte, 0), err
- }
- buf, err := io.ReadAll(reader)
- if err != nil {
- return make([]byte, 0), err
- }
- return buf, nil
-}
-
-func (*JsonCodec[T]) Decode(reader io.Reader) (*T, error) {
-
- body := new(T)
- err := json.NewDecoder(reader).Decode(body)
- return body, err
-}
diff --git a/c2ec/common/codec_test.go b/c2ec/common/codec_test.go
@@ -1,63 +0,0 @@
-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
@@ -1,256 +0,0 @@
-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
@@ -1,62 +0,0 @@
-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
@@ -1,72 +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"
-)
-
-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
@@ -2,8 +2,6 @@ package main
import (
"os"
- "strconv"
- "strings"
"gopkg.in/yaml.v3"
)
@@ -57,20 +55,3 @@ func Parse(path string) (*C2ECConfig, error) {
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
@@ -1,41 +1,68 @@
package main
-import "c2ec/common"
+import "time"
-type TerminalProvider struct {
- ProviderTerminalID int64
- Name string
- BackendBaseURL string
- BackendCredentials string
+const PROVIDER_TABLE_NAME = "provider"
+const PROVIDER_FIELD_NAME_ID = "terminal_id"
+const PROVIDER_FIELD_NAME_NAME = "name"
+const PROVIDER_FIELD_NAME_BACKEND_URL = "backend_base_url"
+const PROVIDER_FIELD_NAME_BACKEND_CREDENTIALS = "backend_credentials"
+
+const TERMINAL_TABLE_NAME = "terminal"
+const TERMINAL_FIELD_NAME_ID = "terminal_id"
+const TERMINAL_FIELD_NAME_ACCESS_TOKEN = "access_token"
+const TERMINAL_FIELD_NAME_ACTIVE = "active"
+const TERMINAL_FIELD_NAME_DESCRIPTION = "description"
+const TERMINAL_FIELD_NAME_PROVIDER_ID = "provider_id"
+
+const WITHDRAWAL_TABLE_NAME = "withdrawal"
+const WITHDRAWAL_FIELD_NAME_ID = "withdrawal_id"
+const WITHDRAWAL_FIELD_NAME_WOPID = "wopid"
+const WITHDRAWAL_FIELD_NAME_RESPUBKEY = "reserve_pub_key"
+const WITHDRAWAL_FIELD_NAME_TS = "registration_ts"
+const WITHDRAWAL_FIELD_NAME_AMOUNT = "amount"
+const WITHDRAWAL_FIELD_NAME_FEES = "fees"
+const WITHDRAWAL_FIELD_NAME_STATUS = "withdrawal_status"
+const WITHDRAWAL_FIELD_NAME_TERMINAL_ID = "terminal_id"
+const WITHDRAWAL_FIELD_NAME_TRANSACTION_ID = "provider_transaction_id"
+const WITHDRAWAL_FIELD_NAME_LAST_RETRY = "last_retry_ts"
+const WITHDRAWAL_FIELD_NAME_RETRY_COUNTER = "retry_counter"
+const WITHDRAWAL_FIELD_NAME_COMPLETION_PROOF = "completion_proof"
+
+type Provider struct {
+ ProviderTerminalID int64 `db:"provider_id"`
+ Name string `db:"name"`
+ BackendBaseURL string `db:"backend_base_url"`
+ BackendCredentials string `db:"backend_credentials"`
}
type Terminal struct {
- TerminalID int64
- AccessToken []byte
- Active bool
- ProviderID int64
- Provider TerminalProvider
- Description string
+ TerminalID int64 `db:"terminal_id"`
+ AccessToken []byte `db:"access_token"`
+ Active bool `db:"active"`
+ Description string `db:"description"`
+ ProviderID int64 `db:"provider_id"`
}
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
+ WithdrawalId []byte `db:"withdrawal_id"`
+ Wopid uint64 `db:"wopid"`
+ ReservePubKey []byte `db:"reserve_pub_key"`
+ RegistrationTs int64 `db:"registration_ts"`
+ Amount TalerAmountCurrency `db:"amount"`
+ Fees TalerAmountCurrency `db:"fees"`
+ WithdrawalStatus WithdrawalOperationStatus `db:"withdrawal_status"`
+ TerminalId int64 `db:"terminal_id"`
+ ProviderTransactionId string `db:"provider_transaction_id"`
+ LastRetryTs int64 `db:"last_retry_ts"`
+ RetryCounter int32 `db:"retry_counter"`
+ CompletionProof []byte `db:"completion_proof"`
}
type TalerAmountCurrency struct {
- Val int64
- Frac int32
- Curr string
+ Val int64 `db:"val"`
+ Frac int32 `db:"frac"`
+ Curr string `db:"curr"`
}
type C2ECDatabase interface {
@@ -43,6 +70,7 @@ type C2ECDatabase interface {
GetWithdrawalByWopid(wopid string) (*Withdrawal, error)
ConfirmPayment(c *C2ECPaymentNotification) error
GetUnconfirmedWithdrawals() ([]*Withdrawal, error)
- GetTerminalProviderById(id int) (*TerminalProvider, error)
+ GetTerminalProviderById(id int) (*Provider, error)
GetTerminalById(id int) (*Terminal, error)
+ AwaitWithdrawalStatusChange(wopid string, duration time.Duration, oldState WithdrawalOperationStatus) (*Withdrawal, error)
}
diff --git a/c2ec/db/0000-c2ec_schema.sql b/c2ec/db/0000-c2ec_schema.sql
@@ -0,0 +1,121 @@
+BEGIN;
+
+SELECT _v.register_patch('0000-c2ec-schema', NULL, NULL);
+
+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 provider (
+ provider_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 provider
+ IS 'Table describing providers of c2ec terminal';
+COMMENT ON COLUMN provider.provider_id
+ IS 'Uniquely identifies a provider';
+COMMENT ON COLUMN provider.name
+ IS 'Name of the provider, used for selection in transaction proofing';
+COMMENT ON COLUMN provider.backend_base_url
+ IS 'URL of the provider backend for transaction proofing';
+COMMENT ON COLUMN 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 provider(provider_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 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 BYTEA
+);
+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';
+
+CREATE INDEX wopid_index ON withdrawal (wopid);
+COMMENT ON INDEX wopid_index
+ IS 'The wopid is the search key for each bank-integration api related request.
+ Thus it makes sense to create an index on the column.';
+
+COMMIT;
diff --git a/c2ec/db/0000-c2ec_status_listener.sql b/c2ec/db/0000-c2ec_status_listener.sql
@@ -0,0 +1,41 @@
+BEGIN;
+
+SELECT _v.register_patch('0000-c2ec-status-listener', ARRAY['0000-c2ec-schema'], NULL);
+
+SET search_path TO c2ec;
+
+-- to create a function, the user needs USAGE privilege on arguments and return types
+CREATE OR REPLACE FUNCTION emit_withdrawal_status()
+RETURNS TRIGGER AS $$
+BEGIN
+ PERFORM pg_notify('withdrawal_' || NEW.withdrawal_id, NEW.withdrawal_status::TEXT);
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+COMMENT ON FUNCTION emit_withdrawal_status
+ IS 'The function selects the withdrawal according to the wopid
+ of the functions argument and sends a notification on the channel
+ "withdrawal-{wopid}" with the status in the payload.';
+
+-- for creating a trigger the user must have TRIGGER pivilege on the table.
+-- to execute the trigger, the user needs EXECUTE privilege on the trigger function.
+CREATE OR REPLACE TRIGGER c2ec_withdrawal_created
+ AFTER INSERT
+ ON withdrawal
+ FOR EACH ROW
+ EXECUTE FUNCTION emit_withdrawal_status();
+COMMENT ON TRIGGER c2ec_withdrawal_created ON withdrawal
+ IS 'After creation of the withdrawal entry a notification shall
+ be triggered using this trigger.';
+
+CREATE OR REPLACE TRIGGER c2ec_withdrawal_changed
+ AFTER UPDATE OF withdrawal_status
+ ON withdrawal
+ FOR EACH ROW
+ WHEN (OLD.withdrawal_status IS DISTINCT FROM NEW.withdrawal_status)
+ EXECUTE FUNCTION emit_withdrawal_status();
+COMMENT ON TRIGGER c2ec_withdrawal_changed ON withdrawal
+ IS 'After the update of the status (only the status is of interest)
+ a notification shall be triggered using this trigger.';
+
+COMMIT;
diff --git a/c2ec/db/versioning.sql b/c2ec/db/versioning.sql
@@ -0,0 +1,294 @@
+-- LICENSE AND COPYRIGHT
+--
+-- Copyright (C) 2010 Hubert depesz Lubaczewski
+--
+-- This program is distributed under the (Revised) BSD License:
+-- L<http://www.opensource.org/licenses/bsd-license.php>
+--
+-- Redistribution and use in source and binary forms, with or without
+-- modification, are permitted provided that the following conditions
+-- are met:
+--
+-- * Redistributions of source code must retain the above copyright
+-- notice, this list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright
+-- notice, this list of conditions and the following disclaimer in the
+-- documentation and/or other materials provided with the distribution.
+--
+-- * Neither the name of Hubert depesz Lubaczewski's Organization
+-- nor the names of its contributors may be used to endorse or
+-- promote products derived from this software without specific
+-- prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+-- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--
+-- Code origin: https://gitlab.com/depesz/Versioning/blob/master/install.versioning.sql
+--
+--
+-- # NAME
+--
+-- **Versioning** - simplistic take on tracking and applying changes to databases.
+--
+-- # DESCRIPTION
+--
+-- This project strives to provide simple way to manage changes to
+-- database.
+--
+-- Instead of making changes on development server, then finding
+-- differences between production and development, deciding which ones
+-- should be installed on production, and finding a way to install them -
+-- you start with writing diffs themselves!
+--
+-- # INSTALLATION
+--
+-- To install versioning simply run install.versioning.sql in your database
+-- (all of them: production, stage, test, devel, ...).
+--
+-- # USAGE
+--
+-- In your files with patches to database, put whole logic in single
+-- transaction, and use \_v.\* functions - usually \_v.register_patch() at
+-- least to make sure everything is OK.
+--
+-- For example. Let's assume you have patch files:
+--
+-- ## 0001.sql:
+--
+-- ```
+-- create table users (id serial primary key, username text);
+-- ```
+--
+-- ## 0002.sql:
+--
+-- ```
+-- insert into users (username) values ('depesz');
+-- ```
+-- To change it to use versioning you would change the files, to this
+-- state:
+--
+-- 0000.sql:
+--
+-- ```
+-- BEGIN;
+-- select _v.register_patch('000-base', NULL, NULL);
+-- create table users (id serial primary key, username text);
+-- COMMIT;
+-- ```
+--
+-- ## 0002.sql:
+--
+-- ```
+-- BEGIN;
+-- select _v.register_patch('001-users', ARRAY['000-base'], NULL);
+-- insert into users (username) values ('depesz');
+-- COMMIT;
+-- ```
+--
+-- This will make sure that patch 001-users can only be applied after
+-- 000-base.
+--
+-- # AVAILABLE FUNCTIONS
+--
+-- ## \_v.register_patch( TEXT )
+--
+-- Registers named patch, or dies if it is already registered.
+--
+-- Returns integer which is id of patch in \_v.patches table - only if it
+-- succeeded.
+--
+-- ## \_v.register_patch( TEXT, TEXT[] )
+--
+-- Same as \_v.register_patch( TEXT ), but checks is all given patches (given as
+-- array in second argument) are already registered.
+--
+-- ## \_v.register_patch( TEXT, TEXT[], TEXT[] )
+--
+-- Same as \_v.register_patch( TEXT, TEXT[] ), but also checks if there are no conflicts with preexisting patches.
+--
+-- Third argument is array of names of patches that conflict with current one. So
+-- if any of them is installed - register_patch will error out.
+--
+-- ## \_v.unregister_patch( TEXT )
+--
+-- Removes information about given patch from the versioning data.
+--
+-- It doesn't remove objects that were created by this patch - just removes
+-- metainformation.
+--
+-- ## \_v.assert_user_is_superuser()
+--
+-- Make sure that current patch is being loaded by superuser.
+--
+-- If it's not - it will raise exception, and break transaction.
+--
+-- ## \_v.assert_user_is_not_superuser()
+--
+-- Make sure that current patch is not being loaded by superuser.
+--
+-- If it is - it will raise exception, and break transaction.
+--
+-- ## \_v.assert_user_is_one_of(TEXT, TEXT, ... )
+--
+-- Make sure that current patch is being loaded by one of listed users.
+--
+-- If ```current_user``` is not listed as one of arguments - function will raise
+-- exception and break the transaction.
+
+BEGIN;
+
+
+-- This file adds versioning support to database it will be loaded to.
+-- It requires that PL/pgSQL is already loaded - will raise exception otherwise.
+-- All versioning "stuff" (tables, functions) is in "_v" schema.
+
+-- All functions are defined as 'RETURNS SETOF INT4' to be able to make them to RETURN literally nothing (0 rows).
+-- >> RETURNS VOID<< IS similar, but it still outputs "empty line" in psql when calling
+CREATE SCHEMA IF NOT EXISTS _v;
+COMMENT ON SCHEMA _v IS 'Schema for versioning data and functionality.';
+
+CREATE TABLE IF NOT EXISTS _v.patches (
+ patch_name TEXT PRIMARY KEY,
+ applied_tsz TIMESTAMPTZ NOT NULL DEFAULT now(),
+ applied_by TEXT NOT NULL,
+ requires TEXT[],
+ conflicts TEXT[]
+);
+COMMENT ON TABLE _v.patches IS 'Contains information about what patches are currently applied on database.';
+COMMENT ON COLUMN _v.patches.patch_name IS 'Name of patch, has to be unique for every patch.';
+COMMENT ON COLUMN _v.patches.applied_tsz IS 'When the patch was applied.';
+COMMENT ON COLUMN _v.patches.applied_by IS 'Who applied this patch (PostgreSQL username)';
+COMMENT ON COLUMN _v.patches.requires IS 'List of patches that are required for given patch.';
+COMMENT ON COLUMN _v.patches.conflicts IS 'List of patches that conflict with given patch.';
+
+CREATE OR REPLACE FUNCTION _v.register_patch( IN in_patch_name TEXT, IN in_requirements TEXT[], in_conflicts TEXT[], OUT versioning INT4 ) RETURNS setof INT4 AS $$
+DECLARE
+ t_text TEXT;
+ t_text_a TEXT[];
+ i INT4;
+BEGIN
+ -- Thanks to this we know only one patch will be applied at a time
+ LOCK TABLE _v.patches IN EXCLUSIVE MODE;
+
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name;
+ IF FOUND THEN
+ RAISE EXCEPTION 'Patch % is already applied!', in_patch_name;
+ END IF;
+
+ t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE patch_name = any( in_conflicts ) );
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Versioning patches conflict. Conflicting patche(s) installed: %.', array_to_string( t_text_a, ', ' );
+ END IF;
+
+ IF array_upper( in_requirements, 1 ) IS NOT NULL THEN
+ t_text_a := '{}';
+ FOR i IN array_lower( in_requirements, 1 ) .. array_upper( in_requirements, 1 ) LOOP
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_requirements[i];
+ IF NOT FOUND THEN
+ t_text_a := t_text_a || in_requirements[i];
+ END IF;
+ END LOOP;
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Missing prerequisite(s): %.', array_to_string( t_text_a, ', ' );
+ END IF;
+ END IF;
+
+ INSERT INTO _v.patches (patch_name, applied_tsz, applied_by, requires, conflicts ) VALUES ( in_patch_name, now(), current_user, coalesce( in_requirements, '{}' ), coalesce( in_conflicts, '{}' ) );
+ RETURN;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[], TEXT[] ) IS 'Function to register patches in database. Raises exception if there are conflicts, prerequisites are not installed or the migration has already been installed.';
+
+CREATE OR REPLACE FUNCTION _v.register_patch( TEXT, TEXT[] ) RETURNS setof INT4 AS $$
+ SELECT _v.register_patch( $1, $2, NULL );
+$$ language sql;
+COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[] ) IS 'Wrapper to allow registration of patches without conflicts.';
+CREATE OR REPLACE FUNCTION _v.register_patch( TEXT ) RETURNS setof INT4 AS $$
+ SELECT _v.register_patch( $1, NULL, NULL );
+$$ language sql;
+COMMENT ON FUNCTION _v.register_patch( TEXT ) IS 'Wrapper to allow registration of patches without requirements and conflicts.';
+
+CREATE OR REPLACE FUNCTION _v.unregister_patch( IN in_patch_name TEXT, OUT versioning INT4 ) RETURNS setof INT4 AS $$
+DECLARE
+ i INT4;
+ t_text_a TEXT[];
+BEGIN
+ -- Thanks to this we know only one patch will be applied at a time
+ LOCK TABLE _v.patches IN EXCLUSIVE MODE;
+
+ t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE in_patch_name = ANY( requires ) );
+ IF array_upper( t_text_a, 1 ) IS NOT NULL THEN
+ RAISE EXCEPTION 'Cannot uninstall %, as it is required by: %.', in_patch_name, array_to_string( t_text_a, ', ' );
+ END IF;
+
+ DELETE FROM _v.patches WHERE patch_name = in_patch_name;
+ GET DIAGNOSTICS i = ROW_COUNT;
+ IF i < 1 THEN
+ RAISE EXCEPTION 'Patch % is not installed, so it can''t be uninstalled!', in_patch_name;
+ END IF;
+
+ RETURN;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.unregister_patch( TEXT ) IS 'Function to unregister patches in database. Dies if the patch is not registered, or if unregistering it would break dependencies.';
+
+CREATE OR REPLACE FUNCTION _v.assert_patch_is_applied( IN in_patch_name TEXT ) RETURNS TEXT as $$
+DECLARE
+ t_text TEXT;
+BEGIN
+ SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'Patch % is not applied!', in_patch_name;
+ END IF;
+ RETURN format('Patch %s is applied.', in_patch_name);
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_patch_is_applied( TEXT ) IS 'Function that can be used to make sure that patch has been applied.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_superuser() RETURNS TEXT as $$
+DECLARE
+ v_super bool;
+BEGIN
+ SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user;
+ IF v_super THEN
+ RETURN 'assert_user_is_superuser: OK';
+ END IF;
+ RAISE EXCEPTION 'Current user is not superuser - cannot continue.';
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_superuser() IS 'Function that can be used to make sure that patch is being applied using superuser account.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_not_superuser() RETURNS TEXT as $$
+DECLARE
+ v_super bool;
+BEGIN
+ SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user;
+ IF v_super THEN
+ RAISE EXCEPTION 'Current user is superuser - cannot continue.';
+ END IF;
+ RETURN 'assert_user_is_not_superuser: OK';
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_not_superuser() IS 'Function that can be used to make sure that patch is being applied using normal (not superuser) account.';
+
+CREATE OR REPLACE FUNCTION _v.assert_user_is_one_of(VARIADIC p_acceptable_users TEXT[] ) RETURNS TEXT as $$
+DECLARE
+BEGIN
+ IF current_user = any( p_acceptable_users ) THEN
+ RETURN 'assert_user_is_one_of: OK';
+ END IF;
+ RAISE EXCEPTION 'User is not one of: % - cannot continue.', p_acceptable_users;
+END;
+$$ language plpgsql;
+COMMENT ON FUNCTION _v.assert_user_is_one_of(TEXT[]) IS 'Function that can be used to make sure that patch is being applied by one of defined users.';
+
+COMMIT;
diff --git a/c2ec/go.mod b/c2ec/go.mod
@@ -2,20 +2,20 @@ module c2ec
go 1.22.0
-require gotest.tools/v3 v3.5.1
+require (
+ github.com/jackc/pgx/v5 v5.5.5
+ gopkg.in/yaml.v3 v3.0.1
+ 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
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/rogpeppe/go-internal v1.6.1 // indirect
golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/sync v0.1.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
@@ -1,35 +1,46 @@
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/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/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
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=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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/http-util.go b/c2ec/http-util.go
@@ -0,0 +1,278 @@
+package main
+
+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
+}
+
+// The function parses a parameter of the query
+// of the request. If the parameter is not present
+// (empty string) it will not create an error and
+// just return nil.
+func OptionalQueryParamOrError[T any](
+ name string,
+ req *http.Request,
+ transform func(s string) (T, error),
+) (*T, error) {
+
+ paramStr := req.URL.Query().Get(name)
+ if paramStr != "" {
+
+ if t, err := transform(paramStr); err != nil {
+ return nil, err
+ } else {
+ return &t, nil
+ }
+ }
+ return nil, 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/http-util_test.go b/c2ec/http-util_test.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "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 := HttpGet(
+ URL_GET,
+ map[string]string{
+ "id": "1",
+ },
+ map[string]string{},
+ 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 := HttpPost(
+ URL_POST,
+ map[string]string{
+ "id": "1",
+ },
+ map[string]string{},
+ &TestStruct{
+ UserId: 1,
+ Id: 1,
+ Title: "TEST",
+ Completed: false,
+ },
+ NewJsonCodec[TestStruct](),
+ NewJsonCodec[TestStruct](),
+ )
+
+ if err != nil {
+ t.Errorf("%s", err.Error())
+ t.FailNow()
+ }
+
+ fmt.Println("res:", res, ", status:", status)
+}
diff --git a/c2ec/model.go b/c2ec/model.go
@@ -0,0 +1,92 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+)
+
+// 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"
+)
+
+func ToWithdrawalOpStatus(s string) (WithdrawalOperationStatus, error) {
+ switch s {
+ case string(PENDING):
+ return PENDING, nil
+ case string(SELECTED):
+ return SELECTED, nil
+ case string(ABORTED):
+ return ABORTED, nil
+ case string(CONFIRMED):
+ return CONFIRMED, nil
+ default:
+ return "", errors.New(fmt.Sprintf("unknown withdrawal operation status '%s'", s))
+ }
+}
+
+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/postgres.go b/c2ec/postgres.go
@@ -1,52 +1,240 @@
package main
import (
+ "context"
"errors"
"time"
- pgx "github.com/jackc/pgx"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgconn"
+ "github.com/jackc/pgx/v5/pgxpool"
)
-const PS_INSERT_WITHDRAWAL = "INSERT INTO withdrawal " +
- "(wopid, reserve_pub_key, registration_ts, amount, terminal_id)" +
- " VALUES ($1, $2, $3, $4, $5)"
+const PS_INSERT_WITHDRAWAL = "INSERT INTO " + WITHDRAWAL_TABLE_NAME + " (" +
+ WITHDRAWAL_FIELD_NAME_WOPID + "," +
+ WITHDRAWAL_FIELD_NAME_RESPUBKEY + "," +
+ WITHDRAWAL_FIELD_NAME_STATUS + "," +
+ WITHDRAWAL_FIELD_NAME_TS + "," +
+ WITHDRAWAL_FIELD_NAME_AMOUNT + "," +
+ WITHDRAWAL_FIELD_NAME_TERMINAL_ID + ")" +
+ " VALUES ($1, $2, $3, $4, $5, $6);"
-const PS_GET_WITHDRAWAL_BY_WOPID = "SELECT * FROM withdrawal WHERE wopid=$1"
+const PS_GET_UNCONFIRMED_WITHDRAWALS = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_STATUS + " != '" + CONFIRMED + "'" +
+ " AND " + WITHDRAWAL_FIELD_NAME_STATUS + " != '" + ABORTED + "';"
+
+const PS_CONFIRM_WITHDRAWAL = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
+ WITHDRAWAL_FIELD_NAME_TRANSACTION_ID + "," +
+ WITHDRAWAL_FIELD_NAME_AMOUNT + "," +
+ WITHDRAWAL_FIELD_NAME_FEES + "," +
+ WITHDRAWAL_FIELD_NAME_COMPLETION_PROOF + ")" +
+ " = ($1, $2, $3, $4)" +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$5"
+
+const PS_GET_WITHDRAWAL_BY_WOPID = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$1"
+
+const PS_GET_PROVIDER_BY_ID = "SELECT * FROM " + PROVIDER_TABLE_NAME +
+ " WHERE " + PROVIDER_FIELD_NAME_ID + "=$1"
+
+const PS_GET_TERMINAL_BY_ID = "SELECT * FROM " + TERMINAL_TABLE_NAME +
+ " WHERE " + TERMINAL_FIELD_NAME_ID + "=$1"
// Postgres implementation of the C2ECDatabase
type C2ECPostgres struct {
C2ECDatabase
- ConnPool *pgx.ConnPool
+ ctx context.Context
+ pool *pgxpool.Pool
}
-func (db *C2ECPostgres) RegisterWithdrawal(r *C2ECWithdrawRegistration) error {
+func NewC2ECPostgres(cfg C2ECDatabseConfig) (*C2ECPostgres, error) {
+
+ ctx := context.Background()
+ db := new(C2ECPostgres)
+
+ dbCfg := pgxpool.Config{
+ ConnConfig: &pgx.ConnConfig{
+ Config: pgconn.Config{
+ Host: cfg.Host,
+ Port: uint16(cfg.Port),
+ User: cfg.Username,
+ Password: cfg.Password,
+ },
+ },
+ }
+
+ pool, err := pgxpool.NewWithConfig(ctx, &dbCfg)
+ if err != nil {
+ return nil, err
+ }
+
+ db.ctx = ctx
+ db.pool = pool
+
+ return db, nil
+}
+
+func (db *C2ECPostgres) RegisterWithdrawal(
+ wopid uint32,
+ resPubKey EddsaPublicKey,
+ amount Amount,
+ terminalId uint64,
+) error {
ts := time.Now()
- res, err := db.ConnPool.Query(
+ res, err := db.pool.Query(
+ db.ctx,
PS_INSERT_WITHDRAWAL,
- r.Wopid,
- r.ReservePubKey,
- ts,
- r.Amount,
- r.ProviderId,
+ wopid,
+ resPubKey,
+ SELECTED,
+ ts.Unix(),
+ amount,
+ terminalId,
)
- defer res.Close()
-
- return err
+ if err != nil {
+ return err
+ }
+ res.Close()
+ return nil
}
func (db *C2ECPostgres) GetWithdrawalByWopid(wopid string) (*Withdrawal, error) {
- return nil, errors.New("not yet implemented")
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_WITHDRAWAL_BY_WOPID,
+ wopid,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ withdrawals, err := pgx.CollectRows(row, pgx.RowToAddrOfStructByName[Withdrawal])
+ if err != nil {
+ return nil, err
+ }
+
+ return withdrawals[0], nil
+ }
+}
+
+func (db *C2ECPostgres) ConfirmPayment(
+ providerTransactionId string,
+ amount Amount,
+ fees Amount,
+ completion_proof []byte,
+ confirmOrAbort WithdrawalOperationStatus,
+) error {
+
+ res, err := db.pool.Query(
+ db.ctx,
+ PS_CONFIRM_WITHDRAWAL,
+ providerTransactionId,
+ amount,
+ fees,
+ completion_proof,
+ confirmOrAbort,
+ )
+ if err != nil {
+ return err
+ }
+ res.Close()
+ return nil
}
-func (db *C2ECPostgres) ConfirmPayment(c *C2ECPaymentNotification) error {
+// TODO this is probably not needed when using the LISTEN / NOTIFY feature
+func (db *C2ECPostgres) GetUnconfirmedWithdrawals(wopid string) ([]*Withdrawal, error) {
+
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_WITHDRAWAL_BY_WOPID,
+ wopid,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
- return errors.New("not yet implemented")
+ withdrawals, err := pgx.CollectRows(row, pgx.RowToAddrOfStructByName[Withdrawal])
+ if err != nil {
+ return nil, err
+ }
+
+ return withdrawals, nil
+ }
}
-func (db *C2ECPostgres) GetUnconfirmedWithdrawals() ([]*Withdrawal, error) {
+func (db *C2ECPostgres) AwaitWithdrawalStatusChange(
+ wopid string,
+ timeout time.Duration,
+ oldState WithdrawalOperationStatus,
+) (*Withdrawal, error) {
+ // TODO ... examples: https://github.com/jackc/pgxlisten/blob/master/pgxlisten_test.go
+ // -> Start a handler listening for the respective withdrawal
+ limitedTimeCtx, cancel := context.WithTimeout(db.ctx, timeout)
+ defer cancel()
+ conn, err := db.pool.Acquire(limitedTimeCtx)
+ if err != nil {
+ return nil, err
+ }
+ conn.Conn().PgConn()
+ conn.Conn().WaitForNotification(limitedTimeCtx)
return nil, errors.New("not yet implemented")
}
+
+func (db *C2ECPostgres) GetTerminalProviderById(id int) (*Provider, error) {
+
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_PROVIDER_BY_ID,
+ id,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ provider, err := pgx.CollectRows(row, pgx.RowToAddrOfStructByName[Provider])
+ if err != nil {
+ return nil, err
+ }
+
+ return provider[0], nil
+ }
+}
+func (db *C2ECPostgres) GetTerminalById(id int) (*Terminal, error) {
+
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_TERMINAL_BY_ID,
+ id,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ terminal, err := pgx.CollectRows(row, pgx.RowToAddrOfStructByName[Terminal])
+ if err != nil {
+ return nil, err
+ }
+
+ return terminal[0], nil
+ }
+}
diff --git a/c2ec/wire-gateway.go b/c2ec/wire-gateway.go
@@ -2,7 +2,6 @@ package main
import (
"bytes"
- "c2ec/common"
"log"
http "net/http"
)
@@ -26,17 +25,17 @@ type WireConfig struct {
// 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"`
+ RequestUid HashCode `json:"request_uid"`
+ Amount Amount `json:"amount"`
+ ExchangeBaseUrl string `json:"exchange_base_url"`
+ Wtid 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"`
+ Timestamp Timestamp `json:"timestamp"`
+ RowId int `json:"row_id"`
}
// https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory
@@ -47,12 +46,12 @@ type IncomingHistory struct {
// 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 string `json:"type"`
+ RowId int `json:"row_id"`
+ Date Timestamp `json:"date"`
+ Amount Amount `json:"amount"`
+ DebitAccount string `json:"debit_account"`
+ ReservePub EddsaPublicKey `json:"reserve_pub"`
}
// https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory
@@ -63,12 +62,12 @@ type OutgoingHistory struct {
// 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"`
+ RowId int `json:"row_id"`
+ Date Timestamp `json:"date"`
+ Amount Amount `json:"amount"`
+ CreditAccount string `json:"credit_account"`
+ Wtid ShortHashCode `json:"wtid"`
+ ExchangeBaseUrl string `json:"exchange_base_url"`
}
func wireGatewayConfig(res http.ResponseWriter, req *http.Request) {
@@ -78,33 +77,33 @@ func wireGatewayConfig(res http.ResponseWriter, req *http.Request) {
Version: "0:0:1",
}
- serializedCfg, err := common.NewJsonCodec[WireConfig]().EncodeToBytes(&cfg)
+ serializedCfg, err := NewJsonCodec[WireConfig]().EncodeToBytes(&cfg)
if err != nil {
log.Default().Printf("failed serializing config: %s", err.Error())
- res.WriteHeader(common.HTTP_INTERNAL_SERVER_ERROR)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
- res.WriteHeader(common.HTTP_OK)
+ res.WriteHeader(HTTP_OK)
res.Write(serializedCfg)
}
func transfer(res http.ResponseWriter, req *http.Request) {
- res.WriteHeader(common.HTTP_OK)
+ res.WriteHeader(HTTP_OK)
res.Write(bytes.NewBufferString("retrieved transfer request").Bytes())
}
func historyIncoming(res http.ResponseWriter, req *http.Request) {
- res.WriteHeader(common.HTTP_OK)
+ res.WriteHeader(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)
+ res.WriteHeader(HTTP_BAD_REQUEST)
}
// ---------------------
@@ -113,18 +112,18 @@ func historyOutgoing(res http.ResponseWriter, req *http.Request) {
// 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"`
+ Amount Amount `json:"amount"`
+ ReservcePub 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"`
+ Timestamp 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)
+ res.WriteHeader(HTTP_BAD_REQUEST)
}
diff --git a/data/c2ec_schema.sql b/data/c2ec_schema.sql
@@ -1,111 +0,0 @@
--- => 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 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 BYTEA
-);
-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/docs/content/architecture/overview.tex b/docs/content/architecture/overview.tex
@@ -2,7 +2,7 @@
\begin{figure}[h]
\centering
- \includegraphics[width=0.7\textwidth]{pictures/diagrams/components_images.png}
+ \includegraphics[width=0.7\textwidth]{pictures/diagrams/components_image.png}
\caption{Involved components and devices}
\label{fig-logo-components}
\end{figure}
@@ -18,14 +18,14 @@ The component diagram shows the components involved by the withdrawal using the
\label{fig-diagram-all-components}
\end{figure}
-The \autoref{fig-diagram-all-components} shows a high level overview of the components involved and how they interact. The numbers in the diagrams are picked up by the description of the steps what is done between the different components:
+The \autoref{fig-diagram-all-components} shows a high level overview of the components involved and how they interact. In an initial step (before the process is effectively started as depicted), the customer or owner of the terminal selects the \textit{Exchange}, which shall be used for the withdrawal. The process is then started. The numbers in the diagrams are picked up by the description of the steps what is done between the different components:
\begin{enumerate}
\item Wallee Terminal requests to be notified when parameters are \textit{selected} by C2EC.
\item The Wallet scans the QR code at the Terminal.
\item The Wallet registers a reserve public key and the \textit{wopid}.
\item The Bank-Integration API of C2EC notifies the Terminal, that the parameters were selected.
- \item The Terminal accepts the credit card of the customer.
+ \item The POS initiates a payment to the account of the GNU Taler Exchange. For the payment the POS terminal requests a payment card and a PIN for authorizing the payment.
\item The Terminal triggers the payment at the Wallee Backend.
\item The Terminal receives the result of the payment.
\begin{enumerate}
@@ -57,7 +57,7 @@ The Terminal initiates the withdrawal leveraging an application which works as f
\begin{enumerate}
\item At startup of the application, the terminal loads the C2EC configuration
- \item When a user wishes to do a withdrawal, the owner of the terminal opens the application and initiates a new withdrawal.
+ \item When a user wishes to do a withdrawal, the owner of the terminal opens the application and initiates a new withdrawal. A withdrawal is basically a funds transfer to the IBAN account of the \textit{Exchange}.
\begin{enumerate}
\item Application creates a \textit{wopid}
\item The application starts long polling at the C2EC and awaits the selection of the reserve parameters mapped to the \textit{wopid}. The parameters are sent by the Wallet to C2EC.
@@ -68,7 +68,8 @@ The Terminal initiates the withdrawal leveraging an application which works as f
\end{enumerate}
\item The user now scans the QR Code using his Wallet.
\item The application receives the notification of the C2EC, that the parameters for the withdrawal were selected.
- \item The Terminal executes the payment (after user presented their credit card, using the Terminal Backend).
+ \item The Terminal executes the payment (after user presented their credit card, using the Terminal Backend).
+ \item The terminal initiate the fund transfer to the \textit{Exchange}. The customer has to authorize the payment by presenting his payment card and authorizing the transaction with his PIN. The terminal processes the payment over the an available connector configured on the \textit{Wallee Backend}. Possible connectors are Master Card, VISA, TWINT, Maestro, Post Finance, and others \cite{wallee-available-connectors}.
\begin{enumerate}
\item It presents the result to the user.
\item It tells the C2EC, that the payment was successful.
diff --git a/docs/content/implementation/c2ec.tex b/docs/content/implementation/c2ec.tex
@@ -0,0 +1,5 @@
+\section{C2EC}
+
+\subsection{Database}
+
+\subsection{Server}
diff --git a/docs/content/implementation/exchange.tex b/docs/content/implementation/exchange.tex
@@ -1 +0,0 @@
-\section{implementing C2EC docs}
-\ No newline at end of file
diff --git a/docs/content/implementation/terminal.tex b/docs/content/implementation/terminal.tex
@@ -1 +1 @@
-\section{implementing terminal docs}
-\ No newline at end of file
+\section{Wallee POS Terminal}
+\ No newline at end of file
diff --git a/docs/content/implementation/wallee.tex b/docs/content/implementation/wallee.tex
@@ -1 +0,0 @@
-\section{implementing wallee docs}
-\ No newline at end of file
diff --git a/docs/content/implementation/wallet.tex b/docs/content/implementation/wallet.tex
@@ -0,0 +1 @@
+\section{Wallet}
+\ No newline at end of file
diff --git a/docs/content/introduction/goal.tex b/docs/content/introduction/goal.tex
@@ -4,7 +4,7 @@ The goal of this thesis is to propose a framework for cashless withdrawals and i
\subsection{C2EC}
-Therefore a new component, named \textbf{C2EC}, will be implemented as part of the Taler Exchange. C2EC will mediate between the Taler Exchange and the terminal provider. This includes checking that the transaction of the debitor reaches the account of the Exchange and therefore the digital currency can be withdrawn by the user, using its Wallet.
+Therefore a new component, named C2EC, will be implemented as part of the Taler Exchange. C2EC will mediate between the Taler Exchange and the terminal provider. This includes checking that the transaction of the debitor reaches the account of the Exchange and therefore the digital currency can be withdrawn by the user, using its Wallet.
-\subsection{Wallee}
-A new app for the payment terminal provider \textbf{Wallee} must be implemented which allows to start the withdrawal using providers facilities. The provider will guarantee through its backend, that the payment was successful. This puts the liability of the payment on the provider of the terminal.
+\subsection{Wallee POS Terminal}
+The Wallee payment terminal, also called Point of Sales (POS) terminal, interfaces with payment cards (Credit Cards, Debit Cards) to make electronic fund transfers, i.e. a fund transfer to a given GNU Taler Exchange. For our purpose, we will extend the functionality of the terminal to initiate the corresponding counter payment from the exchange to the GNU Taler wallet of the payee.
diff --git a/docs/pictures/diagrams/components_image.png b/docs/pictures/diagrams/components_image.png
Binary files differ.
diff --git a/docs/pictures/diagrams/components_images.png b/docs/pictures/diagrams/components_images.png
Binary files differ.
diff --git a/docs/project.bib b/docs/project.bib
@@ -85,6 +85,13 @@
howpublished = {\url{https://app-wallee.com/de-de/doc/api/web-service#refund-service}}
}
+@misc{wallee-available-connectors,
+ author = {Wallee},
+ title = {Payment Connectors},
+ url = {https://app-wallee.com/connectors},
+ howpublished = {\url{https://app-wallee.com/connectors}}
+}
+
@misc{rfc8959,
series = {Request for Comments},
number = 8959,
diff --git a/docs/thesis.pdf b/docs/thesis.pdf
Binary files differ.
diff --git a/docs/thesis.tex b/docs/thesis.tex
@@ -195,9 +195,9 @@
\input{content/architecture/wallee}
\chapter{Implementation}
-\input{content/implementation/exchange}
+\input{content/implementation/c2ec}
\input{content/implementation/terminal}
-\input{content/implementation/wallee}
+\input{content/implementation/wallet}
\chapter{Results}
\input{content/results/discussion}
diff --git a/specs/components_images.odg b/specs/components_images.odg
Binary files differ.