[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-cashless2ecash] branch master updated: code: implement wire gatew
From: |
gnunet |
Subject: |
[taler-cashless2ecash] branch master updated: code: implement wire gateway |
Date: |
Wed, 03 Apr 2024 19:13:05 +0200 |
This is an automated email from the git hooks/post-receive script.
joel-haeberli pushed a commit to branch master
in repository cashless2ecash.
The following commit(s) were added to refs/heads/master by this push:
new dfdf9bb code: implement wire gateway
dfdf9bb is described below
commit dfdf9bbb7c928827928f3855ff5afea91daa28b9
Author: Joel-Haeberli <haebu@rubigen.ch>
AuthorDate: Wed Apr 3 19:12:52 2024 +0200
code: implement wire gateway
---
.gitignore | 2 +
bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru | 2 +-
c2ec/auth.go | 34 +++
c2ec/bank-integration.go | 59 ++---
c2ec/c2ec-config.yaml | 1 +
c2ec/config.go | 1 +
c2ec/db.go | 33 ++-
c2ec/db/0000-c2ec_schema.sql | 5 +
c2ec/db/0000-c2ec_transfers.sql | 22 ++
c2ec/main.go | 9 +-
c2ec/payto.go | 90 ++++++++
c2ec/postgres.go | 191 ++++++++++++++++-
c2ec/provider-client.go | 2 +-
c2ec/simulation-attestor.go | 8 +-
c2ec/simulation-client.go | 6 +-
c2ec/wallee-attestor.go | 4 +-
c2ec/wallee-client.go | 5 +-
c2ec/wire-gateway.go | 308 ++++++++++++++++++++++++---
18 files changed, 690 insertions(+), 92 deletions(-)
diff --git a/.gitignore b/.gitignore
index bcbd7bc..382b88a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
schemaspy/*.jar
schemaspy/Makefile
infra/
+bruno/
+c2ec/c2ec
LocalMakefile
diff --git a/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru
b/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru
index 7de1e1b..3281b68 100644
--- a/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru
+++ b/bruno/c2ec/(LOCAL-BIA) Withdrawal Status.bru
@@ -5,7 +5,7 @@ meta {
}
get {
- url: http://localhost:8081/c2ec/withdrawal-operation/WOPID
+ url: http://localhost:8081/c2ec/withdrawal-operation/
body: none
auth: none
}
diff --git a/c2ec/auth.go b/c2ec/auth.go
index 8fdb761..a8e102e 100644
--- a/c2ec/auth.go
+++ b/c2ec/auth.go
@@ -7,6 +7,23 @@ import (
const AUTHORIZATION_HEADER = "Authorization"
const BEARER_TOKEN_PREFIX = "Bearer"
+// Authentication in C2EC requires following use cases:
+// 1. Wallet authenticating itself using the Bank-Integration API
+// 2. Provider authentication itself using the Bank-Integration API
+// 3. Exchange Wirewatch component, using the Wire-Gateway API
+// 3.1 The Wire-Gateway API specifies Basic-Auth (RFC7617)
+//
+// The Wire-Gateway API is the only API specification which makes
+// prescriptions concerning the authenticaion. For simplicity,
+// Basic-Auth will be applied to all client types (Exchange, Wallet, Providers)
+// To distinguish what client type wants to request, a special format
+// for the username type is created.
+//
+// use `PROVIDER-[PROVIDER_ID]:[PROVIDER_SECRET]` for provider clients
+// use `WALLET:`
+//
+// in case no prefix was specified, it is assumed that the request originates
+// from the exchange.
func isAllowed(req *http.Request) bool {
return true
@@ -20,3 +37,20 @@ func isAllowed(req *http.Request) bool {
// return strings.EqualFold(token, "")
}
+
+// Is this needed? Understand how the wallet authenticates itself at the
exchange currently first.
+// https://docs.taler.net/design-documents/049-auth.html#dd48-token
+// https://docs.taler.net/core/api-corebank.html#authentication
+//
+// /accounts/$USERNAME/token
+//
+// The username in our case is the reserve public key
+// registered for withdrawal. At the initial registration
+// of the reserve public key we leverage a TOFU trust model.
+// during the registration of the reserve public key a new
+// access token will be created with a limited lifetime.
+// The token will not be refreshable and become invalid
+// only after a few minutes. Since the Wallet will register
+// a wopid and
+// func handleTokenRequest() {
+// }
diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go
index 9b5305a..86fe78a 100644
--- a/c2ec/bank-integration.go
+++ b/c2ec/bank-integration.go
@@ -17,6 +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_ABORTION_PATTERN =
WITHDRAWAL_OPERATION_BY_WOPID_PATTERN + "/abort"
const DEFAULT_LONG_POLL_MS = 1000
@@ -42,10 +43,8 @@ type BankIntegrationConfig struct {
}
type C2ECWithdrawRegistration struct {
- Wopid WithdrawalIdentifier `json:"wopid"`
- ReservePubKey EddsaPublicKey `json:"reserve_pub_key"`
- Amount Amount `json:"amount"`
- TerminalId uint64 `json:"terminal_id"`
+ ReservePubKey EddsaPublicKey `json:"reserve_pub_key"`
+ TerminalId uint64 `json:"terminal_id"`
}
type C2ECWithdrawalStatus struct {
@@ -98,10 +97,27 @@ func handleWithdrawalRegistration(res http.ResponseWriter,
req *http.Request) {
return
}
+ // read and validate the wopid path parameter
+ wopid := req.PathValue(WOPID_PARAMETER)
+ if _, ok := any(wopid).(WithdrawalIdentifier); !ok {
+
+ 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 = DB.RegisterWithdrawal(
- registration.Wopid,
+ WithdrawalIdentifier(wopid),
registration.ReservePubKey,
- registration.Amount,
registration.TerminalId,
)
@@ -357,34 +373,3 @@ func getWithdrawalOrWriteError(wopid string, res
http.ResponseWriter, reqUri str
res.Write(withdrawalStatusBytes)
}
}
-
-// ----------------------
-// OFFICIAL MODELS
-// ----------------------
-//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationPostRequest
-type BankWithdrawalOperationPostRequest struct {
- ReservePub string `json:"reserve_pub"`
- SelectedExchange string `json:"selected_exchange"`
-}
-
-//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationPostResponse
-type BankWithdrawalOperationPostResponse struct {
- Status WithdrawalOperationStatus `json:"status"`
- ConfirmTransferUrl string
`json:"confirm_transfer_url"`
- TransferDone bool `json:"transfer_done"`
-}
-
-//
https://docs.taler.net/core/api-bank-integration.html#tsref-type-BankWithdrawalOperationStatus
-type BankWithdrawalOperationStatus struct {
- Status WithdrawalOperationStatus `json:"status"`
- Amount Amount `json:"amount"`
- SenderWire string `json:"sender_wire"`
- SuggestedExchange string
`json:"suggested_exchange"`
- ConfirmTransferUrl string
`json:"confirm_transfer_url"`
- WireTypes []string `json:"wire_types"`
- SelectedReservePub string
`json:"selected_reserve_pub"`
- SelectedExchangeAccount string
`json:"selected_exchange_account"`
- Aborted bool `json:"aborted"`
- SelectionDone bool
`json:"selection_done"`
- TransferDone bool `json:"transfer_done"`
-}
diff --git a/c2ec/c2ec-config.yaml b/c2ec/c2ec-config.yaml
index 3032884..1938a3d 100644
--- a/c2ec/c2ec-config.yaml
+++ b/c2ec/c2ec-config.yaml
@@ -5,6 +5,7 @@ c2ec:
unix-domain-socket: false
unix-socket-path: "c2ec.sock"
fail-on-missing-attestors: false # forced if prod=true
+ credit-account: "payto://iban/CH50030202099498" # this account must be
specified at the providers backends as well
db:
host: "localhost"
port: 5432
diff --git a/c2ec/config.go b/c2ec/config.go
index d4a3028..0ce723d 100644
--- a/c2ec/config.go
+++ b/c2ec/config.go
@@ -19,6 +19,7 @@ type C2ECServerConfig struct {
UseUnixDomainSocket bool `yaml:"unix-domain-socket"`
UnixSocketPath string `yaml:"unix-socket-path"`
StrictAttestors bool `yaml:"fail-on-missing-attestors"`
+ CreditAccount string `yaml:"credit-account"`
}
type C2ECDatabseConfig struct {
diff --git a/c2ec/db.go b/c2ec/db.go
index 12fe2e4..0a24202 100644
--- a/c2ec/db.go
+++ b/c2ec/db.go
@@ -7,6 +7,7 @@ import (
const PROVIDER_TABLE_NAME = "c2ec.provider"
const PROVIDER_FIELD_NAME_ID = "terminal_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"
const PROVIDER_FIELD_NAME_BACKEND_CREDENTIALS = "backend_credentials"
@@ -31,9 +32,14 @@ const WITHDRAWAL_FIELD_NAME_LAST_RETRY = "last_retry_ts"
const WITHDRAWAL_FIELD_NAME_RETRY_COUNTER = "retry_counter"
const WITHDRAWAL_FIELD_NAME_COMPLETION_PROOF = "completion_proof"
+const TRANSFER_TABLE_NAME = "c2ec.transfer"
+const TRANSFER_FIELD_NAME_ID = "request_uid"
+const TRANSFER_FIELD_NAME_HASH = "request_hash"
+
type Provider struct {
ProviderTerminalID int64 `db:"provider_id"`
Name string `db:"name"`
+ PaytoTargetType string `db:"payto_target_type"`
BackendBaseURL string `db:"backend_base_url"`
BackendCredentials string `db:"backend_credentials"`
}
@@ -47,7 +53,7 @@ type Terminal struct {
}
type Withdrawal struct {
- WithdrawalId []byte `db:"withdrawal_id"`
+ WithdrawalId uint64 `db:"withdrawal_id"`
Wopid uint64 `db:"wopid"`
ReservePubKey []byte `db:"reserve_pub_key"`
RegistrationTs int64 `db:"registration_ts"`
@@ -67,6 +73,11 @@ type TalerAmountCurrency struct {
Curr string `db:"curr"`
}
+type Transfer struct {
+ RequestId HashCode `db:"request_uid"`
+ RequestHash string `db:"request_hash"`
+}
+
// C2ECDatabase defines the operations which a
// C2EC compliant database interface must implement
// in order to be bound to the c2ec API.
@@ -76,13 +87,15 @@ type C2ECDatabase interface {
RegisterWithdrawal(
wopid WithdrawalIdentifier,
resPubKey EddsaPublicKey,
- amount Amount,
terminalId uint64,
) error
// Get the withdrawal associated with the given wopid.
GetWithdrawalByWopid(wopid string) (*Withdrawal, error)
+ // Get the withdrawal associated with the provider specific transaction
id.
+ GetWithdrawalByProviderTransactionId(tid string) (*Withdrawal, error)
+
// When the terminal receives the notification of the
// Provider, that the payment went through, this will
// save the provider specific transaction id in the database
@@ -110,12 +123,28 @@ type C2ECDatabase interface {
completionProof []byte,
) 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.
+ GetConfirmedWithdrawals(start int, delta int) ([]*Withdrawal, error)
+
// Get a provider entry by its name
GetTerminalProviderByName(name string) (*Provider, error)
+ // Get a provider entry by its name
+ GetTerminalProviderByPaytoTargetType(paytoTargetType string)
(*Provider, error)
+
// Get a terminal entry by its identifier
GetTerminalById(id int) (*Terminal, error)
+ // Returns the transfer for the given hashcode.
+ GetTransferById(requestUid HashCode) (*Transfer, error)
+
+ // Inserts a new transfer into the database.
+ AddTransfer(requestId HashCode, requestHash string) error
+
// This will listen for on the given channel
// and write results to the out channels.
// Errors will be propagated through the errs
diff --git a/c2ec/db/0000-c2ec_schema.sql b/c2ec/db/0000-c2ec_schema.sql
index 75f1f0e..6c85d52 100644
--- a/c2ec/db/0000-c2ec_schema.sql
+++ b/c2ec/db/0000-c2ec_schema.sql
@@ -35,6 +35,7 @@ COMMENT ON TYPE taler_amount_currency
CREATE TABLE IF NOT EXISTS provider (
provider_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
+ payto_target_type TEXT NOT NULL UNIQUE,
backend_base_url TEXT NOT NULL,
backend_credentials TEXT NOT NULL
);
@@ -44,6 +45,10 @@ COMMENT ON COLUMN provider.provider_id
IS 'Uniquely identifies a provider';
COMMENT ON COLUMN provider.name
IS 'Name of the provider, used for selection in transaction proofing';
+COMMENT ON COLUMN provider.payto_target_type
+ IS 'The Payto target type associated with the provider. Each payto target
type
+ has exctly one provider. This is needed so that the attestor client can be
dynamically
+ selected by C2EC.';
COMMENT ON COLUMN provider.backend_base_url
IS 'URL of the provider backend for transaction proofing';
COMMENT ON COLUMN provider.backend_credentials
diff --git a/c2ec/db/0000-c2ec_transfers.sql b/c2ec/db/0000-c2ec_transfers.sql
new file mode 100644
index 0000000..6eb2fdb
--- /dev/null
+++ b/c2ec/db/0000-c2ec_transfers.sql
@@ -0,0 +1,22 @@
+BEGIN;
+
+SELECT _v.register_patch('0000-c2ec-transfers', ARRAY['0000-c2ec-schema'],
NULL);
+
+SET search_path TO c2ec;
+
+CREATE TABLE IF NOT EXISTS transfer (
+ request_uid INT8 UNIQUE PRIMARY KEY,
+ request_hash TEXT NOT NULL
+);
+COMMENT ON TABLE transfer
+ IS 'Table storing transfers which are sent by the exchange.';
+COMMENT ON COLUMN transfers.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 transfers.request_hash
+ IS 'Hash of the entire transfer request. Requests with the same
+ request identifier must have the identical hash to be processed
+ further.';
+
+COMMIT;
\ No newline at end of file
diff --git a/c2ec/main.go b/c2ec/main.go
index 9eaf45e..120c267 100644
--- a/c2ec/main.go
+++ b/c2ec/main.go
@@ -21,6 +21,11 @@ const DEFAULT_C2EC_CONFIG_PATH = "c2ec-config.yaml"
var DB C2ECDatabase
+// This map contains all clients initialized during the
+// startup of the application. The clients SHALL register
+// themselfs during the setup!!
+var PROVIDER_CLIENTS = map[string]ProviderClient{}
+
// Starts the c2ec process.
// The program takes following arguments (ordered):
// 1. path to configuration file (.yaml) (optional)
@@ -154,7 +159,7 @@ func setupBankIntegrationRoutes(router *http.ServeMux) {
)
router.HandleFunc(
- POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_PATTERN,
+ POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN,
handleWithdrawalRegistration,
)
@@ -164,7 +169,7 @@ func setupBankIntegrationRoutes(router *http.ServeMux) {
)
router.HandleFunc(
- POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN,
+ POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_PAYMENT_PATTERN,
handlePaymentNotification,
)
diff --git a/c2ec/payto.go b/c2ec/payto.go
new file mode 100644
index 0000000..a40266c
--- /dev/null
+++ b/c2ec/payto.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+const PAYTO_PARTS_SEPARATOR = "/"
+
+const PAYTO_SCHEME_PREFIX = "payto://"
+const PAYTO_TAGRET_TYPE_IBAN = "iban"
+const PAYTO_TARGET_TYPE_WALLEE_TRANSACTION = "wallee-transaction"
+
+var REGISTERED_TARGET_TYPES = []string{
+ "ach",
+ "bic",
+ "iban",
+ "upi",
+ "bitcoin",
+ "ilp",
+ "void",
+ "ldap",
+ "eth",
+ "interac-etransfer",
+ "wallee-transaction",
+}
+
+// This method parses a payto-uri (RFC 8905:
https://www.rfc-editor.org/rfc/rfc8905.html)
+// The method only parses the target type "wallee-transaction" as specified
+// in the payto GANA registry
(https://gana.gnunet.org/payto-payment-target-types/payto_payment_target_types.html)
+func ParsePaytoWalleeTransaction(uri string) (string, int, error) {
+
+ if t, i, err := ParsePaytoUri(uri); err != nil {
+
+ tid, err := strconv.Atoi(i)
+ if err != nil {
+ return "", -1, errors.New("invalid transaction-id for
wallee-transaction")
+ }
+
+ return t, tid, nil
+ } else {
+ return t, -1, err
+ }
+}
+
+// returns the Payto Target Type and Target Identifier as string
+// if the uri is malformed, an error is returned (target type and
+// identifier will be empty strings).
+func ParsePaytoUri(uri string) (string, string, error) {
+
+ if raw, found := strings.CutPrefix(uri, PAYTO_SCHEME_PREFIX); found {
+
+ parts := strings.Split(raw, PAYTO_PARTS_SEPARATOR)
+ if len(parts) < 2 {
+ return "", "", errors.New("invalid wallee-transaction
payto-uri")
+ }
+
+ return parts[0], parts[1], nil
+ }
+ return "", "", errors.New("invalid payto-uri")
+}
+
+func FormatPaytoWalleeTransaction(tid int) string {
+ return fmt.Sprintf("%s%s/%d",
+ PAYTO_SCHEME_PREFIX,
+ PAYTO_TARGET_TYPE_WALLEE_TRANSACTION,
+ tid,
+ )
+}
+
+func ParsePaytoTargetType(uri string) (string, error) {
+
+ if raw, found := strings.CutPrefix(uri, PAYTO_SCHEME_PREFIX); found {
+
+ parts := strings.Split(raw, PAYTO_PARTS_SEPARATOR)
+ if len(parts) < 2 {
+ return "", errors.New("invalid wallee-transaction
payto-uri")
+ }
+
+ for _, target := range REGISTERED_TARGET_TYPES {
+ if strings.EqualFold(target, parts[0]) {
+ return parts[0], nil
+ }
+ }
+ return "", errors.New("target type '" + parts[0] + "' is not
registered")
+ }
+ return "", errors.New("invalid payto-uri")
+}
diff --git a/c2ec/postgres.go b/c2ec/postgres.go
index acd703e..e11a635 100644
--- a/c2ec/postgres.go
+++ b/c2ec/postgres.go
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"errors"
"fmt"
+ "math"
"time"
"github.com/jackc/pgx/v5"
@@ -14,14 +15,16 @@ import (
"github.com/jackc/pgxlisten"
)
+const PS_ASC_SELECTOR = "ASC"
+const PS_DESC_SELECTOR = "DESC"
+
const PS_INSERT_WITHDRAWAL = "INSERT INTO " + WITHDRAWAL_TABLE_NAME + " (" +
WITHDRAWAL_FIELD_NAME_WOPID + "," +
WITHDRAWAL_FIELD_NAME_RESPUBKEY + "," +
WITHDRAWAL_FIELD_NAME_STATUS + "," +
WITHDRAWAL_FIELD_NAME_TS + "," +
- WITHDRAWAL_FIELD_NAME_AMOUNT + "," +
WITHDRAWAL_FIELD_NAME_TERMINAL_ID + ")" +
- " VALUES ($1, $2, $3, $4, $5, $6);"
+ " VALUES ($1, $2, $3, $4, $5);"
const PS_GET_UNCONFIRMED_WITHDRAWALS = "SELECT * FROM " +
WITHDRAWAL_TABLE_NAME +
" WHERE " + WITHDRAWAL_FIELD_NAME_TRANSACTION_ID + " IS NOT NULL" +
@@ -40,15 +43,33 @@ const PS_FINALISE_PAYMENT = "UPDATE " +
WITHDRAWAL_TABLE_NAME + " SET (" +
" = ($1, $2)" +
" WHERE " + WITHDRAWAL_FIELD_NAME_ID + "=$3"
+const PS_CONFIRMED_TRANSACTIONS = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
+ " LIMIT $1" +
+ " OFFSET $2" +
+ " ORDER BY " + WITHDRAWAL_FIELD_NAME_ID + " $3"
+
const PS_GET_WITHDRAWAL_BY_WOPID = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
" WHERE " + WITHDRAWAL_FIELD_NAME_WOPID + "=$1"
+const PS_GET_WITHDRAWAL_BY_PTID = "SELECT * FROM " + WITHDRAWAL_TABLE_NAME +
+ " WHERE " + WITHDRAWAL_FIELD_NAME_TRANSACTION_ID + "=$1"
+
const PS_GET_PROVIDER_BY_NAME = "SELECT * FROM " + PROVIDER_TABLE_NAME +
" WHERE " + PROVIDER_FIELD_NAME_NAME + "=$1"
+const PS_GET_PROVIDER_BY_PAYTO_TARGET_TYPE = "SELECT * FROM " +
PROVIDER_TABLE_NAME +
+ " WHERE " + PROVIDER_FIELD_NAME_PAYTO_TARGET_TYPE + "=$1"
+
const PS_GET_TERMINAL_BY_ID = "SELECT * FROM " + TERMINAL_TABLE_NAME +
" WHERE " + TERMINAL_FIELD_NAME_ID + "=$1"
+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)"
+
// Postgres implementation of the C2ECDatabase
type C2ECPostgres struct {
C2ECDatabase
@@ -92,7 +113,6 @@ func NewC2ECPostgres(cfg *C2ECDatabseConfig) (*C2ECPostgres,
error) {
func (db *C2ECPostgres) RegisterWithdrawal(
wopid WithdrawalIdentifier,
resPubKey EddsaPublicKey,
- amount Amount,
terminalId uint64,
) error {
@@ -104,7 +124,6 @@ func (db *C2ECPostgres) RegisterWithdrawal(
resPubKey,
SELECTED,
ts.Unix(),
- amount,
terminalId,
)
if err != nil {
@@ -141,6 +160,32 @@ func (db *C2ECPostgres) GetWithdrawalByWopid(wopid string)
(*Withdrawal, error)
}
}
+func (db *C2ECPostgres) GetWithdrawalByProviderTransactionId(tid string)
(*Withdrawal, error) {
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_WITHDRAWAL_BY_PTID,
+ tid,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ withdrawals, err := pgx.CollectRows(row,
pgx.RowToAddrOfStructByName[Withdrawal])
+ if err != nil {
+ return nil, err
+ }
+
+ if len(withdrawals) < 1 {
+ return nil, nil
+ }
+ return withdrawals[0], nil
+ }
+}
+
func (db *C2ECPostgres) NotifyPayment(
wopid WithdrawalIdentifier,
providerTransactionId string,
@@ -209,6 +254,65 @@ func (db *C2ECPostgres) FinaliseWithdrawal(
return nil
}
+// The query at the postgres database works as specified by the
+// wire gateway api.
+func (db *C2ECPostgres) GetConfirmedWithdrawals(start int, delta int)
([]*Withdrawal, error) {
+
+ sort := PS_ASC_SELECTOR
+ if delta < 0 {
+ sort = PS_DESC_SELECTOR
+ }
+
+ limit := math.Abs(float64(delta))
+ offset := start
+ if delta < 0 {
+ offset = start - int(limit)
+ }
+ if offset < 0 {
+ offset = 0
+ }
+
+ var row pgx.Rows
+ var err error
+ if start < 0 {
+ // use MAX(id) instead of a concrete id, because start
+ // identifier was negative. Inidicates to read the most
+ // recent ids.
+ row, err = db.pool.Query(
+ db.ctx,
+ PS_CONFIRMED_TRANSACTIONS,
+ limit,
+ "MAX("+WITHDRAWAL_FIELD_NAME_ID+")",
+ sort,
+ )
+ } else {
+ row, err = db.pool.Query(
+ db.ctx,
+ PS_CONFIRMED_TRANSACTIONS,
+ limit,
+ offset,
+ sort,
+ )
+ }
+
+ if err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ withdrawals, err := pgx.CollectRows(row,
pgx.RowToAddrOfStructByName[Withdrawal])
+ if err != nil {
+ return nil, err
+ }
+
+ return withdrawals, nil
+ }
+}
+
func (db *C2ECPostgres) GetTerminalProviderByName(name string) (*Provider,
error) {
if row, err := db.pool.Query(
@@ -237,6 +341,34 @@ func (db *C2ECPostgres) GetTerminalProviderByName(name
string) (*Provider, error
}
}
+func (db *C2ECPostgres) GetTerminalProviderByPaytoTargetType(paytoTargetType
string) (*Provider, error) {
+
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_PROVIDER_BY_PAYTO_TARGET_TYPE,
+ paytoTargetType,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ provider, err := pgx.CollectRows(row,
pgx.RowToAddrOfStructByName[Provider])
+ if err != nil {
+ return nil, err
+ }
+
+ if len(provider) < 1 {
+ return nil, nil
+ }
+
+ return provider[0], nil
+ }
+}
+
func (db *C2ECPostgres) GetTerminalById(id int) (*Terminal, error) {
if row, err := db.pool.Query(
@@ -252,13 +384,60 @@ func (db *C2ECPostgres) GetTerminalById(id int)
(*Terminal, error) {
defer row.Close()
- terminal, err := pgx.CollectRows(row,
pgx.RowToAddrOfStructByName[Terminal])
+ terminals, err := pgx.CollectRows(row,
pgx.RowToAddrOfStructByName[Terminal])
+ if err != nil {
+ return nil, err
+ }
+
+ if len(terminals) < 1 {
+ return nil, nil
+ }
+
+ return terminals[0], nil
+ }
+}
+
+func (db *C2ECPostgres) GetTransferById(requestUid HashCode) (*Transfer,
error) {
+
+ if row, err := db.pool.Query(
+ db.ctx,
+ PS_GET_TRANSFER_BY_ID,
+ requestUid,
+ ); err != nil {
+ if row != nil {
+ row.Close()
+ }
+ return nil, err
+ } else {
+
+ defer row.Close()
+
+ transfers, err := pgx.CollectRows(row,
pgx.RowToAddrOfStructByName[Transfer])
if err != nil {
return nil, err
}
- return terminal[0], nil
+ if len(transfers) < 1 {
+ return nil, nil
+ }
+ return transfers[0], nil
}
+
+}
+
+func (db *C2ECPostgres) AddTransfer(requestId HashCode, requestHash string)
error {
+
+ res, err := db.pool.Query(
+ db.ctx,
+ PS_ADD_TRANSFER,
+ requestId,
+ requestHash,
+ )
+ if err != nil {
+ return err
+ }
+ res.Close()
+ return nil
}
func (db *C2ECPostgres) ListenForWithdrawalStatusChange(
diff --git a/c2ec/provider-client.go b/c2ec/provider-client.go
index 39260ff..22da076 100644
--- a/c2ec/provider-client.go
+++ b/c2ec/provider-client.go
@@ -5,7 +5,7 @@ type ProviderTransaction interface {
Bytes() []byte
}
-type ProviderClient[T any] interface {
+type ProviderClient interface {
SetupClient(provider *Provider) error
GetTransaction(transactionId string) (ProviderTransaction, error)
Refund(transactionId string) error
diff --git a/c2ec/simulation-attestor.go b/c2ec/simulation-attestor.go
index 30be17e..41b3214 100644
--- a/c2ec/simulation-attestor.go
+++ b/c2ec/simulation-attestor.go
@@ -12,9 +12,11 @@ import (
)
type SimulationAttestor struct {
+ Attestor[SimulationClient]
+
listener *pgxlisten.Listener
provider *Provider
- providerClient ProviderClient[WalleeClient]
+ providerClient ProviderClient
}
func (wa *SimulationAttestor) Setup(p *Provider, cfg *C2ECDatabseConfig) (chan
*pgconn.Notification, error) {
@@ -84,15 +86,11 @@ func (wa *SimulationAttestor) Attest(withdrawalId int,
providerTransactionId str
err = DB.FinaliseWithdrawal(withdrawalId, CONFIRMED,
transaction.Bytes())
if err != nil {
- // TODO : do we abort the withdrawal here??
errs <- err
}
} else {
- // TODO : this might be too early ?! What if the payment was
not yet
- // processed by the Wallee backend? Needs testing.
err = DB.FinaliseWithdrawal(withdrawalId, ABORTED,
transaction.Bytes())
if err != nil {
- // TODO : do we abort the withdrawal here??
errs <- err
}
}
diff --git a/c2ec/simulation-client.go b/c2ec/simulation-client.go
index e7ae253..a9c8304 100644
--- a/c2ec/simulation-client.go
+++ b/c2ec/simulation-client.go
@@ -11,7 +11,7 @@ type SimulationTransaction struct {
}
type SimulationClient struct {
- ProviderClient[SimulationTransaction]
+ ProviderClient
// toggle this to simulate failed transactions.
AllowNextWithdrawal bool
@@ -22,9 +22,11 @@ func (st *SimulationTransaction) AllowWithdrawal() bool {
return st.allow
}
-func (*SimulationClient) SetupClient(p *Provider) error {
+func (sc *SimulationClient) SetupClient(p *Provider) error {
fmt.Println("setting up simulation client. probably not what you want
in production")
+
+ PROVIDER_CLIENTS["Simulation"] = sc
return nil
}
diff --git a/c2ec/wallee-attestor.go b/c2ec/wallee-attestor.go
index b65ed21..efc3709 100644
--- a/c2ec/wallee-attestor.go
+++ b/c2ec/wallee-attestor.go
@@ -12,9 +12,11 @@ import (
)
type WalleeAttestor struct {
+ Attestor[WalleeClient]
+
listener *pgxlisten.Listener
provider *Provider
- providerClient ProviderClient[WalleeClient]
+ providerClient ProviderClient
}
func (wa *WalleeAttestor) Setup(p *Provider, cfg *C2ECDatabseConfig) (chan
*pgconn.Notification, error) {
diff --git a/c2ec/wallee-client.go b/c2ec/wallee-client.go
index 1c40af1..6347d0a 100644
--- a/c2ec/wallee-client.go
+++ b/c2ec/wallee-client.go
@@ -30,7 +30,7 @@ type WalleeCredentials struct {
}
type WalleeClient struct {
- ProviderClient[WalleeTransaction]
+ ProviderClient
name string
baseUrl string
@@ -52,6 +52,9 @@ func (w *WalleeClient) SetupClient(p *Provider) error {
w.name = p.Name
w.baseUrl = p.BackendBaseURL
w.credentials = creds
+
+ PROVIDER_CLIENTS[w.name] = w
+
return nil
}
diff --git a/c2ec/wire-gateway.go b/c2ec/wire-gateway.go
index e308a28..473ba9c 100644
--- a/c2ec/wire-gateway.go
+++ b/c2ec/wire-gateway.go
@@ -1,9 +1,13 @@
package main
import (
- "bytes"
+ "context"
+ "crypto"
+ "encoding/base64"
"log"
http "net/http"
+ "strconv"
+ "time"
)
const WIRE_GATEWAY_CONFIG_ENDPOINT = "/config"
@@ -15,6 +19,8 @@ const WIRE_HISTORY_INCOMING_PATTERN =
WIRE_GATEWAY_HISTORY_ENDPOINT + "/incoming
const WIRE_HISTORY_OUTGOING_PATTERN = WIRE_GATEWAY_HISTORY_ENDPOINT +
"/outgoing"
const WIRE_ADMIN_ADD_INCOMING_PATTERN = "/admin/add-incoming"
+const INCOMING_RESERVE_TRANSACTION_TYPE = "RESERVE"
+
// https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig
type WireConfig struct {
Name string `json:"name"`
@@ -54,20 +60,21 @@ type IncomingReserveTransaction struct {
ReservePub EddsaPublicKey `json:"reserve_pub"`
}
-// https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory
-type OutgoingHistory struct {
- OutgoingBankTransaction []OutgoingBankTransaction
`json:"outgoing_bank_transaction"`
- DebitAccount string `json:"debit_account"`
-}
-
-//
https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction
-type OutgoingBankTransaction struct {
- RowId int `json:"row_id"`
- Date 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.Date = Timestamp{
+ Ts: int(w.RegistrationTs),
+ }
+ t.DebitAccount = ""
+ t.ReservePub = EddsaPublicKey(w.ReservePubKey)
+ t.RowId = int(w.WithdrawalId)
+ t.Type = INCOMING_RESERVE_TRANSACTION_TYPE
+ return t
}
func wireGatewayConfig(res http.ResponseWriter, req *http.Request) {
@@ -90,14 +97,250 @@ func wireGatewayConfig(res http.ResponseWriter, req
*http.Request) {
func transfer(res http.ResponseWriter, req *http.Request) {
- res.WriteHeader(HTTP_OK)
- res.Write(bytes.NewBufferString("retrieved transfer request").Bytes())
+ jsonCodec := NewJsonCodec[TransferRequest]()
+ transfer, err := ReadStructFromBody(req, jsonCodec)
+ if err != nil {
+
+ err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
+ Title: "invalid request",
+ Detail: "the transfer request is malformed (error: "
+ err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ paytoTargetType, tid, err :=
ParsePaytoWalleeTransaction(transfer.CreditAccount)
+ if err != nil {
+ err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
+ Title: "invalid payto-uri",
+ Detail: "the transfer request contains an invalid
payto-uri (error: " + err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ p, err := DB.GetTerminalProviderByPaytoTargetType(paytoTargetType)
+ if err != nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
+ Title: "database request failed",
+ Detail: "failed to retrieve the provider for the
payto target type '" + paytoTargetType + "'",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ t, err := DB.GetTransferById(transfer.RequestUid)
+ 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
+ }
+
+ 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)
+ if err != nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
+ Title: "database request failed",
+ Detail: "there was an error creating the
transfer",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+ } else {
+ // the transfer is only processed if the body matches.
+ if requestHash != t.RequestHash {
+ err := WriteProblem(res, HTTP_BAD_REQUEST,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_TRANSFER_INVALID_REQ",
+ Title: "invalid request",
+ Detail: "the transfer request did not match
previous request with the same request identifier",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ ptid := strconv.Itoa(tid)
+ w, err := DB.GetWithdrawalByProviderTransactionId(ptid)
+ if err != nil || w == nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
+ Title: "database request failed",
+ Detail: "there was an error processing the
database query or no withdrawal could been found.",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ refundClient := PROVIDER_CLIENTS[p.Name]
+ if refundClient == nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_UNKNOWN_TRANSFER_MECHANISM",
+ Title: "unknown refund mechanism",
+ Detail: "the target type of the payto uri for
the transfer is not registered",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+ refundClient.Refund(ptid)
+ }
}
+// :query start: *Optional.*
+//
+// Row identifier to explicitly set the *starting point* of the query.
+//
+// :query delta:
+//
+// The *delta* value that determines the range of the query.
+//
+// :query long_poll_ms: *Optional.*
+//
+// If this parameter is specified and the result of the query would be
empty,
+// the bank will wait up to ``long_poll_ms`` milliseconds for new
transactions
+// that match the query to arrive and only then send the HTTP response.
+// A client must never rely on this behavior, as the bank may return a
response
+// immediately or after waiting only a fraction of ``long_poll_ms``.
func historyIncoming(res http.ResponseWriter, req *http.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()
+ }
+
+ withdrawals, err := DB.GetConfirmedWithdrawals(start, delta)
+
+ if err != nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_DATABASE_FAILURE",
+ Title: "database request failed",
+ Detail: "there was an error processing the database
query",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
+ if len(withdrawals) < 1 {
+ res.WriteHeader(HTTP_NOT_FOUND)
+ return
+ }
+
+ transactions := make([]*IncomingReserveTransaction, len(withdrawals))
+ for _, w := range withdrawals {
+ transactions = append(transactions,
NewIncomingReserveTransaction(w))
+ }
+
+ enc, err :=
NewJsonCodec[[]*IncomingReserveTransaction]().EncodeToBytes(&transactions)
+ if err != nil {
+ err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR,
&RFC9457Problem{
+ TypeUri: TALER_URI_PROBLEM_PREFIX +
"/C2EC_RESPONSE_ENCODING_FAILED",
+ Title: "encoding failed",
+ Detail: "the encoding of the response failed (error:"
+ err.Error() + ")",
+ Instance: req.RequestURI,
+ })
+ if err != nil {
+ res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
+ }
+ return
+ }
+
res.WriteHeader(HTTP_OK)
- res.Write(bytes.NewBufferString("retrieved history incoming
request").Bytes())
+ res.Write(enc)
}
// This method is currently dead and implemented for API conformance
@@ -107,25 +350,22 @@ func historyOutgoing(res http.ResponseWriter, req
*http.Request) {
res.WriteHeader(HTTP_BAD_REQUEST)
}
-// ---------------------
-// TESTING (ONLY ADMINS)
-// ---------------------
-
-// https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest
-type AddIncomingRequest struct {
- Amount Amount `json:"amount"`
- ReservcePub EddsaPublicKey `json:"reserve_pub"`
- DebitAccount string `json:"debit_account"`
-}
-
-//
https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse
-type AddIncomingResponse struct {
- Timestamp Timestamp `json:"timestamp"`
-}
-
// This method is currently dead and implemented for API conformance
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)
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-cashless2ecash] branch master updated: code: implement wire gateway,
gnunet <=