self_hold_payments_test.go raw
1 package transactions
2
3 import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "github.com/getAlby/hub/config"
13 "github.com/getAlby/hub/constants"
14 "github.com/getAlby/hub/db"
15 "github.com/getAlby/hub/lnclient"
16 "github.com/getAlby/hub/tests"
17 )
18
19 func TestSelfHoldPaymentSettled(t *testing.T) {
20 ctx := context.TODO()
21
22 svc, err := tests.CreateTestService(t)
23 require.NoError(t, err)
24 defer svc.Remove()
25
26 preimage := tests.MockLNClientHoldTransaction.Preimage
27 paymentHash := tests.MockLNClientHoldTransaction.PaymentHash
28
29 transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
30 transaction, err := transactionsService.MakeHoldInvoice(ctx, 1000, "Hold payment test", "", 0, paymentHash, nil, nil, svc.LNClient, nil, nil)
31 require.NoError(t, err)
32 assert.True(t, transaction.Hold)
33 // use the pubkey from the decoded tests.MockLNClientHoldTransaction invoice
34 svc.LNClient.(*tests.MockLn).Pubkey = "038a73de75fdc3c7ec951a5e0b0fa95c5cd353bd7ca72df2086aa228c1f92819a5"
35
36 var wg sync.WaitGroup
37 wg.Add(1)
38 go func() {
39 defer wg.Done()
40 result, err := transactionsService.SendPaymentSync(transaction.PaymentRequest, nil, nil, svc.LNClient, nil, nil)
41 assert.NoError(t, err)
42 require.NotNil(t, result)
43 assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, result.State)
44 assert.Equal(t, true, result.SelfPayment)
45 assert.Equal(t, false, result.Hold)
46 }()
47
48 // wait for payment to be accepted
49 time.Sleep(10 * time.Millisecond)
50 settledTransaction, err := transactionsService.SettleHoldInvoice(ctx, preimage, svc.LNClient)
51 assert.NoError(t, err)
52 require.NotNil(t, settledTransaction)
53 assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledTransaction.State)
54 assert.Equal(t, true, settledTransaction.SelfPayment)
55 assert.Equal(t, true, settledTransaction.Hold)
56 wg.Wait()
57 }
58 func TestSelfHoldPaymentCanceled(t *testing.T) {
59 ctx := context.TODO()
60
61 svc, err := tests.CreateTestService(t)
62 require.NoError(t, err)
63 defer svc.Remove()
64
65 paymentHash := tests.MockLNClientHoldTransaction.PaymentHash
66
67 transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
68 transaction, err := transactionsService.MakeHoldInvoice(ctx, 1000, "Hold payment test", "", 0, paymentHash, nil, nil, svc.LNClient, nil, nil)
69 require.NoError(t, err)
70 assert.True(t, transaction.Hold)
71 // use the pubkey from the decoded tests.MockLNClientHoldTransaction invoice
72 svc.LNClient.(*tests.MockLn).Pubkey = "038a73de75fdc3c7ec951a5e0b0fa95c5cd353bd7ca72df2086aa228c1f92819a5"
73
74 var wg sync.WaitGroup
75 wg.Add(1)
76 go func() {
77 defer wg.Done()
78 result, err := transactionsService.SendPaymentSync(transaction.PaymentRequest, nil, nil, svc.LNClient, nil, nil)
79 assert.ErrorIs(t, err, lnclient.NewHoldInvoiceCanceledError())
80 assert.Nil(t, result)
81
82 outgoingTransactionType := constants.TRANSACTION_TYPE_OUTGOING
83 failedOutgoingTransaction, err := transactionsService.LookupTransaction(ctx, paymentHash, &outgoingTransactionType, svc.LNClient, nil)
84 assert.Nil(t, err)
85 require.NotNil(t, failedOutgoingTransaction)
86 assert.Equal(t, constants.TRANSACTION_STATE_FAILED, failedOutgoingTransaction.State)
87 assert.Equal(t, true, failedOutgoingTransaction.SelfPayment)
88 assert.Equal(t, false, failedOutgoingTransaction.Hold)
89 }()
90
91 // wait for payment to be accepted
92 time.Sleep(10 * time.Millisecond)
93 err = transactionsService.CancelHoldInvoice(ctx, paymentHash, svc.LNClient)
94 assert.NoError(t, err)
95
96 incomingTransactionType := constants.TRANSACTION_TYPE_INCOMING
97 updatedHoldTransaction, err := transactionsService.LookupTransaction(ctx, paymentHash, &incomingTransactionType, svc.LNClient, nil)
98 assert.Nil(t, err)
99 assert.Equal(t, constants.TRANSACTION_STATE_FAILED, updatedHoldTransaction.State)
100 assert.Equal(t, true, updatedHoldTransaction.SelfPayment)
101 assert.Equal(t, true, updatedHoldTransaction.Hold)
102 wg.Wait()
103 }
104
105 func TestWrappedInvoice(t *testing.T) {
106 ctx := context.TODO()
107
108 svc, err := tests.CreateTestService(t)
109 svc.Cfg.SetUpdate("LNBackendType", config.LDKBackendType, "")
110 require.NoError(t, err)
111 defer svc.Remove()
112
113 // invoices were created with long expiry in LND (Polar)
114 /*
115 lnd@grace:/$ lncli addinvoice --amt <amount> --preimage fcf200c74d9900dc77af17eb1f57c02eec0f94b5b169d3eee23df9a216a3411b --expiry 31536000
116 bash: amount: No such file or directory
117 lnd@grace:/$ lncli addinvoice --amt 1000 --preimage fcf200c74d9900dc77af17eb1f57c02eec0f94b5b169d3eee23df9a21
118 6a3411b --expiry 31536000
119 {
120 "r_hash": "8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e",
121 "payment_request": "lnbcrt10u1p5cammypp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5k9qse5srd9mfaxlgucr0p2gzf9464xte5xtqgjxyxj7794jxav9q9qxpqysgqnvwx0j9qxkx6k9efetdr0vdkrnp4vn23ud4gwpm0k28vf3v0rcmzfnued907r7ju6d86wr25ypt366szd0f7s28nzrvwmp4rck4358spfr9vx9",
122 "add_index": "1",
123 "payment_addr": "b1410cd20369769e9be8e606f0a902496baa9979a1960448c434bde2d646eb0a"
124 }
125 lnd@grace:/$ lncli addholdinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e --amt 1100
126 --expiry 31536000
127 [lncli] rpc error: code = Unknown desc = invoice with payment hash already exists
128 lnd@grace:/$ lncli cancelinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e
129 {}
130 lnd@grace:/$ lncli deletecanceledinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e
131 {
132 "status": "canceled invoice deleted successfully: invoice hash 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e"
133 }
134 lnd@grace:/$ lncli addholdinvoice 8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e --amt 1100 --expiry 31536000
135 {
136 "payment_request": "lnbcrt11u1p5cama7pp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5mtvgejxel3yjyk55e58dgjg9v2mnxva9xn83yg05mducwz3ujhrq9qxpqysgqp48sr0r7x6hj5vcefn3wtj7g8r33agyp4aqfasyr2fptxp066wzppmr7rw9my3frezy65hw7u5l0tnqh7393x2km7tf2tk3efdl0c7qptan9m9",
137 "add_index": "2",
138 "payment_addr": "dad88cc8d9fc49225a94cd0ed4490562b73333a534cf1221f4db79870a3c95c6"
139 }
140 */
141
142 // use the pubkey from Bob's invoice to activate self payments
143 svc.LNClient.(*tests.MockLn).Pubkey = "03a53c23a3e12cb3b16dc23c8a6d18e5930b480443c5f46860f553b33d74731342"
144
145 transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
146
147 // Charlie creates invoice with payment hash
148 // Bob also creates invoice with payment hash, but it's a HOLD invoice one.
149
150 // Create 3 isolated apps: Charlie (invoice creator), Bob (wrapper), Alice (payer)
151 charlieApp, _, err := svc.AppsService.CreateApp("Charlie", "", 0, "", nil, []string{constants.MAKE_INVOICE_SCOPE}, true, nil)
152 require.NoError(t, err)
153 require.NotNil(t, charlieApp)
154
155 bobApp, _, err := svc.AppsService.CreateApp("Bob", "", 0, "", nil, []string{constants.MAKE_INVOICE_SCOPE, constants.PAY_INVOICE_SCOPE}, true, nil)
156 require.NoError(t, err)
157 require.NotNil(t, bobApp)
158
159 aliceApp, _, err := svc.AppsService.CreateApp("Alice", "", 0, "", nil, []string{constants.PAY_INVOICE_SCOPE}, true, nil)
160 require.NoError(t, err)
161 require.NotNil(t, aliceApp)
162
163 // created with sandbox.albylabs.com
164 // Charlie's 1000 sat invoice
165 mockCharlieInvoice := &lnclient.Transaction{
166 Type: "incoming",
167 Invoice: "lnbcrt10u1p5cammypp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5k9qse5srd9mfaxlgucr0p2gzf9464xte5xtqgjxyxj7794jxav9q9qxpqysgqnvwx0j9qxkx6k9efetdr0vdkrnp4vn23ud4gwpm0k28vf3v0rcmzfnued907r7ju6d86wr25ypt366szd0f7s28nzrvwmp4rck4358spfr9vx9",
168 Description: "mock hold invoice",
169 DescriptionHash: "",
170 Preimage: "fcf200c74d9900dc77af17eb1f57c02eec0f94b5b169d3eee23df9a216a3411b",
171 PaymentHash: "8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e",
172 AmountMsat: 1000_000,
173 }
174
175 // Bob's 1100 sat invoice
176 // (same payment hash)
177
178 mockBobHoldInvoice := &lnclient.Transaction{
179 Type: "incoming",
180 Invoice: "lnbcrt11u1p5cama7pp53u7058xu7xw7fdhah8kr8xm5wrnjf5x82kmuw45pvn2a7xmsafhqdqqcqzzsxq97zvuqsp5mtvgejxel3yjyk55e58dgjg9v2mnxva9xn83yg05mducwz3ujhrq9qxpqysgqp48sr0r7x6hj5vcefn3wtj7g8r33agyp4aqfasyr2fptxp066wzppmr7rw9my3frezy65hw7u5l0tnqh7393x2km7tf2tk3efdl0c7qptan9m9",
181 Description: "mock hold invoice",
182 DescriptionHash: "",
183 Preimage: "",
184 PaymentHash: "8f3cfa1cdcf19de4b6fdb9ec339b7470e724d0c755b7c7568164d5df1b70ea6e",
185 AmountMsat: 1100_000,
186 }
187
188 svc.LNClient.(*tests.MockLn).MakeInvoiceResponses = []*lnclient.Transaction{
189 mockCharlieInvoice,
190 mockBobHoldInvoice,
191 }
192 svc.LNClient.(*tests.MockLn).MakeInvoiceErrors = []error{nil, nil}
193
194 var preimages = []string{tests.MockLNClientHoldTransaction.Preimage, tests.MockLNClientHoldTransaction.Preimage}
195 svc.LNClient.(*tests.MockLn).PayInvoiceResponses = []*lnclient.PayInvoiceResponse{{
196 Preimage: preimages[0],
197 }, {
198 Preimage: preimages[1],
199 }}
200 svc.LNClient.(*tests.MockLn).PayInvoiceErrors = []error{nil, nil}
201
202 // Charlie creates a standard invoice for 1000 sats
203 charlieInvoice, err := transactionsService.MakeInvoice(ctx, 1000, "Charlie invoice", "", 0, nil, svc.LNClient, &charlieApp.ID, nil, nil)
204 require.NoError(t, err)
205 require.False(t, charlieInvoice.Hold)
206 require.Equal(t, mockCharlieInvoice.Invoice, charlieInvoice.PaymentRequest)
207
208 // Bob creates a wrapped invoice with the same payment hash but higher amount (1100 sats)
209 // Bob acts as an intermediary, adding a fee of 100 sats
210 bobWrappedInvoice, err := transactionsService.MakeHoldInvoice(ctx, 1100, "Bob wrapped invoice", "", 0, charlieInvoice.PaymentHash, nil, nil, svc.LNClient, &bobApp.ID, nil)
211 require.NoError(t, err)
212 require.True(t, bobWrappedInvoice.Hold)
213 require.Equal(t, mockBobHoldInvoice.Invoice, bobWrappedInvoice.PaymentRequest)
214
215 // Top up Alice's wallet
216 aliceFundingTx := db.Transaction{
217 AppId: &aliceApp.ID,
218 RequestEventId: nil,
219 Type: constants.TRANSACTION_TYPE_INCOMING,
220 State: constants.TRANSACTION_STATE_SETTLED,
221 FeeReserveMsat: uint64(0),
222 AmountMsat: 10000_000,
223 PaymentRequest: "lnbc100u1p5hkvrndpz2pshjmt9de6zqmmxyqcnqvpsxqs8xct5wvnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp5hpd6h7t023cf3q8d06y9slqcnkydffgzun9th5vjm62nsw8wssgqsp5dfddw9ezn93u7g9xmzh4q74kmwxlf0gxgx8c5e4cuu2ce3eapmgq9qyysgqcqzp2xqyz5vqp0t02p3882uhqsz0qf56jgy6mrf2523tudqnf5d2f6f83ud3krd9tu4zkd4yzwyc74acprnvz2853yf9lc89n90sy3r0lvckyy59racq40428t",
224 PaymentHash: "b85babf96f54709880ed7e88587c189d88d4a502e4cabbd192de953838ee8410",
225 SelfPayment: true,
226 }
227 svc.DB.Save(&aliceFundingTx)
228
229 // Top up Bob's wallet
230 bobFundingTx := db.Transaction{
231 AppId: &bobApp.ID,
232 RequestEventId: nil,
233 Type: constants.TRANSACTION_TYPE_INCOMING,
234 State: constants.TRANSACTION_STATE_SETTLED,
235 FeeReserveMsat: uint64(0),
236 AmountMsat: 10000_000,
237 PaymentRequest: "lnbc100u1p5hkvy7dpz2pshjmt9de6zqmmxyqcnqvpsxqs8xct5wvnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp526ulrmuxlgr9zmn56etnkr8eear5xyt2f3h5vecyfe9fwwmjgakqsp5ld7hwc4m6dy3zhy94lr24gklnqyuh7c2xe3wue7qjnlqf59l36rs9qyysgqcqzp2xqyz5vqpj7xg854g2wvdqxcdvt5pjucw0ljxckey3cm82tpx4nlgr8lgs6qv6jwvmnamurguxvwyxt3eft653zeqv7s2gq3gzag925mr796yhqqfaczhq",
238 PaymentHash: "56b9f1ef86fa06516e74d6573b0cf9cf4743116a4c6f4667044e4a973b72476c",
239 SelfPayment: true,
240 }
241 svc.DB.Save(&bobFundingTx)
242
243 // Alice pays Bob's wrapped invoice
244 var wg sync.WaitGroup
245 wg.Add(1)
246 go func() {
247 defer wg.Done()
248 result, err := transactionsService.SendPaymentSync(bobWrappedInvoice.PaymentRequest, nil, nil, svc.LNClient, &aliceApp.ID, nil)
249 assert.NoError(t, err)
250 require.NotNil(t, result)
251 assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, result.State)
252 }()
253
254 // TODO: rather than wait, listen for the event
255 // Wait for Alice's payment to be accepted
256 time.Sleep(10 * time.Millisecond)
257
258 // Bob pays Charlie's invoice to get the preimage
259 result, err := transactionsService.SendPaymentSync(charlieInvoice.PaymentRequest, nil, nil, svc.LNClient, &bobApp.ID, nil)
260 assert.NoError(t, err)
261 require.NotNil(t, result)
262 assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, result.State)
263
264 // TODO: expect Alice's payment is still held here
265
266 // Bob settles Alice's invoice using the preimage from Charlie
267 // TODO: allow passing a payment request
268 settledAliceInvoice, err := transactionsService.SettleHoldInvoice(ctx, *result.Preimage, svc.LNClient)
269 assert.NoError(t, err)
270 require.NotNil(t, settledAliceInvoice)
271 assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledAliceInvoice.State)
272 assert.Equal(t, true, settledAliceInvoice.SelfPayment)
273 assert.Equal(t, true, settledAliceInvoice.Hold)
274
275 wg.Wait()
276 }
277