Skip to content

Commit

Permalink
Feat: dynamic budgets (#226)
Browse files Browse the repository at this point in the history
* feat: add transactions table

* feat: add transactions service with makeinvoice method

* feat: use internal transactions WIP

* feat: check unsettled transactions

* feat: use transactions service in nip47 package WIP

* feat: use transactions service in NIP-47 handlers

* fix: transaction list colors

* fix: tests

* chore: remove old payments table

* fix: return budget usage as sats

* feat: use internal transactions table for keysend payments

* feat: consume nwc_payment_received event in transaction service (WIP)

* feat: update existing transaction from nwc_payment_received event in transactions service

* feat: update existing transaction from nwc_payment_sent event in transactions service

* feat: handle async payment failed events in transactions service

* fix: tests

* feat: correctly implement NIP-47 NOT_FOUND error code

* feat: intercept self payments

* feat: isolated balance and visibility

* feat: validate keysend payment does not exceed app internal balance

* feat: budget check in transactions service, correctly pass payment errors to NIP-47 response

also reduce query duplication

* fix: order transactions when looking up transaction

* feat: add fee reserves to unsettled outgoing transactions

* chore: rename app permissions max amount field

* chore: rename transaction amount values to be clearly millisats

* chore: merge balance type and visibility into isolated property on apps table

* chore: move duplicated permission check from nip-47 controllers to handler

* fix: app name in transaction list

* Feat: permissions revamp v2 (#273)

* feat: revamp permissions component

* chore: changes

* chore: changes

* chore: add view mode for show app screen

* chore: further changes

* feat: new illustration for linking account (#254)

* feat: new illustration for linking account

* fix: update paths

* fix: icon props (#256)

* fix: use date from frontend

* chore: add expiryselect component

* chore: further changes

* chore: further changes

* chore: add date-fns for date picker

* chore: further changes

* chore: further changes

* fix: add central LDK gossip node to help gossip new public channels (#262)

* fix: make dialog responsive (#258)

* chore: further changes

* typo

* chore: spacing issues

* chore: use scopes from capabilities

* chore: change scope type descriptions

* chore: styling fixes

* chore: fix typings

* chore: budget renewal component

* fix: LDK mark channel as inactive and show error if counterparty forwarding info missing (#267)

fix: mark channel as inactive and show error if counterparty forwarding info missing

* chore: remove unnecessary dark classes (#255)

* fix: links to open first channel in sidebar and onboarding checklist (#268)

* feat: improve migrate node UI (#269)

* fix: migrate node copy (#270)

* fix: stop nostr when app is shutdown and use context to stop lnclient (#271)

* fix: permissions revamp WIP

* feat: basic isolated apps UI

* fix: new app connection, edit app connection, deep linking

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com>
Co-authored-by: Michael Bumann <hello@michaelbumann.com>

* fix: make transactions table ID autoincrement

* fix: do not send unrelated notifications to isolated apps

* fix: nip-47 notifications not receiving updated transaction state

* fix: migrate existing tables to have autoincrementing primary keys (#274)

* fix: migrate existing tables to have autoincrementing primary keys

* fix: recreate request and response event tables after apps

* fix: remove null from request_events app_id

* fix: do not crash when reloading app created page

* chore: update pragma commands, add busy_timeout

* fix: remove unnecessary migration of user_configs table

* chore: update comment on autoincrement migration

* feat: sqlite database config improvements

* fix: autoincrement migration to delete unlinked app permissions

* chore: update tests for dynamic budgets (WIP)

* fix: get balance tests

* chore: add extra event handler tests

* chore: add extra event handler test

* chore: add extra multi_pay_invoice tests

* chore: transactions service tests (WIP)

* chore: add transactions service payment tests

* chore: add tests for self payments

* fix: notifications tests

* chore: add notifications tests for transactions service

* chore: add fee reserve tests for transactions service

* chore: add list transactions tests

* chore: add keysend tests for transactions service

* feat: subscribe for payments and invoices (#281)

* 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

* fix: incorrect key on transaction list items

* fix: test

* fix: disable isolated app type on non-supported backends

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com>
Co-authored-by: Michael Bumann <hello@michaelbumann.com>
  • Loading branch information
4 people authored Jul 19, 2024
1 parent b72d044 commit 1391119
Show file tree
Hide file tree
Showing 96 changed files with 5,657 additions and 1,870 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ frontend/src/utils/request.ts
frontend/src/utils/openLink.ts

# generated by rust go bindings for local development
glalby
glalby

*.db-shm
*.db-wal
*.db-journal
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ If the client creates the secret the client only needs to share the public key o
- `budget_renewal` (optional) reset the budget at the end of the given budget renewal. Can be `never` (default), `daily`, `weekly`, `monthly`, `yearly`
- `request_methods` (optional) url encoded, space separated list of request types that you need permission for: `pay_invoice` (default), `get_balance` (see NIP47). For example: `..&request_methods=pay_invoice%20get_balance`
- `notification_types` (optional) url encoded, space separated list of notification types that you need permission for: For example: `..&notification_types=payment_received%20payment_sent`
- `isolated` (optional) makes an isolated app connection with its own balance and only access to its own transaction list. e.g. `&isolated=true`. If using this option, you should not pass any custom request methods or notification types, nor set a budget or expiry.

Example:

Expand Down
66 changes: 50 additions & 16 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import (
"gorm.io/gorm"

"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/transactions"
)

type albyOAuthService struct {
Expand Down Expand Up @@ -273,13 +275,13 @@ func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnc

logger.Logger.WithField("amount", amount).WithError(err).Error("Draining Alby shared wallet funds")

transaction, err := lnClient.MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120)
transaction, err := transactions.NewTransactionsService(svc.db).MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120, lnClient, nil, nil)
if err != nil {
logger.Logger.WithField("amount", amount).WithError(err).Error("Failed to make invoice")
return err
}

err = svc.SendPayment(ctx, transaction.Invoice)
err = svc.SendPayment(ctx, transaction.PaymentRequest)
if err != nil {
logger.Logger.WithField("amount", amount).WithError(err).Error("Failed to pay invoice from shared node")
return err
Expand Down Expand Up @@ -393,7 +395,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
}
notificationTypes := lnClient.GetSupportedNIP47NotificationTypes()
if len(notificationTypes) > 0 {
scopes = append(scopes, permissions.NOTIFICATIONS_SCOPE)
scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
}

app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp(
Expand All @@ -403,6 +405,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
renewal,
nil,
scopes,
false,
)

if err != nil {
Expand All @@ -423,25 +426,58 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
return nil
}

func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) error {
func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
// run non-blocking
go svc.consumeEvent(ctx, event, globalProperties)
}

func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
// TODO: rename this config option to be specific to the alby API
if !svc.cfg.GetEnv().LogEvents {
logger.Logger.WithField("event", event).Debug("Skipped sending to alby events API")
return nil
return
}

if event.Event == "nwc_backup_channels" {
if err := svc.backupChannels(ctx, event); err != nil {
logger.Logger.WithError(err).Error("Failed to backup channels")
return err
}
return nil
return
}

if event.Event == "nwc_payment_received" {
type paymentReceivedEventProperties struct {
PaymentHash string `json:"payment_hash"`
}
// pass a new custom event with less detail
event = &events.Event{
Event: event.Event,
Properties: &paymentReceivedEventProperties{
PaymentHash: event.Properties.(*lnclient.Transaction).PaymentHash,
},
}
}

if event.Event == "nwc_payment_sent" {
type paymentSentEventProperties struct {
PaymentHash string `json:"payment_hash"`
Duration uint64 `json:"duration"`
}

// pass a new custom event with less detail
event = &events.Event{
Event: event.Event,
Properties: &paymentSentEventProperties{
PaymentHash: event.Properties.(*lnclient.Transaction).PaymentHash,
Duration: uint64(*event.Properties.(*lnclient.Transaction).SettledAt - event.Properties.(*lnclient.Transaction).CreatedAt),
},
}
}

token, err := svc.fetchUserToken(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch user token")
return err
return
}

client := svc.oauthConf.Client(ctx, token)
Expand All @@ -452,7 +488,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve

if err != nil {
logger.Logger.WithError(err).Error("Failed to encode request payload")
return err
return
}

type eventWithPropertiesMap struct {
Expand All @@ -464,7 +500,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
err = json.Unmarshal(originalEventBuffer.Bytes(), &eventWithGlobalProperties)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode request payload")
return err
return
}
if eventWithGlobalProperties.Properties == nil {
eventWithGlobalProperties.Properties = map[string]interface{}{}
Expand All @@ -485,13 +521,13 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve

if err != nil {
logger.Logger.WithError(err).Error("Failed to encode request payload")
return err
return
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", svc.cfg.GetEnv().AlbyAPIURL), body)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request /events")
return err
return
}

req.Header.Set("User-Agent", "NWC-next")
Expand All @@ -502,18 +538,16 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
logger.Logger.WithFields(logrus.Fields{
"event": eventWithGlobalProperties,
}).WithError(err).Error("Failed to send request to /events")
return err
return
}

if resp.StatusCode >= 300 {
logger.Logger.WithFields(logrus.Fields{
"event": eventWithGlobalProperties,
"status": resp.StatusCode,
}).Error("Request to /events returned non-success status")
return errors.New("request to /events returned non-success status")
return
}

return nil
}

func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.Event) error {
Expand Down
Loading

0 comments on commit 1391119

Please sign in to comment.