diff --git a/payjoin-ffi/javascript/package-lock.json b/payjoin-ffi/javascript/package-lock.json index f3b0bec5b..cff87509d 100644 --- a/payjoin-ffi/javascript/package-lock.json +++ b/payjoin-ffi/javascript/package-lock.json @@ -1,16 +1,17 @@ { "name": "payjoin", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "payjoin", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "uniffi-bindgen-react-native": "github:spacebear21/uniffi-bindgen-react-native#50229433d02f1a37d34f0bdfe7ea615e9a364142" }, "devDependencies": { + "@types/node": "25.9.1", "prettier": "3.6.2", "tsx": "4.20.6", "typescript": "5.9.3" @@ -458,6 +459,16 @@ "node": ">=18" } }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -588,6 +599,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, "node_modules/uniffi-bindgen-react-native": { "version": "0.31.0-2", "resolved": "git+ssh://git@github.com/spacebear21/uniffi-bindgen-react-native.git#50229433d02f1a37d34f0bdfe7ea615e9a364142", diff --git a/payjoin-ffi/javascript/package.json b/payjoin-ffi/javascript/package.json index 3a858948f..94dc65dd9 100644 --- a/payjoin-ffi/javascript/package.json +++ b/payjoin-ffi/javascript/package.json @@ -26,6 +26,7 @@ "rust" ], "devDependencies": { + "@types/node": "25.9.1", "prettier": "3.6.2", "tsx": "4.20.6", "typescript": "5.9.3" diff --git a/payjoin-ffi/javascript/test/integration.test.ts b/payjoin-ffi/javascript/test/integration.test.ts index d16e1f2a6..36aed0f2c 100644 --- a/payjoin-ffi/javascript/test/integration.test.ts +++ b/payjoin-ffi/javascript/test/integration.test.ts @@ -34,6 +34,15 @@ 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 PJNested< + K extends keyof PayjoinModule, + N extends keyof PayjoinModule[K], +> = InstanceType any)>; + class MempoolAcceptanceCallback { private connection: testUtils.RpcClient; @@ -129,6 +138,8 @@ class CheckInputsNotSeenCallback { } callback(_outpoint: ArrayBuffer): boolean { + if (this.connection) { + } return false; } } @@ -154,7 +165,7 @@ function createReceiverContext( directory: string, ohttpKeys: ReturnType, persister: InMemoryReceiverPersister, -): InstanceType { +): PJ<"Initialized"> { return new payjoin.ReceiverBuilder(address, directory, ohttpKeys) .build() .save(persister); @@ -162,7 +173,7 @@ function createReceiverContext( function buildSweepPsbt( sender: testUtils.RpcClient, - pjUri: InstanceType, + pjUri: PJ<"PjUri">, ): string { const outputs: Record = {}; outputs[pjUri.address()] = 50; @@ -191,21 +202,22 @@ function buildSweepPsbt( function getInputs( payjoin: PayjoinModule, rpcConnection: testUtils.RpcClient, -): InstanceType[] { +): PJ<"InputPair">[] { const utxos: Utxo[] = JSON.parse(rpcConnection.call("listunspent", [])); - const inputs: InstanceType[] = []; + 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({ @@ -219,25 +231,22 @@ function getInputs( } async function processProvisionalProposal( - payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"ProvisionalProposal">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { return proposal .finalizeProposal(new ProcessPsbtCallback(receiver)) .save(recvPersister); } async function processWantsFeeRange( - payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"WantsFeeRange">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const wantsFeeRange = proposal.applyFeeRange(1n, 10n).save(recvPersister); return await processProvisionalProposal( - payjoin, wantsFeeRange, receiver, recvPersister, @@ -246,16 +255,15 @@ async function processWantsFeeRange( async function processWantsInputs( payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"WantsInputs">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const provisionalProposal = proposal .contributeInputs(getInputs(payjoin, receiver)) .commitInputs() .save(recvPersister); return await processWantsFeeRange( - payjoin, provisionalProposal, receiver, recvPersister, @@ -264,10 +272,10 @@ async function processWantsInputs( async function processWantsOutputs( payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"WantsOutputs">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const wantsInputs = proposal.commitOutputs().save(recvPersister); return await processWantsInputs( payjoin, @@ -279,10 +287,10 @@ async function processWantsOutputs( async function processOutputsUnknown( payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"OutputsUnknown">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const wantsOutputs = proposal .identifyReceiverOutputs(new IsScriptOwnedCallback(receiver)) .save(recvPersister); @@ -296,10 +304,10 @@ async function processOutputsUnknown( async function processMaybeInputsSeen( payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"MaybeInputsSeen">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const outputsUnknown = proposal .checkNoInputsSeenBefore(new CheckInputsNotSeenCallback(receiver)) .save(recvPersister); @@ -313,10 +321,10 @@ async function processMaybeInputsSeen( async function processMaybeInputsOwned( payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"MaybeInputsOwned">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const maybeInputsOwned = proposal .checkInputsNotOwned(new IsScriptOwnedCallback(receiver)) .save(recvPersister); @@ -330,10 +338,10 @@ async function processMaybeInputsOwned( async function processUncheckedProposal( payjoin: PayjoinModule, - proposal: InstanceType, + proposal: PJ<"UncheckedOriginalPayload">, receiver: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, -): Promise> { +): Promise> { const uncheckedProposal = proposal .checkBroadcastSuitability( undefined, @@ -350,11 +358,11 @@ async function processUncheckedProposal( async function retrieveReceiverProposal( payjoin: PayjoinModule, - receiver: InstanceType, + receiver: PJ<"Initialized">, receiverRpc: testUtils.RpcClient, recvPersister: InMemoryReceiverPersister, ohttpRelay: string, -): Promise | null> { +): Promise | null> { const request = receiver.createPollRequest(ohttpRelay); const response = await fetch(request.request.url, { method: "POST", @@ -384,20 +392,20 @@ async function retrieveReceiverProposal( async function processReceiverProposal( payjoin: PayjoinModule, receiver: - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType, + | 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> { +): Promise | null> { if (receiver instanceof payjoin.Initialized) { return await retrieveReceiverProposal( payjoin, @@ -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, @@ -591,7 +593,9 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise { 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(); @@ -665,15 +669,8 @@ async function testIntegrationV2ToV2(payjoin: PayjoinModule): Promise { ); 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); diff --git a/payjoin-ffi/javascript/test/unit.test.ts b/payjoin-ffi/javascript/test/unit.test.ts index 972a2c2ab..a88b5882c 100644 --- a/payjoin-ffi/javascript/test/unit.test.ts +++ b/payjoin-ffi/javascript/test/unit.test.ts @@ -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: [], }); @@ -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(), ); }); }); diff --git a/payjoin-ffi/python/test/test_payjoin_integration_test.py b/payjoin-ffi/python/test/test_payjoin_integration_test.py index 500ddb654..146fc41fe 100644 --- a/payjoin-ffi/python/test/test_payjoin_integration_test.py +++ b/payjoin-ffi/python/test/test_payjoin_integration_test.py @@ -2,6 +2,7 @@ import sys import httpx import json +from typing import cast, Protocol, Any from payjoin import * from payjoin.http import fetch_ohttp_keys @@ -14,10 +15,13 @@ 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) ) -from pprint import * from .utils import InMemoryReceiverPersister, InMemorySenderPersister +class HasInner(Protocol): + inner: Any + + class TestPayjoin(unittest.IsolatedAsyncioTestCase): @classmethod def setUpClass(cls): @@ -88,35 +92,53 @@ async def process_receiver_proposal( receiver: ReceiveSession, recv_persister: InMemoryReceiverPersister, ohttp_relay: str, - ) -> Optional[ReceiveSession]: + ) -> Optional[ReceiveSession.PAYJOIN_PROPOSAL]: if receiver.is_INITIALIZED(): res = await self.retrieve_receiver_proposal( - receiver.inner, recv_persister, ohttp_relay + cast(ReceiveSession.INITIALIZED, receiver).inner, + recv_persister, + ohttp_relay, ) if res is None: return None return res - if receiver.is_UNCHECKED_PROPOSAL(): - return await self.process_unchecked_proposal(receiver.inner, recv_persister) + if receiver.is_UNCHECKED_ORIGINAL_PAYLOAD(): + return await self.process_unchecked_proposal( + cast(ReceiveSession.UNCHECKED_ORIGINAL_PAYLOAD, receiver).inner, + recv_persister, + ) if receiver.is_MAYBE_INPUTS_OWNED(): - return await self.process_maybe_inputs_owned(receiver.inner, recv_persister) + return await self.process_maybe_inputs_owned( + cast(ReceiveSession.MAYBE_INPUTS_OWNED, receiver).inner, recv_persister + ) if receiver.is_MAYBE_INPUTS_SEEN(): - return await self.process_maybe_inputs_seen(receiver.inner, recv_persister) + return await self.process_maybe_inputs_seen( + cast(ReceiveSession.MAYBE_INPUTS_SEEN, receiver).inner, recv_persister + ) if receiver.is_OUTPUTS_UNKNOWN(): - return await self.process_outputs_unknown(receiver.inner, recv_persister) + return await self.process_outputs_unknown( + cast(ReceiveSession.OUTPUTS_UNKNOWN, receiver).inner, recv_persister + ) if receiver.is_WANTS_OUTPUTS(): - return await self.process_wants_outputs(receiver.inner, recv_persister) + return await self.process_wants_outputs( + cast(ReceiveSession.WANTS_OUTPUTS, receiver).inner, recv_persister + ) if receiver.is_WANTS_INPUTS(): - return await self.process_wants_inputs(receiver.inner, recv_persister) + return await self.process_wants_inputs( + cast(ReceiveSession.WANTS_INPUTS, receiver).inner, recv_persister + ) if receiver.is_WANTS_FEE_RANGE(): - return await self.process_wants_fee_range(receiver.inner, recv_persister) + return await self.process_wants_fee_range( + cast(ReceiveSession.WANTS_FEE_RANGE, receiver).inner, recv_persister + ) if receiver.is_PROVISIONAL_PROPOSAL(): return await self.process_provisional_proposal( - receiver.inner, recv_persister + cast(ReceiveSession.PROVISIONAL_PROPOSAL, receiver).inner, + recv_persister, ) if receiver.is_PAYJOIN_PROPOSAL(): - return receiver + return cast(ReceiveSession.PAYJOIN_PROPOSAL, receiver) raise Exception(f"Unknown receiver state: {receiver}") @@ -152,7 +174,9 @@ async def retrieve_receiver_proposal( ) if res.is_STASIS(): return None - return await self.process_unchecked_proposal(res.inner, recv_persister) + return await self.process_unchecked_proposal( + cast(ReceiveSession.UNCHECKED_ORIGINAL_PAYLOAD, res).inner, recv_persister + ) async def process_unchecked_proposal( self, @@ -244,7 +268,9 @@ async def test_integration_v2_to_v2(self): receiver_address, directory, ohttp_keys, recv_persister ) process_response = await self.process_receiver_proposal( - ReceiveSession.INITIALIZED(session), recv_persister, ohttp_relay + cast(ReceiveSession, ReceiveSession.INITIALIZED(session)), + recv_persister, + ohttp_relay, ) self.assertIsNone(process_response) @@ -258,14 +284,16 @@ async def test_integration_v2_to_v2(self): .build_recommended(1000) .save(sender_persister) ) - request: RequestOhttpContext = req_ctx.create_v2_post_request(ohttp_relay) + request_send: RequestOhttpContext = req_ctx.create_v2_post_request( + ohttp_relay + ) response = await agent.post( - url=request.request.url, - headers={"Content-Type": request.request.content_type}, - content=request.request.body, + url=request_send.request.url, + headers={"Content-Type": request_send.request.content_type}, + content=request_send.request.body, ) send_ctx: PollingForProposal = req_ctx.process_response( - response.content, request.ohttp_ctx + response.content, request_send.ohttp_ctx ).save(sender_persister) # POST Original PSBT @@ -274,19 +302,29 @@ async def test_integration_v2_to_v2(self): # GET fallback psbt payjoin_proposal = await self.process_receiver_proposal( - ReceiveSession.INITIALIZED(session), recv_persister, ohttp_relay + cast(ReceiveSession, ReceiveSession.INITIALIZED(session)), + recv_persister, + ohttp_relay, ) self.assertIsNotNone(payjoin_proposal) - self.assertEqual(payjoin_proposal.is_PAYJOIN_PROPOSAL(), True) + self.assertEqual( + isinstance(payjoin_proposal, ReceiveSession.PAYJOIN_PROPOSAL), True + ) - payjoin_proposal = payjoin_proposal.inner - request: RequestResponse = payjoin_proposal.create_post_request(ohttp_relay) + payjoin_proposal = cast( + ReceiveSession.PAYJOIN_PROPOSAL, payjoin_proposal + ).inner + request_recv: RequestResponse = payjoin_proposal.create_post_request( + ohttp_relay + ) response = await agent.post( - url=request.request.url, - headers={"Content-Type": request.request.content_type}, - content=request.request.body, + url=request_recv.request.url, + headers={"Content-Type": request_recv.request.content_type}, + content=request_recv.request.body, + ) + payjoin_proposal.process_response( + response.content, request_recv.client_response ) - payjoin_proposal.process_response(response.content, request.client_response) # ********************** # Inside the Sender: @@ -311,7 +349,7 @@ async def test_integration_v2_to_v2(self): payjoin_psbt = json.loads( self.sender.call( "walletprocesspsbt", - [outcome.inner.psbt_base64], + [cast(HasInner, outcome).inner.psbt_base64], ) )["psbt"] final_psbt = json.loads( @@ -411,10 +449,10 @@ def callback(self, tx): "testmempoolaccept", [json.dumps([bytes(tx).hex()])] ) )[0]["allowed"] - return res + return cast(bool, res) except Exception as e: print(f"An error occurred: {e}") - return None + return False class IsScriptOwnedCallback(IsScriptOwned): @@ -464,7 +502,7 @@ class CheckInputsNotSeenCallback(IsOutputKnown): def __init__(self, connection: RpcClient): self.connection = connection - def callback(self, _outpoint): + def callback(self, outpoint): return False diff --git a/payjoin-ffi/python/test/test_payjoin_unit_test.py b/payjoin-ffi/python/test/test_payjoin_unit_test.py index 235f1f6f4..44c0f32a0 100644 --- a/payjoin-ffi/python/test/test_payjoin_unit_test.py +++ b/payjoin-ffi/python/test/test_payjoin_unit_test.py @@ -1,4 +1,5 @@ import unittest +from typing import cast import payjoin from .utils import ( InMemoryReceiverPersister, @@ -268,7 +269,7 @@ async def run_test(): class TestValidation(unittest.TestCase): def test_receiver_builder_rejects_bad_address(self): - with self.assertRaises(payjoin.ReceiverBuilderError): + with self.assertRaises(cast(type[Exception], payjoin.ReceiverBuilderError)): payjoin.ReceiverBuilder( "not-an-address", "https://example.com", @@ -280,7 +281,7 @@ def test_receiver_builder_rejects_bad_address(self): ) def test_input_pair_rejects_invalid_outpoint(self): - with self.assertRaises(payjoin.InputPairError): + with self.assertRaises(cast(type[Exception], payjoin.InputPairError)): txin = payjoin.TxIn( previous_output=payjoin.OutPoint(txid="deadbeef", vout=0), script_sig=bytes(), @@ -296,7 +297,7 @@ def test_sender_builder_rejects_bad_psbt(self): uri = payjoin.Uri.parse( "bitcoin:tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4?pj=https://example.com/pj" ).check_pj_supported() - with self.assertRaises(payjoin.SenderInputError): + with self.assertRaises(cast(type[Exception], payjoin.SenderInputError)): payjoin.SenderBuilder("not-a-psbt", uri)