diff --git a/controllers/keysend.go b/controllers/keysend.go new file mode 100644 index 0000000..dcc86a6 --- /dev/null +++ b/controllers/keysend.go @@ -0,0 +1,49 @@ +package controllers + +import ( + "fmt" + + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/labstack/echo/v4" +) + +// KeySendController : Pay invoice controller struct +type KeySendController struct { + svc *service.LndhubService +} + +func NewKeySendController(svc *service.LndhubService) *KeySendController { + return &KeySendController{svc: svc} +} + +type KeySendRequestBody struct { + Amount int64 `json:"amount" validate:"required"` + Destination string `json:"destination" validate:"required"` + Memo string `json:"memo" validate:"omitempty"` +} + +type KeySendResponseBody struct { + RHash *lib.JavaScriptBuffer `json:"payment_hash,omitempty"` + Amount int64 `json:"num_satoshis,omitempty"` + Description string `json:"description,omitempty"` + Destination string `json:"destination,omitempty"` + DescriptionHashStr string `json:"description_hash,omitempty"` + PaymentError string `json:"payment_error,omitempty"` + PaymentPreimage *lib.JavaScriptBuffer `json:"payment_preimage,omitempty"` + PaymentRoute *service.Route `json:"route,omitempty"` +} + +// KeySend : Pay invoice Controller +func (controller *KeySendController) KeySend(c echo.Context) error { + /* + TODO: copy code from payinvoice.ctrl.go and modify where needed: + - do not decode the payment request because there is no payment request. + Instead, construct the lnrpc.PaymentRequest manually from the KeySendRequestBody. + - add outgoing invoice: same as payinvoice, make sure to set keysend=true + - do a balance check: same as payinvoice, in fact do this before doing anything else + - call svc.PayInvoice : same as payinvoice as long as keysend=true in Invoice + - response will be slightly different due to lack of payment request + */ + return fmt.Errorf("TODO") +} diff --git a/db/migrations/20220304103000_keysend_invoice.up.sql b/db/migrations/20220304103000_keysend_invoice.up.sql new file mode 100644 index 0000000..d7ca05d --- /dev/null +++ b/db/migrations/20220304103000_keysend_invoice.up.sql @@ -0,0 +1 @@ +alter table invoices add column keysend boolean; \ No newline at end of file diff --git a/db/models/invoice.go b/db/models/invoice.go index a117adf..d8419d6 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -21,6 +21,7 @@ type Invoice struct { RHash string `json:"r_hash"` Preimage string `json:"preimage" bun:",nullzero"` Internal bool `json:"internal" bun:",nullzero"` + KeySend bool `json:"keysend" bun:",nullzero"` State string `json:"state" bun:",default:'initialized'"` ErrorMessage string `json:"error_message" bun:",nullzero"` AddIndex uint64 `json:"add_index" bun:",nullzero"` diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go new file mode 100644 index 0000000..f7bd03a --- /dev/null +++ b/integration_tests/keysend_test.go @@ -0,0 +1,12 @@ +package integration_tests + +func (suite *PaymentTestSuite) TestKeysendPayment() { + // destination pubkey strings: + // simnet-lnd-2: 025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220 + // simnet-lnd-3: 03c7092d076f799ab18806743634b4c9bb34e351bdebc91d5b35963f3dc63ec5aa + // simnet-cln-1: 0242898f86064c2fd72de22059c947a83ba23e9d97aedeae7b6dba647123f1d71b + // (put this in utils) + // fund account, test making keysend payments to any of these nodes (lnd-2 and lnd-3 is fine) + // test making a keysend payment to a destination that does not exist + // test making a keysend payment with a memo that is waaaaaaay too long +} diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 9b43d93..e8d6df1 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -98,6 +98,9 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, invoice *mode } func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { + if invoice.KeySend { + return svc.KeySendPaymentSync(ctx, invoice) + } sendPaymentResponse := SendPaymentResponse{} // TODO: set dynamic fee limit feeLimit := lnrpc.FeeLimit{ diff --git a/lib/service/ln.go b/lib/service/ln.go index a9f609a..3d8554c 100644 --- a/lib/service/ln.go +++ b/lib/service/ln.go @@ -2,10 +2,60 @@ package service import ( "context" + "encoding/hex" + "errors" + "github.com/getAlby/lndhub.go/db/models" "github.com/lightningnetwork/lnd/lnrpc" ) +//https://github.com/hsjoberg/blixt-wallet/blob/9fcc56a7dc25237bc14b85e6490adb9e044c009c/src/utils/constants.ts#L5 +const ( + KEYSEND_CUSTOM_RECORD = 5482373484 + TLV_WHATSAT_MESSAGE = 34349334 + TLV_RECORD_NAME = 128100 +) + func (svc *LndhubService) GetInfo(ctx context.Context) (*lnrpc.GetInfoResponse, error) { return svc.LndClient.GetInfo(ctx, &lnrpc.GetInfoRequest{}) } + +func (svc *LndhubService) KeySendPaymentSync(ctx context.Context, invoice *models.Invoice) (result SendPaymentResponse, err error) { + sendPaymentResponse := SendPaymentResponse{} + // TODO: set dynamic fee limit + feeLimit := lnrpc.FeeLimit{ + Limit: &lnrpc.FeeLimit_Fixed{ + Fixed: 300, + }, + } + preImage := makePreimageHex() + // Prepare the LNRPC call + //See: https://github.com/hsjoberg/blixt-wallet/blob/9fcc56a7dc25237bc14b85e6490adb9e044c009c/src/lndmobile/index.ts#L251-L270 + sendPaymentRequest := lnrpc.SendRequest{ + Dest: []byte(invoice.DestinationPubkeyHex), + Amt: invoice.Amount, + FeeLimit: &feeLimit, + DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, + DestCustomRecords: map[uint64][]byte{KEYSEND_CUSTOM_RECORD: preImage, TLV_WHATSAT_MESSAGE: []byte(invoice.Memo)}, + } + + // Execute the payment + sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, &sendPaymentRequest) + if err != nil { + return sendPaymentResponse, err + } + + // If there was a payment error we return an error + if sendPaymentResult.GetPaymentError() != "" || sendPaymentResult.GetPaymentPreimage() == nil { + return sendPaymentResponse, errors.New(sendPaymentResult.GetPaymentError()) + } + + preimage := sendPaymentResult.GetPaymentPreimage() + sendPaymentResponse.PaymentPreimage = preimage + sendPaymentResponse.PaymentPreimageStr = hex.EncodeToString(preimage[:]) + paymentHash := sendPaymentResult.GetPaymentHash() + sendPaymentResponse.PaymentHash = paymentHash + sendPaymentResponse.PaymentHashStr = hex.EncodeToString(paymentHash[:]) + sendPaymentResponse.PaymentRoute = &Route{TotalAmt: sendPaymentResult.PaymentRoute.TotalAmt, TotalFees: sendPaymentResult.PaymentRoute.TotalFees} + return sendPaymentResponse, nil +}