diff --git a/README.md b/README.md index b4467f17..2b6eb268 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) @@ -141,8 +143,6 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `list_transactions` - ⚠️ from and until in request not supported -❌ `pay_keysend` - ❌ `multi_pay_invoice (TBC)` ❌ `multi_pay_keysend (TBC)` @@ -160,6 +160,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) @@ -171,8 +174,6 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `list_transactions` - ⚠️ offset and unpaid in request not supported -❌ `pay_keysend` - ❌ `multi_pay_invoice (TBC)` ❌ `multi_pay_keysend (TBC)` diff --git a/alby.go b/alby.go index 663d635c..617e5e2d 100644 --- a/alby.go +++ b/alby.go @@ -392,10 +392,10 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey endpoint := "/invoices" switch invoiceType { - case "incoming": - endpoint += "/incoming" - case "outgoing": - endpoint += "/outgoing" + case "incoming": + endpoint += "/incoming" + case "outgoing": + endpoint += "/outgoing" } req, err := http.NewRequest("GET", fmt.Sprintf("%s%s?%s", svc.cfg.AlbyAPIURL, endpoint, urlParams.Encode()), nil) @@ -518,6 +518,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_list_transactions_request.go b/handle_list_transactions_request.go index 38ca1a53..d4f50fb0 100644 --- a/handle_list_transactions_request.go +++ b/handle_list_transactions_request.go @@ -33,7 +33,7 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni 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{ @@ -45,9 +45,9 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni return svc.createResponse(event, Nip47Response{ ResultType: request.Method, Error: &Nip47Error{ - Code: code, - Message: message, - }}, ss) + Code: code, + Message: message, + }}, ss) } 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 c0218201..a39ab204 100644 --- a/lnd.go +++ b/lnd.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/rand" + "crypto/sha256" "encoding/hex" "errors" "time" @@ -18,6 +20,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) @@ -188,6 +191,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 670837e0..7d85e930 100644 --- a/models.go +++ b/models.go @@ -17,6 +17,7 @@ const ( NIP_47_MAKE_INVOICE_METHOD = "make_invoice" NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice" NIP_47_LIST_TRANSACTIONS_METHOD = "list_transactions" + 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" @@ -25,7 +26,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 ( @@ -144,6 +145,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"` @@ -192,6 +199,7 @@ type Identity struct { Privkey string } +// TODO: move to models/Nip47 type Nip47Request struct { Method string `json:"method"` Params json.RawMessage `json:"params"` @@ -214,6 +222,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 3767bb84..0bb7507b 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" ) @@ -204,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: @@ -263,7 +264,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{ @@ -305,7 +306,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 643d6232..d27fc741 100644 --- a/service_test.go +++ b/service_test.go @@ -56,30 +56,44 @@ const nip47ListTransactionsJson = ` "limit": 10, "offset": 0, "type": "incoming" +} +` + +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" } } ` @@ -318,6 +332,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) @@ -337,10 +415,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, @@ -367,7 +443,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{ @@ -386,7 +462,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", @@ -406,7 +482,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{ @@ -425,7 +501,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{ @@ -494,7 +570,8 @@ func TestHandleEvent(t *testing.T) { assert.Equal(t, mockInvoices[0].Amount, transaction.Amount) assert.Equal(t, mockInvoices[0].FeesPaid, transaction.FeesPaid) assert.Equal(t, mockInvoices[0].SettledAt.Unix(), transaction.SettledAt.Unix()) - // 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{ @@ -516,7 +593,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, @@ -579,7 +656,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 }