diff options
Diffstat (limited to 'design-documents/037-wallet-transactions-lifecycle.rst')
-rw-r--r-- | design-documents/037-wallet-transactions-lifecycle.rst | 985 |
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. |