Skip to content

Commit

Permalink
Merge branch 'feature/breez-backend' into server-self-host
Browse files Browse the repository at this point in the history
* feature/breez-backend: (38 commits)
  chore: add dependabot
  feat: add support both internal and public relay URL
  fix: relay public url
  fix: expiresAt and maxAmount handling
  fix: app expiresAt handling
  fix: style for darkmode
  chore: correctly handle query parameters in new UI (WIP)
  chore: rename app name parameter (#154)
  fix: navbar padding on lg
  fix: ui cleanup
  no more default protocol-redirect (#107)
  ui improvements & layout simplification (#153)
  feat: nwc connection page ui (#151)
  chore: address migration feedback
  chore: format with prettier
  feat: run css scripts via npm
  chore: add human-readable name to migration ids
  feat: add manual migrations using gormigration
  fix: don't log resp id
  fix: convert expiresAt to int
  ...
  • Loading branch information
bumi committed Nov 24, 2023
2 parents 929ad8b + 8da01ac commit a9b7ee7
Show file tree
Hide file tree
Showing 47 changed files with 1,588 additions and 862 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ ALBY_CLIENT_ID=
OAUTH_REDIRECT_URL=http://localhost:8080/alby/callback
COOKIE_SECRET=secretsecret
RELAY=wss://relay.getalby.com/v1
PUBLIC_RELAY=
PORT=8080
6 changes: 6 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
views/apps/show.html
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ As data storage SQLite or PostgreSQL (recommended) can be used.

## Development

`go run .`
`go run .` or `gow -e=go,mod,html,css run .` using [gow](https://github.com/mitranim/gow)

To build the CSS run:

`npx tailwindcss -i ./views/application.css -o ./public/css/application.css --watch`
1. `npm install`
2. `npm run css`

### Testing

Expand All @@ -44,6 +45,7 @@ To build the CSS run:
- `NOSTR_PRIVKEY`: the private key of this service. Should be a securely randomly generated 32 byte hex string.
- `CLIENT_NOSTR_PUBKEY`: if set, this service will only listen to events authored by this public key. You can set this to your own nostr public key.
- `RELAY`: default: "wss://relay.getalby.com/v1"
- `PUBLIC_RELAY`: optional relay URL to be used in connection strings if `RELAY` is an internal URL
- `LN_BACKEND_TYPE`: ALBY or LND
- `ALBY_CLIENT_SECRET`= Alby OAuth client secret (used with the Alby backend)
- `ALBY_CLIENT_ID`= Alby OAuth client ID (used with the Alby backend)
Expand All @@ -66,28 +68,27 @@ The default option is that the NWC app creates a secret and the user uses the no

##### Query parameter options

- `c`: the name of the client app
- `name`: the name of the client app

Example:

`/apps/new?c=myapp`
`/apps/new?name=myapp`

#### Client created secret
If the client creates the secret the client only needs to share the public key of that secret for authorization. The user authorized that pubkey and no sensitivate data needs to be shared.

##### Query parameter options for /new
- `c`: the name of the client app
- `name`: the name of the client app
- `pubkey`: the public key of the client's secret for the user to authorize
- `return_to`: (optional) if a `return_to` URL is provided the user will be redirected to that URL after authorization. The `lud16`, `relay` and `pubkey` query parameters will be added to the URL.
- `expires_at` (optional) connection cannot be used after this date. Unix timestamp in seconds.
- `max_amount` (optional) maximum amount in sats that can be sent per renewal period
- `budget_renewal` (optional) reset the budget at the end of the given budget renewal. Can be `never` (default), `daily`, `weekly`, `monthly`, `yearly`
- `editable` (optional) set to `false` to disable form editing by the user
- `request_methods` (optional) url encoded, space seperated list of request types that you need permission for: `pay_invoice` (default), `get_balance` (see NIP47). For example: `..&request_methods=pay_invoice%20get_balance`
- `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`

Example:

`/apps/new?c=myapp&pubkey=47c5a21...&return_to=https://example.com`
`/apps/new?name=myapp&pubkey=47c5a21...&return_to=https://example.com`

#### Web-flow: client created secret
Web clients can open a new prompt popup to load the authorization page.
Expand All @@ -106,7 +107,8 @@ await nwc.initNWC({name: 'myapp'});

## Help

If you need help contact hello@getalby.com or reach out on Nostr: npub1getal6ykt05fsz5nqu4uld09nfj3y3qxmv8crys4aeut53unfvlqr80nfm
If you need help contact support@getalby.com or reach out on Nostr: npub1getal6ykt05fsz5nqu4uld09nfj3y3qxmv8crys4aeut53unfvlqr80nfm
You can also visit the chat of our Community on [Telegram](https://t.me/getalby).


## ⚡️Donations
Expand Down
148 changes: 114 additions & 34 deletions alby.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin
"description": description,
"descriptionHash": descriptionHash,
"expiry": expiry,
}).Errorf("Value must be 1 sat or greater")
return "", "", errors.New("Value must be 1 sat or greater")
}).Errorf("amount must be 1000 msat or greater");
return "", "", errors.New("amount must be 1000 msat or greater")
}

svc.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -174,21 +174,99 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin
"paymentHash": responsePayload.PaymentHash,
}).Info("Make invoice successful")
return responsePayload.PaymentRequest, responsePayload.PaymentHash, nil
} else {
errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"amount": amount,
"description": description,
"descriptionHash": descriptionHash,
"expiry": expiry,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Make invoice failed %s", string(errorPayload.Message))
return "", "", errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) {
// TODO: move to a shared function
app := App{}
err = svc.db.Preload("User").First(&app, &App{
NostrPubkey: senderPubkey,
}).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"amount": amount,
"description": description,
"descriptionHash": descriptionHash,
"expiry": expiry,
"paymentHash": paymentHash,
}).Errorf("App not found: %v", err)
return "", false, err
}

svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
}).Info("Processing lookup invoice request")
tok, err := svc.FetchUserToken(ctx, app)
if err != nil {
return "", false, err
}
client := svc.oauthConf.Client(ctx, tok)

body := bytes.NewBuffer([]byte{})

// TODO: move to a shared function
req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.AlbyAPIURL, paymentHash), body)
if err != nil {
svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s", paymentHash)
return "", false, err
}

req.Header.Set("User-Agent", "NWC")
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
}).Errorf("Failed to lookup invoice: %v", err)
return "", false, err
}

if resp.StatusCode < 300 {
responsePayload := &LookupInvoiceResponse{}
err = json.NewDecoder(resp.Body).Decode(responsePayload)
if err != nil {
return "", false, err
}
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Make invoice failed %s", string(errorPayload.Message))
return "", "", errors.New(errorPayload.Message)
"paymentRequest": responsePayload.PaymentRequest,
"settled": responsePayload.Settled,
}).Info("Lookup invoice successful")
return responsePayload.PaymentRequest, responsePayload.Settled, nil
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Lookup invoice failed %s", string(errorPayload.Message))
return "", false, errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) {
Expand Down Expand Up @@ -238,17 +316,17 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string
"userId": app.User.ID,
}).Info("Balance fetch successful")
return int64(responsePayload.Balance), nil
} else {
errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Balance fetch failed %s", string(errorPayload.Message))
return 0, errors.New(errorPayload.Message)
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Balance fetch failed %s", string(errorPayload.Message))
return 0, errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, payReq string) (preimage string, err error) {
Expand Down Expand Up @@ -312,23 +390,25 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey,
"bolt11": payReq,
"appId": app.ID,
"userId": app.User.ID,
"paymentHash": responsePayload.PaymentHash,
}).Info("Payment successful")
return responsePayload.Preimage, nil
} else {
errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"bolt11": payReq,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Payment failed %s", string(errorPayload.Message))
return "", errors.New(errorPayload.Message)
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"bolt11": payReq,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Payment failed %s", string(errorPayload.Message))
return "", errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) AuthHandler(c echo.Context) error {
appName := c.QueryParam("c") // c - for client
// clear current session
sess, _ := session.Get(CookieName, c)
if sess.Values["user_id"] != nil {
Expand All @@ -341,7 +421,7 @@ func (svc *AlbyOAuthService) AuthHandler(c echo.Context) error {
sess.Save(c.Request(), c.Response())
}

url := svc.oauthConf.AuthCodeURL("")
url := svc.oauthConf.AuthCodeURL(appName) // pass on the appName as state
return c.Redirect(302, url)
}

Expand Down
5 changes: 5 additions & 0 deletions breez/breez.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package breez

import (
"context"
"errors"
"log"
"os"
"path/filepath"
Expand Down Expand Up @@ -76,3 +77,7 @@ func (bs *BreezService) MakeInvoice(ctx context.Context, senderPubkey string, am
}
return resp.Bolt11, resp.PaymentHash, nil
}

func (bs *BreezService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) {
return "", false, errors.New("Not implemented")
}
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Config struct {
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
ClientPubkey string `envconfig:"CLIENT_NOSTR_PUBKEY"`
Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
PublicRelay string `envconfig:"PUBLIC_RELAY"`
LNBackendType string `envconfig:"LN_BACKEND_TYPE" default:"ALBY"`
LNDAddress string `envconfig:"LND_ADDRESS"`
LNDCertFile string `envconfig:"LND_CERT_FILE"`
Expand Down
Loading

0 comments on commit a9b7ee7

Please sign in to comment.