diff --git a/payjoin-ffi/csharp/CONTRIBUTING.md b/payjoin-ffi/csharp/CONTRIBUTING.md new file mode 100644 index 000000000..513ecc61e --- /dev/null +++ b/payjoin-ffi/csharp/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# Contributing to the Payjoin C# Bindings + +C# bindings for the [Payjoin Dev Kit](https://payjoindevkit.org/), generated from +`payjoin-ffi` with UniFFI. This document covers building the bindings from +source, running the tests, and producing the NuGet package. For using the +published package, see [`README.md`](./README.md). + +## Development + +With nix, the C# development shell provides the Rust toolchain and .NET 10 SDK: + +```shell +nix develop .#csharp -c bash payjoin-ffi/csharp/contrib/test.sh +``` + +Without nix, a Rust toolchain (MSRV: 1.85.0 for this repository) and the .NET 10 +SDK are required: + +```shell +git clone https://github.com/payjoin/rust-payjoin.git +cd rust-payjoin/payjoin-ffi/csharp + +bash ./scripts/generate_bindings.sh +dotnet build Payjoin.Tests.csproj +dotnet test Payjoin.Tests.csproj +``` + +### Windows + +```powershell +git clone https://github.com/payjoin/rust-payjoin.git +cd rust-payjoin/payjoin-ffi/csharp + +powershell -ExecutionPolicy Bypass -File .\scripts\generate_bindings.ps1 +dotnet build Payjoin.Tests.csproj +dotnet test Payjoin.Tests.csproj +``` + +Generation uses the Cargo-managed C# generator pinned in `payjoin-ffi/Cargo.toml`. +By default, development generation enables `_test-utils` to keep parity with the +test suite. For production bindings, set `PAYJOIN_FFI_FEATURES` to an empty value +(bash) or pass `-ProductionBindings` (PowerShell — Windows cannot represent an +empty environment variable, so the switch is the only reliable signal there). + +## Packaging + +Build the release native asset for the current host RID (production features are +the default when `PAYJOIN_FFI_FEATURES` is not set; this step does not regenerate +the C# bindings): + +```shell +bash ./scripts/build_nuget_native.sh +``` + +Any supported RID can also be cross-compiled from a Linux host, which is how CI +builds every native asset (`pip install -r scripts/cross-requirements.txt` for +the version- and hash-pinned toolchain CI uses, and `rustup target add` the +matching triple first): + +```shell +PAYJOIN_FFI_CROSS=1 PAYJOIN_FFI_RID=osx-arm64 bash ./scripts/build_nuget_native.sh +``` + +On Windows: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\build_nuget_native.ps1 +``` + +To pack, gather every supported RID under `artifacts/runtimes/{rid}/native/`, +generate production bindings, and run: + +```shell +PAYJOIN_FFI_FEATURES= PAYJOIN_FFI_PROFILE=release bash ./scripts/generate_bindings.sh +dotnet pack Payjoin.csproj --configuration Release --output artifacts/packages +``` + +On Windows: + +```powershell +$env:PAYJOIN_FFI_PROFILE = "release" +powershell -ExecutionPolicy Bypass -File .\scripts\generate_bindings.ps1 -ProductionBindings +dotnet pack Payjoin.csproj --configuration Release --output artifacts/packages +``` + +Validate the package in a clean sample app (`auto` derives the version from the +packed artifact; an explicit version is also accepted): + +```shell +bash ./scripts/smoke_nuget_package.sh artifacts/packages auto linux-x64 +``` + +CI performs the package build from release native assets and runs the smoke test +on each supported RID before publishing should be considered. The maintainer +release and publish workflow is documented in +[`RELEASING.md`](https://github.com/payjoin/rust-payjoin/blob/master/payjoin-ffi/csharp/RELEASING.md). diff --git a/payjoin-ffi/csharp/IntegrationTests.cs b/payjoin-ffi/csharp/IntegrationTests.cs index c009e7043..545a876ad 100644 --- a/payjoin-ffi/csharp/IntegrationTests.cs +++ b/payjoin-ffi/csharp/IntegrationTests.cs @@ -5,12 +5,36 @@ namespace Payjoin.Tests { + /// + /// End-to-end walkthrough of the BIP 77 (asynchronous payjoin) flow, and the + /// usage reference the package README points at. + /// drives a complete payjoin between two regtest wallets: the receiver opens a + /// session and produces a BIP 21 URI, the sender posts its original PSBT to an + /// untrusted directory, the receiver fetches that original PSBT, contributes an input, + /// and posts a payjoin proposal back, and the sender signs and broadcasts the final + /// transaction. Neither side talks to the other directly; every message travels + /// through the directory, encapsulated in OHTTP so the directory cannot observe + /// session content. Additionally all requests to the directory are routed through an + /// OHTTP relay to prevent the directory from learning the receiver and sender identities. + /// + /// The callback classes at the top are the seams where the library asks the + /// wallet questions it cannot answer itself (is this outpoint mine, is this + /// transaction broadcastable, sign these inputs). A production integration + /// implements the same interfaces against its own wallet backend. + /// public class IntegrationTests : IAsyncLifetime { private static string RpcCall(RpcClient rpc, string method, params string?[] args) => rpc.Call(method, args); private TestServices? _services; private HttpClient? _httpClient; + /// + /// Answers whether a transaction would be accepted by the mempool, via + /// Bitcoin Core's testmempoolaccept. The receiver runs this against the + /// sender's original transaction before doing anything else: that original + /// is the fallback that pays the receiver if the payjoin never completes, + /// so a receiver must not build on an original that could never confirm. + /// private sealed class MempoolAcceptanceCallback : CanBroadcast { private readonly RpcClient _connection; @@ -37,6 +61,14 @@ public bool Callback(byte[] tx) } } + /// + /// Answers "does this script belong to my wallet?" by decoding the script + /// to an address and asking Bitcoin Core getaddressinfo. The receiver uses + /// this to identify which outputs of the original pay the receiver, since + /// those are the outputs the payjoin may modify. Outputs are script-keyed + /// because an output's script in the transaction is the real payment + /// destination; input ownership uses the outpoint-keyed callback below. + /// private sealed class IsScriptOwnedCallback : IsScriptOwned { private readonly RpcClient _connection; @@ -111,6 +143,13 @@ public bool Callback(byte[] script) } } + /// + /// Answers "does my wallet own the UTXO at this outpoint?". Input ownership + /// is decided by outpoint, never by the PSBT's script data: a counterparty + /// can forge a script through a bare witness_utxo, but the outpoint is the + /// spend claim itself. This keeps the check aligned with how the wallet + /// recognizes and signs its own inputs. + /// private sealed class IsInputOwnedCallback : IsInputOwned { private readonly RpcClient _connection; @@ -150,11 +189,24 @@ public bool Callback(OutPoint outpoint) } } + /// + /// Answers "have I seen this input in an earlier session?". Automated + /// receivers track this to stop probing attacks, where a sender retries the + /// same input across sessions to map the receiver's UTXO set. The test + /// always answers no; a production receiver persists every outpoint it has + /// seen and answers from that store. + /// private sealed class CheckInputsNotSeenCallback : IsOutputKnown { public bool Callback(OutPoint _outpoint) => false; } + /// + /// Signs the receiver's contributed inputs in the finished proposal, via + /// Bitcoin Core's walletprocesspsbt. This is the last receiver-side step: + /// after it, the proposal goes back to the sender, who re-signs their own + /// inputs and broadcasts. + /// private sealed class ProcessPsbtCallback : ProcessPsbt { private readonly RpcClient _connection; @@ -173,6 +225,12 @@ public string Callback(string psbt) } } + /// + /// Collects the receiver wallet's spendable UTXOs as InputPair candidates + /// for contribution. An InputPair carries the transaction input plus the + /// PSBT metadata (here a witness_utxo) the sender needs to see the coin + /// being spent. + /// private static InputPair[] GetInputs(RpcClient rpc) { var utxosJson = RpcCall(rpc, "listunspent"); @@ -202,6 +260,21 @@ private static InputPair[] GetInputs(RpcClient rpc) return inputs.ToArray(); } + /// + /// One receiver poll of the directory mailbox, through the OHTTP relay. + /// The relay sees an encrypted blob and the client address; the directory + /// sees the blob only. Save persists the step to the session's event log, + /// and the outcome is either Stasis (nothing arrived yet, poll again + /// later) or Progress (the sender's original PSBT arrived), in which case + /// the receiver checklist below runs to completion. + /// + /// Each state transition here follows the same shape the whole API uses: + /// the state object creates a request, the application relays it with its + /// own HTTP client, the response feeds ProcessResponse, and Save moves the + /// session to the next persisted state. A crashed application replays the + /// event log with PayjoinMethods.ReplayReceiverEventLog and resumes from + /// the same state. + /// private async Task RetrieveReceiverProposal( Initialized receiver, RpcClient receiverRpc, @@ -237,6 +310,15 @@ private static InputPair[] GetInputs(RpcClient rpc) throw new InvalidOperationException("Unknown initialized transition outcome"); } + // The methods below are the receiver checklist from BIP 78, one typestate + // per check. The compiler enforces the order: each state exposes only its + // own check, and Save yields the next state. + + /// + /// Check 1: the sender's original transaction must be broadcastable. It is + /// the fallback payment if the payjoin does not complete, and its fee rate + /// also bounds what the merged transaction can look like. + /// private Task ProcessUncheckedProposal( UncheckedOriginalPayload proposal, RpcClient receiverRpc, @@ -248,6 +330,12 @@ private Task ProcessUncheckedProposal( return ProcessMaybeInputsOwned(maybeInputsOwned, receiverRpc, recvPersister); } + /// + /// Check 2: none of the original's inputs may belong to the receiver, + /// decided by outpoint against the wallet's own records. + /// Otherwise a malicious sender could get the receiver to sign away a coin + /// the receiver already owns. + /// private Task ProcessMaybeInputsOwned( MaybeInputsOwned proposal, RpcClient receiverRpc, @@ -259,6 +347,10 @@ private Task ProcessMaybeInputsOwned( return ProcessMaybeInputsSeen(maybeInputsSeen, receiverRpc, recvPersister); } + /// + /// Check 3: none of the original's inputs may have appeared in an earlier + /// session. Retried inputs are how a prober maps the receiver's wallet. + /// private Task ProcessMaybeInputsSeen( MaybeInputsSeen proposal, RpcClient receiverRpc, @@ -270,6 +362,12 @@ private Task ProcessMaybeInputsSeen( return ProcessOutputsUnknown(outputsUnknown, receiverRpc, recvPersister); } + /// + /// Check 4: find which outputs of the original pay the receiver. These are + /// the outputs the payjoin is allowed to substitute or amend; everything + /// else belongs to the sender and stays untouched. This check also ensures + /// that at least one output actually pays the receiver. + /// private Task ProcessOutputsUnknown( OutputsUnknown proposal, RpcClient receiverRpc, @@ -281,6 +379,11 @@ private Task ProcessOutputsUnknown( return ProcessWantsOutputs(wantsOutputs, receiverRpc, recvPersister); } + /// + /// The receiver may replace its output set here, for example to substitute + /// a fresh address or split the payment. This walkthrough keeps the + /// original outputs and just commits them. + /// private Task ProcessWantsOutputs( WantsOutputs proposal, RpcClient receiverRpc, @@ -292,6 +395,12 @@ private Task ProcessWantsOutputs( return ProcessWantsInputs(wantsInputs, receiverRpc, recvPersister); } + /// + /// The heart of the payjoin: the receiver contributes its own input. The + /// merged transaction now spends coins from both parties, which is what + /// breaks the common-input-ownership heuristic. The library selects from + /// the candidates to avoid unnecessary-input heuristics where it can. + /// private Task ProcessWantsInputs( WantsInputs proposal, RpcClient receiverRpc, @@ -304,6 +413,13 @@ private Task ProcessWantsInputs( return ProcessWantsFeeRange(wantsFeeRange, receiverRpc, recvPersister); } + /// + /// The receiver's added input makes the transaction larger, and someone + /// must pay for those bytes. ApplyFeeRange(min, max) bounds the fee rate + /// the receiver will accept; the library deducts the receiver's share from + /// the receiver's own output so the sender never pays for the receiver's + /// contribution. + /// private Task ProcessWantsFeeRange( WantsFeeRange proposal, RpcClient receiverRpc, @@ -315,6 +431,11 @@ private Task ProcessWantsFeeRange( return ProcessProvisionalProposal(provisional, receiverRpc, recvPersister); } + /// + /// Finalization: the wallet signs the receiver's contributed inputs (see + /// ProcessPsbtCallback), and the result is the proposal to post back to + /// the directory for the sender to pick up. + /// private Task ProcessProvisionalProposal( ProvisionalProposal proposal, RpcClient receiverRpc, @@ -342,6 +463,12 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } + /// + /// Session bootstrap: fetch the directory's OHTTP keys through an OHTTP + /// relay, so the directory never learns the client's IP address. This is + /// the first network step of every session; on mainnet the cert parameter + /// is null and the relay's real TLS certificate is used. + /// [Fact] public async Task FetchAndDecodeOhttpKeysViaRelayProxy() { @@ -477,6 +604,12 @@ public void TestFfiValidation() }); } + /// + /// The complete BIP 77 payjoin, receiver and sender in one place. In a real + /// deployment these are two applications that never talk directly; the + /// directory carries every message between them, and both sides survive a + /// restart because every state transition is saved to a persister first. + /// [Fact] public async Task TestIntegrationV2ToV2() { @@ -497,30 +630,54 @@ public async Task TestIntegrationV2ToV2() var ohttpKeys = _services.FetchOhttpKeys(); + // The persisters are the session event logs. Implement + // JsonReceiverSessionPersister and JsonSenderSessionPersister over your + // own storage (these in-memory versions append each event to a list); + // async variants exist for database-backed stores. var recvPersister = new InMemoryReceiverPersister(); var senderPersister = new InMemorySenderPersister(); + // ***************************** + // RECEIVER SIDE + // Open a session for the receiving address. Save persists the opening + // event and yields the Initialized state, whose PjUri() is the BIP 21 + // URI to show the sender. Any wallet can pay this URI: a payjoin-aware + // sender upgrades to a payjoin, any other wallet just sends to the + // address. using var receiverBuilder = new ReceiverBuilder(receiverAddress, directory, ohttpKeys); using var receiveTransition = receiverBuilder.Build(); using var session = receiveTransition.Save(recvPersister); + // First poll: the sender has not posted anything yet, so the outcome + // is Stasis and the helper returns null. A production receiver polls + // on a timer. var initial = await RetrieveReceiverProposal(session, receiver, recvPersister, ohttpRelay, cancellationToken); Assert.Null(initial); // ***************************** // SENDER SIDE - // Get PayJoin URI from receiver + // The sender starts from the receiver's BIP 21 URI. In production this + // arrives as a scanned QR code or a pasted string, and + // Payjoin.Uri.Parse(...).CheckPjSupported() turns it into the PjUri + // used below. using var pjUri = session.PjUri(); - // Create a funded PSBT that sweeps all funds to receiver + // The original PSBT is a normal transaction paying the URI's address, + // built and signed by the sender's wallet. It doubles as the fallback: + // if the payjoin never completes, this is the transaction that pays + // the receiver. var psbt = BuildSweepPsbt(sender, pjUri); - // Build sender request context + // BuildRecommended validates the original against the URI and sets the + // minimum fee rate (sat/kWU) the sender will accept for the merged + // transaction. Save persists the sender session the same way the + // receiver side persists its own. using var senderBuilder = new SenderBuilder(psbt, pjUri); using var senderTransition = senderBuilder.BuildRecommended(1000); using var reqCtx = senderTransition.Save(senderPersister); - // Create V2 POST request with OHTTP + // Post the original PSBT to the receiver's directory mailbox, OHTTP + // encapsulated like every other hop. using var request = reqCtx.CreateV2PostRequest(ohttpRelay); var response = await _httpClient!.PostAsync( request.Request.Url, @@ -538,12 +695,16 @@ public async Task TestIntegrationV2ToV2() // ********************* // RECEIVER SIDE - // Poll for the proposal + // This poll finds the sender's original in the mailbox and runs the + // whole receiver pipeline (checks 1 through 4, output commit, input + // contribution, fee range, signing; see the Process* methods above). + // The result is the receiver-signed payjoin proposal. using var payjoinProposal = await RetrieveReceiverProposal(session, receiver, recvPersister, ohttpRelay, cancellationToken); Assert.NotNull(payjoinProposal); Assert.IsType(payjoinProposal); - // Post the payjoin proposal back to the directory + // Post the proposal to the sender's mailbox and confirm the directory + // accepted it. using var proposalRequest = payjoinProposal!.CreatePostRequest(ohttpRelay); using var proposalResponse = await _httpClient.PostAsync( proposalRequest.Request.Url, @@ -558,7 +719,11 @@ public async Task TestIntegrationV2ToV2() // ******************************* // SENDER SIDE (FINALIZATION) - // Poll for the final payjoin PSBT + // Poll the sender mailbox until the receiver's proposal arrives + // (Stasis until then, like the receiver's poll). The library has + // already validated the proposal against the original during + // ProcessResponse; what comes out is the payjoin PSBT waiting for the + // sender's signatures. PollingForProposalTransitionOutcome? pollOutcome = null; var attempts = 0; while (true) @@ -591,7 +756,8 @@ public async Task TestIntegrationV2ToV2() var progressOutcome = (PollingForProposalTransitionOutcome.Progress)pollOutcome!; - // Sign the payjoin PSBT + // The sender re-signs its own inputs. The receiver's contributed input + // changed the transaction, so the original signatures no longer apply. var payjoinPsbt = progressOutcome.PsbtBase64; var processedPsbtJson = RpcCall(sender, "walletprocesspsbt", JsonSerializer.Serialize(payjoinPsbt)); using var processedDoc = JsonDocument.Parse(processedPsbtJson); @@ -610,6 +776,9 @@ public async Task TestIntegrationV2ToV2() // ******************************* // VERIFY RESULTS + // The broadcast transaction spends one input from each party into the + // receiver's output: on chain it is indistinguishable from an ordinary + // transaction, which is the point. // Decode PSBT to get network fees var decodedPsbtJson = RpcCall(sender, "decodepsbt", JsonSerializer.Serialize(finalPsbt)); using var decodedPsbtDoc = JsonDocument.Parse(decodedPsbtJson); diff --git a/payjoin-ffi/csharp/README.md b/payjoin-ffi/csharp/README.md index 6c6d0f98d..fcc807eae 100644 --- a/payjoin-ffi/csharp/README.md +++ b/payjoin-ffi/csharp/README.md @@ -1,11 +1,8 @@ # Payjoin C# Bindings -C# bindings for the [Payjoin Dev Kit](https://payjoindevkit.org/), generated from -`payjoin-ffi` with UniFFI. +Welcome to the C# language bindings for the [Payjoin Dev Kit](https://payjoindevkit.org/)! -The NuGet package is still prepared as a preview while the C# API stabilizes. The -first release-ready package layout targets .NET 10 and ships a managed -`Payjoin.dll` plus RID-specific native `payjoin_ffi` libraries. +Payjoin lets the receiver of a Bitcoin transfer contribute inputs to the sender's transaction. The result looks like any other transaction, which preserves privacy by poisoning the common-input-ownership heuristic that chain surveillance depends on, and it lets the receiver batch its own operations into the same transaction. These bindings implement both [BIP 78](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki) (synchronous payjoin) and [BIP 77](https://github.com/bitcoin/bips/blob/master/bip-0077.md) (asynchronous payjoin, where sender and receiver exchange the transaction through an untrusted directory and never need to be online at the same time), and ship with native libraries for every supported platform, so no Rust toolchain is required. ## Install @@ -13,123 +10,39 @@ first release-ready package layout targets .NET 10 and ships a managed dotnet add package Payjoin --prerelease ``` -## Requirements +Requires .NET 10.0 or later, on one of: -- .NET 10.0 or higher -- A supported RID native asset in the package +| OS | RIDs | +| ------- | -------------------------- | +| Linux | `linux-x64`, `linux-arm64` | +| macOS | `osx-arm64`, `osx-x64` | +| Windows | `win-x64`, `win-arm64` | -The first preview release matrix is: +## End to end example -| OS | RID | Native library | -| ------------------- | ------------- | ---------------------- | -| Linux arm64 | `linux-arm64` | `libpayjoin_ffi.so` | -| Linux x64 | `linux-x64` | `libpayjoin_ffi.so` | -| macOS Apple Silicon | `osx-arm64` | `libpayjoin_ffi.dylib` | -| macOS x64 | `osx-x64` | `libpayjoin_ffi.dylib` | -| Windows arm64 | `win-arm64` | `payjoin_ffi.dll` | -| Windows x64 | `win-x64` | `payjoin_ffi.dll` | +The usage reference is the commented walkthrough in [`IntegrationTests.cs`](https://github.com/payjoin/rust-payjoin/blob/master/payjoin-ffi/csharp/IntegrationTests.cs): `TestIntegrationV2ToV2` drives a complete payjoin from both sides, executed by CI on every change so it cannot go stale, and narrates each protocol step, from opening and persisting the session through the receiver checklist to signing the proposal. -The package follows the .NET native asset layout: +## Receive a payjoin -- `ref/net10.0/Payjoin.dll` -- `runtimes/any/lib/net10.0/Payjoin.dll` -- `runtimes/{rid}/native/{native-library}` +A receiver session produces a BIP 21 URI to show the sender. It works with every wallet: a payjoin-aware sender upgrades to a payjoin, any other wallet simply sends to the address as usual. -## Minimal Usage +A session starts with the directory's OHTTP keys, fetched through an OHTTP relay so the directory never learns your IP address, then builds a receiver for your address and persists every step to an event log so your app can crash or restart and resume where it left off. From there the session advances through a typestate flow: each state hands you a request to relay with your own HTTP client, and the response moves you to the next state, through checking the sender's original transaction, contributing inputs, and posting the proposal. -```csharp -var uri = Payjoin.Url.Parse( - "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"); +## Send a payjoin -Console.WriteLine(uri.AsString()); -``` - -## Development - -With nix, the C# development shell provides the Rust toolchain and .NET 10 SDK: +A sender session starts from a BIP 21 URI scanned from the receiver (`Payjoin.Uri.Parse(...).CheckPjSupported()`), which is used to create and sign the original PSBT, posted to the receiver's mailbox on the directory. The sender then polls for the receiver's payjoin proposal through the same request/response flow, and once it arrives, signs it and broadcasts it to the network. The sender half of the same walkthrough shows every step. -```shell -nix develop .#csharp -c bash payjoin-ffi/csharp/contrib/test.sh -``` +## Resume after a restart -Without nix, a Rust toolchain (MSRV: 1.85.0 for this repository) and the .NET 10 -SDK are required: - -```shell -git clone https://github.com/payjoin/rust-payjoin.git -cd rust-payjoin/payjoin-ffi/csharp - -bash ./scripts/generate_bindings.sh -dotnet build Payjoin.Tests.csproj -dotnet test Payjoin.Tests.csproj -``` +Sessions persist each step to an event log through a persister you implement over your own storage; replaying the log with `PayjoinMethods.ReplayReceiverEventLog` recovers the current state after a crash or restart. Every `Save` has a `SaveAsync` counterpart, with async persister interfaces for database-backed storage. -### Windows +## Preview status -```powershell -git clone https://github.com/payjoin/rust-payjoin.git -cd rust-payjoin/payjoin-ffi/csharp - -powershell -ExecutionPolicy Bypass -File .\scripts\generate_bindings.ps1 -dotnet build Payjoin.Tests.csproj -dotnet test Payjoin.Tests.csproj -``` - -Generation uses the Cargo-managed C# generator pinned in `payjoin-ffi/Cargo.toml`. -By default, development generation enables `_test-utils` to keep parity with the -test suite. For production bindings, set `PAYJOIN_FFI_FEATURES` to an empty value -(bash) or pass `-ProductionBindings` (PowerShell — Windows cannot represent an -empty environment variable, so the switch is the only reliable signal there). - -## Packaging - -Build the release native asset for the current host RID (production features are -the default when `PAYJOIN_FFI_FEATURES` is not set; this step does not regenerate -the C# bindings): - -```shell -bash ./scripts/build_nuget_native.sh -``` +The package is in preview while the C# API stabilizes alongside the Rust core's 1.0 release candidates. Expect breaking changes between previews; the package version tracks the underlying `payjoin-ffi` crate. -Any supported RID can also be cross-compiled from a Linux host, which is how CI -builds every native asset (`pip install -r scripts/cross-requirements.txt` for -the version- and hash-pinned toolchain CI uses, and `rustup target add` the -matching triple first): +## Documentation and help -```shell -PAYJOIN_FFI_CROSS=1 PAYJOIN_FFI_RID=osx-arm64 bash ./scripts/build_nuget_native.sh -``` - -On Windows: - -```powershell -powershell -ExecutionPolicy Bypass -File .\scripts\build_nuget_native.ps1 -``` - -To pack, gather every supported RID under `artifacts/runtimes/{rid}/native/`, -generate production bindings, and run: - -```shell -PAYJOIN_FFI_FEATURES= PAYJOIN_FFI_PROFILE=release bash ./scripts/generate_bindings.sh -dotnet pack Payjoin.csproj --configuration Release --output artifacts/packages -``` - -On Windows: - -```powershell -$env:PAYJOIN_FFI_PROFILE = "release" -powershell -ExecutionPolicy Bypass -File .\scripts\generate_bindings.ps1 -ProductionBindings -dotnet pack Payjoin.csproj --configuration Release --output artifacts/packages -``` - -Validate the package in a clean sample app (`auto` derives the version from the -packed artifact; an explicit version is also accepted): - -```shell -bash ./scripts/smoke_nuget_package.sh artifacts/packages auto linux-x64 -``` +- [Payjoin Dev Kit](https://payjoindevkit.org/) for protocol background and guides +- [rust-payjoin](https://github.com/payjoin/rust-payjoin) is the Rust core these bindings are generated from, with the [issue tracker](https://github.com/payjoin/rust-payjoin/issues) for bugs and feature requests -CI performs the package build from release native assets and runs the smoke test -on each supported RID before publishing should be considered. The maintainer -release and publish workflow is documented in -[`RELEASING.md`](https://github.com/payjoin/rust-payjoin/blob/master/payjoin-ffi/csharp/RELEASING.md). +To build the bindings from source, run the tests, or produce the NuGet package locally, see [`CONTRIBUTING.md`](https://github.com/payjoin/rust-payjoin/blob/master/payjoin-ffi/csharp/CONTRIBUTING.md).