create_connection_controller.go raw

   1  package controllers
   2  
   3  import (
   4  	"context"
   5  	"slices"
   6  	"time"
   7  
   8  	"github.com/getAlby/go-nostr"
   9  	"github.com/getAlby/hub/alby"
  10  	"github.com/getAlby/hub/constants"
  11  	"github.com/getAlby/hub/logger"
  12  	"github.com/getAlby/hub/nip47/models"
  13  	"github.com/getAlby/hub/nip47/permissions"
  14  	"github.com/sirupsen/logrus"
  15  )
  16  
  17  type createConnectionParams struct {
  18  	Pubkey            string                 `json:"pubkey"` // pubkey of the app connection
  19  	Name              string                 `json:"name"`
  20  	RequestMethods    []string               `json:"request_methods"`
  21  	NotificationTypes []string               `json:"notification_types"`
  22  	MaxAmount         uint64                 `json:"max_amount"`
  23  	BudgetRenewal     string                 `json:"budget_renewal"`
  24  	ExpiresAt         *uint64                `json:"expires_at"` // unix timestamp
  25  	Isolated          bool                   `json:"isolated"`
  26  	Metadata          map[string]interface{} `json:"metadata,omitempty"`
  27  }
  28  
  29  type createConnectionResponse struct {
  30  	// pubkey is given, user requesting already knows relay.
  31  	WalletPubkey string `json:"wallet_pubkey"`
  32  }
  33  
  34  func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, publishResponse publishFunc) {
  35  	params := &createConnectionParams{}
  36  	resp := decodeRequest(nip47Request, params)
  37  	if resp != nil {
  38  		publishResponse(resp, nostr.Tags{})
  39  		return
  40  	}
  41  
  42  	logger.Logger.WithFields(logrus.Fields{
  43  		"request_event_id": requestEventId,
  44  		"params":           params,
  45  	}).Info("creating app")
  46  
  47  	var expiresAt *time.Time
  48  	if params.ExpiresAt != nil {
  49  		expiresAtUnsigned := *params.ExpiresAt
  50  		expiresAtValue := time.Unix(int64(expiresAtUnsigned), 0)
  51  		expiresAt = &expiresAtValue
  52  	}
  53  
  54  	maxAmountSat := params.MaxAmount / 1000
  55  
  56  	if params.Name == alby.ALBY_ACCOUNT_APP_NAME {
  57  		publishResponse(&models.Response{
  58  			ResultType: nip47Request.Method,
  59  			Error: &models.Error{
  60  				Code:    constants.ERROR_BAD_REQUEST,
  61  				Message: "cannot create a new app that has reserved name: " + alby.ALBY_ACCOUNT_APP_NAME,
  62  			},
  63  		}, nostr.Tags{})
  64  		return
  65  	}
  66  
  67  	// explicitly do not allow creating an app with create_connection permission
  68  	if slices.Contains(params.RequestMethods, models.CREATE_CONNECTION_METHOD) {
  69  		publishResponse(&models.Response{
  70  			ResultType: nip47Request.Method,
  71  			Error: &models.Error{
  72  				Code:    constants.ERROR_BAD_REQUEST,
  73  				Message: "cannot create a new app that has create_connection permission via NWC",
  74  			},
  75  		}, nostr.Tags{})
  76  		return
  77  	}
  78  
  79  	// ensure there is at least one request method
  80  	if len(params.RequestMethods) == 0 {
  81  		publishResponse(&models.Response{
  82  			ResultType: nip47Request.Method,
  83  			Error: &models.Error{
  84  				Code:    constants.ERROR_BAD_REQUEST,
  85  				Message: "No request methods provided",
  86  			},
  87  		}, nostr.Tags{})
  88  		return
  89  	}
  90  
  91  	supportedMethods := controller.lnClient.GetSupportedNIP47Methods()
  92  	if slices.ContainsFunc(params.RequestMethods, func(method string) bool {
  93  		return !slices.Contains(supportedMethods, method)
  94  	}) {
  95  		publishResponse(&models.Response{
  96  			ResultType: nip47Request.Method,
  97  			Error: &models.Error{
  98  				Code:    constants.ERROR_BAD_REQUEST,
  99  				Message: "One or more methods are not supported by the current LNClient",
 100  			},
 101  		}, nostr.Tags{})
 102  		return
 103  	}
 104  
 105  	scopes, err := permissions.RequestMethodsToScopes(params.RequestMethods)
 106  
 107  	if err != nil {
 108  		logger.Logger.WithFields(logrus.Fields{
 109  			"request_event_id": requestEventId,
 110  		}).WithError(err).Error("Failed to convert request methods to scopes")
 111  		publishResponse(&models.Response{
 112  			ResultType: nip47Request.Method,
 113  			Error:      mapNip47Error(err),
 114  		}, nostr.Tags{})
 115  		return
 116  	}
 117  
 118  	supportedNotificationTypes := controller.lnClient.GetSupportedNIP47NotificationTypes()
 119  	if len(params.NotificationTypes) > 0 {
 120  		if slices.ContainsFunc(params.NotificationTypes, func(method string) bool {
 121  			return !slices.Contains(supportedNotificationTypes, method)
 122  		}) {
 123  			publishResponse(&models.Response{
 124  				ResultType: nip47Request.Method,
 125  				Error: &models.Error{
 126  					Code:    constants.ERROR_BAD_REQUEST,
 127  					Message: "One or more notification types are not supported by the current LNClient",
 128  				},
 129  			}, nostr.Tags{})
 130  			return
 131  		}
 132  		scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
 133  	}
 134  
 135  	app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, maxAmountSat, params.BudgetRenewal, expiresAt, scopes, params.Isolated, params.Metadata)
 136  	if err != nil {
 137  		logger.Logger.WithFields(logrus.Fields{
 138  			"request_event_id": requestEventId,
 139  		}).WithError(err).Error("Failed to create app")
 140  		publishResponse(&models.Response{
 141  			ResultType: nip47Request.Method,
 142  			Error:      mapNip47Error(err),
 143  		}, nostr.Tags{})
 144  		return
 145  	}
 146  
 147  	responsePayload := createConnectionResponse{
 148  		WalletPubkey: *app.WalletPubkey,
 149  	}
 150  
 151  	publishResponse(&models.Response{
 152  		ResultType: nip47Request.Method,
 153  		Result:     responsePayload,
 154  	}, nostr.Tags{})
 155  }
 156