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

Feat: dynamic budgets #226

Merged
merged 55 commits into from
Jul 19, 2024
Merged

Feat: dynamic budgets #226

merged 55 commits into from
Jul 19, 2024

Conversation

rolznz
Copy link
Contributor

@rolznz rolznz commented Jul 8, 2024

TLDR:

  • New "Isolated" app type which has its own balance and cannot interact with the rest of the wallet
  • Budgets are now secure, with transactions around payments, unsettled payments, fees and fee reserves are now taken into account
  • This is possible with a new internal transactions table - now all outgoing and incoming payments are tied to apps

Fixes #25
Fixes #52
Fixes #122
Partially addresses #284 (no multi-connection setup though)
Partially addresses #105

Breaking Changes

  • Payments table is deleted as part of the migration.
  • Now we use internal transactions. Transactions from the individual LNClients are NOT migrated.

What this enables

Changes

Architecture

  • New Transactions Layer (layer between api/nip-47 handlers and LN clients)
    • Creating a transaction will internally call the LNClient to create the transaction, then save it to our DB with additional metadata, such as the app the transaction was made for.
    • The transactions service is a consumer of our events system, so it receives notifications when transactions are received or sent transactions are settled. We then update our internal transactions.
    • Self-payments can now easily be intercepted by checking the payee of the invoice in the transactions service
    • for backends that do not receive events for updates, we check recently created unsettled transactions when lookup_invoice or list_transactions is called (Note: this will not work for receiving keysend payments, so the LND backend needs to implement notifications)
  • LNClient
    • LDK nwc_payment_received and nwc_payment_sent has been updated to send entire payment details to the event publisher, which the transactions service will consume.

Database

  • New transaction table with additional data than the old payment table, notably:
    • type incoming or outgoing
    • state which can be used to ensure budgets are not exceeded
    • metadata JSON-encoded transaction metadata
  • New properties on app permissions
    • balance_type (full or isolated) - isolated balance is summed from that app's received and sent payments
      • move to apps instead of app_permissions
    • visibility (full or isolated) - isolated only allows accessing that app's created invoices
      • move to apps instead of app_permissions
      • merge balance_type and visibility into a single property. can just be an isolated boolean
  • rename app_permissions max amount to be clearly in sats

TODOs:

  • update all transaction related usages of lnclient to go through transactions service
  • Test payment timeouts correctly apply to transactions (2 usages)
  • Ensure not found error propagates (see NIP-47 NOT_FOUND)
  • Add fees to budget calculation
  • Keysend (sends)
  • Keysend (receives)
  • Test payment metadata (boostagrams)
  • LDK event firing -> update transaction service
    • received payment
      • keysends
      • BOLT11
    • payment sent
      • keysend
      • BOLT11
    • payment failed
      • keysend
      • BOLT11
  • Intercept self payments
  • Review unique constraints
    • Duplicate outgoing payments for the same payment hash are possible if a payment fails and we retry - now sorted by settled des, then created desc
  • Should the internal wallet have it's own app ID - not needed
  • NIP-47 notifier receives a notification of a payment, but we do not know the app ID - test both for apps and global view
    • notifier should be updated to check if the transaction app ID matches the app ID, or it's an app with a global view
    • make sure notifier gets updated transaction (it must be consumed by transactions service first)
  • remove old payments table
  • review it's ok NOT to migrate transaction history. I would email all early users and let them know.
  • review keysend transactions initially do not have a payment hash, as it's generated by the LNClient - this is ok except for Greenlight/Breez, which are not a priority right now
  • review newly added app_permissions properties - move to app, internal boolean
  • To confirm: either implement notifications support in LND using SubscribeInvoices and SubscribeTransactions RPC methods and ensure it works for keysend, OR change the method of checking unsettled payments to list all payments from LNClient for a certain period of time. -> implement notifications support in LND
  • nwc_payment_failed event handling
  • fix lookup invoice returning not found
  • isolated balance check for keysend
  • remove duplication in isolated balance code
  • budget checks inside transaction
  • exceeded balance - add a proper error type so correct code is passed in NIP-47 response
  • exceeded budget - add a proper error type so correct code is passed in NIP-47 response
  • apply fee reserves in budget/balance checks
  • fee reserves for unsettled transactions
  • remove amount checks in NIP-47 handlers (now checked by transactions service) - PermissionsService.HasPermission should not check amounts, this is done by the transactions service in a database transaction
  • DB Transactions around payments
  • Add fee reserve to budget calculation for unsettled payments
  • Change code names of satoshi fields in DB models (using column rename)
  • review fee_reserve implementation for unsettled payments (column on transactions DB)
  • isolated app should not have access to node info
  • isolated app should not be able to sign messages -> merge the permissions revamp
  • add AUTOINCREMENT on the transactions table
  • add AUTOINCREMENT on the existing tables, drop request and response events
  • unknown external sent payments should not throw an error
  • fix autoincrement migration, add DELETE FROM app_permissions WHERE app_id NOT IN (SELECT id FROM apps);
  • review extra SQLite pragma commands

UI

  • transaction list
  • app cards with isolated balance
  • how to create/edit apps to be isolated? how does it affect the rest of the UI?

Unit tests

  • get_balance controller for isolated balance
  • event handler tests
  • update nip-47 controller tests
  • transactions service tests

Testing

  • NIP-47 notifications
  • Non-LDK LNClient (e.g. Cashu)
  • LND async payments
  • LND self-payments (do they expose the preimage? otherwise we can generate it, same as LNDHub)

To move to new issues:

  • Enable Roman's cleanup task for old request and response events
  • isolated app possible follow-ups (in case we wish to spend more time on isolated apps)
    • internal payments should trigger NIP-47 notifications (this can be done by the transactions service publishing its own notifications rather than the notifier subscribing to LNClient notifications - this would simplify things too)
    • restrict isolated balance to not receive more than a certain budget (it's hard to do?)
    • prevent hub not spending balance that is set aside for other apps
    • prevent paying more than actual balance from main wallet into isolated wallets (you can send an infinite amount)
    • cashu isolated app payments does not work and causes money to be "lost"
  • currently there are no DB indexes. I am not sure how this will perform when the number of payments grow
  • ListTransactions on LNClients that support notifications should actually just throw an error (they should never be used)
  • fee reserve calculation could be per-LNClient to completely match what the node sets
  • Extra nip-47 controller tests
    • multi_pay_keysend (partial success)
    • list_transactions (paging params)

rolznz added 30 commits July 1, 2024 22:01
…rors to NIP-47 response

also reduce query duplication
lnclient/lnd/lnd.go Outdated Show resolved Hide resolved
rolznz and others added 6 commits July 19, 2024 15:54
* fix: db transaction should be passed as pointer

* feat(lnd): subscribe for payments and invoices

* chore: rearrange check for settled

* chore: remove TODO

* chore: retry on error and add select

* chore: publish payment failed event

* chore: use json logging

* feat: add lnd notification types

* chore: remove sleep
@rolznz rolznz marked this pull request as ready for review July 19, 2024 09:49
expires_at datetime,
settled_at datetime,
metadata text,
self_payment boolean
Copy link
Contributor

Choose a reason for hiding this comment

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

we probably should have an index on

  • app_id
  • state
  • type
  • payment_hash
  • created_at / settled_at?

need to check what queries we make, but I think we need something here.
for example for this a combined might be helpful

"app_id = ? AND type = ? AND (state = ? OR state = ?) AND created_at > ?"
app_id = ? AND type = ? AND state = ?

make sure type is no reserved word.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not a reserved word. Do we need to add indexes now or would it be better to wait until we hit performance issues so we know which ones actually work?

Copy link
Contributor

Choose a reason for hiding this comment

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

why would you want to wait? what is an argument of not adding an index?
you know which queries you do and I am not sure if we can identify slow queries.

Migrate: func(tx *gorm.DB) error {

if err := tx.Exec(`
CREATE TABLE transactions(
Copy link
Contributor

Choose a reason for hiding this comment

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

transactions is also often a reserved word - esp in the DB context this can lead to strange things. are we good there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it works, but I also don't like the same terminology being used for 2 different things. I added a note to #299

}
tx.
Table("transactions").
Select("SUM(amount_msat + coalesce(fee_msat, 0) + coalesce(fee_reserve_msat, 0)) as sum").
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need the coalesce ? isn't a fee_msat default of 0 ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently fee_msat is nullable, because we don't always know the fee (for some connectors) and it seems wrong for it to be zero (I think we chatted about this in the past)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd actually add zero in our wallet - imo such exceptions because some connectors will make the app complicated and now we have to do things like coalesce because some connector misses a feature.
and in the next code this coalesce or similar will be forgotten and just causes bugs.

@rolznz rolznz enabled auto-merge (squash) July 19, 2024 16:29
@rolznz rolznz disabled auto-merge July 19, 2024 16:30
@rolznz rolznz merged commit 1391119 into master Jul 19, 2024
6 of 8 checks passed
@rolznz rolznz deleted the feat/dynamic-budgets branch July 19, 2024 16:30
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.

Improve budget calculation Create a common interface for NIP-47 controller methods Dynamic Budgets
3 participants