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