summaryrefslogtreecommitdiff
path: root/design-documents/037-wallet-transactions-lifecycle.rst
diff options
context:
space:
mode:
Diffstat (limited to 'design-documents/037-wallet-transactions-lifecycle.rst')
-rw-r--r--design-documents/037-wallet-transactions-lifecycle.rst985
1 files changed, 731 insertions, 254 deletions
diff --git a/design-documents/037-wallet-transactions-lifecycle.rst b/design-documents/037-wallet-transactions-lifecycle.rst
index 7eace858..535e4ff8 100644
--- a/design-documents/037-wallet-transactions-lifecycle.rst
+++ b/design-documents/037-wallet-transactions-lifecycle.rst
@@ -1,6 +1,9 @@
DD 37: Wallet Transaction Lifecycle
###################################
+.. contents:: Table of Contents
+ :depth: 2
+
Summary
=======
@@ -12,54 +15,68 @@ Motivation
The transactions in wallet-core all should have an associated state machine. All transactions
should have some common actions that work uniformly across all transactions.
+Requirements
+============
+
+The underlying state machine should make it obvious what interactions
+are possible for the user. The number of possible user interactions
+in any state should be small.
+
Proposed Solution
=================
+
Common States
-------------
-The following states apply to multiple different transactions. They can
-have transaction-specific sub-states, denoted by ``state(substate)``.
+The following states apply to multiple different transactions. Only pending
+and aborting have transaction-specific sub-states, denoted by ``state(substate)``.
``pending``: A pending transaction waits for some external event/service.
-The transaction stays pending until its change on the wallet's material balance is finished.
+The transaction stays pending until its change on the wallet's material balance
+is finished. Any pending state can be suspended and resumed.
There are some other distinctions for pending transactions:
-* long-polling vs exponential backoff: A pending transaction is either waiting
+* long-polling vs. exponential backoff: A pending transaction is either waiting
on an external service by making a long-polling request or by repeating requests
with exponential back-off.
* ``lastError``: A pending transaction is either clean (i.e. the network interaction
is literally active in transmission or the external service successfully
- communicated that it is not ready yet) or has a ``lastError``, which is a ``TalerErrorDetails``
+ communicated that it is not ready yet and this is perfectly normal)
+ or has a ``lastError``, which is a ``TalerErrorDetails``
object with details about what happened during the last attempt to proceed
with the transaction.
``done``: A transaction that is done does not require any more processing. It also
never has a ``lastError`` but is considered successful.
+``dialog``: A transaction requires input from the user.
+
``aborting``: Similar to a pending transaction, but instead of taking active steps to
complete the transaction, the wallet is taking active steps to abort it. The ``lastError``
indicates errors the wallet experienced while taking active steps to abort the transaction.
-.. attention::
- Should there be an abortReason for aborted transactions?
-
- sebasjm: yes, although it doesn't mean that the user need to add this information
- manually. The information we save in the abort operation can help the user to know
- how and when the operation was aborted.
-
-``aborted``: Similar to a ``done`` transaction, but the transaction was successfully aborted
-instead of successfully finished.
+``aborted``: Similar to ``done``, but the transaction was successfully aborted
+instead of successfully finished. It will have the information of when (timestamp) it was
+aborted and in which pending sub-state the abort action was initiated. Also, we can
+include more information information relevant to the transaction in ``abortReason``
``suspended``: Similar to a ``aborted`` transaction, but the transaction was could be
resumed and may then still succeed.
-``failed``: Similar to ``done``, but the transaction could not even be aborted successfully.
+``suspended-aborting``: Network requests or other expensive work
+to abort a transaction is paused.
+
+``failed``: Similar to ``done``, but the transaction could not be completed or
+possibly not even be aborted properly. The user may have lost money. In some
+cases, a report to the auditor would make sense in this state.
+
+``expired``: Similar to ``failed``, but the failure was caused by a timeout.
``deleted``: A ``deleted`` state is always a final state. We only use this
state for illustrative purposes. In the implementation, the data associated
-with the transaction would be deleted.
+with the transaction would be literally deleted.
Common Transitions
@@ -67,12 +84,6 @@ Common Transitions
Transitions are actions or other events.
-``[action:delete]``: Deleting a transaction (also called "forgetting" in the UI)
-completely deletes the transaction in the database. Depending on the type of
-transaction, some of the other data *resulting* from the transaction might
-still survive deletion. For example, deleting a withdrawal transaction does not
-delete already successfully withdrawn coins.
-
``[action:retry]``: Retrying a transaction *(1.)* stops ongoing long-polling
requests for the transaction *(2.)* resets the retry timeout *(3.)* re-runs the
handler to process the transaction. Retries are always possible the following
@@ -82,126 +93,228 @@ states: ``pending(*)`` and ``aborting(*)``.
Should we show the retry timeout in the UI somewhere? Should we show it in dev mode?
- sebasjm: Since the wallet will retry anyway, maybe is better if we replace the "retry"
- button with a "try now" button and a side text "retrying in xxx seconds"
-
-``[action:abort]``: Aborting a transaction either directly stops processing for the
-transaction and puts it in an ``aborted`` state or starts the necessary steps to
-actively abort the transaction (e.g. to avoid losing money) and puts it in an
+ SEBASJM: Since the wallet will retry anyway, maybe is better if we replace the "retry"
+ button with a "try now" button and a side text "retrying in xxx seconds".
+
+ CG: Instead of a side text, this *might* make a good mouse-over hint for
+ a "retry" (or "try now") button. I would not make this overly visible with
+ side-text as the information is not that important. The text should also be
+ "retrying next at XXX" using an absolute time XXX --- otherwise the UI would
+ be way too busy recomputing/updating all of these strings: Using an absolute time,
+ we only have to redraw anything once a retry actually happened. Given that
+ retries should basically never be > 24h (we can impose a hard cap), the absolute
+ time can just be in the format HH:MM:SS (without day).
+
+``[action:suspend]``: Suspends a pending transaction, stopping any associated
+network activities, but with a chance of trying again at a later time. This
+could be useful if a user needs to save battery power or bandwidth and an
+operation is expected to take longer (such as a backup, recovery or very large
+withdrawal operation).
+
+``[action:resume]``: Suspended transactions may be resumed, placing them back
+into a pending state.
+
+``[action:abort]``: Aborting a transaction either directly stops processing for the
+transaction and puts it in an ``aborted`` state, or starts the necessary steps to
+actively abort the transaction (e.g. to avoid losing money) and puts it in an
``aborting`` state.
-``[action:suspend]``: Suspends a pending transaction, stopping any associated network activities, but with a chance of trying
-again at a later time. This could be useful if a user needs to save battery power or bandwidth and an operation is expected
-to take longer (such as a backup, recovery or very large withdrawal operation).
+``[action:fail]``: Directly puts an ``aborting`` or ``pending`` transaction into a
+``failed`` state. May result in an ultimate loss of funds (beyond fees) to the
+user and thus requires additional consent.
-``[action:resume]``: Suspended transactions may be resumed, placing them back into a pending state.
+``[action:delete]``: Deleting a transaction completely deletes the transaction
+from the database. Depending on the type of transaction, some of the other
+data *resulting* from the transaction might still survive deletion. For
+example, deleting a withdrawal transaction does not delete already
+successfully withdrawn coins. Deleting is only safe (no money lost) on initial
+and final states (failed, aborted, done).
-``[action:abort-force]``: Directly puts an ``aborting`` transaction into the ``failed`` state.
+Whether aborting, deleting or suspending are possible depends on
+the transaction type, and usually only one of the four choices should be
+offered.
-Whether aborting or resuming is possible depends on the transaction type, and usually only one
-of the two choices should be offered.
+.. image:: ../images/transaction-common-states.png
-.. image:: ../transaction-common-states.svg
- :width: 400
+Boxed labels indicate an end state in which there is no network activity and
+hence no need to give the user a way to abort or suspend the activity. The
+circle indicates the initial state. Ovals are states with network activity.
-Boxed label means end state, where it is safe to delete the transaction record since no work is due.
+Blue arrows are used for user-triggered actions (via UI buttons). Purple
+arrows are used to indicate externally triggered actions. Black arrows
+without labels are used for the normal successful path. Red arrows indicate
+failure paths.
-Blue arrows means mean user-triggered actions
Common pending sub-states
----------------------------------
+-------------------------
During the pending state the transaction can go through several sub-states before
-reaching a final state. Some of this sub-states are shared between different
+reaching a final state. Some of this sub-states are shared between different
transaction types:
-``kyc-required``: The transaction can't proceed because the user needs to actively
-finish a KYC process. Part of a withdrawal process or peer-to-peer push credit.
-
-``aml-required``: The transaction can't proceed because the user needs to wait for
-the exchange operator to conclude an AML investigation by the staff at the exchange.
-The user is not expected to take any action and should just wait for the investigation
-to conclude. Part of a withdrawal process or peer-to-peer push credit.
+``kyc``: The transaction cannot proceed because the user needs to actively
+finish a KYC process. The wallet should show the user a hint on how to
+start the KYC process.
-``aml-frozen``: The staff at the exchange decided that the account needed to be frozen.
-The user should contact the exchange provider's customer service department and
-seek resolution (possibly through the courts) to avoid loosing the funds for good.
-Part of a withdrawal process or peer-to-peer push credit.
+``aml``: The transaction can't proceed because the user needs to wait for the
+exchange operator to conclude an AML investigation by the staff at the
+exchange. There are two AML substates. In the substate ``pending`` the user
+is not expected to take any action and should just wait for the investigation
+to conclude. In the substate ``frozen`` the staff at the exchange decided that
+the account needed to be frozen. The user should contact the exchange
+provider's customer service department and seek resolution (possibly through
+the courts) to avoid losing the funds for good.
Transaction Type: Withdrawal
----------------------------
-XXX: What if available denominations change? Does this require a user re-approval if fees
-change due to this?
-CG: I think the answer can be "no", for two reasons: the wallet MUST pick denominations
-to withdraw with the "most long-term" withdraw window (i.e. active denominations that have
-the longest available withdraw durations). So in 99.9% of all cases, this will just succeed
-as a sane exchange will have a reasonable duration overlap, and in the 0.1% of cases it's
-really the user's fault for going offline in the middle of the operation. Plus, even in those
-0.1% of cases, it is highly unlikely that the fee would actually change: again 99% of key
-rotations can be expected to be there to rotate the key, and not to adjust the withdraw fee.
-And in the 1:1M case that the fee does *increase*, it's again unlikely to matter much to the
-user. So special-casing this and testing this is IMO just not worth it.
-
* ``pending(bank-register-reserve)``
Initial state for bank-integrated withdrawals. The wallet submits the reserve public key
- and selected exchange to the bank (via the bank integration API).
+ and selected exchange to the bank (via the bank integration API). Note that if the
+ user aborts at this stage, we do not know if the bank is in the confirmation stage,
+ so we must still *try* to abort the transaction at the bank.
+
+ * ``[processed-success] => pending(bank-confirm-transfer)``
+ * ``[processed-error] => failed``: On permanent errors (like 404 for the withdrawal operation),
+ the wallet gives up.
+ * ``[action:abort] => aborting(bank)``
+
+* ``pending(bank-confirm-transfer)``
+
+ The wallet waits until the bank has confirmed the withdrawal operation;
+ usually the user has to complete a 2FA step to *approve* that the money is
+ wired to the chosen exchange. Note that the user's *approve* action is done
+ in the bank's user interface and not the wallet's user interface. The wallet
+ internally merely *polls* for the success or failure of the approve action.
+ The wallet **may** occasionally (after some initial delay, especially on
+ failures from the bank-poll to return any result) long-poll for the reserve
+ status and, if successful, may then directly jump to
+ ``pending(withdraw-coins)`` if the reserve is filled even if the poll at
+ the bank did not return success or failure.
+
+ * ``[bank-poll-success] => pending(exchange-wait-reserve)``
+ * ``[bank-aborted] => aborted``: Bank denied the operation.
+ * ``[exchange-poll-success] => pending(withdraw-coins)``: Optional
+ short-cut transition. Exchange was faster than the bank.
+ * ``[action:abort] => aborting(bank)``
+
+* ``aborting(bank)``
+
+ The user aborted the withdraw operation in the wallet. The wallet must now
+ try to signal the bank that the wire transfer should no longer be performed.
+ Note that it is possible that the bank registration never succeeded (if the
+ user aborted us during ``pending(bank-register-reserve)``) and in this case
+ we get an ``unknown transaction`` failure here. It is also theoretically
+ possible that the user approved the transaction in the bank while
+ simultaneously aborting in the wallet. In this case, we transition to
+ ``suspended(exchange-wait-reserve)`` (treating the ``abort`` action as a ``suspend``
+ action).
- * ``[processed-success] => pending(bank-confirming)``
- * ``[processed-error(bank-aborted)] => aborted(bank-to-wallet)``
+ * ``[processed-success] => aborted``
+ * ``[processed-error(already-confirmed)] => suspended(exchange-wait-reserve)``: We
+ keep a transaction history entry reminding the user about when the already
+ wired funds will be returned.
+ * ``[processed-error(unknown-transaction)] => failed``
-* ``pending(bank-confirming)``
+* ``suspended(exchange-wait-reserve)``
- The wallet waits until the bank has confirmed the withdrawal operation; usually
- the user has to complete a 2FA step to confirm that the money is wired to the chosen
- exchange.
+ State where funds were (presumably) wired to the exchange but the wallet
+ was asked to not proceed with the withdraw, but we still resume.
- * ``[poll-success] => pending(exchange-wait-reserve)``
- * ``[action:abort] => aborting(wallet-to-bank)``
+ In this state, the wallet should show to the user that the money from the
+ withdrawal reserve will be sent back to the originating bank account after
+ ``$closing_delay``. Note that the ``resume`` action should be disabled
+ after ``$closing_delay``.
+
+ * ``[action:delete] => deleted``
+ * ``[action:resume] => pending(exchange-wait-reserve)``
* ``pending(exchange-wait-reserve)``
- Initial state for manual withdrawals.
+ Initial state for manual withdrawals. Here, the wallet long-polls the
+ exchange for the reserve status, waiting for the wire transfer to arrive
+ at the exchange.
- * ``[poll-success] => pending(withdrawing-coins)``
+ * ``[exchange-poll-success] => pending(withdraw-coins)``
+ * ``[action:suspend] => suspended(exchange-wait-reserve)``
-* ``pending(withdrawing-coins)``
+* ``pending(withdraw-coins)``
+
+ State where we are finally withdrawing the actual coins. Depending on
+ the AML and KYC thresholds, we may at any time transition into a
+ holding pattern on the AML or KYC checks of the exchange.
+
+ It is possible that the selected denominations expired.
+ In that case, the wallet will re-select denominations.
* ``[processed-success] => done``
- * ``[processed-kyc-required] => pending(kyc-required)``
+ * ``[processed-kyc-required] => pending(kyc)``
+ * ``[processed-aml-required] => pending(aml)``
+ * ``[reserve-expired] => expired(reserve)``
+ * ``[action:suspend] => suspended(withdraw-coins)``
+
+* ``pending(kyc)``
+
+ State where the user needs to provide some identity data to pass a KYC
+ check. The wallet only shows the user the link for starting the KYC
+ process and long-polls the exchange in anticipation of the user
+ completing the KYC requirement.
-* ``pending(kyc-required)``
+ * ``[poll-success] => pending(withdraw-coins)``
+ * ``[action:suspend] => suspended(kyc)``
- * ``[poll-success] => pending(withdrawing-coins)``
+* ``suspended(kyc)``
+
+ State where the user needs to provide some identity data to pass a KYC
+ check, but the long-polling was explicitly stopped. The user can
+ choose to resume or delete.
+
+ * ``[action:delete] => deleted``
+ * ``[action:resume] => pending(kyc)``
-* ``aborting(wallet-to-bank)``
+* ``pending(aml)``
- * ``[processed-success] => aborted(wallet-to-bank)``
- * ``[processed-error(already-confirmed)] => aborted(after-wired)``
+ State where the wallet needs to wait for completion of an AML process by an
+ AML officer of the exchange. The wallet shows that the AML process is
+ blocking progress. The message shown should distinguish between a mere
+ pending AML process and an AML freezing decision in terms of the message
+ shown to the user. If the AML decision is pending at the exchange, he user
+ should be urged to simply wait. If the funds were frozen, the wallet
+ informs the user that their funds were frozen due to an AML decision. The
+ user is urged to contact the exchange operator's AML department out-of-band.
+ In any case, the wallet long-polls for the AML decision to be made or change
+ (possibly at a lower frequeny in case of a freeze).
-* ``aborted(bank-to-wallet)``: The bank notified the wallet that the withdrawal
- was aborted on the side of the bank and won't proceed.
+ * ``[poll-success] => pending(withdraw-coins)``
+ * ``[action:suspend] => suspended(aml)``
-* ``aborted(wallet-to-bank)``: The wallet notified the bank that the withdrawal
- should be aborted, before any money was wired.
+* ``suspended(aml)``
-* ``aborted(after-wired)``:
+ State where the user needs to await some AML decision by the exchange.
+ The long-polling was explicitly stopped. The user can choose to resume or delete.
- In this state, the wallet should show to the user that the money from the withdrawal
- reserve will be sent back to the originating bank account after ``$closing_delay``.
+ * ``[action:delete] => deleted``
+ * ``[action:resume] => pending(aml)``
-* ``aborted(partially-withdrawn)``:
+* ``suspended(withdraw-coins)``
In this state, the wallet should show how much money arrived into the wallet
- and the rest of the money will be sent back to the originating bank account
- after ``$closing_delay``.
+ and the rest of the money will be sent back to the originating bank account
+ after ``$closing_delay``. Note that the ``resume`` action should be
+ disabled after ``$closing_delay``.
+
+ * ``[action:delete] => deleted``
+ * ``[action:resume] => pending(exchange-wait-reserve)``
* ``done``
+ The withdrawal operation is complete.
+
* ``[action:delete] => deleted``
* ``deleted``
@@ -211,197 +324,305 @@ user. So special-casing this and testing this is IMO just not worth it.
Only once all coins were spent, the withdraw is fully removed.
-.. image:: ../transaction-withdrawal-states.svg
- :width: 800
+.. image:: ../images/transaction-withdrawal-states.png
Transaction Type: Payment to Merchant
-------------------------------------
-XXX: Also consider re-selection when the wallet accidentally double-spends coins
-or the selected coins have expired. Do we ask the user in this case?
-
-CG: I think no. We correct our balance (after all, we got a proof of
-double-spending) and try other coins. If we do not have enough money left, we
-abort and simply inform the user that their balance was insufficient to make
-the payment after all (very sorry...).
-
-Note that the case of selected coins having expired shouldn't really happen,
-as the wallet should have noticed that when is started up, tried to refresh,
-and if that already failed should have update the balance with a transaction
-history entry saying something like "coins expired, offline too long" or
-something like that.
+* ``pending(claim-proposal)``
-* ``pending(download-proposal)``
+ We received a ``pay`` URI. Download (claim) the proposal from the merchant. Can fail if
+ the proposal was already claimed by someone else. If repurchase detection
+ tells us that we already paid for this product, we go immediately to
+ ``failed(repurchase)`` state for this transaction, but with a side-effect of
+ transitioning the UI into a ``pending(repurchase-session-reset)`` on a
+ *different* transaction (which before was in ``done``).
- Initial state. Download (claim) the proposal from the merchant.
+ A ``failed(repurchase)`` transaction will eventually be GCed (=deleted)
+ automatically.
- XXX: Also consider repurchase detection here?
+ * ``[error:already-claimed] => failed(already-claimed)`` -- the proposal was
+ already claimed by someone else.
+ * ``[error:invalid-proposal] => failed(invalid-proposal)`` -- the merchant provided a
+ proposal that is invalid (e.g. malformed contract
+ terms or bad signature).
- CG: Well, we could mention that this is a possible transition from
- ``pending(download-proposal)`` to ``deleted`` with a side-effect
- of transitioning the UI into a ``pending(repurchase-session-reset)``
- on a different transaction (which before was in ``done``).
-
-* ``pending(proposed)``
+* ``dialog(merchant-order-proposed)``
Let the user accept (or refuse) the payment.
* ``[action:pay-accept] => pending(submit-payment)``
- * ``[action:abort] => deleted`` -- user explicitly decides not
- to proceed
- * ``[action:expired] => deleted`` -- when the offer expires
- before the user decides to make the payment! (We can keep
- pending contracts even in a 'pending transaction' list to
- allow the user to choose to not proceed, but then this
- transition would clean up that list).
+ * ``[action:pay-refuse] => ``aborted(refused)`` -- The user explicitly
+ decided not to proceed (at least not with this wallet).
+ * ``[expired] => failed(expired)`` -- The offer has expired before the user made any
+ decision. Note that we should use this transition at
+ least a few seconds before the offer *actually* expires to avoid
+ encountering an expiration during ``pending(submit-payment)`` in most
+ real-world scenarios. Basically, we should prevent last-second payments to
+ be event attempted client-side.
+
+ The ``failed(expired)`` might be automatically deleted upon GC.
* ``pending(submit-payment)``
- * ``[action:abort] => aborting(refund)``
- * ``[processed-success(auto-refund-enabled)] => pending(paid-auto-refund-check)``
- * ``[processed-error(expired)] => aborting(refresh)`` XXX: If the order is expired but the payment
- succeeded partially before, do we still try an abort-refund? CG: YES, but of course
- we probably should use the ``expired`` transition above a few seconds before the
- offer *actually* expires to avoid this problem in 99.9% of real-world scenarios
- ("prevent last-second payments client-side")
+ Submit coin-by-coin (or in bulk groups) until payment is complete.
-* ``pending(submit-payment-replay)``
+ * ``[action:abort] => aborting(pay-incomplete)`` -- The user explicitly decided to
+ abort the process while the payment was happening. Note that if the
+ payment was already completed (and hence the merchant refuses any
+ refunds), it is theoretically possible that pressing the abort button will
+ nevertheless end up in a ``pending(auto-refund)`` state (and subsequently
+ a ``done`` state) instead!
+ * ``[success] => pending(auto-refund)`` -- Upon receiving confirmation from
+ the merchant that the purchase was completed.
+ * ``[error(insufficient balance)] => aborting(pay-incomplete)`` This transition
+ happens if we detect double-spending and our balance is not sufficient
+ after the double-spending. It is also conceivable (but should be rare)
+ that this transition happens because the offer expired.
-* ``pending(paid-auto-refund-check)``
+* ``pending(auto-refund)``
- * ``[auto-refund-timeout] => done``
+ The payment succeed. We remain in this state as long as an auto-refund-check
+ is active. If auto refunds are not enabled, we immediately continue to
+ ``done``.
-* ``pending(paid-check-refund)``
+ * ``[no-auto-refund] => done``
+ * ``[timeout] => done`` -- This happens when the auto refund set by the
+ contract expired.
+ * ``[long-poll:refund] => aborting(pay-incomplete)`` -- An auto-refund was detected.
+ * ``[action:abort] => done`` -- The user may explicitly request to abort the
+ auto-refund processing (for example to enable subsequent deletion before
+ the auto-refund delay expires).
-* ``done``
+* ``aborting(pay-incomplete)``
- * ``[action:check-refund] => pending(paid-check-refund)``
- * ``[action:pay-replay] => pending(submit-payment-replay)``
- * ``[action:delete] => deleted``
+ The wallet should interact with the merchant to request
+ a refund on the incomplete payment.
-* ``aborting(refund)``
+ * ``[success] => aborted(pay-incomplete)``
+ * ``[already-paid] => done``
- * ``[processed-success] => aborted(refunded)``
- * ``[processed-failure] => aborting(refresh)``
+* ``aborted(refunded)``
-* ``aborting(refresh)``
+ The purchase ended with a (partial) refund. The state (and UI) should show
+ the specific provenance of the state, which may include an insufficient
+ balance (due to double-spending being detected during payment), and one or
+ more partial or full refunds.
-* ``failed(invalid-proposal)``
+ * ``[action:delete] => deleted``
- The merchant provided a proposal that is invalid (e.g. malformed contract terms or bad signature).
+* ``done``
-* ``aborted(refunded)``
+ The purchase is completed.
* ``[action:delete] => deleted``
+ * ``[repurchase] => pending(rebind-session)``: Another offer
+ became pending for this product and we need to update the session so
+ that the user does not have to buy it again.
+ * ``[check-refunds]` => pending(check-refunds)``: New refunds
+ might be available for this purchase.
+
+* ``pending(check-refund)``
+
+ New refunds might be available for this purchase.
+ This state must only be entered *after* the payment has successfully
+ completed. It is not relevant for auto-refunds or refunds for incomplete
+ payments.
+
+ * ``[refunds-checked] => pending(user-new-refund)`` --- New
+ refund(s) are available, user needs to confirm.
+ * ``[refunds-checked] => done`` --- Refunds were checked, but no
+ new refunds are available.
+ * ``[action:stop-refund-query] => done`` ---
+ This action would usually only be offered when the state is pending
+ with errors. It stops the refund query, but the payment of course
+ is left intact.
+
+* ``pending(rebind-session)``
+
+ The wallet should reset the associated session for the already purchased
+ (digital) item.
+
+ * ``[success] => done``
+ * ``[action:abort] => done`` -- User aborted the session reset.
* ``deleted``
- When a payment is deleted, associated refunds are always deleted with it
+ When a payment is deleted, associated refund transactions are always deleted
+ with it.
+.. image:: ../images/transaction-payment-states.png
Transaction Type: Refund
------------------------
-A refund is a pseudo-transaction that is always associated with a merchant payment transaction.
-
-* ``pending``
+A refund is a pseudo-transaction that is always associated with a merchant
+payment transaction.
- A refund is pending when the merchant is getting a non-permanent error from
- the exchange (and relaying that error response to the wallet).
+* ``pending(accept)``
- * ``[processed-success] => done``
- * ``[processed-error] => failed``
+ Initial state for a refund.
-* ``done``
+ * ``[processed-error] => failed``: we received a permanent failure (such as money already wired to the merchant)
* ``failed``
- A failed refund can technically still transition to ``done``, because the wallet
- doesn't query some refund resource, but the purchase for refunds. Thus, a previously
- failed refund can suddenly transition to ``done``.
-
- * ``[payment-refund-processed-success] => done``
+ The refund failed permanently.
-* ``*``
+.. image:: ../images/transaction-refund-states.png
- Transitions from any state:
-
- * ``[action:delete] => deleted`` Deleting a refund has no effect on the wallet's balance.
Transaction Type: Refresh
-------------------------
-XXX: If we have to adjust the refund amount (because a coin has fewer funds on
-it than we expect), what is the resulting state of the whole refresh?
+This is about refreshes that are triggered via coin expiration or as part of
+getting change after making a payment. In the first case, the refresh
+transaction is forever shown as a separate transaction in the history unless
+it did not affect the wallet balance (in which case we hide it). In the second
+case, the refresh transaction is folded into the payment transaction upon
+completion, so that the balance changes are included in the fees of the
+transaction that caused us to obtain change.
-CG: first the pending balance is decreased by the reduced amount, and then of
-course the final balance. The coin transaction responsible for the reduction
-in funds is historic (and we don't have details), so that just changes the total
-available balance in the wallet, but without an associated history entry (as we
-cannot give details).
+If we have to adjust the refund amount (because a coin has fewer funds on it
+than we expect) the transaction only shows the changes due to the refresh, and
+we merely adjust the current balance of the wallet but without giving any
+justification (as we cannot give details we do not have). So this will look
+the same as if the double-spending transaction had been deleted by the user.
* ``pending``
+ A refresh operation is pending.
+
* ``[processed-success] => done``
- * ``[action:abort] => aborted``: Money that has not been refreshed yet is lost.
+ * ``[action:suspend] => suspended``
+ * ``[failed] => failed``
+
+* ``suspended``
+
+ A refresh operation was suspended by the user.
+
+ * ``[action:resume] => pending``
* ``done``
-Transaction Type: Tip
----------------------
+ The refresh operation completed.
-* ``pending(initial)``
+ * ``[action:delete] => deleted``
- The wallet has downloaded metadata for the tip from the merchant and
- stored it in the databse. The user needs to accept/refuse it.
+* ``failed``
- * ``[tip-expired] => failed(expired)``
- * ``[action:accept-tip] => pending(pickup)``
- * ``[action:abort] => aborted``
+ The refresh operation failed. The user lost funds.
-* ``pending(pickup)``
+ * ``[action:delete] => deleted``
- * ``[tip-expired] => failed(expired)``
- * ``[processed-success] => done``
- * ``[action:abort] => aborted``
+* ``deleted``
+
+ All memory of the refresh operation is lost, but of course the resulting
+ fresh coins are preserved.
+
+.. image:: ../images/transaction-refresh-states.png
Transaction Type: Deposit
-------------------------
-XXX: Handle expired/invalid coins in the coin selection. Does this require user approval if fees changed?
+* ``pending(deposit)``
-CG: Again, expired coins should never happen. If deposit fees *increase* due
-to a double-spend detection during payment, we might want to have an
-_optional_ dialog ("Balance reduced by X as wallet state was not up-to-date
-(did you restore from backup?). Consequently, the fees for this transactions
-increased from Y to Z. [Abort] [Continue] + checkbox: [X] Do not ask again."
+ Initial state for deposit transactions.
+ We deposit the amount coin-by-coin (or in bulk groups) until deposit is completed.
-* ``pending(initial)``
+ * ``[action:suspend] => suspended(submit-deposit)``
+ * ``[processed-success] => pending(track)``
+ * ``[processed-failure] => aborting(refund)``
- The wallet deposits coins with the exchange.
+* ``suspended(deposit)``
- * ``[processed-success] => pending(track)``
+ The user suspended our ongoing deposit operation.
+
+ * ``[action:resume] => pending(deposit)``
* ``[action:abort] => aborting(refund)``
* ``pending(track)``
+ All the coins were submitted, waiting to be wired.
+
* ``[poll-success] => done``
+ * ``[poll-accepted-kyc] => pending(kyc)``
+ * ``[poll-accepted-aml] => pending(aml)``
+ * ``[action:abort] => aborting(refund)``
+
+* ``pending(kyc)``
+
+ Exchange requires KYC before making the wire transfer.
+
+ * ``[long-poll:kyc] => done``
+ * ``[action:suspend] => suspended(kyc)``
+
+* ``suspended(kyc)``
+
+ The user suspended us while we were waiting for KYC to be finished.
+
+ * ``[action:resume] => pending(kyc)``
+
+* ``pending(aml)``
+
+ Exchange requires AML before making the wire transfer.
+
+ * ``[long-poll:aml] => done``
+ * ``[action:suspend] => suspended(aml)``
+
+* ``suspended(aml)``
+
+ The user suspended us while we were waiting for AML to be finished.
+
+ * ``[action:resume] => pending(aml)``
* ``aborting(refund)``
- ``[processed-success] => aborting(refresh)``
- ``[processed-error] => aborting(refresh)`` XXX Shouldn't this be some error state?
+ Wallet should try to get the deposited amount back from the exchange (by submitting a refund).
+
+ * ``[action:suspend] => suspended(refund)``
+ * ``[processed-success] => aborting(refresh)``
+ * ``[processed-error] => aborting(refresh)``: Even if the refund attempt failed, maybe the deposit failed as well and we can still succeed with a refresh.
+
+* ``suspended(refund)``
+
+ The user suspended us while we were trying to get a refund.
+
+ * ``[action:resume] => aborting(refund)``
* ``aborting(refresh)``
- ``[processed-success] => aborted``
- ``[processed-error] => failed``
+ * ``[action:suspend] => suspended(refresh)``
+ * ``[processed-success] => aborted``
+ * ``[processed-error] => failed``
+
+* ``suspended(refresh)``
+
+ The user suspended us while we were trying to do the refresh.
+
+ * ``[action:resume] => aborting(refresh)``
+
+* ``aborted``
+
+ The operation was aborted, some funds may have been lost (to fees or deposited anyway).
+
+ * ``[action:delete] => deleted``
* ``done``
+ The deposit operation completed.
+
+ * ``[action:delete] => deleted``
+
+* ``deleted``
+
+ All memory of the deposit operation is lost.
+
+.. image:: ../images/transaction-deposit-states.png
+
Transaction Type: Peer Push Debit
---------------------------------
@@ -411,49 +632,74 @@ to another wallet.
States and transitions:
-* ``pending(initial)``
+* ``pending(purse-create)``
+
+ The wallet is creating a purse. Initial state.
- In this state, the user is not yet able to send the payment to somebody else.
+ * ``[process-success] => pending(ready)``: The wallet has created the purse.
+ * ``[process-failure] => aborting(refund)``: The purse creation failed.
+ * ``[action:suspend] => suspended(purse-create)``: The user suspended the operation.
- * ``[action:abort] => aborted``: The payment is aborted early, before the wallet even had the chance to create a purse.
- No fees are incurred.
- * ``[action:delete] => deleted``: No funds are lost.
- * ``[processsing-success] => pending(purse-created)``: The wallet was able to successfully create a purse.
+* ``suspended(purse-create)``
-* ``pending(purse-created)``
+ * ``[action:resume] => pending(purse-create)``: The user resumed the operation.
+ * ``[action:abort] => aborting(refund)``: The user aborted the operation.
+
+* ``pending(ready)``
In this state, the user can send / show the ``taler://`` URI or QR code to somebody else.
* ``[action:abort] => aborting(delete-purse)``: The user aborts the P2P payment. The wallet tries to reclaim money in the purse.
- * ``[purse-timeout] => aborting(refresh)``: The other party was too slow.
+ * ``[purse-timeout] => aborting(refresh)``: The other party was too slow and the purse has now expired.
* ``[poll-success] => done``: The other party has accepted the payment.
- * ``[poll-error] => aborting(refresh)``: The exchange claims that there is a permanent error regarding the purse.
+ * ``[poll-error] => aborting(refresh)``: The exchange claims that there is a permanent error regarding the purse. (FIXME(CG): not clear that this is the best transition! Could also go to ``aborting(refund)`` or ``aborting(delete-purse)``; best choice may depend on the specific error returned.)
* ``aborting(delete-purse)``
+ The wallet is deleting the purse to prevent the receiver from merging it and to reclaim the funds in it.
+
* ``[processed-success] => aborting(refresh)``: The purse was deleted successfully, and refunded coins must be refreshed.
* ``[processed-failed(already-merged)] => done``: The other party claimed the funds faster that we were able to abort.
* ``[processed-failed(other)] => aborting(refresh)``: The exchange reports a permanent error. We still try to refresh.
+ * ``[action:fail] => failed``: The user explicitly asked us to give up and accepted the possible loss of funds.
+
+* ``aborting(refund)``
- * ``[action:abort-force] => failed``
+ We abandon the purse that was never fully funded and ask for the deposited coins to be refunded.
+
+ * ``[processed-success] => aborting(refresh)``: After the refund, we still need to refresh the coins.
+ * ``[processed-failure] => aborting(refresh)``: The refund failed, we still try to refresh the coins.
+ * ``[action:fail] => failed``: The user explicitly asked us to give up and accepted the possible loss of funds.
* ``aborting(refresh)``
- * ``[processed-success] => aborted)``: Refresh group finished. Aborting was successful, money was reclaimed
- * ``[processed-failed] => failed)``: Refresh group failed to complete with a permanent error.
- * ``[action:abort-force] => failed``: XXX will this abort the refresh session or just orphan it?
+ * ``[processed-success] => aborted``: Refresh group finished. Aborting was successful, money was reclaimed.
+ * ``[processed-failed] => failed``: Refresh group failed to complete with a permanent error.
+ * ``[action:fail] => failed``: The user explicitly asked us to give up and accepted the possible loss of funds.
* ``done``
- * ``[action:delete]`` No money should be lost in this case.
+ The transfer was successful.
+
+ * ``[action:delete] => deleted``
* ``aborted``
- * ``[action:delete]`` No additional money is lost other than fees from aborting/refreshing.
+ The transfer was aborted. Except for fees, the money was recovered.
+
+ * ``[action:delete] => deleted``
* ``failed``
- * ``[action:delete]``: Money will be lost.
+ The transfer failed. Money was lost. Unless on a forced abort, we should probably complain to the auditor.
+
+ * ``[action:delete] => deleted``
+
+* ``deleted``
+
+ All memory of the push debit operation is lost.
+
+.. image:: ../images/transaction-push-debit-states.png
Transaction Type: Peer Push Credit
@@ -464,109 +710,340 @@ a ``taler://pay-push`` URI.
States and transitions:
-* ``pending(initial)``
+* ``pending(download)``
- * ``[processed-success] => pending(withdrawing)``: Merging the reserve was successful
+ Wallet read the taler:// URI and is downloading the contract details for the user.
-* ``pending(withdrawing)``
+ * ``[processed-success] => pending(user)``: Contract can be shown to the user.
+ * ``[action:suspend] => suspended(download)``: User suspended the operation.
- * ``[processed-kyc-required] => kyc-required``
+* ``suspended(download)``
-* ``kyc-required``
+ The download of the purse meta data was suspended by the user.
- * ``[poll-success] => pending(withdrawing)``
- * ``[action:abort] => aborted``: The user will lose the coins they were not able to withdraw yet, unless they
- resume the transaction again.
+ * ``[action:resume] => pending(download)``
-* ``aborted``
+* ``pending(user)``
+
+ User needs to decide about accepting the money.
+
+ * ``[action:accept] => pending(merge)``
+ * ``[timeout] => failed``: User took too long to decide.
+
+* ``pending(merge)``
+
+ * ``[processed-success] => pending(withdraw)``: Merging the reserve was successful.
+ * ``[kyc-required] => pending(merge-kyc)``: User must pass KYC checks before the purse can be merged.
+ * ``[timeout] => failed``: The purse expired before we could complete the merge.
+ * ``[failure] => failed``: The merge failed permanently.
+ * FIXME(CG): do we want to allow suspending here?
+
+* ``pending(merge-kyc)``
+
+ We cannot merge the purse until passing a KYC check.
+ The user is shown a hint where to begin the KYC
+ process and the wallet long-polls on the KYC status.
+
+ * ``[poll-success] => pending(withdraw)``
+ * ``[action:suspend] => suspended(kyc)``
+ * ``[timeout] => failed``: The purse expired before we could complete the merge.
+
+* ``suspended(merge-kyc)``
+
+ We cannot merge the purse until passing a KYC check,
+ and that check was suspended by the user.
+
+ * ``[action:resume] => pending(kyc)``
+ * ``[timeout] => failed``: The purse expired before we could complete the merge.
+
+* ``pending(withdraw)``
+
+ The wallet is withdrawing coins from the reserve that was filled by merging
+ the purse.
- * ``[action:resume] => pending(withdrawing)``
- * ``[action:delete] => deleted``: The user will irrevocable lose coins that were not withdrawn from the reserve yet.
+ * ``[kyc-required] => pending(withdraw-kyc)``
+ * ``[aml-required] => pending(withdraw-aml)``
+ * ``[withdraw-failure] => failed``
+ * ``[withdraw-success] => done``
+ * ``[action:suspend] => suspended(withdraw)``
+
+* ``suspended(withdraw)``
+
+ The user requested the withdraw operation to be suspended.
+
+ * ``[action:resume] => pending(withdraw)``
+
+* ``pending(withdraw-kyc)``
+
+ We cannot withdraw more coins until passing a KYC check.
+ The user is shown a hint where to begin the KYC
+ process and the wallet long-polls on the KYC status.
+
+ * ``[poll-success] => pending(withdraw-coins)``
+ * ``[action:suspend] => suspended(withdraw-kyc)``
+
+* ``suspended(withdraw-kyc)``
+
+ We cannot withdraw from the reserve until passing a KYC check,
+ and that check was suspended by the user.
+
+ * ``[action:resume] => pending(withdraw-kyc)``
+
+* ``pending(withdraw-aml)``
+
+ We cannot withdraw more coins until AML rules are satisfied.
+ The user is shown a hint as to the AML status (pending or frozen).
+
+ * ``[poll-success] => pending(withdraw-coins)``
+ * ``[action:suspend] => suspended(withdraw-aml)``
+
+* ``suspended(withdraw-aml)``
+
+ We cannot withdraw from the reserve until AML rules are satisfied,
+ and the status check was suspended by the user.
+
+ * ``[action:resume] => pending(withdraw-aml)``
+ * ``[action:delete] => deleted``
+
+* ``failed``
+
+ The operation failed. Details are shown to the user. The money from the purse eventually goes to the sender (or some other wallet that merged it).
+
+ * ``[action:delete] => deleted``
* ``done``
+ The operation succeeded.
+
* ``[action:delete] => deleted``: No money will be lost, the withdrawn coins will be kept
+* ``deleted``
+
+ All memory of the push credit operation is lost.
+
+.. image:: ../images/transaction-push-credit-states.png
Transaction Type: Peer Pull Credit
----------------------------------
-TODO: Also specify variant where account reserve needs to be created / funded first.
+TODO: Also specify variant where account reserve needs to be created / funded first (Note: post 1.0-feature).
+
+* ``pending(purse-create)``
+
+ The wallet is creating a purse. Initial state.
+
+ * ``[process-success] => pending(ready)``: The wallet has created the purse.
+ * ``[process-failure] => deleted``: The purse creation failed. We only show a transient error.
+ * ``[action:abort] => deleted``: The user aborted the operation.
-* ``pending(initial)``
+* ``pending(ready)``
- In this state, the purse is created (already in a merged state, with the initiator
- providing the reserve).
+ In this state, the user can send / show the ``taler://`` URI or QR code to
+ somebody else.
- * ``[action:abort] => aborted``: At this stage, it's safe to just abort.
+ * ``[action:abort] => aborting(delete-purse)``: The user aborts the P2P payment.
+ * ``[purse-timeout] => aborted``: The other party was too slow and the purse
+ has now expired.
+ * ``[poll-success] => pending(withdraw)``: The other party has made the payment.
+ * ``[poll-error] => aborting(delete-purse)``: The exchange claims that there
+ is a permanent error regarding the purse. We should try to delete it.
- CG: is this not 'suspend' (safe to resume!). Also, deletion transitions are missing.
+* ``aborting(delete-purse)``
+
+ We are cleaning up the purse after the operation failed or was aborted by
+ the user.
-* ``pending(wait-deposit)``
+ * ``[failure:already-merged] => pending(withdraw)``: Too late to abort, the
+ other side already paid the invoice.
+ * ``[process-success] => aborted``: The wallet has deleted the purse.
+ * ``[failure:other] => failed``: The purse deletion failed; we are
+ nevertheless done.
+ * ``[action:fail] => failed``: Money may be lost if it was deposited
+ into the purse in the meantime.
- We're waiting for the other party to pay into the pre-merged purse.
+* ``aborted``
- * ``[action:abort] => aborting(delete-purse)``: At this stage, it's safe to just abort.
- * ``[process-failed(expired)] => failed(expired)``
+ The invoicing process ended without success.
-* ``pending(withdrawing)``
+ * ``[action:delete] => deleted``
+
+* ``pending(withdraw)``
+
+ The wallet is withdrawing the money paid for the invoice.
* ``[processed-success] => done``
+ * ``[failure] => failed``
+ * ``[processed-kyc] => pending(kyc)``
+ * ``[processed-aml] => pending(aml)``
+ * ``[action:suspend] => suspended(withdraw)``
-* ``aborting(delete-purse)``
+* ``suspended(withdraw)``
- * ``[processed-success] => aborted``
- * ``[processed-failed(merge)] => done``
- * ``[processed-failed(expired)] => failed(expired)``
+ The user suspended a withdraw operation.
-* ``aborted``
+ * ``[action:resume] => pending(withdraw)``
+
+* ``pending(kyc)``
+
+ The user must supply KYC information before withdrawing can continue.
+
+ * ``[poll-success] => pending(withdraw)``
+ * ``[action:suspend] => suspended(kyc)``
+
+* ``suspended(kyc)``
+
+ The user suspended waiting for the KYC operation to complete.
+
+ * ``[action:resume] => pending(kyc)``
+
+* ``pending(aml)``
+
+ The user must await a positive exchange AML decision.
+
+ * ``[poll-success] => pending(withdraw)``
+ * ``[action:suspend] => suspended(aml)``
+
+* ``suspended(aml)``
+
+ The user suspended waiting for the AML decision to be successful.
+
+ * ``[action:resume] => pending(aml)``
+
+* ``failed``
+
+ Obtaining the money for the invoce failed. This is likely a case for the
+ auditor.
+
+ * ``[action:delete] => deleted``
* ``done``
-* ``failed(expired)``
+ The payment for the invoice was successfully received.
+
+ * ``[action:delete] => deleted``
+
+* ``deleted``
+
+.. image:: ../images/transaction-pull-credit-states.png
-
Transaction Type: Peer Pull Debit
---------------------------------
-* ``pending(initial)``
+* ``pending(download)``
+
+ We are downloading the information about the invoice. Initial state.
+
+ * ``[action:suspend] => suspended(download)``
+ * ``[success] => pending(user)``
+
+* ``suspended(download)``
+
+ User suspended downloading the information about the invoice.
- We've downloaded information about the pull payment and are waiting
- for the user to confirm.
+ * ``[action:resume] => pending(download)``
+ * ``[action:delete] => deleted``
+
+* ``pending(user)``
- * ``[action:abort] => aborted``: Safe to abort!
- * ``[action:confirm-pay] => pending(deposit)``: Safe to abort!
+ We have downloaded information about the pull payment and are waiting for
+ the user to confirm.
+
+ * ``[action:confirm-pay] => pending(submit-payment)``
+ * ``[action:delete] => deleted``
+ * ``[timeout] => aborted``
* ``pending(deposit)``
The user has confirmed the payment and the wallet tries to deposit
into the provided purse.
+ * ``[action:suspend] => suspended(deposit)``
* ``[processed-success] => done``
- * ``[action:abort] => aborting(refresh)``: Wallet tries to refresh coins
- that were not already deposited. XXX Do we really always refresh even if no deposit
- attempt has been made yet? CG: only every refresh those coins that are dirty.
+ * ``[failure:timeout] => aborting(refresh)``
+ * ``[processed-success] => done``
+ * ``[failure:other] => aborting(refund)``
+
+* ``suspended(deposit)``
+
+ User suspended depositing into the purse.
+
+ * ``[action:resume] => pending(deposit)``
+ * ``[action:abort] => aborting_refund``
+
+* ``aborting(refund)``
+
+ Aborts the payment, asking for the already deposited coins to be refunded.
+
+ * ``[processed-success] => aborted(refunded)``
+ * ``[processed-failure] => aborting(refresh)``
+ * ``[action:fail] => failed``
* ``aborting(refresh)``
- XXX Before refreshing, should we not wait until the purse has expired?
+ Refreshes the coins that were previously deposited into the purse to recover their value.
* ``[processed-success] => aborted``
* ``[processed-failed] => failed``
* ``done``
+ The invoice was successfully paid.
+
+ * ``[action:delete] => deleted``
+
+* ``deleted``
+
+ All information about the invoice has been deleted.
+
+.. image:: ../images/transaction-pull-debit-states.png
+
Alternatives
============
-* each transaction could be treated completely separately
+* Each transaction could be treated completely separately; however, uniform
+ terminology for actions (and thus button labels) is likely more helpful for
+ the user experience.
+
+* We could require user re-approval if fees changed when the available
+ denominations change during a *withdraw*. This would require a different
+ state machine on withdraw. We believe the answer can be "no", for two
+ reasons: the wallet MUST pick denominations to withdraw with the "most
+ long-term" withdraw window (i.e. active denominations that have the longest
+ available withdraw durations). So in virtually all normal cases, this will
+ just succeed as a sane exchange will have a reasonable duration overlap, and
+ in the very few cases it's really the user's fault for going offline in the
+ middle of the operation. Plus, even in those few cases, it is highly
+ unlikely that the fee would actually change: again most key rotations can be
+ expected to be there to rotate the key, and not to adjust the withdraw fee.
+ And in the extremely rare case that the user went offline and in the
+ meantime the fees did *increase*, it's again unlikely to matter much to the
+ user. So special-casing this and testing this is probably not worth it.
+
+* We could require user re-approval if due to expired/invalid coins the coin
+ selection (and thus fees) changes during a *deposit*. Again, expired coins
+ should virtually never happen unless a user goes offline for a long time in
+ the middle of a purchase (which would be very strange). If deposit fees
+ *increase* due to a double-spend detection during payment, we might want to
+ have an *optional* dialog ("Balance reduced by X as wallet state was not
+ up-to-date (did you restore from backup?). Consequently, the fees for this
+ transactions increased from Y to Z. [Abort] [Continue] + checkbox: [X] Do
+ not ask again."). Probably at best a post-1.0 feature.
-Drawbacks
-=========
Discussion / Q&A
================
-(This should be filled in with results from discussions on mailing lists / personal communication.)
+* The diagrams only show what is happening **after** the wallet
+ has created the transaction. It is possible that network requests
+ are happening before that, but they are not considered to be part
+ of the transaction.
+* We have decided against a ``cancel`` state, because it resulted
+ in too much complexity. Instead of doing a direct ``cancel``,
+ the user has to go to the transaction and abort and/or delete
+ it.
+* We might add a ``revive`` action in the future that allows
+ to go from ``aborting`` back to ``pending`` for transactions
+ where this makes sense. We're not doing it right now
+ to simplify things.