From b99182db642a4a310f43d7199beaa302d68e2bfb Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 00:23:09 +0100 Subject: [PATCH 01/15] Add configurable fixed amount for fee --- .env_example | 1 + lib/service/config.go | 1 + 2 files changed, 2 insertions(+) diff --git a/.env_example b/.env_example index d955a9a..97a12b0 100644 --- a/.env_example +++ b/.env_example @@ -6,3 +6,4 @@ JWT_EXPIRY=604800 LND_ADDRESS= LND_MACAROON_HEX= LND_CERT_HEX= +FIXED_FEE=10 \ No newline at end of file diff --git a/lib/service/config.go b/lib/service/config.go index 44b4526..c824619 100644 --- a/lib/service/config.go +++ b/lib/service/config.go @@ -11,4 +11,5 @@ type Config struct { LNDCertHex string `envconfig:"LND_CERT_HEX"` CustomName string `envconfig:"CUSTOM_NAME"` Port int `envconfig:"PORT" default:"3000"` + FixedFee int `envconfig:"FIXED_FEE" default:"10"` } From 38585286072eb2145b9b24b130c6e1993e627020 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 00:23:37 +0100 Subject: [PATCH 02/15] Add transaction entry for fee --- lib/service/invoices.go | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 9b43d93..48d05e0 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -190,7 +190,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic // The payment was successful. invoice.Preimage = paymentResponse.PaymentPreimageStr - err = svc.HandleSuccessfulPayment(context.Background(), invoice) + err = svc.HandleSuccessfulPayment(context.Background(), invoice, entry) return &paymentResponse, err } @@ -223,11 +223,33 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode return err } -func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice *models.Invoice) error { +func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice *models.Invoice, parentEntry models.TransactionEntry) error { + // Get the user's fee account for the transaction entry, current account is already there in parent entry + feeAccount, err := svc.AccountFor(ctx, common.AccountTypeFees, invoice.UserID) + if err != nil { + svc.Logger.Errorf("Could not find fees account user_id:%v", invoice.UserID) + return err + } + // add transaction entry for fee + entry := models.TransactionEntry{ + UserID: invoice.UserID, + InvoiceID: invoice.ID, + CreditAccountID: feeAccount.ID, + DebitAccountID: parentEntry.DebitAccountID, + Amount: int64(svc.Config.FixedFee), + ParentID: parentEntry.ID, + } + _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) + if err != nil { + sentry.CaptureException(err) + svc.Logger.Errorf("Could not insert fee transaction entry user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + return err + } + invoice.State = common.InvoiceStateSettled invoice.SettledAt = schema.NullTime{Time: time.Now()} - _, err := svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) + _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) if err != nil { sentry.CaptureException(err) svc.Logger.Errorf("Could not update sucessful payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) From e489320363f3336d541fce9a4e53ba3ece40a2cb Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 19:21:16 +0100 Subject: [PATCH 03/15] Change transaction entries count in existing tests --- integration_tests/internal_payment_test.go | 8 ++++---- integration_tests/outgoing_payment_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index fc3ff08..67cc216 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -154,11 +154,11 @@ func (suite *PaymentTestSuite) TestInternalPaymentFail() { } // check if there are 4 transaction entries, with reversed credit and debit account ids for last 2 - assert.Equal(suite.T(), 4, len(transactonEntries)) - assert.Equal(suite.T(), transactonEntries[2].CreditAccountID, transactonEntries[3].DebitAccountID) - assert.Equal(suite.T(), transactonEntries[2].DebitAccountID, transactonEntries[3].CreditAccountID) - assert.Equal(suite.T(), transactonEntries[2].Amount, int64(bobSatRequested)) + assert.Equal(suite.T(), 5, len(transactonEntries)) + assert.Equal(suite.T(), transactonEntries[3].CreditAccountID, transactonEntries[4].DebitAccountID) + assert.Equal(suite.T(), transactonEntries[3].DebitAccountID, transactonEntries[4].CreditAccountID) assert.Equal(suite.T(), transactonEntries[3].Amount, int64(bobSatRequested)) + assert.Equal(suite.T(), transactonEntries[4].Amount, int64(bobSatRequested)) // assert that balance was reduced only once assert.Equal(suite.T(), int64(aliceFundingSats)-int64(bobSatRequested), int64(aliceBalance)) } diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 1a8ec20..f5465e9 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -48,5 +48,5 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { if err != nil { fmt.Printf("Error when getting transaction entries %v\n", err.Error()) } - assert.Equal(suite.T(), 2, len(transactonEntries)) + assert.Equal(suite.T(), 3, len(transactonEntries)) } From 5033a335937ca456f1a636cf49a5bce79ab9a148 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 19:40:46 +0100 Subject: [PATCH 04/15] Add fee handling to tests --- integration_tests/internal_payment_test.go | 7 ++++--- integration_tests/outgoing_payment_test.go | 3 ++- integration_tests/util.go | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 67cc216..cfbb9aa 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -111,8 +111,9 @@ func (suite *PaymentTestSuite) TestInternalPayment() { } func (suite *PaymentTestSuite) TestInternalPaymentFail() { - aliceFundingSats := 1000 + aliceFundingSats := 1010 bobSatRequested := 500 + fee := 10 //fund alice account invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) sendPaymentRequest := lnrpc.SendRequest{ @@ -153,14 +154,14 @@ func (suite *PaymentTestSuite) TestInternalPaymentFail() { fmt.Printf("Error when getting balance %v\n", err.Error()) } - // check if there are 4 transaction entries, with reversed credit and debit account ids for last 2 + // check if there are 5 transaction entries, with reversed credit and debit account ids for last 2 assert.Equal(suite.T(), 5, len(transactonEntries)) assert.Equal(suite.T(), transactonEntries[3].CreditAccountID, transactonEntries[4].DebitAccountID) assert.Equal(suite.T(), transactonEntries[3].DebitAccountID, transactonEntries[4].CreditAccountID) assert.Equal(suite.T(), transactonEntries[3].Amount, int64(bobSatRequested)) assert.Equal(suite.T(), transactonEntries[4].Amount, int64(bobSatRequested)) // assert that balance was reduced only once - assert.Equal(suite.T(), int64(aliceFundingSats)-int64(bobSatRequested), int64(aliceBalance)) + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(bobSatRequested+fee), int64(aliceBalance)) } func TestInternalPaymentTestSuite(t *testing.T) { diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index f5465e9..388f2d7 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -12,6 +12,7 @@ import ( func (suite *PaymentTestSuite) TestOutGoingPayment() { aliceFundingSats := 1000 externalSatRequested := 500 + fee := 10 //fund alice account invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) sendPaymentRequest := lnrpc.SendRequest{ @@ -41,7 +42,7 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { if err != nil { fmt.Printf("Error when getting balance %v\n", err.Error()) } - assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested), aliceBalance) + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested+fee), aliceBalance) // check that no additional transaction entry was created transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) diff --git a/integration_tests/util.go b/integration_tests/util.go index 5f0a443..5194b77 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -42,6 +42,7 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi JWTExpiry: 3600, LNDAddress: lnd1RegtestAddress, LNDMacaroonHex: lnd1RegtestMacaroonHex, + FixedFee: 10, } dbConn, err := db.Open(c.DatabaseUri) if err != nil { From 31cf4fbd1361333a510c276310dbba5adf4c19fe Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 20:08:16 +0100 Subject: [PATCH 05/15] Add transaction entries checks in outgoing payment test --- integration_tests/outgoing_payment_test.go | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 388f2d7..3b1fa32 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/getAlby/lndhub.go/common" "github.com/lightningnetwork/lnd/lnrpc" "github.com/stretchr/testify/assert" ) @@ -49,5 +50,36 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { if err != nil { fmt.Printf("Error when getting transaction entries %v\n", err.Error()) } + // verify transaction entries data + feeAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeFees, userId) + incomingAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeIncoming, userId) + outgoingAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeOutgoing, userId) + currentAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeCurrent, userId) + + outgoingInvoices, _ := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) + incomingInvoices, _ := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeIncoming) + assert.Equal(suite.T(), 1, len(outgoingInvoices)) + assert.Equal(suite.T(), 1, len(incomingInvoices)) + assert.Equal(suite.T(), 3, len(transactonEntries)) + + assert.Equal(suite.T(), int64(aliceFundingSats), transactonEntries[0].Amount) + assert.Equal(suite.T(), currentAccount.ID, transactonEntries[0].CreditAccountID) + assert.Equal(suite.T(), incomingAccount.ID, transactonEntries[0].DebitAccountID) + assert.Equal(suite.T(), int64(0), transactonEntries[0].ParentID) + assert.Equal(suite.T(), incomingInvoices[0].ID, transactonEntries[0].InvoiceID) + + assert.Equal(suite.T(), int64(externalSatRequested), transactonEntries[1].Amount) + assert.Equal(suite.T(), outgoingAccount.ID, transactonEntries[1].CreditAccountID) + assert.Equal(suite.T(), currentAccount.ID, transactonEntries[1].DebitAccountID) + assert.Equal(suite.T(), int64(0), transactonEntries[1].ParentID) + assert.Equal(suite.T(), outgoingInvoices[0].ID, transactonEntries[1].InvoiceID) + + assert.Equal(suite.T(), int64(fee), transactonEntries[2].Amount) + assert.Equal(suite.T(), feeAccount.ID, transactonEntries[2].CreditAccountID) + assert.Equal(suite.T(), currentAccount.ID, transactonEntries[2].DebitAccountID) + assert.Equal(suite.T(), outgoingInvoices[0].ID, transactonEntries[2].InvoiceID) + + // make sure fee entry parent id is previous entry + assert.Equal(suite.T(), transactonEntries[1].ID, transactonEntries[2].ParentID) } From 0669546a08d7f77fd82a231462996cd47cfed9d4 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 20:13:00 +0100 Subject: [PATCH 06/15] Add amount checks for internal payment test transaction entries --- integration_tests/internal_payment_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index cfbb9aa..589d6ee 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -156,6 +156,9 @@ func (suite *PaymentTestSuite) TestInternalPaymentFail() { // check if there are 5 transaction entries, with reversed credit and debit account ids for last 2 assert.Equal(suite.T(), 5, len(transactonEntries)) + assert.Equal(suite.T(), int64(aliceFundingSats), transactonEntries[0].Amount) + assert.Equal(suite.T(), int64(bobSatRequested), transactonEntries[1].Amount) + assert.Equal(suite.T(), int64(fee), transactonEntries[2].Amount) assert.Equal(suite.T(), transactonEntries[3].CreditAccountID, transactonEntries[4].DebitAccountID) assert.Equal(suite.T(), transactonEntries[3].DebitAccountID, transactonEntries[4].CreditAccountID) assert.Equal(suite.T(), transactonEntries[3].Amount, int64(bobSatRequested)) From d572b1585f9701a9226eb6a94bec254cfbfd4224 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 25 Feb 2022 20:24:06 +0100 Subject: [PATCH 07/15] Add fee check to internal payment test --- integration_tests/internal_payment_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 589d6ee..600b3e4 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -85,6 +85,7 @@ func (suite *PaymentTestSuite) TearDownTest() { func (suite *PaymentTestSuite) TestInternalPayment() { aliceFundingSats := 1000 bobSatRequested := 500 + fee := 10 //fund alice account invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) sendPaymentRequest := lnrpc.SendRequest{ @@ -102,12 +103,31 @@ func (suite *PaymentTestSuite) TestInternalPayment() { //pay bob from alice payResponse := suite.createPayInvoiceReq(bobInvoice.PayReq, suite.aliceToken) assert.NotEmpty(suite.T(), payResponse.PaymentPreimage) + + aliceId := getUserIdFromToken(suite.aliceToken) + bobId := getUserIdFromToken(suite.bobToken) + //try to pay Bob more than we currently have //create invoice for bob tooMuch := suite.createAddInvoiceReq(10000, "integration test internal payment bob", suite.bobToken) //pay bob from alice errorResp := suite.createPayInvoiceReqError(tooMuch.PayReq, suite.aliceToken) assert.Equal(suite.T(), responses.NotEnoughBalanceError.Code, errorResp.Code) + + transactonEntriesAlice, _ := suite.service.TransactionEntriesFor(context.Background(), aliceId) + aliceBalance, _ := suite.service.CurrentUserBalance(context.Background(), aliceId) + assert.Equal(suite.T(), 3, len(transactonEntriesAlice)) + assert.Equal(suite.T(), int64(aliceFundingSats), transactonEntriesAlice[0].Amount) + assert.Equal(suite.T(), int64(bobSatRequested), transactonEntriesAlice[1].Amount) + assert.Equal(suite.T(), int64(fee), transactonEntriesAlice[2].Amount) + assert.Equal(suite.T(), transactonEntriesAlice[1].ID, transactonEntriesAlice[2].ParentID) + assert.Equal(suite.T(), int64(aliceFundingSats-bobSatRequested-fee), aliceBalance) + + bobBalance, _ := suite.service.CurrentUserBalance(context.Background(), bobId) + transactionEntriesBob, _ := suite.service.TransactionEntriesFor(context.Background(), bobId) + assert.Equal(suite.T(), 1, len(transactionEntriesBob)) + assert.Equal(suite.T(), int64(bobSatRequested), transactionEntriesBob[0].Amount) + assert.Equal(suite.T(), int64(bobSatRequested), bobBalance) } func (suite *PaymentTestSuite) TestInternalPaymentFail() { From 735cd6a839a2625a45ce243139607aec12d5d747 Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Tue, 1 Mar 2022 10:06:29 +0100 Subject: [PATCH 08/15] fee handling should come from payment response --- db/migrations/20220301100000_invoice_add_fee.up.sql | 1 + db/models/invoice.go | 1 + integration_tests/util.go | 1 - lib/service/config.go | 1 - lib/service/invoices.go | 3 ++- 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 db/migrations/20220301100000_invoice_add_fee.up.sql diff --git a/db/migrations/20220301100000_invoice_add_fee.up.sql b/db/migrations/20220301100000_invoice_add_fee.up.sql new file mode 100644 index 0000000..375ca3b --- /dev/null +++ b/db/migrations/20220301100000_invoice_add_fee.up.sql @@ -0,0 +1 @@ +alter table invoices ADD COLUMN fee bigint; \ No newline at end of file diff --git a/db/models/invoice.go b/db/models/invoice.go index a117adf..b790d52 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -14,6 +14,7 @@ type Invoice struct { UserID int64 `json:"user_id" validate:"required"` User *User `bun:"rel:belongs-to,join:user_id=id"` Amount int64 `json:"amount" validate:"gte=0"` + Fee int64 `json:"fee" validate:"gte=0"` Memo string `json:"memo" bun:",nullzero"` DescriptionHash string `json:"description_hash" bun:",nullzero"` PaymentRequest string `json:"payment_request" bun:",nullzero"` diff --git a/integration_tests/util.go b/integration_tests/util.go index 5194b77..5f0a443 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -42,7 +42,6 @@ func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *servi JWTExpiry: 3600, LNDAddress: lnd1RegtestAddress, LNDMacaroonHex: lnd1RegtestMacaroonHex, - FixedFee: 10, } dbConn, err := db.Open(c.DatabaseUri) if err != nil { diff --git a/lib/service/config.go b/lib/service/config.go index c824619..44b4526 100644 --- a/lib/service/config.go +++ b/lib/service/config.go @@ -11,5 +11,4 @@ type Config struct { LNDCertHex string `envconfig:"LND_CERT_HEX"` CustomName string `envconfig:"CUSTOM_NAME"` Port int `envconfig:"PORT" default:"3000"` - FixedFee int `envconfig:"FIXED_FEE" default:"10"` } diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 48d05e0..dfcfa97 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -190,6 +190,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic // The payment was successful. invoice.Preimage = paymentResponse.PaymentPreimageStr + invoice.Fee = paymentResponse.PaymentRoute.TotalFees err = svc.HandleSuccessfulPayment(context.Background(), invoice, entry) return &paymentResponse, err } @@ -236,7 +237,7 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * InvoiceID: invoice.ID, CreditAccountID: feeAccount.ID, DebitAccountID: parentEntry.DebitAccountID, - Amount: int64(svc.Config.FixedFee), + Amount: int64(invoice.Fee), ParentID: parentEntry.ID, } _, err = svc.DB.NewInsert().Model(&entry).Exec(ctx) From c63aa0c7d228806252ea8627b7db8dd7c6d6dd2e Mon Sep 17 00:00:00 2001 From: kiwiidb Date: Tue, 1 Mar 2022 13:05:39 +0100 Subject: [PATCH 09/15] add lnd3 credentials --- integration_tests/util.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/integration_tests/util.go b/integration_tests/util.go index 5f0a443..8903128 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -29,6 +29,11 @@ const ( lnd1RegtestMacaroonHex = "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed" lnd2RegtestAddress = "rpc.lnd2.regtest.getalby.com:443" lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" + //TODO: use lnd3 for a funding client + //when testing out fee handling for payments + //done by lnd-1. This will cause a fee to be charged by lnd-2 (lnd default fee 1 sat base + 1 ppm) + lnd3RegtestAddress = "rpc.lnd3.regtest.getalby.com:443" + lnd3RegtestMacaroonHex = "0201036c6e6402f801030a102a5aa69a5efdf4b4a55a5304b164641f1201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620defbb5a809262297fd661a9ab6d3deb4b7acca4f1309c79addb952f0dc2d8c82" ) func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) { From cd6a08690ce253cca566e73e645f9f561536020a Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Tue, 1 Mar 2022 23:22:45 +0100 Subject: [PATCH 10/15] Fix PR comments --- db/models/invoice.go | 2 +- lib/service/invoices.go | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/db/models/invoice.go b/db/models/invoice.go index b790d52..b59dd3b 100644 --- a/db/models/invoice.go +++ b/db/models/invoice.go @@ -14,7 +14,7 @@ type Invoice struct { UserID int64 `json:"user_id" validate:"required"` User *User `bun:"rel:belongs-to,join:user_id=id"` Amount int64 `json:"amount" validate:"gte=0"` - Fee int64 `json:"fee" validate:"gte=0"` + Fee int64 `json:"fee" bun:",nullzero"` Memo string `json:"memo" bun:",nullzero"` DescriptionHash string `json:"description_hash" bun:",nullzero"` PaymentRequest string `json:"payment_request" bun:",nullzero"` diff --git a/lib/service/invoices.go b/lib/service/invoices.go index dfcfa97..48630a0 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -189,6 +189,7 @@ func (svc *LndhubService) PayInvoice(ctx context.Context, invoice *models.Invoic paymentResponse.TransactionEntry = &entry // The payment was successful. + // These changes to the invoice are persisted in the `HandleSuccessfulPayment` function invoice.Preimage = paymentResponse.PaymentPreimageStr invoice.Fee = paymentResponse.PaymentRoute.TotalFees err = svc.HandleSuccessfulPayment(context.Background(), invoice, entry) @@ -225,12 +226,22 @@ func (svc *LndhubService) HandleFailedPayment(ctx context.Context, invoice *mode } func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice *models.Invoice, parentEntry models.TransactionEntry) error { + invoice.State = common.InvoiceStateSettled + invoice.SettledAt = schema.NullTime{Time: time.Now()} + + _, err := svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) + if err != nil { + sentry.CaptureException(err) + svc.Logger.Errorf("Could not update sucessful payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + } + // Get the user's fee account for the transaction entry, current account is already there in parent entry feeAccount, err := svc.AccountFor(ctx, common.AccountTypeFees, invoice.UserID) if err != nil { svc.Logger.Errorf("Could not find fees account user_id:%v", invoice.UserID) return err } + // add transaction entry for fee entry := models.TransactionEntry{ UserID: invoice.UserID, @@ -247,15 +258,7 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * return err } - invoice.State = common.InvoiceStateSettled - invoice.SettledAt = schema.NullTime{Time: time.Now()} - - _, err = svc.DB.NewUpdate().Model(invoice).WherePK().Exec(ctx) - if err != nil { - sentry.CaptureException(err) - svc.Logger.Errorf("Could not update sucessful payment invoice user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) - } - return err + return nil } func (svc *LndhubService) AddOutgoingInvoice(ctx context.Context, userID int64, paymentRequest string, decodedInvoice *lnrpc.PayReq) (*models.Invoice, error) { From 9c8102082945a5c449c2334256f304c09aebc8ea Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 4 Mar 2022 17:24:38 +0100 Subject: [PATCH 11/15] Fix tests --- integration_tests/internal_payment_test.go | 12 +++++++----- integration_tests/outgoing_payment_test.go | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 600b3e4..30c4543 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -34,8 +34,8 @@ type PaymentTestSuite struct { func (suite *PaymentTestSuite) SetupSuite() { lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{ - Address: lnd2RegtestAddress, - MacaroonHex: lnd2RegtestMacaroonHex, + Address: lnd3RegtestAddress, + MacaroonHex: lnd3RegtestMacaroonHex, }) if err != nil { log.Fatalf("Error setting up funding client: %v", err) @@ -85,7 +85,8 @@ func (suite *PaymentTestSuite) TearDownTest() { func (suite *PaymentTestSuite) TestInternalPayment() { aliceFundingSats := 1000 bobSatRequested := 500 - fee := 10 + // currently fee is 0 for internal payments + fee := 0 //fund alice account invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) sendPaymentRequest := lnrpc.SendRequest{ @@ -131,9 +132,10 @@ func (suite *PaymentTestSuite) TestInternalPayment() { } func (suite *PaymentTestSuite) TestInternalPaymentFail() { - aliceFundingSats := 1010 + aliceFundingSats := 1000 bobSatRequested := 500 - fee := 10 + // currently fee is 0 for internal payments + fee := 0 //fund alice account invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test internal payment alice", suite.aliceToken) sendPaymentRequest := lnrpc.SendRequest{ diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 3b1fa32..3919bc1 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -13,7 +13,8 @@ import ( func (suite *PaymentTestSuite) TestOutGoingPayment() { aliceFundingSats := 1000 externalSatRequested := 500 - fee := 10 + // 1 sat + 1 ppm + fee := 1 //fund alice account invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) sendPaymentRequest := lnrpc.SendRequest{ From 831895a8e0903c5941ea4effbb72c028f073632a Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 4 Mar 2022 17:28:04 +0100 Subject: [PATCH 12/15] Remove integration tests util todo and fix comment --- integration_tests/util.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/integration_tests/util.go b/integration_tests/util.go index 8903128..7f82451 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -29,9 +29,8 @@ const ( lnd1RegtestMacaroonHex = "0201036c6e6402f801030a10e2133a1cac2c5b4d56e44e32dc64c8551201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620c4f9783e0873fa50a2091806f5ebb919c5dc432e33800b401463ada6485df0ed" lnd2RegtestAddress = "rpc.lnd2.regtest.getalby.com:443" lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" - //TODO: use lnd3 for a funding client - //when testing out fee handling for payments - //done by lnd-1. This will cause a fee to be charged by lnd-2 (lnd default fee 1 sat base + 1 ppm) + // Use lnd3 for a funding client when testing out fee handling for payments done by lnd-1, since lnd3 doesn't have a direct channel to lnd1. + // 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" ) @@ -39,7 +38,7 @@ const ( func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) { // change this if you want to run tests using sqlite // dbUri := "file:data_test.db" - //make sure the datbase is empty every time you run the test suite + // make sure the datbase is empty every time you run the test suite dbUri := "postgresql://user:password@localhost/lndhub?sslmode=disable" c := &service.Config{ DatabaseUri: dbUri, From a881131b8dbd112fb9212f8668f5e074fadb3ba3 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 4 Mar 2022 18:29:31 +0100 Subject: [PATCH 13/15] Adapt balance constraint --- .../20220120000700_add_constraints.up.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/db/migrations/20220120000700_add_constraints.up.go b/db/migrations/20220120000700_add_constraints.up.go index cdbd438..6d09e1f 100644 --- a/db/migrations/20220120000700_add_constraints.up.go +++ b/db/migrations/20220120000700_add_constraints.up.go @@ -34,6 +34,7 @@ func init() { DECLARE sum BIGINT; debit_account_type VARCHAR; + credit_account_type VARCHAR; BEGIN -- LOCK the account if the transaction is not from an incoming account @@ -48,8 +49,18 @@ func init() { -- This can happen when two transactions try to access the same account FOR UPDATE NOWAIT; - -- If it is an incoming account return; otherwise check the balance - IF debit_account_type IS NULL + -- check if credit_account type is fees, if it's fees we don't check for negative balance constraint + SELECT INTO credit_account_type type + FROM accounts + WHERE id = NEW.credit_account_id AND type <> 'fees' + -- IMPORTANT: lock rows but do not wait for another lock to be released. + -- Waiting would result in a deadlock because two parallel transactions could try to lock the same rows + -- NOWAIT reports an error rather than waiting for the lock to be released + -- This can happen when two transactions try to access the same account + FOR UPDATE NOWAIT; + + -- If it is an debit incoming account or fees credit account return; otherwise check the balance + IF debit_account_type IS NULL OR credit_account_type IS NULL THEN RETURN NEW; END IF; @@ -60,7 +71,7 @@ func init() { WHERE account_ledgers.account_id = NEW.debit_account_id; -- IF the account would go negative raise an exception - IF sum < 0 AND debit_account_type != 'incoming' + IF sum < 0 THEN RAISE EXCEPTION 'invalid balance [user_id:%] [debit_account_id:%] balance [%]', NEW.user_id, From f13e8888bc33ae24f56ed1e180e7d17cb7e1789a Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 4 Mar 2022 18:32:35 +0100 Subject: [PATCH 14/15] Add test for negative balance --- integration_tests/outgoing_payment_test.go | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 3919bc1..a09be12 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -84,3 +84,81 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { // make sure fee entry parent id is previous entry assert.Equal(suite.T(), transactonEntries[1].ID, transactonEntries[2].ParentID) } + +func (suite *PaymentTestSuite) TestOutGoingPaymentWithNegativeBalance() { + // this will cause balance to go to -1 + aliceFundingSats := 1000 + externalSatRequested := 1000 + // 1 sat + 1 ppm + fee := 1 + //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) + + //create external invoice + externalInvoice := lnrpc.Invoice{ + Memo: "integration tests: external pay from alice", + Value: int64(externalSatRequested), + } + invoice, err := suite.fundingClient.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + //pay external from alice + payResponse := suite.createPayInvoiceReq(invoice.PaymentRequest, suite.aliceToken) + assert.NotEmpty(suite.T(), payResponse.PaymentPreimage) + + // 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+fee), aliceBalance) + assert.Equal(suite.T(), int64(-1), aliceBalance) + + // check that no additional transaction entry was created + transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting transaction entries %v\n", err.Error()) + } + // verify transaction entries data + feeAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeFees, userId) + incomingAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeIncoming, userId) + outgoingAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeOutgoing, userId) + currentAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeCurrent, userId) + + outgoingInvoices, _ := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) + incomingInvoices, _ := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeIncoming) + assert.Equal(suite.T(), 1, len(outgoingInvoices)) + assert.Equal(suite.T(), 1, len(incomingInvoices)) + + assert.Equal(suite.T(), 3, len(transactonEntries)) + + assert.Equal(suite.T(), int64(aliceFundingSats), transactonEntries[0].Amount) + assert.Equal(suite.T(), currentAccount.ID, transactonEntries[0].CreditAccountID) + assert.Equal(suite.T(), incomingAccount.ID, transactonEntries[0].DebitAccountID) + assert.Equal(suite.T(), int64(0), transactonEntries[0].ParentID) + assert.Equal(suite.T(), incomingInvoices[0].ID, transactonEntries[0].InvoiceID) + + assert.Equal(suite.T(), int64(externalSatRequested), transactonEntries[1].Amount) + assert.Equal(suite.T(), outgoingAccount.ID, transactonEntries[1].CreditAccountID) + assert.Equal(suite.T(), currentAccount.ID, transactonEntries[1].DebitAccountID) + assert.Equal(suite.T(), int64(0), transactonEntries[1].ParentID) + assert.Equal(suite.T(), outgoingInvoices[0].ID, transactonEntries[1].InvoiceID) + + assert.Equal(suite.T(), int64(fee), transactonEntries[2].Amount) + assert.Equal(suite.T(), feeAccount.ID, transactonEntries[2].CreditAccountID) + assert.Equal(suite.T(), currentAccount.ID, transactonEntries[2].DebitAccountID) + assert.Equal(suite.T(), outgoingInvoices[0].ID, transactonEntries[2].InvoiceID) + + // make sure fee entry parent id is previous entry + assert.Equal(suite.T(), transactonEntries[1].ID, transactonEntries[2].ParentID) +} From 7e528489d7d5c856d5f39976d19101eab03d8282 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Mon, 7 Mar 2022 11:28:58 +0100 Subject: [PATCH 15/15] Add check for user balance after successful payment --- lib/service/invoices.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/service/invoices.go b/lib/service/invoices.go index 48630a0..498044d 100644 --- a/lib/service/invoices.go +++ b/lib/service/invoices.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "fmt" "math/rand" "time" @@ -258,6 +259,19 @@ func (svc *LndhubService) HandleSuccessfulPayment(ctx context.Context, invoice * return err } + userBalance, err := svc.CurrentUserBalance(ctx, entry.UserID) + if err != nil { + sentry.CaptureException(err) + svc.Logger.Errorf("Could not fetch user balance user_id:%v invoice_id:%v", invoice.UserID, invoice.ID) + return err + } + + if userBalance < 0 { + amountMsg := fmt.Sprintf("User balance is negative transaction_entry_id:%v user_id:%v amount:%v", entry.ID, entry.UserID, userBalance) + svc.Logger.Info(amountMsg) + sentry.CaptureMessage(amountMsg) + } + return nil }