From b617c6452ec92c06533e98a26b09c23165fbbf44 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Sat, 8 Apr 2023 22:13:55 +0200 Subject: review payment states --- Makefile | 4 +- .../037-wallet-transactions-lifecycle.rst | 137 +++++++++++++-------- transaction-payment-states.dot | 44 +++++++ 3 files changed, 131 insertions(+), 54 deletions(-) create mode 100644 transaction-payment-states.dot diff --git a/Makefile b/Makefile index e60c2dd8..b3e04e29 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,8 @@ transaction-common-states.png: transaction-common-states.dot dot -Tpng transaction-common-states.dot > transaction-common-states.png transaction-withdrawal-states.png: transaction-withdrawal-states.dot dot -Tpng transaction-withdrawal-states.dot > transaction-withdrawal-states.png +transaction-payment-states.png: transaction-payment-states.dot + dot -Tpng transaction-payment-states.dot > transaction-payment-states.png coin.png: coin.dot dot -Tpng coin.dot > coin.png deposit.png: deposit.dot @@ -64,7 +66,7 @@ deposit.png: deposit.dot reserve.png: reserve.dot dot -Tpng reserve.dot > reserve.png -diagrams: arch-api.png coin.png deposit.png reserve.png transaction-common-states.png transaction-withdrawal-states.png +diagrams: arch-api.png coin.png deposit.png reserve.png transaction-common-states.png transaction-withdrawal-states.png transaction-payment-states.png # The html-linked builder does not support caching, so we diff --git a/design-documents/037-wallet-transactions-lifecycle.rst b/design-documents/037-wallet-transactions-lifecycle.rst index e2a04820..366079a2 100644 --- a/design-documents/037-wallet-transactions-lifecycle.rst +++ b/design-documents/037-wallet-transactions-lifecycle.rst @@ -286,87 +286,120 @@ shown in the above diagram to keep it on point. 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? +* ``pending(claim-proposal)`` -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...). + Initial state. 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 + ``delete`` 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``). -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. + * ``[error: already claimed] => deleted`` -- the proposal was + already claimed by someone else; we go directly into the ``deleted`` + state and only show a transient warning. + * ``[error: invalid proposal] => deleted`` -- the merchant provided a + proposal that is invalid (e.g. malformed contract + terms or bad signature); we go directly into the ``deleted`` state + and only show a transient warning. -(??) If transaction can be aborted while in-progress then it should return -instructedAmount, effectiveAmount and the affectedAmount (partially effective amount) - -* ``initial(download-proposal)`` - - Initial state. Download (claim) the proposal from the merchant. - - XXX: Also consider repurchase detection here? - - 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``). - - (??) should not reset another transaction? * ``pending(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] => ``aborting(unclaim)`` -- The user explicitly + decided not to proceed (at least not with this wallet). + * ``[expired] => deleted`` -- The offer has expired before the user made any + decision. (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). 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. + +* ``aborting(unclaim)`` + + Tells the merchant that some *other* wallet is now again free to claim this + offer. * ``pending(submit-payment)`` -Submit coin by coin (or in bulk groups) until payment is complete. - - * ``[action:abort] => aborting(refund)`` - * ``[processed-success(auto-refund-enabled)] => pending(refundable)`` - * ``[processed-success(auto-refund-disabled)] => done`` - * ``[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. + + * ``[action:abort] => aborting(refund)`` -- 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(refundable)`` state (and subsequently + a ``done`` state) instead! + * ``[success] => pending(refundable)`` -- Upon receiving confirmation from + the merchant that the purchase was completed. + * ``[error(insufficient balance)] => aborting(refresh)`` 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(refundable)`` -The payment succeed but if auto-refund-check is active it will be checking for refunds + The payment succeed. We remain in this tate as long as an auto-refund-check + is active. If auto refunds are not enabled, we immediately continue to + ``done``. - * ``[auto-refund-timeout] => done`` + * ``[timeout] => done`` -- This happens when the auto refund set by the contract expired. + * ``[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). * ``aborting(refund)`` - * ``[processed-success] => aborted(refunded)`` - * ``[processed-failure] => aborting(refresh)`` + The wallet should interact with the merchant to confirm that a refund + was approved. -* ``aborting(refresh)`` + * ``[success] => aborted(refunded)`` + * ``[failure] => aborting(refresh)`` -* ``failed(invalid-proposal)`` +* ``aborting(refresh)`` - The merchant provided a proposal that is invalid (e.g. malformed contract terms or bad signature). + The wallet should interact with the exchange to obtain fresh coins for + the refunded balance. * ``aborted(refunded)`` + 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. + + * ``[URI:refund] => aborting(refund)`` -- The previous refund was partial, + and we have received an additional refund for this transaction. + * ``[action:delete] => deleted`` + +* ``done`` + + The purchase is completed. + * ``[action:delete] => deleted`` + * ``[refund URI trigger] => aborting(refund)`` -- A refund was initiated for the purchase. + * ``[repurchase] => pending(repurchase-session-reset)`` -- Another offer became pending for this product. + +* ``pending(repurchase-session-reset)`` + + The wallet should reset the associated session for the already purchased 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 refunds are always deleted with it. +.. image:: ../transaction-payment-states.png + :width: 800 + +Purple arrows are used to indicate transitions triggered in special ways outside +of the user interface for this transaction, such as side-effects of repurchase +detection for another purchase or opening of a refund URI. Transaction Type: Refund @@ -709,8 +742,6 @@ Alternatives not ask again."). Probably at best a post-1.0 feature. - - Drawbacks ========= diff --git a/transaction-payment-states.dot b/transaction-payment-states.dot new file mode 100644 index 00000000..066fb026 --- /dev/null +++ b/transaction-payment-states.dot @@ -0,0 +1,44 @@ +digraph G { + + initial[label="", shape="circle"]; + pending_dp[label="pending(claim-proposal)"]; + pending_pr[label="pending(proposed)"]; + pending_sp[label="pending(submit-payment)"]; + pending_rf[label="pending(refundable)"]; + pending_re[label="pending(repurchase-session-reset)"]; + aborting_unclaim[label="aborting(unclaim)"]; + aborting_refund[label="aborting(refund)"]; + aborting_refresh[label="aborting(refresh)"]; + aborted_refund[label="aborted(refunded)", shape="box"]; + done[label="done", shape="box"]; + deleted[label="deleted", shape="box"]; + + subgraph { + rank = same; pending_dp; pending_pr; pending_sp; pending_rf; + } + + initial->pending_dp [color="blue", label="URI trigger"]; + pending_dp->pending_pr; + pending_re->done; + pending_dp->deleted [label="repurchase\ndetected"]; + pending_dp->deleted [color="red", label="already\nclaimed"]; + pending_dp->deleted [color="red", label="invalid\nproposal"]; + pending_pr->pending_sp [color="blue", label="action:pay-accept"]; + pending_pr->aborting_unclaim [color="blue", label="action:pay-refuse"]; + pending_pr->deleted [label="expired"]; + pending_sp->pending_rf; + pending_sp->aborting_refund [color="blue", label="action:abort"]; pending_sp->aborting_refund [color="red", label="insufficient balance"]; + pending_rf->aborting_refund [label="long-poll:refund"]; + done->aborting_refund [color="purple", xlabel="refund URI trigger"]; + aborted_refund->aborting_refund [color="purple", label="refund URI trigger"]; + aborting_refund->aborting_refresh; + aborting_refund->aborted_refund [color="red", label="failure"]; + aborting_refresh->aborted_refund; + aborted_refund->deleted [color="blue", label="action:delete"]; + pending_rf->done [label="timeout"]; + pending_rf->done [color="blue", label="action:abort"]; + aborting_unclaim->deleted; + done->deleted [color="blue", xlabel="action:delete"]; + done->pending_re [color="purple", label="repurchase"]; + pending_re->done [color="blue", label="action:abort"]; +} -- cgit v1.2.3