cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

commit 477d3f5f949db3e57fd8aa8dd59c766cb94ddcfb
parent 16e69702fe0dc194465870ba87162f2af0863a26
Author: Joel-Haeberli <haebu@rubigen.ch>
Date:   Sun, 28 Apr 2024 23:13:05 +0200

chore: save commit

Diffstat:
Mc2ec/api-bank-integration.go | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mc2ec/api-terminals.go | 23+++++++++++++++++++++++
Mc2ec/encoding_test.go | 2+-
Mc2ec/go.mod | 2+-
Mc2ec/main.go | 8+++++++-
Rc2ec/retrier.go -> c2ec/proc-retrier.go | 0
Mc2ec/proc-transfer.go | 8++++++--
Mc2ec/utils.go | 20++++++++++++++++++++
Msimulation/go.mod | 2+-
Aspecs/bank-integration-api.plantuml | 19+++++++++++++++++++
Mspecs/c2ec.plantuml | 45+++++++++++++++++++++------------------------
Aspecs/c2ec_apis.plantuml | 31+++++++++++++++++++++++++++++++
Dspecs/components_images.webp | 0
Dspecs/nonce2ecash_api_spec.yml | 116-------------------------------------------------------------------------------
Dspecs/nonce2ecash_class_diagram.plantuml | 76----------------------------------------------------------------------------
Aspecs/terminals-api.plantuml | 18++++++++++++++++++
Dspecs/wallee_app_class_diagram.plantuml | 89-------------------------------------------------------------------------------
Aspecs/wire-gateway-api.plantuml | 19+++++++++++++++++++
Dwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/BankIntegrationClient.kt | 127-------------------------------------------------------------------------------
Awallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt | 11-----------
Awallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerTerminalConfig.kt | 9+++++++++
Dwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/BankIntegrationConfig.kt | 21---------------------
Dwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/PaymentNotification.kt | 5-----
Awallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalWithdrawalSetup.kt | 24++++++++++++++++++++++++
Dwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperation.kt | 4----
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/WithdrawalOperationStatus.kt | 24++++++++++++++++++++++--
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt | 4++--
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt | 2++
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt | 18------------------
Mwallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt | 50++++++++++++++++++++++++++++----------------------
31 files changed, 465 insertions(+), 527 deletions(-)

diff --git a/c2ec/api-bank-integration.go b/c2ec/api-bank-integration.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "context" "encoding/base64" "fmt" @@ -38,7 +37,6 @@ 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 BankWithdrawalOperationPostRequest struct { @@ -147,11 +145,19 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { // read and validate request query parameters shouldStartLongPoll := true longPollMilli := DEFAULT_LONG_POLL_MS + oldState := DEFAULT_OLD_STATE if longPollMilliPtr, accepted := AcceptOptionalParamOrWriteResponse( "long_poll_ms", strconv.Atoi, req, res, ); accepted { if longPollMilliPtr != nil { longPollMilli = *longPollMilliPtr + if oldStatePtr, accepted := AcceptOptionalParamOrWriteResponse( + "old_state", ToWithdrawalOperationStatus, req, res, + ); accepted { + if oldStatePtr != nil { + oldState = *oldStatePtr + } + } } else { // this means parameter was not given. // no long polling (simple get) @@ -172,6 +178,22 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { if shouldStartLongPoll { + withdrawalAlreadyChanged := make(chan *Withdrawal) + go func() { + // when the current state differs from the old_state + // of the request, return immediately. This goroutine + // does this check and sends the withdrawal to through + // the specified channel, if the withdrawal was already + // changed. + withdrawal, err := DB.GetWithdrawalByWopid(wpd) + if err != nil { + LogError("bank-integration-api", err) + } + if withdrawal.WithdrawalStatus != oldState { + withdrawalAlreadyChanged <- withdrawal + } + }() + timeoutCtx, cancelFunc := context.WithTimeout( req.Context(), time.Duration(longPollMilli)*time.Millisecond, @@ -202,6 +224,9 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { case <-notifications: writeWithdrawalOrError(wpd, res) return + case w := <-withdrawalAlreadyChanged: + writeWithdrawalOrError(w.Wopid, res) + return } } } @@ -210,8 +235,36 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { } func handleWithdrawalAbort(res http.ResponseWriter, req *http.Request) { - res.WriteHeader(HTTP_OK) - res.Write(bytes.NewBufferString("retrieved withdrawal operation abortion request").Bytes()) + + // read and validate the wopid path parameter + wopid := req.PathValue(WOPID_PARAMETER) + wpd, err := ParseWopid(wopid) + if err != nil { + LogWarn("bank-integration-api", "wopid "+wopid+" not valid") + res.WriteHeader(HTTP_BAD_REQUEST) + return + } + + withdrawal, err := DB.GetWithdrawalByWopid(wpd) + if err != nil { + LogError("bank-integration-api", err) + res.WriteHeader(HTTP_NOT_FOUND) + return + } + + if withdrawal.WithdrawalStatus == CONFIRMED { + res.WriteHeader(HTTP_CONFLICT) + return + } + + err = DB.FinaliseWithdrawal(int(withdrawal.WithdrawalRowId), ABORTED, make([]byte, 0)) + if err != nil { + LogError("bank-integration-api", err) + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + return + } + + res.WriteHeader(HTTP_NO_CONTENT) } // Tries to load a WithdrawalOperationStatus from the database. If no diff --git a/c2ec/api-terminals.go b/c2ec/api-terminals.go @@ -10,6 +10,7 @@ const TERMINAL_API_CONFIG = "/config" const TERMINAL_API_REGISTER_WITHDRAWAL = "/withdrawals" const TERMINAL_API_WITHDRAWAL_STATUS = "/withdrawals/{wopid}" const TERMINAL_API_CHECK_WITHDRAWAL = "/withdrawals/{wopid}/check" +const TERMINAL_API_ABORT_WITHDRAWAL = "/withdrawals/{wopid}/abort" type TerminalConfig struct { Name string `json:"name"` @@ -192,6 +193,28 @@ func handleWithdrawalCheck(res http.ResponseWriter, req *http.Request) { res.WriteHeader(HTTP_NO_CONTENT) } +func handleWithdrawalStatusTerminal(res http.ResponseWriter, req *http.Request) { + + _, auth, err := authAndParseProvider(req) + if err != nil || !auth { + res.WriteHeader(HTTP_UNAUTHORIZED) + return + } + + handleWithdrawalStatus(res, req) +} + +func handleWithdrawalAbortTerminal(res http.ResponseWriter, req *http.Request) { + + _, auth, err := authAndParseProvider(req) + if err != nil || !auth { + res.WriteHeader(HTTP_UNAUTHORIZED) + return + } + + handleWithdrawalAbort(res, req) +} + func preventNilAmount(a *Amount) Amount { if a == nil { diff --git a/c2ec/encoding_test.go b/c2ec/encoding_test.go @@ -104,7 +104,7 @@ 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.Log("failed because retrieving random 64 bytes failed") t.FailNow() } diff --git a/c2ec/go.mod b/c2ec/go.mod @@ -6,6 +6,7 @@ require ( github.com/jackc/pgx/v5 v5.5.5 github.com/jackc/pgxlisten v0.0.0-20230728233309-2632bad3185a golang.org/x/crypto v0.22.0 + gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 ) @@ -19,5 +20,4 @@ require ( golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/c2ec/main.go b/c2ec/main.go @@ -14,6 +14,7 @@ import ( const GET = "GET " const POST = "POST " +const DELETE = "DELETE " // https://docs.taler.net/core/api-terminal.html#endpoints-for-integrated-sub-apis const BANK_INTEGRATION_API = "/taler-integration" @@ -307,6 +308,11 @@ func setupTerminalRoutes(router *http.ServeMux) { router.HandleFunc( GET+TERMINAL_API_WITHDRAWAL_STATUS, - handleWithdrawalStatus, + handleWithdrawalStatusTerminal, + ) + + router.HandleFunc( + DELETE+TERMINAL_API_ABORT_WITHDRAWAL, + handleWithdrawalAbortTerminal, ) } diff --git a/c2ec/retrier.go b/c2ec/proc-retrier.go diff --git a/c2ec/proc-transfer.go b/c2ec/proc-transfer.go @@ -47,7 +47,6 @@ func transferCallback(notification *Notification, errs chan error) { return } - // Load Transfer from DB transfer, err := DB.GetTransferById(transferRequestUid) if err != nil { LogError("refunder", err) @@ -55,7 +54,6 @@ func transferCallback(notification *Notification, errs chan error) { errs <- err } - // Load Provider from DB (by payto uri) paytoTargetType, tid, err := ParsePaytoWalleeTransaction(transfer.CreditAccount) if err != nil { errs <- errors.New("malformed transfer request uid: " + err.Error()) @@ -93,11 +91,17 @@ func transferCallback(notification *Notification, errs chan error) { } } +func transferScheduler() { + // TODO somehow schedule transfer jobs periodically +} + func transferFailed( transfer *Transfer, errs chan error, ) { + // TODO: Delay retry somehow... randomize due to self-synch problems + if transfer.Retries > 2 { err := DB.UpdateTransfer( transfer.RequestUid, diff --git a/c2ec/utils.go b/c2ec/utils.go @@ -1,5 +1,10 @@ package main +import ( + "errors" + "strings" +) + // https://docs.taler.net/core/api-common.html#hash-codes type WithdrawalIdentifier string @@ -29,3 +34,18 @@ const ( ABORTED WithdrawalOperationStatus = "aborted" CONFIRMED WithdrawalOperationStatus = "confirmed" ) + +func ToWithdrawalOperationStatus(s string) (WithdrawalOperationStatus, error) { + switch strings.ToLower(s) { + case string(PENDING): + return PENDING, nil + case string(SELECTED): + return SELECTED, nil + case string(ABORTED): + return ABORTED, nil + case string(CONFIRMED): + return CONFIRMED, nil + default: + return "", errors.New("invalid withdrawal operation status '" + s + "'") + } +} diff --git a/simulation/go.mod b/simulation/go.mod @@ -2,4 +2,4 @@ module c2ec-simulation go 1.22.1 -require github.com/gofrs/uuid v4.4.0+incompatible // indirect +require github.com/gofrs/uuid v4.4.0+incompatible diff --git a/specs/bank-integration-api.plantuml b/specs/bank-integration-api.plantuml @@ -0,0 +1,18 @@ +@startuml + +participant bia as "Taler Bank-Integration API" +participant Wallet + +Wallet -> bia: GET /config +bia --> Wallet: Configuration Data + +Wallet -> bia: GET /withdrawal-operation/[WOPID] +bia --> Wallet: Withdrawal Operation Status + +Wallet -> bia: (3) POST /withdrawal-operation/[WOPID] +bia --> Wallet: Withdrawal Operation Parameter Registration + +Wallet -> bia: POST /withdrawal-operation/[WOPID]/abort +bia --> Wallet: Abort Withdrawal Operation + +@enduml +\ No newline at end of file diff --git a/specs/c2ec.plantuml b/specs/c2ec.plantuml @@ -8,40 +8,37 @@ participant TerminalBackend as "Terminal Backend" participant Terminal actor TerminalOwner as "Terminal Owner" -Terminal -> Terminal: configures Exchange (configured, overwritable) -User -> TerminalOwner: "Hi, I want to withdraw 20 coins with my Credit Card" +Terminal -> Terminal: configures Exchanges +User -> TerminalOwner: "Hi, I want to withdraw 20 CHF using Taler with my Credit Card" TerminalOwner -> Terminal: start Taler Withdrawal Application and enters amount -Terminal -> Terminal: Generate wopid -Terminal -> C2EC: (1) Start long polling (wopid) +Terminal -> Terminal: Generate WOPID +Terminal -> C2EC: (0) Setup Withdrawal +Terminal -> C2EC: (1) Start long polling (WOPID) activate C2EC -Terminal -> Terminal: Create QR code (Wopid, Exchange, Amount) +Terminal -> Terminal: Create QR code (WOPID, Exchange, Amount) Terminal -> Wallet: (2) Scan QR code activate Wallet Wallet -> Wallet: If ToS for Exchange not yet accepted, do here. Wallet -> Wallet: Create Reserve Key-Pair -Wallet -> C2EC: (3) Send Wopid, ReservePublicKey -Wallet -> C2EC: Start long polling (wopid) -C2EC -> C2EC: Create mapping (Nonce, ReservePublicKey, Amount) +Wallet -> C2EC: (3) Register reserve public key +C2EC -> C2EC: Link WOPID to reserve public key C2EC --> Terminal: (4) End long polling (selected) deactivate C2EC -Terminal -> Terminal: Show summary with Fees -User -> Terminal: (5) Approve and present card -Terminal -> TerminalBackend: (6) Execute Payment -TerminalBackend --> Terminal: (7) Payment response (success/failure) -alt Payment successful - Terminal -> C2EC: (8) Send PaymentNotification (SUCCESS) - C2EC -> C2EC: Change Mapping state to confirmed - C2EC -> TerminalBackend: (9) Verify payment - C2EC -> C2EC: (10) set status to confirmed or abort - C2EC -> Wallet: (11) notify Wallet that payment has been processed. +Terminal -> Terminal: Show summary with Fees (if any) +User -> Terminal: (5) Approve and authorize transaction +Terminal -> TerminalBackend: (6) Execute transaction +TerminalBackend --> Terminal: (7) transaction response (success/failure) +alt transaction successful + Terminal -> C2EC: (8) Send Confirmation Request (SUCCESS) + C2EC -> TerminalBackend: (9) Verify transaction + C2EC -> C2EC: (10) confirm or abort withdrawal Exchange -> C2EC: (11) get transaction history Exchange -> Exchange: Create Reserve with amount and reserve public key. - Wallet -> Exchange: (12) withdraw when reserve is created + Wallet -> Exchange: (12) Await creation of the reserve Wallet -> Exchange: (13) Withdraw digital cash -else Payment not successful - Terminal -> C2EC: (8) Send PaymentNotification (UNSUCESSFUL) - C2EC -> C2EC: Remove entry in N2C mapping table. - C2EC --> Wallet: End long polling (abort) - Wallet -> Wallet: Remove Wopid and Public Key + deactivate Wallet +else transaction not successful + Terminal -> C2EC: (8) Send Confirmation Request (UNSUCESSFUL) + C2EC -> C2EC: Transition withdrawal to abort state. end @enduml diff --git a/specs/c2ec_apis.plantuml b/specs/c2ec_apis.plantuml @@ -0,0 +1,30 @@ +@startuml + +participant Terminal + +participant Wallet + +box "C2EC" #LightBlue + participant TerminalsApi as "Taler Terminals API" + participant bia as "Taler Bank-Integration API" + participant wga as "Taler Wire-Gateway API" +end box + +participant tww as "Taler Wirewatch (Exchange)" + +Terminal -> TerminalsApi: (0) POST /withdrawals +TerminalsApi --> Terminal: Withdrawal Request ID + +Terminal -> TerminalsApi: (1) GET /withdrawals/[WOPID] +TerminalsApi --> Terminal: Withdrawal Operation Status + +Wallet -> bia: (3) POST /withdrawal-operation/[WOPID] +bia --> Wallet: Withdrawal Operation Parameter Registration + +Terminal -> TerminalsApi: (8) GET /withdrawals/[WOPID]/check +TerminalsApi --> Terminal: response (no content on success) + +tww -> wga: (11) GET /history/incoming +wga --> tww: Incoming History Data + +@enduml +\ No newline at end of file diff --git a/specs/components_images.webp b/specs/components_images.webp Binary files differ. diff --git a/specs/nonce2ecash_api_spec.yml b/specs/nonce2ecash_api_spec.yml @@ -1,116 +0,0 @@ -openapi: 3.0.3 -info: - title: nonce2ecash API - description: API managing a mapping table which links a nonce to a reserve public key. This allows withdrawing from the exchange by a given nonce. - version: 0.0.1 -paths: - /n2c/nonces/withdrawal-registration: - post: - summary: Register Nonce to Reserve Public Key - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/N2CWithdrawalRequest' - responses: - '200': - description: Message received successfully - tags: - - nonces - /n2c/nonces/withdrawal-processing: - post: - summary: Provider notifies the exchange about the payment (if it was successful or not) - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/N2CPaymentNotification' - responses: - '200': - description: Message received successfully - tags: - - nonces - /n2c/nonces/withdrawal-status: - get: - summary: Check Status - parameters: - - name: nonce - in: query - description: The nonce for which to check the status - required: true - schema: - type: string - - name: listenForStatus - in: query - description: The status to listen for the given nonce - required: true - schema: - $ref: '#/components/schemas/N2CStatus' - responses: - '200': - description: Status retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/N2CMapping' - tags: - - nonces -tags: - - name: nonces - description: managing and retrieving information about the mapping table for the nonce to reserve public key mapping. -components: - schemas: - N2CWithdrawalRequest: - description: Maps a nonce generated by the provider to a reserve public key generated by the wallet. - type: object - properties: - nonce: - type: string - reservePubKey: - type: string - format: byte - amount: - type: string - providerType: - $ref: '#/components/schemas/N2CProviderType' - N2CPaymentNotification: - description: Notifies the exchange about an executed payment. This will trigger a provider specific attestation of the payment according to the N2CProviderType. This allows to define the attestation process for each supported provider individually. - type: object - properties: - nonce: - type: string - providerTransactionId: - type: string - description: a unique identifier of the provider which identifies the transaction in the providers system. - success: - type: boolean - amount: - type: number - fees: - type: number - N2CStatus: - description: Describes the states a nonce to reserve public key mapping can be in. - type: string - enum: - - ESTABLISHED - - PAYED - - RESERVE_CREATED - - RESERVE_CREATION_FAILED - N2CMapping: - description: Maps a nonce generated by the provider to a reserve public key generated by the wallet. - type: object - properties: - nonce: - type: string - reservePubKey: - type: string - format: byte - status: - $ref: '#/components/schemas/N2CStatus' - N2CProviderType: - description: Signals the exchange which provider is talking to him. Like this more Providers can be easily supported by adding their verfification flow to the code of nonce2ecash. This allows to extend the system with various providers. - type: string - enum: - - WALLEE diff --git a/specs/nonce2ecash_class_diagram.plantuml b/specs/nonce2ecash_class_diagram.plantuml @@ -1,75 +0,0 @@ -@startuml - -interface PaymentVerifier { - verifyPayment(nonce: string): boolean -} - -enum WalleeTransactionState { - CREATE - PENDING - CONFIRMED - PROCESSING - FAILED - AUTHORIZED - VOIDED - COMPLETED - FULFILL - DECLINE -} - -interface WalleeClient { - getTransactionState(transactionId: string): WalleeTransactionState -} - -class PaymentVerifierProxy { - verifiers: PaymentVerifier[] - -- - getVerifierNames(): string[] - getVerifier(name: string): PaymentVerifier -} - -class WalleePaymentVerifier { - walleeClient: WalleeClient - -- - verifyPayment(nonce: string): boolean -} - -interface NonceToReservePubkeyMappingRepository { - createMapping(nonce: string, reservePubkey: byte[]) - removeMapping(nonce: string) - getMapping(nonce: string): -} - -class NonceToReservePubkeyMappingService { - - repo: NonceToReservePubkeyMappingRepository - - paymentVerifier: PaymentVerifier -} - -enum Status { - ESTABLISHED - PAYED - RESERVE_CREATED -} - -class NonceToReservePubkey { - + nonce: number - + provider_type: ProviderType - + reserve_pubkey: number - + state: Status - + established_t_s: timestamp -} - -enum ProviderType { - WALLEE -} - -NonceToReservePubkey --* ProviderType -NonceToReservePubkey --* Status - -WalleeClient ..> WalleeTransactionState : use -PaymentVerifierProxy --* PaymentVerifier : knows -WalleePaymentVerifier --o WalleeClient: needs -WalleePaymentVerifier ..> PaymentVerifier : implements -NonePaymentVerifier ..> PaymentVerifier : implements -NonceToReservePubkeyMappingService --> NonceToReservePubkeyMappingRepository: use -@enduml -\ No newline at end of file diff --git a/specs/terminals-api.plantuml b/specs/terminals-api.plantuml @@ -0,0 +1,18 @@ +@startuml + +participant Terminal +participant TerminalsApi as "Taler Terminals API" + +Terminal -> TerminalsApi: GET /config +TerminalsApi --> Terminal: Configuration Data + +Terminal -> TerminalsApi: (0) POST /withdrawals +TerminalsApi --> Terminal: Withdrawal Request ID + +Terminal -> TerminalsApi: (1) GET /withdrawals/[WOPID] +TerminalsApi --> Terminal: Withdrawal Operation Status + +Terminal -> TerminalsApi: (8) GET /withdrawals/[WOPID]/check +TerminalsApi --> Terminal: response (no content on success) + +@enduml diff --git a/specs/wallee_app_class_diagram.plantuml b/specs/wallee_app_class_diagram.plantuml @@ -1,88 +0,0 @@ -@startuml - -class TalerTerminalConfiguration { - - exchangeName: string - - exchangeBaseUrl: string -} - -interface TalerExchangeClient { - -- - + getConfig(): TalerTerminalConfiguration - + getKeys() - + awaitEstablishedNonce() - + notifyAboutPayment() - + getToS() -} - -interface NonceProvider { - -- - + generateNonce() -} - -class Fees { - - talerClient: TalerExchangeClient - -- - + totalFees(): number - - exchangeFees(): number - - walleeFees(): number -} - -class NonceEstablishmentActivity { - - talerExchangeClient: TalerExchangeClient - - nonceProvider: NonceProvider - -- - + displayQrCode() -} - -class WithdrawAmountActivity { - - talerExchangeClient: TalerExchangeClient - -- - + calculateFees() - + displaySummary() -} - -class ApproveConditionsActivity { - - talerExchangeClient: TalerExchangeClient - -- - + accept() - + deny() -} - -class PaymentActivity { -} - -class PaymentResultActivity { -} - - -class RegisterNonceToReservePublicKeyMessage { - - nonce: string - - reservePubKey: byte[] - -- - + RegisterNonceToReservePublicKeyMessage(nonce: string, reservePubKey: byte[]) -} - -class PaymentNotificationMessage { - - nonce: string - - success: bool - - amount: double - - fees: double - -- - + PaymentNotificationMessage(nonce: string, success: bool, amount: double, fees: double) -} - -class NonceQR { - - nonce: string - - exchangeConfiguration: TalerTerminalConfiguration -} - -TalerExchangeClient ..> TalerTerminalConfiguration : use -NonceEstablishmentActivity --> NonceQR : use -NonceEstablishmentActivity ..> TalerExchangeClient : use -NonceEstablishmentActivity ..> NonceProvider : use -WithdrawAmountActivity ..> TalerExchangeClient : use -ApproveConditionsActivity ..> TalerExchangeClient : use - -Fees ..* TalerExchangeClient : use - -@enduml -\ No newline at end of file diff --git a/specs/wire-gateway-api.plantuml b/specs/wire-gateway-api.plantuml @@ -0,0 +1,18 @@ +@startuml + +participant wga as "Taler Wire-Gateway API" +participant tww as "Taler Wirewatch (Exchange)" + +tww -> wga: GET /config +wga --> tww: Configuration + +tww -> wga: (11) GET /history/incoming +wga --> tww: Incoming History Data + +tww -> wga: POST /transfer +wga --> tww: Transfer Confirmation + +tww -> wga: GET /history/outgoing +wga --> tww: Outgoing History Data + +@enduml +\ 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 @@ -1,126 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.client.taler - -import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig -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 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/TerminalClient.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClient.kt @@ -0,0 +1,154 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler + +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig +import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalApiConfig +import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmationRequest +import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup +import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetupResponse +import ch.bfh.habej2.wallee_c2ec.client.taler.model.WithdrawalOperationStatus +import com.squareup.moshi.Moshi +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import java.util.Optional + +class TerminalClientException( + val status: Int, + msg: String +): RuntimeException(msg) + +class TerminalClient( + private val config: TalerTerminalConfig +) { + + private val client: OkHttpClient = + OkHttpClient.Builder() + .addInterceptor(C2ECBasicAuthInterceptor(config)) + .build() + + private fun baseUrlBuilder() = HttpUrl.Builder() + .encodedPath(config.terminalApiBaseUrl) + + private fun exchangeConfigUrl() = baseUrlBuilder() + .addPathSegment("config") + .build() + + private fun terminalsConfigUrl() = baseUrlBuilder() + .addPathSegment("config") + .build() + + private fun setupWithdrawalUrl() = baseUrlBuilder() + .addPathSegment("withdrawals") + .build() + + private fun withdrawalByWopid(encodedWopid: String) = baseUrlBuilder() + .addPathSegment("withdrawals") + .addPathSegment(encodedWopid) + .build() + + private fun <T> serializer(clazz: Class<T>) = Moshi.Builder().build().adapter(clazz) + + private fun withdrawalConfirm(encodedWopid: String) = withdrawalByWopid(encodedWopid) + .newBuilder() + .addPathSegment("check") + .build() + private fun withdrawalAbort(encodedWopid: String) = withdrawalByWopid(encodedWopid) + .newBuilder() + .addPathSegment("abort") + .build() + + fun retrieveTerminalApiConfig(): Optional<TerminalApiConfig> { + + val req = Request.Builder() + .get() + .url(terminalsConfigUrl()) + .build() + val response = client.newCall(req).execute() + return parseOrEmpty(response) + } + + fun setupWithdrawal(setupReq: TerminalWithdrawalSetup): Optional<TerminalWithdrawalSetupResponse> { + + val reqBody = RequestBody.create( + "application/json".toMediaType(), + serializer(TerminalWithdrawalSetup::class.java).toJson(setupReq) + ) + val req = Request.Builder() + .post(reqBody) + .url(setupWithdrawalUrl()) + .build() + val response = client.newCall(req).execute() + return parseOrEmpty(response) + } + + fun retrieveWithdrawalStatus( + wopid: String, + longPollMs: Int, + oldState: WithdrawalOperationStatus = WithdrawalOperationStatus.PENDING + ): Optional<TerminalWithdrawalSetup> { + + 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 sendConfirmationRequest(confirmationRequest: TerminalWithdrawalConfirmationRequest) { + + 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 TerminalClientException(response.code, "request unsuccessful") + } + + private class C2ECBasicAuthInterceptor( + private val config: TalerTerminalConfig + ) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + + val base64EncodedCredentials = java.util.Base64 + .getUrlEncoder() + .encode("${config.username}:${config.accessToken}".toByteArray()) + .toString() + + return chain.proceed( + chain.request().newBuilder() + .header("Authorization", base64EncodedCredentials) + .build() + ) + } + } +} + +private fun RequestBody.Companion.create(toMediaType: MediaType): Any { + TODO("Not yet implemented") +} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerBankIntegrationConfig.kt @@ -1,10 +0,0 @@ -package ch.bfh.habej2.wallee_c2ec.client.taler.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/client/taler/config/TalerTerminalConfig.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/config/TalerTerminalConfig.kt @@ -0,0 +1,8 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler.config + +data class TalerTerminalConfig( + val displayName: String, + val terminalApiBaseUrl: String, + val username: String, + val accessToken: String +) +\ 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 @@ -1,20 +0,0 @@ -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 @@ -1,4 +0,0 @@ -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/TerminalWithdrawalSetup.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/model/TerminalWithdrawalSetup.kt @@ -0,0 +1,24 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler.model + +import ch.bfh.habej2.wallee_c2ec.withdrawal.Amount + +data class TerminalApiConfig( + val name: String, + val version: String, + val providerName: String, + val wireType: String +) + +data class TerminalWithdrawalSetup( + val requestUid: String, + val amount: Amount +) + +data class TerminalWithdrawalSetupResponse( + val withdrawalId: String +) + +data class TerminalWithdrawalConfirmationRequest( + val providerTransactionId: String, + val terminalFees: Amount +) 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 @@ -1,3 +0,0 @@ -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 @@ -1,9 +1,29 @@ package ch.bfh.habej2.wallee_c2ec.client.taler.model +import ch.bfh.habej2.wallee_c2ec.withdrawal.Amount + enum class WithdrawalOperationStatus(val value: String) { PENDING("pending"), SELECTED("selected"), CONFIRMED("confirmed"), ABORTED("aborted") -} -\ No newline at end of file +} + +data class BankWitdrawalOperationStatus( + val status: WithdrawalOperationStatus, + val amount: Amount, + val suggestedAmount: Amount, + val maxAmount: Amount, + val cardFees: Amount, + val senderWire: String, + val suggestedExchange: String, + val requiredExchange: String, + val confirmTransferUrl: String, + val wireTypes: Array<String>, + val selectedReservePub: String, + val selectedExchangeAccount: String, + val aborted: Boolean, + val selectionDone: Boolean, + val transferDone: Boolean +) +\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt @@ -7,7 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext -import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig @Composable fun ExchangeSelectionScreen( @@ -29,7 +29,7 @@ fun ExchangeSelectionScreen( val ctx = LocalContext.current Button(onClick = { - model.exchangeUpdated(TalerBankIntegrationConfig("","","","")) + model.exchangeUpdated(TalerTerminalConfig("","","","")) onNavigateToWithdrawal() }) { Text(text = "withdraw") diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt @@ -19,6 +19,8 @@ fun RegisterWithdrawalScreen( val uiState by model.uiState.collectAsState() val activity = (LocalContext.current as Activity) + model.setupWithdrawal() + model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { activity.finish() } diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt @@ -1,31 +1,13 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal -import android.app.Activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.input.KeyboardType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig import ch.bfh.habej2.wallee_c2ec.client.wallee.WalleeResponseHandler 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 class WithdrawalActivity : ComponentActivity() { diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt @@ -1,15 +1,18 @@ package ch.bfh.habej2.wallee_c2ec.withdrawal import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState 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.config.TalerBankIntegrationConfig -import ch.bfh.habej2.wallee_c2ec.client.taler.model.PaymentNotification +import ch.bfh.habej2.wallee_c2ec.client.taler.TerminalClient +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerTerminalConfig import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.Base32Encode +import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalConfirmationRequest +import ch.bfh.habej2.wallee_c2ec.client.taler.model.TerminalWithdrawalSetup import com.wallee.android.till.sdk.data.TransactionCompletionResponse import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -18,6 +21,7 @@ import java.io.Closeable import java.math.BigDecimal import java.security.SecureRandom import java.util.Optional +import java.util.UUID data class Amount( val value: Int, @@ -28,6 +32,7 @@ data class Amount( @Stable interface WithdrawalOperationState{ + val requestUid: String val exchangeBankIntegrationApiUrl: String val encodedWopid: String val amount: Amount @@ -37,6 +42,7 @@ interface WithdrawalOperationState{ } private class MutableWithdrawalOperationState: WithdrawalOperationState { + override val requestUid: String by derivedStateOf{UUID.randomUUID().toString()} override var exchangeBankIntegrationApiUrl: String by mutableStateOf("") override var encodedWopid: String by mutableStateOf("") override var amount: Amount by mutableStateOf(Amount(0,0)) @@ -49,22 +55,29 @@ class WithdrawalViewModel( vararg closeables: Closeable ) : ViewModel(*closeables) { - private var bankIntegrationClient: BankIntegrationClient? = null + private var terminalClient: TerminalClient? = null private val _uiState = MutableStateFlow(MutableWithdrawalOperationState()) val uiState: StateFlow<WithdrawalOperationState> = _uiState - init { - initializeWopid() - } - - fun exchangeUpdated(cfg: TalerBankIntegrationConfig) { - bankIntegrationClient = BankIntegrationClient(cfg) + fun exchangeUpdated(cfg: TalerTerminalConfig) { + terminalClient = TerminalClient(cfg) _uiState.value = MutableWithdrawalOperationState() // reset withdrawal operation - initializeWopid() // initialize new withdrawal operation identifier + updateCurrency("CHF") } - fun initializeWopid() { - _uiState.value.encodedWopid = Base32Encode(wopid()) + fun setupWithdrawal() { + + val setupReq = TerminalWithdrawalSetup( + _uiState.value.requestUid, + _uiState.value.amount + ) + + val res = terminalClient!!.setupWithdrawal(setupReq) + if (!res.isPresent) { + withdrawalOperationFailed() + } + + _uiState.value.encodedWopid = res.get().withdrawalId } fun updateAmount(amount: String) { @@ -100,23 +113,16 @@ class WithdrawalViewModel( fun withdrawalOperationFailed() { viewModelScope.launch { - bankIntegrationClient!!.abortWithdrawal(uiState.value.encodedWopid) + terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) } } fun confirmPayment() { viewModelScope.launch{ - bankIntegrationClient!!.sendPaymentNotification(PaymentNotification()) + terminalClient!!.sendConfirmationRequest(TerminalWithdrawalConfirmationRequest("", Amount(0,0))) } } - private fun wopid(): ByteArray { - val wopid = ByteArray(32) - val rand = SecureRandom() - rand.nextBytes(wopid) // will seed automatically - return wopid - } - /** * Format expected X[.X], X an integer */