Skip to content

Commit

Permalink
Merge pull request #12 from LtbLightning/bdk-integration-fix
Browse files Browse the repository at this point in the history
Bdk integration fix
  • Loading branch information
BitcoinZavior authored Jun 20, 2024
2 parents 5be3708 + 6556cfc commit 315ecf6
Show file tree
Hide file tree
Showing 26 changed files with 1,960 additions and 740 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
## [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`.
#### 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`.
66 changes: 50 additions & 16 deletions example/integration_test/bdk_full_cycle_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,95 @@ 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/receive/v1.dart' as v1;
import 'package:payjoin_flutter/send.dart' as send;
import 'package:payjoin_flutter/uri.dart' as pay_join_uri;
import 'package:payjoin_flutter_example/bdk_client.dart';
import 'package:payjoin_flutter_example/btc_client.dart';
import 'package:payjoin_flutter_example/payjoin_library.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('v1_to_v1', () {
setUp(() async {});
testWidgets('full_cycle', (WidgetTester tester) async {
final payJoinLib = PayJoinLibrary();
final btcClient = BtcClient("sender");
await btcClient.loadWallet();
final sender = BdkClient(
"puppy interest whip tonight dad never sudden response push zone pig patch");
"wpkh(tprv8ZgxMBicQKsPfNH1PykMg16TAvrZgoxDnxr3eorcbhvZxyZzStwFkvqCJegr8Gbwj3GQum8QpXQPh7DGkoobpTB7YbcnUeUSKRDyX2cNN9h/84'/1'/0'/0/*)#ey7hlgpn");
final receiver = BdkClient(
"cart super leaf clinic pistol plug replace close super tooth wealth usage");
"wpkh(tprv8ZgxMBicQKsPczV7D2zfMr7oUzHDhNPEuBUgrwRoWM3ijLRvhG87xYiqh9JFLPqojuhmqwMdo1oJzbe5GUpxCbDHnqyGhQa5Jg1Wt6rc9di/84'/1'/0'/0/*)#kdnuw5lq");
await sender.restoreWallet();
await receiver.restoreWallet();
// Receiver creates the payjoin URI
final pjReceiverAddress = (await receiver.getNewAddress()).address;
final pjSenderAddress = (await sender.getNewAddress()).address;
await btcClient.sendToAddress(await pjSenderAddress.asString(), 10);
await btcClient.sendToAddress(await pjReceiverAddress.asString(), 2);
await btcClient.sendToAddress(await pjSenderAddress.asString(), 1);
await btcClient.sendToAddress(await pjReceiverAddress.asString(), 1);
await btcClient.generate(11, await pjSenderAddress.asString());
await receiver.syncWallet();
await sender.syncWallet();
final pjUri = await payJoinLib.buildPjUri(
0.0083285, await pjReceiverAddress.asString());
// Sender create a funded PSBT (not broadcast) to address with amount given in the pjUri
debugPrint("Sender Balance: ${(await sender.getBalance()).toString()}");
final uri = await pay_join_uri.Uri.fromString(pjUri);
final uri = await pay_join_uri.Uri.fromString(
"${await pjReceiverAddress.toQrUri()}?amount=${0.0083285}&pj=https://example.com");
final address = await uri.address();
int amount = (((await uri.amount()) ?? 0) * 100000000).toInt();

final senderPsbt = (await sender.createPsbt(address, amount, 2000));
final senderPsbtBase64 = await senderPsbt.serialize();
debugPrint(
"\nOriginal sender psbt: $senderPsbtBase64",
);

// Receiver part
final (provisionalProposal, ctx) =
await payJoinLib.handlePjRequest(senderPsbtBase64, pjUri, (e) async {
final script = ScriptBuf(bytes: e);
return (await receiver.getAddressInfo(script));
final (req, ctx) = await (await (await send.RequestBuilder.fromPsbtAndUri(
psbtBase64: senderPsbtBase64, uri: uri))
.buildWithAdditionalFee(
maxFeeContribution: 10000,
minFeeRate: 0,
clampFeeContribution: false))
.extractContextV1();
final headers = common.Headers(map: {
'content-type': 'text/plain',
'content-length': req.body.length.toString(),
});
final uncheckedProposal = await v1.UncheckedProposal.fromRequest(
body: req.body.toList(),
query: (await req.url.query())!,
headers: headers);
// 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;
});
// Receive Check 2: receiver can't sign for proposal inputs
final mixedInputScripts =
await inputsOwned.checkInputsNotOwned(isOwned: (e) async {
return await receiver.getAddressInfo(ScriptBuf(bytes: e));
});

// Receive Check 3: receiver can't sign for proposal inputs
final seenInputs = await mixedInputScripts.checkNoMixedInputScripts();
// Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers.
final provisionalProposal =
await (await seenInputs.checkNoInputsSeenBefore(isKnown: (e) async {
return false;
}))
.identifyReceiverOutputs(isReceiverOutput: (e) async {
return await receiver.getAddressInfo(ScriptBuf(bytes: e));
});
final availableInputs = await receiver.listUnspent();
final unspent = await receiver.listUnspent();
// Select receiver payjoin inputs.
Map<int, common.OutPoint> candidateInputs = {
for (var input in availableInputs)
for (var input in unspent)
input.txout.value: common.OutPoint(
txid: input.outpoint.txid.toString(), vout: input.outpoint.vout)
};
final selectedOutpoint = await provisionalProposal.tryPreservingPrivacy(
candidateInputs: candidateInputs);
var selectedUtxo = availableInputs.firstWhere(
var selectedUtxo = unspent.firstWhere(
(i) =>
i.outpoint.txid.toString() == selectedOutpoint.txid &&
i.outpoint.vout == selectedOutpoint.vout,
Expand All @@ -81,6 +114,7 @@ void main() {
address: await receiverAddress.asString());
final payJoinProposal =
await provisionalProposal.finalizeProposal(processPsbt: (e) async {
debugPrint("\n Original receiver unsigned psbt: $e");
return await (await receiver
.signPsbt(await PartiallySignedTransaction.fromString(e)))
.serialize();
Expand Down
70 changes: 28 additions & 42 deletions example/lib/bdk_client.dart
Original file line number Diff line number Diff line change
@@ -1,60 +1,38 @@
import 'dart:io';

import 'package:bdk_flutter/bdk_flutter.dart';
import 'package:flutter/cupertino.dart';

class BdkClient {
// Bitcoin core credentials
String localEsploraUrl = 'http://0.0.0.0:30000';
// String localEsploraUrl = 'http://0.0.0.0:30000';

late Wallet wallet;
late Blockchain blockchain;
final String mnemonic;

Future<List<Descriptor>> getDescriptors(String mnemonicStr) async {
final descriptors = <Descriptor>[];
try {
for (var e in [KeychainKind.externalChain, KeychainKind.internalChain]) {
final mnemonic = await Mnemonic.fromString(mnemonicStr);
final descriptorSecretKey = await DescriptorSecretKey.create(
network: Network.regtest,
mnemonic: mnemonic,
);
final descriptor = await Descriptor.newBip86(
secretKey: descriptorSecretKey,
network: Network.regtest,
keychain: e);
descriptors.add(descriptor);
}
return descriptors;
} on Exception {
rethrow;
}
}
final String descriptor;

BdkClient(this.mnemonic);
BdkClient(this.descriptor);

Future<void> restoreWallet() async {
try {
final descriptors = await getDescriptors(mnemonic);
await initBlockchain();
wallet = await Wallet.create(
descriptor: descriptors[0],
changeDescriptor: descriptors[1],
network: Network.regtest,
descriptor: await Descriptor.create(
descriptor: descriptor, network: Network.signet),
network: Network.signet,
databaseConfig: const DatabaseConfig.memory());
debugPrint(await (await getNewAddress()).address.asString());
} on Exception {
rethrow;
}
}

Future<void> initBlockchain() async {
String esploraUrl =
Platform.isAndroid ? 'http://10.0.2.2:30000' : localEsploraUrl;
// String esploraUrl =
// Platform.isAndroid ? 'http://10.0.2.2:30000' : localEsploraUrl;
try {
blockchain = await Blockchain.create(
config: BlockchainConfig.esplora(
config: EsploraConfig(baseUrl: esploraUrl, stopGap: 10)));
config: const BlockchainConfig.esplora(
config: EsploraConfig(
baseUrl: "https://mutinynet.com/api", stopGap: 10)));
} on Exception {
rethrow;
}
Expand All @@ -66,24 +44,32 @@ class BdkClient {
return res;
}

Future<List<TransactionDetails>> listTransactions() async {
final res = await wallet.listTransactions(includeRaw: true);
return res;
}

Future<PartiallySignedTransaction> signPsbt(
PartiallySignedTransaction psbt) async {
final isFinalized = await wallet.sign(psbt: psbt);
if (isFinalized) {
return psbt;
} else {
throw Exception("PartiallySignedTransaction not finalized!");
}
await wallet.sign(
psbt: psbt,
signOptions: const SignOptions(
trustWitnessUtxo: true,
allowAllSighashes: false,
removePartialSigs: true,
tryFinalize: true,
signWithTapInternalKey: true,
allowGrinding: false));
return psbt;
}

Future<PartiallySignedTransaction> createPsbt(
String addressStr, int amount, int fee) async {
try {
final txBuilder = TxBuilder();
final address =
await Address.fromString(s: addressStr, network: Network.regtest);
await Address.fromString(s: addressStr, network: Network.signet);
final script = await address.scriptPubkey();

final (psbt, _) = await txBuilder
.addRecipient(script, amount)
.feeAbsolute(fee)
Expand Down
Loading

0 comments on commit 315ecf6

Please sign in to comment.