Skip to content

Commit

Permalink
Merge branch 'master' into task-tlv-records
Browse files Browse the repository at this point in the history
  • Loading branch information
im-adithya committed Aug 5, 2024
2 parents 10afe14 + fef85eb commit 65e6752
Show file tree
Hide file tree
Showing 62 changed files with 1,394 additions and 527 deletions.
14 changes: 0 additions & 14 deletions .do/deploy.template.yaml

This file was deleted.

6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# only enable event logging in production
LOG_EVENTS=false

#WORK_DIR=.data
#DATABASE_URI=nwc.db
#NOSTR_PRIVKEY=
Expand All @@ -8,9 +11,6 @@
#FRONTEND_URL=http://localhost:5173
#AUTO_LINK_ALBY_ACCOUNT=false

# set LDK_GOSSIP_SOURCE as empty to not use RGS
#LDK_GOSSIP_SOURCE=

# Alby OAuth configuration
#ALBY_OAUTH_CLIENT_SECRET=
#ALBY_OAUTH_CLIENT_ID=
Expand Down
58 changes: 37 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ Breez SDK requires gcc to build the Breez bindings. Run `choco install mingw` an

## Optional configuration parameters

The following configuration options can be set as environment variables or in a .env file

- `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"
Expand Down Expand Up @@ -331,17 +333,31 @@ Run NWC on your own node!

## Deploy it yourself

### Digital Ocean
### From the release

[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/getAlby/hub/tree/master)
Download and run the executable.

### Render
Have a look at the [configuration options](#optional-configuration-parameters)

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/getAlby/hub)
```bash
wget https://getalby.com/install/hub/server-linux-x86_64.tar.bz2
tar -xvjf server-linux-x86_64.tar.bz2

# run Alby Hub and done!
./bin/albyhub
```

### Fly.io

Make sure to have the [fly command line tools installed ](https://fly.io/docs/hands-on/install-flyctl/)

### Fly
```bash
wget https://getalby.com/install/hub/fly.toml
fly launch
fly apps open
```

- [install fly](https://fly.io/docs/hands-on/install-flyctl/)
Or manually:
- update `app = 'nwc'` on **line 6** to a unique name in fly.toml e.g. `app = 'nwc-john-doe-1234'`
- run `fly launch`
- press 'y' to copy configuration to the new app and then hit enter
Expand All @@ -362,17 +378,6 @@ LDK logs:

- `fly machine exec "tail -100 data/ldk/logs/ldk_node_latest.log"`

### Custom Ubuntu VM

- install go (using snap)
- install build-essential
- install nvm (curl script)
- with nvm, choose node lts
- install yarn (via npm)
- run `(cd frontend && yarn install`
- run `(cd frontend && yarn build:http)`
- run `go run cmd/http/main.go`

### Docker

#### From Alby's Container Registry
Expand All @@ -381,14 +386,25 @@ _Tested on Linux only_

`docker run -v ~/.local/share/albyhub:/data -e WORK_DIR='/data' -p 8080:8080 ghcr.io/getalby/hub:latest`

#### From Source
##### Build the image locally

_Tested on Linux only_
`docker run -v ~/.local/share/albyhub:/data -e WORK_DIR='/data' -p 8080:8080 $(docker build -q .)`

##### Docker Compose

In this repository. Or manually download the docker-compose.yml file and then run:

`docker compose up`

##### Manually
#### From source

`docker run -v ~/.local/share/albyhub:/data -e WORK_DIR='/data' -p 8080:8080 $(docker build -q .)`
- install go (e.g. using snap)
- install build-essential
- install yarn
- run `(cd frontend && yarn install`
- run `(cd frontend && yarn build:http)`
- run `go run cmd/http/main.go`

### Render.com

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/getAlby/hub)
65 changes: 63 additions & 2 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const (
userIdentifierKey = "AlbyUserIdentifier"
)

const ALBY_ACCOUNT_APP_NAME = "getalby.com"

func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *albyOAuthService {
conf := &oauth2.Config{
ClientID: cfg.GetEnv().AlbyClientId,
Expand Down Expand Up @@ -290,7 +292,7 @@ func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnc

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

transaction, err := transactions.NewTransactionsService(svc.db).MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120, lnClient, nil, nil)
transaction, err := transactions.NewTransactionsService(svc.db).MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120, nil, lnClient, nil, nil)
if err != nil {
logger.Logger.WithField("amount", amount).WithError(err).Error("Failed to make invoice")
return err
Expand Down Expand Up @@ -395,7 +397,24 @@ func (svc *albyOAuthService) GetAuthUrl() string {
return svc.oauthConf.AuthCodeURL("unused")
}

func (svc *albyOAuthService) UnlinkAccount(ctx context.Context) error {
err := svc.destroyAlbyAccountNWCNode(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to destroy Alby Account NWC node")
}
svc.deleteAlbyAccountApps()

svc.cfg.SetUpdate(userIdentifierKey, "", "")
svc.cfg.SetUpdate(accessTokenKey, "", "")
svc.cfg.SetUpdate(accessTokenExpiryKey, "", "")
svc.cfg.SetUpdate(refreshTokenKey, "", "")

return nil
}

func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error {
svc.deleteAlbyAccountApps()

connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create alby account nwc node")
Expand All @@ -413,7 +432,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
}

app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp(
"getalby.com",
ALBY_ACCOUNT_APP_NAME,
connectionPubkey,
budget,
renewal,
Expand Down Expand Up @@ -714,6 +733,40 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri
return responsePayload.Pubkey, nil
}

func (svc *albyOAuthService) destroyAlbyAccountNWCNode(ctx context.Context) error {
token, err := svc.fetchUserToken(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch user token")
}

client := svc.oauthConf.Client(ctx, token)

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

setDefaultRequestHeaders(req)

resp, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to send request to /internal/nwcs")
return err
}

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

logger.Logger.Info("Removed alby account nwc node successfully")

return nil
}

func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) error {
token, err := svc.fetchUserToken(ctx)
if err != nil {
Expand Down Expand Up @@ -1049,3 +1102,11 @@ func setDefaultRequestHeaders(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
}

func (svc *albyOAuthService) deleteAlbyAccountApps() {
// delete any existing getalby.com connections so when re-linking the user only has one
err := svc.db.Where("name = ?", ALBY_ACCOUNT_APP_NAME).Delete(&db.App{}).Error
if err != nil {
logger.Logger.WithError(err).Error("Failed to delete Alby Account apps")
}
}
1 change: 1 addition & 0 deletions alby/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type AlbyOAuthService interface {
GetMe(ctx context.Context) (*AlbyMe, error)
SendPayment(ctx context.Context, invoice string) error
DrainSharedWallet(ctx context.Context, lnClient lnclient.LNClient) error
UnlinkAccount(ctx context.Context) error
RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error)
}

Expand Down
37 changes: 35 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,44 @@ func (api *api) ListApps() ([]App, error) {
return apiApps, nil
}

func (api *api) ListChannels(ctx context.Context) ([]lnclient.Channel, error) {
func (api *api) ListChannels(ctx context.Context) ([]Channel, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
return api.svc.GetLNClient().ListChannels(ctx)
channels, err := api.svc.GetLNClient().ListChannels(ctx)
if err != nil {
return nil, err
}

apiChannels := []Channel{}
for _, channel := range channels {
status := "offline"
if channel.Active {
status = "online"
} else if channel.Confirmations != nil && channel.ConfirmationsRequired != nil && *channel.ConfirmationsRequired > *channel.Confirmations {
status = "opening"
}

apiChannels = append(apiChannels, Channel{
LocalBalance: channel.LocalBalance,
LocalSpendableBalance: channel.LocalSpendableBalance,
RemoteBalance: channel.RemoteBalance,
Id: channel.Id,
RemotePubkey: channel.RemotePubkey,
FundingTxId: channel.FundingTxId,
Active: channel.Active,
Public: channel.Public,
InternalChannel: channel.InternalChannel,
Confirmations: channel.Confirmations,
ConfirmationsRequired: channel.ConfirmationsRequired,
ForwardingFeeBaseMsat: channel.ForwardingFeeBaseMsat,
UnspendablePunishmentReserve: channel.UnspendablePunishmentReserve,
CounterpartyUnspendablePunishmentReserve: channel.CounterpartyUnspendablePunishmentReserve,
Error: channel.Error,
Status: status,
})
}
return apiChannels, nil
}

func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) {
Expand Down
21 changes: 20 additions & 1 deletion api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type API interface {
DeleteApp(userApp *db.App) error
GetApp(userApp *db.App) *App
ListApps() ([]App, error)
ListChannels(ctx context.Context) ([]lnclient.Channel, error)
ListChannels(ctx context.Context) ([]Channel, error)
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
ResetRouter(key string) error
ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error
Expand Down Expand Up @@ -285,3 +285,22 @@ type WalletCapabilitiesResponse struct {
Methods []string `json:"methods"`
NotificationTypes []string `json:"notificationTypes"`
}

type Channel struct {
LocalBalance int64 `json:"localBalance"`
LocalSpendableBalance int64 `json:"localSpendableBalance"`
RemoteBalance int64 `json:"remoteBalance"`
Id string `json:"id"`
RemotePubkey string `json:"remotePubkey"`
FundingTxId string `json:"fundingTxId"`
Active bool `json:"active"`
Public bool `json:"public"`
InternalChannel interface{} `json:"internalChannel"`
Confirmations *uint32 `json:"confirmations"`
ConfirmationsRequired *uint32 `json:"confirmationsRequired"`
ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"`
UnspendablePunishmentReserve uint64 `json:"unspendablePunishmentReserve"`
CounterpartyUnspendablePunishmentReserve uint64 `json:"counterpartyUnspendablePunishmentReserve"`
Error *string `json:"error"`
Status string `json:"status"`
}
2 changes: 1 addition & 1 deletion api/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func (api *api) CreateInvoice(ctx context.Context, amount int64, description str
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amount, description, "", 0, api.svc.GetLNClient(), nil, nil)
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amount, description, "", 0, nil, api.svc.GetLNClient(), nil, nil)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion config/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type AppConfig struct {
AlbyOAuthAuthUrl string `envconfig:"ALBY_OAUTH_AUTH_URL" default:"https://getalby.com/oauth"`
BaseUrl string `envconfig:"BASE_URL" default:"http://localhost:8080"`
FrontendUrl string `envconfig:"FRONTEND_URL"`
LogEvents bool `envconfig:"LOG_EVENTS" default:"false"`
LogEvents bool `envconfig:"LOG_EVENTS" default:"true"`
AutoLinkAlbyAccount bool `envconfig:"AUTO_LINK_ALBY_ACCOUNT" default:"true"`
PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS"`
PhoenixdAuthorization string `envconfig:"PHOENIXD_AUTHORIZATION"`
Expand Down
6 changes: 6 additions & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ const (
SIGN_MESSAGE_SCOPE = "sign_message"
NOTIFICATIONS_SCOPE = "notifications" // covers all notification types
)

// limit encoded metadata length, otherwise relays may have trouble listing multiple transactions
// given a relay limit of 512000 bytes and ideally being able to list 50 transactions,
// each transaction would have to have a maximum size of 10240
// accounting for encryption and other metadata in the response, this is set to 2048 characters
const INVOICE_METADATA_MAX_LENGTH = 2048
26 changes: 13 additions & 13 deletions events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ func NewEventPublisher() *eventPublisher {
return eventPublisher
}

func (el *eventPublisher) RegisterSubscriber(listener EventSubscriber) {
el.subscriberMtx.Lock()
defer el.subscriberMtx.Unlock()
el.listeners = append(el.listeners, listener)
func (ep *eventPublisher) RegisterSubscriber(listener EventSubscriber) {
ep.subscriberMtx.Lock()
defer ep.subscriberMtx.Unlock()
ep.listeners = append(ep.listeners, listener)
}

func (el *eventPublisher) RemoveSubscriber(listenerToRemove EventSubscriber) {
el.subscriberMtx.Lock()
defer el.subscriberMtx.Unlock()
func (ep *eventPublisher) RemoveSubscriber(listenerToRemove EventSubscriber) {
ep.subscriberMtx.Lock()
defer ep.subscriberMtx.Unlock()

for i, listener := range el.listeners {
for i, listener := range ep.listeners {
// delete the listener from the listeners array
if listener == listenerToRemove {
el.listeners[i] = el.listeners[len(el.listeners)-1]
el.listeners = slices.Delete(el.listeners, len(el.listeners)-1, len(el.listeners))
ep.listeners[i] = ep.listeners[len(ep.listeners)-1]
ep.listeners = slices.Delete(ep.listeners, len(ep.listeners)-1, len(ep.listeners))
break
}
}
Expand All @@ -46,14 +46,14 @@ func (el *eventPublisher) RemoveSubscriber(listenerToRemove EventSubscriber) {
func (ep *eventPublisher) Publish(event *Event) {
ep.subscriberMtx.Lock()
defer ep.subscriberMtx.Unlock()
logger.Logger.WithFields(logrus.Fields{"event": event}).Info("Publishing event")
logger.Logger.WithFields(logrus.Fields{"event": event, "global": ep.globalProperties}).Info("Publishing event")
for _, listener := range ep.listeners {
// events are consumed in sequence as some listeners depend on earlier consumers
// (e.g. NIP-47 notifier depends on transactions service updating transactions)
listener.ConsumeEvent(context.Background(), event, ep.globalProperties)
}
}

func (el *eventPublisher) SetGlobalProperty(key string, value interface{}) {
el.globalProperties[key] = value
func (ep *eventPublisher) SetGlobalProperty(key string, value interface{}) {
ep.globalProperties[key] = value
}
Loading

0 comments on commit 65e6752

Please sign in to comment.