Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Backup LDK channels when a new channel is opened (#345) #365

Merged
merged 3 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 84 additions & 11 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import (
"sync"
"time"

"github.com/sirupsen/logrus"
"golang.org/x/oauth2"

"github.com/getAlby/nostr-wallet-connect/config"
"github.com/getAlby/nostr-wallet-connect/db"
"github.com/getAlby/nostr-wallet-connect/events"
"github.com/getAlby/nostr-wallet-connect/nip47"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)

type albyOAuthService struct {
Expand Down Expand Up @@ -251,15 +252,15 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er

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

type PayRequest struct {
type payRequest struct {
Invoice string `json:"invoice"`
}

body := bytes.NewBuffer([]byte{})
payload := &PayRequest{
payload := payRequest{
Invoice: invoice,
}
err = json.NewEncoder(body).Encode(payload)
err = json.NewEncoder(body).Encode(&payload)

if err != nil {
svc.logger.WithError(err).Error("Failed to encode request payload")
Expand Down Expand Up @@ -375,6 +376,14 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
return nil
}

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

token, err := svc.fetchUserToken(ctx)
if err != nil {
svc.logger.WithError(err).Error("Failed to fetch user token")
Expand All @@ -392,12 +401,12 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
return err
}

type EventWithPropertiesMap struct {
type eventWithPropertiesMap struct {
Event string `json:"event"`
Properties map[string]interface{} `json:"properties"`
}

var eventWithGlobalProperties EventWithPropertiesMap
var eventWithGlobalProperties eventWithPropertiesMap
err = json.Unmarshal(originalEventBuffer.Bytes(), &eventWithGlobalProperties)
if err != nil {
svc.logger.WithError(err).Error("Failed to decode request payload")
Expand All @@ -418,7 +427,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
}

body := bytes.NewBuffer([]byte{})
err = json.NewEncoder(body).Encode(eventWithGlobalProperties)
err = json.NewEncoder(body).Encode(&eventWithGlobalProperties)

if err != nil {
svc.logger.WithError(err).Error("Failed to encode request payload")
Expand Down Expand Up @@ -453,6 +462,70 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
return nil
}

func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.Event) error {
bkpEvent, ok := event.Properties.(*events.ChannelBackupEvent)
if !ok {
return fmt.Errorf("invalid nwc_backup_channels event properties, could not cast to the expected type: %+v", event.Properties)
}

token, err := svc.fetchUserToken(ctx)
if err != nil {
return fmt.Errorf("failed to fetch user token: %w", err)
}

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

type channelsBackup struct {
Description string `json:"description"`
Data string `json:"data"`
}

channelsData := bytes.NewBuffer([]byte{})
err = json.NewEncoder(channelsData).Encode(bkpEvent.Channels)
if err != nil {
return fmt.Errorf("failed to encode channels backup data: %w", err)
}

// use the encrypted mnemonic as the password to encrypt the backup data
encryptedMnemonic, err := svc.config.Get("Mnemonic", "")
if err != nil {
return fmt.Errorf("failed to fetch encryption key: %w", err)
}

encrypted, err := config.AesGcmEncrypt(channelsData.String(), encryptedMnemonic)
if err != nil {
return fmt.Errorf("failed to encrypt channels backup data: %w", err)
}

body := bytes.NewBuffer([]byte{})
err = json.NewEncoder(body).Encode(&channelsBackup{
Description: "channels",
Data: encrypted,
})
if err != nil {
return fmt.Errorf("failed to encode channels backup request payload: %w", err)
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", svc.appConfig.AlbyAPIURL), body)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

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

resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request to /internal/backups: %w", err)
}

if resp.StatusCode >= 300 {
return fmt.Errorf("request to /internal/backups returned non-success status: %d", resp.StatusCode)
}

return nil
}

func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (string, error) {
token, err := svc.fetchUserToken(ctx)
if err != nil {
Expand All @@ -461,16 +534,16 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri

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

type CreateNWCNodeRequest struct {
type createNWCNodeRequest struct {
WalletPubkey string `json:"wallet_pubkey"`
}

createNodeRequest := CreateNWCNodeRequest{
createNodeRequest := createNWCNodeRequest{
WalletPubkey: svc.config.GetNostrPublicKey(),
}

body := bytes.NewBuffer([]byte{})
err = json.NewEncoder(body).Encode(createNodeRequest)
err = json.NewEncoder(body).Encode(&createNodeRequest)

if err != nil {
svc.logger.WithError(err).Error("Failed to encode request payload")
Expand Down
12 changes: 12 additions & 0 deletions events/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ type PaymentReceivedEventProperties struct {
Amount uint64 `json:"amount"`
NodeType string `json:"node_type"`
}

type ChannelBackupEvent struct {
Channels []ChannelBackupInfo `json:"channels"`
}

type ChannelBackupInfo struct {
ChannelID string `json:"channel_id"`
NodeID string `json:"node_id"`
PeerID string `json:"peer_id"`
ChannelSize uint64 `json:"channel_size"`
FundingTxID string `json:"funding_tx_id"`
}
28 changes: 28 additions & 0 deletions lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,8 @@ func (ls *LDKService) handleLdkEvent(ctx context.Context, event *ldk_node.Event)
"node_type": config.LDKBackendType,
},
})

ls.publishChannelsBackupEvent()
case ldk_node.EventChannelClosed:
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_channel_closed",
Expand All @@ -1144,6 +1146,32 @@ func (ls *LDKService) handleLdkEvent(ctx context.Context, event *ldk_node.Event)
}
}

func (ls *LDKService) publishChannelsBackupEvent() {
ldkChannels := ls.node.ListChannels()
channels := make([]events.ChannelBackupInfo, 0, len(ldkChannels))
for _, ldkChannel := range ldkChannels {
var fundingTx string
if ldkChannel.FundingTxo != nil {
fundingTx = ldkChannel.FundingTxo.Txid
}

channels = append(channels, events.ChannelBackupInfo{
ChannelID: ldkChannel.ChannelId,
NodeID: ls.node.NodeId(),
PeerID: ldkChannel.CounterpartyNodeId,
ChannelSize: ldkChannel.ChannelValueSats,
FundingTxID: fundingTx,
})
}

ls.eventPublisher.Publish(&events.Event{
Event: "nwc_backup_channels",
Properties: &events.ChannelBackupEvent{
Channels: channels,
},
})
}

func (ls *LDKService) GetBalances(ctx context.Context) (*lnclient.BalancesResponse, error) {
onchainBalance, err := ls.GetOnchainBalance(ctx)
if err != nil {
Expand Down
Loading