Skip to content

Commit

Permalink
Merge pull request #13 from kumulynja/test-bdk-fix
Browse files Browse the repository at this point in the history
Separate receiver and sender logic in steps
  • Loading branch information
BitcoinZavior authored Jun 24, 2024
2 parents c3d300f + e6a5eee commit ead5878
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 107 deletions.
70 changes: 42 additions & 28 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
## [0.13.0-dev.2]

### Fixed

- Fix `assumeInteractiveReceiver` return type

### Refactor

- Separate receiver and sender logic in example app.

## [0.13.0]

### Features & Modules

#### Send module
- ##### V1
- `RequestBuilder` exposes `fromPsbtAndUri`, `buildWithAdditionalFee`, `buildRecommended`, `buildNonIncentivizing`, `alwaysDisableOutputSubstitution`.
- `RequestContext` exposes `extractContextV1` & `extractContextV2`.
- `ContextV1` exposes `processResponse`.
- ##### V2
- `ContextV2` exposes `processResponse`.

- ##### V1
- `RequestBuilder` exposes `fromPsbtAndUri`, `buildWithAdditionalFee`, `buildRecommended`, `buildNonIncentivizing`, `alwaysDisableOutputSubstitution`.
- `RequestContext` exposes `extractContextV1` & `extractContextV2`.
- `ContextV1` exposes `processResponse`.
- ##### V2
- `ContextV2` exposes `processResponse`.

#### Receive module
- ##### V1
- `UncheckedProposal` exposes `fromRequest`, `extractTxToScheduleBroadcast`, `checkBroadcastSuitability`, `buildNonIncentivizing`,
`assumeInteractiveReceiver` &`alwaysDisableOutputSubstitution`.
- `MaybeInputsOwned` exposes `checkInputsNotOwned`.
- `MaybeMixedInputScripts` exposes `checkNoMixedInputScripts`.
- `MaybeInputsSeen` exposes `checkNoInputsSeenBefore`.
- `OutputsUnknown` exposes `identifyReceiverOutputs`.
- `ProvisionalProposal` exposes `substituteOutputAddress`, `contributeNonWitnessInput`, `contributeWitnessInput`, `tryPreservingPrivacy` &
`finalizeProposal`.
- `PayjoinProposal` exposes `isOutputSubstitutionDisabled`, `ownedVouts`, `psbt` & `utxosToBeLocked`.
- ##### V2
- `Enroller` exposes `fromDirectoryConfig`, `processResponse` & `extractRequest`.
- `Enrolled` exposes `extractRequest`, `processResponse` & `fallbackTarget`.
- `UncheckedProposal` exposes `extractTxToScheduleBroadcast`, `checkBroadcastSuitability` & `assumeInteractiveReceiver`.
- `MaybeInputsOwned` exposes `checkInputsNotOwned`.
- `MaybeMixedInputScripts` exposes `checkNoMixedInputScripts`.
- `MaybeInputsSeen` exposes `checkNoInputsSeenBefore`.
- `OutputsUnknown` exposes `identifyReceiverOutputs`.
- `ProvisionalProposal` exposes `substituteOutputAddress`, `contributeNonWitnessInput`, `contributeWitnessInput`, `tryPreservingPrivacy` &
`finalizeProposal`.
- `PayjoinProposal` exposes `deserializeRes`, `extractV1Req`, `extractV2Request`, `isOutputSubstitutionDisabled`, `ownedVouts`, `psbt` &
`utxosToBeLocked`.

- ##### V1
- `UncheckedProposal` exposes `fromRequest`, `extractTxToScheduleBroadcast`, `checkBroadcastSuitability`, `buildNonIncentivizing`,
`assumeInteractiveReceiver` &`alwaysDisableOutputSubstitution`.
- `MaybeInputsOwned` exposes `checkInputsNotOwned`.
- `MaybeMixedInputScripts` exposes `checkNoMixedInputScripts`.
- `MaybeInputsSeen` exposes `checkNoInputsSeenBefore`.
- `OutputsUnknown` exposes `identifyReceiverOutputs`.
- `ProvisionalProposal` exposes `substituteOutputAddress`, `contributeNonWitnessInput`, `contributeWitnessInput`, `tryPreservingPrivacy` &
`finalizeProposal`.
- `PayjoinProposal` exposes `isOutputSubstitutionDisabled`, `ownedVouts`, `psbt` & `utxosToBeLocked`.
- ##### V2
- `Enroller` exposes `fromDirectoryConfig`, `processResponse` & `extractRequest`.
- `Enrolled` exposes `extractRequest`, `processResponse` & `fallbackTarget`.
- `UncheckedProposal` exposes `extractTxToScheduleBroadcast`, `checkBroadcastSuitability` & `assumeInteractiveReceiver`.
- `MaybeInputsOwned` exposes `checkInputsNotOwned`.
- `MaybeMixedInputScripts` exposes `checkNoMixedInputScripts`.
- `MaybeInputsSeen` exposes `checkNoInputsSeenBefore`.
- `OutputsUnknown` exposes `identifyReceiverOutputs`.
- `ProvisionalProposal` exposes `substituteOutputAddress`, `contributeNonWitnessInput`, `contributeWitnessInput`, `tryPreservingPrivacy` &
`finalizeProposal`.
- `PayjoinProposal` exposes `deserializeRes`, `extractV1Req`, `extractV2Request`, `isOutputSubstitutionDisabled`, `ownedVouts`, `psbt` &
`utxosToBeLocked`.
9 changes: 7 additions & 2 deletions example/integration_test/bitcoin_core_full_cycle_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:payjoin_flutter/common.dart' as common;
import 'package:payjoin_flutter/send.dart';
import 'package:payjoin_flutter/uri.dart' as pay_join_uri;
import 'package:payjoin_flutter_example/btc_client.dart';
import 'package:payjoin_flutter_example/payjoin_library.dart';
Expand Down Expand Up @@ -35,11 +36,15 @@ void main() {
final amount = await uri.amount();
final senderPsbt =
(await sender.walletCreateFundedPsbt(amount, address, 2000))["psbt"];
final requestContext = await (await RequestBuilder.fromPsbtAndUri(
psbtBase64: senderPsbt, uri: uri))
.buildRecommended(minFeeRate: 0);
final (_, ctx) = await requestContext.extractContextV1();
debugPrint(
"\nOriginal sender psbt: $senderPsbt",
);
final (provisionalProposal, ctx) =
await payJoinLib.handlePjRequest(senderPsbt, pjUri, (e) async {
final provisionalProposal =
await payJoinLib.handlePjRequest(senderPsbt, (e) async {
final script = ScriptBuf(bytes: e);
final address = await (await Address.fromScript(
script: script, network: Network.regtest))
Expand Down
2 changes: 0 additions & 2 deletions example/lib/bdk_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ class BdkClient {

Future<int> getBalance() async {
final balance = await wallet.getBalance();
final res = "Total Balance: ${balance.total.toString()}";
debugPrint(res);
return balance.total;
}

Expand Down
104 changes: 62 additions & 42 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:bdk_flutter/bdk_flutter.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:payjoin_flutter/common.dart' as common;
import 'package:payjoin_flutter/send.dart';
import 'package:payjoin_flutter/uri.dart' as pay_join_uri;
import 'package:payjoin_flutter_example/bdk_client.dart';
import 'package:payjoin_flutter_example/payjoin_library.dart';
Expand Down Expand Up @@ -50,7 +51,9 @@ class _PayJoinState extends State<PayJoin> {
String displayText = "";
String pjUri = "";
late PartiallySignedTransaction senderPsbt;
late PartiallySignedTransaction processedAndFinalizedPsbt;
late String receiverPsbtBase64;
late ContextV1 contextV1;

@override
void initState() {
sender.restoreWallet();
Expand Down Expand Up @@ -112,6 +115,8 @@ class _PayJoinState extends State<PayJoin> {
setState(() {
displayText = "sync complete";
});
debugPrint(
"receiver balance: ${(await receiver.getBalance()).toString()}");
debugPrint(
"sender balance: ${(await sender.getBalance()).toString()}");
},
Expand Down Expand Up @@ -140,37 +145,50 @@ class _PayJoinState extends State<PayJoin> {
fontWeight: FontWeight.w800),
)),
TextButton(
onPressed: () async {
final balance = await sender.getBalance();
debugPrint("Sender Balance: ${balance.toString()}");
final uri = await pay_join_uri.Uri.fromString(pjUri);
final address = await uri.address();
int amount =
(((await uri.amount()) ?? 0) * 100000000).toInt();
final psbt = (await sender.createPsbt(address, amount, 2000));
debugPrint(
"\nOriginal sender psbt: ${await psbt.serialize()}",
);
setState(() {
senderPsbt = psbt;
});
},
child: Text(
"Create Sender psbt using receiver pjUri",
style: GoogleFonts.manrope(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w800),
)),
onPressed: () async {
final balance = await sender.getBalance();
debugPrint("Sender Balance: ${balance.toString()}");
final uri = await pay_join_uri.Uri.fromString(pjUri);
final address = await uri.address();
int amount = (((await uri.amount()) ?? 0) * 100000000).toInt();
final psbt = (await sender.createPsbt(address, amount, 2000));
debugPrint(
"\nOriginal sender psbt: ${await psbt.serialize()}",
);

final requestContext =
(await (await RequestBuilder.fromPsbtAndUri(
psbtBase64: await psbt.serialize(), uri: uri))
.buildRecommended(minFeeRate: 0));
// In a real scenario, the sender would send the request to
// the payjoin endpoint of the receiver, here we just keep
// the context for the last step.
final (_, ctx) = await requestContext.extractContextV1();

setState(() {
senderPsbt = psbt;
contextV1 = ctx;
});
},
child: Text(
"Create Sender request psbt using receiver pjUri",
style: GoogleFonts.manrope(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w800),
),
),
TextButton(
onPressed: () async {
final (provisionalProposal, contextV1) = await payJoinLibrary
.handlePjRequest(await senderPsbt.serialize(), pjUri,
(e) async {
final script = ScriptBuf(bytes: e);
final provisionalProposal =
await payJoinLibrary.handlePjRequest(
await senderPsbt.serialize(),
(e) async {
final script = ScriptBuf(bytes: e);

return (await receiver.getAddressInfo(script));
});
return (await receiver.getAddressInfo(script));
},
);
final unspent = await receiver.listUnspent();
// Select receiver payjoin inputs.
Map<int, common.OutPoint> candidateInputs = {
Expand All @@ -183,7 +201,7 @@ class _PayJoinState extends State<PayJoin> {
.tryPreservingPrivacy(candidateInputs: candidateInputs);
var selectedUtxo = unspent.firstWhere(
(i) =>
i.outpoint.txid.toString() == selectedOutpoint.txid &&
i.outpoint.txid == selectedOutpoint.txid &&
i.outpoint.vout == selectedOutpoint.vout,
orElse: () => throw Exception('UTXO not found'));
var txoToContribute = common.TxOut(
Expand All @@ -203,37 +221,39 @@ class _PayJoinState extends State<PayJoin> {
address: await newReceiverAddress.address.asString());
final payJoinProposal = await provisionalProposal
.finalizeProposal(processPsbt: (e) async {
debugPrint("\n Original receiver unsigned psbt: $e");
debugPrint("\n Receiver response unsigned psbt: $e");
return await (await receiver.signPsbt(
await PartiallySignedTransaction.fromString(e)))
.serialize();
});
final receiverPsbt = await payJoinProposal.psbt();
debugPrint("\n Original receiver psbt: $receiverPsbt");
final receiverProcessedPsbt = await contextV1.processResponse(
response: utf8.encode(receiverPsbt));
final senderProcessedPsbt = (await sender.signPsbt(
await PartiallySignedTransaction.fromString(
receiverProcessedPsbt)));
debugPrint("\n Receiver response psbt: $receiverPsbt");
setState(() {
processedAndFinalizedPsbt = senderProcessedPsbt;
receiverPsbtBase64 = receiverPsbt;
});
},
child: Text(
"Process and finalize receiver Pj request",
"Create Receiver response psbt",
style: GoogleFonts.manrope(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w800),
)),
TextButton(
onPressed: () async {
final res =
await sender.broadcastPsbt(processedAndFinalizedPsbt);
final processedReceiverResponsePsbt =
await contextV1.processResponse(
response: utf8.encode(receiverPsbtBase64));
final finalizedPsbt = (await sender.signPsbt(
await PartiallySignedTransaction.fromString(
processedReceiverResponsePsbt)));
debugPrint(
'Processed and finalized sender psbt: ${await finalizedPsbt.serialize()}');
final res = await sender.broadcastPsbt(finalizedPsbt);
debugPrint("Broadcast success: $res");
},
child: Text(
"Broadcast processed psbt",
"Process response and broadcast final Sender psbt",
style: GoogleFonts.manrope(
color: Colors.black,
fontSize: 14,
Expand Down
65 changes: 40 additions & 25 deletions example/lib/payjoin_library.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/cupertino.dart';
import 'package:payjoin_flutter/common.dart' as common;
import 'package:payjoin_flutter/receive/v1.dart' as v1;
import 'package:payjoin_flutter/receive/v2.dart';
import 'package:payjoin_flutter/send.dart' as send;
import 'package:payjoin_flutter/uri.dart' as pj_uri;

Expand All @@ -22,39 +24,52 @@ class PayJoinLibrary {
}
}

Future<(v1.ProvisionalProposal, send.ContextV1)> handlePjRequest(
String psbtBase64,
String uriStr,
Future<bool> Function(Uint8List) isOwned) async {
final uri = await pj_uri.Uri.fromString(uriStr);
final (req, cxt) = await (await (await send.RequestBuilder.fromPsbtAndUri(
psbtBase64: psbtBase64, uri: uri))
.buildWithAdditionalFee(
maxFeeContribution: 10000,
minFeeRate: 0,
clampFeeContribution: false))
.extractContextV1();
Future<v1.ProvisionalProposal> handlePjRequest(
String psbtBase64, Future<bool> Function(Uint8List) isOwned) async {
final body = utf8.encode(psbtBase64);

final headers = common.Headers(map: {
'content-type': 'text/plain',
'content-length': req.body.length.toString(),
'content-length': body.length.toString(),
});
final unchecked = await v1.UncheckedProposal.fromRequest(
body: req.body.toList(),
query: (await req.url.query())!,
headers: headers);
final provisionalProposal = await handleUnckedProposal(unchecked, isOwned);
return (provisionalProposal, cxt);
body: body.toList(), query: '', headers: headers);
final provisionalProposal =
await handleUncheckedProposal(unchecked, isOwned);
return provisionalProposal;
}

Future<v1.ProvisionalProposal> handleUnckedProposal(
Future<v1.ProvisionalProposal> handleUncheckedProposal(
v1.UncheckedProposal uncheckedProposal,
Future<bool> Function(Uint8List) isOwned) async {
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
var _ = await uncheckedProposal.extractTxToScheduleBroadcast();
final inputsOwned = await uncheckedProposal.checkBroadcastSuitability(
canBroadcast: (e) async {
return true;
});
// A consumer wallet has some manual interaction to initiate a payjoin, it
// is not a server that can receive a lot of requests without the user
// being aware of it. Therefore we say a consumer wallet app is an
// interactive receiver and an automatic payment processor is
// non-interactive.
//
// The way to check a proposal for these cases are different:
// - For an interactive receiver, you can just call
// `assumeInteractiveReceiver` as used here in the example code.
// - For a non-interactive receiver, you would extract the original tx
// with `extractTxToScheduleBroadcast` and check if it can be
// broadcasted in `checkBroadcastSuitability`. This way, if the sender
// doesn't complete the payjoin, you can still broadcast the original
// tx and get your funds. This protects against sender maliciousness of
// probing your utxo set amongst other things.

final inputsOwned = await uncheckedProposal.assumeInteractiveReceiver();
/*
// Non-interactive receiver example code:
final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast();
final inputsOwned = await uncheckedProposal.checkBroadcastSuitability(
canBroadcast: (e) async {
// Here you would check if the original tx is a valid tx that pays you
// and that can be broadcasted.
return true;
});
*/

// Receive Check 2: receiver can't sign for proposal inputs
final mixedInputScripts =
await inputsOwned.checkInputsNotOwned(isOwned: isOwned);
Expand Down
4 changes: 2 additions & 2 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "v0.31.2-dev.2"
resolved-ref: "18649436038282f379e9a4679656bcd084434ddd"
ref: main
resolved-ref: "821fe90d20c955e3cdfbfecea3b91fe24885552a"
url: "https://github.com/LtbLightning/bdk-flutter"
source: git
version: "0.31.2-dev.2"
Expand Down
Loading

0 comments on commit ead5878

Please sign in to comment.