From 6d311f2267aaf7eb925497a7941dfd443e579a9d Mon Sep 17 00:00:00 2001 From: xstoicunicornx Date: Thu, 21 May 2026 21:50:26 -0500 Subject: [PATCH] FFI: Refactor Javascript integration test Consolidate standalone receiver processing functions into a ReceiverProcessor class that encapsulates the payjoin module, RPC client, and persister. Fix the PJ helper type to use prototype inference, update the web import paths from src to dist, and correct the CheckInputsNotSeenCallback parameter type. --- .../javascript/test/integration.test.ts | 440 +++++++----------- 1 file changed, 174 insertions(+), 266 deletions(-) diff --git a/payjoin-ffi/javascript/test/integration.test.ts b/payjoin-ffi/javascript/test/integration.test.ts index 36aed0f2c..0dde4a267 100644 --- a/payjoin-ffi/javascript/test/integration.test.ts +++ b/payjoin-ffi/javascript/test/integration.test.ts @@ -7,8 +7,8 @@ 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 * as webPayjoinModule from "../dist/web/generated/payjoin.js"; +import initWebAsync from "../dist/web/generated/wasm-bindgen/index.js"; import { InMemoryReceiverPersister, InMemorySenderPersister } from "./utils.ts"; const __filename = fileURLToPath(import.meta.url); @@ -34,10 +34,12 @@ interface Utxo { type PayjoinModule = typeof nodejsPayjoin; const webPayjoin = webPayjoinModule as unknown as PayjoinModule; -// Helper types to avoid repeating InstanceType everywhere. -type PJ = InstanceType< - PayjoinModule[K] & (new (...args: any) => any) ->; +type PJ = PayjoinModule[K] extends { + prototype: infer P; +} + ? P + : never; + type PJNested< K extends keyof PayjoinModule, N extends keyof PayjoinModule[K], @@ -137,7 +139,7 @@ class CheckInputsNotSeenCallback { this.connection = connection; } - callback(_outpoint: ArrayBuffer): boolean { + callback(_outpoint: nodejsPayjoin.OutPoint): boolean { if (this.connection) { } return false; @@ -159,18 +161,6 @@ class ProcessPsbtCallback { } } -function createReceiverContext( - payjoin: PayjoinModule, - address: string, - directory: string, - ohttpKeys: ReturnType, - persister: InMemoryReceiverPersister, -): PJ<"Initialized"> { - return new payjoin.ReceiverBuilder(address, directory, ohttpKeys) - .build() - .save(persister); -} - function buildSweepPsbt( sender: testUtils.RpcClient, pjUri: PJ<"PjUri">, @@ -230,254 +220,175 @@ function getInputs( return inputs; } -async function processProvisionalProposal( - proposal: PJ<"ProvisionalProposal">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - return proposal - .finalizeProposal(new ProcessPsbtCallback(receiver)) - .save(recvPersister); -} +class ReceiverProcessor { + constructor( + private readonly payjoin: PayjoinModule, + private readonly receiver: testUtils.RpcClient, + private readonly recvPersister: InMemoryReceiverPersister, + ) {} + + private async processProvisionalProposal( + proposal: PJ<"ProvisionalProposal">, + ): Promise> { + return proposal + .finalizeProposal(new ProcessPsbtCallback(this.receiver)) + .save(this.recvPersister) as PJ<"PayjoinProposal">; + } -async function processWantsFeeRange( - proposal: PJ<"WantsFeeRange">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const wantsFeeRange = proposal.applyFeeRange(1n, 10n).save(recvPersister); - return await processProvisionalProposal( - wantsFeeRange, - receiver, - recvPersister, - ); -} + private async processWantsFeeRange( + proposal: PJ<"WantsFeeRange">, + ): Promise> { + const provisionalProposal = proposal + .applyFeeRange(1n, 10n) + .save(this.recvPersister) as PJ<"ProvisionalProposal">; + return this.processProvisionalProposal(provisionalProposal); + } -async function processWantsInputs( - payjoin: PayjoinModule, - proposal: PJ<"WantsInputs">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const provisionalProposal = proposal - .contributeInputs(getInputs(payjoin, receiver)) - .commitInputs() - .save(recvPersister); - return await processWantsFeeRange( - provisionalProposal, - receiver, - recvPersister, - ); -} + private async processWantsInputs( + proposal: PJ<"WantsInputs">, + ): Promise> { + const provisionalProposal = proposal + .contributeInputs(getInputs(this.payjoin, this.receiver)) + .commitInputs() + .save(this.recvPersister) as PJ<"WantsFeeRange">; + return this.processWantsFeeRange(provisionalProposal); + } -async function processWantsOutputs( - payjoin: PayjoinModule, - proposal: PJ<"WantsOutputs">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const wantsInputs = proposal.commitOutputs().save(recvPersister); - return await processWantsInputs( - payjoin, - wantsInputs, - receiver, - recvPersister, - ); -} + private async processWantsOutputs( + proposal: PJ<"WantsOutputs">, + ): Promise> { + const wantsInputs = proposal + .commitOutputs() + .save(this.recvPersister) as PJ<"WantsInputs">; + return this.processWantsInputs(wantsInputs); + } -async function processOutputsUnknown( - payjoin: PayjoinModule, - proposal: PJ<"OutputsUnknown">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const wantsOutputs = proposal - .identifyReceiverOutputs(new IsScriptOwnedCallback(receiver)) - .save(recvPersister); - return await processWantsOutputs( - payjoin, - wantsOutputs, - receiver, - recvPersister, - ); -} + private async processOutputsUnknown( + proposal: PJ<"OutputsUnknown">, + ): Promise> { + const wantsOutputs = proposal + .identifyReceiverOutputs(new IsScriptOwnedCallback(this.receiver)) + .save(this.recvPersister) as PJ<"WantsOutputs">; + return this.processWantsOutputs(wantsOutputs); + } -async function processMaybeInputsSeen( - payjoin: PayjoinModule, - proposal: PJ<"MaybeInputsSeen">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const outputsUnknown = proposal - .checkNoInputsSeenBefore(new CheckInputsNotSeenCallback(receiver)) - .save(recvPersister); - return await processOutputsUnknown( - payjoin, - outputsUnknown, - receiver, - recvPersister, - ); -} + private async processMaybeInputsSeen( + proposal: PJ<"MaybeInputsSeen">, + ): Promise> { + const outputsUnknown = proposal + .checkNoInputsSeenBefore( + new CheckInputsNotSeenCallback(this.receiver), + ) + .save(this.recvPersister) as PJ<"OutputsUnknown">; + return this.processOutputsUnknown(outputsUnknown); + } -async function processMaybeInputsOwned( - payjoin: PayjoinModule, - proposal: PJ<"MaybeInputsOwned">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const maybeInputsOwned = proposal - .checkInputsNotOwned(new IsScriptOwnedCallback(receiver)) - .save(recvPersister); - return await processMaybeInputsSeen( - payjoin, - maybeInputsOwned, - receiver, - recvPersister, - ); -} + private async processMaybeInputsOwned( + proposal: nodejsPayjoin.MaybeInputsOwned, + ): Promise> { + const maybeInputsSeen = proposal + .checkInputsNotOwned(new IsScriptOwnedCallback(this.receiver)) + .save(this.recvPersister) as PJ<"MaybeInputsSeen">; + return this.processMaybeInputsSeen(maybeInputsSeen); + } -async function processUncheckedProposal( - payjoin: PayjoinModule, - proposal: PJ<"UncheckedOriginalPayload">, - receiver: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, -): Promise> { - const uncheckedProposal = proposal - .checkBroadcastSuitability( - undefined, - new MempoolAcceptanceCallback(receiver), - ) - .save(recvPersister); - return await processMaybeInputsOwned( - payjoin, - uncheckedProposal, - receiver, - recvPersister, - ); -} + private async processUncheckedProposal( + proposal: PJ<"UncheckedOriginalPayload">, + ): Promise> { + const maybeInputsOwned = proposal + .checkBroadcastSuitability( + undefined, + new MempoolAcceptanceCallback(this.receiver), + ) + .save(this.recvPersister) as PJ<"MaybeInputsOwned">; + return this.processMaybeInputsOwned(maybeInputsOwned); + } -async function retrieveReceiverProposal( - payjoin: PayjoinModule, - receiver: PJ<"Initialized">, - receiverRpc: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, - ohttpRelay: string, -): Promise | null> { - const request = receiver.createPollRequest(ohttpRelay); - const response = await fetch(request.request.url, { - method: "POST", - headers: { "Content-Type": request.request.contentType }, - body: request.request.body, - }); - const responseBuffer = await response.arrayBuffer(); - const res = receiver - .processResponse(responseBuffer, request.clientResponse) - .save(recvPersister); - - if (res instanceof payjoin.InitializedTransitionOutcome.Stasis) { - return null; - } else if (res instanceof payjoin.InitializedTransitionOutcome.Progress) { - const proposal = res.inner.inner; - return await processUncheckedProposal( - payjoin, - proposal, - receiverRpc, - recvPersister, - ); + createReceiverContext( + address: string, + directory: string, + ohttpKeys: ReturnType, + ): PJ<"Initialized"> { + return new this.payjoin.ReceiverBuilder(address, directory, ohttpKeys) + .build() + .save(this.recvPersister) as PJ<"Initialized">; } - throw new Error(`Unknown initialized transition outcome`); -} + private async retrieveReceiverProposal( + session: PJ<"Initialized">, + ohttpRelay: string, + ): Promise | null> { + const request = session.createPollRequest(ohttpRelay); + const response = await fetch(request.request.url, { + method: "POST", + headers: { "Content-Type": request.request.contentType }, + body: request.request.body, + }); + const responseBuffer = await response.arrayBuffer(); + const res = session + .processResponse(responseBuffer, request.clientResponse) + .save(this.recvPersister); + + if (res instanceof this.payjoin.InitializedTransitionOutcome.Stasis) { + return null; + } else if ( + res instanceof this.payjoin.InitializedTransitionOutcome.Progress + ) { + return this.processUncheckedProposal( + res.inner.inner as PJ<"UncheckedOriginalPayload">, + ); + } -async function processReceiverProposal( - payjoin: PayjoinModule, - receiver: - | PJ<"Initialized"> - | PJ<"UncheckedOriginalPayload"> - | PJ<"MaybeInputsOwned"> - | PJ<"MaybeInputsSeen"> - | PJ<"OutputsUnknown"> - | PJ<"WantsOutputs"> - | PJ<"WantsInputs"> - | PJ<"WantsFeeRange"> - | PJ<"ProvisionalProposal"> - | PJ<"PayjoinProposal">, - receiverRpc: testUtils.RpcClient, - recvPersister: InMemoryReceiverPersister, - ohttpRelay: string, -): Promise | null> { - if (receiver instanceof payjoin.Initialized) { - return await retrieveReceiverProposal( - payjoin, - receiver, - receiverRpc, - recvPersister, - ohttpRelay, - ); - } - if (receiver instanceof payjoin.UncheckedOriginalPayload) { - return await processUncheckedProposal( - payjoin, - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.MaybeInputsOwned) { - return await processMaybeInputsOwned( - payjoin, - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.MaybeInputsSeen) { - return await processMaybeInputsSeen( - payjoin, - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.OutputsUnknown) { - return await processOutputsUnknown( - payjoin, - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.WantsOutputs) { - return await processWantsOutputs( - payjoin, - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.WantsInputs) { - return await processWantsInputs( - payjoin, - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.WantsFeeRange) { - return await processWantsFeeRange(receiver, receiverRpc, recvPersister); - } - if (receiver instanceof payjoin.ProvisionalProposal) { - return await processProvisionalProposal( - receiver, - receiverRpc, - recvPersister, - ); - } - if (receiver instanceof payjoin.PayjoinProposal) { - return receiver; + throw new Error(`Unknown initialized transition outcome`); } - throw new Error(`Unknown receiver state`); + async processReceiverProposal( + receiver: + | PJ<"Initialized"> + | PJ<"UncheckedOriginalPayload"> + | PJ<"MaybeInputsOwned"> + | PJ<"MaybeInputsSeen"> + | PJ<"OutputsUnknown"> + | PJ<"WantsOutputs"> + | PJ<"WantsInputs"> + | PJ<"WantsFeeRange"> + | PJ<"ProvisionalProposal"> + | PJ<"PayjoinProposal">, + ohttpRelay: string, + ): Promise | null> { + if (receiver instanceof this.payjoin.Initialized) { + return this.retrieveReceiverProposal(receiver, ohttpRelay); + } + if (receiver instanceof this.payjoin.UncheckedOriginalPayload) { + return this.processUncheckedProposal(receiver); + } + if (receiver instanceof this.payjoin.MaybeInputsOwned) { + return this.processMaybeInputsOwned(receiver); + } + if (receiver instanceof this.payjoin.MaybeInputsSeen) { + return this.processMaybeInputsSeen(receiver); + } + if (receiver instanceof this.payjoin.OutputsUnknown) { + return this.processOutputsUnknown(receiver); + } + if (receiver instanceof this.payjoin.WantsOutputs) { + return this.processWantsOutputs(receiver); + } + if (receiver instanceof this.payjoin.WantsInputs) { + return this.processWantsInputs(receiver); + } + if (receiver instanceof this.payjoin.WantsFeeRange) { + return this.processWantsFeeRange(receiver); + } + if (receiver instanceof this.payjoin.ProvisionalProposal) { + return this.processProvisionalProposal(receiver); + } + if (receiver instanceof this.payjoin.PayjoinProposal) { + return receiver; + } + + throw new Error(`Unknown receiver state`); + } } function testFfiValidation(payjoin: PayjoinModule): void { @@ -598,21 +509,21 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise { ); const recvPersister = new InMemoryReceiverPersister(); + const recvProcessor = new ReceiverProcessor( + payjoin, + receiver, + recvPersister, + ); const senderPersister = new InMemorySenderPersister(); - const session = createReceiverContext( - payjoin, + const session = recvProcessor.createReceiverContext( receiverAddress, directory, ohttpKeys, - recvPersister, ); - let processResponse = await processReceiverProposal( - payjoin, + const processResponse = await recvProcessor.processReceiverProposal( session, - receiver, - recvPersister, ohttpRelay, ); assert.strictEqual( @@ -622,7 +533,7 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise { ); const pjUri = session.pjUri(); - const psbt = buildSweepPsbt(sender, pjUri); + const psbt = buildSweepPsbt(sender, pjUri as PJ<"PjUri">); const reqCtx = new payjoin.SenderBuilder(psbt, pjUri) .buildRecommended(1000n) .save(senderPersister); @@ -638,11 +549,8 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise { .processResponse(responseBuffer, request.ohttpCtx) .save(senderPersister); - let payjoinProposal = await processReceiverProposal( - payjoin, + const payjoinProposal = await recvProcessor.processReceiverProposal( session, - receiver, - recvPersister, ohttpRelay, ); assert.notStrictEqual(