Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions payjoin-ffi/javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions payjoin-ffi/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"rust"
],
"devDependencies": {
"@types/node": "25.9.1",
"prettier": "3.6.2",
"tsx": "4.20.6",
"typescript": "5.9.3"
Expand Down
107 changes: 52 additions & 55 deletions payjoin-ffi/javascript/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ interface Utxo {
type PayjoinModule = typeof nodejsPayjoin;
const webPayjoin = webPayjoinModule as unknown as PayjoinModule;

// Helper types to avoid repeating InstanceType<PayjoinModule[...]> everywhere.
type PJ<K extends keyof PayjoinModule> = InstanceType<
PayjoinModule[K] & (new (...args: any) => any)
>;
type PJNested<
K extends keyof PayjoinModule,
N extends keyof PayjoinModule[K],
> = InstanceType<PayjoinModule[K][N] & (new (...args: any) => any)>;

Comment on lines +37 to +45

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this causing lint errors or just a readability enhancement you decided to make?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was causing LSP errors. For example this return type:

InstanceType<PayjoinModule["Initialized"]>

was triggering this error:

error 2344| Type 'typeof Initialized' does not satisfy the constraint 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.

class MempoolAcceptanceCallback {
private connection: testUtils.RpcClient;

Expand Down Expand Up @@ -129,6 +138,8 @@ class CheckInputsNotSeenCallback {
}

callback(_outpoint: ArrayBuffer): boolean {
if (this.connection) {
}
return false;
}
}
Expand All @@ -154,15 +165,15 @@ function createReceiverContext(
directory: string,
ohttpKeys: ReturnType<PayjoinModule["OhttpKeys"]["decode"]>,
persister: InMemoryReceiverPersister,
): InstanceType<PayjoinModule["Initialized"]> {
): PJ<"Initialized"> {
return new payjoin.ReceiverBuilder(address, directory, ohttpKeys)
.build()
.save(persister);
}

function buildSweepPsbt(
sender: testUtils.RpcClient,
pjUri: InstanceType<PayjoinModule["PjUri"]>,
pjUri: PJ<"PjUri">,
): string {
const outputs: Record<string, number> = {};
outputs[pjUri.address()] = 50;
Expand Down Expand Up @@ -191,21 +202,22 @@ function buildSweepPsbt(
function getInputs(
payjoin: PayjoinModule,
rpcConnection: testUtils.RpcClient,
): InstanceType<PayjoinModule["InputPair"]>[] {
): PJ<"InputPair">[] {
const utxos: Utxo[] = JSON.parse(rpcConnection.call("listunspent", []));
const inputs: InstanceType<PayjoinModule["InputPair"]>[] = [];
const inputs: PJ<"InputPair">[] = [];
for (const utxo of utxos) {
const txin = payjoin.TxIn.create({
previousOutput: payjoin.OutPoint.create({
txid: utxo.txid,
vout: utxo.vout,
}),
scriptSig: new Uint8Array([]),
scriptSig: new Uint8Array([]).buffer,
sequence: 0,
witness: [],
});
const txOut = payjoin.TxOut.create({
valueSat: BigInt(Math.round(utxo.amount * 100_000_000)),
// @ts-ignore
scriptPubkey: Buffer.from(utxo.scriptPubKey, "hex"),
});
const psbtIn = payjoin.PsbtInput.create({
Expand All @@ -219,25 +231,22 @@ function getInputs(
}

async function processProvisionalProposal(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["ProvisionalProposal"]>,
proposal: PJ<"ProvisionalProposal">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
return proposal
.finalizeProposal(new ProcessPsbtCallback(receiver))
.save(recvPersister);
}

async function processWantsFeeRange(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["WantsFeeRange"]>,
proposal: PJ<"WantsFeeRange">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const wantsFeeRange = proposal.applyFeeRange(1n, 10n).save(recvPersister);
return await processProvisionalProposal(
payjoin,
wantsFeeRange,
receiver,
recvPersister,
Expand All @@ -246,16 +255,15 @@ async function processWantsFeeRange(

async function processWantsInputs(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["WantsInputs"]>,
proposal: PJ<"WantsInputs">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const provisionalProposal = proposal
.contributeInputs(getInputs(payjoin, receiver))
.commitInputs()
.save(recvPersister);
return await processWantsFeeRange(
payjoin,
provisionalProposal,
receiver,
recvPersister,
Expand All @@ -264,10 +272,10 @@ async function processWantsInputs(

async function processWantsOutputs(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["WantsOutputs"]>,
proposal: PJ<"WantsOutputs">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const wantsInputs = proposal.commitOutputs().save(recvPersister);
return await processWantsInputs(
payjoin,
Expand All @@ -279,10 +287,10 @@ async function processWantsOutputs(

async function processOutputsUnknown(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["OutputsUnknown"]>,
proposal: PJ<"OutputsUnknown">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const wantsOutputs = proposal
.identifyReceiverOutputs(new IsScriptOwnedCallback(receiver))
.save(recvPersister);
Expand All @@ -296,10 +304,10 @@ async function processOutputsUnknown(

async function processMaybeInputsSeen(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["MaybeInputsSeen"]>,
proposal: PJ<"MaybeInputsSeen">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const outputsUnknown = proposal
.checkNoInputsSeenBefore(new CheckInputsNotSeenCallback(receiver))
.save(recvPersister);
Expand All @@ -313,10 +321,10 @@ async function processMaybeInputsSeen(

async function processMaybeInputsOwned(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["MaybeInputsOwned"]>,
proposal: PJ<"MaybeInputsOwned">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const maybeInputsOwned = proposal
.checkInputsNotOwned(new IsScriptOwnedCallback(receiver))
.save(recvPersister);
Expand All @@ -330,10 +338,10 @@ async function processMaybeInputsOwned(

async function processUncheckedProposal(
payjoin: PayjoinModule,
proposal: InstanceType<PayjoinModule["UncheckedOriginalPayload"]>,
proposal: PJ<"UncheckedOriginalPayload">,
receiver: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]>> {
): Promise<PJ<"PayjoinProposal">> {
const uncheckedProposal = proposal
.checkBroadcastSuitability(
undefined,
Expand All @@ -350,11 +358,11 @@ async function processUncheckedProposal(

async function retrieveReceiverProposal(
payjoin: PayjoinModule,
receiver: InstanceType<PayjoinModule["Initialized"]>,
receiver: PJ<"Initialized">,
receiverRpc: testUtils.RpcClient,
recvPersister: InMemoryReceiverPersister,
ohttpRelay: string,
): Promise<InstanceType<PayjoinModule["PayjoinProposal"]> | null> {
): Promise<PJ<"PayjoinProposal"> | null> {
const request = receiver.createPollRequest(ohttpRelay);
const response = await fetch(request.request.url, {
method: "POST",
Expand Down Expand Up @@ -384,20 +392,20 @@ async function retrieveReceiverProposal(
async function processReceiverProposal(
payjoin: PayjoinModule,
receiver:
| InstanceType<PayjoinModule["Initialized"]>
| InstanceType<PayjoinModule["UncheckedOriginalPayload"]>
| InstanceType<PayjoinModule["MaybeInputsOwned"]>
| InstanceType<PayjoinModule["MaybeInputsSeen"]>
| InstanceType<PayjoinModule["OutputsUnknown"]>
| InstanceType<PayjoinModule["WantsOutputs"]>
| InstanceType<PayjoinModule["WantsInputs"]>
| InstanceType<PayjoinModule["WantsFeeRange"]>
| InstanceType<PayjoinModule["ProvisionalProposal"]>
| InstanceType<PayjoinModule["PayjoinProposal"]>,
| 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<InstanceType<PayjoinModule["PayjoinProposal"]> | null> {
): Promise<PJ<"PayjoinProposal"> | null> {
if (receiver instanceof payjoin.Initialized) {
return await retrieveReceiverProposal(
payjoin,
Expand Down Expand Up @@ -456,16 +464,10 @@ async function processReceiverProposal(
);
}
if (receiver instanceof payjoin.WantsFeeRange) {
return await processWantsFeeRange(
payjoin,
receiver,
receiverRpc,
recvPersister,
);
return await processWantsFeeRange(receiver, receiverRpc, recvPersister);
}
if (receiver instanceof payjoin.ProvisionalProposal) {
return await processProvisionalProposal(
payjoin,
receiver,
receiverRpc,
recvPersister,
Expand Down Expand Up @@ -591,7 +593,9 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise<void> {
const ohttpRelay = services.ohttpRelayUrl();
services.waitForServicesReady();
const ohttpKeysBytes = services.fetchOhttpKeys();
const ohttpKeys = payjoin.OhttpKeys.decode(ohttpKeysBytes.buffer);
const ohttpKeys = payjoin.OhttpKeys.decode(
ohttpKeysBytes.buffer as ArrayBuffer,
);

const recvPersister = new InMemoryReceiverPersister();
const senderPersister = new InMemorySenderPersister();
Expand Down Expand Up @@ -665,15 +669,8 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise<void> {
);

let pollOutcome:
| InstanceType<
PayjoinModule["PollingForProposalTransitionOutcome"]["Progress"]
>
| InstanceType<
PayjoinModule["PollingForProposalTransitionOutcome"]["Stasis"]
>
| InstanceType<
PayjoinModule["PollingForProposalTransitionOutcome"]["Terminal"]
>;
| PJNested<"PollingForProposalTransitionOutcome", "Progress">
| PJNested<"PollingForProposalTransitionOutcome", "Stasis">;
let attempts = 0;
while (true) {
const ohttpContextRequest = sendCtx.createPollRequest(ohttpRelay);
Expand Down
6 changes: 4 additions & 2 deletions payjoin-ffi/javascript/test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ function runUnitTests(name: string, payjoin: typeof nodejsPayjoin) {
txid: "deadbeef",
vout: 0,
}),
scriptSig: new Uint8Array([]),
scriptSig: new Uint8Array([]).buffer,
sequence: 0,
witness: [],
});
Expand All @@ -368,7 +368,9 @@ function runUnitTests(name: string, payjoin: typeof nodejsPayjoin) {
assert.throws(() => {
new payjoin.SenderBuilder(
"not-a-psbt",
"bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX",
payjoin.Uri.parse(
"bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX",
).checkPjSupported(),
);
});
});
Expand Down
Loading
Loading