[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-cashless2ecash] branch master updated (fd19956 -> ad94bc5)
From: |
gnunet |
Subject: |
[taler-cashless2ecash] branch master updated (fd19956 -> ad94bc5) |
Date: |
Fri, 26 Apr 2024 16:13:59 +0200 |
This is an automated email from the git hooks/post-receive script.
joel-haeberli pushed a change to branch master
in repository cashless2ecash.
from fd19956 app: update flow
new 76246c3 fix: crockford encoding
new ad94bc5 fix: implement terminal api, enable basic auth, fix simulation
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
c2ec/{auth.go => api-auth.go} | 84 +++--
c2ec/{auth_test.go => api-auth_test.go} | 0
c2ec/api-bank-integration.go | 252 ++++++++++++++
c2ec/api-terminals.go | 243 ++++++++++++++
c2ec/{wire-gateway.go => api-wire-gateway.go} | 148 ++------
c2ec/bank-integration.go | 371 ---------------------
c2ec/{postgres.go => db-postgres.go} | 95 +++++-
c2ec/db.go | 32 +-
c2ec/db/0001-c2ec_schema.sql | 20 +-
c2ec/db/drop.sql | 2 +
.../db/proc-c2ec_payment_notification_listener.sql | 4 +-
c2ec/db/proc-c2ec_retry_listener.sql | 2 +-
c2ec/db/proc-c2ec_status_listener.sql | 4 +-
c2ec/db/procedures.sql | 162 ---------
c2ec/db/procedures.sql.in | 1 -
...test_c2ec_test.sql => test_c2ec_simulation.sql} | 0
...lback.sql => test_c2ec_simulation_rollback.sql} | 0
c2ec/encoding.go | 127 +++++--
c2ec/encoding_test.go | 12 +-
c2ec/http-util.go | 48 +--
c2ec/install/build_app.sh | 21 ++
c2ec/install/installation_notes.md | 33 ++
c2ec/install/setup_db.sh | 37 ++
c2ec/install/start.sh | 25 ++
c2ec/install/wipe_db.sh | 27 ++
c2ec/main.go | 39 ++-
c2ec/{attestor.go => proc-attestor.go} | 4 +-
c2ec/{listener.go => proc-listener.go} | 0
c2ec/{transfer.go => proc-transfer.go} | 0
cli/cli.go | 99 +++++-
cli/db.go | 2 +-
.../bank-integration.tex} | 0
docs/content/implementation/terminal-api.tex | 27 ++
.../{.gitkeep => implementation/wire-gateway.tex} | 0
docs/project.bib | 7 +
simulation/c2ec-simulation | Bin 7570759 -> 7648407
bytes
simulation/encoding.go | 138 ++++++--
simulation/go.mod | 2 +
simulation/go.sum | 2 +
simulation/http-util.go | 110 ++----
simulation/main.go | 24 --
simulation/model.go | 7 -
simulation/sim-terminal.go | 182 +++++++---
simulation/sim-wallet.go | 52 ++-
wallee-c2ec/app/src/main/AndroidManifest.xml | 2 +
.../client/taler/encoding/CryptoUtils.kt | 105 ++++++
.../client/taler/encoding/TalerBase32Codec.kt | 16 +-
.../client/wallee/WalleeResponseHandler.kt | 14 +-
.../habej2/wallee_c2ec/withdrawal/AmountScreen.kt | 51 +++
.../withdrawal/AuthorizePaymentScreen.kt | 57 ++++
.../withdrawal/ExchangeSelectionScreen.kt | 42 +++
.../withdrawal/RegisterWithdrawalScreen.kt | 44 +++
.../wallee_c2ec/withdrawal/WithdrawalActivity.kt | 159 ++-------
.../client/taler/encoding/CyptoUtilsTest.kt | 54 +++
54 files changed, 1848 insertions(+), 1141 deletions(-)
rename c2ec/{auth.go => api-auth.go} (73%)
rename c2ec/{auth_test.go => api-auth_test.go} (100%)
create mode 100644 c2ec/api-bank-integration.go
create mode 100644 c2ec/api-terminals.go
rename c2ec/{wire-gateway.go => api-wire-gateway.go} (69%)
delete mode 100644 c2ec/bank-integration.go
rename c2ec/{postgres.go => db-postgres.go} (88%)
delete mode 100644 c2ec/db/procedures.sql
delete mode 100644 c2ec/db/procedures.sql.in
rename c2ec/db/{test_c2ec_test.sql => test_c2ec_simulation.sql} (100%)
rename c2ec/db/{test_c2ec_test_rollback.sql =>
test_c2ec_simulation_rollback.sql} (100%)
create mode 100644 c2ec/install/build_app.sh
create mode 100644 c2ec/install/installation_notes.md
create mode 100755 c2ec/install/setup_db.sh
create mode 100755 c2ec/install/start.sh
create mode 100755 c2ec/install/wipe_db.sh
rename c2ec/{attestor.go => proc-attestor.go} (98%)
rename c2ec/{listener.go => proc-listener.go} (100%)
rename c2ec/{transfer.go => proc-transfer.go} (100%)
copy docs/content/{.gitkeep => implementation/bank-integration.tex} (100%)
create mode 100644 docs/content/implementation/terminal-api.tex
copy docs/content/{.gitkeep => implementation/wire-gateway.tex} (100%)
create mode 100644 simulation/go.sum
create mode 100644
wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CryptoUtils.kt
create mode 100644
wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt
create mode 100644
wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt
create mode 100644
wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt
create mode 100644
wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt
create mode 100644
wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CyptoUtilsTest.kt
diff --git a/c2ec/auth.go b/c2ec/api-auth.go
similarity index 73%
rename from c2ec/auth.go
rename to c2ec/api-auth.go
index b008f31..cfcb985 100644
--- a/c2ec/auth.go
+++ b/c2ec/api-auth.go
@@ -45,16 +45,19 @@ func AuthenticateTerminal(req *http.Request) bool {
decoded, err := base64.StdEncoding.DecodeString(basicAuth)
if err != nil {
+ LogWarn("auth", "failed decoding basic auth header from
base64")
return false
}
username, password, err := parseBasicAuth(string(decoded))
if err != nil {
+ LogWarn("auth", "failed parsing username password from
basic auth")
return false
}
provider, terminalId, err := parseTerminalUser(username)
if err != nil {
+ LogWarn("auth", "failed parsing terminal from username
in basic auth")
return false
}
LogInfo("auth", fmt.Sprintf("req=%s by terminal with id=%d,
provider=%s", req.RequestURI, terminalId, provider))
@@ -86,28 +89,6 @@ func AuthenticateTerminal(req *http.Request) bool {
return false
}
-// find out how the wallet authenticates itself.
-// returns true if authentication was successful, otherwise false
-// when not successful, the api shall return immediately
-func AuthenticateWallet(req *http.Request) bool {
-
- // Is this needed? Understand how the wallet authenticates itself at
the exchange currently first.
- // https://docs.taler.net/design-documents/049-auth.html#dd48-token
- // https://docs.taler.net/core/api-corebank.html#authentication
- //
- // /accounts/$USERNAME/token
- //
- // The username in our case is the reserve public key
- // registered for withdrawal. At the initial registration
- // of the reserve public key we leverage a TOFU trust model.
- // during the registration of the reserve public key a new
- // access token will be created with a limited lifetime.
- // The token will not be refreshable and become invalid
- // only after a few minutes. Since the Wallet will register
- // a wopid and
- return true
-}
-
func parseBasicAuth(basicAuth string) (string, string, error) {
parts := strings.Split(basicAuth, ":")
@@ -137,6 +118,65 @@ func parseTerminalUser(username string) (string, int,
error) {
return providerName, terminalId, nil
}
+// Parses the terminal id from the token.
+// This function is used to determine the terminal
+// which orchestrates the withdrawal.
+func parseTerminalId(req *http.Request) int {
+ auth := req.Header.Get(AUTHORIZATION_HEADER)
+ if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX);
found {
+
+ decoded, err := base64.StdEncoding.DecodeString(basicAuth)
+ if err != nil {
+ return -1
+ }
+
+ username, _, err := parseBasicAuth(string(decoded))
+ if err != nil {
+ return -1
+ }
+
+ _, terminalId, err := parseTerminalUser(username)
+ if err != nil {
+ return -1
+ }
+
+ return terminalId
+ }
+
+ return -1
+}
+
+func parseProvider(req *http.Request) (*Provider, error) {
+
+ auth := req.Header.Get(AUTHORIZATION_HEADER)
+ if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX);
found {
+
+ decoded, err := base64.StdEncoding.DecodeString(basicAuth)
+ if err != nil {
+ return nil, err
+ }
+
+ username, _, err := parseBasicAuth(string(decoded))
+ if err != nil {
+ return nil, err
+ }
+
+ providerName, _, err := parseTerminalUser(username)
+ if err != nil {
+ return nil, err
+ }
+
+ p, err := DB.GetTerminalProviderByName(providerName)
+ if err != nil {
+ return nil, err
+ }
+
+ return p, nil
+ }
+
+ return nil, errors.New("authorization header did not match
expectations")
+}
+
// takes a password and a base64 encoded password hash, including salt and
checks
// the password supplied against it.
// the format of the password hash is expected to be the following:
diff --git a/c2ec/auth_test.go b/c2ec/api-auth_test.go
similarity index 100%
rename from c2ec/auth_test.go
rename to c2ec/api-auth_test.go
diff --git a/c2ec/api-bank-integration.go b/c2ec/api-bank-integration.go
new file mode 100644
index 0000000..ec7044b
--- /dev/null
+++ b/c2ec/api-bank-integration.go
@@ -0,0 +1,252 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "fmt"
+ http "net/http"
+ "strconv"
+ "time"
+)
+
+const WITHDRAWAL_OPERATION = "/withdrawal-operation"
+
+const WOPID_PARAMETER = "wopid"
+const BANK_INTEGRATION_CONFIG_PATTERN = "/config"
+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"`
+ Currency string `json:"currency"`
+ NumFractionalInputDigits int
`json:"num_fractional_input_digits"`
+ NumFractionalNormalDigits int
`json:"num_fractional_normal_digits"`
+ NumFractionalTrailingZeroDigits int
`json:"num_fractional_trailing_zero_digits"`
+ AltUnitNames string `json:"alt_unit_names"`
+}
+
+//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankIntegrationConfig
+type BankIntegrationConfig struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Implementation string `json:"implementation"`
+ Currency string `json:"currency"`
+ CurrencySpecification CurrencySpecification
`json:"currency_specification"`
+ // TODO: maybe add exchanges payto uri for transfers etc.?
+}
+
+type BankWithdrawalOperationPostRequest struct {
+ ReservePubKey EddsaPublicKey `json:"reserve_pub"`
+ SelectedExchange string `json:"selected_exchange"`
+ Amount *Amount `json:"amount"`
+}
+
+type BankWithdrawalOperationPostResponse struct {
+ Status WithdrawalOperationStatus `json:"status"`
+ ConfirmTransferUrl string
`json:"confirm_transfer_url"`
+ TransferDone bool `json:"transfer_done"`
+}
+
+type BankWithdrawalOperationStatus struct {
+ Status WithdrawalOperationStatus `json:"status"`
+ Amount Amount `json:"amount"`
+ SenderWire string `json:"sender_wire"`
+ WireTypes []string `json:"wire_types"`
+ ReservePubKey EddsaPublicKey `json:"selected_reserve_pub"`
+}
+
+func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) {
+
+ cfg := BankIntegrationConfig{
+ Name: "taler-bank-integration",
+ Version: "0:0:1",
+ }
+
+ serializedCfg, err :=
NewJsonCodec[BankIntegrationConfig]().EncodeToBytes(&cfg)
+ if err != nil {
+ LogInfo("bank-integration-api", fmt.Sprintf("failed serializing
config: %s", err.Error()))
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ res.WriteHeader(HTTP_OK)
+ res.Write(serializedCfg)
+}
+
+func handleParameterRegistration(res http.ResponseWriter, req *http.Request) {
+
+ jsonCodec := NewJsonCodec[BankWithdrawalOperationPostRequest]()
+ registration, err := ReadStructFromBody(req, jsonCodec)
+ if err != nil {
+ LogWarn("bank-integration-api", fmt.Sprintf("invalid body for
withdrawal registration error=%s", err.Error()))
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ // read and validate the wopid path parameter
+ wopid := req.PathValue(WOPID_PARAMETER)
+ wpd, err := ParseWopid(wopid)
+ if err != nil {
+ LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ if _, err = DB.GetWithdrawalByWopid(wpd); err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_NOT_FOUND)
+ return
+ }
+
+ if err = DB.RegisterWithdrawalParameters(
+ wpd,
+ registration.ReservePubKey,
+ ); err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ withdrawal, err := DB.GetWithdrawalByWopid(wpd)
+ if err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+
+ resbody := &BankWithdrawalOperationPostResponse{
+ Status: withdrawal.WithdrawalStatus,
+ ConfirmTransferUrl: "", // not used in our case
+ TransferDone: withdrawal.WithdrawalStatus == CONFIRMED,
+ }
+
+ resbyts, err :=
NewJsonCodec[BankWithdrawalOperationPostResponse]().EncodeToBytes(resbody)
+ if err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+
+ res.Write(resbyts)
+}
+
+// Get status of withdrawal associated with the given WOPID
+//
+// Parameters:
+// - long_poll_ms (optional):
+// milliseconds to wait for state to change
+// given old_state until responding
+// - old_state (optional):
+// Default is 'pending'
+func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
+
+ // read and validate request query parameters
+ shouldStartLongPoll := true
+ longPollMilli := DEFAULT_LONG_POLL_MS
+ if longPollMilliPtr, accepted := AcceptOptionalParamOrWriteResponse(
+ "long_poll_ms", strconv.Atoi, req, res,
+ ); accepted {
+ if longPollMilliPtr != nil {
+ longPollMilli = *longPollMilliPtr
+ } else {
+ // this means parameter was not given.
+ // no long polling (simple get)
+ shouldStartLongPoll = false
+ }
+ } else {
+ shouldStartLongPoll = false
+ }
+
+ // read and validate the wopid path parameter
+ wopid := req.PathValue(WOPID_PARAMETER)
+ wpd, err := ParseWopid(wopid)
+ if err != nil {
+ LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ if shouldStartLongPoll {
+
+ timeoutCtx, cancelFunc := context.WithTimeout(
+ req.Context(),
+ time.Duration(longPollMilli)*time.Millisecond,
+ )
+ defer cancelFunc()
+
+ notifications := make(chan *Notification)
+ channel := "w_" + base64.StdEncoding.EncodeToString(wpd)
+
+ listenFunc, err := DB.NewListener(
+ channel,
+ notifications,
+ )
+
+ if err != nil {
+ res.WriteHeader(HTTP_NO_CONTENT)
+ return
+ }
+
+ go listenFunc(timeoutCtx)
+
+ for {
+ select {
+ case <-timeoutCtx.Done():
+ LogInfo("bank-integration-api", "long poll time
exceeded")
+ res.WriteHeader(HTTP_NO_CONTENT)
+ return
+ case <-notifications:
+ writeWithdrawalOrError(wpd, res)
+ return
+ }
+ }
+ }
+
+ writeWithdrawalOrError(wpd, res)
+}
+
+func handleWithdrawalAbort(res http.ResponseWriter, req *http.Request) {
+ res.WriteHeader(HTTP_OK)
+ res.Write(bytes.NewBufferString("retrieved withdrawal operation
abortion request").Bytes())
+}
+
+// Tries to load a WithdrawalOperationStatus from the database. If no
+// entry could been found, it will write the correct error to the response.
+func writeWithdrawalOrError(wopid []byte, res http.ResponseWriter) {
+ // read the withdrawal from the database
+ withdrawal, err := DB.GetWithdrawalByWopid(wopid)
+ if err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ if withdrawal == nil {
+ // not found -> 404
+ res.WriteHeader(HTTP_NOT_FOUND)
+ return
+ }
+
+ // return the C2ECWithdrawalStatus
+ if amount, err := ToAmount(withdrawal.Amount); err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ } else {
+ withdrawalStatusBytes, err :=
NewJsonCodec[BankWithdrawalOperationStatus]().EncodeToBytes(&BankWithdrawalOperationStatus{
+ Status: withdrawal.WithdrawalStatus,
+ Amount: *amount,
+ })
+ if err != nil {
+ LogError("bank-integration-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+ res.WriteHeader(HTTP_OK)
+ res.Write(withdrawalStatusBytes)
+ }
+}
diff --git a/c2ec/api-terminals.go b/c2ec/api-terminals.go
new file mode 100644
index 0000000..b120a7e
--- /dev/null
+++ b/c2ec/api-terminals.go
@@ -0,0 +1,243 @@
+package main
+
+import (
+ "crypto/rand"
+ "fmt"
+ "net/http"
+)
+
+const TERMINAL_API_CONFIG = "/config"
+const TERMINAL_API_REGISTER_WITHDRAWAL = "/withdrawals"
+const TERMINAL_API_WITHDRAWAL_STATUS = "/withdrawals/{wopid}"
+const TERMINAL_API_CHECK_WITHDRAWAL = "/withdrawals/{wopid}/check"
+
+type TerminalConfig struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ ProviderName string `json:"provider_name"`
+ WireType string `json:"wire_type"`
+}
+
+type TerminalWithdrawalSetup struct {
+ Amount *Amount `json:"amount"`
+ SuggestedAmount *Amount `json:"suggested_amount"`
+ ProviderTransactionId string `json:"provider_transaction_id"`
+ TerminalFees *Amount `json:"terminal_fees"`
+ RequestUid string `json:"request_uid"`
+ UserUuid string `json:"user_uuid"`
+ Lock string `json:"lock"`
+}
+
+type TerminalWithdrawalSetupResponse struct {
+ Wopid string `json:"withdrawal_id"`
+}
+
+type TerminalWithdrawalConfirmationRequest struct {
+ ProviderTransactionId string `json:"provider_transaction_id"`
+ TerminalFees *Amount `json:"terminal_fees"`
+ UserUuid string `json:"user_uuid"`
+ Lock string `json:"lock"`
+}
+
+func handleTerminalConfig(res http.ResponseWriter, req *http.Request) {
+
+ p, auth, err := authAndParseProvider(req)
+ if !auth {
+ res.WriteHeader(HTTP_UNAUTHORIZED)
+ return
+ }
+
+ if err != nil || p == nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ cfg, err :=
NewJsonCodec[TerminalConfig]().EncodeToBytes(&TerminalConfig{
+ Name: "taler-terminal",
+ Version: "0:0:0",
+ ProviderName: p.Name,
+ WireType: p.PaytoTargetType,
+ })
+ if err != nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+ res.WriteHeader(HTTP_OK)
+ res.Write(cfg)
+}
+
+func handleWithdrawalSetup(res http.ResponseWriter, req *http.Request) {
+
+ p, auth, err := authAndParseProvider(req)
+ if !auth {
+ res.WriteHeader(HTTP_UNAUTHORIZED)
+ return
+ }
+ if err != nil || p == nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ jsonCodec := NewJsonCodec[TerminalWithdrawalSetup]()
+ setup, err := ReadStructFromBody(req, jsonCodec)
+ if err != nil {
+ LogWarn("terminals-api", fmt.Sprintf("invalid body for
withdrawal registration error=%s", err.Error()))
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ if hasConflict(setup) {
+ res.WriteHeader(HTTP_CONFLICT)
+ return
+ }
+
+ // generate wopid
+ generatedWopid := make([]byte, 32)
+ _, err = rand.Read(generatedWopid)
+ if err != nil {
+ LogWarn("terminals-api", "unable to generate correct wopid")
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+
+ err = DB.SetupWithdrawal(
+ generatedWopid,
+ preventNilAmount(setup.SuggestedAmount),
+ preventNilAmount(setup.Amount),
+ setup.ProviderTransactionId,
+ preventNilAmount(setup.TerminalFees),
+ setup.RequestUid,
+ )
+
+ if err != nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ encodedBody, err :=
NewJsonCodec[TerminalWithdrawalSetupResponse]().EncodeToBytes(
+ &TerminalWithdrawalSetupResponse{
+ Wopid: talerBinaryEncode(generatedWopid),
+ },
+ )
+ if err != nil {
+ LogError("terminal-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ return
+ }
+
+ res.Write(encodedBody)
+}
+
+func handleWithdrawalCheck(res http.ResponseWriter, req *http.Request) {
+
+ p, auth, err := authAndParseProvider(req)
+ if !auth {
+ res.WriteHeader(HTTP_UNAUTHORIZED)
+ return
+ }
+
+ if err != nil || p == nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ wopid := req.PathValue(WOPID_PARAMETER)
+ wpd, err := ParseWopid(wopid)
+ if err != nil {
+ LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
+ if wopid == "" {
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+ }
+
+ jsonCodec := NewJsonCodec[TerminalWithdrawalConfirmationRequest]()
+ paymentNotification, err := ReadStructFromBody(req, jsonCodec)
+ if err != nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ LogInfo("bank-integration-api", "received payment notification")
+
+ terminalId := parseTerminalId(req)
+ if terminalId == -1 {
+ LogWarn("terminals-api", "terminal id could not be read from
authorization header")
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ if paymentNotification.TerminalFees == nil {
+ paymentNotification.TerminalFees = &Amount{"", 0, 0}
+ }
+
+ err = DB.NotifyPayment(
+ wpd,
+ paymentNotification.ProviderTransactionId,
+ terminalId,
+ *paymentNotification.TerminalFees,
+ )
+ if err != nil {
+ LogError("terminals-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
+ return
+ }
+
+ res.WriteHeader(HTTP_NO_CONTENT)
+}
+
+func preventNilAmount(a *Amount) Amount {
+
+ if a == nil {
+ return Amount{"", 0, 0}
+ }
+
+ return *a
+}
+
+func hasConflict(t *TerminalWithdrawalSetup) bool {
+
+ w, err := DB.GetWithdrawalByRequestUid(t.RequestUid)
+ if err != nil {
+ LogError("terminals-api", err)
+ return true
+ }
+
+ if w == nil {
+ return false // no request with this uid
+ }
+
+ isEqual := w.Amount.Curr == t.Amount.Currency &&
+ w.Amount.Val == int64(t.Amount.Value) &&
+ w.Amount.Frac == int32(t.Amount.Fraction) &&
+ w.TerminalFees.Curr == t.TerminalFees.Currency &&
+ uint64(w.TerminalFees.Val) == t.TerminalFees.Value &&
+ uint64(w.TerminalFees.Frac) == t.TerminalFees.Fraction &&
+ w.SuggestedAmount.Curr == t.SuggestedAmount.Currency &&
+ uint64(w.SuggestedAmount.Val) == t.SuggestedAmount.Value &&
+ uint64(w.SuggestedAmount.Frac) == t.SuggestedAmount.Fraction &&
+ w.ProviderTransactionId == &t.ProviderTransactionId &&
+ w.RequestUid == t.RequestUid
+
+ return !isEqual
+}
+
+func authAndParseProvider(req *http.Request) (*Provider, bool, error) {
+
+ if authenticated := AuthenticateTerminal(req); !authenticated {
+ return nil, false, nil
+ }
+
+ p, err := parseProvider(req)
+ if err != nil {
+ return nil, true, err
+ }
+
+ return p, true, nil
+}
diff --git a/c2ec/wire-gateway.go b/c2ec/api-wire-gateway.go
similarity index 69%
rename from c2ec/wire-gateway.go
rename to c2ec/api-wire-gateway.go
index 0e793af..41cd2b5 100644
--- a/c2ec/wire-gateway.go
+++ b/c2ec/api-wire-gateway.go
@@ -103,7 +103,7 @@ func NewIncomingReserveTransaction(w *Withdrawal)
*IncomingReserveTransaction {
}
t.DebitAccount = client.FormatPayto(w)
t.ReservePub = FormatEddsaPubKey(w.ReservePubKey)
- t.RowId = int(w.WithdrawalId)
+ t.RowId = int(w.WithdrawalRowId)
t.Type = INCOMING_RESERVE_TRANSACTION_TYPE
return t
}
@@ -148,72 +148,36 @@ func transfer(res http.ResponseWriter, req *http.Request)
{
jsonCodec := NewJsonCodec[TransferRequest]()
transfer, err := ReadStructFromBody(req, jsonCodec)
if err != nil {
-
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
- Title: "invalid request",
- Detail: "the transfer request is malformed (error: "
+ err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
return
}
paytoTargetType, tid, err :=
ParsePaytoWalleeTransaction(transfer.CreditAccount)
if err != nil {
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
- Title: "invalid payto-uri",
- Detail: "the transfer request contains an invalid
payto-uri (error: " + err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
return
}
p, err := DB.GetTerminalProviderByPaytoTargetType(paytoTargetType)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
- Title: "database request failed",
- Detail: "failed to retrieve the provider for the
payto target type '" + paytoTargetType + "'",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
- decodedRequestUid, err := talerBase32Decode(string(transfer.RequestUid))
+ decodedRequestUid, err := talerBinaryDecode(string(transfer.RequestUid))
if err != nil {
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
- Title: "invalid request",
- Detail: "the transfer request is malformed (error: "
+ err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_BAD_REQUEST)
return
}
t, err := DB.GetTransferById(decodedRequestUid)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
- Title: "database request failed",
- Detail: "there was an error processing the database
query",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
@@ -227,15 +191,8 @@ func transfer(res http.ResponseWriter, req *http.Request) {
transfer.CreditAccount,
)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
- Title: "database request failed",
- Detail: "there was an error creating the
transfer",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
} else {
@@ -247,44 +204,23 @@ func transfer(res http.ResponseWriter, req *http.Request)
{
transfer.Wtid != ShortHashCode(t.Wtid) ||
transfer.CreditAccount != t.CreditAccount {
- err := WriteProblem(res, HTTP_CONFLICT, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
- Title: "invalid request",
- Detail: "the transfer request did not match
previous request with the same request identifier",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogWarn("wire-gateway-api", "idempotency violation")
+ res.WriteHeader(HTTP_CONFLICT)
return
}
ptid := strconv.Itoa(tid)
w, err := DB.GetWithdrawalByProviderTransactionId(ptid)
if err != nil || w == nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
- Title: "database request failed",
- Detail: "there was an error processing the
database query or no withdrawal could been found.",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
refundClient := PROVIDER_CLIENTS[p.Name]
if refundClient == nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_UNKNOWN_TRANSFER_MECHANISM",
- Title: "unknown refund mechanism",
- Detail: "the target type of the payto uri for
the transfer is not registered",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", errors.New("client for
provider "+p.Name+" not initialized"))
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
refundClient.Refund(ptid)
@@ -365,15 +301,8 @@ func historyIncoming(res http.ResponseWriter, req
*http.Request) {
withdrawals, err := DB.GetConfirmedWithdrawals(start, delta)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
- Title: "database request failed",
- Detail: "there was an error processing the database
query. error=" + err.Error(),
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
@@ -392,15 +321,8 @@ func historyIncoming(res http.ResponseWriter, req
*http.Request) {
enc, err :=
NewJsonCodec[[]*IncomingReserveTransaction]().EncodeToBytes(&transactions)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_RESPONSE_ENCODING_FAILED",
- Title: "encoding failed",
- Detail: "the encoding of the response failed (error:"
+ err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
@@ -459,15 +381,8 @@ func historyOutgoing(res http.ResponseWriter, req
*http.Request) {
transfers, err := DB.GetTransfers(start, delta)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
- Title: "database request failed",
- Detail: "there was an error processing the database
query",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
@@ -495,15 +410,8 @@ func historyOutgoing(res http.ResponseWriter, req
*http.Request) {
}
enc, err :=
NewJsonCodec[OutgoingHistory]().EncodeToBytes(&outgoingHistory)
if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_RESPONSE_ENCODING_FAILED",
- Title: "encoding failed",
- Detail: "the encoding of the response failed (error:"
+ err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ LogError("wire-gateway-api", err)
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return
}
diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go
deleted file mode 100644
index 35710a6..0000000
--- a/c2ec/bank-integration.go
+++ /dev/null
@@ -1,371 +0,0 @@
-package main
-
-import (
- "bytes"
- "context"
- "encoding/base64"
- "fmt"
- http "net/http"
- "strconv"
- "time"
-)
-
-const BANK_INTEGRATION_CONFIG_ENDPOINT = "/config"
-const WITHDRAWAL_OPERATION = "/withdrawal-operation"
-
-const WOPID_PARAMETER = "wopid"
-const BANK_INTEGRATION_CONFIG_PATTERN = BANK_INTEGRATION_CONFIG_ENDPOINT
-const WITHDRAWAL_OPERATION_PATTERN = WITHDRAWAL_OPERATION
-const WITHDRAWAL_OPERATION_BY_WOPID_PATTERN = WITHDRAWAL_OPERATION + "/{" +
WOPID_PARAMETER + "}"
-const WITHDRAWAL_OPERATION_PAYMENT_PATTERN =
WITHDRAWAL_OPERATION_BY_WOPID_PATTERN + "/confirm"
-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"`
- Currency string `json:"currency"`
- NumFractionalInputDigits int
`json:"num_fractional_input_digits"`
- NumFractionalNormalDigits int
`json:"num_fractional_normal_digits"`
- NumFractionalTrailingZeroDigits int
`json:"num_fractional_trailing_zero_digits"`
- AltUnitNames string `json:"alt_unit_names"`
-}
-
-//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankIntegrationConfig
-type BankIntegrationConfig struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Implementation string `json:"implementation"`
- Currency string `json:"currency"`
- CurrencySpecification CurrencySpecification
`json:"currency_specification"`
- // TODO: maybe add exchanges payto uri for transfers etc.?
-}
-
-type C2ECWithdrawRegistration struct {
- ReservePubKey EddsaPublicKey `json:"reserve_pub_key"`
-}
-
-type C2ECWithdrawalStatus struct {
- 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"`
- TerminalId int `json:"terminal_id"`
- Amount Amount `json:"amount"`
- Fees Amount `json:"card_fees"`
-}
-
-func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) {
-
- cfg := BankIntegrationConfig{
- Name: "taler-bank-integration",
- Version: "0:0:1",
- }
-
- serializedCfg, err :=
NewJsonCodec[BankIntegrationConfig]().EncodeToBytes(&cfg)
- if err != nil {
- LogInfo("bank-integration-api", fmt.Sprintf("failed serializing
config: %s", err.Error()))
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- return
- }
-
- res.WriteHeader(HTTP_OK)
- res.Write(serializedCfg)
-}
-
-func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) {
-
- jsonCodec := NewJsonCodec[C2ECWithdrawRegistration]()
- registration, err := ReadStructFromBody(req, jsonCodec)
- if err != nil {
- LogWarn("bank-integration-api", fmt.Sprintf("invalid body for
withdrawal registration error=%s", err.Error()))
- 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(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- // read and validate the wopid path parameter
- wopid := req.PathValue(WOPID_PARAMETER)
- wpd, err := ParseWopid(wopid)
- if err != nil {
- LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_INVALID_PATH_PARAMETER",
- Title: "invalid request path parameter",
- Detail: "the withdrawal status request path parameter
'wopid' is malformed",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- err = DB.RegisterWithdrawal(
- wpd,
- registration.ReservePubKey,
- )
-
- if err != nil {
-
- 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(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- writeWithdrawalOrError(wpd, res, req.RequestURI)
-}
-
-// Get status of withdrawal associated with the given WOPID
-//
-// Parameters:
-// - long_poll_ms (optional):
-// milliseconds to wait for state to change
-// given old_state until responding
-// - old_state (optional):
-// Default is 'pending'
-func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
-
- // read and validate request query parameters
- shouldStartLongPoll := true
- longPollMilli := DEFAULT_LONG_POLL_MS
- if longPollMilliPtr, accepted := AcceptOptionalParamOrWriteResponse(
- "long_poll_ms", strconv.Atoi, req, res,
- ); accepted {
- if longPollMilliPtr != nil {
- longPollMilli = *longPollMilliPtr
- } else {
- // this means parameter was not given.
- // no long polling (simple get)
- shouldStartLongPoll = false
- }
- } else {
- shouldStartLongPoll = false
- }
-
- // read and validate the wopid path parameter
- wopid := req.PathValue(WOPID_PARAMETER)
- wpd, err := ParseWopid(wopid)
- if err != nil {
- LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_INVALID_PATH_PARAMETER",
- Title: "invalid request path parameter",
- Detail: "the withdrawal status request path parameter
'wopid' is malformed",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- if shouldStartLongPoll {
-
- timeoutCtx, cancelFunc := context.WithTimeout(
- req.Context(),
- time.Duration(longPollMilli)*time.Millisecond,
- )
- defer cancelFunc()
-
- notifications := make(chan *Notification)
- channel := "w_" + base64.StdEncoding.EncodeToString(wpd)
-
- listenFunc, err := DB.NewListener(
- channel,
- notifications,
- )
-
- if err != nil {
- err := WriteProblem(res, HTTP_NO_CONTENT,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_LISTEN_FAILURE",
- Title: "Failed setting up listener",
- Detail: fmt.Sprintf("unable to start long
polling due to %s", err.Error()),
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- go listenFunc(timeoutCtx)
-
- // go DB.ListenForWithdrawalStatusChange(timeoutCtx,
WithdrawalIdentifier(base64.StdEncoding.EncodeToString(wpd)), statusChannel,
errChan)
- for {
- select {
- case <-timeoutCtx.Done():
- err := WriteProblem(res, HTTP_NO_CONTENT,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_LONG_POLL_TIME_EXCEEDED",
- Title: "time exceeded",
- Detail: fmt.Sprintf("long poll ended
due to timeout: %dms", longPollMilli),
- Instance: req.RequestURI,
- })
- if err != nil {
-
res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- case <-notifications:
- writeWithdrawalOrError(wpd, res, req.RequestURI)
- return
- }
- }
- }
-
- writeWithdrawalOrError(wpd, res, req.RequestURI)
-}
-
-func handlePaymentNotification(res http.ResponseWriter, req *http.Request) {
-
- wopid := req.PathValue(WOPID_PARAMETER)
- wpd, err := ParseWopid(wopid)
- if err != nil {
- LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
- if wopid == "" {
- err := WriteProblem(res, HTTP_BAD_REQUEST,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_INVALID_PATH_PARAMETER",
- Title: "invalid request path parameter",
- Detail: "the withdrawal status request path
parameter 'wopid' is malformed",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
- }
-
- jsonCodec := NewJsonCodec[C2ECPaymentNotification]()
- paymentNotification, err := ReadStructFromBody(req, jsonCodec)
- if err != nil {
- LogWarn("bank-integration-api", fmt.Sprintf("invalid body for
payment notification error=%s", err.Error()))
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_WITHDRAW_REGISTRATION_INVALID_REQ",
- Title: "invalid request",
- Detail: "the payment notification request for the
withdrawal is malformed (error: " + err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- LogInfo("bank-integration-api", "received payment notification")
-
- err = DB.NotifyPayment(
- wpd,
- paymentNotification.ProviderTransactionId,
- paymentNotification.TerminalId,
- paymentNotification.Amount,
- paymentNotification.Fees,
- )
- if err != nil {
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_PAYMENT_NOTIFICATION_FAILED",
- Title: "payment notification failed",
- Detail: "the payment notification failed during the
processing of the message: " + err.Error(),
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- res.WriteHeader(HTTP_NO_CONTENT)
-}
-
-func handleWithdrawalAbort(res http.ResponseWriter, req *http.Request) {
-
- res.WriteHeader(HTTP_OK)
- res.Write(bytes.NewBufferString("retrieved withdrawal operation
abortion request").Bytes())
-}
-
-// Tries to load a WithdrawalOperationStatus from the database. If no
-// entry could been found, it will write the correct error to the response.
-func writeWithdrawalOrError(wopid []byte, res http.ResponseWriter, reqUri
string) {
- // 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: "db failure while requesting withdrawal
(error=" + err.Error() + ")",
- Instance: reqUri,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
-
- if withdrawal == nil {
- // not found -> 404
- err := WriteProblem(res, HTTP_NOT_FOUND, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_WITHDRAWAL_NOT_FOUND",
- Title: "Not Found",
- Detail: "No withdrawal with wopid=" +
talerBase32Encode(wopid) + " could been found.",
- Instance: reqUri,
- })
- 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: reqUri,
- })
- 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: reqUri,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
- res.WriteHeader(HTTP_OK)
- res.Write(withdrawalStatusBytes)
- }
-}
diff --git a/c2ec/postgres.go b/c2ec/db-postgres.go
similarity index 88%
rename from c2ec/postgres.go
rename to c2ec/db-postgres.go
index e9a085a..bff4ead 100644
--- a/c2ec/postgres.go
+++ b/c2ec/db-postgres.go
@@ -16,21 +16,27 @@ import (
)
const PS_INSERT_WITHDRAWAL = "INSERT INTO " + WITHDRAWAL_TABLE_NAME + " (" +
- WITHDRAWAL_FIELD_NAME_WOPID + "," +
+ WITHDRAWAL_FIELD_NAME_WOPID + ", " + WITHDRAWAL_FIELD_NAME_RUID + ", " +
+ WITHDRAWAL_FIELD_NAME_SUGGESTED_AMOUNT + ", " +
WITHDRAWAL_FIELD_NAME_AMOUNT + ", " +
+ WITHDRAWAL_FIELD_NAME_TRANSACTION_ID + ", " +
WITHDRAWAL_FIELD_NAME_FEES + ", " + WITHDRAWAL_FIELD_NAME_TS +
+ ") VALUES ($1,$2,($3,$4,$5),($6,$7,$8),$9,($10,$11,$12),$13)"
+
+const PS_REGISTER_WITHDRAWAL_PARAMS = "UPDATE " + WITHDRAWAL_TABLE_NAME + "
SET (" +
WITHDRAWAL_FIELD_NAME_RESPUBKEY + "," +
WITHDRAWAL_FIELD_NAME_STATUS + "," +
WITHDRAWAL_FIELD_NAME_TS + ")" +
- " VALUES ($1, $2, $3, $4);"
+ " = ($1,$2,$3)" +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$4"
const PS_GET_UNCONFIRMED_WITHDRAWALS = "SELECT * FROM " +
WITHDRAWAL_TABLE_NAME +
" WHERE " + WITHDRAWAL_FIELD_NAME_TRANSACTION_ID + " IS NOT NULL" +
" AND " + WITHDRAWAL_FIELD_NAME_STATUS + " = '" + string(SELECTED) + "'"
const PS_PAYMENT_NOTIFICATION = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
- WITHDRAWAL_FIELD_NAME_AMOUNT + "," + WITHDRAWAL_FIELD_NAME_FEES + "," +
- WITHDRAWAL_FIELD_NAME_TRANSACTION_ID + "," +
WITHDRAWAL_FIELD_NAME_TERMINAL_ID + ")" +
- " = (($1, $2, $3),($4, $5, $6),$7, $8)" +
- " WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$9"
+ WITHDRAWAL_FIELD_NAME_FEES + "," + WITHDRAWAL_FIELD_NAME_TRANSACTION_ID
+ "," +
+ WITHDRAWAL_FIELD_NAME_TERMINAL_ID + ")" +
+ " = (($1,$2,$3),$4,$5)" +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$6"
const PS_FINALISE_PAYMENT = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
WITHDRAWAL_FIELD_NAME_STATUS + "," +
@@ -58,6 +64,9 @@ const PS_CONFIRMED_TRANSACTIONS_DESC = "SELECT * FROM " +
WITHDRAWAL_TABLE_NAME
" LIMIT $1" +
" OFFSET $2"
+const PS_GET_WITHDRAWAL_BY_RUID = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_RUID + "=$1"
+
const PS_GET_WITHDRAWAL_BY_ID = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
" WHERE " + WITHDRAWAL_FIELD_NAME_ID + "=$1"
@@ -161,7 +170,43 @@ func (db *C2ECPostgres) registerCustomTypesHook(ctx
context.Context, conn *pgx.C
return nil
}
-func (db *C2ECPostgres) RegisterWithdrawal(
+func (db *C2ECPostgres) SetupWithdrawal(
+ wopid []byte,
+ suggestedAmount Amount,
+ amount Amount,
+ providerTransactionId string,
+ terminalFees Amount,
+ requestUid string,
+) error {
+
+ ts := time.Now()
+ res, err := db.pool.Exec(
+ db.ctx,
+ PS_INSERT_WITHDRAWAL,
+ wopid,
+ requestUid,
+ suggestedAmount.Value,
+ suggestedAmount.Fraction,
+ suggestedAmount.Currency,
+ amount.Value,
+ amount.Fraction,
+ amount.Currency,
+ providerTransactionId,
+ terminalFees.Value,
+ terminalFees.Fraction,
+ terminalFees.Currency,
+ ts.Unix(),
+ )
+ if err != nil {
+ LogError("postgres", err)
+ return err
+ }
+ LogInfo("postgres", "query="+PS_INSERT_WITHDRAWAL)
+ LogInfo("postgres", "setup withdrawal successfully. affected
rows="+strconv.Itoa(int(res.RowsAffected())))
+ return nil
+}
+
+func (db *C2ECPostgres) RegisterWithdrawalParameters(
wopid []byte,
resPubKey EddsaPublicKey,
) error {
@@ -174,21 +219,47 @@ func (db *C2ECPostgres) RegisterWithdrawal(
ts := time.Now()
res, err := db.pool.Exec(
db.ctx,
- PS_INSERT_WITHDRAWAL,
- wopid,
+ PS_REGISTER_WITHDRAWAL_PARAMS,
resPubKeyBytes,
SELECTED,
ts.Unix(),
+ wopid,
)
if err != nil {
LogError("postgres", err)
return err
}
- LogInfo("postgres", "query="+PS_INSERT_WITHDRAWAL)
+ LogInfo("postgres", "query="+PS_REGISTER_WITHDRAWAL_PARAMS)
LogInfo("postgres", "registered withdrawal successfully. affected
rows="+strconv.Itoa(int(res.RowsAffected())))
return nil
}
+func (db *C2ECPostgres) GetWithdrawalByRequestUid(requestUid string)
(*Withdrawal, error) {
+
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_WITHDRAWAL_BY_RUID,
+ requestUid,
+ ); err != nil {
+ LogError("postgres", err)
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+ defer row.Close()
+ LogInfo("postgres", "query="+PS_GET_WITHDRAWAL_BY_RUID)
+ collected, err := pgx.CollectOneRow(row,
pgx.RowToAddrOfStructByName[Withdrawal])
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return collected, nil
+ }
+}
+
func (db *C2ECPostgres) GetWithdrawalById(withdrawalId int) (*Withdrawal,
error) {
if row, err := db.pool.Query(
@@ -252,16 +323,12 @@ func (db *C2ECPostgres) NotifyPayment(
wopid []byte,
providerTransactionId string,
terminalId int,
- amount Amount,
fees Amount,
) error {
res, err := db.pool.Exec(
db.ctx,
PS_PAYMENT_NOTIFICATION,
- amount.Value,
- amount.Fraction,
- amount.Currency,
fees.Value,
fees.Fraction,
fees.Currency,
diff --git a/c2ec/db.go b/c2ec/db.go
index b48d88f..ee975e0 100644
--- a/c2ec/db.go
+++ b/c2ec/db.go
@@ -19,12 +19,14 @@ const TERMINAL_FIELD_NAME_DESCRIPTION = "description"
const TERMINAL_FIELD_NAME_PROVIDER_ID = "provider_id"
const WITHDRAWAL_TABLE_NAME = "c2ec.withdrawal"
-const WITHDRAWAL_FIELD_NAME_ID = "withdrawal_id"
+const WITHDRAWAL_FIELD_NAME_ID = "withdrawal_row_id"
+const WITHDRAWAL_FIELD_NAME_RUID = "request_uid"
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_SUGGESTED_AMOUNT = "suggested_amount"
+const WITHDRAWAL_FIELD_NAME_FEES = "terminal_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"
@@ -60,12 +62,14 @@ type Terminal struct {
}
type Withdrawal struct {
- WithdrawalId uint64 `db:"withdrawal_id"`
+ WithdrawalRowId uint64 `db:"withdrawal_row_id"`
+ RequestUid string `db:"request_uid"`
Wopid []byte `db:"wopid"`
ReservePubKey []byte `db:"reserve_pub_key"`
RegistrationTs int64 `db:"registration_ts"`
Amount *TalerAmountCurrency `db:"amount"
scan:"follow"`
- Fees *TalerAmountCurrency `db:"fees"
scan:"follow"`
+ SuggestedAmount *TalerAmountCurrency `db:"suggested_amount"
scan:"follow"`
+ TerminalFees *TalerAmountCurrency `db:"terminal_fees"
scan:"follow"`
WithdrawalStatus WithdrawalOperationStatus `db:"withdrawal_status"`
TerminalId *int64 `db:"terminal_id"`
ProviderTransactionId *string
`db:"provider_transaction_id"`
@@ -101,13 +105,28 @@ type Notification struct {
// C2EC compliant database interface must implement
// in order to be bound to the c2ec API.
type C2ECDatabase interface {
- // Registers a wopid and reserve public key.
+ // A terminal sets up a withdrawal
+ // with this query.
// This initiates the withdrawal.
- RegisterWithdrawal(
+ SetupWithdrawal(
+ wopid []byte,
+ suggestedAmount Amount,
+ amount Amount,
+ providerTransactionId string,
+ terminalFees Amount,
+ requestUid string,
+ ) error
+
+ // Registers a reserve public key
+ // belonging to the respective wopid.
+ RegisterWithdrawalParameters(
wopid []byte,
resPubKey EddsaPublicKey,
) error
+ // Get the withdrawal associated with the given request uid.
+ GetWithdrawalByRequestUid(requestUid string) (*Withdrawal, error)
+
// Get the withdrawal associated with the given withdrawal identifier.
GetWithdrawalById(withdrawalId int) (*Withdrawal, error)
@@ -124,7 +143,6 @@ type C2ECDatabase interface {
wopid []byte,
providerTransactionId string,
terminalId int,
- amount Amount,
fees Amount,
) error
diff --git a/c2ec/db/0001-c2ec_schema.sql b/c2ec/db/0001-c2ec_schema.sql
index 7fbfa60..633f5c5 100644
--- a/c2ec/db/0001-c2ec_schema.sql
+++ b/c2ec/db/0001-c2ec_schema.sql
@@ -75,12 +75,14 @@ COMMENT ON COLUMN terminal.provider_id
CREATE TABLE IF NOT EXISTS withdrawal (
- withdrawal_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ withdrawal_row_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ request_uid TEXT UNIQUE NOT NULL,
wopid BYTEA CHECK (LENGTH(wopid)=32) UNIQUE NOT NULL,
- reserve_pub_key BYTEA CHECK (LENGTH(reserve_pub_key)=32) NOT NULL,
+ reserve_pub_key BYTEA CHECK (LENGTH(reserve_pub_key)=32),
registration_ts INT8 NOT NULL,
amount taler_amount_currency,
- fees taler_amount_currency,
+ suggested_amount taler_amount_currency,
+ terminal_fees taler_amount_currency,
withdrawal_status withdrawal_operation_status NOT NULL DEFAULT 'pending',
terminal_id INT8 REFERENCES terminal(terminal_id),
provider_transaction_id TEXT,
@@ -90,7 +92,10 @@ CREATE TABLE IF NOT EXISTS withdrawal (
);
COMMENT ON TABLE withdrawal
IS 'Table representing withdrawal processes initiated by terminals';
-COMMENT ON COLUMN withdrawal.withdrawal_id
+COMMENT ON COLUMN withdrawal.request_uid
+ IS 'The request uid identifies each request and is stored to make the API
interacting
+ with withdrawals idempotent.';
+COMMENT ON COLUMN withdrawal.withdrawal_row_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.
@@ -101,8 +106,11 @@ 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.suggested_amount
+ IS 'The suggested amount is given by the entity initializing the wihdrawal.
+ If the suggested amount is given, the wallet may still change the amount.';
+COMMENT ON COLUMN withdrawal.terminal_fees
+ IS 'Fees associated with the withdrawal but not related to the taler payment
system.';
COMMENT ON COLUMN withdrawal.withdrawal_status
IS 'Status of the withdrawal process';
COMMENT ON COLUMN withdrawal.terminal_id
diff --git a/c2ec/db/drop.sql b/c2ec/db/drop.sql
index 042a9c4..3694296 100644
--- a/c2ec/db/drop.sql
+++ b/c2ec/db/drop.sql
@@ -3,4 +3,6 @@ BEGIN;
DROP SCHEMA IF EXISTS c2ec CASCADE;
+DROP SCHEMA IF EXISTS _v CASCADE;
+
COMMIT;
\ No newline at end of file
diff --git a/c2ec/db/proc-c2ec_payment_notification_listener.sql
b/c2ec/db/proc-c2ec_payment_notification_listener.sql
index c9168b6..f6c54c4 100644
--- a/c2ec/db/proc-c2ec_payment_notification_listener.sql
+++ b/c2ec/db/proc-c2ec_payment_notification_listener.sql
@@ -15,10 +15,10 @@ BEGIN
ON t.provider_id = p.provider_id
LEFT JOIN c2ec.withdrawal AS w
ON t.terminal_id = NEW.terminal_id
- WHERE w.withdrawal_id = NEW.withdrawal_id;
+ WHERE w.withdrawal_row_id = NEW.withdrawal_row_id;
PERFORM pg_notify('payment_notification',
provider_name || '|' ||
- NEW.withdrawal_id || '|' ||
+ NEW.withdrawal_row_id || '|' ||
NEW.provider_transaction_id
);
RETURN NULL;
diff --git a/c2ec/db/proc-c2ec_retry_listener.sql
b/c2ec/db/proc-c2ec_retry_listener.sql
index 801735f..bcaad85 100644
--- a/c2ec/db/proc-c2ec_retry_listener.sql
+++ b/c2ec/db/proc-c2ec_retry_listener.sql
@@ -8,7 +8,7 @@ SET search_path TO c2ec;
CREATE OR REPLACE FUNCTION emit_retry_notification()
RETURNS TRIGGER AS $$
BEGIN
- PERFORM pg_notify('retry', '' || NEW.withdrawal_id);
+ PERFORM pg_notify('retry', '' || NEW.withdrawal_row_id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
diff --git a/c2ec/db/proc-c2ec_status_listener.sql
b/c2ec/db/proc-c2ec_status_listener.sql
index d40f5c8..38b10a0 100644
--- a/c2ec/db/proc-c2ec_status_listener.sql
+++ b/c2ec/db/proc-c2ec_status_listener.sql
@@ -28,13 +28,13 @@ 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
+CREATE OR REPLACE TRIGGER c2ec_withdrawal_status_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
+COMMENT ON TRIGGER c2ec_withdrawal_status_changed ON withdrawal
IS 'After the update of the status (only the status is of interest)
a notification shall be triggered using this trigger.';
diff --git a/c2ec/db/procedures.sql b/c2ec/db/procedures.sql
deleted file mode 100644
index 50ff87f..0000000
--- a/c2ec/db/procedures.sql
+++ /dev/null
@@ -1,162 +0,0 @@
-BEGIN;
-
-SELECT _v.register_patch('proc-c2ec-status-listener',
ARRAY['0001-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('w_' || encode(NEW.wopid::BYTEA, 'base64'),
NEW.withdrawal_status::TEXT);
- RETURN NULL;
-END;
-$$ LANGUAGE plpgsql;
-COMMENT ON FUNCTION emit_withdrawal_status
- IS 'The function encodes the wopid in base64 and
- sends a notification on the channel "w_{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;
-
-BEGIN;
-
-SELECT _v.register_patch('proc-c2ec-retry-listener',
ARRAY['0001-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_retry_notification()
-RETURNS TRIGGER AS $$
-BEGIN
- PERFORM pg_notify('retry', '' || NEW.withdrawal_id);
- RETURN NULL;
-END;
-$$ LANGUAGE plpgsql;
-COMMENT ON FUNCTION emit_retry_notification
- IS 'The function emits the id of the withdrawal for which the last
- retry timestamp was updated. This shall trigger a retry operation.
- How many retries are attempted is specified and handled by the
application';
-
--- 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_retry_notify
- AFTER UPDATE OF last_retry_ts
- ON withdrawal
- FOR EACH ROW
- EXECUTE FUNCTION emit_retry_notification();
-COMMENT ON TRIGGER c2ec_retry_notify ON withdrawal
- IS 'After setting the last retry timestamp on the withdrawal,
- trigger the retry mechanism through the respective mechanism.';
-
-COMMIT;
-
-BEGIN;
-
-SELECT _v.register_patch('proc-c2ec-payment-notification-listener',
ARRAY['0001-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_payment_notification()
-RETURNS TRIGGER AS $$
-DECLARE
- provider_name TEXT;
-BEGIN
- SELECT p.name INTO provider_name FROM c2ec.provider AS p
- LEFT JOIN c2ec.terminal AS t
- ON t.provider_id = p.provider_id
- LEFT JOIN c2ec.withdrawal AS w
- ON t.terminal_id = w.terminal_id
- WHERE w.withdrawal_id = NEW.withdrawal_id;
- PERFORM pg_notify('payment_notification',
- provider_name || '|' ||
- NEW.withdrawal_id || '|' ||
- NEW.provider_transaction_id
- );
- RETURN NULL;
-END;
-$$ LANGUAGE plpgsql;
-COMMENT ON FUNCTION emit_payment_notification
- IS 'The function emits the name of the provider, row id of the
withdrawal
- and the provider_transaction_id, on the channel "payment_notification".
- The format of the payload is as follows:
- "{PROVIDER_NAME}|{WITHDRAWAL_ID}|{PROVIDER_TRANSACTION_ID}". The subscriber
- shall decide which attestation process to use, based on the name of
- the provider.';
-
--- 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_on_payment_notify
- AFTER UPDATE OF provider_transaction_id
- ON withdrawal
- FOR EACH ROW
- WHEN (NEW.provider_transaction_id IS NOT NULL)
- EXECUTE FUNCTION emit_payment_notification();
-COMMENT ON TRIGGER c2ec_on_payment_notify ON withdrawal
- IS 'After setting the provider transaction id following a payment
notification,
- trigger the emit to the respective channel.';
-
-COMMIT;
-
-BEGIN;
-
-SELECT _v.register_patch('proc-c2ec-transfer-listener',
ARRAY['0001-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_transfer_notification()
-RETURNS TRIGGER AS $$
-BEGIN
- PERFORM pg_notify('transfer', encode(NEW.request_uid::BYTEA, 'base64'));
- RETURN NULL;
-END;
-$$ LANGUAGE plpgsql;
-COMMENT ON FUNCTION emit_transfer_notification
- IS 'The function emits the request_uid of a transfer which shall
trigger a transfer
- by the receiver of the notification.';
-
--- 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_on_transfer_failed
- AFTER INSERT
- ON transfer
- FOR EACH ROW
- EXECUTE FUNCTION emit_transfer_notification();
-COMMENT ON TRIGGER c2ec_on_transfer_failed ON transfer
- IS 'When a new transfer is set, the transfer shall executed. This trigger
aims to
- trigger this operation at its listeners.';
-
-CREATE OR REPLACE TRIGGER c2ec_on_transfer_failed
- AFTER UPDATE OF retries
- ON transfer
- FOR EACH ROW
- WHEN (NEW.retries > 0)
- EXECUTE FUNCTION emit_transfer_notification();
-COMMENT ON TRIGGER c2ec_on_transfer_failed ON transfer
- IS 'When retries is (re)set this will trigger the notification of the
listening
- receivers, which will further process the transfer';
-
-COMMIT;
\ No newline at end of file
diff --git a/c2ec/db/procedures.sql.in b/c2ec/db/procedures.sql.in
deleted file mode 100644
index 1bdd666..0000000
--- a/c2ec/db/procedures.sql.in
+++ /dev/null
@@ -1 +0,0 @@
-Note: cat into procedures.sql
\ No newline at end of file
diff --git a/c2ec/db/test_c2ec_test.sql b/c2ec/db/test_c2ec_simulation.sql
similarity index 100%
rename from c2ec/db/test_c2ec_test.sql
rename to c2ec/db/test_c2ec_simulation.sql
diff --git a/c2ec/db/test_c2ec_test_rollback.sql
b/c2ec/db/test_c2ec_simulation_rollback.sql
similarity index 100%
rename from c2ec/db/test_c2ec_test_rollback.sql
rename to c2ec/db/test_c2ec_simulation_rollback.sql
diff --git a/c2ec/encoding.go b/c2ec/encoding.go
index e00a8f4..59fec08 100644
--- a/c2ec/encoding.go
+++ b/c2ec/encoding.go
@@ -1,43 +1,24 @@
package main
import (
- "encoding/base32"
"errors"
- "net/url"
+ "math"
"strings"
)
-// 32 characters for decoding, using RFC 3548.
-const TALER_BASE32_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+func talerBinaryEncode(byts []byte) string {
-func talerBase32Encode(byts []byte) string {
- return talerBase32Encoding().EncodeToString(byts)
+ return encodeCrock(byts)
}
-func talerBase32Decode(str string) ([]byte, error) {
+func talerBinaryDecode(str string) ([]byte, error) {
- decoded, err := talerBase32Encoding().DecodeString(strings.ToUpper(str))
- if err != nil {
- return nil, err
- }
- return decoded, nil
-}
-
-func talerBase32Encoding() *base32.Encoding {
- // 32 characters for decoding, using RFC 3548.
- // character set copied from
[TALER-EXCHANGE]/src/util/crypto_confirmation.c
- return base32.NewEncoding(TALER_BASE32_CHARACTER_SET)
+ return decodeCrock(str)
}
func ParseWopid(wopid string) ([]byte, error) {
- unescaped, err := url.PathUnescape(wopid)
- if err != nil {
- LogError("encoding", err)
- return nil, errors.New("decoding failed")
- }
-
- wopidBytes, err := talerBase32Decode(unescaped)
+ wopidBytes, err := talerBinaryDecode(wopid)
if err != nil {
return nil, err
}
@@ -53,15 +34,105 @@ func ParseWopid(wopid string) ([]byte, error) {
func FormatWopid(wopid []byte) string {
- return url.PathEscape(talerBase32Encode(wopid))
+ return talerBinaryEncode(wopid)
}
func ParseEddsaPubKey(key EddsaPublicKey) ([]byte, error) {
- return talerBase32Decode(string(key))
+ return talerBinaryDecode(string(key))
}
func FormatEddsaPubKey(key []byte) EddsaPublicKey {
- return EddsaPublicKey(talerBase32Encode(key))
+ return EddsaPublicKey(talerBinaryEncode(key))
+}
+
+func decodeCrock(e string) ([]byte, error) {
+ size := len(e)
+ bitpos := 0
+ bitbuf := 0
+ readPosition := 0
+ outLen := int(math.Floor((float64(size) * 5.0) / 8.0))
+ out := make([]byte, outLen)
+ outPos := 0
+
+ getValue := func(c byte) (int, error) {
+ alphabet := "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+ switch c {
+ case 'o', 'O':
+ return 0, nil
+ case 'i', 'I', 'l', 'L':
+ return 1, nil
+ case 'u', 'U':
+ return 27, nil
+ }
+
+ i := strings.IndexRune(alphabet, rune(c))
+ if i > -1 && i < 32 {
+ return i, nil
+ }
+
+ return -1, errors.New("encoding error")
+ }
+
+ for readPosition < size || bitpos > 0 {
+ if readPosition < size {
+ v, err := getValue(e[readPosition])
+ if err != nil {
+ return nil, err
+ }
+ readPosition++
+ bitbuf = bitbuf<<5 | v
+ bitpos += 5
+ }
+ for bitpos >= 8 {
+ d := byte(bitbuf >> (bitpos - 8) & 0xff)
+ out[outPos] = d
+ outPos++
+ bitpos -= 8
+ }
+ if readPosition == size && bitpos > 0 {
+ bitbuf = bitbuf << (8 - bitpos) & 0xff
+ if bitbuf == 0 {
+ bitpos = 0
+ } else {
+ bitpos = 8
+ }
+ }
+ }
+ return out, nil
+}
+
+func encodeCrock(data []byte) string {
+ out := ""
+ bitbuf := 0
+ bitpos := 0
+
+ encodeValue := func(value int) byte {
+ alphabet := "ABCDEFGHJKMNPQRSTVWXYZ"
+ switch {
+ case value >= 0 && value <= 9:
+ return byte('0' + value)
+ case value >= 10 && value <= 31:
+ return alphabet[value-10]
+ default:
+ panic("Invalid value for encoding")
+ }
+ }
+
+ for _, b := range data {
+ bitbuf = bitbuf<<8 | int(b&0xff)
+ bitpos += 8
+ for bitpos >= 5 {
+ value := bitbuf >> (bitpos - 5) & 0x1f
+ out += string(encodeValue(value))
+ bitpos -= 5
+ }
+ }
+ if bitpos > 0 {
+ bitbuf = bitbuf << (5 - bitpos)
+ value := bitbuf & 0x1f
+ out += string(encodeValue(value))
+ }
+ return out
}
diff --git a/c2ec/encoding_test.go b/c2ec/encoding_test.go
index 1b9dbd0..d9ad0e7 100644
--- a/c2ec/encoding_test.go
+++ b/c2ec/encoding_test.go
@@ -41,9 +41,9 @@ func TestTalerBase32(t *testing.T) {
input := []byte("This is some text")
t.Log("in:", string(input))
t.Log("in:", input)
- encoded := talerBase32Encode(input)
+ encoded := talerBinaryEncode(input)
t.Log("encoded:", encoded)
- out, err := talerBase32Decode(encoded)
+ out, err := talerBinaryDecode(encoded)
if err != nil {
t.Error(err)
t.FailNow()
@@ -75,9 +75,9 @@ func TestTalerBase32Rand32(t *testing.T) {
}
t.Log("in:", input)
- encoded := talerBase32Encode(input)
+ encoded := talerBinaryEncode(input)
t.Log("encoded:", encoded)
- out, err := talerBase32Decode(encoded)
+ out, err := talerBinaryDecode(encoded)
if err != nil {
t.Error(err)
t.FailNow()
@@ -109,9 +109,9 @@ func TestTalerBase32Rand64(t *testing.T) {
}
t.Log("in:", input)
- encoded := talerBase32Encode(input)
+ encoded := talerBinaryEncode(input)
t.Log("encoded:", encoded)
- out, err := talerBase32Decode(encoded)
+ out, err := talerBinaryDecode(encoded)
if err != nil {
t.Error(err)
t.FailNow()
diff --git a/c2ec/http-util.go b/c2ec/http-util.go
index f753050..bb91d1e 100644
--- a/c2ec/http-util.go
+++ b/c2ec/http-util.go
@@ -20,31 +20,6 @@ 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.WriteHeader(status)
- res.Write(problm)
- return nil
-}
-
// Function reads and validates a param of a request in the
// correct format according to the transform function supplied.
// When the transform fails, it returns false as second return
@@ -61,15 +36,7 @@ func AcceptOptionalParamOrWriteResponse[T any](
ptr, err := OptionalQueryParamOrError(name, transform, req)
if err != nil {
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_INVALID_REQUEST_QUERY_PARAMETER",
- Title: "invalid request query parameter",
- Detail: "the withdrawal status request parameter '" +
name + "' is malformed (error: " + err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ res.WriteHeader(HTTP_BAD_REQUEST)
return nil, false
}
@@ -82,15 +49,7 @@ func AcceptOptionalParamOrWriteResponse[T any](
assertedObj, ok := any(obj).(T)
if !ok {
// this should generally not happen (due to the implementation)
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_FATAL_ERROR",
- Title: "Fatal Error",
- Detail: "Something strange happened. Probably not
your fault.",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
return nil, false
}
return &assertedObj, true
@@ -178,6 +137,9 @@ func HttpGet[T any](
if codec == nil {
return nil, res.StatusCode, err
} else {
+ if res.StatusCode > 299 {
+ return nil, res.StatusCode, nil
+ }
resBody, err := codec.Decode(res.Body)
return resBody, res.StatusCode, err
}
diff --git a/c2ec/install/build_app.sh b/c2ec/install/build_app.sh
new file mode 100644
index 0000000..87d872f
--- /dev/null
+++ b/c2ec/install/build_app.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+if [ "$#" -ne 1 ]; then
+ echo "Usage: $0 <source-root>"
+ exit 1
+fi
+
+REPO_ROOT=$1
+
+build_c2ec() {
+ go build $REPO_ROOT
+ if [ $? -ne 0 ]; then
+ echo "Failed to build C2EC using Go"
+ exit 1
+ fi
+}
+
+build_c2ec
+if [ $? -ne 0 ]; then
+ exit 1
+fi
diff --git a/c2ec/install/installation_notes.md
b/c2ec/install/installation_notes.md
new file mode 100644
index 0000000..8858173
--- /dev/null
+++ b/c2ec/install/installation_notes.md
@@ -0,0 +1,33 @@
+# Installation of C2EC and surrounding components
+
+how to install exchange and C2EC
+
+## Prerequisites
+
+- Debian
+- git
+- postgres >= 15.6
+- it's a good idea to read [Exchange Operator
Manual](https://docs.taler.net/taler-exchange-manual.html) first
+
+## Required Exchange binaries
+
+To allow the withdrawal of Taler, I will need following binaries:
+
+- taler-exchange-httpd
+- taler-exchange-secmod-rsa
+- taler-exchange-secmod-cs
+- taler-exchange-secmod-eddsa
+- taler-exchange-closer
+- taler-exchange-wirewatch
+
+## Setup Commands
+
+`sudo echo "deb [signed-by=/etc/apt/keyrings/taler-systems.gpg]
https://deb.taler.net/apt/debian bookworm main" >
/etc/apt/sources.list.d/taler.list`
+
+`sudo wget -O /etc/apt/keyrings/taler-systems.gpg
https://taler.net/taler-systems.gpg`
+
+`sudo apt update`
+
+`sudo apt install taler-exchange`
+
+## Configure
\ No newline at end of file
diff --git a/c2ec/install/setup_db.sh b/c2ec/install/setup_db.sh
new file mode 100755
index 0000000..6038997
--- /dev/null
+++ b/c2ec/install/setup_db.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+if [ "$#" -ne 4 ]; then
+ echo "Usage: $0 <db-username> <db-password> <db-name> <source-root>"
+ exit 1
+fi
+
+DB_USERNAME=$1
+DB_PASSWORD=$2
+DB_NAME=$3
+REPO_ROOT=$4
+
+SQL_SCRIPTS=(
+ "$REPO_ROOT/db/versioning.sql"
+ "$REPO_ROOT/db/0001-c2ec_schema.sql"
+ "$REPO_ROOT/db/proc-c2ec_status_listener.sql"
+ "$REPO_ROOT/db/proc-c2ec_payment_notification_listener.sql"
+ "$REPO_ROOT/db/proc-c2ec_retry_listener.sql"
+ "$REPO_ROOT/db/proc-c2ec_transfer_listener.sql"
+)
+
+execute_sql_scripts() {
+ for script in "${SQL_SCRIPTS[@]}"; do
+ echo "Executing SQL script: $script"
+ PGPASSWORD=$DB_PASSWORD psql -U $DB_USERNAME -d $DB_NAME -f $script
+ if [ $? -ne 0 ]; then
+ echo "Failed to execute SQL script: $script"
+ exit 1
+ fi
+ done
+ PGPASSWORD=""
+}
+
+execute_sql_scripts
+if [ $? -ne 0 ]; then
+ exit 1
+fi
diff --git a/c2ec/install/start.sh b/c2ec/install/start.sh
new file mode 100755
index 0000000..031da29
--- /dev/null
+++ b/c2ec/install/start.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+if [ "$#" -ne 1 ]; then
+ echo "Usage: $0 <config-file-path>"
+ exit 1
+fi
+CONFIG_FILE=$1
+
+build_and_run_go_app() {
+ go build -o app
+ if [ $? -ne 0 ]; then
+ echo "Failed to build Go application"
+ exit 1
+ fi
+
+ ./app "$CONFIG_FILE"
+ if [ $? -ne 0 ]; then
+ echo "Failed to run Go application"
+ exit 1
+ fi
+}
+
+build_and_run_go_app
+
+rm -f app
\ No newline at end of file
diff --git a/c2ec/install/wipe_db.sh b/c2ec/install/wipe_db.sh
new file mode 100755
index 0000000..69088e9
--- /dev/null
+++ b/c2ec/install/wipe_db.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+if [ "$#" -ne 3 ]; then
+ echo "Usage: $0 <db-username> <db-password> <db-name>"
+ exit 1
+fi
+
+DB_USERNAME=$1
+DB_PASSWORD=$2
+DB_NAME=$3
+
+SQL_SCRIPTS=(
+ "./../db/drop.sql"
+)
+
+execute_sql_scripts() {
+ for script in "${SQL_SCRIPTS[@]}"; do
+ PGPASSWORD=$DB_PASSWORD psql -U $DB_USERNAME -d $DB_NAME -f "$script"
+ if [ $? -ne 0 ]; then
+ echo "Failed to execute SQL script: $script"
+ exit 1
+ fi
+ done
+ PGPASSWORD=""
+}
+
+execute_sql_scripts
diff --git a/c2ec/main.go b/c2ec/main.go
index 3eb8d98..7f1195e 100644
--- a/c2ec/main.go
+++ b/c2ec/main.go
@@ -15,8 +15,9 @@ import (
const GET = "GET "
const POST = "POST "
-const BANK_INTEGRATION_API = "/c2ec"
-const WIRE_GATEWAY_API = "/wire"
+//
https://docs.taler.net/core/api-terminal.html#endpoints-for-integrated-sub-apis
+const BANK_INTEGRATION_API = "/taler-integration"
+const WIRE_GATEWAY_API = "/taler-wire-gateway"
const DEFAULT_C2EC_CONFIG_PATH = "c2ec-config.yaml" // "c2ec-config.conf"
@@ -94,6 +95,8 @@ func main() {
setupWireGatewayRoutes(router)
+ setupTerminalRoutes(router)
+
server := http.Server{
Handler: router,
}
@@ -241,19 +244,14 @@ func setupBankIntegrationRoutes(router *http.ServeMux) {
bankIntegrationConfig,
)
- router.HandleFunc(
- POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN,
- handleWithdrawalRegistration,
- )
-
router.HandleFunc(
GET+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN,
handleWithdrawalStatus,
)
router.HandleFunc(
- POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_PAYMENT_PATTERN,
- handlePaymentNotification,
+ POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN,
+ handleParameterRegistration,
)
router.HandleFunc(
@@ -289,3 +287,26 @@ func setupWireGatewayRoutes(router *http.ServeMux) {
adminAddIncoming,
)
}
+
+func setupTerminalRoutes(router *http.ServeMux) {
+
+ router.HandleFunc(
+ GET+TERMINAL_API_CONFIG,
+ handleTerminalConfig,
+ )
+
+ router.HandleFunc(
+ POST+TERMINAL_API_REGISTER_WITHDRAWAL,
+ handleWithdrawalSetup,
+ )
+
+ router.HandleFunc(
+ POST+TERMINAL_API_CHECK_WITHDRAWAL,
+ handleWithdrawalCheck,
+ )
+
+ router.HandleFunc(
+ GET+TERMINAL_API_WITHDRAWAL_STATUS,
+ handleWithdrawalStatus,
+ )
+}
diff --git a/c2ec/attestor.go b/c2ec/proc-attestor.go
similarity index 98%
rename from c2ec/attestor.go
rename to c2ec/proc-attestor.go
index fb2bda2..443f2ea 100644
--- a/c2ec/attestor.go
+++ b/c2ec/proc-attestor.go
@@ -47,7 +47,7 @@ func attestationCallback(notification *Notification, errs
chan error) {
}
withdrawalRowId, err := strconv.Atoi(payload[1])
if err != nil {
- errs <- errors.New("malformed withdrawal_id: " + err.Error())
+ errs <- errors.New("malformed withdrawal_row_id: " +
err.Error())
return
}
providerTransactionId := payload[2]
@@ -136,7 +136,7 @@ func prepareRetryOrAbort(
if withdrawal.RetryCounter >= CONFIG.Server.MaxRetries {
- LogInfo("attestor", fmt.Sprintf("max retries for withdrawal
with id=%d was reached. withdrawal is aborted.", withdrawal.WithdrawalId))
+ LogInfo("attestor", fmt.Sprintf("max retries for withdrawal
with id=%d was reached. withdrawal is aborted.", withdrawal.WithdrawalRowId))
err := DB.FinaliseWithdrawal(withdrawalRowId, ABORTED,
make([]byte, 0))
if err != nil {
LogError("attestor", err)
diff --git a/c2ec/listener.go b/c2ec/proc-listener.go
similarity index 100%
rename from c2ec/listener.go
rename to c2ec/proc-listener.go
diff --git a/c2ec/transfer.go b/c2ec/proc-transfer.go
similarity index 100%
rename from c2ec/transfer.go
rename to c2ec/proc-transfer.go
diff --git a/cli/cli.go b/cli/cli.go
index ea36f23..88f9b72 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -2,6 +2,7 @@ package main
import (
"bufio"
+ "bytes"
"context"
"crypto/rand"
"encoding/base64"
@@ -16,6 +17,7 @@ import (
)
const ACTION_HELP = "h"
+const ACTION_SETUP_SIMULATION = "sim"
const ACTION_REGISTER_PROVIDER = "rp"
const ACTION_REGISTER_TERMINAL = "rt"
const ACTION_DEACTIVATE_TERMINAL = "dt"
@@ -147,7 +149,7 @@ func registerWalleeTerminal() error {
INSERT_TERMINAL,
hashedAccessToken,
description,
- p.ProviderTerminalID,
+ p.ProviderId,
)
if err != nil {
return err
@@ -176,6 +178,10 @@ func registerWalleeTerminal() error {
func deactivateTerminal() error {
+ if DB == nil {
+ return errors.New("connect to the database first (cmd: db)")
+ }
+
fmt.Println("You are about to deactivate terminal which allows
withdrawals. This will make the terminal unusable.")
tuid := read("Terminal-User-Id: ")
parts := strings.Split(tuid, "-")
@@ -199,6 +205,94 @@ func deactivateTerminal() error {
return nil
}
+func setupSimulation() error {
+
+ if DB == nil {
+ return errors.New("connect to the database first (cmd: db)")
+ }
+
+ // SETTING UP PROVIDER
+ fmt.Println("Setting up simulation provider and terminal.")
+ name := "Simulation"
+ paytotargettype := "void"
+ backendUrl := "simulation provider will not contact any backend."
+ credsEncoded :=
base64.StdEncoding.EncodeToString(bytes.NewBufferString("simulation provider
will not contact any backend.").Bytes())
+
+ _, err := DB.Exec(
+ context.Background(),
+ INSERT_PROVIDER,
+ name,
+ paytotargettype,
+ backendUrl,
+ credsEncoded,
+ )
+ if err != nil {
+ return err
+ }
+
+ // SETTING UP TERMINAL
+ description := "simulation terminal"
+
+ rows, err := DB.Query(
+ context.Background(),
+ GET_PROVIDER_BY_NAME,
+ name,
+ )
+ if err != nil {
+ return err
+ }
+
+ p, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Provider])
+ if err != nil {
+ return err
+ }
+ rows.Close() // release rows / connection
+
+ accessToken := make([]byte, 32)
+ _, err = rand.Read(accessToken)
+ if err != nil {
+ return err
+ }
+
+ accessTokenBase64 := base64.StdEncoding.EncodeToString(accessToken)
+
+ hashedAccessToken, err := pbkdf(accessTokenBase64)
+ if err != nil {
+ return err
+ }
+
+ _, err = DB.Exec(
+ context.Background(),
+ INSERT_TERMINAL,
+ hashedAccessToken,
+ description,
+ p.ProviderId,
+ )
+ if err != nil {
+ return err
+ }
+
+ fmt.Println("looking up last inserted terminal")
+ rows, err = DB.Query(
+ context.Background(),
+ GET_LAST_INSERTED_TERMINAL,
+ )
+ if err != nil {
+ return err
+ }
+ t, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Terminal])
+ if err != nil {
+ return err
+ }
+ rows.Close()
+
+ fmt.Println("Terminal-User-Id (used to identify terminal at the api.
You want to note this):", name+"-"+strconv.Itoa(int(t.TerminalID)))
+ fmt.Println("GENERATED ACCESS-TOKEN (save it in your password manager.
Can't be recovered!!):")
+ fmt.Println(accessTokenBase64)
+
+ return nil
+}
+
func connectDatabase() error {
u := read("Username: ")
@@ -234,6 +328,7 @@ func showHelp() error {
fmt.Println("register wallee provider (", ACTION_REGISTER_PROVIDER, ")")
fmt.Println("register wallee terminal (", ACTION_REGISTER_TERMINAL, ")")
fmt.Println("deactivate wallee terminal (", ACTION_DEACTIVATE_TERMINAL,
")")
+ fmt.Println("setup simulation (", ACTION_SETUP_SIMULATION, ")")
fmt.Println("connect database (", ACTION_CONNECT_DB, ")")
fmt.Println("show help (", ACTION_HELP, ")")
fmt.Println("quit (", ACTION_QUIT, ")")
@@ -299,6 +394,8 @@ func dispatchCommand(cmd string) error {
err = registerWalleeTerminal()
case ACTION_DEACTIVATE_TERMINAL:
err = deactivateTerminal()
+ case ACTION_SETUP_SIMULATION:
+ err = setupSimulation()
default:
fmt.Println("unknown action")
}
diff --git a/cli/db.go b/cli/db.go
index 246e858..d75295a 100644
--- a/cli/db.go
+++ b/cli/db.go
@@ -7,7 +7,7 @@ const GET_PROVIDER_BY_NAME = "SELECT * FROM c2ec.provider WHERE
name=$1"
const GET_LAST_INSERTED_TERMINAL = "SELECT * FROM c2ec.terminal WHERE
terminal_id = (SELECT MAX(terminal_id) FROM c2ec.terminal)"
type Provider struct {
- ProviderTerminalID int64 `db:"provider_id"`
+ ProviderId int64 `db:"provider_id"`
Name string `db:"name"`
PaytoTargetType string `db:"payto_target_type"`
BackendBaseURL string `db:"backend_base_url"`
diff --git a/docs/content/.gitkeep
b/docs/content/implementation/bank-integration.tex
similarity index 100%
copy from docs/content/.gitkeep
copy to docs/content/implementation/bank-integration.tex
diff --git a/docs/content/implementation/terminal-api.tex
b/docs/content/implementation/terminal-api.tex
new file mode 100644
index 0000000..81ac4d5
--- /dev/null
+++ b/docs/content/implementation/terminal-api.tex
@@ -0,0 +1,27 @@
+\subsection{Terminal API}
+
+This section describes the Implementation of the Terminal API
\cite{taler-terminal-api}.
+
+The C2EC component does not implement the \texttt{/quotas/*} endpoints, since
those are not relevant for the withdrawal using a payment terminal.
+
+The exact specification can be found in (TODO REF APPENDIX)
+
+\subsubsection{Configuration (/config)}
+
+This endpoint returns the configuration. Especially the fields
\texttt{currency} and \texttt{wire_type} are interesting, since they are used
to
+
+\subsubsection{Registering a withdrawal (/withdrawals)}
+
+The registration of a withdrawal initializes the flow by the
+
+\subsubsection{Trigger Attestation (/withdrawals/[wopid]/check)}
+
+
+
+\subsubsection{Taler Integration (/taler-integration/*)}
+
+
+
+\subsubsection{Taler Integration (/taler-wire-gateway/*)}
+
+
diff --git a/docs/content/.gitkeep
b/docs/content/implementation/wire-gateway.tex
similarity index 100%
copy from docs/content/.gitkeep
copy to docs/content/implementation/wire-gateway.tex
diff --git a/docs/project.bib b/docs/project.bib
index 32deea7..97400f7 100644
--- a/docs/project.bib
+++ b/docs/project.bib
@@ -226,6 +226,13 @@
howpublished =
{\url{https://docs.taler.net/core/api-corebank.html#authentication}}
}
+@misc{taler-terminal-api,
+ author = {Taler},
+ howpublished = {\url{https://docs.taler.net/core/api-terminal.html}},
+ title = {Terminal API},
+ url =
{https://docs.taler.net/core/api-terminal.html#endpoints-for-integrated-sub-apis}
+}
+
@misc{taler-design-document-49,
author = {Taler},
title = {Authentication},
diff --git a/simulation/c2ec-simulation b/simulation/c2ec-simulation
index d1066dd..781d796 100755
Binary files a/simulation/c2ec-simulation and b/simulation/c2ec-simulation
differ
diff --git a/simulation/encoding.go b/simulation/encoding.go
index 98d1d4a..0ccaf53 100644
--- a/simulation/encoding.go
+++ b/simulation/encoding.go
@@ -1,51 +1,36 @@
package main
import (
- "encoding/base32"
"errors"
- "fmt"
- "net/url"
+ "math"
"strings"
)
-// 32 characters for decoding, using RFC 3548.
-const TALER_BASE32_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+func talerBinaryEncode(byts []byte) string {
-func talerBase32Encode(byts []byte) string {
- return talerBase32Encoding().EncodeToString(byts)
+ return encodeCrock(byts)
+ //return talerBase32Encoding().EncodeToString(byts)
}
-func talerBase32Decode(str string) ([]byte, error) {
+func talerBinaryDecode(str string) ([]byte, error) {
- decoded, err := talerBase32Encoding().DecodeString(strings.ToUpper(str))
- if err != nil {
- return nil, err
- }
- return decoded, nil
-}
-
-func talerBase32Encoding() *base32.Encoding {
- // 32 characters for decoding, using RFC 3548.
- // character set copied from
[TALER-EXCHANGE]/src/util/crypto_confirmation.c
- return base32.NewEncoding(TALER_BASE32_CHARACTER_SET)
+ return decodeCrock(str)
+ // decoded, err :=
talerBase32Encoding().DecodeString(strings.ToUpper(str))
+ // if err != nil {
+ // return nil, err
+ // }
+ // return decoded, nil
}
func ParseWopid(wopid string) ([]byte, error) {
- unescaped, err := url.PathUnescape(wopid)
- if err != nil {
- fmt.Println("encoding", err)
- return nil, errors.New("decoding failed")
- }
-
- wopidBytes, err := talerBase32Decode(unescaped)
+ wopidBytes, err := talerBinaryDecode(wopid)
if err != nil {
return nil, err
}
if len(wopidBytes) != 32 {
err = errors.New("invalid wopid")
- fmt.Println("encoding", err)
return nil, err
}
@@ -54,10 +39,105 @@ func ParseWopid(wopid string) ([]byte, error) {
func FormatWopid(wopid []byte) string {
- return url.PathEscape(talerBase32Encode(wopid))
+ return talerBinaryEncode(wopid)
}
func ParseEddsaPubKey(key EddsaPublicKey) ([]byte, error) {
- return talerBase32Decode(string(key))
+ return talerBinaryDecode(string(key))
+}
+
+func FormatEddsaPubKey(key []byte) EddsaPublicKey {
+
+ return EddsaPublicKey(talerBinaryEncode(key))
+}
+
+func decodeCrock(e string) ([]byte, error) {
+ size := len(e)
+ bitpos := 0
+ bitbuf := 0
+ readPosition := 0
+ outLen := int(math.Floor((float64(size) * 5.0) / 8.0))
+ out := make([]byte, outLen)
+ outPos := 0
+
+ getValue := func(c byte) (int, error) {
+ alphabet := "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+ switch c {
+ case 'o', 'O':
+ return 0, nil
+ case 'i', 'I', 'l', 'L':
+ return 1, nil
+ case 'u', 'U':
+ return 27, nil
+ }
+
+ i := strings.IndexRune(alphabet, rune(c))
+ if i > -1 && i < 32 {
+ return i, nil
+ }
+
+ return -1, errors.New("encoding error")
+ }
+
+ for readPosition < size || bitpos > 0 {
+ if readPosition < size {
+ v, err := getValue(e[readPosition])
+ if err != nil {
+ return nil, err
+ }
+ readPosition++
+ bitbuf = bitbuf<<5 | v
+ bitpos += 5
+ }
+ for bitpos >= 8 {
+ d := byte(bitbuf >> (bitpos - 8) & 0xff)
+ out[outPos] = d
+ outPos++
+ bitpos -= 8
+ }
+ if readPosition == size && bitpos > 0 {
+ bitbuf = bitbuf << (8 - bitpos) & 0xff
+ if bitbuf == 0 {
+ bitpos = 0
+ } else {
+ bitpos = 8
+ }
+ }
+ }
+ return out, nil
+}
+
+func encodeCrock(data []byte) string {
+ out := ""
+ bitbuf := 0
+ bitpos := 0
+
+ encodeValue := func(value int) byte {
+ alphabet := "ABCDEFGHJKMNPQRSTVWXYZ"
+ switch {
+ case value >= 0 && value <= 9:
+ return byte('0' + value)
+ case value >= 10 && value <= 31:
+ return alphabet[value-10]
+ default:
+ panic("Invalid value for encoding")
+ }
+ }
+
+ for _, b := range data {
+ bitbuf = bitbuf<<8 | int(b&0xff)
+ bitpos += 8
+ for bitpos >= 5 {
+ value := bitbuf >> (bitpos - 5) & 0x1f
+ out += string(encodeValue(value))
+ bitpos -= 5
+ }
+ }
+ if bitpos > 0 {
+ bitbuf = bitbuf << (5 - bitpos)
+ value := bitbuf & 0x1f
+ out += string(encodeValue(value))
+ }
+ return out
}
diff --git a/simulation/go.mod b/simulation/go.mod
index ff3212f..dde9d9d 100644
--- a/simulation/go.mod
+++ b/simulation/go.mod
@@ -1,3 +1,5 @@
module c2ec-simulation
go 1.22.1
+
+require github.com/gofrs/uuid v4.4.0+incompatible // indirect
diff --git a/simulation/go.sum b/simulation/go.sum
new file mode 100644
index 0000000..c0ad687
--- /dev/null
+++ b/simulation/go.sum
@@ -0,0 +1,2 @@
+github.com/gofrs/uuid v4.4.0+incompatible
h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod
h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
diff --git a/simulation/http-util.go b/simulation/http-util.go
index afebb9f..72c7f67 100644
--- a/simulation/http-util.go
+++ b/simulation/http-util.go
@@ -3,7 +3,6 @@ package main
import (
"bytes"
- "errors"
"fmt"
"net/http"
"strings"
@@ -185,12 +184,6 @@ func HttpGet[T any](
}
if res.StatusCode > 299 {
- errBody, err := NewJsonCodec[RFC9457Problem]().Decode(res.Body)
- if err != nil {
- fmt.Println("error happened on GET. Failed parsing
error")
- return nil, -1, err
- }
- fmt.Printf("Error (%d): %s (%s)", res.StatusCode,
errBody.Title, errBody.Detail)
return nil, res.StatusCode, nil
}
@@ -202,101 +195,40 @@ func HttpGet[T any](
}
}
-// 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,
+ url string,
+ headers map[string]string,
body *T,
- requestCodec Codec[T],
- responseCodec Codec[R],
+ reqCodec Codec[T],
+ resCodec 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
- }
-
- buf := make([]byte, res.ContentLength)
- _, err = res.Body.Read(buf)
- if err != nil {
- fmt.Println("body after post:", string(buf))
- }
- }
+ bodyEncoded, err := reqCodec.EncodeToBytes(body)
+ if err != nil {
+ return nil, -1, err
}
- if responseCodec == nil {
- return nil, res.StatusCode, nil
+ req, err := http.NewRequest(HTTP_POST, url,
bytes.NewBuffer(bodyEncoded))
+ if err != nil {
+ return nil, -1, err
}
- if res.StatusCode > 299 {
- errBody, err := NewJsonCodec[RFC9457Problem]().Decode(res.Body)
- if err != nil {
- return nil, -1, err
- }
- fmt.Printf("Error (%d): %s (%s)", res.StatusCode,
errBody.Title, errBody.Detail)
- return nil, res.StatusCode, nil
+ for k, v := range headers {
+ req.Header.Add(k, v)
}
+ req.Header.Add("Accept", reqCodec.HttpApplicationContentHeader())
- resBody, err := responseCodec.Decode(res.Body)
+ res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, -1, err
}
- return resBody, res.StatusCode, err
+
+ if resCodec == nil {
+ return nil, res.StatusCode, err
+ } else {
+ resBody, err := resCodec.Decode(res.Body)
+ return resBody, res.StatusCode, err
+ }
}
// builds request URL containing the path and query
diff --git a/simulation/main.go b/simulation/main.go
index 3eef8cd..7e9bbd4 100644
--- a/simulation/main.go
+++ b/simulation/main.go
@@ -8,11 +8,6 @@ import (
const DISABLE_DELAYS = true
const C2EC_BASE_URL = "http://localhost:8082"
-const C2EC_BANK_BASE_URL = C2EC_BASE_URL + "/c2ec"
-const C2EC_BANK_CONFIG_URL = C2EC_BANK_BASE_URL + "/config"
-const C2EC_BANK_WITHDRAWAL_STATUS_URL = C2EC_BANK_BASE_URL +
"/withdrawal-operation/:wopid"
-const C2EC_BANK_WITHDRAWAL_REGISTRATION_URL = C2EC_BANK_BASE_URL +
"/withdrawal-operation/:wopid"
-const C2EC_BANK_WITHDRAWAL_PAYMENT_URL = C2EC_BANK_BASE_URL +
"/withdrawal-operation/:wopid/confirm"
// simulates the terminal talking to its backend system and executing the
payment.
const PROVIDER_BACKEND_PAYMENT_DELAY_MS = 1000
@@ -26,25 +21,6 @@ const TERMINAL_ACCEPT_CARD_DELAY_MS = 5000
// simulates the user scanning the QR code presented at the terminal
const WALLET_SCAN_QR_CODE_DELAY_MS = 5000
-//
https://docs.taler.net/core/api-exchange.html#tsref-type-CurrencySpecification
-type CurrencySpecification struct {
- Name string `json:"name"`
- Currency string `json:"currency"`
- NumFractionalInputDigits int
`json:"num_fractional_input_digits"`
- NumFractionalNormalDigits int
`json:"num_fractional_normal_digits"`
- NumFractionalTrailingZeroDigits int
`json:"num_fractional_trailing_zero_digits"`
- AltUnitNames string `json:"alt_unit_names"`
-}
-
-//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankIntegrationConfig
-type BankIntegrationConfig struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Implementation string `json:"implementation"`
- Currency string `json:"currency"`
- CurrencySpecification CurrencySpecification
`json:"currency_specification"`
-}
-
type SimulatedPhysicalInteraction struct {
Msg string
}
diff --git a/simulation/model.go b/simulation/model.go
index 51c94b4..ba55911 100644
--- a/simulation/model.go
+++ b/simulation/model.go
@@ -60,10 +60,3 @@ type C2ECWithdrawalStatus struct {
WireTypes []string `json:"wire_types"`
ReservePubKey EddsaPublicKey `json:"selected_reserve_pub"`
}
-
-type C2ECPaymentNotification struct {
- ProviderTransactionId string `json:"provider_transaction_id"`
- TerminalId int `json:"terminal_id"`
- Amount Amount `json:"amount"`
- Fees Amount `json:"card_fees"`
-}
diff --git a/simulation/sim-terminal.go b/simulation/sim-terminal.go
index a639fc8..827614d 100644
--- a/simulation/sim-terminal.go
+++ b/simulation/sim-terminal.go
@@ -1,65 +1,131 @@
package main
import (
- "bytes"
- "crypto/rand"
"encoding/base64"
"errors"
"fmt"
- "net/http"
"strconv"
"time"
+
+ "github.com/gofrs/uuid"
)
+const C2EC_TERMINAL_CONFIG_API = C2EC_BASE_URL + "/config"
+const C2EC_TERMINAL_SETUP_WITHDRAWAL_API = C2EC_BASE_URL + "/withdrawals"
+const C2EC_TERMINAL_STATUS_WITHDRAWAL_API = C2EC_BASE_URL +
"/withdrawals/:wopid"
+const C2EC_TERMINAL_CHECK_WITHDRAWAL_API = C2EC_BASE_URL +
"/withdrawals/:wopid/check"
+
const TERMINAL_PROVIDER = "Simulation"
-const TERMINAL_ID = "1"
+// this must be the id retrieved by the cli
+const TERMINAL_ID = "2"
// retrieved from the cli tool when added the terminal
-const TERMINAL_USER_ID = TERMINAL_PROVIDER + "-" + TERMINAL_ID
+const TERMINAL_USER_ID = "Simulation-" + TERMINAL_ID
// retrieved from the cli tool when added the terminal
-const TERMINAL_ACCESS_TOKEN = "secret"
+const TERMINAL_ACCESS_TOKEN = "oVclsDlWVl0LaQg83e05M7/vCk2PfdJ785GaI0MQ0wc="
const SIM_TERMINAL_LONG_POLL_MS_STR = "20000" // 20 seconds
-const QR_CODE_CONTENT_BASE = "taler://withdraw/localhost:8082/c2ec/"
+const QR_CODE_CONTENT_BASE =
"taler://withdraw/localhost:8082/taler-integration/"
func Terminal(in chan *SimulatedPhysicalInteraction, out chan
*SimulatedPhysicalInteraction, kill chan error) {
fmt.Println("TERMINAL: Terminal idle... awaiting readiness message of
sim-wallet")
<-in
- fmt.Println("TERMINAL: Sim-Wallet ready, generating WOPID... ")
- wopidBytes := make([]byte, 32)
- _, err := rand.Read(wopidBytes)
+ fmt.Println("TERMINAL: basic auth header:", TerminalAuth())
+ fmt.Println("TERMINAL: loading terminal api config")
+ terminalApiCfg, status, err := HttpGet(
+ C2EC_TERMINAL_CONFIG_API,
+ map[string]string{"Authorization": TerminalAuth()},
+ NewJsonCodec[TerminalConfig](),
+ )
+ if err != nil {
+ kill <- err
+ return
+ }
+ if status != 200 {
+ kill <- errors.New("terminal api configuration failed with
status " + strconv.Itoa(status))
+ return
+ }
+ fmt.Println("TERMINAL: API config loaded.", terminalApiCfg.Name,
terminalApiCfg.Version, terminalApiCfg.ProviderName, terminalApiCfg.WireType)
+
+ fmt.Println("TERMINAL: Sim-Wallet ready, intiating withdrawal...")
+
+ uuid, err := uuid.NewGen().NewV7()
+ if err != nil {
+ kill <- err
+ return
+ }
+
+ setupReq := &TerminalWithdrawalSetup{
+ Amount: &Amount{"CHF", 10, 50},
+ SuggestedAmount: &Amount{"CHF", 10, 50},
+ ProviderTransactionId: "",
+ TerminalFees: &Amount{},
+ RequestUid: uuid.String(),
+ UserUuid: "",
+ Lock: "",
+ }
+
+ url := FormatUrl(
+ C2EC_TERMINAL_SETUP_WITHDRAWAL_API,
+ map[string]string{},
+ map[string]string{},
+ )
+ fmt.Println("TERMINAL: requesting url:", url)
+ response, status, err := HttpPost(
+ url,
+ map[string]string{"Authorization": TerminalAuth()},
+ setupReq,
+ NewJsonCodec[TerminalWithdrawalSetup](),
+ NewJsonCodec[TerminalWithdrawalSetupResponse](),
+ )
+ if err != nil {
+ kill <- err
+ return
+ }
+ if status != 200 {
+ kill <- errors.New("status of withdrawal setup response was " +
strconv.Itoa(status))
+ return
+ }
+
+ wopidEncoded := response.Wopid
+ fmt.Println("TERMINAL: received wopid:", wopidEncoded)
+
+ // this decoding encoding cycle is useless but tests
+ // decoding and encoding of the wopid. That's why it is
+ // done here.
+ wopidDecoded, err := ParseWopid(wopidEncoded)
if err != nil {
- fmt.Println("TERMINAL: failed creating the wopid:",
err.Error(), "(ends simulation)")
kill <- err
+ return
}
+ wopidEncoded = FormatWopid(wopidDecoded)
- wopid := FormatWopid(wopidBytes)
- fmt.Println("TERMINAL: Generated Nonce (base64 url encoded):", wopid)
- uri := QR_CODE_CONTENT_BASE + wopid
+ uri := QR_CODE_CONTENT_BASE + wopidEncoded
fmt.Println("TERMINAL: Taler Withdrawal URI:", uri)
// note for realworld implementation
// -> start long polling always before showing the QR code
- awaitSelection := make(chan *C2ECWithdrawalStatus)
+ awaitSelection := make(chan *BankWithdrawalOperationStatus)
longPollFailed := make(chan error)
fmt.Println("TERMINAL: now sending long poll request to c2ec from
terminal and await parameter selection")
go func() {
url := FormatUrl(
- C2EC_BANK_WITHDRAWAL_STATUS_URL,
- map[string]string{"wopid": wopid},
+ C2EC_TERMINAL_STATUS_WITHDRAWAL_API,
+ map[string]string{"wopid": wopidEncoded},
map[string]string{"long_poll_ms":
SIM_TERMINAL_LONG_POLL_MS_STR},
)
+ fmt.Println("TERMINAL: requesting status update for
withdrawal", url)
response, status, err := HttpGet(
url,
map[string]string{"Authorization": TerminalAuth()},
- NewJsonCodec[C2ECWithdrawalStatus](),
+ NewJsonCodec[BankWithdrawalOperationStatus](),
)
if err != nil {
kill <- err
@@ -73,7 +139,7 @@ func Terminal(in chan *SimulatedPhysicalInteraction, out
chan *SimulatedPhysical
awaitSelection <- response
}()
- fmt.Println("Go is too fast :) ... need to sleep a bit that long
polling request is guaranteed to be executed before the POST of the
registration. This won't be a problem in real world appliance.")
+ fmt.Println("need to sleep a bit that long polling request is
guaranteed to be executed before the POST of the registration. This won't be a
problem in real world appliance.")
time.Sleep(time.Duration(10) * time.Millisecond)
if !DISABLE_DELAYS {
@@ -100,50 +166,39 @@ func Terminal(in chan *SimulatedPhysicalInteraction, out
chan *SimulatedPhysical
fmt.Println("TERMINAL: card accepted. terminal
waits for response of provider backend.")
}
- terminalId, err := strconv.Atoi(TERMINAL_ID)
- if err != nil {
- fmt.Println("failed parsing the terminal id.")
- kill <- err
- }
-
- fmt.Println("TERMINAL: payment was processed at the
provider backend. sending payment notification.")
- paymentNotification := &C2ECPaymentNotification{
+ fmt.Println("TERMINAL: payment was processed at the
provider backend. sending check notification.")
+ checkNotification :=
&TerminalWithdrawalConfirmationRequest{
ProviderTransactionId:
"simulation-transaction-id-0",
- TerminalId: terminalId,
- Amount: Amount{
- Currency: "CHF",
- Fraction: 10,
- Value: 10,
- },
- Fees: Amount{
+ TerminalFees: &Amount{
Currency: "CHF",
Fraction: 10,
Value: 0,
},
}
- cdc := NewJsonCodec[C2ECPaymentNotification]()
- pnbytes, err := cdc.EncodeToBytes(paymentNotification)
- if err != nil {
- fmt.Println("TERMINAL: failed serializing
payment notification")
- kill <- err
- }
- paymentUrl := FormatUrl(
- C2EC_BANK_WITHDRAWAL_PAYMENT_URL,
- map[string]string{"wopid": wopid},
+ checkurl := FormatUrl(
+ C2EC_TERMINAL_CHECK_WITHDRAWAL_API,
+ map[string]string{"wopid": wopidEncoded},
map[string]string{},
)
- _, err = http.Post(
- paymentUrl,
- cdc.HttpApplicationContentHeader(),
- bytes.NewReader(pnbytes),
+ fmt.Println("TERMINAL: check url", checkurl)
+ _, status, err =
HttpPost[TerminalWithdrawalConfirmationRequest, any](
+ checkurl,
+ map[string]string{"Authorization":
TerminalAuth()},
+ checkNotification,
+
NewJsonCodec[TerminalWithdrawalConfirmationRequest](),
+ nil,
)
if err != nil {
fmt.Println("TERMINAL: error on POST request:",
err.Error())
kill <- err
}
+ if status != 204 {
+ fmt.Println("TERMINAL: error while check
payment POST: " + strconv.Itoa(status))
+ kill <- errors.New("payment check request by
terminal failed")
+ }
fmt.Println("TERMINAL: Terminal flow ended")
case f := <-longPollFailed:
- fmt.Println("TERMINAL: long-polling for selection
failed... error:", err.Error())
+ fmt.Println("TERMINAL: long-polling for selection
failed... error:", err)
kill <- f
}
}
@@ -152,5 +207,34 @@ func Terminal(in chan *SimulatedPhysicalInteraction, out
chan *SimulatedPhysical
func TerminalAuth() string {
userAndPw := fmt.Sprintf("%s:%s", TERMINAL_USER_ID,
TERMINAL_ACCESS_TOKEN)
- return base64.StdEncoding.EncodeToString([]byte(userAndPw))
+ return "Basic " + base64.StdEncoding.EncodeToString([]byte(userAndPw))
+}
+
+// Structs copied from c2ec
+type TerminalConfig struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ ProviderName string `json:"provider_name"`
+ WireType string `json:"wire_type"`
+}
+
+type TerminalWithdrawalSetup struct {
+ Amount *Amount `json:"amount"`
+ SuggestedAmount *Amount `json:"suggested_amount"`
+ ProviderTransactionId string `json:"provider_transaction_id"`
+ TerminalFees *Amount `json:"terminal_fees"`
+ RequestUid string `json:"request_uid"`
+ UserUuid string `json:"user_uuid"`
+ Lock string `json:"lock"`
+}
+
+type TerminalWithdrawalSetupResponse struct {
+ Wopid string `json:"withdrawal_id"`
+}
+
+type TerminalWithdrawalConfirmationRequest struct {
+ ProviderTransactionId string `json:"provider_transaction_id"`
+ TerminalFees *Amount `json:"terminal_fees"`
+ UserUuid string `json:"user_uuid"`
+ Lock string `json:"lock"`
}
diff --git a/simulation/sim-wallet.go b/simulation/sim-wallet.go
index 46dd5f7..0a6f17e 100644
--- a/simulation/sim-wallet.go
+++ b/simulation/sim-wallet.go
@@ -12,6 +12,11 @@ import (
"time"
)
+const C2EC_BANK_BASE_URL = C2EC_BASE_URL + "/taler-integration"
+const C2EC_BANK_CONFIG_URL = C2EC_BANK_BASE_URL + "/config"
+const C2EC_BANK_WITHDRAWAL_STATUS_URL = C2EC_BANK_BASE_URL +
"/withdrawal-operation/:wopid"
+const C2EC_BANK_WITHDRAWAL_REGISTRATION_URL = C2EC_BANK_BASE_URL +
"/withdrawal-operation/:wopid"
+
const SIM_WALLET_LONG_POLL_MS_STR = "10000" // 10 seconds
func Wallet(in chan *SimulatedPhysicalInteraction, out chan
*SimulatedPhysicalInteraction, kill chan error) {
@@ -37,9 +42,11 @@ func Wallet(in chan *SimulatedPhysicalInteraction, out chan
*SimulatedPhysicalIn
map[string]string{},
)
- cdc := NewJsonCodec[C2ECWithdrawRegistration]()
- reg := new(C2ECWithdrawRegistration)
+ cdc := NewJsonCodec[BankWithdrawalOperationPostRequest]()
+ reg := new(BankWithdrawalOperationPostRequest)
reg.ReservePubKey = EddsaPublicKey(simulateReservePublicKey())
+ reg.Amount = nil
+ reg.SelectedExchange = C2EC_BANK_BASE_URL
body, err := cdc.EncodeToBytes(reg)
regByte := bytes.NewBuffer(body)
// fmt.Println("WALLET : body (bytes):", regByte.Bytes())
@@ -128,5 +135,44 @@ func simulateReservePublicKey() string {
if err != nil {
return ""
}
- return talerBase32Encode(mockedPubKey)
+ return talerBinaryEncode(mockedPubKey)
+}
+
+type CurrencySpecification struct {
+ Name string `json:"name"`
+ Currency string `json:"currency"`
+ NumFractionalInputDigits int
`json:"num_fractional_input_digits"`
+ NumFractionalNormalDigits int
`json:"num_fractional_normal_digits"`
+ NumFractionalTrailingZeroDigits int
`json:"num_fractional_trailing_zero_digits"`
+ AltUnitNames string `json:"alt_unit_names"`
+}
+
+//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankIntegrationConfig
+type BankIntegrationConfig struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Implementation string `json:"implementation"`
+ Currency string `json:"currency"`
+ CurrencySpecification CurrencySpecification
`json:"currency_specification"`
+ // TODO: maybe add exchanges payto uri for transfers etc.?
+}
+
+type BankWithdrawalOperationPostRequest struct {
+ ReservePubKey EddsaPublicKey `json:"reserve_pub"`
+ SelectedExchange string `json:"selected_exchange"`
+ Amount *Amount `json:"amount"`
+}
+
+type BankWithdrawalOperationPostResponse struct {
+ Status WithdrawalOperationStatus `json:"status"`
+ ConfirmTransferUrl string
`json:"confirm_transfer_url"`
+ TransferDone bool `json:"transfer_done"`
+}
+
+type BankWithdrawalOperationStatus struct {
+ Status WithdrawalOperationStatus `json:"status"`
+ Amount Amount `json:"amount"`
+ SenderWire string `json:"sender_wire"`
+ WireTypes []string `json:"wire_types"`
+ ReservePubKey EddsaPublicKey `json:"selected_reserve_pub"`
}
diff --git a/wallee-c2ec/app/src/main/AndroidManifest.xml
b/wallee-c2ec/app/src/main/AndroidManifest.xml
index f276936..5d5d90f 100644
--- a/wallee-c2ec/app/src/main/AndroidManifest.xml
+++ b/wallee-c2ec/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
+ <uses-permission android:name="android.permission.INTERNET" />
+
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CryptoUtils.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CryptoUtils.kt
new file mode 100644
index 0000000..ab1880a
--- /dev/null
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CryptoUtils.kt
@@ -0,0 +1,105 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/*
+ * The code in this file was copied from the Taler Wallet App
+ * source:
https://git.taler.net/taler-android.git/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt
+ */
+
+package ch.bfh.habej2.wallee_c2ec.client.taler.encoding
+
+import kotlin.math.floor
+
+object CyptoUtils {
+ internal fun getValue(c: Char): Int {
+ val a = when (c) {
+ 'o','O' -> '0'
+ 'i','I','l','L' -> '1'
+ 'u','U' -> 'V'
+ else -> c
+ }
+ if (a in '0'..'9') {
+ return a - '0'
+ }
+ val A = if (a in 'a'..'z') a.uppercaseChar() else a
+ var dec = 0
+ if (A in 'A'..'Z') {
+ if ('I' < A) dec++
+ if ('L' < A) dec++
+ if ('O' < A) dec++
+ if ('U' < A) dec++
+ return A - 'A' + 10 - dec
+ }
+ throw Error("encoding error")
+ }
+
+ fun decodeCrock(e: String): ByteArray {
+ val size = e.length
+ var bitpos = 0
+ var bitbuf = 0
+ var readPosition = 0
+ val outLen = floor((size * 5f) / 8).toInt()
+ val out = ByteArray(outLen)
+ var outPos = 0
+ while (readPosition < size || bitpos > 0) {
+ if (readPosition < size) {
+ val v = getValue(e[readPosition++])
+ bitbuf = bitbuf.shl(5).or(v)
+ bitpos += 5
+ }
+ while (bitpos >= 8) {
+ val d = bitbuf.shr(bitpos -8).and(0xff).toByte()
+ out[outPos++] = d
+ bitpos -= 8
+ }
+ if (readPosition == size && bitpos > 0) {
+ bitbuf = bitbuf.shl( 8 - bitpos).and(0xff)
+ bitpos = if (bitbuf == 0) 0 else 8
+ }
+ }
+ return out
+ }
+
+ fun encodeCrock(data: ByteArray): String {
+ val out = StringBuilder()
+ var bitbuf = 0
+ var bitpos = 0
+ for (byte in data) {
+ bitbuf = bitbuf.shl(8).or(byte.toInt() and 0xff)
+ bitpos += 8
+ while (bitpos >= 5) {
+ val value = bitbuf.shr(bitpos - 5).and(0x1f)
+ out.append(encodeValue(value))
+ bitpos -= 5
+ }
+ }
+ if (bitpos > 0) {
+ bitbuf = bitbuf.shl(5 - bitpos)
+ val value = bitbuf.and(0x1f)
+ out.append(encodeValue(value))
+ }
+ return out.toString()
+ }
+
+ private fun encodeValue(value: Int): Char {
+ val alphabet = "ABCDEFGHJKMNPQRSTVWXYZ"
+ return when (value) {
+ in 0..9 -> ('0'.code + value).toChar() // '0' to '9'
+ in 10..31 -> alphabet[value - 10] // 'A' to 'Z' (without 'i' 'l'
'o' 'u')
+ else -> throw IllegalArgumentException("Invalid value for
encoding: $value")
+ }
+ }
+}
\ No newline at end of file
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt
index 5464858..d5ebea8 100644
---
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt
@@ -1,8 +1,16 @@
package ch.bfh.habej2.wallee_c2ec.client.taler.encoding
-import android.util.Base64
-import org.apache.commons.codec.binary.Base32
+import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.decodeCrock
+import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.encodeCrock
+
+fun Base32Encode(byts: ByteArray): String = encodeCrock(byts)
+
+fun Base32Decode(enc: String) = decodeCrock(enc)
+
+
+
+
+
+
-fun Base32Encode(byts: ByteArray): String = Base64.encodeToString(byts, 0) //
Base32().encodeAsString(byts)
-fun Base32Decode(enc: String) = Base32().decode(enc)
\ No newline at end of file
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt
index 5419ce9..be0f601 100644
---
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt
@@ -1,9 +1,10 @@
package ch.bfh.habej2.wallee_c2ec.client.wallee
+import ch.bfh.habej2.wallee_c2ec.withdrawal.WithdrawalActivity
import com.wallee.android.till.sdk.ResponseHandler
import com.wallee.android.till.sdk.data.TransactionResponse
-class WalleeResponseHandler : ResponseHandler() {
+class WalleeResponseHandler(private val activity: WithdrawalActivity) :
ResponseHandler() {
override fun authorizeTransactionReply(response: TransactionResponse?) {
@@ -16,4 +17,15 @@ class WalleeResponseHandler : ResponseHandler() {
response.transaction.metaData.get("id")
}
+
+ override fun checkApiServiceCompatibilityReply(
+ isCompatible: Boolean?,
+ apiServiceVersion: String?
+ ) {
+
+ if (isCompatible == null || !isCompatible) {
+ // just dont start withdrawals when api is not compatible
+ activity.finish()
+ }
+ }
}
\ No newline at end of file
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt
new file mode 100644
index 0000000..e22ea0a
--- /dev/null
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt
@@ -0,0 +1,51 @@
+package ch.bfh.habej2.wallee_c2ec.withdrawal
+
+import android.app.Activity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.KeyboardType
+
+@Composable
+fun AmountScreen(model: WithdrawalViewModel, navigateToWhenAmountEntered: ()
-> Unit) {
+
+ val activity = LocalContext.current as Activity
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Text(text = "present card, trigger payment")
+
+ TextField(
+ value = "",
+ onValueChange = {
+ model.updateAmount(it)
+ },
+ label = { Text(text = "Enter amount") },
+ placeholder = { Text(text = "amount") },
+ keyboardOptions = KeyboardOptions(
+ autoCorrect = false,
+ keyboardType = KeyboardType.Number
+ )
+ )
+
+ Button(onClick = {
+ navigateToWhenAmountEntered()
+ }) {
+ Text(text = "pay")
+ }
+
+ Button(onClick = {
+ model.withdrawalOperationFailed()
+ activity.finish()
+ }) {
+ Text(text = "abort")
+ }
+ }
+}
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt
new file mode 100644
index 0000000..a82213a
--- /dev/null
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt
@@ -0,0 +1,57 @@
+package ch.bfh.habej2.wallee_c2ec.withdrawal
+
+import android.app.Activity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.platform.LocalContext
+import com.wallee.android.till.sdk.ApiClient
+import com.wallee.android.till.sdk.data.LineItem
+import com.wallee.android.till.sdk.data.Transaction
+import com.wallee.android.till.sdk.data.TransactionProcessingBehavior
+import java.math.BigDecimal
+import java.util.Currency
+
+@Composable
+fun AuthorizePaymentScreen(model: WithdrawalViewModel, client: ApiClient) {
+
+ val uiState by model.uiState.collectAsState()
+ val activity = LocalContext.current as Activity
+
+ val withdrawalAmount = LineItem
+ .ListBuilder(
+ uiState.encodedWopid,
+ BigDecimal("${uiState.amount.value}.${uiState.amount.frac}")
+ )
+ .build()
+
+ val transaction = Transaction.Builder(withdrawalAmount)
+ .setCurrency(Currency.getInstance(uiState.currency))
+ .setInvoiceReference(uiState.encodedWopid)
+ .setMerchantReference(uiState.encodedWopid)
+
.setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY)
+ .build()
+
+ try {
+ client.authorizeTransaction(transaction)
+ } catch (e: Exception) {
+ model.withdrawalOperationFailed()
+ activity.finish()
+ e.printStackTrace()
+ }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Text(text = "Transaction Executed")
+
+ Button(onClick = { activity.finish() }) {
+ Text(text = "finish")
+ }
+ }
+}
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt
new file mode 100644
index 0000000..458b158
--- /dev/null
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt
@@ -0,0 +1,42 @@
+package ch.bfh.habej2.wallee_c2ec.withdrawal
+
+import android.app.Activity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.platform.LocalContext
+import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig
+
+@Composable
+fun ExchangeSelectionScreen(
+ model: WithdrawalViewModel,
+ onNavigateToWithdrawal: () -> Unit
+) {
+
+ val activity = LocalContext.current as Activity
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Text(text = "Choose the exchange to withdraw from")
+
+ // TODO let user select exchanges from config here
+ // config must contain display name, credentials (generated by cli)
+ // and the base url of the c2ec bank-integration api
+
+ val ctx = LocalContext.current
+ Button(onClick = {
+ model.exchangeUpdated(TalerBankIntegrationConfig("","","",""))
+ onNavigateToWithdrawal()
+ }) {
+ Text(text = "withdraw")
+ }
+
+ Button(onClick = { activity.finish() }) {
+ Text(text = "abort")
+ }
+ }
+}
\ No newline at end of file
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt
new file mode 100644
index 0000000..59a43fd
--- /dev/null
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt
@@ -0,0 +1,44 @@
+package ch.bfh.habej2.wallee_c2ec.withdrawal
+
+import android.app.Activity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+fun RegisterWithdrawalScreen(
+ model: WithdrawalViewModel,
+ navigateToWhenRegistered: () -> Unit
+) {
+
+ val uiState by model.uiState.collectAsState()
+ val activity = (LocalContext.current as Activity)
+
+ model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) {
+ activity.finish()
+ }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Text(text = "QR-Code content:
${formatTalerUri(uiState.exchangeBankIntegrationApiUrl, uiState.encodedWopid)}")
+
+ QRCode(formatTalerUri(uiState.exchangeBankIntegrationApiUrl,
uiState.encodedWopid))
+
+ Button(onClick = {
+ model.withdrawalOperationFailed()
+ activity.finish()
+ }) {
+ Text(text = "abort")
+ }
+ }
+}
+
+private fun formatTalerUri(exchangeBankIntegrationApiPath: String,
encodedWopid: String) =
+ "taler://withdraw/$exchangeBankIntegrationApiPath/$encodedWopid"
\ No newline at end of file
diff --git
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt
index 526924c..c4c4d2c 100644
---
a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt
+++
b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt
@@ -29,9 +29,15 @@ import java.util.Currency
class WithdrawalActivity : ComponentActivity() {
+ private lateinit var walleeClient: ApiClient
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ walleeClient = ApiClient(WalleeResponseHandler(this))
+ walleeClient.bind(this)
+ walleeClient.checkApiServiceCompatibility()
+
setContent {
val model = WithdrawalViewModel()
@@ -54,155 +60,34 @@ class WithdrawalActivity : ComponentActivity() {
}
}
composable("authorizePaymentScreen") {
- AuthorizePaymentScreen(model)
+ AuthorizePaymentScreen(model, walleeClient)
}
}
}
}
-}
-
-@Composable
-fun RegisterWithdrawalScreen(
- model: WithdrawalViewModel,
- navigateToWhenRegistered: () -> Unit
-) {
-
- val uiState by model.uiState.collectAsState()
- val activity = (LocalContext.current as Activity)
-
- model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) {
- activity.finish()
- }
-
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
-
- Text(text = "QR-Code content: ${formatTalerUri(uiState.encodedWopid)}")
-
- QRCode(formatTalerUri(uiState.encodedWopid))
- Button(onClick = {
- model.withdrawalOperationFailed()
- activity.finish()
- }) {
- Text(text = "abort")
- }
+ override fun onStart() {
+ super.onStart()
+ walleeClient.bind(this)
}
-}
-
-@Composable
-fun AmountScreen(model: WithdrawalViewModel, navigateToWhenAmountEntered: ()
-> Unit) {
-
- val activity = LocalContext.current as Activity
-
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(text = "present card, trigger payment")
-
- TextField(
- value = "",
- onValueChange = {
- model.updateAmount(it)
- },
- label = { Text(text = "Enter amount") },
- placeholder = { Text(text = "amount") },
- keyboardOptions = KeyboardOptions(
- autoCorrect = false,
- keyboardType = KeyboardType.Number
- )
- )
-
- Button(onClick = {
- navigateToWhenAmountEntered()
- }) {
- Text(text = "pay")
- }
-
- Button(onClick = {
- model.withdrawalOperationFailed()
- activity.finish()
- }) {
- Text(text = "abort")
- }
+ override fun onResume() {
+ super.onResume()
+ walleeClient.bind(this)
}
-}
-
-@Composable
-fun ExchangeSelectionScreen(
- model: WithdrawalViewModel,
- onNavigateToWithdrawal: () -> Unit
-) {
-
- val activity = LocalContext.current as Activity
-
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
-
- Text(text = "Choose the exchange to withdraw from")
- // TODO let user select exchanges from config here
- // config must contain display name, credentials (generated by cli)
- // and the base url of the c2ec bank-integration api
-
- val ctx = LocalContext.current
- Button(onClick = {
- model.exchangeUpdated(TalerBankIntegrationConfig("","","",""))
- onNavigateToWithdrawal()
- }) {
- Text(text = "withdraw")
- }
-
- Button(onClick = { activity.finish() }) {
- Text(text = "abort")
- }
+ override fun onStop() {
+ super.onStop()
+ walleeClient.unbind(this)
}
-}
-@Composable
-fun AuthorizePaymentScreen(model: WithdrawalViewModel) {
-
- val uiState by model.uiState.collectAsState()
- val activity = LocalContext.current as Activity
- val client = ApiClient(WalleeResponseHandler())
-
- client.bind(activity)
-
- val withdrawalAmount = LineItem
- .ListBuilder(
- uiState.encodedWopid,
- BigDecimal("${uiState.amount.value}.${uiState.amount.frac}")
- )
- .build()
-
- val transaction = Transaction.Builder(withdrawalAmount)
- .setCurrency(Currency.getInstance(uiState.currency))
- .setInvoiceReference(uiState.encodedWopid)
- .setMerchantReference(uiState.encodedWopid)
-
.setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY)
- .build()
-
- try {
- client.authorizeTransaction(transaction)
- } catch (e: Exception) {
- e.printStackTrace()
+ override fun onDestroy() {
+ super.onDestroy()
+ walleeClient.unbind(this)
}
- client.unbind(activity)
-
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
-
- Text(text = "Transaction Executed")
-
- Button(onClick = { activity.finish() }) {
- Text(text = "finish")
- }
+ override fun onPause() {
+ super.onPause()
+ walleeClient.unbind(this)
}
}
-
-private fun formatTalerUri(encodedWopid: String) =
"taler://withdraw/$encodedWopid"
diff --git
a/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CyptoUtilsTest.kt
b/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CyptoUtilsTest.kt
new file mode 100644
index 0000000..e207843
--- /dev/null
+++
b/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CyptoUtilsTest.kt
@@ -0,0 +1,54 @@
+package ch.bfh.habej2.wallee_c2ec.client.taler.encoding
+
+import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.decodeCrock
+import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.encodeCrock
+import org.junit.Test
+import java.security.SecureRandom
+
+class CyptoUtilsTest {
+
+ @Test
+ fun crockford() {
+
+ val origin = rand32Bytes()
+ println("origin: $origin")
+ val encoded = encodeCrock(origin)
+ println("encoded: $encoded")
+ val decoded = decodeCrock(encoded)
+ println("decoded: $decoded")
+
+ assert(origin.contentEquals(decoded))
+ }
+
+ @Test
+ fun crockford_taler_special() {
+ // see https://docs.taler.net/core/api-common.html#binary-data
+
+ val origin = "BNR1DRKYZ676T5KMHJMNPV68V32W95S9Q35P081TJ5ZZTJ8M5WJG"
+ println("origin: $origin")
+ val decodedNormal = decodeCrock(origin)
+ val originWithReplacedOcrProblems1 = origin
+ .replace('1', 'L')
+ .replace('V', 'U')
+ .replace('0', 'O')
+ val originWithReplacedOcrProblems2 = origin
+ .replace('1', 'I')
+ .replace('V', 'U')
+ .replace('0', 'O')
+ println("encoded 1: $originWithReplacedOcrProblems1")
+ println("encoded 2: $originWithReplacedOcrProblems2")
+ val decoded1 = decodeCrock(originWithReplacedOcrProblems1)
+ val decoded2 = decodeCrock(originWithReplacedOcrProblems2)
+
+ assert(decodedNormal.contentEquals(decoded1))
+ assert(decodedNormal.contentEquals(decoded2))
+ assert(decoded1.contentEquals(decoded2))
+ }
+
+ private fun rand32Bytes(): ByteArray {
+ val wopid = ByteArray(32)
+ val rand = SecureRandom()
+ rand.nextBytes(wopid) // will seed automatically
+ return wopid
+ }
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-cashless2ecash] branch master updated (fd19956 -> ad94bc5),
gnunet <=