diff --git a/go.mod b/go.mod index 787cd325..4a5e4d68 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/davrux/echo-logrus/v4 v4.0.3 github.com/elnosh/gonuts v0.1.1-0.20240602162005-49da741613e4 github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59 - github.com/getAlby/ldk-node-go v0.0.0-20240730160541-be661a91409b + github.com/getAlby/ldk-node-go v0.0.0-20240801181008-94e3b8403ad3 github.com/go-gormigrate/gormigrate/v2 v2.1.2 github.com/gorilla/sessions v1.3.0 github.com/labstack/echo-contrib v0.17.1 diff --git a/go.sum b/go.sum index 8bc9f6b6..3393f62b 100644 --- a/go.sum +++ b/go.sum @@ -188,8 +188,8 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59 h1:fSqdXE9uKhLcOOQaLtzN+D8RN3oEcZQkGX5E8PyiKy0= github.com/getAlby/glalby-go v0.0.0-20240621192717-95673c864d59/go.mod h1:ViyJvjlvv0GCesTJ7mb3fBo4G+/qsujDAFN90xZ7a9U= -github.com/getAlby/ldk-node-go v0.0.0-20240730160541-be661a91409b h1:60D6iN0//asM9NErwW0q3wqHv3gjzGnBRVJh1J2Q5/8= -github.com/getAlby/ldk-node-go v0.0.0-20240730160541-be661a91409b/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= +github.com/getAlby/ldk-node-go v0.0.0-20240801181008-94e3b8403ad3 h1:0V68vTnGZITuZeGVo1zqYrc4MP6cGyM3maWEZbWyW8k= +github.com/getAlby/ldk-node-go v0.0.0-20240801181008-94e3b8403ad3/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/lnclient/breez/breez.go b/lnclient/breez/breez.go index 7ad178f4..fe13e52c 100644 --- a/lnclient/breez/breez.go +++ b/lnclient/breez/breez.go @@ -118,8 +118,9 @@ func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*ln } -func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { - extraTlvs := []breez_sdk.TlvEntry{} +func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { + // TODO: re-enable when passing custom preimage is possible + /*extraTlvs := []breez_sdk.TlvEntry{} for _, record := range custom_records { decodedValue, err := hex.DecodeString(record.Value) if err != nil { @@ -144,7 +145,8 @@ func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destinat if resp.Payment.Details != nil { lnDetails, _ = resp.Payment.Details.(breez_sdk.PaymentDetailsLn) } - return lnDetails.Data.PaymentHash, lnDetails.Data.PaymentPreimage, resp.Payment.FeeMsat, nil + return lnDetails.Data.PaymentHash, lnDetails.Data.PaymentPreimage, resp.Payment.FeeMsat, nil*/ + return nil, errors.New("not supported") } func (bs *BreezService) GetBalance(ctx context.Context) (balance int64, err error) { @@ -481,7 +483,7 @@ func (bs *BreezService) DisconnectPeer(ctx context.Context, peerId string) error } func (bs *BreezService) GetSupportedNIP47Methods() []string { - return []string{"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"} + return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"} } func (bs *BreezService) GetSupportedNIP47NotificationTypes() []string { diff --git a/lnclient/cashu/cashu.go b/lnclient/cashu/cashu.go index 97f48f91..063d6d1e 100644 --- a/lnclient/cashu/cashu.go +++ b/lnclient/cashu/cashu.go @@ -89,8 +89,8 @@ func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (re }, nil } -func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { - return "", "", 0, errors.New("keysend not supported") +func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { + return nil, errors.New("keysend not supported") } func (cs *CashuService) GetBalance(ctx context.Context) (balance int64, err error) { diff --git a/lnclient/greenlight/greenlight.go b/lnclient/greenlight/greenlight.go index 3abcbf84..a8b3e2e4 100644 --- a/lnclient/greenlight/greenlight.go +++ b/lnclient/greenlight/greenlight.go @@ -127,9 +127,10 @@ func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) }, nil } -func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { +func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { - extraTlvs := []glalby.TlvEntry{} + // TODO: re-enable when passing custom preimage is possible + /*extraTlvs := []glalby.TlvEntry{} for _, customRecord := range custom_records { @@ -153,7 +154,8 @@ func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, des // TODO: get payment hash from response - return "", response.PaymentPreimage, 0, nil + return "", response.PaymentPreimage, 0, nil*/ + return nil, errors.New("not supported") } func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err error) { @@ -682,7 +684,7 @@ func (gs *GreenlightService) DisconnectPeer(ctx context.Context, peerId string) } func (gs *GreenlightService) GetSupportedNIP47Methods() []string { - return []string{"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"} + return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"} } func (gs *GreenlightService) GetSupportedNIP47NotificationTypes() []string { diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index ca2120d4..d34c0e12 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -230,7 +230,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226@170.75.163.209:9735", // WoS "02fcc5bfc48e83f06c04483a2985e1c390cb0f35058baa875ad2053858b8e80dbd@35.239.148.251:9735", // Blink "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190@3.230.33.224:9735", // c= - "038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf@64.23.162.51:9735", // Megalith LSP + "038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf@64.23.162.51:9735", // Megalith LSP } logger.Logger.Info("Connecting to some peers to retrieve P2P gossip data") for _, peer := range peers { @@ -515,14 +515,14 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc }, nil } -func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { +func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { paymentStart := time.Now() customTlvs := []ldk_node.TlvEntry{} for _, customRecord := range custom_records { decodedValue, err := hex.DecodeString(customRecord.Value) if err != nil { - return "", "", 0, err + return nil, err } customTlvs = append(customTlvs, ldk_node.TlvEntry{ Type: customRecord.Type, @@ -533,12 +533,13 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe() defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription) - paymentHash, err = ls.node.SpontaneousPayment().Send(amount, destination, customTlvs) + paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, customTlvs, &preimage) if err != nil { logger.Logger.WithError(err).Error("Keysend failed") - return paymentHash, "", 0, err + return nil, err } - + fee := uint64(0) + paid := false for start := time.Now(); time.Since(start) < time.Second*60; { event := <-ldkEventSubscription @@ -547,25 +548,8 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash { logger.Logger.Info("Got payment success event") - payment := ls.node.Payment(paymentHash) - if payment == nil { - logger.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash) - return paymentHash, "", 0, errors.New("Payment not found") - } - spontaneousPaymentKind, ok := payment.Kind.(ldk_node.PaymentKindSpontaneous) - - if !ok { - logger.Logger.WithFields(logrus.Fields{ - "payment": payment, - }).Error("Payment is not a spontaneous kind") - } - - if spontaneousPaymentKind.Preimage == nil { - logger.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash) - return paymentHash, "", 0, errors.New("Payment preimage not found") - } - preimage = *spontaneousPaymentKind.Preimage + paid = true if eventPaymentSuccessful.FeePaidMsat != nil { fee = *eventPaymentSuccessful.FeePaidMsat @@ -581,21 +565,23 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio "reason": failureReasonMessage, }).Error("Received payment failed event") - return paymentHash, "", 0, fmt.Errorf("payment failed event: %s", failureReasonMessage) + return nil, fmt.Errorf("payment failed event: %s", failureReasonMessage) } } - if preimage == "" { + if !paid { logger.Logger.WithFields(logrus.Fields{ - "paymentHash": paymentHash, + "payment_hash": paymentHash, }).Warn("Timed out waiting for keysend to be sent") - return paymentHash, "", 0, lnclient.NewTimeoutError() + return nil, lnclient.NewTimeoutError() } logger.Logger.WithFields(logrus.Fields{ "duration": time.Since(paymentStart).Milliseconds(), "fee": fee, }).Info("Successful keysend payment") - return paymentHash, preimage, fee, nil + return &lnclient.PayKeysendResponse{ + Fee: fee, + }, nil } func (ls *LDKService) GetBalance(ctx context.Context) (balance int64, err error) { diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go index a9915062..c273d4b1 100644 --- a/lnclient/lnd/lnd.go +++ b/lnclient/lnd/lnd.go @@ -2,7 +2,6 @@ package lnd import ( "context" - "crypto/rand" "crypto/sha256" "encoding/hex" "errors" @@ -300,37 +299,29 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnc }, nil } -func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { +func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { destBytes, err := hex.DecodeString(destination) if err != nil { - return "", "", 0, err + return nil, err } - var preImageBytes []byte - - preImageBytes, err = makePreimageHex() - preimage = hex.EncodeToString(preImageBytes) - + preImageBytes, err := hex.DecodeString(preimage) if err != nil || len(preImageBytes) != 32 { logger.Logger.WithFields(logrus.Fields{ - "amount": amount, - "destination": destination, - "preimage": preimage, - "customRecords": custom_records, - "error": err, - }).Errorf("Invalid preimage") - return "", "", 0, err + "preimage": preimage, + }).WithError(err).Error("Invalid preimage") + return nil, err } paymentHash256 := sha256.New() paymentHash256.Write(preImageBytes) paymentHashBytes := paymentHash256.Sum(nil) - paymentHash = hex.EncodeToString(paymentHashBytes) + paymentHash := hex.EncodeToString(paymentHashBytes) destCustomRecords := map[uint64][]byte{} for _, record := range custom_records { decodedValue, err := hex.DecodeString(record.Value) if err != nil { - return "", "", 0, err + return nil, err } destCustomRecords[record.Type] = decodedValue } @@ -354,7 +345,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati "customRecords": custom_records, "error": err, }).Errorf("Failed to send keysend payment") - return paymentHash, "", 0, err + return nil, err } if resp.PaymentError != "" { logger.Logger.WithFields(logrus.Fields{ @@ -365,7 +356,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati "customRecords": custom_records, "paymentError": resp.PaymentError, }).Errorf("Keysend payment has payment error") - return paymentHash, "", 0, errors.New(resp.PaymentError) + return nil, errors.New(resp.PaymentError) } respPreimage := hex.EncodeToString(resp.PaymentPreimage) if respPreimage != preimage { @@ -377,7 +368,7 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati "customRecords": custom_records, "paymentError": resp.PaymentError, }).Errorf("Preimage in keysend response does not match") - return paymentHash, "", 0, errors.New("preimage in keysend response does not match") + return nil, errors.New("preimage in keysend response does not match") } logger.Logger.WithFields(logrus.Fields{ "amount": amount, @@ -388,16 +379,9 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati "respPreimage": respPreimage, }).Info("Keysend payment successful") - return paymentHash, respPreimage, uint64(resp.PaymentRoute.TotalFeesMsat), 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 + return &lnclient.PayKeysendResponse{ + Fee: uint64(resp.PaymentRoute.TotalFeesMsat), + }, nil } func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.LNClient, err error) { diff --git a/lnclient/models.go b/lnclient/models.go index 513bc49a..7607a08e 100644 --- a/lnclient/models.go +++ b/lnclient/models.go @@ -45,7 +45,7 @@ type NodeConnectionInfo struct { type LNClient interface { SendPaymentSync(ctx context.Context, payReq string) (*PayInvoiceResponse, error) - SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord) (paymentHash string, preimage string, fee uint64, err error) + SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord, preimage string) (*PayKeysendResponse, error) GetBalance(ctx context.Context) (balance int64, err error) GetPubkey() string GetInfo(ctx context.Context) (info *NodeInfo, err error) @@ -157,6 +157,10 @@ type PayInvoiceResponse struct { Fee uint64 `json:"fee"` } +type PayKeysendResponse struct { + Fee uint64 `json:"fee"` +} + type BalancesResponse struct { Onchain OnchainBalanceResponse `json:"onchain"` Lightning LightningBalanceResponse `json:"lightning"` diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index e8611b84..781d4695 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -431,8 +431,8 @@ func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) ( }, nil } -func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { - return "", "", 0, errors.New("not implemented") +func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { + return nil, errors.New("not implemented") } func (svc *PhoenixService) RedeemOnchainFunds(ctx context.Context, toAddress string) (txId string, err error) { diff --git a/nip47/controllers/multi_pay_invoice_controller_test.go b/nip47/controllers/multi_pay_invoice_controller_test.go index c651a350..e11d22f4 100644 --- a/nip47/controllers/multi_pay_invoice_controller_test.go +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -300,6 +300,13 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) { HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) + assert.Equal(t, 2, len(dTags)) + // we can't guarantee which request was processed first + // so swap them if they are back to front + if responses[0].Result == nil { + responses[0], responses[1] = responses[1], responses[0] + dTags[0], dTags[1] = dTags[1], dTags[0] + } assert.Equal(t, "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf", dTags[0].GetFirst([]string{"d"}).Value()) assert.Equal(t, "123preimage", responses[0].Result.(payResponse).Preimage) diff --git a/nip47/controllers/multi_pay_keysend_controller_test.go b/nip47/controllers/multi_pay_keysend_controller_test.go index f02bcd0f..386c0c00 100644 --- a/nip47/controllers/multi_pay_keysend_controller_test.go +++ b/nip47/controllers/multi_pay_keysend_controller_test.go @@ -112,32 +112,68 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) { assert.Equal(t, 2, len(responses)) for i := 0; i < len(responses); i++ { - assert.Equal(t, "12345preimage", responses[i].Result.(payResponse).Preimage) + assert.Equal(t, 64, len(responses[i].Result.(payResponse).Preimage)) + assert.Equal(t, uint64(1), responses[i].Result.(payResponse).FeesPaid) assert.Nil(t, responses[i].Error) assert.Equal(t, "123pubkey", dTags[i].GetFirst([]string{"d"}).Value()) } } -// TODO: fix and re-enable this as a separate test -// budget overflow -/*newMaxAmount := 500 -err = svc.DB.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error -assert.NoError(t, err) - -err = json.Unmarshal([]byte(nip47MultiPayKeysendOneOverflowingBudgetJson), request) -assert.NoError(t, err) - -payload, err = nip04.Encrypt(nip47MultiPayKeysendOneOverflowingBudgetJson, ss) -assert.NoError(t, err) -reqEvent.Content = payload - -reqEvent.ID = "multi_pay_keysend_with_budget_overflow" -requestEvent.NostrId = reqEvent.ID -responses = []*models.Response{} -dTags = []nostr.Tags{} -svc.nip47Svc.HandleMultiPayKeysendEvent(ctx, request, requestEvent, app, publishResponse) - -assert.Equal(t, responses[0].Error.Code, models.ERROR_QUOTA_EXCEEDED) -assert.Equal(t, "500pubkey", dTags[0].GetFirst([]string{"d"}).Value()) -assert.Equal(t, responses[1].Result.(payResponse).Preimage, "12345preimage") -assert.Equal(t, "customId", dTags[1].GetFirst([]string{"d"}).Value())*/ +func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + MaxAmountSat: 400, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47MultiPayKeysendOneOverflowingBudgetJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + responses := []*models.Response{} + dTags := []nostr.Tags{} + + var mu sync.Mutex + + publishResponse := func(response *models.Response, tags nostr.Tags) { + mu.Lock() + defer mu.Unlock() + responses = append(responses, response) + dTags = append(dTags, tags) + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) + + // we can't guarantee which request was processed first + // so swap them if they are back to front + if responses[0].Result == nil { + responses[0], responses[1] = responses[1], responses[0] + dTags[0], dTags[1] = dTags[1], dTags[0] + } + + assert.Equal(t, "customId", dTags[0].GetFirst([]string{"d"}).Value()) + assert.Nil(t, responses[0].Error) + assert.Equal(t, 64, len(responses[0].Result.(payResponse).Preimage)) + assert.Equal(t, uint64(1), responses[0].Result.(payResponse).FeesPaid) + + assert.Nil(t, responses[1].Result) + assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, responses[1].Error.Code) +} diff --git a/nip47/controllers/pay_invoice_controller.go b/nip47/controllers/pay_invoice_controller.go index 62efb5a7..b012cf9f 100644 --- a/nip47/controllers/pay_invoice_controller.go +++ b/nip47/controllers/pay_invoice_controller.go @@ -57,7 +57,7 @@ func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, payme "bolt11": bolt11, }).Info("Sending payment") - response, err := controller.transactionsService.SendPaymentSync(ctx, bolt11, controller.lnClient, &app.ID, &requestEventId) + transaction, err := controller.transactionsService.SendPaymentSync(ctx, bolt11, controller.lnClient, &app.ID, &requestEventId) if err != nil { logger.Logger.WithFields(logrus.Fields{ "request_event_id": requestEventId, @@ -90,8 +90,8 @@ func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, payme publishResponse(&models.Response{ ResultType: nip47Request.Method, Result: payResponse{ - Preimage: *response.Preimage, - FeesPaid: response.FeeMsat, + Preimage: *transaction.Preimage, + FeesPaid: transaction.FeeMsat, }, }, tags) } diff --git a/nip47/controllers/pay_keysend_controller.go b/nip47/controllers/pay_keysend_controller.go index 36cb8e66..874a5af1 100644 --- a/nip47/controllers/pay_keysend_controller.go +++ b/nip47/controllers/pay_keysend_controller.go @@ -36,7 +36,7 @@ func (controller *nip47Controller) payKeysend(ctx context.Context, payKeysendPar "senderPubkey": payKeysendParams.Pubkey, }).Info("Sending keysend payment") - transaction, err := controller.transactionsService.SendKeysend(ctx, payKeysendParams.Amount, payKeysendParams.Pubkey, payKeysendParams.TLVRecords, controller.lnClient, &app.ID, &requestEventId) + transaction, err := controller.transactionsService.SendKeysend(ctx, payKeysendParams.Amount, payKeysendParams.Pubkey, payKeysendParams.TLVRecords, payKeysendParams.Preimage, controller.lnClient, &app.ID, &requestEventId) if err != nil { logger.Logger.WithFields(logrus.Fields{ "request_event_id": requestEventId, @@ -68,6 +68,7 @@ func (controller *nip47Controller) payKeysend(ctx context.Context, payKeysendPar ResultType: nip47Request.Method, Result: payResponse{ Preimage: *transaction.Preimage, + FeesPaid: transaction.FeeMsat, }, }, tags) } diff --git a/nip47/controllers/pay_keysend_controller_test.go b/nip47/controllers/pay_keysend_controller_test.go index 224bc48a..0fcf7474 100644 --- a/nip47/controllers/pay_keysend_controller_test.go +++ b/nip47/controllers/pay_keysend_controller_test.go @@ -30,6 +30,21 @@ const nip47KeysendJson = ` } ` +const nip47KeysendJsonWithPreimage = ` +{ + "method": "pay_keysend", + "params": { + "amount": 123000, + "pubkey": "123pubkey", + "preimage": "018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b", + "tlv_records": [{ + "type": 5482373484, + "value": "fajsn341414fq" + }] + } +} +` + func TestHandlePayKeysendEvent(t *testing.T) { ctx := context.TODO() defer tests.RemoveTestService() @@ -68,5 +83,47 @@ func TestHandlePayKeysendEvent(t *testing.T) { HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Error) - assert.Equal(t, "12345preimage", publishedResponse.Result.(payResponse).Preimage) + assert.Equal(t, 64, len(publishedResponse.Result.(payResponse).Preimage)) + assert.Equal(t, uint64(1), publishedResponse.Result.(payResponse).FeesPaid) +} +func TestHandlePayKeysendEvent_WithPreimage(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47KeysendJsonWithPreimage), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) + + assert.Nil(t, publishedResponse.Error) + assert.Equal(t, "018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b", publishedResponse.Result.(payResponse).Preimage) + assert.Equal(t, uint64(1), publishedResponse.Result.(payResponse).FeesPaid) } diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index a9043625..ae1644d9 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -80,8 +80,10 @@ func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclien }, nil } -func (mln *MockLn) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) { - return "paymenthash", "12345preimage", 0, nil +func (mln *MockLn) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { + return &lnclient.PayKeysendResponse{ + Fee: 1, + }, nil } func (mln *MockLn) GetBalance(ctx context.Context) (balance int64, err error) { diff --git a/transactions/keysend_test.go b/transactions/keysend_test.go index f6e3691d..8eac2773 100644 --- a/transactions/keysend_test.go +++ b/transactions/keysend_test.go @@ -19,7 +19,7 @@ func TestSendKeysend(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, nil, nil) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, nil, nil) assert.NoError(t, err) assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) @@ -27,6 +27,28 @@ func TestSendKeysend(t *testing.T) { assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) assert.Zero(t, transaction.FeeReserveMsat) + assert.NotNil(t, transaction.Preimage) + assert.Equal(t, 64, len(*transaction.Preimage)) +} +func TestSendKeysend_CustomPreimage(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + customPreimage := "018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b" + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, customPreimage, svc.LNClient, nil, nil) + + assert.NoError(t, err) + assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) + assert.Equal(t, uint64(1000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Zero(t, transaction.FeeReserveMsat) + assert.NotNil(t, transaction.Preimage) + assert.Equal(t, customPreimage, *transaction.Preimage) } func TestSendKeysend_App_NoPermission(t *testing.T) { @@ -44,7 +66,7 @@ func TestSendKeysend_App_NoPermission(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.Error(t, err) assert.Equal(t, "app does not have pay_invoice scope", err.Error()) @@ -74,7 +96,7 @@ func TestSendKeysend_App_WithPermission(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.NoError(t, err) assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) @@ -84,6 +106,8 @@ func TestSendKeysend_App_WithPermission(t *testing.T) { assert.Equal(t, app.ID, *transaction.AppId) assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) assert.Zero(t, transaction.FeeReserveMsat) + assert.NotNil(t, transaction.Preimage) + assert.Equal(t, 64, len(*transaction.Preimage)) } func TestSendKeysend_App_BudgetExceeded(t *testing.T) { @@ -110,7 +134,7 @@ func TestSendKeysend_App_BudgetExceeded(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.ErrorIs(t, err, NewQuotaExceededError()) assert.Nil(t, transaction) @@ -139,7 +163,7 @@ func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.NoError(t, err) assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) @@ -149,6 +173,8 @@ func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) { assert.Equal(t, app.ID, *transaction.AppId) assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) assert.Zero(t, transaction.FeeReserveMsat) + assert.NotNil(t, transaction.Preimage) + assert.Equal(t, 64, len(*transaction.Preimage)) } func TestSendKeysend_App_BalanceExceeded(t *testing.T) { @@ -183,7 +209,7 @@ func TestSendKeysend_App_BalanceExceeded(t *testing.T) { }) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.ErrorIs(t, err, NewInsufficientBalanceError()) assert.Nil(t, transaction) @@ -221,7 +247,7 @@ func TestSendKeysend_App_BalanceSufficient(t *testing.T) { }) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.NoError(t, err) assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) @@ -230,6 +256,8 @@ func TestSendKeysend_App_BalanceSufficient(t *testing.T) { assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) assert.Equal(t, app.ID, *transaction.AppId) assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) + assert.NotNil(t, transaction.Preimage) + assert.Equal(t, 64, len(*transaction.Preimage)) assert.Zero(t, transaction.FeeReserveMsat) } @@ -246,12 +274,14 @@ func TestSendKeysend_TLVs(t *testing.T) { Type: 7629169, Value: "48656C6C6F2C20776F726C64", }, - }, svc.LNClient, nil, nil) + }, "", svc.LNClient, nil, nil) assert.NoError(t, err) assert.Equal(t, `{"destination":"fake destination","tlv_records":[{"type":7629169,"value":"48656C6C6F2C20776F726C64"}]}`, transaction.Metadata) assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.NotNil(t, transaction.Preimage) + assert.Equal(t, 64, len(*transaction.Preimage)) assert.Zero(t, transaction.FeeReserveMsat) } diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 474ffa4b..5fbcccda 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -2,6 +2,9 @@ package transactions import ( "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -31,7 +34,7 @@ type TransactionsService interface { LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error) SendPaymentSync(ctx context.Context, payReq string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) - SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) + SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) } type Transaction = db.Transaction @@ -234,7 +237,28 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri return &dbTransaction, nil } -func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) { +func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) { + + if preimage == "" { + preImageBytes, err := makePreimageHex() + if err != nil { + return nil, err + } + preimage = hex.EncodeToString(preImageBytes) + } + + preImageBytes, err := hex.DecodeString(preimage) + if err != nil || len(preImageBytes) != 32 { + logger.Logger.WithFields(logrus.Fields{ + "preimage": preimage, + }).WithError(err).Error("Invalid preimage") + return nil, err + } + + paymentHash256 := sha256.New() + paymentHash256.Write(preImageBytes) + paymentHashBytes := paymentHash256.Sum(nil) + paymentHash := hex.EncodeToString(paymentHashBytes) metadata := map[string]interface{}{} @@ -255,7 +279,6 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, return err } - // NOTE: transaction is created without payment hash :scream: dbTransaction = db.Transaction{ AppId: appId, RequestEventId: requestEventId, @@ -264,6 +287,8 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, FeeReserveMsat: svc.calculateFeeReserveMsat(uint64(amount)), AmountMsat: amount, Metadata: string(metadataBytes), + PaymentHash: paymentHash, + Preimage: &preimage, } err = tx.Create(&dbTransaction).Error @@ -278,7 +303,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, return nil, err } - paymentHash, preimage, fee, err := lnClient.SendKeysend(ctx, amount, destination, customRecords) + payKeysendResponse, err := lnClient.SendKeysend(ctx, amount, destination, customRecords, preimage) if err != nil { logger.Logger.WithFields(logrus.Fields{ @@ -327,9 +352,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, now := time.Now() dbErr := svc.db.Model(&dbTransaction).Updates(map[string]interface{}{ "State": constants.TRANSACTION_STATE_SETTLED, - "PaymentHash": paymentHash, - "Preimage": &preimage, - "FeeMsat": &fee, + "FeeMsat": &payKeysendResponse.Fee, "FeeReserveMsat": 0, "SettledAt": &now, }).Error @@ -752,3 +775,12 @@ func (svc *transactionsService) calculateFeeReserveMsat(amount uint64) uint64 { // NOTE: LDK defaults to 1% of the payment amount + 50 sats return uint64(math.Max(math.Ceil(float64(amount)*0.01), 10000)) } + +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 +}