summaryrefslogtreecommitdiff
path: root/taler-wallet.rst
diff options
context:
space:
mode:
Diffstat (limited to 'taler-wallet.rst')
-rw-r--r--taler-wallet.rst656
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?