From acb95a2345dc3a154100689a6bc67a6067ad359d Mon Sep 17 00:00:00 2001 From: spacebear Date: Thu, 12 Jun 2025 16:54:49 -0400 Subject: [PATCH 01/24] Initialize payjoin_dart --- 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 | 11 ++++++++++- 5 files changed, 34 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 73e051f88..385ae53c5 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/spacebear21/uniffi-dart.git", branch = "upgrading-to-0.29", 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/spacebear21/uniffi-dart.git", branch = "upgrading-to-0.29", 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..6714c0fb5 100644 --- a/payjoin-ffi/uniffi-bindgen.rs +++ b/payjoin-ffi/uniffi-bindgen.rs @@ -1,4 +1,13 @@ fn main() { #[cfg(feature = "uniffi")] - uniffi::uniffi_bindgen_main() + uniffi::uniffi_bindgen_main(); + #[cfg(feature = "uniffi")] + uniffi_dart::gen::generate_dart_bindings( + "src/payjoin_ffi.udl".into(), + None, + Some("dart/lib".into()), + "target/release/libpayjoin_ffi.dylib".into(), + true, + ) + .unwrap(); } From 7a4d884738a9fdf2383629d96b77a0b38d6090d5 Mon Sep 17 00:00:00 2001 From: user Date: Mon, 23 Jun 2025 10:50:27 -0400 Subject: [PATCH 02/24] Add initial dart tests to prove out the utilization of uniffi-dart Minimizing the import I was able to get rid of all errors introduced by uniffi-dart and built a successful uniffi binding. These tests are just the barebones of what is built but proves out that we have some functioning bindings when creating a minimal build. --- payjoin-ffi/dart/pubspec.yaml | 2 + .../dart/test/test_payjoin_unit_test.dart | 53 +++++++++++++++++++ 2 files changed, 55 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..573966072 --- /dev/null +++ b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart @@ -0,0 +1,53 @@ +import 'package:test/test.dart'; +import "../lib/payjoin_ffi.dart" as payjoin; + +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', () { + // import test_utils to allow for const EXAMPLE_URL + final https = "https://example.com"; + 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"); + } + } + } + }); + }); +} From fa3f83e3c5a857274fc766c54a3c3de814cd50e0 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 27 Jun 2025 15:11:30 -0400 Subject: [PATCH 03/24] Add persistence unit tests --- .../dart/test/test_payjoin_unit_test.dart | 96 ++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_unit_test.dart b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart index 573966072..ed3fa5670 100644 --- a/payjoin-ffi/dart/test/test_payjoin_unit_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_unit_test.dart @@ -1,5 +1,53 @@ import 'package:test/test.dart'; -import "../lib/payjoin_ffi.dart" as payjoin; +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', () { @@ -26,8 +74,7 @@ void main() { }); test('Test valid uris', () { - // import test_utils to allow for const EXAMPLE_URL - final https = "https://example.com"; + final https = payjoin.exampleUrl(); final onion = "http://vjdpwgybvubne5hda6v4c5iaeeevhge6jvo3w2cl6eocbwwvwxp7b7qd.onion"; @@ -50,4 +97,47 @@ void main() { } }); }); + + 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 c57f23662315f9bc30211e8f393766355423d057 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 19 Jun 2025 12:13:20 -0400 Subject: [PATCH 04/24] Specify the language to create bindings at the bindings creation step I've included support for including --language args beyond those explicitly supported in uniffi-rs with a match block that passes the chosen language to chose what binding cargo package to use. --- payjoin-ffi/uniffi-bindgen.rs | 42 ++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/payjoin-ffi/uniffi-bindgen.rs b/payjoin-ffi/uniffi-bindgen.rs index 6714c0fb5..cbc79bd15 100644 --- a/payjoin-ffi/uniffi-bindgen.rs +++ b/payjoin-ffi/uniffi-bindgen.rs @@ -1,13 +1,35 @@ fn main() { #[cfg(feature = "uniffi")] - uniffi::uniffi_bindgen_main(); - #[cfg(feature = "uniffi")] - uniffi_dart::gen::generate_dart_bindings( - "src/payjoin_ffi.udl".into(), - None, - Some("dart/lib".into()), - "target/release/libpayjoin_ffi.dylib".into(), - true, - ) - .unwrap(); + uniffi_bindgen() +} + +#[cfg(feature = "uniffi")] +fn uniffi_bindgen() { + use std::env; + let args: Vec = 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 6b1fbe670d6fb7abea9e927ea4fa1ae910772a6b Mon Sep 17 00:00:00 2001 From: user Date: Tue, 24 Jun 2025 18:21:14 -0400 Subject: [PATCH 05/24] 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 836fa9f610d36c05b5b1bc7a9c6edcec8725ebe9 Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 1 Jul 2025 01:26:03 -0400 Subject: [PATCH 06/24] Add dart github workflow --- .github/workflows/dart.yml | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/dart.yml diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 000000000..13f8e731e --- /dev/null +++ b/.github/workflows/dart.yml @@ -0,0 +1,40 @@ +name: Build and Test Dart +on: + pull_request: + paths: + - payjoin-ffi/** + +jobs: + test: + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: payjoin-ffi/dart + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: "Use cache" + uses: Swatinem/rust-cache@v2 + + - name: Generate bindings and binaries + run: | + if [ "${{ matrix.os }}" = "macos-latest" ]; then + bash ./scripts/generate_macos.sh + else + bash ./scripts/generate_linux.sh + fi + + - name: Install dependencies + run: dart pub get + + - name: Run tests + run: dart test From d1a7208b7e10eedabfa4d818078032f14853b32b Mon Sep 17 00:00:00 2001 From: user Date: Wed, 2 Jul 2025 12:28:53 -0400 Subject: [PATCH 07/24] [WIP] integration tests --- .../test/test_payjoin_integration_test.dart | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 payjoin-ffi/dart/test/test_payjoin_integration_test.dart 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..8e9e1a6f9 --- /dev/null +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -0,0 +1,246 @@ +import "dart:typed_data"; + +import "package:http/http.dart" as http; +import 'package:test/test.dart'; +import "dart:convert"; + +import "../lib/payjoin_ffi.dart" as payjoin; +import "../lib/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; + } +} + +abstract class AlwaysTrueCanBroadcast implements payjoin.CanBroadcast { + @override + bool callback( + Uint8List tx, + ) => + true; +} + +abstract class AlwaysInputsNotSeen implements payjoin.MaybeInputsSeen { + @override + bool callback() => false; +} + +late payjoin.BitcoindEnv env; +late payjoin.BitcoindInstance bitcoind; +late payjoin.RpcClient receiver; +late payjoin.RpcClient sender; + +payjoin.WithContext create_receiver_context( + address, directory, ohttp_keys, expiry, persister) { + var receiver = payjoin.UninitializedReceiver() + .createSession(address, directory, ohttp_keys, null) + .save(persister); + return receiver; +} + +bitcoin.Psbt 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] + }) + ])); + return jsonDecode(sender.call("walletprocesspsbt", + [psbt, jsonEncode(true), jsonEncode("ALL"), jsonEncode(false)]))["psbt"]; +} + +// payjoin.PayjoinProposal process_provisional_proposal( +// payjoin.ProvisionalProposal proposal, +// InMemoryReceiverPersister recv_persister) async { +// final payjoin_proposal = proposal +// .finalizeProposal( +// processPsbt, minFeerateSatPerVb, maxEffectiveFeeRateSatPerVb) +// .save(recv_persister); +// return payjoin.ReceiverSessionEvent.PAYJOIN_PROPOSAL(payjoin_proposal); +// } +// +// payjoin.ProvisionalProposal process_wants_inputs(payjoin.WantsInputs proposal, +// InMemoryReceiverPersister recv_persister) async { +// final provisional_proposal = proposal +// .contributeInputs(replacementInputs) +// .commitInputs() +// .save(recv_persister); +// return await process_provisional_proposal( +// provisional_proposal, recv_persister); +// } +// +// payjoin.WantsInputs process_wants_ouputs(payjoin.WantsOutputs proposal, +// InMemoryReceiverPersister recv_persister) async { +// final wants_inputs = proposal.commitOutputs().save(recv_persister); +// return await process_wants_inputs(wants_inputs, recv_persister); +// } +// +// payjoin.WantsOutputs process_outputs_unknown(payjoin.OutputsUnknown proposal, +// InMemoryReceiverPersister recv_persister) async { +// final wants_outputs = +// proposal.identifyReceiverOutputs(isReceiverOutput).save(recv_persister); +// return await process_wants_inputs(wants_outputs, recv_persister); +// } +// +// payjoin.OutputsUnknown process_maybe_inputs_seen( +// payjoin.MaybeInputsSeen proposal, +// InMemoryReceiverPersister recv_persister) async { +// final outputs_unknown = +// proposal.checkNoInputsSeenBefore(isKnown).save(recv_persister); +// return await process_outputs_unknown(outputs_unknown, recv_persister); +// } +// +// payjoin.MaybeInputsSeen process_maybe_inputs_owned( +// MaybeInputsOwned proposal, InMemoryReceiverPersister recv_persister) async { +// final alwaysInputsNotSeen = AlwaysInputsNotSeen; +// final maybe_inputs_owned = +// proposal.checkInputsNotOwned(alwaysInputsNotSeen).save(recv_persister); +// return await process_maybe_inputs_seen(maybe_inputs_owned, recv_persister); +// } +// +// payjoin.MaybeInputsOwned process_unchecked_proposal( +// payjoin.UncheckedProposal proposal, +// InMemoryReceiverPersister recv_persister) async { +// final canAlwaysBroadcast = AlwaysTrueCanBroadcast; +// final receiver = proposal +// .checkBroadcastSuitability(null, canAlwaysBroadcast) +// .save(recv_persister); +// return await process_maybe_inputs_owned(receiver, recv_persister); +// } +// +// payjoin.ReceiverSessionEvent retrieve_receiver_proposal( +// payjoin.WithContext receiver, +// InMemoryReceiverPersister recv_persister, +// payjoin.Url ohttp_relay) async { +// var agent = http.Client(); +// var request = receiver.extractReq(ohttp_relay.toString()); +// var response = await agent.post(request.request.url, +// headers: {"Content-Type": request.request.contentType}); +// var res = receiver +// .processRes(response.body.toString(), request.clientResponse) +// .save(recv_persister); +// if (res.isNone()) { +// return null; +// } +// var proposal = res.success(); +// return await process_unchecked_proposal(proposal, recv_persister); +// } + +payjoin.ReceiverSessionEvent? process_receiver_proposal(payjoin.ReceiverSessionEvent receiver, InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) { + if receiver is payjoin.WithContext { + var res = await retrieve +} +} + +void main() { + group('Test integration', () { + test('Test integration v2 to v2', () async { + var receiver_address = bitcoin.Address( + jsonEncode(receiver.call("getnewaddress", [])), + bitcoin.Network.regtest); + var services = payjoin.TestServices.initialize(); + + services.waitForServicesReady(); + var directory = services.directoryUrl(); + 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, null, recv_persister); + // var process_response = + // await process_receiver_proposal(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.toString(), pj_uri) + .buildRecommended(1000) + .save(sender_persister); + payjoin.RequestV2PostContext request = + req_ctx.extractV2(ohttp_relay.toString()); + var response = await agent.post(Uri.https(request.request.url.toString()), + headers: {"Content-Type": request.request.contentType}, + body: request.request.body); + payjoin.V2GetContext send_ctx = req_ctx + .processResponse( + Uint8List.fromList(utf8.encode(response.body)), request.context) + .save(sender_persister); + // POST Original PSBT + + // ********************** + // Inside the Receiver: + + // GET fallback psbt + var payjoin_proposal = await process_receiver_proposal(); + expect(payjoin_proposal, isNotNull); + expect(payjoin_proposal, isA()); + + payjoin_proposal = payjoin_proposal.inner; + payjoin.RequestResponse request = + payjoin_proposal.extractReq(ohttp_relay.toString()); + var response = await agent.post(Uri.https(request.request.url.toString()), + headers: {"Content-Type": request.request.contentType}, + body: request.request.body); + payjoin_proposal.processRes(response.body, request.context); + + expect(true, false); + }); + }); +} From 5de47d0e746159678cc1d8e687543ece26861997 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 2 Jul 2025 14:29:47 -0400 Subject: [PATCH 08/24] [WIP] almost done --- .../test/test_payjoin_integration_test.dart | 350 +++++++++++++----- 1 file changed, 248 insertions(+), 102 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 8e9e1a6f9..a6aaf8bc7 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -1,12 +1,18 @@ +import "dart:convert"; import "dart:typed_data"; import "package:http/http.dart" as http; import 'package:test/test.dart'; -import "dart:convert"; +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; @@ -54,25 +60,74 @@ class InMemorySenderPersister implements payjoin.JsonSenderSessionPersister { } } -abstract class AlwaysTrueCanBroadcast implements payjoin.CanBroadcast { +class MempoolAcceptanceCallback implements payjoin.CanBroadcast { + final payjoin.RpcClient connection; + + MempoolAcceptanceCallback(this.connection); + @override - bool callback( - Uint8List tx, - ) => - true; + 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(); + } } -abstract class AlwaysInputsNotSeen implements payjoin.MaybeInputsSeen { +class IsScriptOwnedCallback implements payjoin.IsScriptOwned { + final payjoin.RpcClient connection; + + IsScriptOwnedCallback(this.connection); + @override - bool callback() => false; + bool callback(Uint8List script) { + try { + final scriptObj = bitcoin.Script(script); + final address = + bitcoin.Address.fromScript(scriptObj, bitcoin.Network.regtest); + final result = connection.call("getaddressinfo", [address.toString()]); + final decoded = jsonDecode(result); + return decoded["ismone"] == true; + } catch (e) { + print("An error occured: $e"); + return false; + } + } } -late payjoin.BitcoindEnv env; -late payjoin.BitcoindInstance bitcoind; -late payjoin.RpcClient receiver; -late payjoin.RpcClient sender; +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); -payjoin.WithContext create_receiver_context( + @override + String callback(String psbt) { + final res = jsonDecode(connection.call("walletprocesspsbt", [psbt])); + return res["psbt"]; + } +} + +payjoin.Initialized create_receiver_context( address, directory, ohttp_keys, expiry, persister) { var receiver = payjoin.UninitializedReceiver() .createSession(address, directory, ohttp_keys, null) @@ -97,87 +152,136 @@ bitcoin.Psbt build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { [psbt, jsonEncode(true), jsonEncode("ALL"), jsonEncode(false)]))["psbt"]; } -// payjoin.PayjoinProposal process_provisional_proposal( -// payjoin.ProvisionalProposal proposal, -// InMemoryReceiverPersister recv_persister) async { -// final payjoin_proposal = proposal -// .finalizeProposal( -// processPsbt, minFeerateSatPerVb, maxEffectiveFeeRateSatPerVb) -// .save(recv_persister); -// return payjoin.ReceiverSessionEvent.PAYJOIN_PROPOSAL(payjoin_proposal); -// } -// -// payjoin.ProvisionalProposal process_wants_inputs(payjoin.WantsInputs proposal, -// InMemoryReceiverPersister recv_persister) async { -// final provisional_proposal = proposal -// .contributeInputs(replacementInputs) -// .commitInputs() -// .save(recv_persister); -// return await process_provisional_proposal( -// provisional_proposal, recv_persister); -// } -// -// payjoin.WantsInputs process_wants_ouputs(payjoin.WantsOutputs proposal, -// InMemoryReceiverPersister recv_persister) async { -// final wants_inputs = proposal.commitOutputs().save(recv_persister); -// return await process_wants_inputs(wants_inputs, recv_persister); -// } -// -// payjoin.WantsOutputs process_outputs_unknown(payjoin.OutputsUnknown proposal, -// InMemoryReceiverPersister recv_persister) async { -// final wants_outputs = -// proposal.identifyReceiverOutputs(isReceiverOutput).save(recv_persister); -// return await process_wants_inputs(wants_outputs, recv_persister); -// } -// -// payjoin.OutputsUnknown process_maybe_inputs_seen( -// payjoin.MaybeInputsSeen proposal, -// InMemoryReceiverPersister recv_persister) async { -// final outputs_unknown = -// proposal.checkNoInputsSeenBefore(isKnown).save(recv_persister); -// return await process_outputs_unknown(outputs_unknown, recv_persister); -// } -// -// payjoin.MaybeInputsSeen process_maybe_inputs_owned( -// MaybeInputsOwned proposal, InMemoryReceiverPersister recv_persister) async { -// final alwaysInputsNotSeen = AlwaysInputsNotSeen; -// final maybe_inputs_owned = -// proposal.checkInputsNotOwned(alwaysInputsNotSeen).save(recv_persister); -// return await process_maybe_inputs_seen(maybe_inputs_owned, recv_persister); -// } -// -// payjoin.MaybeInputsOwned process_unchecked_proposal( -// payjoin.UncheckedProposal proposal, -// InMemoryReceiverPersister recv_persister) async { -// final canAlwaysBroadcast = AlwaysTrueCanBroadcast; -// final receiver = proposal -// .checkBroadcastSuitability(null, canAlwaysBroadcast) -// .save(recv_persister); -// return await process_maybe_inputs_owned(receiver, recv_persister); -// } -// -// payjoin.ReceiverSessionEvent retrieve_receiver_proposal( -// payjoin.WithContext receiver, -// InMemoryReceiverPersister recv_persister, -// payjoin.Url ohttp_relay) async { -// var agent = http.Client(); -// var request = receiver.extractReq(ohttp_relay.toString()); -// var response = await agent.post(request.request.url, -// headers: {"Content-Type": request.request.contentType}); -// var res = receiver -// .processRes(response.body.toString(), request.clientResponse) -// .save(recv_persister); -// if (res.isNone()) { -// return null; -// } -// var proposal = res.success(); -// return await process_unchecked_proposal(proposal, recv_persister); -// } - -payjoin.ReceiverSessionEvent? process_receiver_proposal(payjoin.ReceiverSessionEvent receiver, InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) { - if receiver is payjoin.WithContext { - var res = await retrieve +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._(previous_output); + var raw_tx = jsonDecode(rpc_connection.call("gettransaction", + [jsonEncode(utxo["txid"]), jsonEncode(true), jsonEncode(true)])); + var prev_out = raw_tx["decoded"]["vout"][utxo["vout"]]; + var prev_spk = bitcoin.Script(Uint8List.fromList( + hex.decode(prev_out["ScriptPubkey"]["hex"].toString()))); + var prev_amount = bitcoin.Amount.fromBtc(prev_out["value"]); + var tx_out = bitcoin.TxOut._(prev_amount, prev_spk); + var psbt_in = payjoin.PsbtInput._(tx_out, null, null); + inputs.add(payjoin.InputPair(txin, psbt_in)); + } + + return inputs; } + +process_provisional_proposal( + proposal, InMemoryReceiverPersister recv_persister) async { + final payjoin_proposal = proposal + .finalizeProposal(ProcessPsbtCallback(receiver), 1, 10) + .save(recv_persister); + return payjoin.PayjoinProposalReceiveSession(payjoin_proposal); +} + +process_wants_inputs(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); +} + +process_wants_outputs( + proposal, InMemoryReceiverPersister recv_persister) async { + final wants_inputs = proposal.commitOutputs().save(recv_persister); + return await process_wants_inputs(wants_inputs, recv_persister); +} + +process_outputs_unknown( + proposal, InMemoryReceiverPersister recv_persister) async { + final wants_outputs = proposal + .identifyReceiverOutputs(IsScriptOwnedCallback(receiver)) + .save(recv_persister); + return await process_wants_outputs(wants_outputs, recv_persister); +} + +process_maybe_inputs_seen( + proposal, InMemoryReceiverPersister recv_persister) async { + final outputs_unknown = proposal + .checkNoInputsSeenBefore(CheckInputsNotSeenCallback(receiver)) + .save(recv_persister); + return await process_outputs_unknown(outputs_unknown, recv_persister); +} + +process_maybe_inputs_owned( + 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); +} + +process_unchecked_proposal( + 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(receiver, + InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) async { + var agent = http.Client(); + var request = receiver.extractReq(ohttp_relay.toString()); + var response = await agent.post(Uri.https(request.request.url.toString()), + headers: {"Content-Type": request.request.contentType}); + var res = receiver + .processRes(Uint8List.fromList(utf8.encode(response.body)), + request.clientResponse) + .save(recv_persister); + if (res.isNone()) { + return null; + } + var proposal = res.success(); + return await process_unchecked_proposal(proposal, recv_persister); +} + +Future process_receiver_proposal( + payjoin.ReceiveSession receiver, + InMemoryReceiverPersister recv_persister, + payjoin.Url ohttp_relay) async { + if (receiver is payjoin.Initialized) { + var res = + await retrieve_receiver_proposal(receiver, recv_persister, ohttp_relay); + if (res == null) { + return null; + } + return res; + } + + if (receiver is payjoin.UncheckedProposal) { + return await process_unchecked_proposal(receiver, recv_persister); + } + if (receiver is payjoin.MaybeInputsOwned) { + return await process_maybe_inputs_owned(receiver, recv_persister); + } + if (receiver is payjoin.MaybeInputsSeen) { + return await process_maybe_inputs_seen(receiver, recv_persister); + } + if (receiver is payjoin.OutputsUnknown) { + return await process_outputs_unknown(receiver, recv_persister); + } + if (receiver is payjoin.WantsOutputs) { + return await process_wants_outputs(receiver, recv_persister); + } + if (receiver is payjoin.WantsInputs) { + return await process_wants_inputs(receiver, recv_persister); + } + if (receiver is payjoin.ProvisionalProposal) { + return await process_provisional_proposal(receiver, recv_persister); + } + if (receiver is payjoin.PayjoinProposal) { + return receiver; + } + + throw Exception("Unknown receiver state: $receiver"); } void main() { @@ -228,19 +332,61 @@ void main() { // Inside the Receiver: // GET fallback psbt - var payjoin_proposal = await process_receiver_proposal(); + payjoin.ReceiveSession? payjoin_proposal = + await process_receiver_proposal( + payjoin.InitializedReceiveSession(session), + recv_persister, + ohttp_relay); expect(payjoin_proposal, isNotNull); expect(payjoin_proposal, isA()); - payjoin_proposal = payjoin_proposal.inner; - payjoin.RequestResponse request = - payjoin_proposal.extractReq(ohttp_relay.toString()); - var response = await agent.post(Uri.https(request.request.url.toString()), - headers: {"Content-Type": request.request.contentType}, - body: request.request.body); - payjoin_proposal.processRes(response.body, request.context); + payjoin.Initialized payjoin_proposal_inner = payjoin_proposal; + payjoin.RequestResponse request_response = + payjoin_proposal_inner.extractReq(ohttp_relay.toString()); + var fallback_response = await agent.post( + Uri.https(request_response.request.url.toString()), + headers: {"Content-Type": request_response.request.contentType}, + body: request_response.request.body); + payjoin_proposal_inner.processRes( + Uint8List.fromList(utf8.encode(fallback_response.body)), + 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.toString()); + var final_response = await agent.post( + Uri.https(ohttp_context_request.request.url.toString()), + headers: {"Content-Type": ohttp_context_request.request.contentType}, + body: ohttp_context_request.request.body); + var checked_payjoin_proposal_psbt = send_ctx + .processResponse(Uint8List.fromList(utf8.encode(final_response.body)), + ohttp_context_request.ohttpCtx) + .save(sender_persister) + .success(); + print("checked_payjoin_proposal: $checked_payjoin_proposal_psbt"); + 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()))]); - expect(true, false); + // 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(sender.call("getbalance", []), 0); }); }); } From 3acf3ff316cf6d76319c4a83c244bb12e5ad3d3d Mon Sep 17 00:00:00 2001 From: user Date: Wed, 2 Jul 2025 15:39:41 -0400 Subject: [PATCH 09/24] determine proposal return type --- payjoin-ffi/dart/test/test_payjoin_integration_test.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index a6aaf8bc7..798d1bda5 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -340,14 +340,15 @@ void main() { expect(payjoin_proposal, isNotNull); expect(payjoin_proposal, isA()); - payjoin.Initialized payjoin_proposal_inner = payjoin_proposal; + payjoin.PayjoinProposal proposal = + payjoin_proposal as payjoin.PayjoinProposal; payjoin.RequestResponse request_response = - payjoin_proposal_inner.extractReq(ohttp_relay.toString()); + proposal.extractReq(ohttp_relay.toString()); var fallback_response = await agent.post( Uri.https(request_response.request.url.toString()), headers: {"Content-Type": request_response.request.contentType}, body: request_response.request.body); - payjoin_proposal_inner.processRes( + proposal.processRes( Uint8List.fromList(utf8.encode(fallback_response.body)), request_response.clientResponse); From 55a5de66185fbb03354322767f4bfbb617424286 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 2 Jul 2025 16:12:16 -0400 Subject: [PATCH 10/24] fix type defs --- .../test/test_payjoin_integration_test.dart | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 798d1bda5..640d93bf6 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -153,18 +153,21 @@ bitcoin.Psbt build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { } List get_inputs(payjoin.RpcClient rpc_connection) { - var utxos = jsonDecode(rpc_connection.call("listunspent", [])); + var utxos = jsonDecode(rpc_connection.call("listunspent", [null])); List inputs = []; for (var utxo in utxos) { - var txin = bitcoin.TxIn._(previous_output); + var txin = bitcoin.TxIn.inner( + bitcoin.OutPoint.inner(utxo["txid"], utxo["vout"]), + bitcoin.Script(Uint8List.fromList([])), + 0, []); var raw_tx = jsonDecode(rpc_connection.call("gettransaction", [jsonEncode(utxo["txid"]), jsonEncode(true), jsonEncode(true)])); var prev_out = raw_tx["decoded"]["vout"][utxo["vout"]]; var prev_spk = bitcoin.Script(Uint8List.fromList( hex.decode(prev_out["ScriptPubkey"]["hex"].toString()))); var prev_amount = bitcoin.Amount.fromBtc(prev_out["value"]); - var tx_out = bitcoin.TxOut._(prev_amount, prev_spk); - var psbt_in = payjoin.PsbtInput._(tx_out, null, null); + var tx_out = bitcoin.TxOut.inner(prev_amount, prev_spk); + var psbt_in = payjoin.PsbtInput.inner(tx_out, null, null); inputs.add(payjoin.InputPair(txin, psbt_in)); } @@ -287,8 +290,12 @@ Future process_receiver_proposal( 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( - jsonEncode(receiver.call("getnewaddress", [])), + jsonEncode(receiver.call("getnewaddress", [null])), bitcoin.Network.regtest); var services = payjoin.TestServices.initialize(); @@ -384,10 +391,10 @@ void main() { expect(payjoin_tx.input().length, 2); expect(payjoin_tx.output().length, 1); expect( - jsonDecode(receiver.call("getbalances", []))["mine"] + jsonDecode(receiver.call("getbalances", [null]))["mine"] ["untrusted_pending"], 100 - network_fees); - expect(sender.call("getbalance", []), 0); + expect(sender.call("getbalance", [null]), 0); }); }); } From f182adadd04e4e07a2f4e87f36015c0bde43195d Mon Sep 17 00:00:00 2001 From: user Date: Wed, 2 Jul 2025 16:15:08 -0400 Subject: [PATCH 11/24] spellcheck --- payjoin-ffi/dart/test/test_payjoin_integration_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 640d93bf6..89cb2e2ea 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -98,7 +98,7 @@ class IsScriptOwnedCallback implements payjoin.IsScriptOwned { final decoded = jsonDecode(result); return decoded["ismone"] == true; } catch (e) { - print("An error occured: $e"); + print("An error occurred: $e"); return false; } } From 0c20c112dd1b59da29ff7ec510c09768fd642d77 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 2 Jul 2025 19:06:12 -0400 Subject: [PATCH 12/24] add http dep --- payjoin-ffi/dart/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/payjoin-ffi/dart/pubspec.yaml b/payjoin-ffi/dart/pubspec.yaml index 67a445c44..de663a3de 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 From 4c392e27eb9832a2d08ecad6cb34bb484459acf4 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 2 Jul 2025 19:06:58 -0400 Subject: [PATCH 13/24] fix Record constructors The constructors are public as of uniffi-dart db4eed728b14 --- .../dart/test/test_payjoin_integration_test.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 89cb2e2ea..a4b358758 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -156,18 +156,16 @@ List get_inputs(payjoin.RpcClient rpc_connection) { var utxos = jsonDecode(rpc_connection.call("listunspent", [null])); List inputs = []; for (var utxo in utxos) { - var txin = bitcoin.TxIn.inner( - bitcoin.OutPoint.inner(utxo["txid"], utxo["vout"]), - bitcoin.Script(Uint8List.fromList([])), - 0, []); + var txin = bitcoin.TxIn(bitcoin.OutPoint(utxo["txid"], utxo["vout"]), + bitcoin.Script(Uint8List.fromList([])), 0, []); var raw_tx = jsonDecode(rpc_connection.call("gettransaction", [jsonEncode(utxo["txid"]), jsonEncode(true), jsonEncode(true)])); var prev_out = raw_tx["decoded"]["vout"][utxo["vout"]]; var prev_spk = bitcoin.Script(Uint8List.fromList( hex.decode(prev_out["ScriptPubkey"]["hex"].toString()))); var prev_amount = bitcoin.Amount.fromBtc(prev_out["value"]); - var tx_out = bitcoin.TxOut.inner(prev_amount, prev_spk); - var psbt_in = payjoin.PsbtInput.inner(tx_out, null, null); + var tx_out = bitcoin.TxOut(prev_amount, prev_spk); + var psbt_in = payjoin.PsbtInput(tx_out, null, null); inputs.add(payjoin.InputPair(txin, psbt_in)); } From 2c7c7cadb9ccaa7b825641dcc9fabb8f5f2737f0 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 2 Jul 2025 19:07:28 -0400 Subject: [PATCH 14/24] Fix directory arg the FFI create_session method takes a String, not a URL. --- payjoin-ffi/dart/test/test_payjoin_integration_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index a4b358758..ba0a2f6f6 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -298,7 +298,7 @@ void main() { var services = payjoin.TestServices.initialize(); services.waitForServicesReady(); - var directory = services.directoryUrl(); + var directory = services.directoryUrl().toString(); var ohttp_keys = services.fetchOhttpKeys(); var ohttp_relay = services.ohttpRelayUrl(); var agent = http.Client(); From 59790c59b259756f5dd4a90c0864daab61cc1cf3 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 2 Jul 2025 20:02:46 -0400 Subject: [PATCH 15/24] remove null placeholders uniffi-dart empty sequence handling was fixed in 8536f955ad7b --- .../dart/test/test_payjoin_integration_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index ba0a2f6f6..b3513bf09 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -153,7 +153,7 @@ bitcoin.Psbt build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { } List get_inputs(payjoin.RpcClient rpc_connection) { - var utxos = jsonDecode(rpc_connection.call("listunspent", [null])); + var utxos = jsonDecode(rpc_connection.call("listunspent", [])); List inputs = []; for (var utxo in utxos) { var txin = bitcoin.TxIn(bitcoin.OutPoint(utxo["txid"], utxo["vout"]), @@ -293,7 +293,7 @@ void main() { receiver = env.getReceiver(); sender = env.getSender(); var receiver_address = bitcoin.Address( - jsonEncode(receiver.call("getnewaddress", [null])), + jsonEncode(receiver.call("getnewaddress", [])), bitcoin.Network.regtest); var services = payjoin.TestServices.initialize(); @@ -375,7 +375,7 @@ void main() { print("checked_payjoin_proposal: $checked_payjoin_proposal_psbt"); expect(checked_payjoin_proposal_psbt, isNotNull); var payjoin_psbt = jsonDecode(sender.call("walletprocesspsbt", - [checked_payjoin_proposal_psbt?.serializeBase64()]))["psbt"]; + [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(); @@ -389,10 +389,10 @@ void main() { expect(payjoin_tx.input().length, 2); expect(payjoin_tx.output().length, 1); expect( - jsonDecode(receiver.call("getbalances", [null]))["mine"] + jsonDecode(receiver.call("getbalances", []))["mine"] ["untrusted_pending"], 100 - network_fees); - expect(sender.call("getbalance", [null]), 0); + expect(sender.call("getbalance", []), 0); }); }); } From 8230be1969b1ccfc4d657acb83ffed5be9d7acce Mon Sep 17 00:00:00 2001 From: user Date: Wed, 2 Jul 2025 18:33:33 -0400 Subject: [PATCH 16/24] fix some null pointers --- .../dart/test/test_payjoin_integration_test.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index b3513bf09..9fb1ac26f 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -128,9 +128,12 @@ class ProcessPsbtCallback implements payjoin.ProcessPsbt { } payjoin.Initialized create_receiver_context( - address, directory, ohttp_keys, expiry, persister) { + bitcoin.Address address, + payjoin.Url directory, + payjoin.OhttpKeys ohttp_keys, + InMemoryReceiverPersister persister) { var receiver = payjoin.UninitializedReceiver() - .createSession(address, directory, ohttp_keys, null) + .createSession(address, directory.toString(), ohttp_keys, 600) .save(persister); return receiver; } @@ -302,13 +305,16 @@ void main() { var ohttp_keys = services.fetchOhttpKeys(); var ohttp_relay = services.ohttpRelayUrl(); var agent = http.Client(); + print("Gets here: 1"); // ********************** // Inside the Receiver: var recv_persister = InMemoryReceiverPersister("1"); var sender_persister = InMemorySenderPersister("1"); + print("Gets here: 2"); var session = create_receiver_context( - receiver_address, directory, ohttp_keys, null, recv_persister); + receiver_address, directory, ohttp_keys, recv_persister); + print("Gets here: 3"); // var process_response = // await process_receiver_proposal(session, recv_persister, ohttp_relay); // expect(process_response, isNull); @@ -316,12 +322,14 @@ void main() { // ********************** // Inside the Sender: // Create a funded PSBT (not broadcasted) to address with amount given in the pj_uri + print("Gets here: 4"); var pj_uri = session.pjUri(); var psbt = build_sweep_psbt(sender, pj_uri); payjoin.WithReplyKey req_ctx = payjoin.SenderBuilder(psbt.toString(), pj_uri) .buildRecommended(1000) .save(sender_persister); + print("Gets here: 5"); payjoin.RequestV2PostContext request = req_ctx.extractV2(ohttp_relay.toString()); var response = await agent.post(Uri.https(request.request.url.toString()), From d9cfcd946af6ada78c1e954bda0f766af856fdbd Mon Sep 17 00:00:00 2001 From: user Date: Thu, 3 Jul 2025 13:05:02 -0400 Subject: [PATCH 17/24] state machine function return types --- .../test/test_payjoin_integration_test.dart | 85 +++++++++++-------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 9fb1ac26f..e4c9991a0 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -129,11 +129,11 @@ class ProcessPsbtCallback implements payjoin.ProcessPsbt { payjoin.Initialized create_receiver_context( bitcoin.Address address, - payjoin.Url directory, + String directory, payjoin.OhttpKeys ohttp_keys, InMemoryReceiverPersister persister) { var receiver = payjoin.UninitializedReceiver() - .createSession(address, directory.toString(), ohttp_keys, 600) + .createSession(address, directory, ohttp_keys, null) .save(persister); return receiver; } @@ -175,15 +175,18 @@ List get_inputs(payjoin.RpcClient rpc_connection) { return inputs; } -process_provisional_proposal( - proposal, InMemoryReceiverPersister recv_persister) async { +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); } -process_wants_inputs(proposal, InMemoryReceiverPersister recv_persister) async { +Future process_wants_inputs( + payjoin.WantsInputs proposal, + InMemoryReceiverPersister recv_persister) async { final provisional_proposal = proposal .contributeInputs(get_inputs(receiver)) .commitInputs() @@ -192,46 +195,53 @@ process_wants_inputs(proposal, InMemoryReceiverPersister recv_persister) async { provisional_proposal, recv_persister); } -process_wants_outputs( - proposal, InMemoryReceiverPersister recv_persister) async { +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); } -process_outputs_unknown( - proposal, InMemoryReceiverPersister recv_persister) async { +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); } -process_maybe_inputs_seen( - proposal, InMemoryReceiverPersister recv_persister) async { +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); } -process_maybe_inputs_owned( - proposal, InMemoryReceiverPersister recv_persister) async { +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); } -process_unchecked_proposal( - proposal, InMemoryReceiverPersister recv_persister) async { +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(receiver, - InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) async { +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.toString()); var response = await agent.post(Uri.https(request.request.url.toString()), @@ -244,7 +254,8 @@ Future retrieve_receiver_proposal(receiver, return null; } var proposal = res.success(); - return await process_unchecked_proposal(proposal, recv_persister); + return await process_unchecked_proposal( + proposal as payjoin.UncheckedProposal, recv_persister); } Future process_receiver_proposal( @@ -252,8 +263,8 @@ Future process_receiver_proposal( InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) async { if (receiver is payjoin.Initialized) { - var res = - await retrieve_receiver_proposal(receiver, recv_persister, ohttp_relay); + var res = await retrieve_receiver_proposal( + receiver as payjoin.Initialized, recv_persister, ohttp_relay); if (res == null) { return null; } @@ -261,28 +272,35 @@ Future process_receiver_proposal( } if (receiver is payjoin.UncheckedProposal) { - return await process_unchecked_proposal(receiver, recv_persister); + return await process_unchecked_proposal( + receiver as payjoin.UncheckedProposal, recv_persister); } if (receiver is payjoin.MaybeInputsOwned) { - return await process_maybe_inputs_owned(receiver, recv_persister); + return await process_maybe_inputs_owned( + receiver as payjoin.MaybeInputsOwned, recv_persister); } if (receiver is payjoin.MaybeInputsSeen) { - return await process_maybe_inputs_seen(receiver, recv_persister); + return await process_maybe_inputs_seen( + receiver as payjoin.MaybeInputsSeen, recv_persister); } if (receiver is payjoin.OutputsUnknown) { - return await process_outputs_unknown(receiver, recv_persister); + return await process_outputs_unknown( + receiver as payjoin.OutputsUnknown, recv_persister); } if (receiver is payjoin.WantsOutputs) { - return await process_wants_outputs(receiver, recv_persister); + return await process_wants_outputs( + receiver as payjoin.WantsOutputs, recv_persister); } if (receiver is payjoin.WantsInputs) { - return await process_wants_inputs(receiver, recv_persister); + return await process_wants_inputs( + receiver as payjoin.WantsInputs, recv_persister); } if (receiver is payjoin.ProvisionalProposal) { - return await process_provisional_proposal(receiver, recv_persister); + return await process_provisional_proposal( + receiver as payjoin.ProvisionalProposal, recv_persister); } if (receiver is payjoin.PayjoinProposal) { - return receiver; + return receiver as payjoin.PayjoinProposalReceiveSession; } throw Exception("Unknown receiver state: $receiver"); @@ -296,7 +314,7 @@ void main() { receiver = env.getReceiver(); sender = env.getSender(); var receiver_address = bitcoin.Address( - jsonEncode(receiver.call("getnewaddress", [])), + jsonDecode(receiver.call("getnewaddress", [])), bitcoin.Network.regtest); var services = payjoin.TestServices.initialize(); @@ -305,31 +323,26 @@ void main() { var ohttp_keys = services.fetchOhttpKeys(); var ohttp_relay = services.ohttpRelayUrl(); var agent = http.Client(); - print("Gets here: 1"); // ********************** // Inside the Receiver: var recv_persister = InMemoryReceiverPersister("1"); var sender_persister = InMemorySenderPersister("1"); - print("Gets here: 2"); var session = create_receiver_context( receiver_address, directory, ohttp_keys, recv_persister); - print("Gets here: 3"); - // var process_response = - // await process_receiver_proposal(session, recv_persister, ohttp_relay); + // var process_response = await process_receiver_proposal( + // session as payjoin.ReceiveSession, 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 - print("Gets here: 4"); var pj_uri = session.pjUri(); var psbt = build_sweep_psbt(sender, pj_uri); payjoin.WithReplyKey req_ctx = payjoin.SenderBuilder(psbt.toString(), pj_uri) .buildRecommended(1000) .save(sender_persister); - print("Gets here: 5"); payjoin.RequestV2PostContext request = req_ctx.extractV2(ohttp_relay.toString()); var response = await agent.post(Uri.https(request.request.url.toString()), From e89eb02280a0eb20fed5baa8ce8a7570d39caf26 Mon Sep 17 00:00:00 2001 From: spacebear Date: Mon, 7 Jul 2025 18:59:28 -0400 Subject: [PATCH 18/24] use asString() for URLs toString() isn't defined due to Display trait not yet implemented in uniffi-dart, so use asString() instead which is defined. --- .../test/test_payjoin_integration_test.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index e4c9991a0..6e57b9aef 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -243,8 +243,8 @@ Future retrieve_receiver_proposal( InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) async { var agent = http.Client(); - var request = receiver.extractReq(ohttp_relay.toString()); - var response = await agent.post(Uri.https(request.request.url.toString()), + var request = receiver.extractReq(ohttp_relay.asString()); + var response = await agent.post(Uri.https(request.request.url.asString()), headers: {"Content-Type": request.request.contentType}); var res = receiver .processRes(Uint8List.fromList(utf8.encode(response.body)), @@ -319,7 +319,7 @@ void main() { var services = payjoin.TestServices.initialize(); services.waitForServicesReady(); - var directory = services.directoryUrl().toString(); + var directory = services.directoryUrl().asString(); var ohttp_keys = services.fetchOhttpKeys(); var ohttp_relay = services.ohttpRelayUrl(); var agent = http.Client(); @@ -344,8 +344,8 @@ void main() { .buildRecommended(1000) .save(sender_persister); payjoin.RequestV2PostContext request = - req_ctx.extractV2(ohttp_relay.toString()); - var response = await agent.post(Uri.https(request.request.url.toString()), + req_ctx.extractV2(ohttp_relay.asString()); + var response = await agent.post(Uri.https(request.request.url.asString()), headers: {"Content-Type": request.request.contentType}, body: request.request.body); payjoin.V2GetContext send_ctx = req_ctx @@ -369,9 +369,9 @@ void main() { payjoin.PayjoinProposal proposal = payjoin_proposal as payjoin.PayjoinProposal; payjoin.RequestResponse request_response = - proposal.extractReq(ohttp_relay.toString()); + proposal.extractReq(ohttp_relay.asString()); var fallback_response = await agent.post( - Uri.https(request_response.request.url.toString()), + Uri.https(request_response.request.url.asString()), headers: {"Content-Type": request_response.request.contentType}, body: request_response.request.body); proposal.processRes( @@ -383,9 +383,9 @@ void main() { // Sender checks, isngs, finalizes, extracts, and broadcasts // Replay post fallback to get the response payjoin.RequestOhttpContext ohttp_context_request = - send_ctx.extractReq(ohttp_relay.toString()); + send_ctx.extractReq(ohttp_relay.asString()); var final_response = await agent.post( - Uri.https(ohttp_context_request.request.url.toString()), + Uri.https(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 From 0db80e7af5da02a5ab8dad2ea53114e02dd9b432 Mon Sep 17 00:00:00 2001 From: spacebear Date: Mon, 7 Jul 2025 19:30:15 -0400 Subject: [PATCH 19/24] Fix build_sweep_psbt Pass the "psbt" value as parameter, and return a String instead of a bitcoin.Psbt --- .../dart/test/test_payjoin_integration_test.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 6e57b9aef..3181ed45e 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -138,7 +138,7 @@ payjoin.Initialized create_receiver_context( return receiver; } -bitcoin.Psbt build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { +String build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { var outputs = {}; outputs[pj_uri.address()] = 50; var psbt = jsonDecode(sender.call("walletcreatefundedpsbt", [ @@ -150,7 +150,7 @@ bitcoin.Psbt build_sweep_psbt(payjoin.RpcClient sender, payjoin.PjUri pj_uri) { "fee_rate": 10, "subtract_fee_from_outputs": [0] }) - ])); + ]))["psbt"]; return jsonDecode(sender.call("walletprocesspsbt", [psbt, jsonEncode(true), jsonEncode("ALL"), jsonEncode(false)]))["psbt"]; } @@ -339,10 +339,9 @@ void main() { // 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.toString(), pj_uri) - .buildRecommended(1000) - .save(sender_persister); + 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.https(request.request.url.asString()), From 770821dfa38edd8ab87d09aad36c3d38a0d81d92 Mon Sep 17 00:00:00 2001 From: spacebear Date: Mon, 7 Jul 2025 21:35:16 -0400 Subject: [PATCH 20/24] s/Uri.https/Uri/parse Uri.https() expects only the host portion, not a complete URL. Use Uri.parse() instead. --- payjoin-ffi/dart/test/test_payjoin_integration_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 3181ed45e..3118843e9 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -244,7 +244,7 @@ Future retrieve_receiver_proposal( payjoin.Url ohttp_relay) async { var agent = http.Client(); var request = receiver.extractReq(ohttp_relay.asString()); - var response = await agent.post(Uri.https(request.request.url.asString()), + var response = await agent.post(Uri.parse(request.request.url.asString()), headers: {"Content-Type": request.request.contentType}); var res = receiver .processRes(Uint8List.fromList(utf8.encode(response.body)), @@ -344,7 +344,7 @@ void main() { .save(sender_persister); payjoin.RequestV2PostContext request = req_ctx.extractV2(ohttp_relay.asString()); - var response = await agent.post(Uri.https(request.request.url.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 @@ -370,7 +370,7 @@ void main() { payjoin.RequestResponse request_response = proposal.extractReq(ohttp_relay.asString()); var fallback_response = await agent.post( - Uri.https(request_response.request.url.asString()), + Uri.parse(request_response.request.url.asString()), headers: {"Content-Type": request_response.request.contentType}, body: request_response.request.body); proposal.processRes( @@ -384,7 +384,7 @@ void main() { payjoin.RequestOhttpContext ohttp_context_request = send_ctx.extractReq(ohttp_relay.asString()); var final_response = await agent.post( - Uri.https(ohttp_context_request.request.url.asString()), + 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 From 05759994558d1b20cfe3a7f9ac181712f9d8bf7a Mon Sep 17 00:00:00 2001 From: spacebear Date: Tue, 8 Jul 2025 16:06:57 -0400 Subject: [PATCH 21/24] Fix process_receiver_proposal --- .../test/test_payjoin_integration_test.dart | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 3118843e9..17cb85011 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -245,10 +245,10 @@ Future retrieve_receiver_proposal( 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}); + headers: {"Content-Type": request.request.contentType}, + body: request.request.body); var res = receiver - .processRes(Uint8List.fromList(utf8.encode(response.body)), - request.clientResponse) + .processRes(response.bodyBytes, request.clientResponse) .save(recv_persister); if (res.isNone()) { return null; @@ -262,45 +262,38 @@ Future process_receiver_proposal( payjoin.ReceiveSession receiver, InMemoryReceiverPersister recv_persister, payjoin.Url ohttp_relay) async { - if (receiver is payjoin.Initialized) { + if (receiver is payjoin.InitializedReceiveSession) { var res = await retrieve_receiver_proposal( - receiver as payjoin.Initialized, recv_persister, ohttp_relay); + receiver.inner, recv_persister, ohttp_relay); if (res == null) { return null; } return res; } - if (receiver is payjoin.UncheckedProposal) { - return await process_unchecked_proposal( - receiver as payjoin.UncheckedProposal, recv_persister); + if (receiver is payjoin.UncheckedProposalReceiveSession) { + return await process_unchecked_proposal(receiver.inner, recv_persister); } - if (receiver is payjoin.MaybeInputsOwned) { - return await process_maybe_inputs_owned( - receiver as payjoin.MaybeInputsOwned, recv_persister); + if (receiver is payjoin.MaybeInputsOwnedReceiveSession) { + return await process_maybe_inputs_owned(receiver.inner, recv_persister); } - if (receiver is payjoin.MaybeInputsSeen) { - return await process_maybe_inputs_seen( - receiver as payjoin.MaybeInputsSeen, recv_persister); + if (receiver is payjoin.MaybeInputsSeenReceiveSession) { + return await process_maybe_inputs_seen(receiver.inner, recv_persister); } - if (receiver is payjoin.OutputsUnknown) { - return await process_outputs_unknown( - receiver as payjoin.OutputsUnknown, recv_persister); + if (receiver is payjoin.OutputsUnknownReceiveSession) { + return await process_outputs_unknown(receiver.inner, recv_persister); } - if (receiver is payjoin.WantsOutputs) { - return await process_wants_outputs( - receiver as payjoin.WantsOutputs, recv_persister); + if (receiver is payjoin.WantsOutputsReceiveSession) { + return await process_wants_outputs(receiver.inner, recv_persister); } - if (receiver is payjoin.WantsInputs) { - return await process_wants_inputs( - receiver as payjoin.WantsInputs, recv_persister); + if (receiver is payjoin.WantsInputsReceiveSession) { + return await process_wants_inputs(receiver.inner, recv_persister); } - if (receiver is payjoin.ProvisionalProposal) { - return await process_provisional_proposal( - receiver as payjoin.ProvisionalProposal, recv_persister); + if (receiver is payjoin.ProvisionalProposalReceiveSession) { + return await process_provisional_proposal(receiver.inner, recv_persister); } - if (receiver is payjoin.PayjoinProposal) { - return receiver as payjoin.PayjoinProposalReceiveSession; + if (receiver is payjoin.PayjoinProposalReceiveSession) { + return receiver; } throw Exception("Unknown receiver state: $receiver"); @@ -330,9 +323,11 @@ void main() { var sender_persister = InMemorySenderPersister("1"); var session = create_receiver_context( receiver_address, directory, ohttp_keys, recv_persister); - // var process_response = await process_receiver_proposal( - // session as payjoin.ReceiveSession, recv_persister, ohttp_relay); - // expect(process_response, isNull); + var process_response = await process_receiver_proposal( + payjoin.InitializedReceiveSession(session), + recv_persister, + ohttp_relay); + expect(process_response, isNull); // ********************** // Inside the Sender: From 9c91b0e3579d8193d34dc8920a5459f393e4abfb Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 9 Jul 2025 12:08:32 -0400 Subject: [PATCH 22/24] Fix response bytes --- .../dart/test/test_payjoin_integration_test.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 17cb85011..c0543fd9b 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -343,8 +343,7 @@ void main() { headers: {"Content-Type": request.request.contentType}, body: request.request.body); payjoin.V2GetContext send_ctx = req_ctx - .processResponse( - Uint8List.fromList(utf8.encode(response.body)), request.context) + .processResponse(response.bodyBytes, request.context) .save(sender_persister); // POST Original PSBT @@ -369,8 +368,7 @@ void main() { headers: {"Content-Type": request_response.request.contentType}, body: request_response.request.body); proposal.processRes( - Uint8List.fromList(utf8.encode(fallback_response.body)), - request_response.clientResponse); + fallback_response.bodyBytes, request_response.clientResponse); // ********************** // Inside the Sender: @@ -383,8 +381,8 @@ void main() { headers: {"Content-Type": ohttp_context_request.request.contentType}, body: ohttp_context_request.request.body); var checked_payjoin_proposal_psbt = send_ctx - .processResponse(Uint8List.fromList(utf8.encode(final_response.body)), - ohttp_context_request.ohttpCtx) + .processResponse( + final_response.bodyBytes, ohttp_context_request.ohttpCtx) .save(sender_persister) .success(); print("checked_payjoin_proposal: $checked_payjoin_proposal_psbt"); From 6d6f45e684fdac46498ba95806307ab8b9faedcc Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 9 Jul 2025 14:10:40 -0400 Subject: [PATCH 23/24] Fix RPC calls --- .../test/test_payjoin_integration_test.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index c0543fd9b..563226652 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -69,7 +69,7 @@ class MempoolAcceptanceCallback implements payjoin.CanBroadcast { bool callback(Uint8List tx) { try { final hexTx = bytesToHex(tx); - final resultJson = connection.call("testmempoolaccept", ['[$hexTx]']); + final resultJson = connection.call("testmempoolaccept", ['["$hexTx"]']); final decoded = jsonDecode(resultJson); return decoded[0]['allowed'] == true; } catch (e) { @@ -94,9 +94,11 @@ class IsScriptOwnedCallback implements payjoin.IsScriptOwned { final scriptObj = bitcoin.Script(script); final address = bitcoin.Address.fromScript(scriptObj, bitcoin.Network.regtest); - final result = connection.call("getaddressinfo", [address.toString()]); + // 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["ismone"] == true; + return decoded["ismine"] == true; } catch (e) { print("An error occurred: $e"); return false; @@ -161,13 +163,8 @@ List get_inputs(payjoin.RpcClient rpc_connection) { for (var utxo in utxos) { var txin = bitcoin.TxIn(bitcoin.OutPoint(utxo["txid"], utxo["vout"]), bitcoin.Script(Uint8List.fromList([])), 0, []); - var raw_tx = jsonDecode(rpc_connection.call("gettransaction", - [jsonEncode(utxo["txid"]), jsonEncode(true), jsonEncode(true)])); - var prev_out = raw_tx["decoded"]["vout"][utxo["vout"]]; - var prev_spk = bitcoin.Script(Uint8List.fromList( - hex.decode(prev_out["ScriptPubkey"]["hex"].toString()))); - var prev_amount = bitcoin.Amount.fromBtc(prev_out["value"]); - var tx_out = bitcoin.TxOut(prev_amount, prev_spk); + 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)); } From 8cdb017edef5719c16b0b455eb500f6bbab35822 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 9 Jul 2025 14:49:45 -0400 Subject: [PATCH 24/24] Fix test assertions --- payjoin-ffi/dart/test/test_payjoin_integration_test.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart index 563226652..6362fe99e 100644 --- a/payjoin-ffi/dart/test/test_payjoin_integration_test.dart +++ b/payjoin-ffi/dart/test/test_payjoin_integration_test.dart @@ -354,10 +354,10 @@ void main() { recv_persister, ohttp_relay); expect(payjoin_proposal, isNotNull); - expect(payjoin_proposal, isA()); + expect(payjoin_proposal, isA()); payjoin.PayjoinProposal proposal = - payjoin_proposal as payjoin.PayjoinProposal; + (payjoin_proposal as payjoin.PayjoinProposalReceiveSession).inner; payjoin.RequestResponse request_response = proposal.extractReq(ohttp_relay.asString()); var fallback_response = await agent.post( @@ -382,7 +382,6 @@ void main() { final_response.bodyBytes, ohttp_context_request.ohttpCtx) .save(sender_persister) .success(); - print("checked_payjoin_proposal: $checked_payjoin_proposal_psbt"); expect(checked_payjoin_proposal_psbt, isNotNull); var payjoin_psbt = jsonDecode(sender.call("walletprocesspsbt", [checked_payjoin_proposal_psbt!.serializeBase64()]))["psbt"]; @@ -402,7 +401,7 @@ void main() { jsonDecode(receiver.call("getbalances", []))["mine"] ["untrusted_pending"], 100 - network_fees); - expect(sender.call("getbalance", []), 0); + expect(jsonDecode(sender.call("getbalance", [])), 0.0); }); }); }