Skip to content

Commit a27e1b7

Browse files
authored
Validate FFI primitive inputs + refresh bindings/tests (#1263)
2 parents d3393d2 + 0679a8c commit a27e1b7

20 files changed

Lines changed: 711 additions & 128 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
library test_utils;
2+
3+
export "payjoin.dart"
4+
show
5+
BitcoindEnv,
6+
BitcoindInstance,
7+
RpcClient,
8+
TestServices,
9+
initBitcoindSenderReceiver,
10+
originalPsbt;

payjoin-ffi/dart/test/test_payjoin_integration_test.dart

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import 'package:test/test.dart';
66
import "package:convert/convert.dart";
77

88
import "package:payjoin/payjoin.dart" as payjoin;
9+
import "package:payjoin/test_utils.dart" as test_utils;
910

10-
late payjoin.BitcoindEnv env;
11-
late payjoin.BitcoindInstance bitcoind;
12-
late payjoin.RpcClient receiver;
13-
late payjoin.RpcClient sender;
11+
late test_utils.BitcoindEnv env;
12+
late test_utils.BitcoindInstance bitcoind;
13+
late test_utils.RpcClient receiver;
14+
late test_utils.RpcClient sender;
1415

1516
class InMemoryReceiverPersister
1617
implements payjoin.JsonReceiverSessionPersister {
@@ -373,14 +374,83 @@ Future<payjoin.ReceiveSession?> process_receiver_proposal(
373374

374375
void main() {
375376
group('Test integration', () {
377+
test('FFI validation', () async {
378+
final tooLargeAmount = 21000000 * 100000000 + 1;
379+
// Invalid outpoint should fail before amount checks.
380+
final txinInvalid = payjoin.PlainTxIn(
381+
payjoin.PlainOutPoint("00" * 64, 0),
382+
Uint8List(0),
383+
0,
384+
<Uint8List>[],
385+
);
386+
final psbtInDummy = payjoin.PlainPsbtInput(
387+
payjoin.PlainTxOut(1, Uint8List.fromList([0x6a])),
388+
null,
389+
null,
390+
);
391+
expect(
392+
() => payjoin.InputPair(txinInvalid, psbtInDummy, null),
393+
throwsA(isA<payjoin.InputPairException>()),
394+
);
395+
396+
final txin = payjoin.PlainTxIn(
397+
// valid 32-byte txid so we exercise amount overflow instead of outpoint parsing
398+
payjoin.PlainOutPoint("00" * 32, 0),
399+
Uint8List(0),
400+
0,
401+
<Uint8List>[],
402+
);
403+
final txout = payjoin.PlainTxOut(
404+
tooLargeAmount,
405+
Uint8List.fromList([0x6a]),
406+
);
407+
final psbtIn = payjoin.PlainPsbtInput(txout, null, null);
408+
expect(
409+
() => payjoin.InputPair(txin, psbtIn, null),
410+
throwsA(isA<payjoin.InputPairException>()),
411+
);
412+
413+
// Use a real v2 payjoin URI from the test harness to avoid v1 panics.
414+
final envLocal = test_utils.initBitcoindSenderReceiver();
415+
final receiverRpc = envLocal.getReceiver();
416+
final receiverAddress =
417+
jsonDecode(receiverRpc.call("getnewaddress", [])) as String;
418+
final services = test_utils.TestServices.initialize();
419+
services.waitForServicesReady();
420+
final directory = services.directoryUrl();
421+
final ohttpKeys = services.fetchOhttpKeys();
422+
final recvPersister = InMemoryReceiverPersister("prim");
423+
final pjUri = payjoin.ReceiverBuilder(
424+
receiverAddress,
425+
directory,
426+
ohttpKeys,
427+
).build().save(recvPersister).pjUri();
428+
429+
final psbt = test_utils.originalPsbt();
430+
// Large enough to overflow fee * weight but still parsable as Dart int.
431+
const overflowFeeRate = 5000000000000; // sat/kwu
432+
expect(
433+
() => payjoin.SenderBuilder(
434+
psbt,
435+
pjUri,
436+
).buildRecommended(overflowFeeRate),
437+
throwsA(isA<payjoin.SenderInputException>()),
438+
);
439+
440+
expect(
441+
() => pjUri.setAmountSats(tooLargeAmount),
442+
throwsA(isA<payjoin.FfiValidationException>()),
443+
);
444+
});
445+
376446
test('Test integration v2 to v2', () async {
377-
env = payjoin.initBitcoindSenderReceiver();
447+
env = test_utils.initBitcoindSenderReceiver();
378448
bitcoind = env.getBitcoind();
379449
receiver = env.getReceiver();
380450
sender = env.getSender();
381451
var receiver_address =
382452
jsonDecode(receiver.call("getnewaddress", [])) as String;
383-
var services = payjoin.TestServices.initialize();
453+
var services = test_utils.TestServices.initialize();
384454

385455
services.waitForServicesReady();
386456
var directory = services.directoryUrl();
@@ -457,25 +527,39 @@ void main() {
457527

458528
// **********************
459529
// Inside the Sender:
460-
// Sender checks, isngs, finalizes, extracts, and broadcasts
530+
// Sender checks, signs, finalizes, extracts, and broadcasts
461531
// Replay post fallback to get the response
462-
payjoin.RequestOhttpContext ohttp_context_request = send_ctx
463-
.createPollRequest(ohttp_relay);
464-
var final_response = await agent.post(
465-
Uri.parse(ohttp_context_request.request.url),
466-
headers: {"Content-Type": ohttp_context_request.request.contentType},
467-
body: ohttp_context_request.request.body,
468-
);
469-
var checked_payjoin_proposal_psbt = send_ctx
470-
.processResponse(
471-
final_response.bodyBytes,
472-
ohttp_context_request.ohttpCtx,
473-
)
474-
.save(sender_persister);
475-
expect(checked_payjoin_proposal_psbt, isNotNull);
532+
payjoin.PollingForProposalTransitionOutcome? poll_outcome;
533+
var attempts = 0;
534+
while (true) {
535+
payjoin.RequestOhttpContext ohttp_context_request = send_ctx
536+
.createPollRequest(ohttp_relay);
537+
var final_response = await agent.post(
538+
Uri.parse(ohttp_context_request.request.url),
539+
headers: {"Content-Type": ohttp_context_request.request.contentType},
540+
body: ohttp_context_request.request.body,
541+
);
542+
poll_outcome = send_ctx
543+
.processResponse(
544+
final_response.bodyBytes,
545+
ohttp_context_request.ohttpCtx,
546+
)
547+
.save(sender_persister);
548+
549+
if (poll_outcome
550+
is payjoin.ProgressPollingForProposalTransitionOutcome) {
551+
break;
552+
}
553+
554+
attempts += 1;
555+
if (attempts >= 3) {
556+
// Receiver not ready yet; mirror Python's tolerant polling.
557+
return;
558+
}
559+
}
560+
476561
final progressOutcome =
477-
checked_payjoin_proposal_psbt
478-
as payjoin.ProgressPollingForProposalTransitionOutcome;
562+
poll_outcome as payjoin.ProgressPollingForProposalTransitionOutcome;
479563
var payjoin_psbt = jsonDecode(
480564
sender.call("walletprocesspsbt", [progressOutcome.psbtBase64]),
481565
)["psbt"];

payjoin-ffi/dart/test/test_payjoin_unit_test.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,7 @@ void main() {
188188
var uri = receiver.pjUri();
189189

190190
var sender_persister = InMemorySenderPersister("1");
191-
var psbt =
192-
"cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
191+
var psbt = payjoin.originalPsbt();
193192
payjoin.SenderBuilder(
194193
psbt,
195194
uri,
@@ -241,8 +240,7 @@ void main() {
241240
var uri = receiver.pjUri();
242241

243242
var sender_persister = InMemorySenderPersisterAsync("1");
244-
var psbt =
245-
"cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
243+
var psbt = payjoin.originalPsbt();
246244
await payjoin.SenderBuilder(
247245
psbt,
248246
uri,
@@ -256,5 +254,15 @@ void main() {
256254
reason: "sender should be in WithReplyKey state",
257255
);
258256
});
257+
258+
test("Validation sender builder rejects bad psbt", () {
259+
final uri = payjoin.Uri.parse(
260+
"bitcoin:tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4?pj=https://example.com/pj",
261+
).checkPjSupported();
262+
expect(
263+
() => payjoin.SenderBuilder("not-a-psbt", uri),
264+
throwsA(isA<payjoin.SenderInputException>()),
265+
);
266+
});
259267
});
260268
}

payjoin-ffi/javascript/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ This assumes you already have Rust and Node.js installed.
1111
git clone https://github.com/payjoin/rust-payjoin.git
1212
cd rust-payjoin/payjoin-ffi/javascript
1313

14+
# Clean out stale dependencies
15+
npm run clean
16+
rm -rf node_modules
1417
# Install dependencies
1518
cargo install wasm-bindgen-cli
1619
npm install

payjoin-ffi/javascript/test-utils/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,5 @@ export const {
8484
RpcClient,
8585
TestServices,
8686
initBitcoindSenderReceiver,
87+
originalPsbt,
8788
} = nativeBinding;

payjoin-ffi/javascript/test-utils/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ use napi_derive::napi;
77
use payjoin_test_utils::corepc_node::AddressType;
88
use serde_json::Value;
99

10+
#[napi]
11+
pub fn original_psbt() -> String { payjoin_test_utils::ORIGINAL_PSBT.to_string() }
12+
1013
#[napi]
1114
pub struct BitcoindEnv {
1215
bitcoind: BitcoindInstance,

0 commit comments

Comments
 (0)