diff --git a/controllers/auth.ctrl.go b/controllers/auth.ctrl.go index 8add25c..0780566 100644 --- a/controllers/auth.ctrl.go +++ b/controllers/auth.ctrl.go @@ -35,7 +35,8 @@ func (controller *AuthController) Auth(c echo.Context) error { var body AuthRequestBody if err := c.Bind(&body); err != nil { - return err + c.Logger().Errorf("Failed to load auth user request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } if err := c.Validate(&body); err != nil { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) diff --git a/controllers/create.ctrl.go b/controllers/create.ctrl.go index 5f5f314..f58708c 100644 --- a/controllers/create.ctrl.go +++ b/controllers/create.ctrl.go @@ -3,6 +3,7 @@ package controllers import ( "net/http" + "github.com/getAlby/lndhub.go/lib/responses" "github.com/getAlby/lndhub.go/lib/service" "github.com/labstack/echo/v4" ) @@ -33,12 +34,13 @@ func (controller *CreateUserController) CreateUser(c echo.Context) error { var body CreateUserRequestBody if err := c.Bind(&body); err != nil { - return err + c.Logger().Errorf("Failed to load create user request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } user, err := controller.svc.CreateUser(c.Request().Context(), body.Login, body.Password) - //todo json response if err != nil { - return err + c.Logger().Errorf("Failed to create user: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } var ResponseBody CreateUserResponseBody diff --git a/controllers/keysend.ctrl.go b/controllers/keysend.ctrl.go new file mode 100644 index 0000000..bf28ad3 --- /dev/null +++ b/controllers/keysend.ctrl.go @@ -0,0 +1,102 @@ +package controllers + +import ( + "fmt" + "net/http" + + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/getAlby/lndhub.go/lnd" + "github.com/getsentry/sentry-go" + "github.com/labstack/echo/v4" + "github.com/lightningnetwork/lnd/lnrpc" +) + +// KeySendController : Key send 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 : Key send Controller +func (controller *KeySendController) KeySend(c echo.Context) error { + userID := c.Get("UserID").(int64) + reqBody := KeySendRequestBody{} + if err := c.Bind(&reqBody); err != nil { + c.Logger().Errorf("Failed to load keysend request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + if err := c.Validate(&reqBody); err != nil { + c.Logger().Errorf("Invalid keysend request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + lnPayReq := &lnd.LNPayReq{ + PayReq: &lnrpc.PayReq{ + Destination: reqBody.Destination, + NumSatoshis: reqBody.Amount, + Description: reqBody.Memo, + }, + Keysend: true, + } + + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, "", lnPayReq) + if err != nil { + return err + } + + currentBalance, err := controller.svc.CurrentUserBalance(c.Request().Context(), userID) + if err != nil { + return err + } + + if currentBalance < invoice.Amount { + c.Logger().Errorf("User does not have enough balance invoice_id=%v user_id=%v balance=%v amount=%v", invoice.ID, userID, currentBalance, invoice.Amount) + return c.JSON(http.StatusBadRequest, responses.NotEnoughBalanceError) + } + + sendPaymentResponse, err := controller.svc.PayInvoice(c.Request().Context(), invoice) + if err != nil { + c.Logger().Errorf("Payment failed: %v", err) + sentry.CaptureException(err) + return c.JSON(http.StatusBadRequest, echo.Map{ + "error": true, + "code": 10, + "message": fmt.Sprintf("Payment failed. Does the receiver have enough inbound capacity? (%v)", err), + }) + } + + responseBody := &KeySendResponseBody{} + responseBody.RHash = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentHash} + responseBody.Amount = invoice.Amount + responseBody.Destination = invoice.DestinationPubkeyHex + responseBody.Description = invoice.Memo + responseBody.DescriptionHashStr = invoice.DescriptionHash + responseBody.PaymentError = sendPaymentResponse.PaymentError + responseBody.PaymentPreimage = &lib.JavaScriptBuffer{Data: sendPaymentResponse.PaymentPreimage} + responseBody.PaymentRoute = sendPaymentResponse.PaymentRoute + + return c.JSON(http.StatusOK, responseBody) +} diff --git a/controllers/payinvoice.ctrl.go b/controllers/payinvoice.ctrl.go index 36c4e7e..1a658cd 100644 --- a/controllers/payinvoice.ctrl.go +++ b/controllers/payinvoice.ctrl.go @@ -7,6 +7,7 @@ import ( "github.com/getAlby/lndhub.go/lib" "github.com/getAlby/lndhub.go/lib/responses" "github.com/getAlby/lndhub.go/lib/service" + "github.com/getAlby/lndhub.go/lnd" "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) @@ -69,7 +70,12 @@ func (controller *PayInvoiceController) PayInvoice(c echo.Context) error { } */ - invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, decodedPaymentRequest) + lnPayReq := &lnd.LNPayReq{ + PayReq: decodedPaymentRequest, + Keysend: false, + } + + invoice, err := controller.svc.AddOutgoingInvoice(c.Request().Context(), userID, paymentRequest, lnPayReq) if err != nil { return err } 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 b59dd3b..17d6ae5 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -22,6 +22,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/go.mod b/go.mod index 102b910..f69af60 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/getAlby/lndhub.go go 1.17 + // +heroku goVersion go1.17 require ( - github.com/btcsuite/btcd v0.22.0-beta.0.20211005184431-e3449998be39 github.com/getsentry/sentry-go v0.12.0 github.com/go-playground/validator/v10 v10.10.0 github.com/golang-jwt/jwt v3.2.2+incompatible @@ -27,149 +27,8 @@ require ( ) require ( - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect - github.com/aead/siphash v1.0.1 // indirect - github.com/andybalholm/brotli v1.0.3 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect - github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // indirect - github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 // indirect - github.com/btcsuite/btcwallet v0.13.0 // indirect - github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 // indirect - github.com/btcsuite/btcwallet/wallet/txrules v1.1.0 // indirect - github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect - github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec // indirect - github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c // indirect - github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect - github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect - github.com/cespare/xxhash/v2 v2.1.1 // indirect - github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.3.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/lru v1.0.0 // indirect - github.com/dsnet/compress v0.0.1 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/fergusstrange/embedded-postgres v1.10.0 // indirect - github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect - github.com/go-errors/errors v1.0.1 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.0.1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.10.0 // indirect - github.com/jackc/pgio v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.1.1 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.8.1 // indirect - github.com/jackc/pgx/v4 v4.13.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jonboulle/clockwork v0.2.2 // indirect - github.com/jrick/logrotate v1.0.0 // indirect - github.com/json-iterator/go v1.1.11 // indirect - github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/kkdai/bstream v1.0.0 // indirect - github.com/klauspost/compress v1.13.6 // indirect - github.com/klauspost/pgzip v1.2.5 // indirect - github.com/leodido/go-urn v1.2.1 // indirect - github.com/lib/pq v1.10.3 // indirect - github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect - github.com/lightninglabs/neutrino v0.13.0 // indirect - github.com/lightningnetwork/lightning-onion v1.0.2-0.20210520211913-522b799e65b1 // indirect - github.com/lightningnetwork/lnd/clock v1.1.0 // indirect - github.com/lightningnetwork/lnd/healthcheck v1.2.0 // indirect - github.com/lightningnetwork/lnd/kvdb v1.2.1 // indirect - github.com/lightningnetwork/lnd/queue v1.1.0 // indirect - github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect - github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-sqlite3 v1.14.10 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mholt/archiver/v3 v3.5.0 // indirect - github.com/miekg/dns v1.1.43 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/nwaples/rardecode v1.1.2 // indirect - github.com/pierrec/lz4/v4 v4.1.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.11.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.26.0 // indirect - github.com/prometheus/procfs v0.6.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect - github.com/rogpeppe/fastuuid v1.2.0 // indirect - github.com/rs/zerolog v1.26.0 // indirect - github.com/sirupsen/logrus v1.7.0 // indirect - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect - github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect - github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/ulikunitz/xz v0.5.10 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.1 // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect - go.etcd.io/bbolt v1.3.6 // indirect - go.etcd.io/etcd/api/v3 v3.5.0 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect - go.etcd.io/etcd/client/v2 v2.305.0 // indirect - go.etcd.io/etcd/client/v3 v3.5.0 // indirect - go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect - go.etcd.io/etcd/raft/v3 v3.5.0 // indirect - go.etcd.io/etcd/server/v3 v3.5.0 // indirect - go.opentelemetry.io/contrib v0.20.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect - go.opentelemetry.io/otel v0.20.0 // indirect - go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect - go.opentelemetry.io/otel/metric v0.20.0 // indirect - go.opentelemetry.io/otel/sdk v0.20.0 // indirect - go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect - go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect - go.opentelemetry.io/otel/trace v0.20.0 // indirect - go.opentelemetry.io/proto/otlp v0.7.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.17.0 // indirect - golang.org/x/mod v0.5.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect - golang.org/x/tools v0.1.8 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/errgo.v1 v1.0.1 // indirect - gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - lukechampine.com/uint128 v1.1.1 // indirect - mellium.im/sasl v0.2.1 // indirect - modernc.org/cc/v3 v3.35.22 // indirect - modernc.org/ccgo/v3 v3.14.0 // indirect - modernc.org/libc v1.13.2 // indirect - modernc.org/mathutil v1.4.1 // indirect - modernc.org/memory v1.0.5 // indirect - modernc.org/opt v0.1.1 // indirect - modernc.org/sqlite v1.14.3 // indirect - modernc.org/strutil v1.1.1 // indirect - modernc.org/token v1.0.0 // indirect - sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/integration_tests/keysend_test.go b/integration_tests/keysend_test.go new file mode 100644 index 0000000..d834a27 --- /dev/null +++ b/integration_tests/keysend_test.go @@ -0,0 +1,122 @@ +package integration_tests + +import ( + "context" + "fmt" + "log" + "testing" + "time" + + "github.com/getAlby/lndhub.go/controllers" + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/getAlby/lndhub.go/lib/tokens" + "github.com/getAlby/lndhub.go/lnd" + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type KeySendTestSuite struct { + TestSuite + fundingClient *lnd.LNDWrapper + service *service.LndhubService + aliceLogin controllers.CreateUserResponseBody + aliceToken string + invoiceUpdateSubCancelFn context.CancelFunc +} + +func (suite *KeySendTestSuite) SetupSuite() { + lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ + Address: lnd2RegtestAddress, + MacaroonHex: lnd2RegtestMacaroonHex, + }) + if err != nil { + log.Fatalf("Error setting up funding client: %v", err) + } + suite.fundingClient = lndClient + + svc, err := LndHubTestServiceInit(nil) + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + users, userTokens, err := createUsers(svc, 1) + if err != nil { + log.Fatalf("Error creating test users: %v", err) + } + // Subscribe to LND invoice updates in the background + // store cancel func to be called in tear down suite + ctx, cancel := context.WithCancel(context.Background()) + suite.invoiceUpdateSubCancelFn = cancel + go svc.InvoiceUpdateSubscription(ctx) + suite.service = svc + e := echo.New() + + e.HTTPErrorHandler = responses.HTTPErrorHandler + e.Validator = &lib.CustomValidator{Validator: validator.New()} + suite.echo = e + assert.Equal(suite.T(), 1, len(users)) + assert.Equal(suite.T(), 1, len(userTokens)) + suite.aliceLogin = users[0] + suite.aliceToken = userTokens[0] + suite.echo.Use(tokens.Middleware([]byte(suite.service.Config.JWTSecret))) + suite.echo.GET("/balance", controllers.NewBalanceController(suite.service).Balance) + suite.echo.POST("/addinvoice", controllers.NewAddInvoiceController(suite.service).AddInvoice) + suite.echo.POST("/payinvoice", controllers.NewPayInvoiceController(suite.service).PayInvoice) + suite.echo.POST("/keysend", controllers.NewKeySendController(suite.service).KeySend) +} + +func (suite *KeySendTestSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func (suite *KeySendTestSuite) TestKeysendPayment() { + aliceFundingSats := 1000 + externalSatRequested := 500 + //fund alice account + invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoiceResponse.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + //wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + + suite.createKeySendReq(int64(externalSatRequested), "key send test", simnetLnd3PubKey, suite.aliceToken) + + // check that balance was reduced + userId := getUserIdFromToken(suite.aliceToken) + aliceBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested), aliceBalance) +} + +func (suite *KeySendTestSuite) TestKeysendPaymentNonExistentDestination() { + aliceFundingSats := 1000 + externalSatRequested := 500 + //fund alice account + invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoiceResponse.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + //wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + + suite.createKeySendReqError(int64(externalSatRequested), "key send test", "12345", suite.aliceToken) +} + +func TestKeySendTestSuite(t *testing.T) { + suite.Run(t, new(KeySendTestSuite)) +} diff --git a/integration_tests/util.go b/integration_tests/util.go index 4de82b7..9030694 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -33,6 +33,9 @@ const ( // This will cause payment to be routed through lnd2, which will charge a fee (lnd default fee 1 sat base + 1 ppm). lnd3RegtestAddress = "rpc.lnd3.regtest.getalby.com:443" lnd3RegtestMacaroonHex = "0201036c6e6402f801030a102a5aa69a5efdf4b4a55a5304b164641f1201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620defbb5a809262297fd661a9ab6d3deb4b7acca4f1309c79addb952f0dc2d8c82" + simnetLnd1PubKey = "0242898f86064c2fd72de22059c947a83ba23e9d97aedeae7b6dba647123f1d71b" + simnetLnd2PubKey = "025c1d5d1b4c983cc6350fc2d756fbb59b4dc365e45e87f8e3afe07e24013e8220" + simnetLnd3PubKey = "03c7092d076f799ab18806743634b4c9bb34e351bdebc91d5b35963f3dc63ec5aa" ) func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) { @@ -154,6 +157,44 @@ func (suite *TestSuite) createAddInvoiceReq(amt int, memo, token string) *contro return invoiceResponse } +func (suite *TestSuite) createKeySendReq(amount int64, memo, destination, token string) *controllers.KeySendResponseBody { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.KeySendRequestBody{ + Amount: amount, + Destination: destination, + Memo: memo, + })) + req := httptest.NewRequest(http.MethodPost, "/keysend", &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + suite.echo.ServeHTTP(rec, req) + + keySendResponse := &controllers.KeySendResponseBody{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(keySendResponse)) + return keySendResponse +} + +func (suite *TestSuite) createKeySendReqError(amount int64, memo, destination, token string) *responses.ErrorResponse { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.KeySendRequestBody{ + Amount: amount, + Destination: destination, + Memo: memo, + })) + req := httptest.NewRequest(http.MethodPost, "/keysend", &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + suite.echo.ServeHTTP(rec, req) + + errorResponse := &responses.ErrorResponse{} + assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) + return errorResponse +} + func (suite *TestSuite) createPayInvoiceReq(payReq string, token string) *controllers.PayInvoiceResponseBody { rec := httptest.NewRecorder() var buf bytes.Buffer diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 498044d..90702a8 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/getAlby/lndhub.go/common" "github.com/getAlby/lndhub.go/db/models" + "github.com/getAlby/lndhub.go/lnd" "github.com/getsentry/sentry-go" "github.com/labstack/gommon/random" "github.com/lightningnetwork/lnd/lnrpc" @@ -100,25 +102,14 @@ func (svc *LndhubService) SendInternalPayment(ctx context.Context, invoice *mode func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.Invoice) (SendPaymentResponse, error) { sendPaymentResponse := SendPaymentResponse{} - // TODO: set dynamic fee limit - feeLimit := lnrpc.FeeLimit{ - //Limit: &lnrpc.FeeLimit_Percent{ - // Percent: 2, - //}, - Limit: &lnrpc.FeeLimit_Fixed{ - Fixed: 300, - }, - } - // Prepare the LNRPC call - sendPaymentRequest := lnrpc.SendRequest{ - PaymentRequest: invoice.PaymentRequest, - Amt: invoice.Amount, - FeeLimit: &feeLimit, + sendPaymentRequest, err := createLnRpcSendRequest(invoice) + if err != nil { + return sendPaymentResponse, err } // Execute the payment - sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, &sendPaymentRequest) + sendPaymentResult, err := svc.LndClient.SendPaymentSync(ctx, sendPaymentRequest) if err != nil { return sendPaymentResponse, err } @@ -138,6 +129,44 @@ func (svc *LndhubService) SendPaymentSync(ctx context.Context, invoice *models.I return sendPaymentResponse, nil } +func createLnRpcSendRequest(invoice *models.Invoice) (*lnrpc.SendRequest, error) { + // TODO: set dynamic fee limit + feeLimit := lnrpc.FeeLimit{ + //Limit: &lnrpc.FeeLimit_Percent{ + // Percent: 2, + //}, + Limit: &lnrpc.FeeLimit_Fixed{ + Fixed: 300, + }, + } + + if !invoice.Keysend { + return &lnrpc.SendRequest{ + PaymentRequest: invoice.PaymentRequest, + Amt: invoice.Amount, + FeeLimit: &feeLimit, + }, nil + } + + preImage := makePreimageHex() + pHash := sha256.New() + pHash.Write(preImage) + // Prepare the LNRPC call + //See: https://github.com/hsjoberg/blixt-wallet/blob/9fcc56a7dc25237bc14b85e6490adb9e044c009c/src/lndmobile/index.ts#L251-L270 + destBytes, err := hex.DecodeString(invoice.DestinationPubkeyHex) + if err != nil { + return nil, err + } + return &lnrpc.SendRequest{ + Dest: destBytes, + Amt: invoice.Amount, + PaymentHash: pHash.Sum(nil), + FeeLimit: &feeLimit, + DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ}, + DestCustomRecords: map[uint64][]byte{KEYSEND_CUSTOM_RECORD: preImage, TLV_WHATSAT_MESSAGE: []byte(invoice.Memo)}, + }, nil +} + func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoice) (*SendPaymentResponse, error) { userId := invoice.UserID @@ -275,19 +304,20 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * return nil } -func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq) (*models.Invoice, error) { +func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, lnPayReq *lnd.LNPayReq) (*models.Invoice, error) { // Initialize new DB invoice invoice := models.Invoice{ Type: common.InvoiceTypeOutgoing, UserID: userID, PaymentRequest: paymentRequest, - RHash: decodedInvoice.PaymentHash, - Amount: decodedInvoice.NumSatoshis, + RHash: lnPayReq.PayReq.PaymentHash, + Amount: lnPayReq.PayReq.NumSatoshis, State: common.InvoiceStateInitialized, - DestinationPubkeyHex: decodedInvoice.Destination, - DescriptionHash: decodedInvoice.DescriptionHash, - Memo: decodedInvoice.Description, - ExpiresAt: bun.NullTime{Time: time.Unix(decodedInvoice.Timestamp, 0).Add(time.Duration(decodedInvoice.Expiry) * time.Second)}, + DestinationPubkeyHex: lnPayReq.PayReq.Destination, + DescriptionHash: lnPayReq.PayReq.DescriptionHash, + Memo: lnPayReq.PayReq.Description, + Keysend: lnPayReq.Keysend, + ExpiresAt: bun.NullTime{Time: time.Unix(lnPayReq.PayReq.Timestamp, 0).Add(time.Duration(lnPayReq.PayReq.Expiry) * time.Second)}, } // Save invoice diff --git a/lib/service/ln.go b/lib/service/ln.go index a9f609a..f139e2b 100644 --- a/lib/service/ln.go +++ b/lib/service/ln.go @@ -6,6 +6,13 @@ import ( "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{}) } diff --git a/lnd/lnd.go b/lnd/lnd.go index dbfe8c8..bdfe887 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -15,6 +15,11 @@ import ( "gopkg.in/macaroon.v2" ) +type LNPayReq struct { + PayReq *lnrpc.PayReq + Keysend bool +} + // LNDoptions are the options for the connection to the lnd node. type LNDoptions struct { Address string diff --git a/main.go b/main.go index c67ff7e..19b5843 100644 --- a/main.go +++ b/main.go @@ -134,6 +134,7 @@ func main() { secured.GET("/checkpayment/:payment_hash", controllers.NewCheckPaymentController(svc).CheckPayment) secured.GET("/balance", controllers.NewBalanceController(svc).Balance) secured.GET("/getinfo", controllers.NewGetInfoController(svc).GetInfo) + secured.POST("/keysend", controllers.NewKeySendController(svc).KeySend) // These endpoints are currently not supported and we return a blank response for backwards compatibility blankController := controllers.NewBlankController(svc)