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