From 08a80c480748e0ada498804ecc380c4be78780e3 Mon Sep 17 00:00:00 2001 From: Valera <50830352+ValeraFinebits@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:28:44 +0200 Subject: [PATCH 1/3] Add test: FetchOhttpKeys in non-Tokio context --- payjoin-ffi/csharp/IntegrationTests.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/payjoin-ffi/csharp/IntegrationTests.cs b/payjoin-ffi/csharp/IntegrationTests.cs index d17742a79..ccc67d6a5 100644 --- a/payjoin-ffi/csharp/IntegrationTests.cs +++ b/payjoin-ffi/csharp/IntegrationTests.cs @@ -323,6 +323,31 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } + /// + /// Regression test: called from a plain .NET async + /// context should successfully return OHTTP keys. + /// + /// Without the fix this test fails with: + /// PanicException: "there is no reactor running, must be called from the context of a Tokio 1.x runtime" + /// + [Fact] + public async Task FetchOhttpKeys_ShouldWorkFromNonTokioContext() + { + // Arrange: use TestServices' URLs so connectivity is guaranteed and + // the failure (if any) is purely about missing Tokio runtime, not networking. + _services!.WaitForServicesReady(); + var ohttpRelay = _services.OhttpRelayUrl(); + var directory = _services.DirectoryUrl(); + + // Act: call the raw UniFFI async binding directly — NOT TestServices.FetchOhttpKeys(), + // which uses an internal block_on(RUNTIME) and therefore always has a Tokio context. + // PayjoinMethods.FetchOhttpKeys() has no such safety net. + var keys = await PayjoinMethods.FetchOhttpKeys(ohttpRelay, directory); + + // Assert + Assert.NotNull(keys); + } + [Fact] public void TestFfiValidation() { From d9cbd21a579ba6bb84f8cdd4a7eddbd96dea5b2f Mon Sep 17 00:00:00 2001 From: chavic Date: Sun, 22 Feb 2026 14:42:34 +0200 Subject: [PATCH 2/3] Enable Tokio runtime for UniFFI IO futures FetchOhttpKeys is exported as a UniFFI async function and is polled from plain .NET async threads in the C# binding. Those threads do not carry a Tokio runtime, so reqwest panics with "there is no reactor running" when the future is polled. Enable UniFFI's tokio feature and mark fetch_ohttp_keys with #[uniffi::export(async_runtime = "tokio")] so UniFFI polls it inside a Tokio-compatible runtime context. This fixes the runtime panic path and allows errors to be surfaced as regular IO failures instead of a PanicException. --- payjoin-ffi/Cargo.toml | 2 +- payjoin-ffi/src/io.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/payjoin-ffi/Cargo.toml b/payjoin-ffi/Cargo.toml index c55bdd187..f186ec753 100644 --- a/payjoin-ffi/Cargo.toml +++ b/payjoin-ffi/Cargo.toml @@ -33,7 +33,7 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" thiserror = "2.0.14" tokio = { version = "1.47.1", features = ["full"], optional = true } -uniffi = { version = "0.30.0", features = ["cli"] } +uniffi = { version = "0.30.0", features = ["cli", "tokio"] } uniffi-bindgen-cs = { git = "https://github.com/chavic/uniffi-bindgen-cs.git", rev = "878a3d269eacce64beadcd336ade0b7c8da09824", optional = true } uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart.git", rev = "f830323", optional = true } url = "2.5.4" diff --git a/payjoin-ffi/src/io.rs b/payjoin-ffi/src/io.rs index 34a1f6d5c..6d2060056 100644 --- a/payjoin-ffi/src/io.rs +++ b/payjoin-ffi/src/io.rs @@ -21,7 +21,7 @@ pub mod error { /// /// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This /// directory stores and forwards payjoin client payloads. -#[uniffi::export] +#[uniffi::export(async_runtime = "tokio")] pub async fn fetch_ohttp_keys( ohttp_relay: &str, payjoin_directory: &str, From 6c5441faa5cc22ddc5caeb640bcfc48d8e3f5408 Mon Sep 17 00:00:00 2001 From: chavic Date: Sun, 22 Feb 2026 15:10:57 +0200 Subject: [PATCH 3/3] Use cert-aware OHTTP fetch in C# test After enabling Tokio runtime polling for UniFFI async IO, the C# non-Tokio regression moved past PanicException and began failing with an IoException against local HTTPS test services. That failure was a TLS trust issue: TestServices uses a generated self-signed certificate, while FetchOhttpKeys uses default trust. The regression should validate async runtime behavior, not certificate store configuration. Switch the regression to FetchOhttpKeysWithCert and pass TestServices certificate bytes. Also export fetch_ohttp_keys_with_cert with Tokio runtime polling and include _manual-tls in default C# binding generation features so local and CI builds stay consistent. --- payjoin-ffi/csharp/IntegrationTests.cs | 11 ++++++----- payjoin-ffi/csharp/README.md | 6 +++--- payjoin-ffi/csharp/scripts/generate_bindings.ps1 | 5 +++-- payjoin-ffi/csharp/scripts/generate_bindings.sh | 5 +++-- payjoin-ffi/src/io.rs | 1 + 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/payjoin-ffi/csharp/IntegrationTests.cs b/payjoin-ffi/csharp/IntegrationTests.cs index ccc67d6a5..cc5150690 100644 --- a/payjoin-ffi/csharp/IntegrationTests.cs +++ b/payjoin-ffi/csharp/IntegrationTests.cs @@ -324,7 +324,7 @@ public ValueTask DisposeAsync() } /// - /// Regression test: called from a plain .NET async + /// Regression test: called from a plain .NET async /// context should successfully return OHTTP keys. /// /// Without the fix this test fails with: @@ -333,16 +333,17 @@ public ValueTask DisposeAsync() [Fact] public async Task FetchOhttpKeys_ShouldWorkFromNonTokioContext() { - // Arrange: use TestServices' URLs so connectivity is guaranteed and - // the failure (if any) is purely about missing Tokio runtime, not networking. + // Arrange: use TestServices' URLs and certificate so connectivity is guaranteed and + // the failure (if any) is purely about missing Tokio runtime, not TLS trust setup. _services!.WaitForServicesReady(); var ohttpRelay = _services.OhttpRelayUrl(); var directory = _services.DirectoryUrl(); + var cert = _services.Cert(); // Act: call the raw UniFFI async binding directly — NOT TestServices.FetchOhttpKeys(), // which uses an internal block_on(RUNTIME) and therefore always has a Tokio context. - // PayjoinMethods.FetchOhttpKeys() has no such safety net. - var keys = await PayjoinMethods.FetchOhttpKeys(ohttpRelay, directory); + // PayjoinMethods.FetchOhttpKeysWithCert() has no such safety net. + var keys = await PayjoinMethods.FetchOhttpKeysWithCert(ohttpRelay, directory, cert); // Assert Assert.NotNull(keys); diff --git a/payjoin-ffi/csharp/README.md b/payjoin-ffi/csharp/README.md index 9afccc5c3..a24fbb074 100644 --- a/payjoin-ffi/csharp/README.md +++ b/payjoin-ffi/csharp/README.md @@ -56,12 +56,12 @@ dotnet test Generation uses the Cargo-managed C# generator from `payjoin-ffi/Cargo.toml`. -By default, generation builds `payjoin-ffi` with `_test-utils` enabled to keep parity with other language test scripts. Override via `PAYJOIN_FFI_FEATURES`. +By default, generation builds `payjoin-ffi` with `_test-utils,_manual-tls` so C# integration tests can use local HTTPS services with generated self-signed certificates. Override via `PAYJOIN_FFI_FEATURES`. ### Unix shells ```shell -export PAYJOIN_FFI_FEATURES=_test-utils # default behavior +export PAYJOIN_FFI_FEATURES=_test-utils,_manual-tls # default behavior # export PAYJOIN_FFI_FEATURES="" # build without extra features bash ./scripts/generate_bindings.sh ``` @@ -69,7 +69,7 @@ bash ./scripts/generate_bindings.sh ### PowerShell ```powershell -$env:PAYJOIN_FFI_FEATURES = "_test-utils" # default behavior +$env:PAYJOIN_FFI_FEATURES = "_test-utils,_manual-tls" # default behavior # $env:PAYJOIN_FFI_FEATURES = "" # build without extra features powershell -ExecutionPolicy Bypass -File .\scripts\generate_bindings.ps1 dotnet build diff --git a/payjoin-ffi/csharp/scripts/generate_bindings.ps1 b/payjoin-ffi/csharp/scripts/generate_bindings.ps1 index 52e782508..ff195ef87 100644 --- a/payjoin-ffi/csharp/scripts/generate_bindings.ps1 +++ b/payjoin-ffi/csharp/scripts/generate_bindings.ps1 @@ -40,8 +40,9 @@ Write-Host "Generating payjoin C#..." if ($null -ne $env:PAYJOIN_FFI_FEATURES) { $payjoinFfiFeatures = $env:PAYJOIN_FFI_FEATURES } else { - # Keep parity with other language test scripts: include _test-utils by default. - $payjoinFfiFeatures = "_test-utils" + # Include test utilities and manual TLS by default so local test services + # can fetch OHTTP keys over HTTPS with their generated self-signed cert. + $payjoinFfiFeatures = "_test-utils,_manual-tls" } if ($payjoinFfiFeatures) { diff --git a/payjoin-ffi/csharp/scripts/generate_bindings.sh b/payjoin-ffi/csharp/scripts/generate_bindings.sh index 630ab2146..5de84d415 100755 --- a/payjoin-ffi/csharp/scripts/generate_bindings.sh +++ b/payjoin-ffi/csharp/scripts/generate_bindings.sh @@ -22,8 +22,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR/../.." echo "Generating payjoin C#..." -# Keep parity with other language test scripts: include _test-utils by default. -PAYJOIN_FFI_FEATURES=${PAYJOIN_FFI_FEATURES:-_test-utils} +# Include test utilities and manual TLS by default so local test services +# can fetch OHTTP keys over HTTPS with their generated self-signed cert. +PAYJOIN_FFI_FEATURES=${PAYJOIN_FFI_FEATURES:-_test-utils,_manual-tls} GENERATOR_FEATURES="csharp" if [[ -n $PAYJOIN_FFI_FEATURES ]]; then GENERATOR_FEATURES="$GENERATOR_FEATURES,$PAYJOIN_FFI_FEATURES" diff --git a/payjoin-ffi/src/io.rs b/payjoin-ffi/src/io.rs index 6d2060056..6069c4e75 100644 --- a/payjoin-ffi/src/io.rs +++ b/payjoin-ffi/src/io.rs @@ -43,6 +43,7 @@ pub async fn fetch_ohttp_keys( /// /// * `cert_der`: The DER-encoded certificate to use for local HTTPS connections. #[cfg(feature = "_manual-tls")] +#[uniffi::export(async_runtime = "tokio")] pub async fn fetch_ohttp_keys_with_cert( ohttp_relay: &str, payjoin_directory: &str,