summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel-Haeberli <haebu@rubigen.ch>2024-04-19 23:19:28 +0200
committerJoel-Haeberli <haebu@rubigen.ch>2024-04-19 23:19:28 +0200
commit76bb4ceeb9799fba19a10b267233571b0de07cab (patch)
tree2125b03caa6f2e95c6754998b1ccd01b30774e3f
parentdc1ef32a87c72a0311934bbcf07ee55de84d7a1c (diff)
downloadcashless2ecash-76bb4ceeb9799fba19a10b267233571b0de07cab.tar.gz
cashless2ecash-76bb4ceeb9799fba19a10b267233571b0de07cab.tar.bz2
cashless2ecash-76bb4ceeb9799fba19a10b267233571b0de07cab.zip
feat: android basic structure
-rw-r--r--c2ec/attestor.go4
-rw-r--r--c2ec/bank-integration.go76
-rw-r--r--c2ec/db.go42
-rw-r--r--c2ec/db/0001-c2ec_schema.sql29
-rw-r--r--c2ec/db/proc-c2ec_payment_notification_listener.sql2
-rw-r--r--c2ec/db/proc-c2ec_retry_listener.sql2
-rw-r--r--c2ec/db/procedures.sql122
-rw-r--r--c2ec/encoding.go62
-rw-r--r--c2ec/encoding_test.go134
-rw-r--r--c2ec/main.go3
-rw-r--r--c2ec/model.go22
-rw-r--r--c2ec/postgres.go83
-rw-r--r--c2ec/retrier.go2
-rw-r--r--c2ec/simulation-client.go5
-rw-r--r--c2ec/wire-gateway.go191
-rw-r--r--docs/content/appendix/meeting_notes.tex24
-rw-r--r--docs/content/implementation/c2ec.tex56
-rw-r--r--docs/content/implementation/concepts.tex6
-rw-r--r--docs/thesis.pdfbin1668345 -> 1671172 bytes
-rwxr-xr-xsimulation/c2ec-simulationbin7571703 -> 7570759 bytes
-rw-r--r--simulation/encoding.go63
-rw-r--r--simulation/main.go4
-rw-r--r--simulation/model.go4
-rw-r--r--simulation/sim-terminal.go19
-rw-r--r--simulation/sim-wallet.go14
-rw-r--r--wallee-c2ec/.idea/deploymentTargetDropDown.xml15
-rw-r--r--wallee-c2ec/app/build.gradle.kts6
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt3
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt10
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt99
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt56
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt65
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt102
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECClient.kt48
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECPaymentNotification.kt4
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECWithdrawalOperationStatus.kt4
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt126
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/BankIntegrationConfig.kt20
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/PaymentNotification.kt4
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperation.kt3
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperationStatus.kt9
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt19
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/ExchangeConfig.kt21
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt10
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt7
-rw-r--r--wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt7
-rw-r--r--wallee-c2ec/gradle/libs.versions.toml8
47 files changed, 1251 insertions, 364 deletions
diff --git a/c2ec/attestor.go b/c2ec/attestor.go
index ea777b8..f268a0a 100644
--- a/c2ec/attestor.go
+++ b/c2ec/attestor.go
@@ -71,6 +71,8 @@ func listenCallback(
func dispatch(notification *Notification, errs chan error) {
+ LogInfo("attestor", fmt.Sprintf("retrieved information on channel=%s with payload=%s", notification.Channel, notification.Payload))
+
// The payload is formatted like: "{PROVIDER_NAME}|{WITHDRAWAL_ID}|{PROVIDER_TRANSACTION_ID}"
// the validation is strict. This means, that the dispatcher emits an error
// and returns, if a property is malformed.
@@ -174,7 +176,7 @@ func prepareRetryOrAbort(
return
}
- if withdrawal.RetryCounter > CONFIG.Server.MaxRetries {
+ if withdrawal.RetryCounter >= CONFIG.Server.MaxRetries {
LogInfo("attestor", fmt.Sprintf("max retries for withdrawal with id=%d was reached. withdrawal is aborted.", withdrawal.WithdrawalId))
err := DB.FinaliseWithdrawal(withdrawalRowId, ABORTED, make([]byte, 0))
diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go
index 89f9c26..d4d72f0 100644
--- a/c2ec/bank-integration.go
+++ b/c2ec/bank-integration.go
@@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
+ "encoding/base64"
"fmt"
http "net/http"
"strconv"
@@ -16,7 +17,7 @@ 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 + "/payment"
+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
@@ -39,11 +40,11 @@ type BankIntegrationConfig struct {
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"`
- TerminalId uint64 `json:"terminal_id"`
}
type C2ECWithdrawalStatus struct {
@@ -56,8 +57,9 @@ type C2ECWithdrawalStatus struct {
type C2ECPaymentNotification struct {
ProviderTransactionId string `json:"provider_transaction_id"`
+ TerminalId int `json:"terminal_id"`
Amount Amount `json:"amount"`
- Fees Amount `json:"fees"`
+ Fees Amount `json:"card_fees"`
}
func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) {
@@ -98,27 +100,24 @@ func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) {
// read and validate the wopid path parameter
wopid := req.PathValue(WOPID_PARAMETER)
- wopid, err = ParseWopid(wopid)
+ 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
+ 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(
- WithdrawalIdentifier(wopid),
+ wpd,
registration.ReservePubKey,
- registration.TerminalId,
)
if err != nil {
@@ -135,7 +134,7 @@ func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) {
return
}
- res.WriteHeader(HTTP_NO_CONTENT)
+ writeWithdrawalOrError(wpd, res, req.RequestURI)
}
// Get status of withdrawal associated with the given WOPID
@@ -167,21 +166,19 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
// read and validate the wopid path parameter
wopid := req.PathValue(WOPID_PARAMETER)
- wopid, err := ParseWopid(wopid)
+ 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
+ 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 {
@@ -195,7 +192,7 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
statusChannel := make(chan WithdrawalOperationStatus)
errChan := make(chan error)
- go DB.ListenForWithdrawalStatusChange(timeoutCtx, WithdrawalIdentifier(wopid), statusChannel, errChan)
+ go DB.ListenForWithdrawalStatusChange(timeoutCtx, WithdrawalIdentifier(base64.StdEncoding.EncodeToString(wpd)), statusChannel, errChan)
for {
select {
case <-timeoutCtx.Done():
@@ -221,19 +218,19 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) {
}
return
case <-statusChannel:
- getWithdrawalOrWriteError(wopid, res, req.RequestURI)
+ writeWithdrawalOrError(wpd, res, req.RequestURI)
return
}
}
}
- getWithdrawalOrWriteError(wopid, res, req.RequestURI)
+ writeWithdrawalOrError(wpd, res, req.RequestURI)
}
func handlePaymentNotification(res http.ResponseWriter, req *http.Request) {
wopid := req.PathValue(WOPID_PARAMETER)
- wopid, err := ParseWopid(wopid)
+ wpd, err := ParseWopid(wopid)
if err != nil {
LogWarn("bank-integration-api", "wopid "+wopid+" not valid")
if wopid == "" {
@@ -266,11 +263,14 @@ func handlePaymentNotification(res http.ResponseWriter, req *http.Request) {
return
}
+ LogInfo("bank-integration-api", "received payment notification")
+
err = DB.NotifyPayment(
- WithdrawalIdentifier(wopid),
+ wpd,
paymentNotification.ProviderTransactionId,
+ paymentNotification.TerminalId,
paymentNotification.Amount,
- paymentNotification.Amount,
+ paymentNotification.Fees,
)
if err != nil {
err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
@@ -296,7 +296,7 @@ func handleWithdrawalAbort(res http.ResponseWriter, req *http.Request) {
// Tries to load a WithdrawalOperationStatus from the database. If no
// entry could been found, it will write the correct error to the response.
-func getWithdrawalOrWriteError(wopid string, res http.ResponseWriter, reqUri string) {
+func writeWithdrawalOrError(wopid []byte, res http.ResponseWriter, reqUri string) {
// read the withdrawal from the database
withdrawal, err := DB.GetWithdrawalByWopid(wopid)
if err != nil {
@@ -318,7 +318,7 @@ func getWithdrawalOrWriteError(wopid string, res http.ResponseWriter, reqUri str
err := WriteProblem(res, HTTP_NOT_FOUND, &RFC9457Problem{
TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_NOT_FOUND",
Title: "Not Found",
- Detail: "No withdrawal with wopid=" + wopid + " could been found.",
+ Detail: "No withdrawal with wopid=" + talerBase32Encode(wopid) + " could been found.",
Instance: reqUri,
})
if err != nil {
diff --git a/c2ec/db.go b/c2ec/db.go
index fed8cdb..6607415 100644
--- a/c2ec/db.go
+++ b/c2ec/db.go
@@ -5,7 +5,7 @@ import (
)
const PROVIDER_TABLE_NAME = "c2ec.provider"
-const PROVIDER_FIELD_NAME_ID = "terminal_id"
+const PROVIDER_FIELD_NAME_ID = "provider_id"
const PROVIDER_FIELD_NAME_NAME = "name"
const PROVIDER_FIELD_NAME_PAYTO_TARGET_TYPE = "payto_target_type"
const PROVIDER_FIELD_NAME_BACKEND_URL = "backend_base_url"
@@ -34,7 +34,11 @@ const WITHDRAWAL_FIELD_NAME_COMPLETION_PROOF = "completion_proof"
const TRANSFER_TABLE_NAME = "c2ec.transfer"
const TRANSFER_FIELD_NAME_ID = "request_uid"
-const TRANSFER_FIELD_NAME_HASH = "request_hash"
+const TRANSFER_FIELD_NAME_ROW_ID = "row_id"
+const TRANSFER_FIELD_NAME_AMOUNT = "amount"
+const TRANSFER_FIELD_NAME_EXCHANGE_BASE_URL = "exchange_base_url"
+const TRANSFER_FIELD_NAME_WTID = "wtid"
+const TRANSFER_FIELD_NAME_CREDIT_ACCOUNT = "credit_account"
type Provider struct {
ProviderId int64 `db:"provider_id"`
@@ -60,7 +64,7 @@ type Withdrawal struct {
Amount *TalerAmountCurrency `db:"amount" scan:"follow"`
Fees *TalerAmountCurrency `db:"fees" scan:"follow"`
WithdrawalStatus WithdrawalOperationStatus `db:"withdrawal_status"`
- TerminalId int64 `db:"terminal_id"`
+ TerminalId *int64 `db:"terminal_id"`
ProviderTransactionId *string `db:"provider_transaction_id"`
LastRetryTs *int64 `db:"last_retry_ts"`
RetryCounter int32 `db:"retry_counter"`
@@ -74,8 +78,13 @@ type TalerAmountCurrency struct {
}
type Transfer struct {
- RequestId HashCode `db:"request_uid"`
- RequestHash string `db:"request_hash"`
+ RowId int `db:"row_id"`
+ RequestUid HashCode `db:"request_uid"`
+ Amount *TalerAmountCurrency `db:"amount"`
+ ExchangeBaseUrl string `db:"exchange_base_url"`
+ Wtid string `db:"wtid"`
+ CreditAccount string `db:"credit_account"`
+ TransactionTs int64 `db:"transaction_ts"`
}
type Notification struct {
@@ -90,16 +99,15 @@ type C2ECDatabase interface {
// Registers a wopid and reserve public key.
// This initiates the withdrawal.
RegisterWithdrawal(
- wopid WithdrawalIdentifier,
+ wopid []byte,
resPubKey EddsaPublicKey,
- terminalId uint64,
) error
// Get the withdrawal associated with the given withdrawal identifier.
GetWithdrawalById(withdrawalId int) (*Withdrawal, error)
// Get the withdrawal associated with the given wopid.
- GetWithdrawalByWopid(wopid string) (*Withdrawal, error)
+ GetWithdrawalByWopid(wopid []byte) (*Withdrawal, error)
// Get the withdrawal associated with the provider specific transaction id.
GetWithdrawalByProviderTransactionId(tid string) (*Withdrawal, error)
@@ -108,8 +116,9 @@ type C2ECDatabase interface {
// Provider, that the payment went through, this will
// save the provider specific transaction id in the database
NotifyPayment(
- wopid WithdrawalIdentifier,
+ wopid []byte,
providerTransactionId string,
+ terminalId int,
amount Amount,
fees Amount,
) error
@@ -163,7 +172,20 @@ type C2ECDatabase interface {
GetTransferById(requestUid HashCode) (*Transfer, error)
// Inserts a new transfer into the database.
- AddTransfer(requestId HashCode, requestHash string) error
+ AddTransfer(
+ requestUid HashCode,
+ amount *Amount,
+ exchangeBaseUrl string,
+ wtid string,
+ credit_account string,
+ ) error
+
+ // The wire gateway allows the exchange to retrieve transactions
+ // starting at a certain starting point up until a certain delta
+ // if the delta is negative, previous transactions relative to the
+ // starting point are considered. When start is negative, the latest
+ // id shall be used as starting point.
+ GetTransfers(start int, delta int) ([]*Transfer, error)
// This will listen for notifications on the
// channel withdrawal notifications are sent.
diff --git a/c2ec/db/0001-c2ec_schema.sql b/c2ec/db/0001-c2ec_schema.sql
index fbaeb83..80171c1 100644
--- a/c2ec/db/0001-c2ec_schema.sql
+++ b/c2ec/db/0001-c2ec_schema.sql
@@ -82,7 +82,7 @@ CREATE TABLE IF NOT EXISTS withdrawal (
amount taler_amount_currency,
fees taler_amount_currency,
withdrawal_status withdrawal_operation_status NOT NULL DEFAULT 'pending',
- terminal_id INT8 NOT NULL REFERENCES terminal(terminal_id),
+ terminal_id INT8 REFERENCES terminal(terminal_id),
provider_transaction_id TEXT,
last_retry_ts INT8,
retry_counter INT4 NOT NULL DEFAULT 0,
@@ -122,18 +122,27 @@ COMMENT ON INDEX wopid_index
Thus it makes sense to create an index on the column.';
CREATE TABLE IF NOT EXISTS transfer (
- request_uid INT8 UNIQUE PRIMARY KEY,
- request_hash TEXT NOT NULL
+ request_uid BYTEA UNIQUE PRIMARY KEY,
+ row_id INT8 GENERATED BY DEFAULT AS IDENTITY,
+ amount taler_amount_currency,
+ exchange_base_url TEXT,
+ wtid TEXT,
+ credit_account TEXT,
+ transaction_ts INT8,
);
COMMENT ON TABLE transfer
IS 'Table storing transfers which are sent by the exchange.';
+COMMENT ON COLUMN transfer.row_id
+ IS 'The row id is used to support the history outgoing';
COMMENT ON COLUMN transfer.request_uid
- IS 'A unique identifier for the transfer. In the case of this
- implementation its gonna be the wopid of the withdrawal which
- is addressed by the transfer.';
-COMMENT ON COLUMN transfer.request_hash
- IS 'Hash of the entire transfer request. Requests with the same
- request identifier must have the identical hash to be processed
- further.';
+ IS 'A unique identifier for the transfer. ';
+COMMENT ON COLUMN transfer.amount
+ IS 'The amount to be transferred';
+COMMENT ON COLUMN transfer.exchange_base_url
+ IS 'The base url of the exchange, sending the transfer request';
+COMMENT ON COLUMN transfer.wtid
+ IS 'The id of the transaction';
+COMMENT ON COLUMN transfer.credit_account
+ IS 'The payto address of the transfer target';
COMMIT;
diff --git a/c2ec/db/proc-c2ec_payment_notification_listener.sql b/c2ec/db/proc-c2ec_payment_notification_listener.sql
index 5688a69..c9168b6 100644
--- a/c2ec/db/proc-c2ec_payment_notification_listener.sql
+++ b/c2ec/db/proc-c2ec_payment_notification_listener.sql
@@ -14,7 +14,7 @@ BEGIN
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
+ ON t.terminal_id = NEW.terminal_id
WHERE w.withdrawal_id = NEW.withdrawal_id;
PERFORM pg_notify('payment_notification',
provider_name || '|' ||
diff --git a/c2ec/db/proc-c2ec_retry_listener.sql b/c2ec/db/proc-c2ec_retry_listener.sql
index e98f828..801735f 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_id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
diff --git a/c2ec/db/procedures.sql b/c2ec/db/procedures.sql
index 69a2ba6..8bac56b 100644
--- a/c2ec/db/procedures.sql
+++ b/c2ec/db/procedures.sql
@@ -1 +1,121 @@
--- generated from \ No newline at end of file
+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;
diff --git a/c2ec/encoding.go b/c2ec/encoding.go
new file mode 100644
index 0000000..970f062
--- /dev/null
+++ b/c2ec/encoding.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "encoding/base32"
+ "errors"
+ "net/url"
+ "strings"
+)
+
+// 32 characters for decoding, using RFC 3548.
+const TALER_BASE32_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+
+func talerBase32Encode(byts []byte) string {
+ return talerBase32Encoding().EncodeToString(byts)
+}
+
+func talerBase32Decode(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)
+}
+
+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)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(wopidBytes) != 32 {
+ err = errors.New("invalid wopid")
+ LogError("encoding", err)
+ return nil, err
+ }
+
+ return wopidBytes, nil
+}
+
+func FormatWopid(wopid []byte) string {
+
+ return url.PathEscape(talerBase32Encode(wopid))
+}
+
+func ParseEddsaPubKey(key EddsaPublicKey) ([]byte, error) {
+
+ return talerBase32Decode(string(key))
+}
diff --git a/c2ec/encoding_test.go b/c2ec/encoding_test.go
new file mode 100644
index 0000000..1b9dbd0
--- /dev/null
+++ b/c2ec/encoding_test.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+ "crypto/rand"
+ "testing"
+)
+
+func TestWopidEncodeDecode(t *testing.T) {
+
+ wopid := make([]byte, 32)
+ n, err := rand.Read(wopid)
+ if err != nil || n != 32 {
+ t.Log("failed because retrieving random 32 bytes failed")
+ t.FailNow()
+ }
+
+ encodedWopid := FormatWopid(wopid)
+ t.Log("encoded wopid:", encodedWopid)
+ decodedWopid, err := ParseWopid(encodedWopid)
+ if err != nil {
+ t.Error(err)
+ t.FailNow()
+ }
+
+ if len(decodedWopid) != len(wopid) {
+ t.Log("uneven length.", len(decodedWopid), "!=", len(wopid))
+ t.FailNow()
+ }
+
+ for i, b := range wopid {
+
+ if b != decodedWopid[i] {
+ t.Log("unequal at position", i)
+ t.FailNow()
+ }
+ }
+}
+
+func TestTalerBase32(t *testing.T) {
+
+ input := []byte("This is some text")
+ t.Log("in:", string(input))
+ t.Log("in:", input)
+ encoded := talerBase32Encode(input)
+ t.Log("encoded:", encoded)
+ out, err := talerBase32Decode(encoded)
+ if err != nil {
+ t.Error(err)
+ t.FailNow()
+ }
+ t.Log("decoded:", out)
+ t.Log("decoded:", string(out))
+
+ if len(out) != len(input) {
+ t.Log("uneven length.", len(out), "!=", len(input))
+ t.FailNow()
+ }
+
+ for i, b := range input {
+
+ if b != out[i] {
+ t.Log("unequal at position", i)
+ t.FailNow()
+ }
+ }
+}
+
+func TestTalerBase32Rand32(t *testing.T) {
+
+ input := make([]byte, 32)
+ n, err := rand.Read(input)
+ if err != nil || n != 32 {
+ t.Log("failed because retrieving random 32 bytes failed")
+ t.FailNow()
+ }
+
+ t.Log("in:", input)
+ encoded := talerBase32Encode(input)
+ t.Log("encoded:", encoded)
+ out, err := talerBase32Decode(encoded)
+ if err != nil {
+ t.Error(err)
+ t.FailNow()
+ }
+ t.Log("decoded:", out)
+ t.Log("decoded:", string(out))
+
+ if len(out) != len(input) {
+ t.Log("uneven length.", len(out), "!=", len(input))
+ t.FailNow()
+ }
+
+ for i, b := range input {
+
+ if b != out[i] {
+ t.Log("unequal at position", i)
+ t.FailNow()
+ }
+ }
+}
+
+func TestTalerBase32Rand64(t *testing.T) {
+
+ input := make([]byte, 64)
+ n, err := rand.Read(input)
+ if err != nil || n != 64 {
+ t.Log("failed because retrieving random 32 bytes failed")
+ t.FailNow()
+ }
+
+ t.Log("in:", input)
+ encoded := talerBase32Encode(input)
+ t.Log("encoded:", encoded)
+ out, err := talerBase32Decode(encoded)
+ if err != nil {
+ t.Error(err)
+ t.FailNow()
+ }
+ t.Log("decoded:", out)
+ t.Log("decoded:", string(out))
+
+ if len(out) != len(input) {
+ t.Log("uneven length.", len(out), "!=", len(input))
+ t.FailNow()
+ }
+
+ for i, b := range input {
+
+ if b != out[i] {
+ t.Log("unequal at position", i)
+ t.FailNow()
+ }
+ }
+}
diff --git a/c2ec/main.go b/c2ec/main.go
index bc69a96..f7be5d8 100644
--- a/c2ec/main.go
+++ b/c2ec/main.go
@@ -145,7 +145,6 @@ func main() {
case attestationError := <-attestorErrs:
LogError("main from attestor", attestationError)
case <-attestorCtx.Done():
- // The attestation process died for some reason. let's restart it.
attestorCancel() // first run old cancellation function
attestorCtx, attestorCancel = context.WithCancel(context.Background())
RunAttestor(attestorCtx, attestorErrs)
@@ -195,6 +194,7 @@ func setupProviderClients(cfg *C2ECConfig) error {
if err != nil {
return err
}
+ LogInfo("main", "setup the Simulation provider")
}
}
@@ -205,6 +205,7 @@ func setupProviderClients(cfg *C2ECConfig) error {
if err != nil {
return err
}
+ LogInfo("main", "setup the Wallee provider")
}
// For new added provider, add the respective if-clause
diff --git a/c2ec/model.go b/c2ec/model.go
index db6f30f..9057241 100644
--- a/c2ec/model.go
+++ b/c2ec/model.go
@@ -1,8 +1,6 @@
package main
import (
- "encoding/base64"
- "errors"
"fmt"
)
@@ -51,26 +49,6 @@ func ToWithdrawalOpStatus(s string) (WithdrawalOperationStatus, error) {
}
}
-func ParseWopid(wopid string) (string, error) {
-
- decoded, err := base64.URLEncoding.DecodeString(wopid)
- if err != nil {
- LogError("model", err)
- return "", errors.New("decoding failed")
- }
- return base64.StdEncoding.EncodeToString(decoded), nil
-}
-
-func WopidValid(wopid string) bool {
-
- decoded, err := base64.URLEncoding.DecodeString(wopid)
- if err != nil {
- LogError("model", err)
- return false
- }
- return len(decoded) == 32
-}
-
type ErrorDetail struct {
// Numeric error code unique to the condition.
diff --git a/c2ec/postgres.go b/c2ec/postgres.go
index a7264bf..ce9bc9a 100644
--- a/c2ec/postgres.go
+++ b/c2ec/postgres.go
@@ -2,8 +2,6 @@ package main
import (
"context"
- "encoding/base32"
- "encoding/base64"
"errors"
"fmt"
"math"
@@ -23,18 +21,18 @@ const PS_INSERT_WITHDRAWAL = "INSERT INTO " + WITHDRAWAL_TABLE_NAME + " (" +
WITHDRAWAL_FIELD_NAME_WOPID + "," +
WITHDRAWAL_FIELD_NAME_RESPUBKEY + "," +
WITHDRAWAL_FIELD_NAME_STATUS + "," +
- WITHDRAWAL_FIELD_NAME_TS + "," +
- WITHDRAWAL_FIELD_NAME_TERMINAL_ID + ")" +
- " VALUES ($1, $2, $3, $4, $5);"
+ WITHDRAWAL_FIELD_NAME_TS + ")" +
+ " VALUES ($1, $2, $3, $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 + ")" +
- " = (($1, $2, $3),($4, $5, $6),$7)" +
- " WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$8"
+ 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"
const PS_FINALISE_PAYMENT = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
WITHDRAWAL_FIELD_NAME_STATUS + "," +
@@ -42,14 +40,12 @@ const PS_FINALISE_PAYMENT = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
" = ($1, $2)" +
" WHERE " + WITHDRAWAL_FIELD_NAME_ID + "=$3"
-const PS_SET_LAST_RETRY = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
- WITHDRAWAL_FIELD_NAME_LAST_RETRY + ")" +
- " = ($1)" +
+const PS_SET_LAST_RETRY = "UPDATE " + WITHDRAWAL_TABLE_NAME +
+ " SET " + WITHDRAWAL_FIELD_NAME_LAST_RETRY + "=$1" +
" WHERE " + WITHDRAWAL_FIELD_NAME_ID + "=$2"
-const PS_SET_RETRY_COUNTER = "UPDATE " + WITHDRAWAL_TABLE_NAME + " SET (" +
- WITHDRAWAL_FIELD_NAME_RETRY_COUNTER + ")" +
- " = ($1)" +
+const PS_SET_RETRY_COUNTER = "UPDATE " + WITHDRAWAL_TABLE_NAME +
+ " SET " + WITHDRAWAL_FIELD_NAME_RETRY_COUNTER + "=($1)" +
" WHERE " + WITHDRAWAL_FIELD_NAME_ID + "=$2"
const PS_CONFIRMED_TRANSACTIONS = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
@@ -84,8 +80,9 @@ const PS_GET_TRANSFER_BY_ID = "SELECT * FROM " + TRANSFER_TABLE_NAME +
" WHERE " + TRANSFER_FIELD_NAME_ID + "=$1"
const PS_ADD_TRANSFER = "INSERT INTO " + TRANSFER_TABLE_NAME +
- " (" + TRANSFER_FIELD_NAME_ID + ", " + TRANSFER_FIELD_NAME_HASH + ")" +
- " VALUES ($1, $2)"
+ " (" + TRANSFER_FIELD_NAME_ID + ", " + TRANSFER_FIELD_NAME_AMOUNT + ", " +
+ TRANSFER_FIELD_NAME_EXCHANGE_BASE_URL + ", " + TRANSFER_FIELD_NAME_WTID + ", " +
+ TRANSFER_FIELD_NAME_CREDIT_ACCOUNT + ")" + " VALUES ($1, $2, $3, $4, $5, $6)"
// Postgres implementation of the C2ECDatabase
type C2ECPostgres struct {
@@ -140,17 +137,11 @@ func (db *C2ECPostgres) registerCustomTypesHook(ctx context.Context, conn *pgx.C
}
func (db *C2ECPostgres) RegisterWithdrawal(
- wopid WithdrawalIdentifier,
+ wopid []byte,
resPubKey EddsaPublicKey,
- terminalId uint64,
) error {
- wopidBytes, err := base64.StdEncoding.DecodeString(string(wopid))
- if err != nil {
- return err
- }
-
- resPubKeyBytes, err := base32.HexEncoding.DecodeString(string(resPubKey))
+ resPubKeyBytes, err := ParseEddsaPubKey(resPubKey)
if err != nil {
return err
}
@@ -159,11 +150,10 @@ func (db *C2ECPostgres) RegisterWithdrawal(
res, err := db.pool.Exec(
db.ctx,
PS_INSERT_WITHDRAWAL,
- wopidBytes,
+ wopid,
resPubKeyBytes,
SELECTED,
ts.Unix(),
- terminalId,
)
if err != nil {
LogError("postgres", err)
@@ -201,17 +191,12 @@ func (db *C2ECPostgres) GetWithdrawalById(withdrawalId int) (*Withdrawal, error)
}
}
-func (db *C2ECPostgres) GetWithdrawalByWopid(wopid string) (*Withdrawal, error) {
-
- wopidBytes, err := base64.StdEncoding.DecodeString(string(wopid))
- if err != nil {
- return nil, err
- }
+func (db *C2ECPostgres) GetWithdrawalByWopid(wopid []byte) (*Withdrawal, error) {
if row, err := db.pool.Query(
db.ctx,
PS_GET_WITHDRAWAL_BY_WOPID,
- wopidBytes,
+ wopid,
); err != nil {
LogError("postgres", err)
if row != nil {
@@ -263,17 +248,13 @@ func (db *C2ECPostgres) GetWithdrawalByProviderTransactionId(tid string) (*Withd
}
func (db *C2ECPostgres) NotifyPayment(
- wopid WithdrawalIdentifier,
+ wopid []byte,
providerTransactionId string,
+ terminalId int,
amount Amount,
fees Amount,
) error {
- wopidBytes, err := base64.StdEncoding.DecodeString(string(wopid))
- if err != nil {
- return err
- }
-
res, err := db.pool.Exec(
db.ctx,
PS_PAYMENT_NOTIFICATION,
@@ -284,7 +265,8 @@ func (db *C2ECPostgres) NotifyPayment(
fees.Fraction,
fees.Currency,
providerTransactionId,
- wopidBytes,
+ terminalId,
+ wopid,
)
if err != nil {
LogError("postgres", err)
@@ -575,13 +557,28 @@ func (db *C2ECPostgres) GetTransferById(requestUid HashCode) (*Transfer, error)
}
-func (db *C2ECPostgres) AddTransfer(requestId HashCode, requestHash string) error {
+func (db *C2ECPostgres) AddTransfer(
+ requestUid HashCode,
+ amount *Amount,
+ exchangeBaseUrl string,
+ wtid string,
+ credit_account string,
+) error {
+
+ dbAmount := TalerAmountCurrency{
+ Val: int64(amount.Value),
+ Frac: int32(amount.Fraction),
+ Curr: amount.Currency,
+ }
res, err := db.pool.Query(
db.ctx,
PS_ADD_TRANSFER,
- requestId,
- requestHash,
+ requestUid,
+ dbAmount,
+ exchangeBaseUrl,
+ wtid,
+ credit_account,
)
if err != nil {
LogError("postgres", err)
diff --git a/c2ec/retrier.go b/c2ec/retrier.go
index bab148f..d553708 100644
--- a/c2ec/retrier.go
+++ b/c2ec/retrier.go
@@ -75,7 +75,7 @@ func dispatchRetry(n *Notification, errs chan error) {
return
}
- provider, err := DB.GetProviderByTerminal(int(withdrawal.TerminalId))
+ provider, err := DB.GetProviderByTerminal(int(*withdrawal.TerminalId))
if err != nil {
LogError("retrier", err)
errs <- err
diff --git a/c2ec/simulation-client.go b/c2ec/simulation-client.go
index 6a5d0e7..a6708ec 100644
--- a/c2ec/simulation-client.go
+++ b/c2ec/simulation-client.go
@@ -27,6 +27,11 @@ func (st *SimulationTransaction) AllowWithdrawal() bool {
return st.allow
}
+func (st *SimulationTransaction) AbortWithdrawal() bool {
+
+ return false
+}
+
func (st *SimulationTransaction) Bytes() []byte {
return bytes.NewBufferString("this is a simulated transaction and therefore has no content.").Bytes()
diff --git a/c2ec/wire-gateway.go b/c2ec/wire-gateway.go
index 9ccf4df..424c69c 100644
--- a/c2ec/wire-gateway.go
+++ b/c2ec/wire-gateway.go
@@ -2,8 +2,6 @@ package main
import (
"context"
- "crypto"
- "encoding/base64"
"log"
http "net/http"
"strconv"
@@ -60,23 +58,54 @@ type IncomingReserveTransaction struct {
ReservePub EddsaPublicKey `json:"reserve_pub"`
}
+type OutgoingHistory struct {
+ OutgoingTransactions []*OutgoingBankTransaction `json:"outgoing_transactions"`
+ DebitAccount string `json:"debit_account"`
+}
+
+type OutgoingBankTransaction struct {
+ RowId uint64 `json:"row_id"`
+ Date Timestamp `json:"date"`
+ Amount Amount `json:"amount"`
+ CreditAccount string `json:"credit_account"`
+ Wtid ShortHashCode `json:"wtid"`
+ ExchangeBaseUrl string `json:"exchange_base_url"`
+}
+
func NewIncomingReserveTransaction(w *Withdrawal) *IncomingReserveTransaction {
t := new(IncomingReserveTransaction)
- // t.Amount = Amount{
- // Value: uint64(w.Amount.Val),
- // Fraction: uint64(w.Amount.Frac),
- // Currency: w.Amount.Curr,
- // }
+ t.Amount = Amount{
+ Value: uint64(w.Amount.Val),
+ Fraction: uint64(w.Amount.Frac),
+ Currency: w.Amount.Curr,
+ }
t.Date = Timestamp{
Ts: int(w.RegistrationTs),
}
- t.DebitAccount = ""
+ t.DebitAccount = "" // TODO provider specific payto uri -> needs new interface operation
t.ReservePub = EddsaPublicKey(w.ReservePubKey)
t.RowId = int(w.WithdrawalId)
t.Type = INCOMING_RESERVE_TRANSACTION_TYPE
return t
}
+func NewOutgoingBankTransaction(tr *Transfer) *OutgoingBankTransaction {
+ t := new(OutgoingBankTransaction)
+ t.Amount = Amount{
+ Value: uint64(tr.Amount.Val),
+ Fraction: uint64(tr.Amount.Frac),
+ Currency: tr.Amount.Curr,
+ }
+ t.Date = Timestamp{
+ Ts: int(tr.TransactionTs),
+ }
+ t.CreditAccount = tr.CreditAccount
+ t.ExchangeBaseUrl = tr.ExchangeBaseUrl
+ t.RowId = uint64(tr.RowId)
+ t.Wtid = ShortHashCode(tr.Wtid)
+ return t
+}
+
func wireGatewayConfig(res http.ResponseWriter, req *http.Request) {
cfg := WireConfig{
@@ -155,26 +184,15 @@ func transfer(res http.ResponseWriter, req *http.Request) {
return
}
- // prepare the body to add or compare.
- body := make([]byte, req.ContentLength)
- _, err = req.Body.Read(body)
- if err != nil {
- err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
- TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_READ_BODY_FAILED",
- Title: "reading body failed",
- Detail: "there was an error processing the request body (error: " + err.Error() + ")",
- Instance: req.RequestURI,
- })
- if err != nil {
- res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
- }
- return
- }
- requestHash := hashRequest(body)
-
if t == nil {
// no transfer for this request_id -> generate new
- err := DB.AddTransfer(transfer.RequestUid, requestHash)
+ err := DB.AddTransfer(
+ transfer.RequestUid,
+ &transfer.Amount,
+ transfer.ExchangeBaseUrl,
+ string(transfer.Wtid),
+ transfer.CreditAccount,
+ )
if err != nil {
err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_DATABASE_FAILURE",
@@ -189,8 +207,14 @@ func transfer(res http.ResponseWriter, req *http.Request) {
}
} else {
// the transfer is only processed if the body matches.
- if requestHash != t.RequestHash {
- err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ if transfer.Amount.Value != uint64(t.Amount.Val) ||
+ transfer.Amount.Fraction != uint64(t.Amount.Frac) ||
+ transfer.Amount.Currency != t.Amount.Curr ||
+ transfer.ExchangeBaseUrl != t.ExchangeBaseUrl ||
+ 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",
@@ -344,11 +368,103 @@ func historyIncoming(res http.ResponseWriter, req *http.Request) {
res.Write(enc)
}
-// This method is currently dead and implemented for API conformance
func historyOutgoing(res http.ResponseWriter, req *http.Request) {
- // not implemented, because not used
- res.WriteHeader(HTTP_BAD_REQUEST)
+ // read and validate request query parameters
+ shouldStartLongPoll := true
+ var longPollMilli int
+ if longPollMilliPtr, accepted := AcceptOptionalParamOrWriteResponse(
+ "long_poll_ms", strconv.Atoi, req, res,
+ ); accepted {
+ } else {
+ if longPollMilliPtr != nil {
+ longPollMilli = *longPollMilliPtr
+ } else {
+ // this means parameter was not given.
+ // no long polling (simple get)
+ shouldStartLongPoll = false
+ }
+ }
+
+ var start int
+ if startPtr, accepted := AcceptOptionalParamOrWriteResponse(
+ "start", strconv.Atoi, req, res,
+ ); accepted {
+ } else {
+ if startPtr != nil {
+ start = *startPtr
+ }
+ }
+
+ var delta int
+ if deltaPtr, accepted := AcceptOptionalParamOrWriteResponse(
+ "delta", strconv.Atoi, req, res,
+ ); accepted {
+ } else {
+ if deltaPtr != nil {
+ delta = *deltaPtr
+ } else {
+ // this means parameter was not given.
+ // no long polling (simple get)
+ shouldStartLongPoll = false
+ }
+ }
+
+ if shouldStartLongPoll {
+
+ // wait for the completion of the context
+ waitMs, cancelFunc := context.WithTimeout(req.Context(), time.Duration(longPollMilli)*time.Millisecond)
+ defer cancelFunc()
+
+ // this will just wait / block until the milliseconds are exceeded.
+ <-waitMs.Done()
+ }
+
+ 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)
+ }
+ return
+ }
+
+ if len(transfers) < 1 {
+ res.WriteHeader(HTTP_NOT_FOUND)
+ return
+ }
+
+ transactions := make([]*OutgoingBankTransaction, len(transfers))
+ for _, t := range transfers {
+ transactions = append(transactions, NewOutgoingBankTransaction(t))
+ }
+
+ outgoingHistory := OutgoingHistory{
+ OutgoingTransactions: transactions,
+ DebitAccount: CONFIG.Server.CreditAccount,
+ }
+ 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)
+ }
+ return
+ }
+
+ res.WriteHeader(HTTP_OK)
+ res.Write(enc)
}
// This method is currently dead and implemented for API conformance
@@ -357,16 +473,3 @@ func adminAddIncoming(res http.ResponseWriter, req *http.Request) {
// not implemented, because not used
res.WriteHeader(HTTP_BAD_REQUEST)
}
-
-// hashes the request and encodes the request in base64.
-// use this function to hash a transfer request and compare
-// the result to the content of the database.
-func hashRequest(transferBytes []byte) string {
-
- h := crypto.SHA256.New()
- h.Reset()
- h.Write(transferBytes)
- result := make([]byte, 32)
- result = h.Sum(result)
- return base64.StdEncoding.EncodeToString(result)
-}
diff --git a/docs/content/appendix/meeting_notes.tex b/docs/content/appendix/meeting_notes.tex
index 2b901f7..52a8dc4 100644
--- a/docs/content/appendix/meeting_notes.tex
+++ b/docs/content/appendix/meeting_notes.tex
@@ -242,6 +242,30 @@
\item Remove doubled abstractions (Abstracting attestation is not necessary)
\end{itemize}
+\subsection*{17.04.2024}
+
+\textbf{Participants}
+
+\begin{itemize}
+ \item Hiltgen Alain
+ \item Fehrensen Benjamin
+ \item Grothoff Christian
+ \item H\"aberli Joel
+\end{itemize}
+
+\textbf{Topics}
+\begin{itemize}
+ \item Midterm Meeting with Expert Alain Hitlgen.
+ \item Sequence diagram
+\end{itemize}
+
+\textbf{Action points}
+\begin{itemize}
+ \item Fix Bank-Integration API
+ \item Fees must be shown during the payment on the terminal
+ \item The Wire Gateway API must implement "/history/outgoing" and return entries of the transfer table.
+\end{itemize}
+
% TEMPLATE %
\subsection*{TEMPLATE}
diff --git a/docs/content/implementation/c2ec.tex b/docs/content/implementation/c2ec.tex
index a62993f..0899670 100644
--- a/docs/content/implementation/c2ec.tex
+++ b/docs/content/implementation/c2ec.tex
@@ -1,5 +1,34 @@
\section{C2EC}
+\subsection{Decoupling steps}
+
+To decouple different steps in the withdrawal process an event based architecture is implemented. This means that every write action to the database will represent an operation which will trigger an event. The applications processes are listening to those events. The consumer of the API can wait to be notified by the API, by registering to those events via a long polling request at the API. This long-polling will then wait until the listener receives the event and return the received event to the consumer.
+
+Following a short list of events and from whom they are triggered and who listens to them:
+
+\begin{itemize}
+ \item Registration of the Withdrawal Operation.
+ \begin{itemize}
+ \item Registered by: Wallet
+ \item Listened by: Terminal
+ \end{itemize}
+ \item Payment Confirmation sent to the Bank-Integration API of C2EC.
+ \begin{itemize}
+ \item Registered by: Terminal
+ \item Listened by: Attestor
+ \end{itemize}
+ \item Payment attestation success will send a withdrawal operation status update event.
+ \begin{itemize}
+ \item Registered by: Attestor
+ \item Listened by: Consumers (via Bank-Integration-API)
+ \end{itemize}
+ \item Payment attestation failure will trigger a retry event.
+ \begin{itemize}
+ \item Registered by: Attestor
+ \item Listened by: Retrier
+ \end{itemize}
+\end{itemize}
+
\subsection{Bank-Integration API}
The Bank Integration API was implemented according to the specification \cite{taler-bank-integration-api}. It only implements messages and API specific to the indirect withdrawal operation.
@@ -24,11 +53,12 @@ Following endpoints are implemented by the wire gateway API implementation:
\item GET /config
\item POST /transfer
\item GET /history/incoming
+ \item GET /history/outgoing
\end{itemize}
\subsubsection{Keeping track of transfers}
-The Wire-Gateway specification requires the implementor of the API to keep track of incoming transfer requests in order to guarantee the idempotence of the API. Therefore the implementation keeps track of all transfers in the database table \textit{transfers}. It stores a hash of the entire request related to the requests unqiue identifier. If a request with the same UID is sent to the transfer-API, first it is checked that the incoming request is exactly the same as the previous one by comparing the hash of the requests. Only if the hashes are the same, the transfer request is processed further. Otherwise the API responds with a conflict response.
+The Wire-Gateway specification requires the implementor of the API to keep track of incoming transfer requests in order to guarantee the idempotence of the API. Therefore the implementation keeps track of all transfers in the database table \textit{transfers}. It stores the transfer data in the database. If a request with the same UID is sent to the transfer-API, first it is checked that the incoming request is exactly the same as the previous one by comparing the request to the values stored in the database. Only if the hashes are the same, the transfer request is processed further. Otherwise the API responds with a conflict response.
\subsection{Payment Attestation}
@@ -77,18 +107,6 @@ As indicated by the provider client interface, we will use two API of the Wallee
\subsection{Security}
-\subsubsection{API access}
-
-\textbf{Bank-Integration API}
-
-The Bank-Integration API is accessed by Wallets and Terminals. This results in two different device types for the autentication procedure. The Wallet should be able to authenticate against the exchange by using an access token according to the specified authentication flow of the core bank API \cite{taler-bank-core-authentication} which leverages a bearer token as specified by DD-49 \cite{taler-design-document-49}. For terminals the authentication mechanism is based on a basic auth scheme as specified by RFC-7617 \cite{rfc7617}. Therefore a generated access-token used as password and a username which is generated registering the terminal using the cli explained in \autoref{sec-security-registering-providers} are leveraged.
-
-\textbf{Wire-Gateway API}
-
-The wire gateway specifies a basic auth flow \cite{taler-wire-gateway-api-authentication} as described in RFC-7617 \cite{rfc7617}. Therefore the C2EC component allows the configuration of a username and password for the exchange. During the request of the exchange at the wire gateway API, the credentials are checked.
-
-\textbf{Database}
-
\subsubsection{Authenticating at the Wallee ReST API}
\label{sec-security-auth-wallee}
@@ -108,10 +126,20 @@ The Wallee API specifies four Wallee specific headers which are used to authenti
The resulting string must then be UTF-8 encoded according to RFC-3629 \cite{rfc3629}.
+\subsubsection{API access}
+
+\textbf{Bank-Integration API}
+
+The Bank-Integration API is accessed by Wallets and Terminals. This results in two different device types for the autentication procedure. The Wallet should be able to authenticate against the exchange by using an access token according to the specified authentication flow of the core bank API \cite{taler-bank-core-authentication} which leverages a bearer token as specified by DD-49 \cite{taler-design-document-49}. For terminals the authentication mechanism is based on a basic auth scheme as specified by RFC-7617 \cite{rfc7617}. Therefore a generated access-token used as password and a username which is generated registering the terminal using the cli explained in \autoref{sec-security-registering-providers} are leveraged.
+
+\textbf{Wire-Gateway API}
+
+The wire gateway specifies a basic authentication scheme \cite{taler-wire-gateway-api-authentication} as described in RFC-7617 \cite{rfc7617}. Therefore the C2EC component allows the configuration of a username and password for the exchange. During the request of the exchange at the wire gateway API, the credentials are checked.
+
\subsubsection{Registering Providers and Terminals}
\label{sec-security-registering-providers}
-A provider may want to register a new Terminal or maybe even a new provider shall be registered for the exchange. To make this step easier for the exchange operators, a small cli program (command line interface) was implemented. The cli will either ask for a password or generate an access token in case of the terminal registration. The credentials are stored has hashes using a PBKDF (password based key derivation function) so that even if the database leaks, the credentials cannot be easily read by the attackers.
+A provider may want to register a new Terminal or maybe even a new provider shall be registered for the exchange. To make this step easier for the exchange operators, a simple cli program (command line interface) was implemented. The cli will either ask for a password or generate an access token in case of the terminal registration. The credentials are stored has hashes using a PBKDF (password based key derivation function) so that even if the database leaks, the credentials cannot be easily read by an attacker.
\subsubsection{Deactivating Terminals}
diff --git a/docs/content/implementation/concepts.tex b/docs/content/implementation/concepts.tex
index 6ec2281..b85e2bb 100644
--- a/docs/content/implementation/concepts.tex
+++ b/docs/content/implementation/concepts.tex
@@ -30,7 +30,11 @@ Go standard library contians a package called \textit{context}. You will stumble
\subsubsection{Go Routines}
-In concurrent programs it is a challenge to keep up with the complexity which they add to the code. Also one has to take care of interprocess communication and if memory is accessed in shared manner by the program, the access to the data stored should be mutual exclusive. Go therefore comes with the concept of Goroutines. They are designed to be very cheap, lightweight threads. They share the same address space and are just executed besides each other as simple functions. Also Go encourages the use of channels to communicate between different goroutines. The use of channels makes locking memory for concurrent access obsolete and therefore removes possible concurrency problems by making them impossible by design \cite{golang-share-by-communicating}.
+In concurrent programs it is a challenge to keep up with the complexity which they add to the code. Also one has to take care of interprocess communication and if memory is accessed in shared manner by the program, the access to the data stored should be mutual exclusive. Go therefore comes with the concept of Goroutines. They are designed to be very cheap, lightweight threads. They share the same address space and are just executed besides each other as simple functions. Also Go encourages the use of channels to communicate between different goroutines. The use of channels makes locking memory for concurrent access obsolete and therefore removes possible concurrency problems by making them impossible by design \cite{golang-share-by-communicating}.
+
+\subsubsection{Coroutines}
+
+\textit{Coroutines} are the coutnerpart to the \textit{Go routines} in Kotlin and are leveraged in the development of the Terminal Application.
\subsubsection{Memory safety}
diff --git a/docs/thesis.pdf b/docs/thesis.pdf
index ca2b5a5..167e042 100644
--- a/docs/thesis.pdf
+++ b/docs/thesis.pdf
Binary files differ
diff --git a/simulation/c2ec-simulation b/simulation/c2ec-simulation
index 44f4bbb..b0be13e 100755
--- a/simulation/c2ec-simulation
+++ b/simulation/c2ec-simulation
Binary files differ
diff --git a/simulation/encoding.go b/simulation/encoding.go
new file mode 100644
index 0000000..98d1d4a
--- /dev/null
+++ b/simulation/encoding.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "encoding/base32"
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+)
+
+// 32 characters for decoding, using RFC 3548.
+const TALER_BASE32_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+
+func talerBase32Encode(byts []byte) string {
+ return talerBase32Encoding().EncodeToString(byts)
+}
+
+func talerBase32Decode(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)
+}
+
+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)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(wopidBytes) != 32 {
+ err = errors.New("invalid wopid")
+ fmt.Println("encoding", err)
+ return nil, err
+ }
+
+ return wopidBytes, nil
+}
+
+func FormatWopid(wopid []byte) string {
+
+ return url.PathEscape(talerBase32Encode(wopid))
+}
+
+func ParseEddsaPubKey(key EddsaPublicKey) ([]byte, error) {
+
+ return talerBase32Decode(string(key))
+}
diff --git a/simulation/main.go b/simulation/main.go
index 0a0b8b6..3eef8cd 100644
--- a/simulation/main.go
+++ b/simulation/main.go
@@ -5,14 +5,14 @@ import (
"os"
)
-const DISABLE_DELAYS = false
+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/payment"
+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
diff --git a/simulation/model.go b/simulation/model.go
index 9fcf21f..51c94b4 100644
--- a/simulation/model.go
+++ b/simulation/model.go
@@ -51,7 +51,6 @@ func ToWithdrawalOpStatus(s string) (WithdrawalOperationStatus, error) {
type C2ECWithdrawRegistration struct {
ReservePubKey EddsaPublicKey `json:"reserve_pub_key"`
- TerminalId uint64 `json:"terminal_id"`
}
type C2ECWithdrawalStatus struct {
@@ -64,6 +63,7 @@ type C2ECWithdrawalStatus struct {
type C2ECPaymentNotification struct {
ProviderTransactionId string `json:"provider_transaction_id"`
+ TerminalId int `json:"terminal_id"`
Amount Amount `json:"amount"`
- Fees Amount `json:"fees"`
+ Fees Amount `json:"card_fees"`
}
diff --git a/simulation/sim-terminal.go b/simulation/sim-terminal.go
index fe62c43..a639fc8 100644
--- a/simulation/sim-terminal.go
+++ b/simulation/sim-terminal.go
@@ -13,8 +13,10 @@ import (
const TERMINAL_PROVIDER = "Simulation"
+const TERMINAL_ID = "1"
+
// retrieved from the cli tool when added the terminal
-const TERMINAL_USER_ID = TERMINAL_PROVIDER + "-1"
+const TERMINAL_USER_ID = TERMINAL_PROVIDER + "-" + TERMINAL_ID
// retrieved from the cli tool when added the terminal
const TERMINAL_ACCESS_TOKEN = "secret"
@@ -36,7 +38,7 @@ func Terminal(in chan *SimulatedPhysicalInteraction, out chan *SimulatedPhysical
kill <- err
}
- wopid := base64.URLEncoding.EncodeToString(wopidBytes)
+ wopid := FormatWopid(wopidBytes)
fmt.Println("TERMINAL: Generated Nonce (base64 url encoded):", wopid)
uri := QR_CODE_CONTENT_BASE + wopid
fmt.Println("TERMINAL: Taler Withdrawal URI:", uri)
@@ -84,23 +86,30 @@ func Terminal(in chan *SimulatedPhysicalInteraction, out chan *SimulatedPhysical
for {
select {
case w := <-awaitSelection:
- fmt.Println("TERMINAL: selected parameter:", w.ReservePubKey)
+ fmt.Println("TERMINAL: parameters selected:", w.ReservePubKey)
if !DISABLE_DELAYS {
fmt.Println("TERMINAL: simulating user interaction. customer presents card. delay:", TERMINAL_ACCEPT_CARD_DELAY_MS)
- time.Sleep(time.Duration(TERMINAL_ACCEPT_CARD_DELAY_MS))
+ time.Sleep(time.Duration(TERMINAL_ACCEPT_CARD_DELAY_MS) * time.Millisecond)
} else {
fmt.Println("TERMINAL: simulating user interaction. customer presents card.")
}
if !DISABLE_DELAYS {
fmt.Println("TERMINAL: card accepted. terminal waits for response of provider backend. delay:", PROVIDER_BACKEND_PAYMENT_DELAY_MS)
- time.Sleep(time.Duration(PROVIDER_BACKEND_PAYMENT_DELAY_MS))
+ time.Sleep(time.Duration(PROVIDER_BACKEND_PAYMENT_DELAY_MS) * time.Millisecond)
} else {
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{
ProviderTransactionId: "simulation-transaction-id-0",
+ TerminalId: terminalId,
Amount: Amount{
Currency: "CHF",
Fraction: 10,
diff --git a/simulation/sim-wallet.go b/simulation/sim-wallet.go
index c1a4cde..46dd5f7 100644
--- a/simulation/sim-wallet.go
+++ b/simulation/sim-wallet.go
@@ -3,7 +3,6 @@ package main
import (
"bytes"
"crypto/rand"
- "encoding/base32"
"errors"
"fmt"
"net/http"
@@ -13,7 +12,7 @@ import (
"time"
)
-const SIM_WALLET_LONG_POLL_MS_STR = "5000" // 20 seconds
+const SIM_WALLET_LONG_POLL_MS_STR = "10000" // 10 seconds
func Wallet(in chan *SimulatedPhysicalInteraction, out chan *SimulatedPhysicalInteraction, kill chan error) {
@@ -37,17 +36,10 @@ func Wallet(in chan *SimulatedPhysicalInteraction, out chan *SimulatedPhysicalIn
map[string]string{"wopid": wopid},
map[string]string{},
)
- // TODO take terminal id from uri
- parts := strings.Split(TERMINAL_USER_ID, "-")
- tid, err := strconv.Atoi(parts[1])
- if err != nil {
- kill <- err
- }
cdc := NewJsonCodec[C2ECWithdrawRegistration]()
reg := new(C2ECWithdrawRegistration)
reg.ReservePubKey = EddsaPublicKey(simulateReservePublicKey())
- reg.TerminalId = uint64(tid)
body, err := cdc.EncodeToBytes(reg)
regByte := bytes.NewBuffer(body)
// fmt.Println("WALLET : body (bytes):", regByte.Bytes())
@@ -67,7 +59,7 @@ func Wallet(in chan *SimulatedPhysicalInteraction, out chan *SimulatedPhysicalIn
kill <- err
}
- if res.StatusCode != 204 {
+ if res.StatusCode != 200 {
fmt.Println("WALLET : response status from registration:", res.StatusCode)
kill <- errors.New("failed registering the withdrawal parameters")
}
@@ -136,5 +128,5 @@ func simulateReservePublicKey() string {
if err != nil {
return ""
}
- return base32.HexEncoding.EncodeToString(mockedPubKey)
+ return talerBase32Encode(mockedPubKey)
}
diff --git a/wallee-c2ec/.idea/deploymentTargetDropDown.xml b/wallee-c2ec/.idea/deploymentTargetDropDown.xml
index 24adc84..0c0c338 100644
--- a/wallee-c2ec/.idea/deploymentTargetDropDown.xml
+++ b/wallee-c2ec/.idea/deploymentTargetDropDown.xml
@@ -3,20 +3,7 @@
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
- <State>
- <runningDeviceTargetSelectedWithDropDown>
- <Target>
- <type value="RUNNING_DEVICE_TARGET" />
- <deviceKey>
- <Key>
- <type value="VIRTUAL_DEVICE_PATH" />
- <value value="$USER_HOME$/.android/avd/Pixel_3a_API_34_extension_level_7_x86_64.avd" />
- </Key>
- </deviceKey>
- </Target>
- </runningDeviceTargetSelectedWithDropDown>
- <timeTargetWasSelectedWithDropDown value="2024-04-14T20:44:26.962703844Z" />
- </State>
+ <State />
</entry>
</value>
</component>
diff --git a/wallee-c2ec/app/build.gradle.kts b/wallee-c2ec/app/build.gradle.kts
index 1696be2..44d0084 100644
--- a/wallee-c2ec/app/build.gradle.kts
+++ b/wallee-c2ec/app/build.gradle.kts
@@ -10,7 +10,7 @@ android {
defaultConfig {
applicationId = "ch.bfh.habej2.wallee_c2ec"
minSdk = 27
- targetSdk = 34
+ targetSdk = 28
versionCode = 1
versionName = "1.0"
@@ -52,6 +52,10 @@ android {
dependencies {
implementation(libs.okhttp)
implementation(libs.wallee.sdk)
+ implementation(libs.commons.codec)
+ implementation(libs.core)
+ implementation(libs.moshi.kotlin)
+ implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt
index 134115e..8bdc310 100644
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/ExchangeActivity.kt
@@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
@@ -16,7 +17,7 @@ import androidx.compose.ui.platform.LocalContext
import ch.bfh.habej2.wallee_c2ec.config.EXCHANGES
import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme
-class ExchangeActivity : ComponentActivity() {
+class ExchangeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt
index 413a579..13d84fc 100644
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/MainActivity.kt
@@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
@@ -19,14 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
import ch.bfh.habej2.wallee_c2ec.config.loadConfiguredExchanges
import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme
-class MainActivity : ComponentActivity() {
-
- init {
-
- // TODO this crashes somehow
- //loadConfiguredExchanges()
-
- }
+class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt
index b3bbc5e..e63552b 100644
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/PaymentActivity.kt
@@ -1,21 +1,39 @@
package ch.bfh.habej2.wallee_c2ec
import android.os.Bundle
-import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient
+import ch.bfh.habej2.wallee_c2ec.client.wallee.WalleeResponseHandler
+import ch.bfh.habej2.wallee_c2ec.config.TalerBankIntegrationConfig
import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme
+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
+import java.util.Optional
-class PaymentActivity : ComponentActivity() {
+class PaymentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ // TODO retrieve WithdrawalViewModel here
+ // maybe something like savedStateRegistry.getSavedStateProvider("current-withdrawal")
+ val model = WithdrawalViewModel(BankIntegrationClient(
+ TalerBankIntegrationConfig("TestExchange", "http://localhost:8082/c2ec", "Wallee-1", "secret")))
+ val client = ApiClient(WalleeResponseHandler())
+
setContent {
Walleec2ecTheme {
// A surface container using the 'background' color from the theme
@@ -23,20 +41,87 @@ class PaymentActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
- // TODO use wallee sdk here for payment.
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "present card, trigger payment")
-
- Button(onClick = { finish() }) {
- // TODO: abort payment here
- Text(text = "back")
+
+ TextField(
+ value = "",
+ onValueChange = {
+ val optAmount = parseAmount(it)
+ if (optAmount.isPresent) {
+ model.updateAmount(optAmount.get())
+ }
+ },
+ label = { Text(text = "Enter amount") },
+ placeholder = { Text(text = "amount") }
+ )
+
+ Button(enabled = false, onClick = {
+ val withdrawalAmount = LineItem
+ .ListBuilder(
+ model.uiState.encodedWopid,
+ BigDecimal("${model.uiState.amount.value}.${model.uiState.amount.frac}")
+ )
+ .build()
+
+ val transaction = Transaction.Builder(withdrawalAmount)
+ .setCurrency(Currency.getInstance(model.uiState.currency))
+ .setInvoiceReference(model.uiState.encodedWopid)
+ .setMerchantReference(model.uiState.encodedWopid)
+ .setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY)
+ .build()
+
+ try {
+ client.authorizeTransaction(transaction)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }) {
+ Text(text = "")
+ }
+
+ Button(onClick = {
+ model.withdrawalOperationFailed(applicationContext)
+ finish()
+ }) {
+ Text(text = "abort")
}
}
}
}
}
}
+
+ /**
+ * Format expected X[.X], X an integer
+ */
+ private fun parseAmount(inp: String): Optional<Amount> {
+
+ val points = inp.count { it == '.' }
+ if (points > 1) {
+ return Optional.empty()
+ }
+
+ if (points == 1) {
+ val valueStr = inp.split(".")[0]
+ val fracStr = inp.split(".")[1]
+ return try {
+ val value = valueStr.toInt()
+ val frac = fracStr.toInt()
+ Optional.of(Amount(value, frac))
+ } catch (ex: NumberFormatException) {
+ Optional.empty()
+ }
+ }
+
+ return try {
+ val value = inp.toInt()
+ Optional.of(Amount(value, 0))
+ } catch (ex: NumberFormatException) {
+ Optional.empty()
+ }
+ }
}
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt
new file mode 100644
index 0000000..3fe6004
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/QRCodeComposable.kt
@@ -0,0 +1,56 @@
+package ch.bfh.habej2.wallee_c2ec
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
+import com.google.zxing.BarcodeFormat.QR_CODE
+import com.google.zxing.qrcode.QRCodeWriter
+
+@Composable
+fun QRCode(qrCodeContent: String) {
+
+ Column {
+
+ val qrCodeSize = getQrCodeSize()
+ val btmp = makeQrCode(qrCodeContent).asImageBitmap()
+
+ Image(
+ modifier = androidx.compose.ui.Modifier
+ .size(qrCodeSize)
+ .padding(vertical = 8.dp),
+ bitmap = btmp,
+ contentDescription = "Scan the QR Code to start withdrawal",
+ )
+ }
+}
+
+@Composable
+fun getQrCodeSize(): Dp {
+ val configuration = LocalConfiguration.current
+ val screenHeight = configuration.screenHeightDp.dp
+ val screenWidth = configuration.screenWidthDp.dp
+ return min(screenHeight, screenWidth)
+}
+
+private fun makeQrCode(text: String, size: Int = 256): Bitmap {
+ val qrCodeWriter = QRCodeWriter()
+ val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
+ val height = bitMatrix.height
+ val width = bitMatrix.width
+ val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
+ }
+ }
+ return bmp
+} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt
index e07e400..229f911 100644
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalCreationActivity.kt
@@ -1,50 +1,39 @@
package ch.bfh.habej2.wallee_c2ec
import android.os.Bundle
-import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import ch.bfh.habej2.wallee_c2ec.client.c2ec.C2ECClient
-import ch.bfh.habej2.wallee_c2ec.config.TERMINAL_CONFIG
+import androidx.compose.ui.platform.LocalContext
+import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient
+import ch.bfh.habej2.wallee_c2ec.config.TalerBankIntegrationConfig
import ch.bfh.habej2.wallee_c2ec.ui.theme.Walleec2ecTheme
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import java.security.SecureRandom
-import java.util.Base64
+import java.util.concurrent.Executors
-class WithdrawalCreationActivity : ComponentActivity() {
+class WithdrawalCreationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val encodedWopid = encodeWopid(createWopid())
-
- val client = C2ECClient()
+ // TODO Initialize model properly and put in location where everyone involved has access
+ val model = WithdrawalViewModel(BankIntegrationClient(TalerBankIntegrationConfig(
+ "TestExchange", "http://localhost:8082/c2ec", "Wallee-1", "secret"
+ )))
+ model.initialize()
setContent {
- LaunchedEffect(key1 = "") {
- this.launch {
-
- val withdrawal = client.retrieveWithdrawalStatus(encodedWopid, 30000)
-
- // TODO launch payment activity when selected state is returned,
- // when response arrives, send intent to start PaymentActivity for the withdrawal
- // otherwise show error and leave.
- }
- }
+ // start long polling activity for the created wopid and start authorization
+ model.startAuthorizationWhenReadyOrAbort(LocalContext.current)
Walleec2ecTheme {
- // A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@@ -54,13 +43,17 @@ class WithdrawalCreationActivity : ComponentActivity() {
horizontalAlignment = Alignment.CenterHorizontally
) {
- Text(text = "Generated Random WOPID=$encodedWopid")
+ Text(text = "Generated Random WOPID=${model.uiState.encodedWopid}")
- Text(text = "QR-Code content: ${formatTalerUri(encodedWopid)}")
+ Text(text = "QR-Code content: ${formatTalerUri(model.uiState.encodedWopid)}")
- Button(onClick = { finish() }) {
- // TODO: abort payment here
- Text(text = "back")
+ QRCode(model.uiState.encodedWopid)
+
+ Button(onClick = {
+ Executors.newSingleThreadExecutor().submit { model.withdrawalOperationFailed(applicationContext) }
+ finish()
+ }) {
+ Text(text = "abort")
}
}
}
@@ -68,17 +61,5 @@ class WithdrawalCreationActivity : ComponentActivity() {
}
}
- private fun formatTalerUri(encodedWopid: String) =
- "taler://withdraw/$encodedWopid?terminal_id=${TERMINAL_CONFIG.terminalId}"
-
- private fun encodeWopid(wopid: ByteArray) =
- String(Base64.getUrlEncoder().encode(wopid))
-
- private fun createWopid(): ByteArray {
-
- val wopid = ByteArray(32)
- val rand = SecureRandom()
- rand.nextBytes(wopid) // will seed automatically
- return wopid
- }
+ private fun formatTalerUri(encodedWopid: String) = "taler://withdraw/$encodedWopid"
}
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt
new file mode 100644
index 0000000..f23fa7d
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/WithdrawalViewModel.kt
@@ -0,0 +1,102 @@
+package ch.bfh.habej2.wallee_c2ec
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import ch.bfh.habej2.wallee_c2ec.client.taler.BankIntegrationClient
+import ch.bfh.habej2.wallee_c2ec.client.taler.model.PaymentNotification
+import ch.bfh.habej2.wallee_c2ec.client.wallee.WalleeResponseHandler
+import ch.bfh.habej2.wallee_c2ec.encoding.Base32Encode
+import com.wallee.android.till.sdk.ApiClient
+import com.wallee.android.till.sdk.data.Transaction
+import kotlinx.coroutines.launch
+import java.io.Closeable
+import java.math.BigDecimal
+import java.security.SecureRandom
+
+data class Amount(
+ val value: Int,
+ val frac: Int
+) {
+ fun toBigDecimal(): BigDecimal = BigDecimal("$value.$frac")
+}
+
+@Stable
+interface WithdrawalOperationState{
+ val exchangeBankIntegrationApiUrl: String
+ val encodedWopid: String
+ val amount: Amount
+ val currency: String
+ val payed: Boolean
+ val transaction: Transaction?
+}
+
+private class MutableWithdrawalOperationState: WithdrawalOperationState {
+ override var exchangeBankIntegrationApiUrl: String by mutableStateOf("")
+ override var encodedWopid: String by mutableStateOf("")
+ override var amount: Amount by mutableStateOf(Amount(0,0))
+ override var currency: String by mutableStateOf("")
+ override var payed: Boolean by mutableStateOf(false)
+ override var transaction: Transaction? by mutableStateOf(null)
+}
+
+class WithdrawalViewModel(
+ private val bankIntegrationClient: BankIntegrationClient,
+ vararg closeables: Closeable
+) : ViewModel(*closeables) {
+
+ private val _uiState = MutableWithdrawalOperationState()
+ val uiState: WithdrawalOperationState = _uiState
+
+ fun initialize() {
+ _uiState.encodedWopid = Base32Encode(wopid())
+ }
+
+ fun updateAmount(amount: Amount) {
+ _uiState.amount = amount
+ }
+
+ fun updateCurrency(currency: String) {
+ _uiState.currency = currency
+ }
+
+ fun updateWalleeTransaction(transaction: Transaction) {
+ _uiState.transaction = transaction
+ }
+
+ fun startAuthorizationWhenReadyOrAbort(ctx: Context) {
+ viewModelScope.launch {
+ val result = bankIntegrationClient.retrieveWithdrawalStatus(uiState.encodedWopid, 30000)
+ if (result.isPresent) {
+ ctx.startActivity(Intent(ctx, PaymentActivity::class.java))
+ } else {
+ withdrawalOperationFailed(ctx)
+ }
+ }
+ }
+
+ fun withdrawalOperationFailed(ctx: Context? = null) {
+ viewModelScope.launch {
+ bankIntegrationClient.abortWithdrawal(uiState.encodedWopid)
+ ctx?.startActivity(Intent(ctx, MainActivity::class.java))
+ }
+ }
+
+ fun confirmPayment() {
+ viewModelScope.launch{
+ bankIntegrationClient.sendPaymentNotification(PaymentNotification())
+ }
+ }
+
+ private fun wopid(): ByteArray {
+ val wopid = ByteArray(32)
+ val rand = SecureRandom()
+ rand.nextBytes(wopid) // will seed automatically
+ return wopid
+ }
+} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECClient.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECClient.kt
deleted file mode 100644
index 71cbc16..0000000
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECClient.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package ch.bfh.habej2.wallee_c2ec.client.c2ec
-
-import ch.bfh.habej2.wallee_c2ec.config.CURRENT_EXCHANGE
-import okhttp3.Interceptor
-import okhttp3.OkHttpClient
-import okhttp3.Response
-
-class C2ECClient {
-
- companion object {
- const val WITHDRAWAL_OP = "/c2ec/withdrawal-operation"
- const val WITHDRAWAL_STATUS = "$WITHDRAWAL_OP/:wopid"
- const val WITHDRAWAL_PAYMENT = "$WITHDRAWAL_OP/:wopid/payment"
- }
-
- init {
- var client = OkHttpClient.Builder()
- .addInterceptor(C2ECBasicAuthInterceptor())
- .build();
- }
-
- fun retrieveWithdrawalStatus(wopid: String, longPollMs: Int): C2ECWithdrawalOperationStatus {
-
- println("retrieving withdrawal operation status...")
- return C2ECWithdrawalOperationStatus()
- }
-
- fun sendPaymentNotification(payment: C2ECPaymentNotification) {
- println("sending payment notification...")
- }
-
- private class C2ECBasicAuthInterceptor : Interceptor {
-
- override fun intercept(chain: Interceptor.Chain): Response {
-
- val base64EncodedCredentials = java.util.Base64
- .getUrlEncoder()
- .encode("${CURRENT_EXCHANGE!!.terminalId}:${CURRENT_EXCHANGE!!.accessToken}".toByteArray())
- .toString()
-
- return chain.proceed(
- chain.request().newBuilder()
- .header("Authorization", base64EncodedCredentials)
- .build()
- )
- }
- }
-} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECPaymentNotification.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECPaymentNotification.kt
deleted file mode 100644
index d4b292e..0000000
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECPaymentNotification.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package ch.bfh.habej2.wallee_c2ec.client.c2ec
-
-class C2ECPaymentNotification {
-} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECWithdrawalOperationStatus.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECWithdrawalOperationStatus.kt
deleted file mode 100644
index 6aab68c..0000000
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/c2ec/C2ECWithdrawalOperationStatus.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package ch.bfh.habej2.wallee_c2ec.client.c2ec
-
-class C2ECWithdrawalOperationStatus {
-} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt
new file mode 100644
index 0000000..55b9b8d
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt
@@ -0,0 +1,126 @@
+package ch.bfh.habej2.wallee_c2ec.client.taler
+
+import ch.bfh.habej2.wallee_c2ec.client.taler.model.BankIntegrationConfig
+import ch.bfh.habej2.wallee_c2ec.client.taler.model.PaymentNotification
+import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperation
+import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus
+import ch.bfh.habej2.wallee_c2ec.config.TalerBankIntegrationConfig
+import com.squareup.moshi.Moshi
+import okhttp3.HttpUrl
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import java.util.Optional
+
+class BankIntegrationClientException(
+ val status: Int,
+ msg: String
+): RuntimeException(msg)
+
+class BankIntegrationClient(
+ private val config: TalerBankIntegrationConfig
+) {
+
+ private val WITHDRAWAL_OP = "withdrawal-operation"
+
+ private val client: OkHttpClient =
+ OkHttpClient.Builder()
+ .addInterceptor(C2ECBasicAuthInterceptor(config))
+ .build()
+
+ private fun baseUrlBuilder() = HttpUrl.Builder()
+ .encodedPath(config.bankIntegrationBaseUrl)
+
+ private fun configUrl() = baseUrlBuilder()
+ .addPathSegment("config")
+ .build()
+ private fun withdrawalByWopid(encodedWopid: String) = baseUrlBuilder()
+ .addPathSegment(WITHDRAWAL_OP)
+ .addPathSegment(encodedWopid)
+ .build()
+
+ private fun <T> serializer(clazz: Class<T>) = Moshi.Builder().build().adapter(clazz)
+
+ private fun withdrawalConfirm(encodedWopid: String) = withdrawalByWopid(encodedWopid)
+ .newBuilder()
+ .addPathSegment("confirm")
+ .build()
+ private fun withdrawalAbort(encodedWopid: String) = withdrawalByWopid(encodedWopid)
+ .newBuilder()
+ .addPathSegment("abort")
+ .build()
+
+ fun retrieveBankIntegrationConfig(): Optional<BankIntegrationConfig> {
+
+ val req = Request.Builder()
+ .get()
+ .url(configUrl())
+ .build()
+ val response = client.newCall(req).execute()
+ return parseOrEmpty(response)
+ }
+
+ fun retrieveWithdrawalStatus(
+ wopid: String,
+ longPollMs: Int,
+ oldState: WithdrawalOperationStatus = WithdrawalOperationStatus.PENDING
+ ): Optional<WithdrawalOperation> {
+
+ val req = Request.Builder()
+ .get()
+ .url(withdrawalByWopid(wopid)
+ .newBuilder()
+ .addQueryParameter("long_poll_ms", longPollMs.toString())
+ .addQueryParameter("old_state", oldState.value)
+ .build()
+ )
+ .build()
+ val response = client.newCall(req).execute()
+ return parseOrEmpty(response)
+ }
+
+ fun sendPaymentNotification(payment: PaymentNotification) {
+
+
+ println("sending payment notification...")
+ }
+
+ fun abortWithdrawal(wopid: String) {
+ println("aborting withdrawal")
+ }
+
+ private inline fun <reified T: Any> parseOrEmpty(response: Response): Optional<T> {
+
+ if (response.isSuccessful) {
+ if (response.body != null) {
+ val content = serializer(T::class.java).fromJson(response.body!!.source())
+ if (content != null) {
+ return Optional.of(content)
+ }
+ return Optional.empty()
+ }
+ return Optional.empty()
+ }
+ throw BankIntegrationClientException(response.code, "request unsuccessful")
+ }
+
+ private class C2ECBasicAuthInterceptor(
+ private val config: TalerBankIntegrationConfig
+ ) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+
+ val base64EncodedCredentials = java.util.Base64
+ .getUrlEncoder()
+ .encode("${config.terminalId}:${config.accessToken}".toByteArray())
+ .toString()
+
+ return chain.proceed(
+ chain.request().newBuilder()
+ .header("Authorization", base64EncodedCredentials)
+ .build()
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/BankIntegrationConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/BankIntegrationConfig.kt
new file mode 100644
index 0000000..07bc352
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/BankIntegrationConfig.kt
@@ -0,0 +1,20 @@
+package ch.bfh.habej2.wallee_c2ec.client.taler.model
+
+import com.squareup.moshi.Json
+
+data class BankIntegrationConfig(
+ @Json(name = "name") val name: String,
+ @Json(name = "version") val version: String,
+ @Json(name = "implementation") val implementation: String,
+ @Json(name = "currency") val currency: String,
+ @Json(name = "currency_specification") val currencySpec: CurrencySpecification
+)
+
+data class CurrencySpecification(
+ @Json(name = "name") val name: String,
+ @Json(name = "currency") val currency: String,
+ @Json(name = "num_fractional_input_digits") val numFractionalInputDigits: Int,
+ @Json(name = "num_fractional_normal_digits") val numFractionalNormalDigits: Int,
+ @Json(name = "num_fractional_trailing_zero_digits") val numFractionalTrailingZeroDigits: Int,
+ @Json(name = "alt_unit_names") val altUnitNames: String
+) \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/PaymentNotification.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/PaymentNotification.kt
new file mode 100644
index 0000000..9a8bcc0
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/PaymentNotification.kt
@@ -0,0 +1,4 @@
+package ch.bfh.habej2.wallee_c2ec.client.taler.model
+
+class PaymentNotification {
+} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperation.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperation.kt
new file mode 100644
index 0000000..fcdf107
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperation.kt
@@ -0,0 +1,3 @@
+package ch.bfh.habej2.wallee_c2ec.client.taler.model
+
+data class WithdrawalOperation (val status: WithdrawalOperationStatus) \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperationStatus.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperationStatus.kt
new file mode 100644
index 0000000..2ac8cbc
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperationStatus.kt
@@ -0,0 +1,9 @@
+package ch.bfh.habej2.wallee_c2ec.client.taler.model
+
+enum class WithdrawalOperationStatus(val value: String) {
+
+ PENDING("pending"),
+ SELECTED("selected"),
+ CONFIRMED("confirmed"),
+ ABORTED("aborted")
+} \ 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
new file mode 100644
index 0000000..5419ce9
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt
@@ -0,0 +1,19 @@
+package ch.bfh.habej2.wallee_c2ec.client.wallee
+
+import com.wallee.android.till.sdk.ResponseHandler
+import com.wallee.android.till.sdk.data.TransactionResponse
+
+class WalleeResponseHandler : ResponseHandler() {
+
+ override fun authorizeTransactionReply(response: TransactionResponse?) {
+
+ // TODO find out how to read the transaction id here....
+
+ if (response == null) {
+ // TODO
+ return
+ }
+
+ response.transaction.metaData.get("id")
+ }
+} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/ExchangeConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/ExchangeConfig.kt
deleted file mode 100644
index 5226699..0000000
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/ExchangeConfig.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package ch.bfh.habej2.wallee_c2ec.config
-
-val EXCHANGES: List<ExchangeConfig> = mutableListOf()
-
-var CURRENT_EXCHANGE : ExchangeConfig? = null
-
-data class ExchangeConfig(
- val displayName: String,
- val c2ecBaseUrl: String,
- val terminalId: String,
- val accessToken: String
-)
-
-fun loadConfiguredExchanges() {
-
- // TODO load from config file here
- EXCHANGES.plus(listOf(
- ExchangeConfig("localhost", "http://localhost:8082/c2ec", "Wallee-1", "secret")
- ))
- CURRENT_EXCHANGE = EXCHANGES[0]
-} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt
new file mode 100644
index 0000000..a5dcab9
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TalerBankIntegrationConfig.kt
@@ -0,0 +1,10 @@
+package ch.bfh.habej2.wallee_c2ec.config
+
+// TODO how to configure ?? -> implement ativity allowing to choose one of the configured exchanges
+
+data class TalerBankIntegrationConfig(
+ val displayName: String,
+ val bankIntegrationBaseUrl: String,
+ val terminalId: String,
+ val accessToken: String
+) \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt
index 5ebe54d..e69c74b 100644
--- a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/config/TerminalConfig.kt
@@ -1,13 +1,8 @@
package ch.bfh.habej2.wallee_c2ec.config
-val TERMINAL_CONFIG: TerminalConfig = TerminalConfig("Wallee-1", "secret")
+// TODO how to configure ??
data class TerminalConfig(
val terminalId: String,
val accessToken: String
)
-
-fun loadTerminalConfig() {
-
- // TODO load from config file
-} \ No newline at end of file
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt
new file mode 100644
index 0000000..15c1805
--- /dev/null
+++ b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/encoding/TalerBase32Codec.kt
@@ -0,0 +1,7 @@
+package ch.bfh.habej2.wallee_c2ec.encoding
+
+import org.apache.commons.codec.binary.Base32
+
+fun Base32Encode(byts: ByteArray): String = Base32().encodeAsString(byts)
+
+fun Base32Decode(enc: String) = Base32().decode(enc) \ No newline at end of file
diff --git a/wallee-c2ec/gradle/libs.versions.toml b/wallee-c2ec/gradle/libs.versions.toml
index c9d385d..16e0f1a 100644
--- a/wallee-c2ec/gradle/libs.versions.toml
+++ b/wallee-c2ec/gradle/libs.versions.toml
@@ -1,5 +1,8 @@
[versions]
agp = "8.3.1"
+appcompat = "1.6.1"
+commonsCodec = "1.16.1"
+core = "3.5.0"
kotlin = "1.9.0"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -8,11 +11,15 @@ espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.7.0"
composeBom = "2023.08.00"
+moshiKotlin = "1.15.1"
okhttp = "4.12.0"
sdk = "0.9.12"
[libraries]
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" }
+core = { module = "com.google.zxing:core", version.ref = "core" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -26,6 +33,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
wallee-sdk = { module = "com.wallee.android.till:sdk", version.ref = "sdk" }