diff options
Diffstat (limited to 'taler-wallet.rst')
-rw-r--r-- | taler-wallet.rst | 656 |
1 files changed, 502 insertions, 154 deletions
diff --git a/taler-wallet.rst b/taler-wallet.rst index 1889c585..35210cb5 100644 --- a/taler-wallet.rst +++ b/taler-wallet.rst @@ -52,6 +52,11 @@ To use the wallet as a library in your own project, run: $ npm install taler-wallet +Manual withdrawing +================== + +taler-wallet-cli advanced withdraw-manually --exchange https://exchange.eurint.taler.net/ --amount EUR:5 + WebExtension Wallet =================== @@ -71,218 +76,561 @@ Building from source Android Wallet ============== -*TBD.* +Please see :ref:`Build-apps-from-source` in the :doc:`developers-manual`. APIs and Data Formats ===================== -This section describes the wallet backend API. The goal of this API is to -be easy to consume without having to implement Taler-specific data formats -or algorithms. This is why some redundancy is present, for example in -how amounts are returned. +.. warning:: -Balance -------- + These APIs are still a work in progress and *not* final. -The balance is computed with different types of "slicing": +Balances +-------- -* ``byExchange``: Balance by the exchange it was withdrawn from -* ``byAuditor``: Balance by each auditor trusted by the wallet -* ``byCurrency``: Balance by currency +Balances are the amounts of digital cash held by the wallet. -Each balance entry contains the following information: +:name: ``"getBalances"`` +:description: Get a list of balances per currency. +:response: + .. ts:def:: BalancesResponse -* ``amountCurrent``: Amount available to spend **right now** -* ``amountAvailable``: Amount available to spend after refresh and withdrawal - operations have completed. -* ``amountPendingOutgoing``: Amount that is allocated to be spent on a payment - but hasn't been spent yet. -* ``amountPendingIncoming``: Amount that will be available but is not available yet - (from refreshing and withdrawal) -* ``amountPendingIncomingWithdrawal``: Amount that will be available from pending withdrawals -* ``amountPendingIncomingRefresh``: Amount that will be available from pending refreshes + interface BalancesResponse { + // a list of balances sorted by currency. + // (currencies with shorter names first, then lexically ascending). + // + // Note: Even when a currency has no balance, but pending or past transactions, + // it should be included in this list with a balance of zero. + balances: Balance[]; + } + .. ts:def:: Balance -History -------- + interface Balance { + currency?: string; -All events contain a ``type``, a ``timestamp`` and a ``eventUID``. When -querying the event history, a level can be specified. Only events with a -verbosity level ``<=`` the queried level are returned. + // The total Amount that is currently available to be spent + // including amounts tied up in ongoing refresh operations. These are hidden from the user. + // If the user tries to spend coins locked up this way, + // the wallet will give an error message different from "insufficient balance". + available: Amount; -The following event types are available: + // the total incoming amount that will be added to the available balance + // when all pending transactions succeed (including internal refreshes) + pendingIncoming: Amount; -``exchange-added`` - Emitted when an exchange has ben added to the wallet. + // the total outgoing amount that will be subtracted from the available balance + // when all pending transactions succeed (including internal refreshes) + pendingOutgoing: Amount; -``exchange-update-started`` - Emitted when updating an exchange has started. + // true if the balance has pending transactions + hasPendingTransactions: boolean; -``exchange-update-finished`` - Emitted when updating an exchange has started. + // true if the balance requires user-interaction, e.g. accepting a tip + // (DEV: can be left out of a first implementation) + requiresUserInput: boolean; + } -``reserve-created`` (Level 1) - A reserve has been created. Contains the following detail fields: +Transactions +------------ - * ``exchangeBaseUrl`` - * ``reservePub``: Public key of the reserve - * ``expectedAmount``: Amount that is expected to be in the reserve. - * ``reserveType``: How was the reserve created? Can be ``taler-withdraw`` when - created by dereferencing a ``taler://pay`` URI or ``manual`` when the reserve - has been created manually. +Transactions are all operations or events that are affecting the balance. -``reserve-bank-confirmed`` (Level 1) - Only applies to reserves with ``reserveType`` of ``taler-withdraw``. - This event is emitted when the wallet has successfully sent the details about the - withdrawal (reserve key, selected exchange). +:Name: ``"getTransactions"`` +:Description: Get a list of past and pending transactions. +:Request: + .. ts:def:: TransactionsRequest -``reserve-exchange-confirmed`` (Level 0) - This event is emitted the first time that the exchange returns a success result - for querying the status of the resere. + interface TransactionsRequest { + // return only transactions in the given currency, if present + currency?: string; - * ``exchangeBaseUrl`` - * ``reservePub``: Public key of the reserve - * ``currentAmount``: Amount that is expected to be in the reserve. + // if present, results will be limited to transactions related to the given search string + search?: string; + } +:Response: + .. ts:def:: TransactionsResponse -``reserve-exchange-updated`` (Level 0) - Emitted when a reserve has been updated **and** the remaining amount has changed. + interface TransactionsResponse { + // a list of past and pending transactions sorted by pending, timestamp and transactionId. + // In case two events are both pending and have the same timestamp, + // they are sorted by the transactionId + // (i.e. pending before non-pending transactions, newer before older + // and if all equal transactionId lexically ascending). + transactions: Transaction[]; + } -``withdraw-started`` (Level 1) - Emitted when the wallet starts a withdrawal from a reserve. Contains the following detail fields: + .. ts:def:: Transaction - * ``withdrawReason``: Why was the withdraw started? Can be ``initial`` (first withdrawal to drain a - reserve), ``repeat`` (withdrawing from a manually topped-up reserve) or ``tip`` - * ``withdrawRawAmount``: Amount that is subtracted from the reserve, includes fees. - * ``withdrawEffectiveAmount``: Amount that will be added to the balance. + interface Transaction { + // opaque unique ID for the transaction, used as a starting point for paginating queries + // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) + transactionId: string; -``withdraw-coin-finished`` (Level 2) - An individual coin has been successfully withdrawn. - -``withdraw-finished`` (Level 0) - Withdraw was successful. Details: + // the type of the transaction; different types might provide additional information + type: TransactionType; + + // main timestamp of the transaction + timestamp: Timestamp; + + // true if the transaction is still pending, false otherwise + // If a transaction is not longer pending, its timestamp will be updated, + // but its transactionId will remain unchanged + pending: boolean; + + // if present, the transaction encountered a fatal error that needs to be shown to the user + error?: TransactionError; + + // Raw amount of the transaction (exclusive of fees or other extra costs) + amountRaw: Amount; + + // Amount added or removed from the wallet's balance (including all fees and other costs) + amountEffective: Amount; + } + + .. ts:def:: TransactionType + + type TransactionType = ( + TransactionWithdrawal | + TransactionPayment | + TransactionRefund | + TransactionTip | + TransactionRefresh + ) + + .. ts:def:: TransactionError + + interface TransactionError { + // TALER_EC_* unique error code. + // The action(s) offered and message displayed on the transaction item depend on this code. + ec: number; + + // English-only error hint, if available. + hint?: string; + + // Error details specific to "ec", if applicable/available + details?: any; + } + + .. ts:def:: TransactionWithdrawal + + // This should only be used for actual withdrawals + // and not for tips that have their own transactions type. + interface TransactionWithdrawal extends Transaction { + type: string = "withdrawal", + + // Exchange that was withdrawn from. + exchangeBaseUrl: string; + + // true if the bank has confirmed the withdrawal, false if not. + // An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI. + // See also bankConfirmationUrl below. + confirmed: boolean; + + // If the withdrawal is unconfirmed, this can include a URL for user initiated confirmation. + bankConfirmationUrl?: string; + + // Amount that has been subtracted from the reserve's balance for this withdrawal. + amountRaw: Amount; + + // Amount that actually was (or will be) added to the wallet's balance. + // Should always be shown as a positive amount. + amountEffective: Amount; + } + + .. ts:def:: TransactionPayment + + interface TransactionPayment extends Transaction { + type: string = "payment", + + // Additional information about the payment. + info: TransactionInfo; + + // The current status of this payment. + status: PaymentStatus; + + // Amount that must be paid for the contract + amountRaw: Amount; - * ``withdrawRawAmount``: Amount that is subtracted from the reserve, includes fees. - * ``withdrawEffectiveAmount``: Amount that will be added to the balance. + // Amount that was paid, including deposit, wire and refresh fees. + // Should always be shown as a negative amount. + amountEffective: Amount; + } -``order-offered`` (Level 1) - A merchant has offered the wallet to download an order. + .. ts:def:: TransactionInfo -``order-claimed`` (Level 1) - The wallet has downloaded and claimed an order. + interface TransactionInfo { + // Order ID, uniquely identifies the order within a merchant instance + orderId: string; -``order-pay-confirmed`` (Level 0) - The wallet's user(-agent) has confirmed that a payment should - be made for this order. + // More information about the merchant + merchant: Merchant; -``pay-coin-finished`` (Level 2) - A coin has been sent successfully to the merchant. + // Summary of the order, given by the merchant + summary: string; -``pay-finished`` (Level 0) - An order has been paid for successfully for the first time. - This event is not emitted for payment re-playing. + // Map from IETF BCP 47 language tags to localized summaries + summary_i18n?: { [lang_tag: string]: string }; -``refresh-started`` (Level 1) - A refresh session (one or more coins) has been started. Details: + // List of products that are part of the order + products: Product[]; - * ``refreshReason``: One of ``forced``, ``pay`` or ``refund``. + // URL of the fulfillment, given by the merchant + fulfillmentUrl: string; + } -``refresh-coin-finished`` (Level 2) - Refreshing a single coin has succeeded. + .. ts:def:: PaymentStatus -``refresh-finished`` (Level 0) - A refresh session has succeeded. + enum PaymentStatus { + // Explicitly aborted after timeout / failure + Aborted = "aborted", -``tip-offered`` (Level 1) - A tip has been offered, but not accepted yet. + // Payment failed, wallet will auto-retry. + // User should be given the option to retry now / abort. + Failed = "failed", -``tip-accepted`` (Level 1) - A tip has been accepted. Together with this event, - a corresponding ``withdraw-started`` event is also emitted. + // Paid successfully + Paid = "paid", -``refund`` (Level 0) - The wallet has been notified about a refund. A corresponding - ``refresh-started`` event with ``refreshReason`` set to ``refund`` - will be emitted as well. + // Only offered, user must accept / decline + Offered = "offered", + // User accepted, payment is processing. + Accepted = "accepted", + } -Pending Operations ------------------- + .. ts:def:: TransactionRefund + interface TransactionRefund extends Transaction { + type: string = "refund", -``exchange-update``: - Shown when exchange information (``/keys`` and ``/wire``) is being updated. + // ID for the transaction that is refunded + refundedTransactionId: string; -``reserve``: - Shown when a reserve has been created (manually or via dereferencing a ``taler://withdraw`` URI), - but the reserve has not been confirmed yet. + // Additional information about the refunded payment + info: TransactionInfo; - Details: + // Part of the refund that couldn't be applied because the refund permissions were expired + amountInvalid: Amount; - * ``reserveType``: How was the reserve created? Can be ``taler-withdraw`` when - created by dereferencing a ``taler://pay`` URI or ``manual`` when the reserve - has been created manually. - * ``expectedAmount``: Amount we expect to be in the reserve. - * ``status``: Either ``new`` or ``confirmed-bank``. - * ``lastError``: If present, contains the last error pertaining to the reserve, - either from the bank or from the exchange. + // Amount that has been refunded by the merchant + amountRaw: Amount; - **Rendering**: The pending operation is rendered as "waiting for money transfer". + // Amount will be added to the wallet's balance after fees and refreshing. + // Should always be shown as a positive amount. + amountEffective: Amount; + } -``withdrawal`` - Shown when a withdrawal is in progress (either from a reserve in the wallet or - from tipping). + .. ts:def:: TransactionTip - Details: + interface TransactionTip extends Transaction { + type: string = "tip", - * ``exchangeBaseUrl`` - * ``coinsPending`` - * ``coinsWithdrawn`` - * ``amountWithdrawn`` - * ``amountPending`` - * ``totalWithdrawnAmount``: Amount actually subtracted from the reserve, including fees - * ``totalEffectiveAmount``: Amount that will be added to the balance - * ``lastErrors``: If present, contains the last error for every coin that is - part of this withdrawal operation. + // The current status of this tip. + status: TipStatus; - **Rendering**: The pending operation is rendered as "withdrawing digital cash". + // Exchange that the tip will be (or was) withdrawn from + exchangeBaseUrl: string; -``pay`` - Shown when a payment is in progress. + // More information about the merchant that sent the tip + merchant: Merchant; - Details: + // Raw amount of the tip, without extra fees that apply + amountRaw: Amount; - * ``amountPrice``: Price of the order that is being purchased - * ``coinsPaid``: Number of coins successfully submitted as payment. - * ``coinsPending``: Number of coins successfully submitted as payment. - * ``amountEffectivePrice``: Effective price, including fees for refreshing *and* - coins that are too small to refresh. - * ``lastErrors``: If present, contains the last error for every coin that is - part of this pay operation. + // Amount will be (or was) added to the wallet's balance after fees and refreshing. + // Should always be shown as a positive amount. + amountEffective: Amount; + } - **Rendering**: The pending operation is rendered as "paying". + .. ts:def:: TipStatus -``refresh`` - Shown when a refresh is in progress, either one that's manually forced, one - after payment, or one after a refund. + enum TipStatus { + // Only offered, user must accept / decline + Offered = "offered", - Details: + // User accepted, tip is processing. + Accepted = "accepted", - * ``refreshReason``: One of ``forced``, ``pay`` or ``refund`` - * ``totalRefreshedAmount``: Amount that has been successfully refreshed - as part of this session - * ``coinsPending``: Number of coins that are part of the refresh operation, but - haven't been processed yet. - * ``coinsMelted``: Number of coins that have been successfully melted. - * ``coinsRefreshed``: Number of coins that have been successfully refreshed. - * ``lastErrors``: If present, contains the last error for every coin that is - part of this refresh operation. + // User declined. + Declined = "declined", - **Rendering**: The pending operation is rendered as "fetching change", optionally - with "(after manual request)", "(after payment") or "(after refund)". + // Received successfully + Received = "received", + } -``refund`` - Shown when a merchant's refund permission is handed to the exchange. + .. ts:def:: TransactionRefresh + + // A transaction shown for refreshes that are not associated to other transactions + // such as a refresh necessary before coin expiration. + // It should only be returned by the API if the effective amount is different from zero. + interface TransactionRefresh extends Transaction { + type: string = "refresh", + + // Exchange that the coins are refreshed with + exchangeBaseUrl: string; + + // Raw amount that is refreshed + amountRaw: Amount; + + // Amount that will be paid as fees for the refresh. + // Should always be shown as a negative amount. + amountEffective: Amount; + } + +Refunds +------- + +:Name: ``"applyRefund"`` +:Description: Process a refund from a ``taler://refund`` URI. +:Request: + .. ts:def:: WalletApplyRefundRequest + + interface WalletApplyRefundRequest { + talerRefundUri: string; + } +:Response: + .. ts:def:: WalletApplyRefundResponse + + interface WalletApplyRefundResponse { + // Identifier for the purchase that was refunded + contractTermsHash: string; + } + +Exchange Management: List Exchanges +----------------------------------- + +:Name: ``"listExchanges"`` +:Description: + List all exchanges. +:CLI: + ``taler-wallet-cli exchanges list`` +:Response: + .. ts:def:: ExchangesListRespose + + interface ExchangesListRespose { + exchanges: ExchangeListItem[]; + } + + .. ts:def:: ExchangeListItem + + interface ExchangeListItem { + exchangeBaseUrl: string; + currency: string; + paytoUris: string[]; + } + +Exchange Management: Add Exchange +--------------------------------- + +:Name: ``"addExchange"`` +:Description: + Add an exchange. +:CLI: + ``taler-wallet-cli exchanges add $URL`` +:Request: + .. ts:def:: ExchangeAddRequest + + interface ExchangeAddRequest { + exchangeBaseUrl: string; + } +:Response: + On success, the response is an empty object. + +Exchange Management: Get Terms of Service +----------------------------------------- + +:Name: ``"getExchangeTos"`` +:Description: + Get the exchange's current ToS and which version of the ToS (if any) + the user has accepted. +:CLI: + ``taler-wallet-cli exchanges tos $URL`` +:Request: + .. ts:def:: ExchangeGetTosRequest + + interface ExchangeGetTosRequest { + exchangeBaseUrl: string; + } +:Response: + .. ts:def:: ExchangeGetTosResult + + export interface GetExchangeTosResult { + // Markdown version of the current ToS. + tos: string; + + // Version tag of the current ToS. + currentEtag: string; + + // Version tag of the last ToS that the user has accepted, + // if any. + acceptedEtag: string | undefined; + } + +Exchange Management: Set Accepted Terms of Service Version +---------------------------------------------------------- + +:Name: ``"setExchangeTosAccepted"`` +:Description: + Store that the user has accepted a version of the exchange's ToS. +:CLI: + ``taler-wallet-cli exchanges accept-tos $URL $ETAG`` +:Request: + .. ts:def:: ExchangeSetTosAccepted + + interface ExchangeGetTosRequest { + exchangeBaseUrl: string; + acceptedEtag: string; + } +:Response: + On success, the response is an empty object. + +Withdrawal: Get Manual Withdrawal Info +-------------------------------------- + +:Name: ``"getWithdrawalDetailsForAmount"`` +:Description: + Get information about fees and exchange for a manual withdrawal of a given amount. +:CLI: + ``taler-wallet-cli advanced manual-withdrawal-details $URL $AMOUNT`` +:Request: + .. ts:def:: GetManualWithdrawalDetailsRequest + + interface ExchangeAddRequest { + exchangeBaseUrl: string; + amount: string; + } +:Response: + .. ts:def:: ManualWithdrawalDetails + + export interface ManualWithdrawalDetails { + // Did the user accept the current version of the exchange's + // terms of service? + tosAccepted: boolean; + + // Amount that the user will transfer to the exchange. + rawAmount: AmountString; + + // Amount that will be added to the user's wallet balance. + effectiveAmount: AmountString; + + // Ways to pay the exchange. + paytoUris: string[]; + } + +Integration Tests +================= + +Integration Test Example +------------------------ + +Integration tests can be done with the low-level wallet commands. To select which coins and denominations +to use, the wallet can dump the coins in an easy-to-process format (`CoinDumpJson <https://git.taler.net/wallet-core.git/tree/src/types/talerTypes.ts#n734>`__). + +The database file for the wallet can be selected with the ``--wallet-db`` +option. This option must be passed to the ``taler-wallet-cli`` command and not +the subcommands. If the database file doesn't exist, it will be created. + +The following example does a simple withdrawal recoup: + +.. code-block:: sh + + # Withdraw digital cash + $ taler-wallet-cli --wallet-db=mydb.json testing withdraw \ + -b https://bank.int.taler.net/ \ + -e https://exchange.int.taler.net/ \ + -a INTKUDOS:10 + + $ coins=$(taler-wallet-cli --wallet-db=mydb.json advanced dump-coins) + + # Find coin we want to revoke + $ rc=$(echo "$coins" | jq -r '[.coins[] | select((.denom_value == "INTKUDOS:5"))][0] | .coin_pub') + # Find the denom + $ rd=$(echo "$coins" | jq -r '[.coins[] | select((.denom_value == "INTKUDOS:5"))][0] | .denom_pub_hash') + # Find all other coins, which will be suspended + $ susp=$(echo "$coins" | jq --arg rc "$rc" '[.coins[] | select(.coin_pub != $rc) | .coin_pub]') + + # The exchange revokes the denom + $ taler-exchange-keyup -r $rd + $ taler-deployment-restart + + # Now we suspend the other coins, so later we will pay with the recouped coin + $ taler-wallet-cli --wallet-db=mydb.json advanced suspend-coins "$susp" + + # Update exchange /keys so recoup gets scheduled + $ taler-wallet-cli --wallet-db=mydb.json exchanges update -f https://exchange.int.taler.net/ + + # Block until scheduled operations are done + $ taler-wallet-cli --wallet-db=mydb.json run-until-done + + # Now we buy something, only the coins resulting from recouped will be + # used, as other ones are suspended + $ taler-wallet-cli --wallet-db=mydb.json testing test-pay -m https://backend.int.taler.net/ -k sandbox -a "INTKUDOS:1" -s "foo" + $ taler-wallet-cli --wallet-db=mydb.json run-until-done + + +To test refreshing, force a refresh: + +.. code-block:: sh + + $ taler-wallet-cli --wallet-db=mydb.json advanced force-refresh "$coin_pub" + + +To test zombie coins, use the timetravel option. It **must** be passed to the +top-level command and not the subcommand: + +.. code-block:: sh -``tip`` - Shown when a tip is being picked up from the merchant + # Update exchange /keys with time travel, value in microseconds + $ taler-wallet-cli --timetravel=1000000 --wallet-db=mydb.json exchanges update -f https://exchange.int.taler.net/ + +Test Cases +---------- + +Things we already have tests for: + +* Can the wallet recoup coins and spend them? + [`link <https://git.taler.net/wallet-core.git/tree/integrationtests/test-recoup.sh>`__] + +Things we still need tests for: + +* Does the wallet do retries correctly when the exchange is not reachable? + Or when the merchant is not reachable? Or the bank? + This can be tested by temporarily killing those services. +* How does the wallet deal with processing the same ``taler://(pay|withdraw)`` URI twice? +* Test tipping (accepting/refusing a tip) +* Test refunds +* Test for :ref:`session-based payments <repurchase>` +* Test case for auto-refunds + (scenario where the vending machine finds out that its motor is broken, + so it automatically gives a refund) +* Does the wallet report "insufficient balance" correctly + (as opposed to, say, crashing)? +* Perf tests: How does the wallet handle withdrawing a *LOT* of coins? +* Are the transaction history and pending operations reported correctly? + +Tests for things the wallet doesn't handle correctly yet: + +* What happens if the wallet double-spends a coin? + (Easy to test by copying the wallet DB before spending + and then running a spend again with the old DB). +* What happens when a reserve is changed between accepting withdrawal + and actually withdrawing coins? + (This is harder to test. Might not be possible with the current CLI. + The idea would be be to have some ``--inhibit=withdraw`` flag + that tells the wallet to not actually withdraw, + so we can change the reserve state and then resume the wallet.) +* What happens if the exchange suddenly has a completely new list of denominations on offer? +* What happens if the exchange changes its master public key? + The wallet *should* handle this gracefully + even if we have coins with that exchange, + provided that the old denominations can be recouped. + (That one is pretty difficult!) +* Does the wallet handle :ref:`payment aborts <order-abort>` correctly? + +There are test cases that require us to modify the communication between the wallet and exchange. + +* What does the wallet do when the exchange/merchant announce an incompatible protocol version? +* What happens if some signature made by the exchange/merchant is garbage? +* What if the exchange reports a double-spend and the proof it gives us is invalid? |