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

receiver and sender fallback and p2p lnurlp #1523

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

riccardobl
Copy link
Member

@riccardobl riccardobl commented Oct 27, 2024

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 anymore
    • moved addWalletLog to wallet/server
  • wallet/server exports functions to create wrapped and un-wrapped hodl and regular invoices

    • createHodlInvoice and createInvoice : 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 jsdoc removed because they made the diff hard to follow, will add them back later

  • Strict 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

[1] The current implementation functions correctly, but it relies on a specific export/import order, which is not best practice. This approach can introduce unintended issues as the dependency tree grows more complex, potentially leading to circular dependencies that change the order in unexpected ways even if everything appears correct.
[2] the constant `SN_WALLET` can be passed as userId to target stacker news

Tests

setup

  • stacker1:

    • fee credits: 1000
    • sending attachments [good, bad]
    • receiving attachments []
  • stacker2:

    • fee credits: 0
    • sending attachments: [bad, good]
    • receiving attachments: []
  • stacker3:

    • fee credits: 1000
    • sending attachments: [bad,bad]
    • receiving attachments []
  • stacker4:

    • fee credits: 0
    • sending attachments: []
    • receiving attachments: [good, bad]
  • stacker5:

    • fee credits: 0
    • sending attachments: []
    • receiving attachments: [bad, good]
  • stacker6:

    • fee credits: 0
    • sending attachments: []
    • receiving attachments: [bad, bad]
  • stacker7:

    • fee credits: 0
    • sending attachments: []
    • receiving attachments: []

Zap

send
receive
stacker1 stacker2 stacker3 stacker4
stacker4 [x] p2p [x] p2p [x] cc -
sacker5 [x] p2p [x] p2p [x] cc [x] qr on retry
stacker6 [x] cc [x ] cc [x] cc [x] qr on retry
stacker7 [x] cc [x] cc [x] cc [x] qr on retry

post result

  • stacker1 posts spending from attached wallet
  • stacker2 posts spending from attached wallet
  • stacker3 posts spending from attached wallet (bug: item still says retry until the page is refreshed)
  • stacker4 fails, shows qr on manual retry
  • stacker5 fails, shows qr on manual retry
  • stacker6 fails, shows qr on manual retry

LNURLP

  • stacker3: cc
  • stacker4: p2p
  • stacker5: p2p
  • stacker6: cc

@riccardobl riccardobl marked this pull request as draft October 27, 2024 22:54
@riccardobl riccardobl changed the title minor wallet refactoring (server only) fallback + lnurlp and minor refactoring with types (server only) Oct 28, 2024
@riccardobl riccardobl force-pushed the walletref branch 2 times, most recently from 8b22e03 to e647d2e Compare October 30, 2024 00:19
@riccardobl
Copy link
Member Author

on hold until #1507 is merged, i will rebase any change on top of the new code

@riccardobl riccardobl changed the title fallback + lnurlp and minor refactoring with types (server only) receiver fallback and p2p lnurlp Nov 4, 2024
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 })
Copy link
Member Author

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

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
Copy link
Member Author

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

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
Copy link
Member Author

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
Copy link
Member Author

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

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}` : '.'
Copy link
Member Author

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
}

Copy link
Member Author

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
Copy link
Member Author

@riccardobl riccardobl Nov 4, 2024

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 (?)

@riccardobl riccardobl removed the blocked label Nov 4, 2024
@riccardobl riccardobl marked this pull request as ready for review November 4, 2024 14:05
if (receiverUser.hideInvoiceDesc) {
description = undefined
descriptionHash = undefined
}
Copy link
Member Author

Choose a reason for hiding this comment

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

receiver privacy

@riccardobl riccardobl marked this pull request as draft November 4, 2024 21:36
@riccardobl riccardobl changed the title receiver fallback and p2p lnurlp receiver and sender fallback and p2p lnurlp Nov 4, 2024
}

try {
await invoiceHelper.cancel(invoice)
Copy link
Member Author

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.

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

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 }
}
Copy link
Member Author

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
Copy link
Member Author

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

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
Copy link
Member Author

@riccardobl riccardobl Nov 6, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant