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

Sender fallbacks #1642

Merged
merged 36 commits into from
Nov 28, 2024
Merged

Sender fallbacks #1642

merged 36 commits into from
Nov 28, 2024

Conversation

ekzyis
Copy link
Member

@ekzyis ekzyis commented Nov 23, 2024

Description

Partially solve #1494 for sender side

Based on #1651 #1660

TODO

  • fix payment check in sendPayment: wallets that are enabled but can only receive will throw SenderError instead of WalletNotConfiguredError
  • handle aggregated wallet errors in QR code
  • fix cache not updated with latest invoice on payment success or error
  • fix p2p zap retries create new invoices from SN, not from receiver
  • fix hmacs lost doesn't matter, anon is not affected
  • fix zap retry in notifications not updating to paid when paid

Manual Tests

  • comment create: first wallet fails, second does not fail
  • zap (optimistic action): first wallet fails, second does not fail
  • zap: no wallet succeeds
  • zap: first wallet succeeds
  • zap: no wallet available
  • zap: pay with fee credits (shouldn't be affected at all, just to make sure it still works)
  • zap: anon zaps (shouldn't be affected at all, just to make sure it still works)
  • territory create (pessimistic action): first wallet fails, second does not fail
  • territory create: no wallet succeeds
  • territory create: first wallet succeeds
  • territory create: no wallet available
  • territory create: pay with fee credits

Screenshots

how errors from one or multiple wallets will now be shown:

2024-11-26-233406_1920x1080_scrot

Additional Context

I think the only reason we only allowed retries for optimistic actions was because retries weren't necessary yet for pessimistic actions.

We also should probably move all code from components/payment.js into wallets/payment.js but I didn't to not create more noisy changes than necessary.

Checklist

Are your changes backwards compatible? Please answer below:

tbd

On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:

tbd

For frontend changes: Tested on mobile, light and dark mode? Please answer below:

tbd

Did you introduce any new environment variables? If so, call them out explicitly here:

no, not planned

@ekzyis ekzyis marked this pull request as draft November 23, 2024 05:22
@ekzyis ekzyis added feature new product features that weren't there before wallets labels Nov 23, 2024
@ekzyis ekzyis force-pushed the sender-fallbacks branch 3 times, most recently from 7918299 to 10632fd Compare November 25, 2024 00:53
wallets/payment.js Outdated Show resolved Hide resolved
@ekzyis ekzyis force-pushed the sender-fallbacks branch 2 times, most recently from b5bdf36 to 765d9f3 Compare November 26, 2024 09:46
Comment on lines -247 to -314
if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC)) {
throw new Error(`retryPaidAction - action does not support optimism ${actionType}`)
}

if (!action.retry) {
throw new Error(`retryPaidAction - action does not support retrying ${actionType}`)
}

Copy link
Member Author

Choose a reason for hiding this comment

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

So far, I think it's okay to remove these checks to allow retries for pessimistic actions but I will make more sure

Copy link
Member Author

Choose a reason for hiding this comment

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

I see no problem with removing these. Retries for optimistic actions work like before and pessimistic actions support retries as expected now.

walletError = err
// wallet payment error handling always creates a new invoice to retry
// unless no wallet was even able to attempt a payment
if (err.newInvoice) walletInvoice = err.newInvoice
Copy link
Member Author

@ekzyis ekzyis Nov 26, 2024

Choose a reason for hiding this comment

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

this is maybe the weirdest part of this implementation of sender fallbacks: using an error to pass the new invoice to the calling context

Copy link
Member

Choose a reason for hiding this comment

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

This is very strange indeed, the newInvoice is not logically a property of the error.
Can't you extract the part that recreates the invoice or return it from waitForWalletPayment ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Can't you extract the part that recreates the invoice or return it from waitForWalletPayment ?

I don't think so. Even if I extract it, I still need to pass the latest invoice to the calling context in some way. I can't use return since that would mean the wallet payment succeeded which isn't the case here.

I do return the latest invoice on success though, see the waitForPayment call for pessimistic actions below.

Copy link
Member

@riccardobl riccardobl Nov 27, 2024

Choose a reason for hiding this comment

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

It might seem unimportant in the context this function is used, but these are things that build up technical debt, since now you have a function that leaks invoices, unless you remember that unexpectedly the error contains a new resource you need to cancel if you don't need it, that's why passing around new data in an error field is generally bad practice, errors should report only what and why something failed.

But, unless i'm missing something in the logic, I think you can make a proper error object simply by not recreating the last invoice but just cancelling it and setting a failedInvoice field in the error. Then when you catch it in waitForPayment you can recreate it with await invoiceHelper.retry(error.failedInvoice) only if it is needed.
eg. if skipQr is true, you won't even need to recreate it, right?

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I had it like this before but it looked awkward to do something different on the last payment attempt error (not create new invoice) so I changed it to this with less code.

But the argument about "new data" is a good one, I'll change it back to only save the failed invoice in the error and the caller can retry if they want so. Thanks!!

Copy link
Member Author

@ekzyis ekzyis Nov 27, 2024

Choose a reason for hiding this comment

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

Did this in 96f76a9.

I realized that I can filter for only the wallets that are actually able to attempt a payment so the logic if there is another wallet we can try is indeed basically just if (i === wallets.length - 1). For some reason I didn't think the filter would work so I didn't even try and I think that's why I also didn't do it like this before.

This means the checks in sendPayment aren't really required anymore so maybe we should remove them 🤔

@ekzyis ekzyis marked this pull request as ready for review November 26, 2024 22:46
@ekzyis ekzyis requested a review from riccardobl November 26, 2024 22:47
Copy link
Member

@riccardobl riccardobl left a comment

Choose a reason for hiding this comment

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

this implementation of retryPaidAction doesn't work for p2p actions, it will turn them to pessimistic actions that pay SN.

api/paidAction/index.js Show resolved Hide resolved
walletError = err
// wallet payment error handling always creates a new invoice to retry
// unless no wallet was even able to attempt a payment
if (err.newInvoice) walletInvoice = err.newInvoice
Copy link
Member

Choose a reason for hiding this comment

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

This is very strange indeed, the newInvoice is not logically a property of the error.
Can't you extract the part that recreates the invoice or return it from waitForWalletPayment ?

@ekzyis
Copy link
Member Author

ekzyis commented Nov 27, 2024

Oh, I didn't test p2p zaps. Thanks, will check and fix!

@ekzyis
Copy link
Member Author

ekzyis commented Nov 27, 2024

Need to change some more stuff like the flow when no wallet is available for 96f76a9. Will put in draft until this is ready for review again.

@ekzyis ekzyis marked this pull request as draft November 27, 2024 16:15
@ekzyis ekzyis force-pushed the sender-fallbacks branch 2 times, most recently from 7ce033e to 2398491 Compare November 27, 2024 21:46
@ekzyis ekzyis marked this pull request as ready for review November 27, 2024 21:46
@ekzyis ekzyis requested a review from riccardobl November 27, 2024 21:46
Comment on lines -20 to +21
try {
const lnc = await getLNC(credentials, { logger })
const { paymentError, paymentPreimage: preimage } = await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
if (paymentError) throw new Error(paymentError)
if (!preimage) throw new Error('No preimage in response')
return preimage
} catch (err) {
const msg = err.message || err.toString?.()
if (msg.includes('invoice expired')) throw new InvoiceExpiredError(hash)
if (msg.includes('canceled')) throw new InvoiceCanceledError(hash)
throw err
}
const lnc = await getLNC(credentials, { logger })
const { paymentError, paymentPreimage: preimage } = await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
if (paymentError) throw new Error(paymentError)
if (!preimage) throw new Error('No preimage in response')
return preimage
Copy link
Member Author

Choose a reason for hiding this comment

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

I removed the try/catch here since we already have error handling in waitForPayment (which also checks for canceled or expired invoices) and in sendPayment which wrapps wallet.def.sendPayment.

I think this was just old code that we no longer need.

@ekzyis
Copy link
Member Author

ekzyis commented Nov 27, 2024

this implementation of retryPaidAction doesn't work for p2p actions, it will turn them to pessimistic actions that pay SN.

Fixed now. The code now retries the same receiver wallet if the forward didn't fail. If the forward failed, we immediately fallback to the SN node but we will try the next sender wallet.

I can see more clearly now why it makes sense to do sender + receiver fallbacks in one PR. The difference between both becomes blurry when we start to look into good error handling.

However, I think this is still an improvement that we can review and ship on its own. But with the // TODO: receiver fallbacks in the code, receiver fallback should be easy to add in another PR so ideally we can ship sender+receiver fallbacks together.

@huumn
Copy link
Member

huumn commented Nov 27, 2024

Cool, I'm going to try to refactor out the array of useWalletLogger(), so please don't force push.

@huumn
Copy link
Member

huumn commented Nov 27, 2024

Main thing I did in b608fb6 was

  1. factor out the wallet-specific code from the generic "helper" logic
  2. turn the wallet-specific code it into a hook that returns a "factory", i.e. factory produces a logger given a wallet

I'm not sure how generically we'll be able to apply this pattern but it works here.

Also (pats back):

  1. Fought the urge to refactor all components/wallet-*.js

wallets/index.js Outdated Show resolved Hide resolved
@huumn
Copy link
Member

huumn commented Nov 28, 2024

Is this good to merge? I noticed you added another todo

It might be a consequence of my refactor, but because state is kept in memory, retries don't really work after navigation.

I don't think we'll be able to rely on memory post-auto-retries anyways, so I don't think that's blocking merging this. It's a strict improvement.

Also sorry about the renaming and Error refactors. It helped me understand what was going on to rename some stuff and encapsulate others.

@huumn
Copy link
Member

huumn commented Nov 28, 2024

I looked into the zap retry with fallback cache issue some. It's not clear why it's no longer working but these retry cache updates were the hardest part of my initial retry work. The item act retry and item retry are quite similar though - odd it's just affecting one of them.

The only thing I can thing of is that the retry mutation in useWalletPayment might be conflicting with the cache update

@huumn
Copy link
Member

huumn commented Nov 28, 2024

fix hmacs lost doesn't matter, anon is not affected

Do we always lose hmacs? This is how we authenticate cancelations.

If that's inconsequential, this is ready to merge afaict.

@ekzyis
Copy link
Member Author

ekzyis commented Nov 28, 2024

Do we always lose hmacs?

Only in local storage and if we use attached wallets which poll invoices and return the latest state (= without hmac). Since anons don't have attached wallets, this doesn't matter for them (for now at least).

This is how we authenticate cancelations.

Afaik, cancels between retries use the original invoice state so they work. Since there was no error regarding cancels, I think they work but maybe good to make sure but I am not at home right now.

If that's inconsequential, this is ready to merge afaict.

This is also my view

Copy link
Member

@huumn huumn left a comment

Choose a reason for hiding this comment

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

Feel free to merge into master if there aren't any issues remaining.

@ekzyis ekzyis merged commit 8ce2e46 into master Nov 28, 2024
6 checks passed
@ekzyis ekzyis deleted the sender-fallbacks branch November 28, 2024 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature new product features that weren't there before wallets
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants