Skip to content
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

Merged

Conversation

norwnd
Copy link
Contributor

@norwnd norwnd commented Jan 12, 2023

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):

imageimageimage

Copy link
Member

@chappjc chappjc left a 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.

client/webserver/site/src/js/locales.ts Outdated Show resolved Hide resolved
client/webserver/site/src/js/locales.ts Outdated Show resolved Hide resolved
@JoeGruffins
Copy link
Member

JoeGruffins commented Jan 24, 2023

This is not new, but since you are here, can you remove the "Taker Redemption" line here? If we are pass the taker refund time that is no longer pending and we are trying to refund. Also, the three becomes a 4 and the info there is wrong. Not sure if this is in master but probably is.
image

@chappjc
Copy link
Member

chappjc commented Jan 24, 2023

This is not new, but since you are here, can you remove the "Taker Redemption" line here? If we are pass the taker refund time that is no longer pending and we are trying to refund. Also, the three becomes a 4 and the info there is wrong. Not sure if this is in master but probably is. image

Right, but only when it's passed the taker refund time (not just revoked and counting down). You said this, but I want to reiterate.

The maker could redeem at any time, forcing us to go the redeem path instead of refund.

@norwnd
Copy link
Contributor Author

norwnd commented Jan 25, 2023

I've looked a bit at the issue @JoeGruffins raised above, and given:

The maker could redeem at any time, forcing us to go the redeem path instead of refund.

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"):

but only when it's passed the taker refund time (not just revoked and counting down)

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:

image


edit: this one: and I left another relevant comment for similar (yet unsolved) "unfortunate timing" issue that Maker might encounter seems I already figured out

@JoeGruffins
Copy link
Member

Before redeeming is different and looks ok, but the third line does not go away after redeem. If refunded we don't need to show pending right?
image
image

@norwnd
Copy link
Contributor Author

norwnd commented Jan 26, 2023

If refunded we don't need to show pending right?

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.

@norwnd norwnd force-pushed the ui-add-expected-refund-time-on-match-card branch from 3dc2b75 to 741138d Compare January 27, 2023 11:38
@norwnd
Copy link
Contributor Author

norwnd commented Jan 27, 2023

Updated bodybuilder.tmpl and rebased (in latest force-push), more changes coming.

@norwnd norwnd marked this pull request as draft January 27, 2023 11:41
@norwnd
Copy link
Contributor Author

norwnd commented Jan 30, 2023

If refunded we don't need to show pending right?

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.

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 m.revoked flag) to handle each of them separately, also trying to document all edge-cases I can think of.

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.

@norwnd norwnd marked this pull request as ready for review January 30, 2023 03:31
}
return cid
}
const tryShowCoin = (showPlaceholder: () => void, showCoin: () => void, linkName: string, coin: Coin) => {
Copy link
Member

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

Copy link
Contributor Author

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)

Comment on lines 262 to 277
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
}
Copy link
Member

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.

Copy link
Contributor Author

@norwnd norwnd Jan 31, 2023

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 ?

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, reverted.

client/webserver/site/src/js/order.ts Outdated Show resolved Hide resolved
client/webserver/site/src/js/order.ts Outdated Show resolved Hide resolved
Comment on lines 397 to 373
// because there isn't enough data on match card to figure out whether taker
// refunded already
Copy link
Member

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?

Copy link
Contributor Author

@norwnd norwnd Jan 31, 2023

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 ?

Comment on lines 399 to 400
// - 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.
Copy link
Member

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.

Copy link
Contributor Author

@norwnd norwnd Jan 30, 2023

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 setCoin, so I just kept it same, will update nevermind, updated my comments, let me know if you still can spot ambiguity

client/webserver/site/src/js/order.ts Outdated Show resolved Hide resolved
client/webserver/site/src/js/order.ts Outdated Show resolved Hide resolved
client/webserver/site/src/js/order.ts Outdated Show resolved Hide resolved
dex/asset.go Outdated Show resolved Hide resolved
@chappjc chappjc added this to the 0.6.1 milestone Jan 30, 2023
Comment on lines +22 to +26
// 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

Copy link
Member

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.

Comment on lines 262 to 277
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
}
Copy link
Member

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.

client/webserver/site/src/js/order.ts Outdated Show resolved Hide resolved
client/webserver/site/src/js/order.ts Show resolved Hide resolved
Comment on lines 332 to 401
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)
}
Copy link
Member

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)

Copy link
Contributor Author

@norwnd norwnd Mar 21, 2023

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.

dex/asset.go Outdated Show resolved Hide resolved
// this case.
if (takerSwapCoin(m)) {
const takerRefundsAfter = new Date(m.stamp + lockTimeTakerMs)
expectingRefund = expectingRefund && Date.now() > takerRefundsAfter.getTime()
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

@chappjc chappjc Apr 14, 2023

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:

dcrdex/client/core/trade.go

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?

Copy link
Contributor Author

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:
imageimage

  • 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:
imageimage

  • 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

Copy link
Contributor Author

@norwnd norwnd Apr 17, 2023

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):
image

My DCR wallet is 100% synched:
image

I think it did always publish it in the past, maybe some kind of regression ?

Copy link
Member

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.

Copy link
Contributor Author

@norwnd norwnd Apr 17, 2023

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

@chappjc
Copy link
Member

chappjc commented Apr 11, 2023

Generally OK. Please revive with reviewer comments resolved, otherwise please express intent to abandon. Will close tomorrow.

@norwnd
Copy link
Contributor Author

norwnd commented Apr 11, 2023

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.

@norwnd norwnd force-pushed the ui-add-expected-refund-time-on-match-card branch from 2a15334 to e2ddc69 Compare April 11, 2023 07:47
@norwnd
Copy link
Contributor Author

norwnd commented Apr 11, 2023

Noticed and fixed in 6240b94:

  • I introduced a bug (caused by rogue return) when inlining func
  • we already check m.isCancel at the very first line when entering setMutableMatchCardElements func, so no need to check it 2nd time

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', {
Copy link
Member

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".

Copy link
Contributor Author

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()
Copy link
Member

@chappjc chappjc Apr 14, 2023

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:

dcrdex/client/core/trade.go

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?

@norwnd norwnd force-pushed the ui-add-expected-refund-time-on-match-card branch from a689b3f to aa25f0d Compare April 17, 2023 16:01
@chappjc chappjc removed this from the 0.6.1 milestone Apr 18, 2023
@chappjc
Copy link
Member

chappjc commented Apr 20, 2023

Very tentatively in the v0.6.1 milestone.

@chappjc chappjc force-pushed the ui-add-expected-refund-time-on-match-card branch from aa25f0d to d3c1b78 Compare May 9, 2023 19:03
@chappjc chappjc merged commit 428a1ab into decred:master May 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

UI: Maker Swap - display expected Refund time
4 participants