diff --git a/payjoin-ffi/javascript/test/integration.test.ts b/payjoin-ffi/javascript/test/integration.test.ts index 610bb24d1..d16e1f2a6 100644 --- a/payjoin-ffi/javascript/test/integration.test.ts +++ b/payjoin-ffi/javascript/test/integration.test.ts @@ -1,8 +1,29 @@ -import { payjoin, uniffiInitAsync } from "payjoin"; import * as testUtils from "../test-utils/index.js"; import assert from "assert"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { + payjoin as nodejsPayjoin, + uniffiInitAsync as nodejsUniffiInitAsync, +} from "payjoin"; +import * as webPayjoinModule from "../src/web/generated/payjoin.js"; +import initWebAsync from "../src/web/generated/wasm-bindgen/index.js"; import { InMemoryReceiverPersister, InMemorySenderPersister } from "./utils.ts"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function webUniffiInitAsync() { + const wasmPath = join( + __dirname, + "../src/web/generated/wasm-bindgen/index_bg.wasm", + ); + const wasmBytes = readFileSync(wasmPath); + await initWebAsync({ module_or_path: wasmBytes }); + webPayjoinModule.default.initialize(); +} + interface Utxo { txid: string; vout: number; @@ -10,7 +31,10 @@ interface Utxo { scriptPubKey: string; } -class MempoolAcceptanceCallback implements payjoin.CanBroadcast { +type PayjoinModule = typeof nodejsPayjoin; +const webPayjoin = webPayjoinModule as unknown as PayjoinModule; + +class MempoolAcceptanceCallback { private connection: testUtils.RpcClient; constructor(connection: testUtils.RpcClient) { @@ -31,7 +55,7 @@ class MempoolAcceptanceCallback implements payjoin.CanBroadcast { } } -class IsScriptOwnedCallback implements payjoin.IsScriptOwned { +class IsScriptOwnedCallback { private connection: testUtils.RpcClient; constructor(connection: testUtils.RpcClient) { @@ -97,7 +121,7 @@ class IsScriptOwnedCallback implements payjoin.IsScriptOwned { } } -class CheckInputsNotSeenCallback implements payjoin.IsOutputKnown { +class CheckInputsNotSeenCallback { private connection: testUtils.RpcClient; constructor(connection: testUtils.RpcClient) { @@ -109,7 +133,7 @@ class CheckInputsNotSeenCallback implements payjoin.IsOutputKnown { } } -class ProcessPsbtCallback implements payjoin.ProcessPsbt { +class ProcessPsbtCallback { private connection: testUtils.RpcClient; constructor(connection: testUtils.RpcClient) { @@ -125,20 +149,20 @@ class ProcessPsbtCallback implements payjoin.ProcessPsbt { } function createReceiverContext( + payjoin: PayjoinModule, address: string, directory: string, - ohttpKeys: payjoin.OhttpKeys, + ohttpKeys: ReturnType, persister: InMemoryReceiverPersister, -): payjoin.Initialized { - const receiver = new payjoin.ReceiverBuilder(address, directory, ohttpKeys) +): InstanceType { + return new payjoin.ReceiverBuilder(address, directory, ohttpKeys) .build() .save(persister); - return receiver; } function buildSweepPsbt( sender: testUtils.RpcClient, - pjUri: payjoin.PjUri, + pjUri: InstanceType, ): string { const outputs: Record = {}; outputs[pjUri.address()] = 50; @@ -164,9 +188,12 @@ function buildSweepPsbt( ).psbt; } -function getInputs(rpcConnection: testUtils.RpcClient): payjoin.InputPair[] { +function getInputs( + payjoin: PayjoinModule, + rpcConnection: testUtils.RpcClient, +): InstanceType[] { const utxos: Utxo[] = JSON.parse(rpcConnection.call("listunspent", [])); - const inputs: payjoin.InputPair[] = []; + const inputs: InstanceType[] = []; for (const utxo of utxos) { const txin = payjoin.TxIn.create({ previousOutput: payjoin.OutPoint.create({ @@ -192,23 +219,25 @@ function getInputs(rpcConnection: testUtils.RpcClient): payjoin.InputPair[] { } async function processProvisionalProposal( - proposal: payjoin.ProvisionalProposal, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { - const payjoinProposal = proposal +): Promise> { + return proposal .finalizeProposal(new ProcessPsbtCallback(receiver)) .save(recvPersister); - return payjoinProposal; } async function processWantsFeeRange( - proposal: payjoin.WantsFeeRange, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const wantsFeeRange = proposal.applyFeeRange(1n, 10n).save(recvPersister); return await processProvisionalProposal( + payjoin, wantsFeeRange, receiver, recvPersister, @@ -216,15 +245,17 @@ async function processWantsFeeRange( } async function processWantsInputs( - proposal: payjoin.WantsInputs, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const provisionalProposal = proposal - .contributeInputs(getInputs(receiver)) + .contributeInputs(getInputs(payjoin, receiver)) .commitInputs() .save(recvPersister); return await processWantsFeeRange( + payjoin, provisionalProposal, receiver, recvPersister, @@ -232,45 +263,65 @@ async function processWantsInputs( } async function processWantsOutputs( - proposal: payjoin.WantsOutputs, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const wantsInputs = proposal.commitOutputs().save(recvPersister); - return await processWantsInputs(wantsInputs, receiver, recvPersister); + return await processWantsInputs( + payjoin, + wantsInputs, + receiver, + recvPersister, + ); } async function processOutputsUnknown( - proposal: payjoin.OutputsUnknown, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const wantsOutputs = proposal .identifyReceiverOutputs(new IsScriptOwnedCallback(receiver)) .save(recvPersister); - return await processWantsOutputs(wantsOutputs, receiver, recvPersister); + return await processWantsOutputs( + payjoin, + wantsOutputs, + receiver, + recvPersister, + ); } async function processMaybeInputsSeen( - proposal: payjoin.MaybeInputsSeen, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const outputsUnknown = proposal .checkNoInputsSeenBefore(new CheckInputsNotSeenCallback(receiver)) .save(recvPersister); - return await processOutputsUnknown(outputsUnknown, receiver, recvPersister); + return await processOutputsUnknown( + payjoin, + outputsUnknown, + receiver, + recvPersister, + ); } async function processMaybeInputsOwned( - proposal: payjoin.MaybeInputsOwned, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const maybeInputsOwned = proposal .checkInputsNotOwned(new IsScriptOwnedCallback(receiver)) .save(recvPersister); return await processMaybeInputsSeen( + payjoin, maybeInputsOwned, receiver, recvPersister, @@ -278,10 +329,11 @@ async function processMaybeInputsOwned( } async function processUncheckedProposal( - proposal: payjoin.UncheckedOriginalPayload, + payjoin: PayjoinModule, + proposal: InstanceType, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise { +): Promise> { const uncheckedProposal = proposal .checkBroadcastSuitability( undefined, @@ -289,6 +341,7 @@ async function processUncheckedProposal( ) .save(recvPersister); return await processMaybeInputsOwned( + payjoin, uncheckedProposal, receiver, recvPersister, @@ -296,11 +349,12 @@ async function processUncheckedProposal( } async function retrieveReceiverProposal( - receiver: payjoin.Initialized, + payjoin: PayjoinModule, + receiver: InstanceType, receiverRpc: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, ohttpRelay: string, -): Promise { +): Promise | null> { const request = receiver.createPollRequest(ohttpRelay); const response = await fetch(request.request.url, { method: "POST", @@ -317,6 +371,7 @@ async function retrieveReceiverProposal( } else if (res instanceof payjoin.InitializedTransitionOutcome.Progress) { const proposal = res.inner.inner; return await processUncheckedProposal( + payjoin, proposal, receiverRpc, recvPersister, @@ -327,36 +382,34 @@ async function retrieveReceiverProposal( } async function processReceiverProposal( + payjoin: PayjoinModule, receiver: - | payjoin.Initialized - | payjoin.UncheckedOriginalPayload - | payjoin.MaybeInputsOwned - | payjoin.MaybeInputsSeen - | payjoin.OutputsUnknown - | payjoin.WantsOutputs - | payjoin.WantsInputs - | payjoin.WantsFeeRange - | payjoin.ProvisionalProposal - | payjoin.PayjoinProposal, + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType, receiverRpc: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, ohttpRelay: string, -): Promise { +): Promise | null> { if (receiver instanceof payjoin.Initialized) { - const res = await retrieveReceiverProposal( + return await retrieveReceiverProposal( + payjoin, receiver, receiverRpc, recvPersister, ohttpRelay, ); - if (res === null) { - return null; - } - return res; } - if (receiver instanceof payjoin.UncheckedOriginalPayload) { return await processUncheckedProposal( + payjoin, receiver, receiverRpc, recvPersister, @@ -364,6 +417,7 @@ async function processReceiverProposal( } if (receiver instanceof payjoin.MaybeInputsOwned) { return await processMaybeInputsOwned( + payjoin, receiver, receiverRpc, recvPersister, @@ -371,6 +425,7 @@ async function processReceiverProposal( } if (receiver instanceof payjoin.MaybeInputsSeen) { return await processMaybeInputsSeen( + payjoin, receiver, receiverRpc, recvPersister, @@ -378,22 +433,39 @@ async function processReceiverProposal( } if (receiver instanceof payjoin.OutputsUnknown) { return await processOutputsUnknown( + payjoin, receiver, receiverRpc, recvPersister, ); } if (receiver instanceof payjoin.WantsOutputs) { - return await processWantsOutputs(receiver, receiverRpc, recvPersister); + return await processWantsOutputs( + payjoin, + receiver, + receiverRpc, + recvPersister, + ); } if (receiver instanceof payjoin.WantsInputs) { - return await processWantsInputs(receiver, receiverRpc, recvPersister); + return await processWantsInputs( + payjoin, + receiver, + receiverRpc, + recvPersister, + ); } if (receiver instanceof payjoin.WantsFeeRange) { - return await processWantsFeeRange(receiver, receiverRpc, recvPersister); + return await processWantsFeeRange( + payjoin, + receiver, + receiverRpc, + recvPersister, + ); } if (receiver instanceof payjoin.ProvisionalProposal) { return await processProvisionalProposal( + payjoin, receiver, receiverRpc, recvPersister, @@ -406,7 +478,7 @@ async function processReceiverProposal( throw new Error(`Unknown receiver state`); } -function testFfiValidation(): void { +function testFfiValidation(payjoin: PayjoinModule): void { const tooLargeAmount = 21000000n * 100000000n + 1n; // Invalid outpoint (txid too long) should fail before amount checks. @@ -506,9 +578,8 @@ function testFfiValidation(): void { }, /AmountOutOfRange/); } -async function testIntegrationV2ToV2(): Promise { +async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise { const env = testUtils.initBitcoindSenderReceiver(); - const bitcoind = env.getBitcoind(); const receiver = env.getReceiver(); const sender = env.getSender(); @@ -526,6 +597,7 @@ async function testIntegrationV2ToV2(): Promise { const senderPersister = new InMemorySenderPersister(); const session = createReceiverContext( + payjoin, receiverAddress, directory, ohttpKeys, @@ -533,6 +605,7 @@ async function testIntegrationV2ToV2(): Promise { ); let processResponse = await processReceiverProposal( + payjoin, session, receiver, recvPersister, @@ -562,6 +635,7 @@ async function testIntegrationV2ToV2(): Promise { .save(senderPersister); let payjoinProposal = await processReceiverProposal( + payjoin, session, receiver, recvPersister, @@ -591,9 +665,15 @@ async function testIntegrationV2ToV2(): Promise { ); let pollOutcome: - | payjoin.PollingForProposalTransitionOutcome.Progress - | payjoin.PollingForProposalTransitionOutcome.Stasis - | payjoin.PollingForProposalTransitionOutcome.Terminal; + | InstanceType< + PayjoinModule["PollingForProposalTransitionOutcome"]["Progress"] + > + | InstanceType< + PayjoinModule["PollingForProposalTransitionOutcome"]["Stasis"] + > + | InstanceType< + PayjoinModule["PollingForProposalTransitionOutcome"]["Terminal"] + >; let attempts = 0; while (true) { const ohttpContextRequest = sendCtx.createPollRequest(ohttpRelay); @@ -617,7 +697,6 @@ async function testIntegrationV2ToV2(): Promise { } attempts += 1; if (attempts >= 3) { - // Receiver not ready yet; mirror Dart/Python tolerance. return; } } @@ -658,9 +737,13 @@ async function testIntegrationV2ToV2(): Promise { } async function runTests(): Promise { - await uniffiInitAsync(); - testFfiValidation(); - await testIntegrationV2ToV2(); + await nodejsUniffiInitAsync(); + testFfiValidation(nodejsPayjoin); + await testIntegrationV2ToV2(nodejsPayjoin); + + await webUniffiInitAsync(); + testFfiValidation(webPayjoin); + await testIntegrationV2ToV2(webPayjoin); } runTests().catch((error: unknown) => { diff --git a/payjoin-ffi/javascript/test/unit.test.ts b/payjoin-ffi/javascript/test/unit.test.ts index 1199bcc74..4b085c35f 100644 --- a/payjoin-ffi/javascript/test/unit.test.ts +++ b/payjoin-ffi/javascript/test/unit.test.ts @@ -1,7 +1,14 @@ import { describe, test, before } from "node:test"; import assert from "node:assert"; -import { payjoin, uniffiInitAsync } from "payjoin"; -import * as testUtils from "../test-utils/index.js"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { + payjoin as nodejsPayjoin, + uniffiInitAsync as nodejsUniffiInitAsync, +} from "payjoin"; +import * as webPayjoinModule from "../src/web/generated/payjoin.js"; +import initWebAsync from "../src/web/generated/wasm-bindgen/index.js"; import { InMemoryReceiverPersister, InMemoryReceiverPersisterAsync, @@ -9,417 +16,353 @@ import { InMemorySenderPersisterAsync, } from "./utils.ts"; -before(async () => { - await uniffiInitAsync(); -}); - -describe("URI tests", () => { - test("URL encoded payjoin parameter", () => { - const uri = - "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; - const result = payjoin.Url.parse(uri); - assert.ok(result, "pj url should be url encoded"); - }); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function webUniffiInitAsync() { + const wasmPath = join( + __dirname, + "../src/web/generated/wasm-bindgen/index_bg.wasm", + ); + const wasmBytes = readFileSync(wasmPath); + await initWebAsync({ module_or_path: wasmBytes }); + webPayjoinModule.default.initialize(); +} + +const OHTTP_KEYS = new Uint8Array([ + 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, +]).buffer; + +const ORIGINAL_PSBT = + "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; + +function runUnitTests(name: string, payjoin: typeof nodejsPayjoin) { + describe(`[${name}] URI tests`, () => { + test("URL encoded payjoin parameter", () => { + const uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; + const result = payjoin.Url.parse(uri); + assert.ok(result, "pj url should be url encoded"); + }); - test("valid URL", () => { - const uri = - "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; - const result = payjoin.Url.parse(uri); - assert.ok(result, "pj is not a valid url"); - }); + test("valid URL", () => { + const uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; + const result = payjoin.Url.parse(uri); + assert.ok(result, "pj is not a valid url"); + }); - test("missing amount should be ok", () => { - const uri = - "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pj=https://testnet.demo.btcpayserver.org/BTC/pj"; - const result = payjoin.Url.parse(uri); - assert.ok(result, "missing amount should be ok"); - }); + test("missing amount should be ok", () => { + const uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pj=https://testnet.demo.btcpayserver.org/BTC/pj"; + const result = payjoin.Url.parse(uri); + assert.ok(result, "missing amount should be ok"); + }); - test("valid URIs with different addresses and endpoints", () => { - const https = "https://example.com"; - const onion = - "http://vjdpwgybvubne5hda6v4c5iaeeevhge6jvo3w2cl6eocbwwvwxp7b7qd.onion"; - - const base58 = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX"; - const bech32Upper = - "BITCOIN:TB1Q6D3A2W975YNY0ASUVD9A67NER4NKS58FF0Q8G4"; - const bech32Lower = - "bitcoin:tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; - - const addresses = [base58, bech32Upper, bech32Lower]; - const pjs = [https, onion]; - - for (const address of addresses) { - for (const pj of pjs) { - const uri = `${address}?amount=1&pj=${pj}`; - assert.doesNotThrow( - () => payjoin.Url.parse(uri), - `Failed to create a valid Uri for ${uri}`, - ); + test("valid URIs with different addresses and endpoints", () => { + const https = "https://example.com"; + const onion = + "http://vjdpwgybvubne5hda6v4c5iaeeevhge6jvo3w2cl6eocbwwvwxp7b7qd.onion"; + + const base58 = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX"; + const bech32Upper = + "BITCOIN:TB1Q6D3A2W975YNY0ASUVD9A67NER4NKS58FF0Q8G4"; + const bech32Lower = + "bitcoin:tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + + const addresses = [base58, bech32Upper, bech32Lower]; + const pjs = [https, onion]; + + for (const address of addresses) { + for (const pj of pjs) { + const uri = `${address}?amount=1&pj=${pj}`; + assert.doesNotThrow( + () => payjoin.Url.parse(uri), + `Failed to create a valid Uri for ${uri}`, + ); + } } - } + }); }); -}); -describe("Persistence tests", () => { - test("receiver persistence", () => { - const persister = new InMemoryReceiverPersister(); - const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const builder = new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ); - builder.build().save(persister); - - const result = payjoin.replayReceiverEventLog(persister); - const state = result.state(); - - assert.strictEqual( - state.tag, - "Initialized", - "State should be Initialized", - ); - }); + describe(`[${name}] Persistence tests`, () => { + test("receiver persistence", () => { + const persister = new InMemoryReceiverPersister(); + const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); - test("sender persistence", () => { - const persister = new InMemoryReceiverPersister(); - const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const receiver = new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ) - .build() - .save(persister); - const uri = receiver.pjUri(); - - const senderPersister = new InMemorySenderPersister(); - const psbt = testUtils.originalPsbt(); - const withReplyKey = new payjoin.SenderBuilder(psbt, uri) - .buildRecommended(BigInt(1000)) - .save(senderPersister); - - assert.ok(withReplyKey, "Sender should be created successfully"); - }); -}); + const builder = new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ); + builder.build().save(persister); -describe("Receiver cancel tests", () => { - test("receiver cancel from initialized", () => { - const persister = new InMemoryReceiverPersister(); - const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const initialized = new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ) - .build() - .save(persister); - const cancelTransition = initialized.cancel(); - const fallbackTx = cancelTransition.save(persister); - assert.strictEqual(fallbackTx, undefined); - - const result = payjoin.replayReceiverEventLog(persister); - const state = result.state(); - assert.strictEqual( - state.tag, - "Closed", - "State should be Closed after cancel", - ); - }); + const result = payjoin.replayReceiverEventLog(persister); + const state = result.state(); - test("receiver cancel async from initialized", async () => { - const persister = new InMemoryReceiverPersisterAsync(); - const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const initialized = await new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ) - .build() - .saveAsync(persister); - const cancelTransition = initialized.cancel(); - const fallbackTx = await cancelTransition.saveAsync(persister); - assert.strictEqual(fallbackTx, undefined); - - const result = await payjoin.replayReceiverEventLogAsync(persister); - const state = result.state(); - assert.strictEqual( - state.tag, - "Closed", - "State should be Closed after cancel", - ); - }); -}); + assert.strictEqual( + state.tag, + "Initialized", + "State should be Initialized", + ); + }); -describe("Sender cancel tests", () => { - test("sender cancel from with reply key", () => { - const persister = new InMemoryReceiverPersister(); - const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const receiver = new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ) - .build() - .save(persister); - const uri = receiver.pjUri(); - - const senderPersister = new InMemorySenderPersister(); - const psbt = testUtils.originalPsbt(); - const withReplyKey = new payjoin.SenderBuilder(psbt, uri) - .buildRecommended(BigInt(1000)) - .save(senderPersister); - - const cancelTransition = withReplyKey.cancel(); - const fallbackTx = cancelTransition.save(senderPersister); - assert.ok(fallbackTx, "fallback tx should be returned"); - assert.ok( - fallbackTx.byteLength > 0, - "fallback tx bytes should be non-empty", - ); - - const result = payjoin.replaySenderEventLog(senderPersister); - const state = result.state(); - assert.strictEqual( - state.tag, - "Closed", - "State should be Closed after cancel", - ); - }); + test("sender persistence", () => { + const persister = new InMemoryReceiverPersister(); + const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); - test("sender cancel async from with reply key", async () => { - const persister = new InMemoryReceiverPersisterAsync(); - const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const receiver = await new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ) - .build() - .saveAsync(persister); - const uri = receiver.pjUri(); - - const senderPersister = new InMemorySenderPersisterAsync(); - const psbt = testUtils.originalPsbt(); - const withReplyKey = await new payjoin.SenderBuilder(psbt, uri) - .buildRecommended(BigInt(1000)) - .saveAsync(senderPersister); - - const cancelTransition = withReplyKey.cancel(); - const fallbackTx = await cancelTransition.saveAsync(senderPersister); - assert.ok(fallbackTx, "fallback tx should be returned"); - assert.ok( - fallbackTx.byteLength > 0, - "fallback tx bytes should be non-empty", - ); - - const result = await payjoin.replaySenderEventLogAsync(senderPersister); - const state = result.state(); - assert.strictEqual( - state.tag, - "Closed", - "State should be Closed after cancel", - ); + const receiver = new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ) + .build() + .save(persister); + const uri = receiver.pjUri(); + + const senderPersister = new InMemorySenderPersister(); + const withReplyKey = new payjoin.SenderBuilder(ORIGINAL_PSBT, uri) + .buildRecommended(BigInt(1000)) + .save(senderPersister); + + assert.ok(withReplyKey, "Sender should be created successfully"); + }); }); -}); -describe("Async Persistence tests", () => { - test("receiver async persistence", async () => { - const persister = new InMemoryReceiverPersisterAsync(); - const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const builder = new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ); - await builder.build().saveAsync(persister); - - const result = await payjoin.replayReceiverEventLogAsync(persister); - const state = result.state(); - - assert.strictEqual( - state.tag, - "Initialized", - "State should be Initialized", - ); + describe(`[${name}] Receiver cancel tests`, () => { + test("receiver cancel from initialized", () => { + const persister = new InMemoryReceiverPersister(); + const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const initialized = new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ) + .build() + .save(persister); + const cancelTransition = initialized.cancel(); + const fallbackTx = cancelTransition.save(persister); + assert.strictEqual(fallbackTx, undefined); + + const result = payjoin.replayReceiverEventLog(persister); + const state = result.state(); + assert.strictEqual( + state.tag, + "Closed", + "State should be Closed after cancel", + ); + }); + + test("receiver cancel async from initialized", async () => { + const persister = new InMemoryReceiverPersisterAsync(); + const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const initialized = await new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ) + .build() + .saveAsync(persister); + const cancelTransition = initialized.cancel(); + const fallbackTx = await cancelTransition.saveAsync(persister); + assert.strictEqual(fallbackTx, undefined); + + const result = await payjoin.replayReceiverEventLogAsync(persister); + const state = result.state(); + assert.strictEqual( + state.tag, + "Closed", + "State should be Closed after cancel", + ); + }); }); - test("sender async persistence", async () => { - const persister = new InMemoryReceiverPersisterAsync(); - const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; - const ohttpKeys = payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ); - - const receiver = await new payjoin.ReceiverBuilder( - address, - "https://example.com", - ohttpKeys, - ) - .build() - .saveAsync(persister); - const uri = receiver.pjUri(); - - const senderPersister = new InMemorySenderPersisterAsync(); - const psbt = testUtils.originalPsbt(); - const withReplyKey = await new payjoin.SenderBuilder(psbt, uri) - .buildRecommended(BigInt(1000)) - .saveAsync(senderPersister); - - assert.ok(withReplyKey, "Sender should be created successfully"); + describe(`[${name}] Sender cancel tests`, () => { + test("sender cancel from with reply key", () => { + const persister = new InMemoryReceiverPersister(); + const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const receiver = new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ) + .build() + .save(persister); + const uri = receiver.pjUri(); + + const senderPersister = new InMemorySenderPersister(); + const withReplyKey = new payjoin.SenderBuilder(ORIGINAL_PSBT, uri) + .buildRecommended(BigInt(1000)) + .save(senderPersister); + + const cancelTransition = withReplyKey.cancel(); + const fallbackTx = cancelTransition.save(senderPersister); + assert.ok(fallbackTx, "fallback tx should be returned"); + assert.ok( + fallbackTx.byteLength > 0, + "fallback tx bytes should be non-empty", + ); + + const result = payjoin.replaySenderEventLog(senderPersister); + const state = result.state(); + assert.strictEqual( + state.tag, + "Closed", + "State should be Closed after cancel", + ); + }); + + test("sender cancel async from with reply key", async () => { + const persister = new InMemoryReceiverPersisterAsync(); + const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const receiver = await new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ) + .build() + .saveAsync(persister); + const uri = receiver.pjUri(); + + const senderPersister = new InMemorySenderPersisterAsync(); + const withReplyKey = await new payjoin.SenderBuilder( + ORIGINAL_PSBT, + uri, + ) + .buildRecommended(BigInt(1000)) + .saveAsync(senderPersister); + + const cancelTransition = withReplyKey.cancel(); + const fallbackTx = + await cancelTransition.saveAsync(senderPersister); + assert.ok(fallbackTx, "fallback tx should be returned"); + assert.ok( + fallbackTx.byteLength > 0, + "fallback tx bytes should be non-empty", + ); + + const result = + await payjoin.replaySenderEventLogAsync(senderPersister); + const state = result.state(); + assert.strictEqual( + state.tag, + "Closed", + "State should be Closed after cancel", + ); + }); }); -}); -describe("Validation", () => { - test("receiver builder rejects bad address", () => { - assert.throws(() => { - new payjoin.ReceiverBuilder( - "not-an-address", + describe(`[${name}] Async Persistence tests`, () => { + test("receiver async persistence", async () => { + const persister = new InMemoryReceiverPersisterAsync(); + const address = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const builder = new payjoin.ReceiverBuilder( + address, "https://example.com", - payjoin.OhttpKeys.decode( - new Uint8Array([ - 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, - ]).buffer, - ), + ohttpKeys, + ); + await builder.build().saveAsync(persister); + + const result = await payjoin.replayReceiverEventLogAsync(persister); + const state = result.state(); + + assert.strictEqual( + state.tag, + "Initialized", + "State should be Initialized", ); }); + + test("sender async persistence", async () => { + const persister = new InMemoryReceiverPersisterAsync(); + const address = "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK"; + const ohttpKeys = payjoin.OhttpKeys.decode(OHTTP_KEYS); + + const receiver = await new payjoin.ReceiverBuilder( + address, + "https://example.com", + ohttpKeys, + ) + .build() + .saveAsync(persister); + const uri = receiver.pjUri(); + + const senderPersister = new InMemorySenderPersisterAsync(); + const withReplyKey = await new payjoin.SenderBuilder( + ORIGINAL_PSBT, + uri, + ) + .buildRecommended(BigInt(1000)) + .saveAsync(senderPersister); + + assert.ok(withReplyKey, "Sender should be created successfully"); + }); }); - test("input pair rejects invalid outpoint", () => { - assert.throws(() => { - const txin = payjoin.TxIn.create({ - previousOutput: payjoin.OutPoint.create({ - txid: "deadbeef", - vout: 0, - }), - scriptSig: new Uint8Array([]), - sequence: 0, - witness: [], + describe(`[${name}] Validation`, () => { + test("receiver builder rejects bad address", () => { + assert.throws(() => { + new payjoin.ReceiverBuilder( + "not-an-address", + "https://example.com", + payjoin.OhttpKeys.decode(OHTTP_KEYS), + ); }); - const psbtIn = payjoin.PsbtInput.create({ - witnessUtxo: undefined, - redeemScript: undefined, - witnessScript: undefined, + }); + + test("input pair rejects invalid outpoint", () => { + assert.throws(() => { + const txin = payjoin.TxIn.create({ + previousOutput: payjoin.OutPoint.create({ + txid: "deadbeef", + vout: 0, + }), + scriptSig: new Uint8Array([]), + sequence: 0, + witness: [], + }); + const psbtIn = payjoin.PsbtInput.create({ + witnessUtxo: undefined, + redeemScript: undefined, + witnessScript: undefined, + }); + new payjoin.InputPair(txin, psbtIn, undefined); }); - new payjoin.InputPair(txin, psbtIn, undefined); }); - }); - test("sender builder rejects bad psbt", () => { - assert.throws(() => { - new payjoin.SenderBuilder( - "not-a-psbt", - "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX", - ); + test("sender builder rejects bad psbt", () => { + assert.throws(() => { + new payjoin.SenderBuilder( + "not-a-psbt", + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX", + ); + }); }); }); +} + +before(async () => { + await nodejsUniffiInitAsync(); + await webUniffiInitAsync(); }); + +runUnitTests("nodejs", nodejsPayjoin); +runUnitTests("web", webPayjoinModule as unknown as typeof nodejsPayjoin);