-
-
Notifications
You must be signed in to change notification settings - Fork 109
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
receiver and sender fallback and p2p lnurlp #1523
base: master
Are you sure you want to change the base?
Conversation
8b22e03
to
e647d2e
Compare
on hold until #1507 is merged, i will rebase any change on top of the new code |
throw new Error('forceFeeCredits is set, but user does not have enough fee credits') | ||
// if forceFeeCredit or the action is free or prioritizeFeeCredits perform a fee credit action | ||
if (me && !disableFeeCredit && ((forceFeeCredits || cost === 0n) || (prioritizeFeeCredits && (me?.msats ?? 0n) >= cost))) { | ||
const invoiceData = await createFeeCreditInvoice(receiverUserId, { msats: cost, description }) |
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.
Paid actions had to check if they were paid with fee credit, or with an invoice, until now this wasn't a problem because actions never changed from one method to the other, but now with a post nov5-update fallback mechanism (ie. p2p first, cc last, both on send and receive) , we can have a situations where an action changes payment method.
Using a virtual invoice for fee credit payments is a simple way to treat every action as if it had an invoice.
const action = paidActions[actionType] | ||
async function performAction (invoiceEntry, paidAction, args, context) { | ||
const { retryForInvoice } = context | ||
if (retryForInvoice && paidAction.retry) { |
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 the action supports retrying, we use it. Otherwise we just rerun the action
context.me = context.me ? await context.models.user.findUnique({ where: { id: context.me.id } }) : undefined | ||
context.cost = context.cost ?? await paidAction.getCost(args, context) | ||
if (context.cost < 0n) throw new Error('Cost cannot be negative') | ||
context.sybilFeePercent = context.sybilFeePercent ?? paidAction.getSybilFeePercent ? await paidAction.getSybilFeePercent(args, context) : undefined |
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.
paid actions can set their sybil fee
if (context.sybilFeePercent !== undefined && context.sybilFeePercent < 0n) throw new Error('Sybil fee percent cannot be negative') | ||
|
||
context.description = context.me?.hideInvoiceDesc ? undefined : (context.description ?? await paidAction.describe(args, context)) | ||
context.descriptionHash = context.descriptionHash ?? (paidAction.describeHash ? await paidAction.describeHash(args, context) : undefined) |
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.
paid actions can return a custom description hash
|
||
context.description = context.me?.hideInvoiceDesc ? undefined : (context.description ?? await paidAction.describe(args, context)) | ||
context.descriptionHash = context.descriptionHash ?? (paidAction.describeHash ? await paidAction.describeHash(args, context) : undefined) | ||
context.fallbackToSN = context.fallbackToSN ?? true |
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 can disable fallback to the centralized SN wallet
|
||
// the fee for the zap sybil service | ||
export const ZAP_SYBIL_FEE_PERCENT = 30n | ||
export const MAX_FEE_ESTIMATE_PERMILE = 25n // the maximum fee relative to outgoing we'll allow for the fee estimate |
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.
reduce bigint <-> number conversions
export const toPositive = (x) => { | ||
if (typeof x === 'bigint') return toPositiveBigInt(x) | ||
return toPositiveNumber(x) | ||
} |
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.
Just helpers to strictly handle bigints
descriptionHash = createHash('sha256').update(noteStr).digest('hex') | ||
} else { | ||
res.status(400).json({ status: 'ERROR', reason: 'invalid NIP-57 note' }) | ||
return | ||
} | ||
} else { | ||
description = user.hideInvoiceDesc ? undefined : `Funding @${username} on stacker.news` | ||
description = `Funding @${username} on stacker.news` | ||
description += comment ? `: ${comment}` : '.' |
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 don't need to handle invoice privacy here, it is done by the paid action
wrapped.description = undefined | ||
wrapped.description_hash = undefined | ||
} | ||
|
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 handled upstream by the create*Invoice code in wallets/server
description: 'SN: autowithdrawal', | ||
expiry: 360, | ||
fallbackToSN: false, // if the user doesn't have an attached wallet, there is nothing to withdraw to | ||
direct: true // no need to wrap this one |
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 to disable wrapping, that would otherwise happen automatically because the destination is an user wallet.
It is a bit ugly to have a random flag like that, but supposedly this is one of the few situations where we need this behavior (?)
if (receiverUser.hideInvoiceDesc) { | ||
description = undefined | ||
descriptionHash = undefined | ||
} |
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.
receiver privacy
} | ||
|
||
try { | ||
await invoiceHelper.cancel(invoice) |
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 if somehow the wallet failed but is still trying to pay? (maybe an improper timeout?). Safer to wait for the invoice to be cancelled, so we don't double invoice.
components/use-paid-mutation.js
Outdated
try { | ||
return await waitForWalletPayment(invoice, waitFor) | ||
console.log('could not pay with any wallet, will try with fee credits...') | ||
const retry = await retryPaidAction({ variables: { invoiceId: parseInt(invoice.id), forceFeeCredits: true } }) |
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 use fee credits only if the p2p payment is not possible
} | ||
await waitForQrPayment(invoice, null, { persistOnNavigate, waitFor }) | ||
return { invoice, response } | ||
} |
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.
anons gets the qr code right away
} else { | ||
if (failedInvoice.retriable) retriable = true | ||
context.retriedWithFeeCredits = !failedInvoice.retriableWithFeeCredits | ||
context.retriedManually = !failedInvoice.retriableManually |
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.
should this be a single state with an enum? Maybe not, the 3 flags are not necessarily dependent to each other and we don't need to follow a specific flow.
if (!action.supportsOptimism) { | ||
throw new Error(`retryPaidAction - action does not support optimism ${actionType}`) | ||
let retriable = false | ||
if (forceRetry) { |
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 code limits retries so that after a while we know that everything we tried failed, and we consider the action dead (both client and serverside).
However only the user knows if maybe it is a temporary issue with its node or something like that, so there is this forceRetry
flag that the retry button can use in the ui to restart the cycle over.
const invoiceArgs = await createSNInvoice(actionType, args, context) | ||
context.retryForInvoice = failedInvoice | ||
context.forceFeeCredits = forceFeeCredits | ||
context.actionAttempt = failedInvoice.actionAttempt + 1 // next attempt |
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 used to cycle through the wallets when they look fine to us, but the user client code cannot pay them, so we ensures every retry is for another wallet, eventually we'll find a wallet the user can pay, maybe.
The PR implements fallbacks on receiver, lnurlp for attached wallets and does some refactoring to the wallet interface.
Credit fee priority is toggleable for easier testing.
Closes #1496
Closes #1494
as side effect it should fix this too #1500
Changelog:
made pessimistic actions retryable
Solved some circular dependencies [1]
wallet/server
doesn't self-import to get walletDefs anymoreaddWalletLog
towallet/server
wallet/server
exports functions to create wrapped and un-wrapped hodl and regular invoicescreateHodlInvoice
andcreateInvoice
: create an invoice for an user id (or for stacker news [2]), they will first try to return a wrapped p2p invoice, if it is not possible they will fallback to the SN custodial wallet. If the wrapping fails, the next wallet is used.createFeeCreditInvoice
: create a "virtual" invoice to represent CC transfers,Moved some constants to lib/constants and helpers under the appropriate lib/ module
Added some types with jsdocremoved because they made the diff hard to follow, will add them back laterStrict bigint <-> number casts: bigint are kept bigint unless casting them to number is absolutely needed and only through the helper that checks for range.
implemented lnurlp/@stacker.news ln-address as pessimistic paid action TRANSFER : so it can use the p2p flow
make sybil fee configurable: actions can implement getSybilFeePercent to change the default sybil fee
implemented receiver fallback
implemented sender fallback : attachments -> fee credit -> qr
[2] the constant `SN_WALLET` can be passed as userId to target stacker news
Tests
setup
stacker1:
[good, bad]
[]
stacker2:
[bad, good]
[]
stacker3:
[bad,bad]
[]
stacker4:
[]
[good, bad]
stacker5:
[]
[bad, good]
stacker6:
[]
[bad, bad]
stacker7:
[]
[]
Zap
receive
post result
retry
until the page is refreshed)LNURLP