diff --git a/payjoin-ffi/csharp/UnitTests.cs b/payjoin-ffi/csharp/UnitTests.cs index 655286fa5..e6b9dd053 100644 --- a/payjoin-ffi/csharp/UnitTests.cs +++ b/payjoin-ffi/csharp/UnitTests.cs @@ -254,6 +254,107 @@ public async Task SenderCancelFromWithReplyKeyAsync() } } +public class PersisterLoadAndCloseTests +{ + private static readonly byte[] OhttpKeysData = new byte[] + { + 0x01, 0x00, 0x16, 0x04, 0xba, 0x48, 0xc4, 0x9c, 0x3d, 0x4a, + 0x92, 0xa3, 0xad, 0x00, 0xec, 0xc6, 0x3a, 0x02, 0x4d, 0xa1, + 0x0c, 0xed, 0x02, 0x18, 0x0c, 0x73, 0xec, 0x12, 0xd8, 0xa7, + 0xad, 0x2c, 0xc9, 0x1b, 0xb4, 0x83, 0x82, 0x4f, 0xe2, 0xbe, + 0xe8, 0xd2, 0x8b, 0xfe, 0x2e, 0xb2, 0xfc, 0x64, 0x53, 0xbc, + 0x4d, 0x31, 0xcd, 0x85, 0x1e, 0x8a, 0x65, 0x40, 0xe8, 0x6c, + 0x53, 0x82, 0xaf, 0x58, 0x8d, 0x37, 0x09, 0x57, 0x00, 0x04, + 0x00, 0x01, 0x00, 0x03, + }; + + [Fact] + public void ReceiverLoadAndClose() + { + var persister = new InMemoryReceiverPersister(); + var address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + var ohttpKeys = OhttpKeys.Decode(OhttpKeysData); + + var initialized = new ReceiverBuilder(address, "https://example.com", ohttpKeys) + .Build() + .Save(persister); + Assert.Equal(1, persister.Load().Length); + Assert.False(persister.Closed); + + // Cancelling is terminal, so saving it must close the persister. + initialized.Cancel().Save(persister); + + Assert.True(persister.Closed); + Assert.Equal(2, persister.Load().Length); + } + + [Fact] + public async Task ReceiverLoadAndCloseAsync() + { + var persister = new InMemoryReceiverPersisterAsync(); + var address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + var ohttpKeys = OhttpKeys.Decode(OhttpKeysData); + + var initialized = await new ReceiverBuilder(address, "https://example.com", ohttpKeys) + .Build() + .SaveAsync(persister); + Assert.Equal(1, (await persister.Load()).Length); + Assert.False(persister.Closed); + + await initialized.Cancel().SaveAsync(persister); + + Assert.True(persister.Closed); + Assert.Equal(2, (await persister.Load()).Length); + } + + [Fact] + public void SenderLoadAndClose() + { + var address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; + var ohttpKeys = OhttpKeys.Decode(OhttpKeysData); + var receiver = new ReceiverBuilder(address, "https://example.com", ohttpKeys) + .Build() + .Save(new InMemoryReceiverPersister()); + + var persister = new InMemorySenderPersister(); + var withReplyKey = new SenderBuilder(PayjoinMethods.OriginalPsbt(), receiver.PjUri()) + .BuildRecommended(1000) + .Save(persister); + var pendingFallback = withReplyKey.Cancel().Save(persister); + Assert.Equal(2, persister.Load().Length); + Assert.False(persister.Closed); + + // Closing the fallback is terminal, so saving it must close the persister. + pendingFallback.Close().Save(persister); + + Assert.True(persister.Closed); + Assert.Equal(3, persister.Load().Length); + } + + [Fact] + public async Task SenderLoadAndCloseAsync() + { + var address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; + var ohttpKeys = OhttpKeys.Decode(OhttpKeysData); + var receiver = await new ReceiverBuilder(address, "https://example.com", ohttpKeys) + .Build() + .SaveAsync(new InMemoryReceiverPersisterAsync()); + + var persister = new InMemorySenderPersisterAsync(); + var withReplyKey = await new SenderBuilder(PayjoinMethods.OriginalPsbt(), receiver.PjUri()) + .BuildRecommended(1000) + .SaveAsync(persister); + var pendingFallback = await withReplyKey.Cancel().SaveAsync(persister); + Assert.Equal(2, (await persister.Load()).Length); + Assert.False(persister.Closed); + + await pendingFallback.Close().SaveAsync(persister); + + Assert.True(persister.Closed); + Assert.Equal(3, (await persister.Load()).Length); + } +} + public class ValidationTests { private static readonly byte[] OhttpKeysData = new byte[] diff --git a/payjoin-ffi/dart/test/test_payjoin_unit_test.dart b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart index 31e8325b3..6b0d626ad 100644 --- a/payjoin-ffi/dart/test/test_payjoin_unit_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart @@ -253,6 +253,133 @@ void main() { }); }); + group("Test Persister Load And Close", () { + test("Test receiver load and close", () { + var persister = InMemoryReceiverPersister(); + var initialized = payjoin.ReceiverBuilder( + address: "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", + directory: "https://example.com", + ohttpKeys: payjoin.OhttpKeys.decode( + bytes: Uint8List.fromList( + hex.decode( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003", + ), + ), + ), + ).build().save(persister: persister); + expect(persister.load().length, 1); + expect(persister.closed, isFalse); + + // Cancelling is terminal, so saving it must close the persister. + initialized.cancel().save(persister: persister); + + expect(persister.closed, isTrue); + expect( + persister.load().length, + 2, + reason: "load should still return every event after close", + ); + }); + + test("Test receiver load and close async", () async { + var persister = InMemoryReceiverPersisterAsync(); + var initialized = await payjoin.ReceiverBuilder( + address: "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", + directory: "https://example.com", + ohttpKeys: payjoin.OhttpKeys.decode( + bytes: Uint8List.fromList( + hex.decode( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003", + ), + ), + ), + ).build().saveAsync(persister: persister); + expect((await persister.load()).length, 1); + expect(persister.closed, isFalse); + + await initialized.cancel().saveAsync(persister: persister); + + expect(persister.closed, isTrue); + expect( + (await persister.load()).length, + 2, + reason: "load should still return every event after close", + ); + }); + + test("Test sender load and close", () { + // Create a receiver to just get the pj uri + var receiver = payjoin.ReceiverBuilder( + address: "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", + directory: "https://example.com", + ohttpKeys: payjoin.OhttpKeys.decode( + bytes: Uint8List.fromList( + hex.decode( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003", + ), + ), + ), + ).build().save(persister: InMemoryReceiverPersister()); + + var persister = InMemorySenderPersister(); + var withReplyKey = payjoin.SenderBuilder( + psbt: payjoin.originalPsbt(), + uri: receiver.pjUri(), + ).buildRecommended(minFeeRateSatPerKwu: 1000).save(persister: persister); + var pendingFallback = withReplyKey.cancel().save(persister: persister); + expect(persister.load().length, 2); + expect(persister.closed, isFalse); + + // Closing the fallback is terminal, so saving it must close the persister. + pendingFallback!.close().save(persister: persister); + + expect(persister.closed, isTrue); + expect( + persister.load().length, + 3, + reason: "load should still return every event after close", + ); + }); + + test("Test sender load and close async", () async { + // Create a receiver to just get the pj uri + var receiver = await payjoin.ReceiverBuilder( + address: "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", + directory: "https://example.com", + ohttpKeys: payjoin.OhttpKeys.decode( + bytes: Uint8List.fromList( + hex.decode( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003", + ), + ), + ), + ).build().saveAsync(persister: InMemoryReceiverPersisterAsync()); + + var persister = InMemorySenderPersisterAsync(); + var withReplyKey = + await payjoin.SenderBuilder( + psbt: payjoin.originalPsbt(), + uri: receiver.pjUri(), + ) + .buildRecommended(minFeeRateSatPerKwu: 1000) + .saveAsync(persister: persister); + var pendingFallback = await withReplyKey.cancel().saveAsync( + persister: persister, + ); + expect((await persister.load()).length, 2); + expect(persister.closed, isFalse); + + await pendingFallback!.close().saveAsync(persister: persister); + + expect(persister.closed, isTrue); + expect( + (await persister.load()).length, + 3, + reason: "load should still return every event after close", + ); + }); + }); + group("Test Async Persistence", () { test("Test receiver async persistence", () async { var persister = InMemoryReceiverPersisterAsync(); diff --git a/payjoin-ffi/javascript/test/unit.test.ts b/payjoin-ffi/javascript/test/unit.test.ts index 89386421e..7ca76af31 100644 --- a/payjoin-ffi/javascript/test/unit.test.ts +++ b/payjoin-ffi/javascript/test/unit.test.ts @@ -284,6 +284,127 @@ function runUnitTests(name: string, payjoin: typeof nodejsPayjoin) { }); }); + describe(`[${name}] Persister load and close tests`, () => { + test("receiver load and close", () => { + const persister = new InMemoryReceiverPersister(); + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const initialized = new payjoin.ReceiverBuilder( + "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", + "https://example.com", + ohttpKeys, + ) + .build() + .save(persister); + assert.strictEqual(persister.load().length, 1); + assert.strictEqual(persister.closed, false); + + // Cancelling is terminal, so saving it must close the persister. + initialized.cancel().save(persister); + + assert.strictEqual(persister.closed, true); + assert.strictEqual( + persister.load().length, + 2, + "load should still return every event after close", + ); + }); + + test("receiver load and close async", async () => { + const persister = new InMemoryReceiverPersisterAsync(); + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const initialized = await new payjoin.ReceiverBuilder( + "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", + "https://example.com", + ohttpKeys, + ) + .build() + .saveAsync(persister); + assert.strictEqual((await persister.load()).length, 1); + assert.strictEqual(persister.closed, false); + + await initialized.cancel().saveAsync(persister); + + assert.strictEqual(persister.closed, true); + assert.strictEqual( + (await persister.load()).length, + 2, + "load should still return every event after close", + ); + }); + + test("sender load and close", () => { + const receiverPersister = new InMemoryReceiverPersister(); + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + const uri = new payjoin.ReceiverBuilder( + "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", + "https://example.com", + ohttpKeys, + ) + .build() + .save(receiverPersister) + .pjUri(); + + const persister = new InMemorySenderPersister(); + const pendingFallback = new payjoin.SenderBuilder( + ORIGINAL_PSBT, + uri, + ) + .buildRecommended(BigInt(1000)) + .save(persister) + .cancel() + .save(persister); + assert.strictEqual(persister.load().length, 2); + assert.strictEqual(persister.closed, false); + + // Closing the fallback is terminal, so saving it must close the + // persister. + pendingFallback.close().save(persister); + + assert.strictEqual(persister.closed, true); + assert.strictEqual( + persister.load().length, + 3, + "load should still return every event after close", + ); + }); + + test("sender load and close async", async () => { + const receiverPersister = new InMemoryReceiverPersisterAsync(); + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + const receiver = await new payjoin.ReceiverBuilder( + "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", + "https://example.com", + ohttpKeys, + ) + .build() + .saveAsync(receiverPersister); + + const persister = new InMemorySenderPersisterAsync(); + const withReplyKey = await new payjoin.SenderBuilder( + ORIGINAL_PSBT, + receiver.pjUri(), + ) + .buildRecommended(BigInt(1000)) + .saveAsync(persister); + const pendingFallback = await withReplyKey + .cancel() + .saveAsync(persister); + assert.strictEqual((await persister.load()).length, 2); + assert.strictEqual(persister.closed, false); + + await pendingFallback.close().saveAsync(persister); + + assert.strictEqual(persister.closed, true); + assert.strictEqual( + (await persister.load()).length, + 3, + "load should still return every event after close", + ); + }); + }); + describe(`[${name}] Async Persistence tests`, () => { test("receiver async persistence", async () => { const persister = new InMemoryReceiverPersisterAsync(); diff --git a/payjoin-ffi/python/test/test_payjoin_unit_test.py b/payjoin-ffi/python/test/test_payjoin_unit_test.py index 61b5c0780..1504089e8 100644 --- a/payjoin-ffi/python/test/test_payjoin_unit_test.py +++ b/payjoin-ffi/python/test/test_payjoin_unit_test.py @@ -139,6 +139,128 @@ async def run_test(): asyncio.run(run_test()) +class TestPersisterLoadAndClose(unittest.TestCase): + def test_receiver_load_and_close(self): + persister = InMemoryReceiverPersister() + initialized = ( + payjoin.ReceiverBuilder( + "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", + "https://example.com", + payjoin.OhttpKeys.decode( + bytes.fromhex( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003" + ) + ), + ) + .build() + .save(persister) + ) + self.assertEqual(len(persister.load()), 1) + self.assertFalse(persister.closed) + + # Cancelling is terminal, so saving it must close the persister. + initialized.cancel().save(persister) + + self.assertTrue(persister.closed) + self.assertEqual(len(persister.load()), 2) + + def test_receiver_load_and_close_async(self): + import asyncio + + async def run_test(): + persister = InMemoryReceiverPersisterAsync() + initialized = await ( + payjoin.ReceiverBuilder( + "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", + "https://example.com", + payjoin.OhttpKeys.decode( + bytes.fromhex( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003" + ) + ), + ) + .build() + .save_async(persister) + ) + self.assertEqual(len(await persister.load()), 1) + self.assertFalse(persister.closed) + + await initialized.cancel().save_async(persister) + + self.assertTrue(persister.closed) + self.assertEqual(len(await persister.load()), 2) + + asyncio.run(run_test()) + + def test_sender_load_and_close(self): + # Create a receiver to just get the pj uri + receiver = ( + payjoin.ReceiverBuilder( + "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", + "https://example.com", + payjoin.OhttpKeys.decode( + bytes.fromhex( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003" + ) + ), + ) + .build() + .save(InMemoryReceiverPersister()) + ) + + persister = InMemorySenderPersister() + with_reply_key = ( + payjoin.SenderBuilder(payjoin.original_psbt(), receiver.pj_uri()) + .build_recommended(1000) + .save(persister) + ) + pending_fallback = with_reply_key.cancel().save(persister) + self.assertEqual(len(persister.load()), 2) + self.assertFalse(persister.closed) + + # Closing the fallback is terminal, so saving it must close the persister. + pending_fallback.close().save(persister) + + self.assertTrue(persister.closed) + self.assertEqual(len(persister.load()), 3) + + def test_sender_load_and_close_async(self): + import asyncio + + async def run_test(): + # Create a receiver to just get the pj uri + receiver = await ( + payjoin.ReceiverBuilder( + "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", + "https://example.com", + payjoin.OhttpKeys.decode( + bytes.fromhex( + "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003" + ) + ), + ) + .build() + .save_async(InMemoryReceiverPersisterAsync()) + ) + + persister = InMemorySenderPersisterAsync() + with_reply_key = await ( + payjoin.SenderBuilder(payjoin.original_psbt(), receiver.pj_uri()) + .build_recommended(1000) + .save_async(persister) + ) + pending_fallback = await with_reply_key.cancel().save_async(persister) + self.assertEqual(len(await persister.load()), 2) + self.assertFalse(persister.closed) + + await pending_fallback.close().save_async(persister) + + self.assertTrue(persister.closed) + self.assertEqual(len(await persister.load()), 3) + + asyncio.run(run_test()) + + class TestReceiverCancel(unittest.TestCase): def test_receiver_cancel(self): persister = InMemoryReceiverPersister()