From 1ebe14c2019391f77b8ac6e8dc98351e6a77c1a6 Mon Sep 17 00:00:00 2001 From: spacebear Date: Thu, 12 Jun 2025 16:54:49 -0400 Subject: [PATCH 1/4] Initialize payjoin_dart Specify the language to create bindings for at the creation step Co-authored-by: user --- payjoin-ffi/Cargo.toml | 4 +++- payjoin-ffi/build.rs | 2 ++ payjoin-ffi/dart/.gitignore | 10 ++++++++++ payjoin-ffi/dart/pubspec.yaml | 9 +++++++++ payjoin-ffi/uniffi-bindgen.rs | 34 +++++++++++++++++++++++++++++++++- 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 payjoin-ffi/dart/.gitignore create mode 100644 payjoin-ffi/dart/pubspec.yaml diff --git a/payjoin-ffi/Cargo.toml b/payjoin-ffi/Cargo.toml index 480338eed..986c3e18b 100644 --- a/payjoin-ffi/Cargo.toml +++ b/payjoin-ffi/Cargo.toml @@ -8,7 +8,7 @@ exclude = ["tests"] [features] _test-utils = ["payjoin-test-utils", "tokio", "bitcoind"] _danger-local-https = ["payjoin/_danger-local-https"] -uniffi = ["uniffi/cli", "bitcoin-ffi/default"] +uniffi = ["uniffi/cli", "bitcoin-ffi/default", "uniffi-dart"] [lib] name = "payjoin_ffi" @@ -20,6 +20,7 @@ path = "uniffi-bindgen.rs" [build-dependencies] uniffi = { version = "0.29.1", features = ["build"] } +uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart.git", rev = "b6186bc", features = ["build"] } [dependencies] base64 = "0.22.1" @@ -35,6 +36,7 @@ serde_json = "1.0.128" thiserror = "1.0.58" tokio = { version = "1.38.0", features = ["full"], optional = true } uniffi = { version = "0.29.1", optional = true } +uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart.git", rev = "b6186bc", optional = true} url = "2.5.0" [dev-dependencies] diff --git a/payjoin-ffi/build.rs b/payjoin-ffi/build.rs index c1e2e4f46..c22026e9d 100644 --- a/payjoin-ffi/build.rs +++ b/payjoin-ffi/build.rs @@ -1,4 +1,6 @@ fn main() { #[cfg(feature = "uniffi")] uniffi::generate_scaffolding("src/payjoin_ffi.udl").unwrap(); + #[cfg(feature = "uniffi")] + uniffi_dart::generate_scaffolding("src/payjoin_ffi.udl".into()).unwrap(); } diff --git a/payjoin-ffi/dart/.gitignore b/payjoin-ffi/dart/.gitignore new file mode 100644 index 000000000..bb24da02f --- /dev/null +++ b/payjoin-ffi/dart/.gitignore @@ -0,0 +1,10 @@ +.dart_tool/ +build/ +pubspec.lock +doc/api/ + +.DS_Store + +# Auto-generated bindings +lib/payjoin_ffi.dart +lib/bitcoin.dart diff --git a/payjoin-ffi/dart/pubspec.yaml b/payjoin-ffi/dart/pubspec.yaml new file mode 100644 index 000000000..af274293f --- /dev/null +++ b/payjoin-ffi/dart/pubspec.yaml @@ -0,0 +1,9 @@ +name: payjoin_dart +description: Dart bindings for payjoin +version: 0.24.0 + +environment: + sdk: '^3.2.0' + +dependencies: + ffi: ^2.1.4 diff --git a/payjoin-ffi/uniffi-bindgen.rs b/payjoin-ffi/uniffi-bindgen.rs index 65dd9db75..6a97cd19b 100644 --- a/payjoin-ffi/uniffi-bindgen.rs +++ b/payjoin-ffi/uniffi-bindgen.rs @@ -1,4 +1,36 @@ fn main() { #[cfg(feature = "uniffi")] - uniffi::uniffi_bindgen_main() + uniffi_bindgen() +} + +#[cfg(feature = "uniffi")] +fn uniffi_bindgen() { + // uniffi_bindgen_main parses command line arguments for officially supported languages, + // but we need to parse them manually first to decide whether to use the uniffi_dart plugin. + let args: Vec = std::env::args().collect(); + let language = + args.iter().position(|arg| arg == "--language").and_then(|idx| args.get(idx + 1)); + let library_path = args + .iter() + .position(|arg| arg == "--library") + .and_then(|idx| args.get(idx + 1)) + .expect("specify the library path with --library"); + let output_dir = args + .iter() + .position(|arg| arg == "--out-dir") + .and_then(|idx| args.get(idx + 1)) + .expect("--out-dir is required when using --library"); + match language { + Some(lang) if lang == "dart" => { + uniffi_dart::gen::generate_dart_bindings( + "src/payjoin_ffi.udl".into(), + None, + Some(output_dir.as_str().into()), + library_path.as_str().into(), + true, + ) + .expect("Failed to generate dart bindings"); + } + _ => uniffi::uniffi_bindgen_main(), + } } From 0bd8b1a0ad08eaa67287d0c30f750e6eec546249 Mon Sep 17 00:00:00 2001 From: user Date: Mon, 23 Jun 2025 10:50:27 -0400 Subject: [PATCH 2/4] Add dart unit tests --- payjoin-ffi/dart/pubspec.yaml | 2 + .../dart/test/test_payjoin_unit_test.dart | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 payjoin-ffi/dart/test/test_payjoin_unit_test.dart diff --git a/payjoin-ffi/dart/pubspec.yaml b/payjoin-ffi/dart/pubspec.yaml index af274293f..67a445c44 100644 --- a/payjoin-ffi/dart/pubspec.yaml +++ b/payjoin-ffi/dart/pubspec.yaml @@ -7,3 +7,5 @@ environment: dependencies: ffi: ^2.1.4 +dev_dependencies: + test: ^1.26.2 diff --git a/payjoin-ffi/dart/test/test_payjoin_unit_test.dart b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart new file mode 100644 index 000000000..ed3fa5670 --- /dev/null +++ b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart @@ -0,0 +1,143 @@ +import 'package:test/test.dart'; +import "package:payjoin_dart/payjoin_ffi.dart" as payjoin; +import "package:payjoin_dart/bitcoin.dart" as bitcoin; + +class InMemoryReceiverPersister + implements payjoin.JsonReceiverSessionPersister { + final String id; + final List events = []; + bool closed = false; + + InMemoryReceiverPersister(this.id); + + @override + void save(String event) { + events.add(event); + } + + @override + List load() { + return events; + } + + @override + void close() { + closed = true; + } +} + +class InMemorySenderPersister implements payjoin.JsonSenderSessionPersister { + final String id; + final List events = []; + bool closed = false; + + InMemorySenderPersister(this.id); + + @override + void save(String event) { + events.add(event); + } + + @override + List load() { + return events; + } + + @override + void close() { + closed = true; + } +} + +void main() { + group('Test URIs', () { + test('Test todo url encoded', () { + var uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; + final result = payjoin.Url.parse(uri); + expect(result, isA(), + reason: "pj url should be url encoded"); + }); + + test('Test valid url', () { + var uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=1&pj=https://example.com?ciao"; + final result = payjoin.Url.parse(uri); + expect(result, isA(), reason: "pj is not a valid url"); + }); + + test('Test missing amount', () { + var uri = + "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?pj=https://testnet.demo.btcpayserver.org/BTC/pj"; + final result = payjoin.Url.parse(uri); + expect(result, isA(), reason: "missing amount should be ok"); + }); + + test('Test valid uris', () { + final https = payjoin.exampleUrl(); + final onion = + "http://vjdpwgybvubne5hda6v4c5iaeeevhge6jvo3w2cl6eocbwwvwxp7b7qd.onion"; + + final base58 = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX"; + final bech32Upper = "BITCOIN:TB1Q6D3A2W975YNY0ASUVD9A67NER4NKS58FF0Q8G4"; + final bech32Lower = "bitcoin:tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; + + final addresses = [base58, bech32Upper, bech32Lower]; + final pjs = [https, onion]; + + for (final address in addresses) { + for (final pj in pjs) { + final uri = "$address?amount=1&pj=$pj"; + try { + payjoin.Url.parse(uri); + } catch (e) { + fail("Failed to create a valid Uri for $uri. Error: $e"); + } + } + } + }); + }); + + group("Test Persistence", () { + test("Test receiver persistence", () { + var persister = InMemoryReceiverPersister("1"); + var address = bitcoin.Address( + "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4", bitcoin.Network.signet); + payjoin.UninitializedReceiver() + .createSession( + address, + "https://example.com", + payjoin.OhttpKeys.fromString( + "OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"), + null) + .save(persister); + final result = payjoin.replayReceiverEventLog(persister); + expect(result, isA(), + reason: "persistence should return a replay result"); + }); + + test("Test sender persistence", () { + var receiver_persister = InMemoryReceiverPersister("1"); + var address = bitcoin.Address( + "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK", bitcoin.Network.testnet); + var receiver = payjoin.UninitializedReceiver() + .createSession( + address, + "https://example.com", + payjoin.OhttpKeys.fromString( + "OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"), + null) + .save(receiver_persister); + var uri = receiver.pjUri(); + + var sender_persister = InMemorySenderPersister("1"); + var psbt = + "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; + final result = payjoin.SenderBuilder(psbt, uri) + .buildRecommended(1000) + .save(sender_persister); + expect(result, isA(), + reason: "persistence should return a reply key"); + }); + }); +} From dbd3ee347f4f6946e7fa7692392a95c21c99da48 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 24 Jun 2025 18:21:14 -0400 Subject: [PATCH 3/4] Add dart bindings generation scripts --- payjoin-ffi/dart/.gitignore | 5 ++++ payjoin-ffi/dart/scripts/bindgen_generate.sh | 11 ++++++++ payjoin-ffi/dart/scripts/generate_linux.sh | 20 ++++++++++++++ payjoin-ffi/dart/scripts/generate_macos.sh | 29 ++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100755 payjoin-ffi/dart/scripts/bindgen_generate.sh create mode 100755 payjoin-ffi/dart/scripts/generate_linux.sh create mode 100755 payjoin-ffi/dart/scripts/generate_macos.sh diff --git a/payjoin-ffi/dart/.gitignore b/payjoin-ffi/dart/.gitignore index bb24da02f..f1e3afbc1 100644 --- a/payjoin-ffi/dart/.gitignore +++ b/payjoin-ffi/dart/.gitignore @@ -5,6 +5,11 @@ doc/api/ .DS_Store +# Auto-generated shared libraries +*.dylib +*.so +*.dll + # Auto-generated bindings lib/payjoin_ffi.dart lib/bitcoin.dart diff --git a/payjoin-ffi/dart/scripts/bindgen_generate.sh b/payjoin-ffi/dart/scripts/bindgen_generate.sh new file mode 100755 index 000000000..0c8c9b8a1 --- /dev/null +++ b/payjoin-ffi/dart/scripts/bindgen_generate.sh @@ -0,0 +1,11 @@ + + +#!/bin/bash +chmod +x ./scripts/generate_linux.sh +chmod +x ./scripts/generate_macos.sh + + +# Run each script +scripts/generate_linux.sh +scripts/generate_macos.sh + diff --git a/payjoin-ffi/dart/scripts/generate_linux.sh b/payjoin-ffi/dart/scripts/generate_linux.sh new file mode 100755 index 000000000..e42bba9c4 --- /dev/null +++ b/payjoin-ffi/dart/scripts/generate_linux.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +LIBNAME=libpayjoin_ffi.so +LINUX_TARGET=x86_64-unknown-linux-gnu + +echo "Generating payjoin_ffi.dart..." +cd ../ +# This is a test script the actual release should not include the test utils feature +cargo build --profile release --features uniffi,_test-utils +cargo run --profile release --features uniffi,_test-utils --bin uniffi-bindgen -- --library target/release/$LIBNAME --language dart --out-dir dart/lib/ + +echo "Generating native binaries..." +rustup target add $LINUX_TARGET +# This is a test script the actual release should not include the test utils feature +cargo build --profile release-smaller --target $LINUX_TARGET --features uniffi,_test-utils + +echo "Copying linux payjoin_ffi.so" +cp target/$LINUX_TARGET/release-smaller/$LIBNAME dart/$LIBNAME + +echo "All done!" diff --git a/payjoin-ffi/dart/scripts/generate_macos.sh b/payjoin-ffi/dart/scripts/generate_macos.sh new file mode 100755 index 000000000..b82ad9403 --- /dev/null +++ b/payjoin-ffi/dart/scripts/generate_macos.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -euo pipefail +LIBNAME=libpayjoin_ffi.dylib + +echo "Generating payjoin_ffi.dart..." +cd ../ +# This is a test script the actual release should not include the test utils feature +cargo build --features uniffi,_test-utils --profile release +cargo run --features uniffi,_test-utils --profile release --bin uniffi-bindgen -- --library target/release/$LIBNAME --language dart --out-dir dart/lib/ + +echo "Generating native binaries..." +rustup target add aarch64-apple-darwin x86_64-apple-darwin + +# This is a test script the actual release should not include the test utils feature +cargo build --profile release-smaller --target aarch64-apple-darwin --features uniffi,_test-utils +echo "Done building aarch64-apple-darwin" + +# This is a test script the actual release should not include the test utils feature +cargo build --profile release-smaller --target x86_64-apple-darwin --features uniffi,_test-utils +echo "Done building x86_64-apple-darwin" + +echo "Building macos fat library" + +lipo -create -output dart/$LIBNAME \ + target/aarch64-apple-darwin/release-smaller/$LIBNAME \ + target/x86_64-apple-darwin/release-smaller/$LIBNAME + +echo "All done!" From 20fd94fdd9f05acdd8292a4d010a73f3d9a6b982 Mon Sep 17 00:00:00 2001 From: Ben Allen <108441023+benalleng@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:15:47 -0400 Subject: [PATCH 4/4] Add dart integration test Co-authored-by: spacebear --- payjoin-ffi/dart/pubspec.yaml | 1 + .../test/test_payjoin_integration_test.dart | 407 ++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 payjoin-ffi/dart/test/test_payjoin_integration_test.dart diff --git a/payjoin-ffi/dart/pubspec.yaml b/payjoin-ffi/dart/pubspec.yaml index 67a445c44..a8ba208fa 100644 --- a/payjoin-ffi/dart/pubspec.yaml +++ b/payjoin-ffi/dart/pubspec.yaml @@ -9,3 +9,4 @@ dependencies: ffi: ^2.1.4 dev_dependencies: test: ^1.26.2 + http: ^1.4.0 \ No newline at end of file diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart new file mode 100644 index 000000000..b984053b7 --- /dev/null +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -0,0 +1,407 @@ +import "dart:convert"; +import "dart:typed_data"; + +import "package:http/http.dart" as http; +import 'package:test/test.dart'; +import "package:convert/convert.dart"; + +import "../lib/payjoin_ffi.dart" as payjoin; +import "../lib/bitcoin.dart" as bitcoin; + +late payjoin.BitcoindEnv env; +late payjoin.BitcoindInstance bitcoind; +late payjoin.RpcClient receiver; +late payjoin.RpcClient sender; + +class InMemoryReceiverPersister + implements payjoin.JsonReceiverSessionPersister { + final String id; + final List events = []; + bool closed = false; + + InMemoryReceiverPersister(this.id); + + @override + void save(String event) { + events.add(event); + } + + @override + List load() { + return events; + } + + @override + void close() { + closed = true; + } +} + +class InMemorySenderPersister implements payjoin.JsonSenderSessionPersister { + final String id; + final List events = []; + bool closed = false; + + InMemorySenderPersister(this.id); + + @override + void save(String event) { + events.add(event); + } + + @override + List load() { + return events; + } + + @override + void close() { + closed = true; + } +} + +class MempoolAcceptanceCallback implements payjoin.CanBroadcast { + final payjoin.RpcClient connection; + + MempoolAcceptanceCallback(this.connection); + + @override + bool callback(Uint8List tx) { + try { + final hexTx = bytesToHex(tx); + final resultJson = connection.call("testmempoolaccept", ['["$hexTx"]']); + final decoded = jsonDecode(resultJson); + return decoded[0]['allowed'] == true; + } catch (e) { + print("An error occurred: $e"); + return false; + } + } + + String bytesToHex(Uint8List bytes) { + return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + } +} + +class IsScriptOwnedCallback implements payjoin.IsScriptOwned { + final payjoin.RpcClient connection; + + IsScriptOwnedCallback(this.connection); + + @override + bool callback(Uint8List script) { + try { + final scriptObj = bitcoin.Script(script); + final address = + bitcoin.Address.fromScript(scriptObj, bitcoin.Network.regtest); + // This is a hack due to toString() not being exposed by dart FFI + final address_str = address.toQrUri().split(":")[1]; + final result = connection.call("getaddressinfo", [address_str]); + final decoded = jsonDecode(result); + return decoded["ismine"] == true; + } catch (e) { + print("An error occurred: $e"); + return false; + } + } +} + +class CheckInputsNotSeenCallback implements payjoin.IsOutputKnown { + final payjoin.RpcClient connection; + + CheckInputsNotSeenCallback(this.connection); + + @override + bool callback(_outpoint) { + return false; + } +} + +class ProcessPsbtCallback implements payjoin.ProcessPsbt { + final payjoin.RpcClient connection; + + ProcessPsbtCallback(this.connection); + + @override + String callback(String psbt) { + final res = jsonDecode(connection.call("walletprocesspsbt", [psbt])); + return res["psbt"]; + } +} + +payjoin.Initialized create_receiver_context( + bitcoin.Address address, + String directory, + payjoin.OhttpKeys ohttp_keys, + InMemoryReceiverPersister persister) { + var receiver = payjoin.UninitializedReceiver() + .createSession(address, directory, ohttp_keys, null) + .save(persister); + return receiver; +} + +String build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { + var outputs = {}; + outputs[pj_uri.address()] = 50; + var psbt = jsonDecode(sender.call("walletcreatefundedpsbt", [ + jsonEncode([]), + jsonEncode(outputs), + jsonEncode(0), + jsonEncode({ + "lockUnspents": true, + "fee_rate": 10, + "subtract_fee_from_outputs": [0] + }) + ]))["psbt"]; + return jsonDecode(sender.call("walletprocesspsbt", + [psbt, jsonEncode(true), jsonEncode("ALL"), jsonEncode(false)]))["psbt"]; +} + +List get_inputs(payjoin.RpcClient rpc_connection) { + var utxos = jsonDecode(rpc_connection.call("listunspent", [])); + List inputs = []; + for (var utxo in utxos) { + var txin = bitcoin.TxIn(bitcoin.OutPoint(utxo["txid"], utxo["vout"]), + bitcoin.Script(Uint8List.fromList([])), 0, []); + var tx_out = bitcoin.TxOut(bitcoin.Amount.fromBtc(utxo["amount"]), + bitcoin.Script(Uint8List.fromList(hex.decode(utxo["scriptPubKey"])))); + var psbt_in = payjoin.PsbtInput(tx_out, null, null); + inputs.add(payjoin.InputPair(txin, psbt_in, null)); + } + + return inputs; +} + +Future process_provisional_proposal( + payjoin.ProvisionalProposal proposal, + InMemoryReceiverPersister recv_persister) async { + final payjoin_proposal = proposal + .finalizeProposal(ProcessPsbtCallback(receiver), 1, 10) + .save(recv_persister); + return payjoin.PayjoinProposalReceiveSession(payjoin_proposal); +} + +Future process_wants_inputs( + payjoin.WantsInputs proposal, + InMemoryReceiverPersister recv_persister) async { + final provisional_proposal = proposal + .contributeInputs(get_inputs(receiver)) + .commitInputs() + .save(recv_persister); + return await process_provisional_proposal( + provisional_proposal, recv_persister); +} + +Future process_wants_outputs( + payjoin.WantsOutputs proposal, + InMemoryReceiverPersister recv_persister) async { + final wants_inputs = proposal.commitOutputs().save(recv_persister); + return await process_wants_inputs(wants_inputs, recv_persister); +} + +Future process_outputs_unknown( + payjoin.OutputsUnknown proposal, + InMemoryReceiverPersister recv_persister) async { + final wants_outputs = proposal + .identifyReceiverOutputs(IsScriptOwnedCallback(receiver)) + .save(recv_persister); + return await process_wants_outputs(wants_outputs, recv_persister); +} + +Future process_maybe_inputs_seen( + payjoin.MaybeInputsSeen proposal, + InMemoryReceiverPersister recv_persister) async { + final outputs_unknown = proposal + .checkNoInputsSeenBefore(CheckInputsNotSeenCallback(receiver)) + .save(recv_persister); + return await process_outputs_unknown(outputs_unknown, recv_persister); +} + +Future process_maybe_inputs_owned( + payjoin.MaybeInputsOwned proposal, + InMemoryReceiverPersister recv_persister) async { + final maybe_inputs_owned = proposal + .checkInputsNotOwned(IsScriptOwnedCallback(receiver)) + .save(recv_persister); + return await process_maybe_inputs_seen(maybe_inputs_owned, recv_persister); +} + +Future process_unchecked_proposal( + payjoin.UncheckedProposal proposal, + InMemoryReceiverPersister recv_persister) async { + final unchecked_proposal = proposal + .checkBroadcastSuitability(null, MempoolAcceptanceCallback(receiver)) + .save(recv_persister); + return await process_maybe_inputs_owned(unchecked_proposal, recv_persister); +} + +Future retrieve_receiver_proposal( + payjoin.Initialized receiver, + InMemoryReceiverPersister recv_persister, + payjoin.Url ohttp_relay) async { + var agent = http.Client(); + var request = receiver.extractReq(ohttp_relay.asString()); + var response = await agent.post(Uri.parse(request.request.url.asString()), + headers: {"Content-Type": request.request.contentType}, + body: request.request.body); + var res = receiver + .processRes(response.bodyBytes, request.clientResponse) + .save(recv_persister); + if (res.isNone()) { + return null; + } + var proposal = res.success(); + return await process_unchecked_proposal( + proposal as payjoin.UncheckedProposal, recv_persister); +} + +Future process_receiver_proposal( + payjoin.ReceiveSession receiver, + InMemoryReceiverPersister recv_persister, + payjoin.Url ohttp_relay) async { + if (receiver is payjoin.InitializedReceiveSession) { + var res = await retrieve_receiver_proposal( + receiver.inner, recv_persister, ohttp_relay); + if (res == null) { + return null; + } + return res; + } + + if (receiver is payjoin.UncheckedProposalReceiveSession) { + return await process_unchecked_proposal(receiver.inner, recv_persister); + } + if (receiver is payjoin.MaybeInputsOwnedReceiveSession) { + return await process_maybe_inputs_owned(receiver.inner, recv_persister); + } + if (receiver is payjoin.MaybeInputsSeenReceiveSession) { + return await process_maybe_inputs_seen(receiver.inner, recv_persister); + } + if (receiver is payjoin.OutputsUnknownReceiveSession) { + return await process_outputs_unknown(receiver.inner, recv_persister); + } + if (receiver is payjoin.WantsOutputsReceiveSession) { + return await process_wants_outputs(receiver.inner, recv_persister); + } + if (receiver is payjoin.WantsInputsReceiveSession) { + return await process_wants_inputs(receiver.inner, recv_persister); + } + if (receiver is payjoin.ProvisionalProposalReceiveSession) { + return await process_provisional_proposal(receiver.inner, recv_persister); + } + if (receiver is payjoin.PayjoinProposalReceiveSession) { + return receiver; + } + + throw Exception("Unknown receiver state: $receiver"); +} + +void main() { + group('Test integration', () { + test('Test integration v2 to v2', () async { + env = payjoin.initBitcoindSenderReceiver(); + bitcoind = env.getBitcoind(); + receiver = env.getReceiver(); + sender = env.getSender(); + var receiver_address = bitcoin.Address( + jsonDecode(receiver.call("getnewaddress", [])), + bitcoin.Network.regtest); + var services = payjoin.TestServices.initialize(); + + services.waitForServicesReady(); + var directory = services.directoryUrl().asString(); + var ohttp_keys = services.fetchOhttpKeys(); + var ohttp_relay = services.ohttpRelayUrl(); + var agent = http.Client(); + + // ********************** + // Inside the Receiver: + var recv_persister = InMemoryReceiverPersister("1"); + var sender_persister = InMemorySenderPersister("1"); + var session = create_receiver_context( + receiver_address, directory, ohttp_keys, recv_persister); + var process_response = await process_receiver_proposal( + payjoin.InitializedReceiveSession(session), + recv_persister, + ohttp_relay); + expect(process_response, isNull); + + // ********************** + // Inside the Sender: + // Create a funded PSBT (not broadcasted) to address with amount given in the pj_uri + var pj_uri = session.pjUri(); + var psbt = build_sweep_psbt(sender, pj_uri); + payjoin.WithReplyKey req_ctx = payjoin.SenderBuilder(psbt, pj_uri) + .buildRecommended(1000) + .save(sender_persister); + payjoin.RequestV2PostContext request = + req_ctx.extractV2(ohttp_relay.asString()); + var response = await agent.post(Uri.parse(request.request.url.asString()), + headers: {"Content-Type": request.request.contentType}, + body: request.request.body); + payjoin.V2GetContext send_ctx = req_ctx + .processResponse(response.bodyBytes, request.context) + .save(sender_persister); + // POST Original PSBT + + // ********************** + // Inside the Receiver: + + // GET fallback psbt + payjoin.ReceiveSession? payjoin_proposal = + await process_receiver_proposal( + payjoin.InitializedReceiveSession(session), + recv_persister, + ohttp_relay); + expect(payjoin_proposal, isNotNull); + expect(payjoin_proposal, isA()); + + payjoin.PayjoinProposal proposal = + (payjoin_proposal as payjoin.PayjoinProposalReceiveSession).inner; + payjoin.RequestResponse request_response = + proposal.extractReq(ohttp_relay.asString()); + var fallback_response = await agent.post( + Uri.parse(request_response.request.url.asString()), + headers: {"Content-Type": request_response.request.contentType}, + body: request_response.request.body); + proposal.processRes( + fallback_response.bodyBytes, request_response.clientResponse); + + // ********************** + // Inside the Sender: + // Sender checks, isngs, finalizes, extracts, and broadcasts + // Replay post fallback to get the response + payjoin.RequestOhttpContext ohttp_context_request = + send_ctx.extractReq(ohttp_relay.asString()); + var final_response = await agent.post( + Uri.parse(ohttp_context_request.request.url.asString()), + headers: {"Content-Type": ohttp_context_request.request.contentType}, + body: ohttp_context_request.request.body); + var checked_payjoin_proposal_psbt = send_ctx + .processResponse( + final_response.bodyBytes, ohttp_context_request.ohttpCtx) + .save(sender_persister) + .success(); + expect(checked_payjoin_proposal_psbt, isNotNull); + var payjoin_psbt = jsonDecode(sender.call("walletprocesspsbt", + [checked_payjoin_proposal_psbt!.serializeBase64()]))["psbt"]; + var final_psbt = jsonDecode(sender + .call("finalizepsbt", [payjoin_psbt, jsonEncode(false)]))["psbt"]; + var payjoin_tx = bitcoin.Psbt.deserializeBase64(final_psbt).extractTx(); + sender.call("sendrawtransaction", + [jsonEncode(hex.encode(payjoin_tx.serialize()))]); + + // Check resulting transaction and balances + var network_fees = + bitcoin.Psbt.deserializeBase64(final_psbt).fee().toBtc(); + // Sender sent the entire value of their utxo to the receiver (minus fees) + expect(payjoin_tx.input().length, 2); + expect(payjoin_tx.output().length, 1); + expect( + jsonDecode(receiver.call("getbalances", []))["mine"] + ["untrusted_pending"], + 100 - network_fees); + expect(jsonDecode(sender.call("getbalance", [])), 0.0); + }); + }); +}