commit 114f40cfc85eae3a0e2140a93b22e9f159af9ec3
parent d3e3d4560b708f5254ecced25f8a62bdd5068cf9
Author: Joel-Haeberli <haebu@rubigen.ch>
Date: Sat, 8 Jun 2024 15:13:21 +0200
docs: feedback conclusion
Diffstat:
21 files changed, 378 insertions(+), 81 deletions(-)
diff --git a/c2ec/amount.go b/c2ec/amount.go
@@ -117,6 +117,36 @@ func toFractionStr(frac int, fractionalDigits int) string {
return leadingZerosStr + strconv.Itoa(frac)
}
+// checks if a < b
+// returns error if the currencies do not match.
+func (a *Amount) IsSmallerThan(b Amount) (bool, error) {
+
+ if !strings.EqualFold(a.Currency, b.Currency) {
+ return false, errors.New("unable tos compare different currencies")
+ }
+
+ if a.Value < b.Value {
+ return true, nil
+ }
+
+ if a.Value == b.Value && a.Fraction < b.Fraction {
+ return true, nil
+ }
+
+ return false, nil
+}
+
+// checks if a = b
+// returns error if the currencies do not match.
+func (a *Amount) IsEqualTo(b Amount) (bool, error) {
+
+ if !strings.EqualFold(a.Currency, b.Currency) {
+ return false, errors.New("unable tos compare different currencies")
+ }
+
+ return a.Value == b.Value && a.Fraction == b.Fraction, nil
+}
+
// 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, fractionalDigits int) (*Amount, error) {
@@ -169,7 +199,7 @@ func (a *Amount) Add(b Amount, fractionalDigits int) (*Amount, error) {
func ParseAmount(s string, fractionDigits int) (*Amount, error) {
if s == "" {
- return &Amount{"", 0, 0}, nil
+ return &Amount{CONFIG.Server.Currency, 0, 0}, nil
}
if !strings.Contains(s, ":") {
diff --git a/c2ec/amount_test.go b/c2ec/amount_test.go
@@ -324,3 +324,100 @@ func TestFloat64ToAmount(t *testing.T) {
}
}
}
+
+func TestIsSmallerThan(t *testing.T) {
+ amnts := []string{
+ "CHF:0",
+ "CHF:0.01",
+ "CHF:0.1",
+ "CHF:10",
+ "CHF:20",
+ "CHF:20.01",
+ "CHF:20.02",
+ "CHF:20.023",
+ }
+ amntsParsed := make([]Amount, 0)
+ for _, a := range amnts {
+ a, err := ParseAmount(a, 3)
+ if err != nil {
+ fmt.Println("failed!", err)
+ t.FailNow()
+ }
+ amntsParsed = append(amntsParsed, *a)
+ }
+
+ for i, current := range amntsParsed {
+ if i == 0 {
+ continue
+ }
+
+ last := amntsParsed[i-1]
+ fmt.Printf("checking: %s < %s\n", last.String(3), current.String(3))
+ if smaller, err := last.IsSmallerThan(current); !smaller || err != nil {
+ fmt.Println("failed!", err)
+ t.FailNow()
+ }
+ }
+}
+
+func TestIsSmallerThanNegative(t *testing.T) {
+ amnts := []string{
+ "EUR:20.05",
+ "EUR:0.05",
+ "EUR:0.05",
+ }
+ amntsParsed := make([]Amount, 0)
+ for _, a := range amnts {
+ a, err := ParseAmount(a, 2)
+ if err != nil {
+ fmt.Println("failed!", err)
+ t.FailNow()
+ }
+ amntsParsed = append(amntsParsed, *a)
+ }
+
+ for i, current := range amntsParsed {
+ if i == 0 {
+ continue
+ }
+
+ last := amntsParsed[i-1]
+ fmt.Printf("checking (negative): %s < %s\n", last.String(2), current.String(2))
+ if smaller, err := last.IsSmallerThan(current); smaller || err != nil {
+ fmt.Println("failed!", err)
+ t.FailNow()
+ }
+ }
+}
+
+func TestIsEqualTo(t *testing.T) {
+ amnts := []string{
+ "CHF:10",
+ "CHF:10.00",
+ "CHF:10.1",
+ "CHF:10.10",
+ "CHF:10.01",
+ "CHF:10.01",
+ }
+ amntsParsed := make([]Amount, 0)
+ for _, a := range amnts {
+ a, err := ParseAmount(a, 2)
+ if err != nil {
+ fmt.Println("failed!", err)
+ t.FailNow()
+ }
+ amntsParsed = append(amntsParsed, *a)
+ }
+
+ doubleJump := 1
+ for doubleJump <= len(amntsParsed) {
+ current := amntsParsed[doubleJump]
+ last := amntsParsed[doubleJump-1]
+ fmt.Printf("checking: %s = %s\n", last.String(2), current.String(2))
+ if equal, err := last.IsEqualTo(current); !equal || err != nil {
+ fmt.Println("failed!", err)
+ t.FailNow()
+ }
+ doubleJump += 2
+ }
+}
diff --git a/c2ec/api-terminals.go b/c2ec/api-terminals.go
@@ -2,6 +2,7 @@ package main
import (
"crypto/rand"
+ "errors"
"fmt"
"net/http"
)
@@ -235,7 +236,40 @@ func handleWithdrawalCheck(res http.ResponseWriter, req *http.Request) {
return
}
- LogInfo("terminals-api", "received check request for provider_transactio_id="+paymentNotification.ProviderTransactionId)
+ exchangeFees, err := parseAmount(CONFIG.Server.WithdrawalFees)
+ if err != nil {
+ LogError("terminals-api", errors.New("unable to parse withdrawal fees - FATAL SHOULD NEVER HAPPEN"))
+ LogError("terminals-api", err)
+ setLastResponseCodeForLogger(HTTP_INTERNAL_SERVER_ERROR)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ // Fees are optional here and since the Exchange can specify
+ // zero fees, the value can be zero as well. The case that the
+ // the terminal sends no fees and the exchange does not charge
+ // fees needs to be covered as compliant request, currently done
+ // by the trmlFees < exchangeFees check.
+ // Check that fees are at least as high as the configured withdrawal fees.
+ // a higher value would indicate that the payment service provider does
+ // also charge fees.
+ // incoming fees >= specified fees
+ if smaller, err := trmlFees.IsSmallerThan(exchangeFees); smaller || err != nil {
+ if err != nil {
+ LogError("terminals-api", err)
+ setLastResponseCodeForLogger(HTTP_BAD_REQUEST)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+ if smaller {
+ LogError("terminals-api", errors.New("terminal did specify uncorrect fees"))
+ setLastResponseCodeForLogger(HTTP_BAD_REQUEST)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+ }
+
+ LogInfo("terminals-api", "received valid check request for provider_transaction_id="+paymentNotification.ProviderTransactionId)
err = DB.NotifyPayment(
wpd,
paymentNotification.ProviderTransactionId,
@@ -286,13 +320,13 @@ func parseAmount(amountStr string) (Amount, error) {
return preventNilAmount(a), nil
}
-func preventNilAmount(a *Amount) Amount {
+func preventNilAmount(exchangeFees *Amount) Amount {
- if a == nil {
+ if exchangeFees == nil {
return Amount{"", 0, 0}
}
- return *a
+ return *exchangeFees
}
func hasConflict(t *TerminalWithdrawalSetup) bool {
diff --git a/c2ec/api-wire-gateway.go b/c2ec/api-wire-gateway.go
@@ -105,7 +105,11 @@ func NewIncomingReserveTransaction(w *Withdrawal) *IncomingReserveTransaction {
}
t.DebitAccount = client.FormatPayto(w)
t.ReservePub = FormatEddsaPubKey(w.ReservePubKey)
- t.RowId = int(w.ConfirmedRowId)
+ if w.ConfirmedRowId == nil {
+ LogError("wire-gateway", fmt.Errorf("expected non-nil confirmed_row_id for withdrawal_row_id=%d", w.WithdrawalRowId))
+ return nil
+ }
+ t.RowId = int(*w.ConfirmedRowId)
t.Type = INCOMING_RESERVE_TRANSACTION_TYPE
return t
}
@@ -123,7 +127,11 @@ func NewOutgoingBankTransaction(tr *Transfer) *OutgoingBankTransaction {
}
t.CreditAccount = tr.CreditAccount
t.ExchangeBaseUrl = tr.ExchangeBaseUrl
- t.RowId = uint64(tr.TransferredRowId)
+ if tr.TransferredRowId == nil {
+ LogError("wire-gateway", fmt.Errorf("expected non-nil transferred_row_id for row_id=%d", tr.RowId))
+ return nil
+ }
+ t.RowId = uint64(*tr.TransferredRowId)
t.Wtid = ShortHashCode(tr.Wtid)
return t
}
@@ -204,6 +212,31 @@ func transfer(res http.ResponseWriter, req *http.Request) {
}
if t == nil {
+
+ // limitation: currently only full refunds are implemented.
+ // this means that we also check that no other transaction
+ // to the same recipient with this credit_account is present.
+ transfers, err := DB.GetTransfersByCreditAccount(transfer.CreditAccount)
+ if err != nil {
+ LogWarn("wire-gateway-api", "looking for transfers with the credit account failed")
+ LogError("wire-gateway-api", err)
+ setLastResponseCodeForLogger(HTTP_INTERNAL_SERVER_ERROR)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ if len(transfers) > 0 {
+ // when the withdrawal was already refunded we act like everything is
+ // ok, because the transfer was registered earlier and the customer
+ // will get their money back (or already have). The Exchange will
+ // not loose money on the other hand because the refund is done twice.
+ LogWarn("wire-gateway-api", "full refunds only limitation")
+ LogError("wire-gateway-api", fmt.Errorf("currently only full refunds are supported. Withdrawal %s already refunded", transfer.CreditAccount))
+ setLastResponseCodeForLogger(HTTP_OK)
+ res.WriteHeader(HTTP_OK)
+ return
+ }
+
// no transfer for this request_id -> generate new
amount, err := ParseAmount(transfer.Amount, CONFIG.Server.CurrencyFractionDigits)
if err != nil {
@@ -229,6 +262,16 @@ func transfer(res http.ResponseWriter, req *http.Request) {
return
}
} else {
+
+ // check that the wanted provider is configured.
+ refundClient := PROVIDER_CLIENTS[p.Name]
+ if refundClient == nil {
+ LogError("wire-gateway-api", errors.New("client for provider "+p.Name+" not initialized"))
+ setLastResponseCodeForLogger(HTTP_INTERNAL_SERVER_ERROR)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
// the transfer is only processed if the body matches.
ta, err := ToAmount(t.Amount)
if err != nil {
@@ -256,14 +299,6 @@ func transfer(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
-
- refundClient := PROVIDER_CLIENTS[p.Name]
- if refundClient == nil {
- LogError("wire-gateway-api", errors.New("client for provider "+p.Name+" not initialized"))
- setLastResponseCodeForLogger(HTTP_INTERNAL_SERVER_ERROR)
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- return
- }
}
setLastResponseCodeForLogger(HTTP_OK)
}
@@ -438,13 +473,13 @@ func historyOutgoing(res http.ResponseWriter, req *http.Request) {
} else {
if deltaPtr != nil {
delta = *deltaPtr
- } else {
- // this means parameter was not given.
- // no long polling (simple get)
- shouldStartLongPoll = false
}
}
+ if delta == 0 {
+ delta = 10
+ }
+
if shouldStartLongPoll {
// this will just wait / block until the milliseconds are exceeded.
@@ -478,6 +513,7 @@ func historyOutgoing(res http.ResponseWriter, req *http.Request) {
for _, t := range filtered {
transactions = append(transactions, NewOutgoingBankTransaction(t))
}
+ transactions = removeNulls(transactions)
outgoingHistory := OutgoingHistory{
OutgoingTransactions: transactions,
diff --git a/c2ec/config.go b/c2ec/config.go
@@ -85,6 +85,14 @@ func Parse(path string) (*C2ECConfig, error) {
return nil, err
}
+ a, err := ParseAmount(cfg.Server.WithdrawalFees, cfg.Server.CurrencyFractionDigits)
+ if err != nil {
+ panic("invalid withdrawal fees amount")
+ }
+ if !strings.EqualFold(a.Currency, cfg.Server.Currency) {
+ panic("withdrawal fees currency must be same as the specified currency")
+ }
+
return cfg, nil
}
diff --git a/c2ec/db-postgres.go b/c2ec/db-postgres.go
@@ -83,6 +83,9 @@ const PS_GET_TERMINAL_BY_ID = "SELECT * FROM " + TERMINAL_TABLE_NAME +
const PS_GET_TRANSFER_BY_ID = "SELECT * FROM " + TRANSFER_TABLE_NAME +
" WHERE " + TRANSFER_FIELD_NAME_ID + "=$1"
+const PS_GET_TRANSFER_BY_CREDIT_ACCOUNT = "SELECT * FROM " + TRANSFER_TABLE_NAME +
+ " WHERE " + TRANSFER_FIELD_NAME_CREDIT_ACCOUNT + "=$1"
+
const PS_ADD_TRANSFER = "INSERT INTO " + TRANSFER_TABLE_NAME +
" (" + TRANSFER_FIELD_NAME_ID + ", " + TRANSFER_FIELD_NAME_AMOUNT + ", " +
TRANSFER_FIELD_NAME_EXCHANGE_BASE_URL + ", " + TRANSFER_FIELD_NAME_WTID + ", " +
@@ -420,9 +423,15 @@ func (db *C2ECPostgres) FinaliseWithdrawal(
return errors.New("can only finalise payment when new status is either confirmed or aborted")
}
+ query := PS_FINALISE_PAYMENT
+ if withdrawalId <= 1 {
+ // tweak to intially set confirmed_row_id. Can be removed once confirmed_row_id field is obsolete
+ query = "UPDATE c2ec.withdrawal SET (withdrawal_status,completion_proof,confirmed_row_id) = ($1,$2,1) WHERE withdrawal_row_id=$3"
+ }
+
_, err := db.pool.Exec(
db.ctx,
- PS_FINALISE_PAYMENT,
+ query,
confirmOrAbort,
completionProof,
withdrawalId,
@@ -431,7 +440,7 @@ func (db *C2ECPostgres) FinaliseWithdrawal(
LogError("postgres", err)
return err
}
- LogInfo("postgres", "query="+PS_FINALISE_PAYMENT)
+ LogInfo("postgres", "query="+query)
return nil
}
@@ -688,6 +697,37 @@ func (db *C2ECPostgres) GetTransferById(requestUid []byte) (*Transfer, error) {
}
}
+func (db *C2ECPostgres) GetTransfersByCreditAccount(creditAccount string) ([]*Transfer, error) {
+
+ if rows, err := db.pool.Query(
+ db.ctx,
+ PS_GET_TRANSFER_BY_CREDIT_ACCOUNT,
+ creditAccount,
+ ); err != nil {
+ LogWarn("postgres", "failed query="+PS_GET_TRANSFER_BY_CREDIT_ACCOUNT)
+ LogError("postgres", err)
+ if rows != nil {
+ rows.Close()
+ }
+ return nil, err
+ } else {
+
+ defer rows.Close()
+
+ transfers, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Transfer])
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return make([]*Transfer, 0), nil
+ }
+ LogError("postgres", err)
+ return nil, err
+ }
+
+ LogInfo("postgres", "query="+PS_GET_TRANSFER_BY_CREDIT_ACCOUNT)
+ return removeNulls(transfers), nil
+ }
+}
+
func (db *C2ECPostgres) AddTransfer(
requestUid []byte,
amount *Amount,
@@ -723,26 +763,33 @@ func (db *C2ECPostgres) AddTransfer(
}
func (db *C2ECPostgres) UpdateTransfer(
+ rowId int,
requestUid []byte,
timestamp int64,
status int16,
retries int16,
) error {
+ query := PS_UPDATE_TRANSFER
+ if rowId <= 1 {
+ // tweak to intially set transferred_row_id. Can be removed once transferred_row_id field is obsolete
+ query = "UPDATE c2ec.transfer SET (transfer_ts, transfer_status, retries, transferred_row_id) = ($1,$2,$3,1) WHERE request_uid=$4"
+ }
+
_, err := db.pool.Exec(
db.ctx,
- PS_UPDATE_TRANSFER,
+ query,
timestamp,
status,
retries,
requestUid,
)
if err != nil {
- LogInfo("postgres", "failed query="+PS_UPDATE_TRANSFER)
+ LogInfo("postgres", "failed query="+query)
LogError("postgres", err)
return err
}
- LogInfo("postgres", "query="+PS_UPDATE_TRANSFER)
+ LogInfo("postgres", "query="+query)
return nil
}
@@ -857,12 +904,6 @@ func (db *C2ECPostgres) NewListener(
) (func(context.Context) error, error) {
connectionString := PostgresConnectionString(&CONFIG.Database)
- // pgHost := os.Getenv("PGHOST")
- // if pgHost != "" {
- // LogInfo("postgres", "pghost was set")
- // connectionString = pgHost
- // }
-
cfg, err := pgx.ParseConfig(connectionString)
if err != nil {
return nil, err
diff --git a/c2ec/db.go b/c2ec/db.go
@@ -66,7 +66,7 @@ type Terminal struct {
type Withdrawal struct {
WithdrawalRowId uint64 `db:"withdrawal_row_id"`
- ConfirmedRowId uint64 `db:"confirmed_row_id"`
+ ConfirmedRowId *uint64 `db:"confirmed_row_id"`
RequestUid string `db:"request_uid"`
Wopid []byte `db:"wopid"`
ReservePubKey []byte `db:"reserve_pub_key"`
@@ -90,7 +90,7 @@ type TalerAmountCurrency struct {
type Transfer struct {
RowId int `db:"row_id"`
- TransferredRowId int `db:"transferred_row_id"`
+ TransferredRowId *int `db:"transferred_row_id"`
RequestUid []byte `db:"request_uid"`
Amount *TalerAmountCurrency `db:"amount"`
ExchangeBaseUrl string `db:"exchange_base_url"`
@@ -213,6 +213,7 @@ type C2ECDatabase interface {
// Updates the transfer, if retries is changed, the transfer will be
// triggered again.
UpdateTransfer(
+ rowId int,
requestUid []byte,
timestamp int64,
status int16,
@@ -226,6 +227,16 @@ type C2ECDatabase interface {
// id shall be used as starting point.
GetTransfers(start int, delta int, since time.Time) ([]*Transfer, error)
+ // Load all transfers asscociated with the same credit_account.
+ // The query is used to control that the current limitation of
+ // only allowing full refunds (partial refunds are currently not supported)
+ // is not harmed. It is assumed that the credit_account is unique, which currently
+ // is the case, because it depends on the WOPID of the respective
+ // withdrawal. This query is part of the limitation to only allow
+ // full refunds and not partial refunds. It might be possible to
+ // remove this API when partial refunds are implemented.
+ GetTransfersByCreditAccount(creditAccount string) ([]*Transfer, error)
+
// Returns the transfer entries in the given state.
// This can be used for retry operations.
GetTransfersByState(status int) ([]*Transfer, error)
diff --git a/c2ec/main.go b/c2ec/main.go
@@ -8,7 +8,6 @@ import (
http "net/http"
"os"
"os/signal"
- "strings"
"syscall"
"time"
)
@@ -75,13 +74,6 @@ func main() {
if cfg == nil {
panic("config is nil")
}
- a, err := ParseAmount(cfg.Server.WithdrawalFees, cfg.Server.CurrencyFractionDigits)
- if err != nil {
- panic("invalid withdrawal fees amount")
- }
- if !strings.EqualFold(a.Currency, cfg.Server.Currency) {
- panic("withdrawal fees currency must be same as the specified currency")
- }
CONFIG = *cfg
DB, err = setupDatabase(&CONFIG.Database)
diff --git a/c2ec/proc-attestor.go b/c2ec/proc-attestor.go
@@ -92,6 +92,9 @@ func finaliseOrSetRetry(
prepareRetryOrAbort(withdrawalRowId, errs)
return
} else {
+ if w.WithdrawalStatus == CONFIRMED || w.WithdrawalStatus == ABORTED {
+ return
+ }
if err := transaction.Confirm(w); err != nil {
LogError("proc-attestor", err)
errs <- err
diff --git a/c2ec/proc-transfer.go b/c2ec/proc-transfer.go
@@ -177,6 +177,7 @@ func executePendingTransfers(errs chan error, lastlog time.Time) {
LogInfo("proc-transfer", "setting transfer to success state")
err = DB.UpdateTransfer(
+ t.RowId,
t.RequestUid,
time.Now().Unix(),
TRANSFER_STATUS_SUCCESS, // success
@@ -195,6 +196,7 @@ func transferFailed(
) {
err := DB.UpdateTransfer(
+ transfer.RowId,
transfer.RequestUid,
time.Now().Unix(),
TRANSFER_STATUS_RETRY, // retry transfer.
diff --git a/c2ec/wallee-client.go b/c2ec/wallee-client.go
@@ -61,6 +61,38 @@ func (wt *WalleeTransaction) Confirm(w *Withdrawal) error {
return errors.New("the merchant reference does not match the withdrawal")
}
+ amountFloatFrmt := strconv.FormatFloat(wt.CompletedAmount, 'f', CONFIG.Server.CurrencyFractionDigits, 64)
+ LogInfo("wallee-client", fmt.Sprintf("converted %f (float) to %s (string)", wt.CompletedAmount, amountFloatFrmt))
+ completedAmountStr := fmt.Sprintf("%s:%s", CONFIG.Server.Currency, amountFloatFrmt)
+ completedAmount, err := ParseAmount(completedAmountStr, CONFIG.Server.CurrencyFractionDigits)
+ if err != nil {
+ LogError("wallee-client", err)
+ return err
+ }
+
+ withdrawAmount, err := ToAmount(w.Amount)
+ if err != nil {
+ return err
+ }
+ withdrawFees, err := ToAmount(w.TerminalFees)
+ if err != nil {
+ return err
+ }
+ if completedAmountMinusFees, err := completedAmount.Sub(*withdrawFees, CONFIG.Server.CurrencyFractionDigits); err == nil {
+ if smaller, err := completedAmountMinusFees.IsSmallerThan(*withdrawAmount); smaller || err != nil {
+
+ if err != nil {
+ return err
+ }
+
+ return fmt.Errorf("the confirmed amount (%s) minus the fees (%s) was smaller than the withdraw amount (%s)",
+ completedAmountStr,
+ withdrawFees.String(CONFIG.Server.CurrencyFractionDigits),
+ withdrawAmount.String(CONFIG.Server.CurrencyFractionDigits),
+ )
+ }
+ }
+
return nil
}
@@ -199,22 +231,7 @@ func (w *WalleeClient) Refund(transactionId string) error {
return err
}
- amountFloatFrmt := strconv.FormatFloat(decodedWalleeTransaction.CompletedAmount, 'f', CONFIG.Server.CurrencyFractionDigits, 64)
- LogInfo("wallee-client", fmt.Sprintf("converted %f (float) to %s (string)", decodedWalleeTransaction.CompletedAmount, amountFloatFrmt))
- amountWithFeesStr := fmt.Sprintf("%s:%s", CONFIG.Server.Currency, amountFloatFrmt)
- amountWithFees, err := ParseAmount(amountWithFeesStr, CONFIG.Server.CurrencyFractionDigits)
- if err != nil {
- LogError("wallee-client", err)
- return err
- }
-
- fees, err := ParseAmount(CONFIG.Server.WithdrawalFees, CONFIG.Server.CurrencyFractionDigits)
- if err != nil {
- LogError("wallee-client", err)
- return err
- }
-
- refundAmount, err := amountWithFees.Sub(*fees, CONFIG.Server.CurrencyFractionDigits)
+ refundAmount, err := ToAmount(withdrawal.Amount)
if err != nil {
LogError("wallee-client", err)
return err
diff --git a/cli/README b/cli/README
@@ -2,7 +2,7 @@
This cli allows adding Providers and Terminals to the database and deactivating terminals, using the command line.
-Before using the commands which connect to the database, you first need to connect to the database (can either be done setting `PGHOST` or within the CLI using the `db` command).
+Before using the commands which connect to the database, you first need to connect to the database (can either be done using `-c [PATH-TO-INI-CONFIG]` or within the CLI using the `db` command).
The CLI will take care of generating the access tokens for the newly registered terminal and hashing the authorization key of the provider backend.
@@ -22,10 +22,4 @@ You can set `PGHOST` to connect automatically to the database.
## Actions
-The cli offers following capabilities:
-
-Registering Wallee Provider: `rp`
-
-Registering a Wallee Termina: `rt`
-
-Deactivating a Terminal: `dt`
+type `./c2ec -h` to get all available actions
diff --git a/cli/cli.go b/cli/cli.go
@@ -27,6 +27,7 @@ const ACTION_DEACTIVATE_TERMINAL = "dt"
const ACTION_ACTIVATE_TERMINAL = "at"
const ACTION_WITHDRAWAL_INFOMRATION = "w"
const ACTION_WITHDRAWAL_INFOMRATION_BY_PTID = "wp"
+const ACTION_PROVIDER_CREDENTIALS = "wc"
const ACTION_CONNECT_DB = "db"
const ACTION_QUIT = "q"
@@ -128,11 +129,7 @@ func parseDbConnstrFromIni(path string) (string, error) {
return value.String(), nil
}
-func registerWalleeProvider() error {
-
- if DB == nil {
- return errors.New("connect to the database first (cmd: db)")
- }
+func walleePrepareCredentials() (string, string, string, string, error) {
name := "Wallee"
paytotargettype := "wallee-transaction"
@@ -140,17 +137,17 @@ func registerWalleeProvider() error {
spaceIdStr := read("Wallee Space Id: ")
spaceId, err := strconv.Atoi(spaceIdStr)
if err != nil {
- return err
+ return "", "", "", "", err
}
userIdStr := read("Wallee User Id: ")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
- return err
+ return "", "", "", "", err
}
key := read("Wallee Application User Key: ")
hashedKey, err := pbkdf(key)
if err != nil {
- return err
+ return "", "", "", "", err
}
creds, err := NewJsonCodec[WalleeCredentials]().EncodeToBytes(&WalleeCredentials{
@@ -159,10 +156,39 @@ func registerWalleeProvider() error {
ApplicationUserKey: hashedKey,
})
if err != nil {
- return err
+ return "", "", "", "", err
}
credsEncoded := base64.StdEncoding.EncodeToString(creds)
+ return name, paytotargettype, backendUrl, credsEncoded, nil
+}
+
+func generateProviderCredentials() error {
+
+ name, paytottargettype, backendUrl, credsEncoded, err := walleePrepareCredentials()
+ if err != nil {
+ return err
+ }
+
+ fmt.Println("provider-name:", name)
+ fmt.Println("provider-payto-target-type:", paytottargettype)
+ fmt.Println("provider-backend-url:", backendUrl)
+ fmt.Println("base64 encoded credentials (can be added to the database like this):", credsEncoded)
+
+ return nil
+}
+
+func registerWalleeProvider() error {
+
+ if DB == nil {
+ return errors.New("connect to the database first (cmd: db)")
+ }
+
+ name, paytotargettype, backendUrl, credsEncoded, err := walleePrepareCredentials()
+ if err != nil {
+ return err
+ }
+
_, err = DB.Exec(
context.Background(),
INSERT_PROVIDER,
@@ -524,6 +550,7 @@ func showHelp() error {
fmt.Println("setup simulation (", ACTION_SETUP_SIMULATION, ")")
fmt.Println("withdrawal information by wopid (", ACTION_WITHDRAWAL_INFOMRATION, ")")
fmt.Println("witdhrawal information by provider transaction id (", ACTION_WITHDRAWAL_INFOMRATION_BY_PTID, ")")
+ fmt.Println("create wallee provider credentials string (", ACTION_PROVIDER_CREDENTIALS, ")")
if DB == nil {
fmt.Println("connect database (", ACTION_CONNECT_DB, ")")
}
@@ -597,6 +624,8 @@ func dispatchCommand(cmd string) error {
err = withdrawalInformationByWopid()
case ACTION_WITHDRAWAL_INFOMRATION_BY_PTID:
err = withdrawalInformationByProviderTransactionId()
+ case ACTION_PROVIDER_CREDENTIALS:
+ err = generateProviderCredentials()
case ACTION_SETUP_SIMULATION:
err = setupSimulation()
default:
diff --git a/docs/content/appendix/api-terminals.pdf b/docs/content/appendix/api-terminals.pdf
Binary files differ.
diff --git a/docs/content/implementation/a-fees.tex b/docs/content/implementation/a-fees.tex
@@ -1,11 +1,11 @@
\subsection{Fees}
\label{sec-implementation-fees}
-During the implementation it became obvious that there are several possible fee models, when thinking about the general case. Using a payment service provider to withdraw money, is special because if you want to withdraw 10 CHF then you want 10 CHF in your wallet and not 10 CHF minus the fees. Think of buying a bar of chocolate. You don't care if you pay 10 CHF or 10.10 CHF because in the end you get what you want - a bar of chocolate. When withdrawing money using the credit card you want the amount in your wallet you are asking for - not less. The fees must be transparently communicated to the customer, that they understand why the authorized amount will be higher than the amount they are asking to withdraw. The fees must be calculated before hand. This leads to different models to add fees. These four models were discovered:
+During the implementation it became obvious that there are several possible fee models, when thinking about the general case. Think of buying a bar of chocolate. From the perspective of getting what you want, you don't care if you pay 10 CHF or 10.10 CHF because in the end you get what you want - a bar of chocolate. Using a payment service provider to withdraw money, is special because if you want to withdraw 10 CHF then you want 10 CHF in your wallet and not 10 CHF minus the fees. When withdrawing money using the credit card you want the amount in your wallet you are asking for - not less. The fees must be transparently communicated to the customer, that they understand why the authorized amount will be higher than the amount they are asking to withdraw. The fees must be calculated in advance whenever possible. This leads to different models to add up fees. These four models were discovered:
\subsubsection{Model 1 - Exchange Operator Fees}
-The payment system provider will charge its business users in the background and expects the Exchange operator to calculate the fees to cover its costs. This means that the Exchange operator will specify its fees by approximating its costs to operate and maintain C2EC. Additionally a certain amount could be added on top of the calculated fees to make a profit.
+The payment system provider will charge its contractors (the merchants) in the background and expects the Exchange operator to calculate the fees to cover its costs. This means that the Exchange operator will specify its fees by approximating its costs to operate and maintain C2EC. Additionally a certain amount could be added on top of the calculated fees to make a profit.
\subsubsection{Model 2 - Payment Service Provider Fees}
@@ -22,4 +22,4 @@ A payment service provider might add fees but for some reason cannot tell them t
\subsubsection{Mixing models}
-It could be a problem when mixing the different models in one instance of C2EC, because it could lead to conflicts. For example if a provider using the model 1 and a provider using the model 2 is operated within the same instance this could lead to more fees for the provider using model 2, since the fees of the Exchange operator should not be considered. In the future it might be possible to handle various fee models in one instance. This would require the implementation of all models. For the thesis model 1 was implemented because Wallee uses this model. These models also resolve the discussion of the midterm meeting with Dr. Alain Hiltgen.
+It could be a problem when mixing the different models in one instance of C2EC, because it could lead to conflicts. For example if a provider using the model one and a provider using the model two is operated within the same instance this could lead to more fees for the provider using model 2, since the fees of the Exchange operator should not be considered. In the future it might be possible to handle various fee models in one instance. This would require the implementation of all models.
diff --git a/docs/content/implementation/d-security.tex b/docs/content/implementation/d-security.tex
@@ -14,7 +14,7 @@ Further a \textit{WOPID} can be abused triggering a confirmation or an abort req
This case is possible, when an attacker can trick the C2EC to have confirmed withdrawals in its withdrawal table, without having a real confirmation of the payment service provider. This means the attacker can steal money from the exchange. For this an attacker would need to have the possibility to somehow trick the confirmation process of C2EC to issue confirmation requestes against a backend controlled by the attacker. This backend would then confirm the withdrawal. This will lead to the creation of the reserve on the side of the Exchange.
-\subsubsection{Developer Issues}
+\subsubsection{Implementation Issues}
Another problem could be developers introducing confirmation bugs. The confirmation process of a transaction must be considered as the holy grail from the perspective of the developers. If they do not take biggest care implementing the confirmation process, this could lead to loss of money on the side of the Exchange operator. The program should strictly disallow withdrawals, if the transaction is not guaranteed to be final by the payment system provider. Otherwise the property of the guarantees concerning the finality is harmed and the system no longer secure (in terms of money). When adding new integrations, this section of the code needs great care and review before going to production.
diff --git a/docs/content/results/discussion.tex b/docs/content/results/discussion.tex
@@ -8,6 +8,8 @@ The implementation of the existing Bank-Integration and Wire-Gateway API were a
A challenge which was encountered during the implementation of the terminal application and the C2EC component, was the concurrency of processes. To make the withdrawal flow as easy and useful as possible, a lot of tasks need to be covered in the background and run besides each other. This added the technical requirement to decouple steps and leverage retries to increase the robustness of the process. It helped a lot to understand that the state of a withdrawal was the anchor these retry mechanisms must be built around.
+Fees are a central aspect of the process and decide wether the implementation can be used or not. The different fee models of \autoref{sec-implementation-fees} describe how fees can add up during a withdrawal. The current implementation does not cover all fee models, because fee models two to four also depend on the payment system provider used for a specific withdrawal. The checks that C2EC can make to secure its own fees are implemented. Fee model one seems to be the most secure and easy to implement fee model. It can be covered by the core implementation of C2EC and does not rely on the payment system provider specific implementation. Using other fee models requires great care during the integration and adds complexity. For the thesis, model one was implemented because Wallee uses this model. These models also resolve the discussion of the midterm meeting with Dr. Alain Hiltgen.
+
Towards the end of the implementation it became obvious that a simple authorization was not enough to imitate the real time feeling of the withdrawal. Other requests were necessary to do so. To findout which requests needed to be filed against the Wallee backend some investigation had to be made. The documentation does explain which states exists in Wallee's transaction scheme but does not explain, which operation must be triggered to transition states. This made the investigation somewhat cumbersome. Also the integration of the backend needed more investigation than assumed. This also led to the
Our work makes a faster uptake of GNU Taler possible. Potential customers will not need a bank account or other things to withdraw digital cash. They can now use C2EC and the terminal app for Wallee to withdraw digital cash using GNU Taler.
@@ -31,10 +33,11 @@ Due to the short time available during the thesis, features and integrations are
\item Paydroid app: Run a Wallee terminal on behalf of the BFH.
\item Paydroid app: The app must be released including the credentials. This is a security risk since these credentials are shipped through (secure?) channels. A way to register to an exchange in the app is a nice extension.
\item C2EC: Remove doubled provider structures. Currently providers are saved to the database and must be configured in the configuration. To make the setup and management easier, the providers can only be configured inside the configuration.
- \item C2EC: Proper separation of confirmed and unconfirmed withdrawals to reduce complexity of the implementation.
+ \item C2EC: Proper separation of confirmed and unconfirmed withdrawals and transferred and untransferred refunds to reduce complexity of the implementation.
\item C2EC: Only one provider per instance is allowed to use the same payto target-type. Currently an additional instance must be configured, if two or more payment service providers are using the same payto target type.
\item Implement more fee models: To allow easier integration of other providers, the described fee models can be centralized in one location. This would help to improve the quality and robustness of the system.
\item Database locking: Currently no database locking is used. This can lead to race conditions. To completely prevent this locks can be applied.
\item IPv6 support: The process must also listen on IPv6 addresses.
\item Support cli without interaction. The cli currently is purely interactive. It would be a nice improvement if the cli would be usable without interactio. This would allow to use the cli for automated tasks.
+ \item Partial refunds: The current implementation only allows refunds of the entire withdrawal amount. In the future the implementation can support partial refunds.
\end{enumerate}
diff --git a/docs/content/results/reflexion.tex b/docs/content/results/reflexion.tex
@@ -34,4 +34,4 @@ The world of payment systems seems a bit chaotic to me. I think this is the resu
The thesis was constrained with a lot of insecurities for me. How does the process look? How can I implement the process? How does GNU Taler even work? How does Wallee work? In the end I am proud of what I accomplished during the thesis. I was able to understand the different API and write a program which fulfills the properties needed for the withdrawal. Additionally I could learn a lot about designing an API and especially parallelization in Go and Android.
-I am thankful that the Bern University of Applied Sciences supports free software projects like GNU Taler. It was a great opportunity for me as student and as human to gain direct insights and work on a GNU project during my thesis. I remember Prof. Dr. Christian Grothoff telling me during an onsite session: "Nicht so kurzfristig denken!" (don't think short-term). This also showed the horizon of the project to me. It tries to sustainably change the payment landscape for good. That is what I like the most about free software. It is built to last. The world will not get better when we keep pushing towards short-term profit benefiting individuals, global warming and war. GNU Taler and other GNU projects are making a difference and take a humanitarian perspective on technology. Providing technology supporting \textit{humans}. This was the reason I started my journey in computer science with my apprenticeship in 2015 and eventually decided to do my thesis on GNU Taler. It has not changed since. Even when my contribution is small I believe it is important. When everyone adds their ideas and work to the plate, we can achieve a better world. The title picture, generously provided by cartoonist Bruno Fauser \cite{fauser}, visualizes this attitude. Sometimes it is hard to not loose faith for the good, but do not. The good wins. Always.
+I am thankful that the Bern University of Applied Sciences supports free software projects like GNU Taler. It was a great opportunity for me as student and as human to gain direct insights and work on a GNU project during my thesis. I remember Prof. Dr. Christian Grothoff telling me during an onsite session: "Nicht so kurzfristig denken!" (do not think short-term). This also showed the horizon of the project to me. It tries to sustainably change the payment landscape for good. That is what I like the most about free software. It is built to last. The world will not get better when we keep pushing towards short-term profit benefiting individuals, global warming and war. GNU Taler and other GNU projects are making a difference and take a humanitarian perspective on technology. Providing technology supporting \textit{humans}. This was the reason I started my journey in computer science with my apprenticeship in 2015 and eventually decided to do my thesis on GNU Taler. It has not changed since. Even when my contribution is small I believe it is important. When everyone adds their ideas and work to the plate, we can achieve a better world. The title picture, generously provided by cartoonist Bruno Fauser \cite{fauser}, visualizes this attitude. Sometimes it is hard to not loose faith for the good, but; the good wins. Always.
diff --git a/docs/thesis.pdf b/docs/thesis.pdf
Binary files differ.
diff --git a/simulation/config.yaml b/simulation/config.yaml
@@ -1,4 +1,4 @@
-disable-delays: false
+disable-delays: true
c2ec-base-url: "http://localhost:8080"
parallel-withdrawals: 1
provider-backend-payment-delay: 1000
diff --git a/simulation/sim-terminal.go b/simulation/sim-terminal.go
@@ -163,7 +163,7 @@ func Terminal(in chan *SimulatedPhysicalInteraction, out chan *SimulatedPhysical
fmt.Println("TERMINAL: payment was processed at the provider backend. sending check notification.")
checkNotification := &TerminalWithdrawalConfirmationRequest{
- ProviderTransactionId: "simulation-transaction-id-0",
+ ProviderTransactionId: uuid.String(),
TerminalFees: EXCHANGE_FEES,
}
checkurl := FormatUrl(