multi_pay_keysend_controller_test.go raw
1 package controllers
2
3 import (
4 "context"
5 "encoding/json"
6 "slices"
7 "sync"
8 "testing"
9
10 "github.com/getAlby/go-nostr"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/getAlby/hub/constants"
15 "github.com/getAlby/hub/db"
16 "github.com/getAlby/hub/nip47/models"
17 "github.com/getAlby/hub/tests"
18 )
19
20 const nip47MultiPayKeysendJson = `
21 {
22 "method": "multi_pay_keysend",
23 "params": {
24 "keysends": [{
25 "amount": 123000,
26 "pubkey": "123pubkey2",
27 "tlv_records": [{
28 "type": 5482373484,
29 "value": "fajsn341414fq"
30 }]
31 },
32 {
33 "amount": 123000,
34 "pubkey": "123pubkey2",
35 "tlv_records": [{
36 "type": 5482373484,
37 "value": "fajsn341414fq"
38 }]
39 }
40 ]
41 }
42 }
43 `
44
45 const nip47MultiPayKeysendOneOverflowingBudgetJson = `
46 {
47 "method": "multi_pay_keysend",
48 "params": {
49 "keysends": [{
50 "amount": 123000,
51 "pubkey": "123pubkey2",
52 "id": "customId",
53 "tlv_records": [{
54 "type": 5482373484,
55 "value": "fajsn341414fq"
56 }]
57 },
58 {
59 "amount": 500000,
60 "pubkey": "500pubkey",
61 "tlv_records": [{
62 "type": 5482373484,
63 "value": "fajsn341414fq"
64 }]
65 }
66 ]
67 }
68 }
69 `
70
71 func TestHandleMultiPayKeysendEvent_Success(t *testing.T) {
72 ctx := context.TODO()
73 svc, err := tests.CreateTestService(t)
74 require.NoError(t, err)
75 defer svc.Remove()
76
77 app, _, err := tests.CreateApp(svc)
78 assert.NoError(t, err)
79
80 appPermission := &db.AppPermission{
81 AppId: app.ID,
82 App: *app,
83 Scope: constants.PAY_INVOICE_SCOPE,
84 }
85 err = svc.DB.Create(appPermission).Error
86 assert.NoError(t, err)
87
88 nip47Request := &models.Request{}
89 err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), nip47Request)
90 assert.NoError(t, err)
91
92 dbRequestEvent := &db.RequestEvent{}
93 err = svc.DB.Create(&dbRequestEvent).Error
94 assert.NoError(t, err)
95
96 responses := []*models.Response{}
97 dTags := []nostr.Tags{}
98
99 var mu sync.Mutex
100
101 publishResponse := func(response *models.Response, tags nostr.Tags) {
102 mu.Lock()
103 defer mu.Unlock()
104 responses = append(responses, response)
105 dTags = append(dTags, tags)
106 }
107
108 NewTestNip47Controller(svc).
109 HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
110
111 assert.Equal(t, 2, len(responses))
112 for i := 0; i < len(responses); i++ {
113 assert.Equal(t, 64, len(responses[i].Result.(payResponse).Preimage))
114 assert.Equal(t, uint64(1), responses[i].Result.(payResponse).FeesPaid)
115 assert.Nil(t, responses[i].Error)
116 assert.Equal(t, "123pubkey2", dTags[i].Find("d")[1])
117 }
118 }
119
120 func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) {
121 ctx := context.TODO()
122 svc, err := tests.CreateTestService(t)
123 require.NoError(t, err)
124 defer svc.Remove()
125
126 app, _, err := tests.CreateApp(svc)
127 assert.NoError(t, err)
128
129 appPermission := &db.AppPermission{
130 AppId: app.ID,
131 App: *app,
132 Scope: constants.PAY_INVOICE_SCOPE,
133 MaxAmountSat: 400,
134 }
135 err = svc.DB.Create(appPermission).Error
136 assert.NoError(t, err)
137
138 nip47Request := &models.Request{}
139 err = json.Unmarshal([]byte(nip47MultiPayKeysendOneOverflowingBudgetJson), nip47Request)
140 assert.NoError(t, err)
141
142 dbRequestEvent := &db.RequestEvent{}
143 err = svc.DB.Create(&dbRequestEvent).Error
144 assert.NoError(t, err)
145
146 responses := []*models.Response{}
147 dTags := []nostr.Tags{}
148
149 var mu sync.Mutex
150
151 publishResponse := func(response *models.Response, tags nostr.Tags) {
152 mu.Lock()
153 defer mu.Unlock()
154 responses = append(responses, response)
155 dTags = append(dTags, tags)
156 }
157
158 NewTestNip47Controller(svc).
159 HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
160
161 // we can't guarantee which request was processed first
162 // so swap them if they are back to front
163 if responses[0].Result == nil {
164 responses[0], responses[1] = responses[1], responses[0]
165 dTags[0], dTags[1] = dTags[1], dTags[0]
166 }
167
168 assert.Equal(t, "customId", dTags[0].Find("d")[1])
169 assert.Nil(t, responses[0].Error)
170 assert.Equal(t, 64, len(responses[0].Result.(payResponse).Preimage))
171 assert.Equal(t, uint64(1), responses[0].Result.(payResponse).FeesPaid)
172
173 assert.Nil(t, responses[1].Result)
174 assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, responses[1].Error.Code)
175 }
176
177 func TestHandleMultiPayKeysendEvent_IsolatedApp_ConcurrentPayments(t *testing.T) {
178 ctx := context.TODO()
179 svc, err := tests.CreateTestService(t)
180 require.NoError(t, err)
181 defer svc.Remove()
182
183 app, _, err := tests.CreateApp(svc)
184 app.Isolated = true
185 assert.NoError(t, err)
186 app.Isolated = true
187 svc.DB.Save(&app)
188
189 appPermission := &db.AppPermission{
190 AppId: app.ID,
191 App: *app,
192 Scope: constants.PAY_INVOICE_SCOPE,
193 MaxAmountSat: 400,
194 }
195 err = svc.DB.Create(appPermission).Error
196 assert.NoError(t, err)
197
198 svc.DB.Create(&db.Transaction{
199 AppId: &app.ID,
200 State: constants.TRANSACTION_STATE_SETTLED,
201 Type: constants.TRANSACTION_TYPE_INCOMING,
202 // keysends paid are 123000 millisats
203 AmountMsat: 200000,
204 })
205
206 // force delay inside transaction
207 if svc.DB.Dialector.Name() == "postgres" {
208 err = svc.DB.Exec(`
209 CREATE OR REPLACE FUNCTION slow_down_query()
210 RETURNS TRIGGER AS $slow_down_query$
211 BEGIN
212 -- Introduce a delay of 1 second
213 PERFORM pg_sleep(1);
214 RETURN NEW;
215 END;
216 $slow_down_query$ LANGUAGE plpgsql;
217
218 CREATE TRIGGER slow_down_query
219 AFTER INSERT ON transactions
220 FOR EACH ROW
221 EXECUTE PROCEDURE slow_down_query();`).Error
222
223 require.NoError(t, err)
224 }
225
226 nip47Request := &models.Request{}
227 err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), nip47Request)
228 assert.NoError(t, err)
229
230 dbRequestEvent := &db.RequestEvent{}
231 err = svc.DB.Create(&dbRequestEvent).Error
232 assert.NoError(t, err)
233
234 responses := []*models.Response{}
235 dTags := []nostr.Tags{}
236
237 var mu sync.Mutex
238
239 publishResponse := func(response *models.Response, tags nostr.Tags) {
240 mu.Lock()
241 defer mu.Unlock()
242 responses = append(responses, response)
243 dTags = append(dTags, tags)
244 }
245
246 NewTestNip47Controller(svc).
247 HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
248
249 require.Equal(t, 2, len(responses))
250
251 // we can't guarantee which request was processed first
252 // so put the successful one at the front
253 successfulIdx := slices.IndexFunc(responses, func(r *models.Response) bool {
254 return r.Result != nil
255 })
256 require.GreaterOrEqual(t, successfulIdx, 0)
257
258 if successfulIdx > 0 {
259 responses[0], responses[successfulIdx] = responses[successfulIdx], responses[0]
260 }
261
262 for _, response := range responses[1:] {
263 require.Nil(t, response.Result)
264 assert.Equal(t, constants.ERROR_INSUFFICIENT_BALANCE, response.Error.Code)
265 }
266 }
267