diff --git a/README.md b/README.md index 628ae67b..1916457f 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `pay_invoice` +✅ `pay_keysend` + ⚠️ `make_invoice` - ⚠️ invoice in response missing (TODO) @@ -138,8 +140,6 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light - ⚠️ invoice in response missing (TODO) - ⚠️ response does not match spec, missing fields -❌ `pay_keysend` - ❌ `list_transactions` ❌ `multi_pay_invoice (TBC)` @@ -159,6 +159,9 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `pay_invoice` +✅ `pay_keysend` +- ⚠️ preimage in request not supported + ⚠️ `make_invoice` - ⚠️ expiry in request not supported - ⚠️ invoice in response missing (TODO) @@ -167,8 +170,6 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light - ⚠️ invoice in response missing (TODO) - ⚠️ response does not match spec, missing fields (TODO) -❌ `pay_keysend` - ❌ `list_transactions` ❌ `multi_pay_invoice (TBC)` diff --git a/alby.go b/alby.go index 874748fe..3e772d95 100644 --- a/alby.go +++ b/alby.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -435,6 +436,93 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, return "", errors.New(errorPayload.Message) } +func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + }).Errorf("App not found: %v", err) + return "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing keysend request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return "", err + } + client := svc.oauthConf.Client(ctx, tok) + + customRecordsMap := make(map[string]string) + for _, record := range custom_records { + customRecordsMap[strconv.FormatUint(record.Type, 10)] = record.Value + } + + body := bytes.NewBuffer([]byte{}) + payload := &KeysendRequest{ + Amount: amount, + Destination: destination, + CustomRecords: customRecordsMap, + } + err = json.NewEncoder(body).Encode(payload) + + // here we don't use the preimage from params + req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/keysend", svc.cfg.AlbyAPIURL), body) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /payments/keysend") + return "", 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, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to pay keysend: %v", err) + return "", err + } + + if resp.StatusCode < 300 { + responsePayload := &PayResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "appId": app.ID, + "userId": app.User.ID, + "preimage": responsePayload.Preimage, + "paymentHash": responsePayload.PaymentHash, + }).Info("Keysend payment successful") + return responsePayload.Preimage, nil + } + + errorPayload := &ErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "payeePubkey": destination, + "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 diff --git a/handle_balance_request.go b/handle_balance_request.go index 036fddbc..c39f7ebb 100644 --- a/handle_balance_request.go +++ b/handle_balance_request.go @@ -25,7 +25,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_info_request.go b/handle_info_request.go index 799eed18..9864bff5 100644 --- a/handle_info_request.go +++ b/handle_info_request.go @@ -21,7 +21,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques return nil, err } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_lookup_invoice_request.go b/handle_lookup_invoice_request.go index 8db69f54..428fb77d 100644 --- a/handle_lookup_invoice_request.go +++ b/handle_lookup_invoice_request.go @@ -24,7 +24,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47 } // TODO: move to a shared function - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_make_invoice_request.go b/handle_make_invoice_request.go index 7e3d0733..6a84dc5b 100644 --- a/handle_make_invoice_request.go +++ b/handle_make_invoice_request.go @@ -26,7 +26,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re } // TODO: move to a shared function - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, 0) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/handle_pay_keysend_request.go b/handle_pay_keysend_request.go new file mode 100644 index 00000000..e853f32c --- /dev/null +++ b/handle_pay_keysend_request.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +func (svc *Service) HandlePayKeysendEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) { + + nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"} + err = svc.db.Create(&nostrEvent).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("Failed to save nostr event: %v", err) + return nil, err + } + + payParams := &Nip47KeysendParams{} + err = json.Unmarshal(request.Params, payParams) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("Failed to decode nostr event: %v", err) + return nil, err + } + + // We use pay_invoice permissions for budget and max amount + hasPermission, code, message := svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, payParams.Amount) + + if !hasPermission { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "senderPubkey": payParams.Pubkey, + }).Errorf("App does not have permission: %s %s", code, message) + + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: code, + Message: message, + }}, ss) + } + + payment := Payment{App: app, NostrEvent: nostrEvent, Amount: uint(payParams.Amount / 1000)} + insertPaymentResult := svc.db.Create(&payment) + if insertPaymentResult.Error != nil { + return nil, insertPaymentResult.Error + } + + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "senderPubkey": payParams.Pubkey, + }).Info("Sending payment") + + preimage, err := svc.lnClient.SendKeysend(ctx, event.PubKey, payParams.Amount/1000, payParams.Pubkey, payParams.Preimage, payParams.TLVRecords) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + "senderPubkey": payParams.Pubkey, + }).Infof("Failed to send payment: %v", err) + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR + svc.db.Save(&nostrEvent) + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: NIP_47_ERROR_INTERNAL, + Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()), + }, + }, ss) + } + payment.Preimage = &preimage + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED + svc.db.Save(&nostrEvent) + svc.db.Save(&payment) + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Result: Nip47PayResponse{ + Preimage: preimage, + }, + }, ss) +} diff --git a/handle_payment_request.go b/handle_payment_request.go index 516e28c5..43851c40 100644 --- a/handle_payment_request.go +++ b/handle_payment_request.go @@ -54,7 +54,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req }, ss) } - hasPermission, code, message := svc.hasPermission(&app, event, request.Method, &paymentRequest) + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi) if !hasPermission { svc.Logger.WithFields(logrus.Fields{ diff --git a/lnd.go b/lnd.go index f0855e32..6c3a9db1 100644 --- a/lnd.go +++ b/lnd.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/rand" + "crypto/sha256" "encoding/hex" "errors" @@ -17,6 +19,7 @@ import ( type LNClient interface { SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) + SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) @@ -119,6 +122,110 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, senderPubkey, payReq return hex.EncodeToString(resp.PaymentPreimage), nil } +func (svc *LNDService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (respPreimage string, err error) { + destBytes, err := hex.DecodeString(destination) + if err != nil { + return "", err + } + var preImageBytes []byte + + if preimage == "" { + preImageBytes, err = makePreimageHex() + preimage = hex.EncodeToString(preImageBytes) + } else { + preImageBytes, err = hex.DecodeString(preimage) + } + if err != nil || len(preImageBytes) != 32 { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "destination": destination, + "preimage": preimage, + "customRecords": custom_records, + "error": err, + }).Errorf("Invalid preimage") + return "", err + } + + paymentHash := sha256.New() + paymentHash.Write(preImageBytes) + paymentHashBytes := paymentHash.Sum(nil) + paymentHashHex := hex.EncodeToString(paymentHashBytes) + + destCustomRecords := map[uint64][]byte{} + for _, record := range custom_records { + destCustomRecords[record.Type] = []byte(record.Value) + } + const KEYSEND_CUSTOM_RECORD = 5482373484 + destCustomRecords[KEYSEND_CUSTOM_RECORD] = preImageBytes + sendPaymentRequest := &lnrpc.SendRequest{ + Dest: destBytes, + Amt: amount, + PaymentHash: paymentHashBytes, + DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, + DestCustomRecords: destCustomRecords, + } + + resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "error": err, + }).Errorf("Failed to send keysend payment") + return "", err + } + if resp.PaymentError != "" { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "paymentError": resp.PaymentError, + }).Errorf("Keysend payment has payment error") + return "", errors.New(resp.PaymentError) + } + respPreimage = hex.EncodeToString(resp.PaymentPreimage) + if respPreimage == "" { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "paymentError": resp.PaymentError, + }).Errorf("No preimage in keysend response") + return "", errors.New("No preimage in keysend response") + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "payeePubkey": destination, + "paymentHash": paymentHashHex, + "preimage": preimage, + "customRecords": custom_records, + "respPreimage": respPreimage, + }).Info("Keysend payment successful") + + return respPreimage, nil +} + +func makePreimageHex() ([]byte, error) { + bytes := make([]byte, 32) // 32 bytes * 8 bits/byte = 256 bits + _, err := rand.Read(bytes) + if err != nil { + return nil, err + } + return bytes, nil +} + func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LNDService, err error) { lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ Address: svc.cfg.LNDAddress, diff --git a/models.go b/models.go index c1cc07c2..975be2c3 100644 --- a/models.go +++ b/models.go @@ -16,6 +16,7 @@ const ( NIP_47_GET_INFO_METHOD = "get_info" NIP_47_MAKE_INVOICE_METHOD = "make_invoice" NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice" + NIP_47_PAY_KEYSEND_METHOD = "pay_keysend" NIP_47_ERROR_INTERNAL = "INTERNAL" NIP_47_ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED" NIP_47_ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED" @@ -24,7 +25,7 @@ const ( NIP_47_ERROR_EXPIRED = "EXPIRED" NIP_47_ERROR_RESTRICTED = "RESTRICTED" NIP_47_OTHER = "OTHER" - NIP_47_CAPABILITIES = "pay_invoice,get_balance,get_info,make_invoice,lookup_invoice" + NIP_47_CAPABILITIES = "pay_invoice,pay_keysend,get_balance,get_info,make_invoice,lookup_invoice" ) const ( @@ -126,6 +127,12 @@ type PayRequest struct { } // TODO: move to models/Alby +type KeysendRequest struct { + Amount int64 `json:"amount"` + Destination string `json:"destination"` + CustomRecords map[string]string `json:"custom_records,omitempty"` +} + type BalanceResponse struct { Balance int64 `json:"balance"` Currency string `json:"currency"` @@ -174,6 +181,7 @@ type Identity struct { Privkey string } +// TODO: move to models/Nip47 type Nip47Request struct { Method string `json:"method"` Params json.RawMessage `json:"params"` @@ -196,6 +204,19 @@ type Nip47PayParams struct { type Nip47PayResponse struct { Preimage string `json:"preimage"` } + +type Nip47KeysendParams struct { + Amount int64 `json:"amount"` + Pubkey string `json:"pubkey"` + Preimage string `json:"preimage"` + TLVRecords []TLVRecord `json:"tlv_records"` +} + +type TLVRecord struct { + Type uint64 `json:"type"` + Value string `json:"value"` +} + type Nip47BalanceResponse struct { Balance int64 `json:"balance"` MaxAmount int `json:"max_amount"` diff --git a/service.go b/service.go index 61b23857..546dac2b 100644 --- a/service.go +++ b/service.go @@ -11,7 +11,6 @@ import ( "github.com/labstack/echo/v4" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" - decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -30,6 +29,7 @@ var supportedMethods = map[string]bool{ NIP_47_GET_INFO_METHOD: true, NIP_47_MAKE_INVOICE_METHOD: true, NIP_47_LOOKUP_INVOICE_METHOD: true, + NIP_47_PAY_KEYSEND_METHOD: true, } func (svc *Service) GetUser(c echo.Context) (user *User, err error) { @@ -203,6 +203,8 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result switch nip47Request.Method { case NIP_47_PAY_INVOICE_METHOD: return svc.HandlePayInvoiceEvent(ctx, nip47Request, event, app, ss) + case NIP_47_PAY_KEYSEND_METHOD: + return svc.HandlePayKeysendEvent(ctx, nip47Request, event, app, ss) case NIP_47_GET_BALANCE_METHOD: return svc.HandleGetBalanceEvent(ctx, nip47Request, event, app, ss) case NIP_47_MAKE_INVOICE_METHOD: @@ -260,7 +262,7 @@ func (svc *Service) GetMethods(app *App) []string { return requestMethods } -func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod string, paymentRequest *decodepay.Bolt11) (result bool, code string, message string) { +func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod string, amount int64) (result bool, code string, message string) { // find all permissions for the app appPermissions := []AppPermission{} findPermissionsResult := svc.db.Find(&appPermissions, &AppPermission{ @@ -302,7 +304,7 @@ func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod st if maxAmount != 0 { budgetUsage := svc.GetBudgetUsage(&appPermission) - if budgetUsage+paymentRequest.MSatoshi/1000 > int64(maxAmount) { + if budgetUsage+amount/1000 > int64(maxAmount) { return false, NIP_47_ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment" } } diff --git a/service_test.go b/service_test.go index 877e40b5..fd4496ee 100644 --- a/service_test.go +++ b/service_test.go @@ -47,27 +47,40 @@ const nip47LookupInvoiceJson = ` } } ` +const nip47KeysendJson = ` +{ + "method": "pay_keysend", + "params": { + "amount": 100, + "pubkey": "123pubkey", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + } +} +` const nip47PayJson = ` { "method": "pay_invoice", - "params": { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + "params": { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" } } ` const nip47PayWrongMethodJson = ` { "method": "get_balance", - "params": { - "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" + "params": { + "invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m" } } ` const nip47PayJsonNoInvoice = ` { "method": "pay_invoice", - "params": { - "something": "else" + "params": { + "something": "else" } } ` @@ -277,6 +290,70 @@ func TestHandleEvent(t *testing.T) { assert.NoError(t, err) assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) + + // pay_keysend: without permission + newPayload, err = nip04.Encrypt(nip47KeysendJson, ss) + assert.NoError(t, err) + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_pay_keysend_event_1", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{} + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) + assert.NotNil(t, res) + // pay_keysend: with permission + // update the existing permission to pay_invoice so we can have the budget info and increase max amount + newMaxAmount = 1000 + err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_PAY_INVOICE_METHOD).Update("max_amount", newMaxAmount).Error + assert.NoError(t, err) + err = svc.db.Create(appPermission).Error + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_pay_keysend_event_2", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{ + Result: &Nip47PayResponse{}, + } + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, "123preimage", received.Result.(*Nip47PayResponse).Preimage) + + // keysend: budget overflow + newMaxAmount = 100 + // we change the budget info in pay_invoice permission + err = svc.db.Model(&AppPermission{}).Where("request_method = ?", NIP_47_PAY_INVOICE_METHOD).Update("max_amount", newMaxAmount).Error + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_pay_keysend_event_3", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{ + Result: &Nip47PayResponse{}, + } + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, NIP_47_ERROR_QUOTA_EXCEEDED, received.Error.Code) + assert.NotNil(t, res) + // get_balance: without permission newPayload, err = nip04.Encrypt(nip47GetBalanceJson, ss) assert.NoError(t, err) @@ -296,10 +373,8 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) // get_balance: with permission - // update the existing permission to pay_invoice so we can get the budget info - err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_PAY_INVOICE_METHOD).Error - assert.NoError(t, err) - // create a second permission for getting the budget + // the pay_invoice permmission already exists with the budget info + // create a second permission to fetch the balance and budget info appPermission = &AppPermission{ AppId: app.ID, App: app, @@ -326,7 +401,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, 100000, received.Result.(*Nip47BalanceResponse).MaxAmount) assert.Equal(t, "never", received.Result.(*Nip47BalanceResponse).BudgetRenewal) - // make invoice: without permission + // make_invoice: without permission newPayload, err = nip04.Encrypt(nip47MakeInvoiceJson, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -345,7 +420,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) - // make invoice: with permission + // make_invoice: with permission err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_MAKE_INVOICE_METHOD).Error res, err = svc.HandleEvent(ctx, &nostr.Event{ ID: "test_event_13", @@ -365,7 +440,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, mockInvoice, received.Result.(*Nip47MakeInvoiceResponse).Invoice) assert.Equal(t, mockPaymentHash, received.Result.(*Nip47MakeInvoiceResponse).PaymentHash) - // lookup invoice: without permission + // lookup_invoice: without permission newPayload, err = nip04.Encrypt(nip47LookupInvoiceJson, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -384,7 +459,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) assert.NotNil(t, res) - // lookup invoice: with permission + // lookup_invoice: with permission err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_LOOKUP_INVOICE_METHOD).Error assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -405,7 +480,7 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, mockInvoice, received.Result.(*Nip47LookupInvoiceResponse).Invoice) assert.Equal(t, false, received.Result.(*Nip47LookupInvoiceResponse).Paid) - // get info: without permission + // get_info: without permission newPayload, err = nip04.Encrypt(nip47GetInfoJson, ss) assert.NoError(t, err) res, err = svc.HandleEvent(ctx, &nostr.Event{ @@ -427,7 +502,7 @@ func TestHandleEvent(t *testing.T) { // delete all permissions svc.db.Exec("delete from app_permissions") - // lookup invoice: with permission + // get_info: with permission appPermission = &AppPermission{ AppId: app.ID, App: app, @@ -490,7 +565,10 @@ type MockLn struct { } func (mln *MockLn) SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) { - //todo more advanced behaviour + return "123preimage", nil +} + +func (mln *MockLn) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) { return "123preimage", nil }