Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lnclient/lnd/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
)

const SEND_PAYMENT_TIMEOUT = 50

type LNDService struct {
client *wrapper.LNDWrapper
nodeInfo *lnclient.NodeInfo
Expand Down Expand Up @@ -449,6 +451,7 @@ func (svc *LNDService) SendPaymentSync(payReq string, amount *uint64) (*lnclient
PaymentRequest: payReq,
MaxParts: MAX_PARTIAL_PAYMENTS,
FeeLimitMsat: int64(transactions.CalculateFeeReserveMsat(paymentAmountMsat)),
TimeoutSeconds: SEND_PAYMENT_TIMEOUT,
}

if amount != nil {
Expand Down Expand Up @@ -527,7 +530,6 @@ func (svc *LNDService) SendKeysend(amount uint64, destination string, custom_rec
destCustomRecords[record.Type] = decodedValue
}
const MAX_PARTIAL_PAYMENTS = 16
const SEND_PAYMENT_TIMEOUT = 50
const KEYSEND_CUSTOM_RECORD = 5482373484
destCustomRecords[KEYSEND_CUSTOM_RECORD] = preImageBytes
sendPaymentRequest := &routerrpc.SendPaymentRequest{
Expand Down
16 changes: 16 additions & 0 deletions tests/mock_ln_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ var MockLNClientHoldTransaction = &lnclient.Transaction{
}

type MockLn struct {
MakeInvoiceResponses []*lnclient.Transaction
MakeInvoiceErrors []error
PayInvoiceResponses []*lnclient.PayInvoiceResponse
PayInvoiceErrors []error
PaymentDelay *time.Duration
Expand Down Expand Up @@ -117,10 +119,24 @@ func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err er
}

func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
if len(mln.MakeInvoiceResponses) > 0 {
response := mln.MakeInvoiceResponses[0]
err := mln.MakeInvoiceErrors[0]
mln.MakeInvoiceResponses = mln.MakeInvoiceResponses[1:]
mln.MakeInvoiceErrors = mln.MakeInvoiceErrors[1:]
return response, err
}
return MockLNClientTransaction, nil
}

func (mln *MockLn) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *lnclient.Transaction, err error) {
if len(mln.MakeInvoiceResponses) > 0 {
response := mln.MakeInvoiceResponses[0]
err := mln.MakeInvoiceErrors[0]
mln.MakeInvoiceResponses = mln.MakeInvoiceResponses[1:]
mln.MakeInvoiceErrors = mln.MakeInvoiceErrors[1:]
return response, err
}
return MockLNClientHoldTransaction, nil
}

Expand Down
10 changes: 5 additions & 5 deletions transactions/hold_invoice_self_payment_consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,24 @@ import (
)

type holdInvoiceUpdatedConsumer struct {
paymentHash string
paymentRequest string
settledChannel chan<- *db.Transaction
canceledChannel chan<- *db.Transaction
}

func newHoldInvoiceUpdatedConsumer(paymentHash string, settledChannel chan<- *db.Transaction, canceledChannel chan<- *db.Transaction) *holdInvoiceUpdatedConsumer {
func newHoldInvoiceUpdatedConsumer(paymentRequest string, settledChannel chan<- *db.Transaction, canceledChannel chan<- *db.Transaction) *holdInvoiceUpdatedConsumer {
return &holdInvoiceUpdatedConsumer{
paymentHash: paymentHash,
paymentRequest: paymentRequest,
settledChannel: settledChannel,
canceledChannel: canceledChannel,
}
}

func (consumer *holdInvoiceUpdatedConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
if event.Event == "nwc_payment_received" && event.Properties.(*db.Transaction).PaymentHash == consumer.paymentHash {
if event.Event == "nwc_payment_received" && event.Properties.(*db.Transaction).PaymentRequest == consumer.paymentRequest {
consumer.settledChannel <- event.Properties.(*db.Transaction)
}
if event.Event == "nwc_hold_invoice_canceled" && event.Properties.(*db.Transaction).PaymentHash == consumer.paymentHash {
if event.Event == "nwc_hold_invoice_canceled" && event.Properties.(*db.Transaction).PaymentRequest == consumer.paymentRequest {
consumer.canceledChannel <- event.Properties.(*db.Transaction)
}
}
18 changes: 10 additions & 8 deletions transactions/payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ func TestSendPaymentSync_Duplicate_AlreadyPaid(t *testing.T) {
defer svc.Remove()

svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
AmountMsat: 123000,
State: constants.TRANSACTION_STATE_SETTLED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
AmountMsat: 123000,
})

transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
Expand All @@ -131,10 +132,11 @@ func TestSendPaymentSync_Duplicate_Pending(t *testing.T) {
defer svc.Remove()

svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
AmountMsat: 123000,
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
AmountMsat: 123000,
})

transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
Expand Down
175 changes: 175 additions & 0 deletions transactions/self_hold_payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/tests"
)
Expand Down Expand Up @@ -99,3 +101,176 @@ func TestSelfHoldPaymentCanceled(t *testing.T) {
assert.Equal(t, true, updatedHoldTransaction.Hold)
wg.Wait()
}

func TestWrappedInvoice(t *testing.T) {
ctx := context.TODO()

svc, err := tests.CreateTestService(t)
svc.Cfg.SetUpdate("LNBackendType", config.LDKBackendType, "")
require.NoError(t, err)
defer svc.Remove()

// invoices were created with long expiry in LND (Polar)
/*
lnd@grace:/$ lncli addinvoice --amt <amount> --preimage fcf200c74d9900dc77af17eb1f57c02eec0f94b5b169d3eee23df9a216a3411b --expiry 31536000
bash: amount: No such file or directory
lnd@grace:/$ lncli addinvoice --amt 1000 --preimage fcf200c74d9900dc77af17eb1f57c02eec0f94b5b169d3eee23df9a21
6a3411b --expiry 31536000
{
"r_hash": "8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e",
"payment_request": "lnbcrt10u1p5cammypp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5k9qse5srd9mfaxlgucr0p2gzf9464xte5xtqgjxyxj7794jxav9q9qxpqysgqnvwx0j9qxkx6k9efetdr0vdkrnp4vn23ud4gwpm0k28vf3v0rcmzfnued907r7ju6d86wr25ypt366szd0f7s28nzrvwmp4rck4358spfr9vx9",
"add_index": "1",
"payment_addr": "b1410cd20369769e9be8e606f0a902496baa9979a1960448c434bde2d646eb0a"
}
lnd@grace:/$ lncli addholdinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e --amt 1100
--expiry 31536000
[lncli] rpc error: code = Unknown desc = invoice with payment hash already exists
lnd@grace:/$ lncli cancelinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e
{}
lnd@grace:/$ lncli deletecanceledinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e
{
"status": "canceled invoice deleted successfully: invoice hash 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e"
}
lnd@grace:/$ lncli addholdinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e --amt 1100 --expiry 31536000
{
"payment_request": "lnbcrt11u1p5cama7pp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5mtvgejxel3yjyk55e58dgjg9v2mnxva9xn83yg05mducwz3ujhrq9qxpqysgqp48sr0r7x6hj5vcefn3wtj7g8r33agyp4aqfasyr2fptxp066wzppmr7rw9my3frezy65hw7u5l0tnqh7393x2km7tf2tk3efdl0c7qptan9m9",
"add_index": "2",
"payment_addr": "dad88cc8d9fc49225a94cd0ed4490562b73333a534cf1221f4db79870a3c95c6"
}
*/

// use the pubkey from Bob's invoice to activate self payments
svc.LNClient.(*tests.MockLn).Pubkey = "03a53c23a3e12cb3b16dc23c8a6d18e5930b480443c5f46860f553b33d74731342"

transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)

// Charlie creates invoice with payment hash
// Bob also creates invoice with payment hash, but it's a HOLD invoice one.

// Create 3 isolated apps: Charlie (invoice creator), Bob (wrapper), Alice (payer)
charlieApp, _, err := svc.AppsService.CreateApp("Charlie", "", 0, "", nil, []string{constants.MAKE_INVOICE_SCOPE}, true, nil)
require.NoError(t, err)
require.NotNil(t, charlieApp)

bobApp, _, err := svc.AppsService.CreateApp("Bob", "", 0, "", nil, []string{constants.MAKE_INVOICE_SCOPE, constants.PAY_INVOICE_SCOPE}, true, nil)
require.NoError(t, err)
require.NotNil(t, bobApp)

aliceApp, _, err := svc.AppsService.CreateApp("Alice", "", 0, "", nil, []string{constants.PAY_INVOICE_SCOPE}, true, nil)
require.NoError(t, err)
require.NotNil(t, aliceApp)

// created with sandbox.albylabs.com
// Charlie's 1000 sat invoice
mockCharlieInvoice := &lnclient.Transaction{
Type: "incoming",
Invoice: "lnbcrt10u1p5cammypp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5k9qse5srd9mfaxlgucr0p2gzf9464xte5xtqgjxyxj7794jxav9q9qxpqysgqnvwx0j9qxkx6k9efetdr0vdkrnp4vn23ud4gwpm0k28vf3v0rcmzfnued907r7ju6d86wr25ypt366szd0f7s28nzrvwmp4rck4358spfr9vx9",
Description: "mock hold invoice",
DescriptionHash: "",
Preimage: "fcf200c74d9900dc77af17eb1f57c02eec0f94b5b169d3eee23df9a216a3411b",
PaymentHash: "8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e",
Amount: 1000_000,
}

// Bob's 1100 sat invoice
// (same payment hash)

mockBobHoldInvoice := &lnclient.Transaction{
Type: "incoming",
Invoice: "lnbcrt11u1p5cama7pp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5mtvgejxel3yjyk55e58dgjg9v2mnxva9xn83yg05mducwz3ujhrq9qxpqysgqp48sr0r7x6hj5vcefn3wtj7g8r33agyp4aqfasyr2fptxp066wzppmr7rw9my3frezy65hw7u5l0tnqh7393x2km7tf2tk3efdl0c7qptan9m9",
Description: "mock hold invoice",
DescriptionHash: "",
Preimage: "",
PaymentHash: "8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e",
Amount: 1100_000,
}

svc.LNClient.(*tests.MockLn).MakeInvoiceResponses = []*lnclient.Transaction{
mockCharlieInvoice,
mockBobHoldInvoice,
}
svc.LNClient.(*tests.MockLn).MakeInvoiceErrors = []error{nil, nil}

var preimages = []string{tests.MockLNClientHoldTransaction.Preimage, tests.MockLNClientHoldTransaction.Preimage}
svc.LNClient.(*tests.MockLn).PayInvoiceResponses = []*lnclient.PayInvoiceResponse{{
Preimage: preimages[0],
}, {
Preimage: preimages[1],
}}
svc.LNClient.(*tests.MockLn).PayInvoiceErrors = []error{nil, nil}

// Charlie creates a standard invoice for 1000 sats
charlieInvoice, err := transactionsService.MakeInvoice(ctx, 1000, "Charlie invoice", "", 0, nil, svc.LNClient, &charlieApp.ID, nil, nil)
require.NoError(t, err)
require.False(t, charlieInvoice.Hold)
require.Equal(t, mockCharlieInvoice.Invoice, charlieInvoice.PaymentRequest)

// Bob creates a wrapped invoice with the same payment hash but higher amount (1100 sats)
// Bob acts as an intermediary, adding a fee of 100 sats
bobWrappedInvoice, err := transactionsService.MakeHoldInvoice(ctx, 1100, "Bob wrapped invoice", "", 0, charlieInvoice.PaymentHash, nil, svc.LNClient, &bobApp.ID, nil)
require.NoError(t, err)
require.True(t, bobWrappedInvoice.Hold)
require.Equal(t, mockBobHoldInvoice.Invoice, bobWrappedInvoice.PaymentRequest)

// Top up Alice's wallet
aliceFundingTx := db.Transaction{
AppId: &aliceApp.ID,
RequestEventId: nil,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_SETTLED,
FeeReserveMsat: uint64(0),
AmountMsat: 10000_000,
PaymentRequest: "lnbc100u1p5hkvrndpz2pshjmt9de6zqmmxyqcnqvpsxqs8xct5wvnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp5hpd6h7t023cf3q8d06y9slqcnkydffgzun9th5vjm62nsw8wssgqsp5dfddw9ezn93u7g9xmzh4q74kmwxlf0gxgx8c5e4cuu2ce3eapmgq9qyysgqcqzp2xqyz5vqp0t02p3882uhqsz0qf56jgy6mrf2523tudqnf5d2f6f83ud3krd9tu4zkd4yzwyc74acprnvz2853yf9lc89n90sy3r0lvckyy59racq40428t",
PaymentHash: "b85babf96f54709880ed7e88587c189d88d4a502e4cabbd192de953838ee8410",
SelfPayment: true,
}
svc.DB.Save(&aliceFundingTx)

// Top up Bob's wallet
bobFundingTx := db.Transaction{
AppId: &bobApp.ID,
RequestEventId: nil,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_SETTLED,
FeeReserveMsat: uint64(0),
AmountMsat: 10000_000,
PaymentRequest: "lnbc100u1p5hkvy7dpz2pshjmt9de6zqmmxyqcnqvpsxqs8xct5wvnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp526ulrmuxlgr9zmn56etnkr8eear5xyt2f3h5vecyfe9fwwmjgakqsp5ld7hwc4m6dy3zhy94lr24gklnqyuh7c2xe3wue7qjnlqf59l36rs9qyysgqcqzp2xqyz5vqpj7xg854g2wvdqxcdvt5pjucw0ljxckey3cm82tpx4nlgr8lgs6qv6jwvmnamurguxvwyxt3eft653zeqv7s2gq3gzag925mr796yhqqfaczhq",
PaymentHash: "56b9f1ef86fa06516e74d6573b0cf9cf4743116a4c6f4667044e4a973b72476c",
SelfPayment: true,
}
svc.DB.Save(&bobFundingTx)

// Alice pays Bob's wrapped invoice
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
result, err := transactionsService.SendPaymentSync(bobWrappedInvoice.PaymentRequest, nil, nil, svc.LNClient, &aliceApp.ID, nil)
assert.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, result.State)
}()

// TODO: rather than wait, listen for the event
// Wait for Alice's payment to be accepted
time.Sleep(10 * time.Millisecond)

// Bob pays Charlie's invoice to get the preimage
result, err := transactionsService.SendPaymentSync(charlieInvoice.PaymentRequest, nil, nil, svc.LNClient, &bobApp.ID, nil)
assert.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, result.State)

// TODO: expect Alice's payment is still held here

// Bob settles Alice's invoice using the preimage from Charlie
// TODO: allow passing a payment request
settledAliceInvoice, err := transactionsService.SettleHoldInvoice(ctx, *result.Preimage, svc.LNClient)
assert.NoError(t, err)
require.NotNil(t, settledAliceInvoice)
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledAliceInvoice.State)
assert.Equal(t, true, settledAliceInvoice.SelfPayment)
assert.Equal(t, true, settledAliceInvoice.Hold)

wg.Wait()
}
Loading