-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ui: add expected refund time to match-card #2042
ui: add expected refund time to match-card #2042
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks nice. The one message is potentially too colloquial, so at least for the code I have some suggestions, although I'm tentatively OK with the user-facing message since I think most native English speakers will get the message.
I've looked a bit at the issue @JoeGruffins raised above, and given:
I think (if we are Taker) we should show both Maker Redemption and Taker Redemption (along with Refund message, after Revocation kicks in) up until the moment when Taker gives up on searching for maker redemption (which happens only after successful refund, from what I figured). That is to take into account the case when Maker redeems after Taker starts trying to broadcast his refund transaction but before he manages to do so (I assume this is what @chappjc meant by "it's passed the taker refund time"):
added a commit: c61a71e to implement just that ^ (can remove/update if I got it wrong), so for Taker match-card will look like this until either redeem or refund txn shows up: edit: this one: |
Yeah, after successful refund it shouldn't show up, I didn't have a chance to fully test this PR yet (after latest changes) - planning on testing it more soon. |
3dc2b75
to
741138d
Compare
Updated bodybuilder.tmpl and rebased (in latest force-push), more changes coming. |
I've played around with match-card some more, to keep the complexity manageable, and get some flexibility when showing Refund we might want to display match slightly differently depending on whether it has been revoked. Trying to simply extend conditions that are in place in master gets quite messy, so instead I split match displaying in two sections 7440e28 : success-path, alternative-path (based on Did quite a bit of testing on this, and it seems to be working same way as in master except it should fix issues @JoeGruffins mentioned above. |
} | ||
return cid | ||
} | ||
const tryShowCoin = (showPlaceholder: () => void, showCoin: () => void, linkName: string, coin: Coin) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tryShowCoin
is a little over-engineered. Check out this refactor. buck54321/dcrdex@7440e28...buck54321:dcrdex:refactor-tryshowcoin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied, however I'd also suggest minor improvement on top: 17542c2 to keep all the code related to refund
together in one place, instead of spread out (with that extra if (m.refund) {
), thoughts ?
update: it should be also if (m.refund) {
-> if (!m.refund) {
in your example, right ? I noticed that only after I've done that refactoring 17542c2 (pulling this code close together)
const formatCoinID = (cid: string) => { | ||
if (cid.startsWith(coinIDTakerFoundMakerRedemption)) { | ||
const makerAddr = cid.substring(coinIDTakerFoundMakerRedemption.length) | ||
return intl.prep(intl.ID_TAKER_FOUND_MAKER_REDEMPTION, { makerAddr: makerAddr }) | ||
} | ||
return cid | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be moved. This is the only place it's used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is analogous to setCoinHref
which could also be inlined (since it is used only once),
so since setCoinHref
resides outside seems natural to do same for formatCoinID
, to simplify the setMutableMatchCardElements
overall, or would you prefer to inline both ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can understand an argument for consistency, but it's unrelated to the goals of this PR. Don't fix things that aren't broken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, reverted.
// because there isn't enough data on match card to figure out whether taker | ||
// refunded already |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data on match card
? Did you mean data in m
or data in the Match
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meant data in m
(also updated comment), but I admit I have limited understanding of this subject - clarifications are welcome!
If there is an easy way to know as Maker about Taker refunding we should probably leverage that in UI to relay that info to the user ?
// - as taker, we should expect maker redeeming any time, so we'll have to show | ||
// redeem coins until make redeem shows up, or until we refund. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'll have to show redeem coins
I see now. You're using the word coin in a few places to mean the row which could contain the coin id or the pending message. Can we normalize these new docs and use row instead of coin where fitting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I think the original phrasing (on master) is like nevermind, updated my comments, let me know if you still can spot ambiguitysetCoin
, so I just kept it same, will update
// lockTimeMakerMs must match the value returned from LockTimeMaker func in dexc. | ||
const lockTimeMakerMs = 20 * 60 * 60 * 1000 | ||
// lockTimeTakerMs must match the value returned from LockTimeTaker func in dexc. | ||
const lockTimeTakerMs = 8 * 60 * 60 * 1000 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK as you have it, but on simnet we are probably using the harness, in which case the lock times are probably 30s and 1m.
const formatCoinID = (cid: string) => { | ||
if (cid.startsWith(coinIDTakerFoundMakerRedemption)) { | ||
const makerAddr = cid.substring(coinIDTakerFoundMakerRedemption.length) | ||
return intl.prep(intl.ID_TAKER_FOUND_MAKER_REDEMPTION, { makerAddr: makerAddr }) | ||
} | ||
return cid | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can understand an argument for consistency, but it's unrelated to the goals of this PR. Don't fix things that aren't broken.
if (!m.revoked) { | ||
// Match is still following the usual success-path, it is desirable for the | ||
// user to see it in full (even if to learn how atomic swap is supposed to | ||
// work). | ||
|
||
Doc.setVis(!m.isCancel && (makerSwapCoin(m) || m.active), tmpl.makerSwap) | ||
Doc.setVis(!m.isCancel && (takerSwapCoin(m) || m.active), tmpl.takerSwap) | ||
Doc.setVis(!m.isCancel && (makerRedeemCoin(m) || m.active), tmpl.makerRedeem) | ||
// When maker isn't aware of taker redeem coin, once the match becomes inactive | ||
// (nothing else maker is expected to do in this match) just hide taker redeem. | ||
Doc.setVis(!m.isCancel && (takerRedeemCoin(m) || m.active), tmpl.takerRedeem) | ||
// Refunding isn't a usual part of success-path, but don't rule it out. | ||
Doc.setVis(!m.isCancel && m.refund, tmpl.refund) | ||
} else { | ||
// Match diverged from the usual success-path, since this could have happened | ||
// at any step it is hard (maybe impossible) to predict the final state this | ||
// match will end up in, so show only steps that already happened plus all | ||
// the possibilities on the next step ahead. | ||
|
||
// If we don't have swap coins after revocation, we won't show the pending message. | ||
Doc.setVis(!m.isCancel && (makerSwapCoin(m)), tmpl.makerSwap) | ||
Doc.setVis(!m.isCancel && (takerSwapCoin(m)), tmpl.takerSwap) | ||
// When match is revoked and both swaps are present, maker redeem might still show up: | ||
// - as maker, we'll try to redeem until it isn't possible due to refund by taker, | ||
// so we'll have to show redeem row until we either redeem or refund | ||
// because there isn't enough data in Match m to figure out whether taker | ||
// refunded already | ||
// - as taker, we should expect maker redeeming any time, so we'll have to show | ||
// redeem pending element until maker redeem shows up, or until we refund. | ||
Doc.setVis(!m.isCancel && (makerRedeemCoin(m) || (takerSwapCoin(m) && m.active && !m.refund)), tmpl.makerRedeem) | ||
// When maker isn't aware of taker redeem coin, once the match becomes inactive | ||
// (nothing else maker is expected to do in this match) just hide taker redeem. | ||
Doc.setVis(!m.isCancel && (takerRedeemCoin(m) || (makerRedeemCoin(m) && m.active && !m.refund)), tmpl.takerRedeem) | ||
// As taker, show refund placeholder only if we have outstanding swap to refund. | ||
// There is no need to wait for anything else, we can show refund placeholder | ||
// (to inform the user that it is likely to happen) right after match revocation. | ||
let expectingRefund = Boolean(takerSwapCoin(m)) // as taker | ||
if (m.side === OrderUtil.Maker) { | ||
// As maker, show refund placeholder only if we have outstanding swap to refund. | ||
// If we don't have taker swap there is no need to wait for anything else, we | ||
// can show refund placeholder (to inform the user that it is likely to happen) | ||
// right after match revocation. | ||
expectingRefund = Boolean(makerSwapCoin(m)) | ||
// If we discover taker swap we'll be trying to redeem it (instead of trying | ||
// to refund our own swap) until taker refunds, so start showing refund | ||
// placeholder only after taker is expected to start his refund process in | ||
// this case. | ||
if (takerSwapCoin(m)) { | ||
const takerRefundsAfter = new Date(m.stamp + lockTimeTakerMs) | ||
expectingRefund = expectingRefund && Date.now() > takerRefundsAfter.getTime() | ||
} | ||
} | ||
Doc.setVis(!m.isCancel && (m.refund || (m.active && !m.redeem && !m.counterRedeem && expectingRefund)), tmpl.refund) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most of this could be done with few changes to the existing code. It's somewhat self-documenting.
if (m.isCancel) return
Doc.setVis(makerSwapCoin(m) || (m.active && !m.revoked), tmpl.makerSwap)
Doc.setVis(takerSwapCoin(m) || (m.active && !m.revoked), tmpl.takerSwap)
Doc.setVis(makerRedeemCoin(m) || (!m.revoked && m.active) || (m.revoked && takerSwapCoin(m) && m.active && !m.refund), tmpl.makerRedeem)
Doc.setVis(takerRedeemCoin(m) || (!m.revoked && m.active) || (m.revoked && makerRedeemCoin(m) && m.active && !m.refund), tmpl.takerRedeem)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't checked whether this would be exact 1-to-1 equivalent to what I currently have (if I remember correctly, it also addresses >= 1 issue that we currently have in master),
but it kind of defeats the goal I tried to accomplish here - which is to break down and document each non-trivial path we might have here, having spent quite some time on this I'd say there are plenty of caveats here (and we might need to adjust this code in the future for some reason, so having it all documented could save effort, at least for those who aren't intimately familiar with the relevant code and working on it daily). I don't think getting line-count down is a better trade-off here, if it is the only benefit.
// this case. | ||
if (takerSwapCoin(m)) { | ||
const takerRefundsAfter = new Date(m.stamp + lockTimeTakerMs) | ||
expectingRefund = expectingRefund && Date.now() > takerRefundsAfter.getTime() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is a little bit funny. If we're the maker revoked in TakerSwapCast
, we'll redeem up until we can refund, as long as the taker doesn't refund first. core does not care if the taker's locktime has passed, only whether the taker has actually refunded. (The edge we're addressing is when the taker broadcasts their transaction right before the locktime and we're awaiting requisite confirmations). That said, we have no way of differentiating here whether the taker has actually refunded or if we're waiting on confirmations or wallet connectivity or something else, so this approach is maybe the best we can do with the information available?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have no way of differentiating here whether the taker has actually refunded or if we're waiting on confirmations or wallet connectivity or something else, so this approach is maybe the best we can do with the information available?
Yes, this is my understanding too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're the maker revoked in
TakerSwapCast
, we'll redeem up until we can refund, as long as the taker doesn't refund first. core does not care if the taker's locktime has passed, only whether the taker has actually refunded.
As maker, core
won't redeem the taker's swap if its locktime has passed. That's extremely important because maker is revealing the secret in this step, and the taker could quickly attempt to double spend their own swap contract in a refund and redeem the maker's contract since they now have the secret. Since the first seen policies are going by the wayside recently, it's even riskier. #1548 added this to isRedeemable
:
Lines 1554 to 1581 in d68ae84
switch match.Status { | |
case order.TakerSwapCast: | |
if match.Side == order.Maker { | |
// Check the confirmations on the taker's swap. | |
confs, req, changed, spent, expired, err := t.counterPartyConfirms(ctx, match) | |
if err != nil { | |
if !errors.Is(err, asset.ErrSwapNotInitiated) { | |
// We cannot get the swap data yet but there is no need | |
// to log an error if swap not initiated as this is | |
// expected for newly made swaps involving contracts. | |
t.dc.log.Errorf("isRedeemable: %v", err) | |
} | |
return false, false | |
} | |
if spent { | |
if match.MetaData.Proof.SelfRevoked { | |
return false, false // already self-revoked | |
} | |
// Here we can check to see if this is a redeem we failed to record... | |
t.dc.log.Warnf("Order %s, match %s counter-party's swap is spent before we could redeem", t.ID(), match) | |
return false, true // REVOKE! | |
} | |
if expired { | |
if match.MetaData.Proof.SelfRevoked { | |
return false, false // already self-revoked | |
} | |
t.dc.log.Warnf("Order %s, match %s counter-party's swap expired before we could redeem", t.ID(), match) | |
return false, true // REVOKE! |
We can be sure that as maker there will be no redeem of the taker swap if the taker's swap contract has expired.
What was being displayed incorrectly with the previous logic for show/hiding these fielids?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was being displayed incorrectly with the previous logic for show/hiding these fielids?
Maker, ~2h since matched, before/after:
- so we should be waiting Maker redeeming, not refunding until Taker is able to refund right ? This is fixed here.
Taker, ~2h since matched, before/after:
- in this PR, we display now expected refund time (if refund path gets taken)
- in this PR, we display Maker redemption (3rd line) but not Taker (our) redemption (4rd line), while on master we don't display 3rd line at all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The weird thing I encountered just now is (even when rebased on master): Maker doesn't publish redeem transaction due to:
2023-04-17 05:32:47.103 [INF] CORE: Match 1bf6ef2071d48b6e2f9201770fbcd4605c7a5c9c75a149d5e8da82769f3c0c63 not yet redeemable: current confs = 0, required confs = 1
even though it already has 49 confirmations (according to dcrdata):
My DCR wallet is 100% synched:
I think it did always publish it in the past, maybe some kind of regression ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Smells like another reorg issue related to "external txns" / block filters scan. Notice the Included in Block on dcrdata shows an orphaned block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Smells like another reorg issue related to "external txns" / block filters scan. Notice the Included in Block on dcrdata shows an orphaned block.
Hm, I can look into this a bit later, it has no relation to this PR I believe (and I've now seen it happen without additional orphaned blocks present https://testnet.dcrdata.org/tx/81a88f573794f385a0f5587d3a5edff0b323667f829807a01f06309721c83318/out/0).
We can be sure that as maker there will be no redeem of the taker swap if the taker's swap contract has expired.
If that's the case then there is no need to keep showing Maker redeem (3rd line) like I'm currently doing in this PR, we can hide it like that (it is also hidden on master
but as can be seen on screenshots in my comment above it's overly aggressive and hides it regardless Taker locktime): aa25f0d
Generally OK. Please revive with reviewer comments resolved, otherwise please express intent to abandon. Will close tomorrow. |
Will go through pending reviews today. Update: squashed all those commits into 1 so I can rebase d7f8633 and added 2 commits on top to address reviews. |
2a15334
to
e2ddc69
Compare
Noticed and fixed in 6240b94:
|
const refundAfter = new Date(m.stamp + lockTime) | ||
if (Date.now() > refundAfter.getTime()) tmpl.refundPending.textContent = intl.prep(intl.ID_REFUND_IMMINENT) | ||
else { | ||
const refundAfterStr = refundAfter.toLocaleTimeString('en-GB', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we not use the user's actual locale here? Not just for the positioning of units, but because short
for month is going to be English, like "Mar" or "Aug".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just copied it from here without thinking much about it:
tmpl.matchTime.textContent = time.toLocaleTimeString('en-GB', {
I guess both need changing ? passing in navigator.languages as string[]
there now (like we do for formatters I've worked with recently) and it seems to be working fine.
// this case. | ||
if (takerSwapCoin(m)) { | ||
const takerRefundsAfter = new Date(m.stamp + lockTimeTakerMs) | ||
expectingRefund = expectingRefund && Date.now() > takerRefundsAfter.getTime() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're the maker revoked in
TakerSwapCast
, we'll redeem up until we can refund, as long as the taker doesn't refund first. core does not care if the taker's locktime has passed, only whether the taker has actually refunded.
As maker, core
won't redeem the taker's swap if its locktime has passed. That's extremely important because maker is revealing the secret in this step, and the taker could quickly attempt to double spend their own swap contract in a refund and redeem the maker's contract since they now have the secret. Since the first seen policies are going by the wayside recently, it's even riskier. #1548 added this to isRedeemable
:
Lines 1554 to 1581 in d68ae84
switch match.Status { | |
case order.TakerSwapCast: | |
if match.Side == order.Maker { | |
// Check the confirmations on the taker's swap. | |
confs, req, changed, spent, expired, err := t.counterPartyConfirms(ctx, match) | |
if err != nil { | |
if !errors.Is(err, asset.ErrSwapNotInitiated) { | |
// We cannot get the swap data yet but there is no need | |
// to log an error if swap not initiated as this is | |
// expected for newly made swaps involving contracts. | |
t.dc.log.Errorf("isRedeemable: %v", err) | |
} | |
return false, false | |
} | |
if spent { | |
if match.MetaData.Proof.SelfRevoked { | |
return false, false // already self-revoked | |
} | |
// Here we can check to see if this is a redeem we failed to record... | |
t.dc.log.Warnf("Order %s, match %s counter-party's swap is spent before we could redeem", t.ID(), match) | |
return false, true // REVOKE! | |
} | |
if expired { | |
if match.MetaData.Proof.SelfRevoked { | |
return false, false // already self-revoked | |
} | |
t.dc.log.Warnf("Order %s, match %s counter-party's swap expired before we could redeem", t.ID(), match) | |
return false, true // REVOKE! |
We can be sure that as maker there will be no redeem of the taker swap if the taker's swap contract has expired.
What was being displayed incorrectly with the previous logic for show/hiding these fielids?
a689b3f
to
aa25f0d
Compare
Very tentatively in the v0.6.1 milestone. |
aa25f0d
to
d3c1b78
Compare
Resolves #1947.
With changes in this PR (note, these screenshots were indeed made in November 2022, just waited for #1978 to go in first; I didn't re-test it myself after rebase, but you'll probably be doing your own testing and I'm optimistic that it still works fine):