Skip to content

Commit a276d2d

Browse files
committed
DRAFT Demo payjoin e2e
This is still broken because as far as I can tell the signer will not sign with a missing RedeemScript / WitnessScript. And those are missing.
1 parent d5e73df commit a276d2d

File tree

7 files changed

+221
-139
lines changed

7 files changed

+221
-139
lines changed

.idea/workspace.xml

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/_pkg/payjoin/manager.dart

Lines changed: 133 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,11 @@ Future<String?> pollSender(Sender sender) async {
158158
V2PostContext postReqCtx;
159159
try {
160160
final result = await sender.extractV2(ohttpProxyUrl: ohttpProxyUrl);
161+
print('extracted v2');
161162
postReq = result.$1;
162163
postReqCtx = result.$2;
163164
} catch (e) {
165+
print('failed to extract v2. err: $e');
164166
try {
165167
final (req, v1Ctx) = await sender.extractV1();
166168
print('Posting Original PSBT Payload request...');
@@ -187,28 +189,39 @@ Future<String?> pollSender(Sender sender) async {
187189
},
188190
body: postReq.body,
189191
);
190-
final getCtx = await postReqCtx.processResponse(
191-
response: postRes.bodyBytes,
192-
);
193-
String? proposalPsbt;
194-
while (true) {
195-
final (getRequest, getReqCtx) = await getCtx.extractReq(
196-
ohttpRelay: ohttpProxyUrl,
197-
);
198-
final getRes = await http.post(
199-
Uri.parse(getRequest.url.asString()),
200-
headers: {
201-
'Content-Type': getRequest.contentType,
202-
},
203-
body: getRequest.body,
204-
);
205-
proposalPsbt = await getCtx.processResponse(
206-
response: getRes.bodyBytes,
207-
ohttpCtx: getReqCtx,
192+
try {
193+
print('got post response');
194+
final getCtx = await postReqCtx.processResponse(
195+
response: postRes.bodyBytes,
208196
);
209-
break;
197+
print('processed post response');
198+
String? proposalPsbt;
199+
while (true) {
200+
print('extracting get request');
201+
final (getRequest, getReqCtx) = await getCtx.extractReq(
202+
ohttpRelay: ohttpProxyUrl,
203+
);
204+
print('got get request');
205+
final getRes = await http.post(
206+
Uri.parse(getRequest.url.asString()),
207+
headers: {
208+
'Content-Type': getRequest.contentType,
209+
},
210+
body: getRequest.body,
211+
);
212+
print('got get response');
213+
proposalPsbt = await getCtx.processResponse(
214+
response: getRes.bodyBytes,
215+
ohttpCtx: getReqCtx,
216+
);
217+
print('processed get response');
218+
break;
219+
}
220+
return proposalPsbt;
221+
} catch (e) {
222+
print('err: $e');
223+
throw Exception('Error occurred while polling sender');
210224
}
211-
return proposalPsbt;
212225
}
213226

214227
Future<bool> addressExistsInWallet(String address, bdk.Wallet bdkWallet) async {
@@ -343,24 +356,26 @@ Future<void> _isolateSender(List<dynamic> args) async {
343356

344357
// SIGN AND BROADCAST ---------------------------
345358
try {
359+
print('signing');
346360
final psbtStruct =
347361
await bdk.PartiallySignedTransaction.fromString(proposal!);
348362
await wallet.sign(
349363
psbt: psbtStruct,
350364
signOptions: const bdk.SignOptions(
351-
trustWitnessUtxo: false,
365+
trustWitnessUtxo: true,
352366
allowAllSighashes: false,
353367
removePartialSigs: true,
354368
tryFinalize: true,
355369
signWithTapInternalKey: false,
356370
allowGrinding: true,
357371
),
358372
);
359-
373+
print('signed');
360374
final finalizedTx = psbtStruct.extractTx();
361375
final signedPsbt = psbtStruct.toString();
362376

363377
//Broadcast the transaction
378+
print('broadcasting');
364379
final broadcastedTx =
365380
await blockchain.broadcast(transaction: finalizedTx);
366381
print('Broadcasted transaction: $broadcastedTx');
@@ -430,17 +445,22 @@ void _isolateReceiver(List<dynamic> args) async {
430445
while (unchecked_proposal == null) {
431446
try {
432447
final (req, context) = await receiver.extractReq();
448+
print('making request');
433449
final ohttpResponse = await http.post(
434450
Uri.parse(req.url.asString()),
435451
headers: {
436452
'Content-Type': req.contentType,
437453
},
438454
body: req.body,
439455
);
456+
print('got unchecked response');
440457
unchecked_proposal = await receiver.processRes(
441458
body: ohttpResponse.bodyBytes,
442459
ctx: context,
443460
);
461+
if (unchecked_proposal != null) {
462+
break;
463+
}
444464
} catch (e) {
445465
sendPort.send(
446466
Err(
@@ -452,24 +472,24 @@ void _isolateReceiver(List<dynamic> args) async {
452472
break;
453473
}
454474
}
455-
if (unchecked_proposal == null) {
456-
print('FAILED TO GET PROPOSAL');
457-
}
458475
final payjoin_proposal = await processPayjoinProposal(
459476
unchecked_proposal!,
460477
isTestnet,
461478
wallet,
462479
blockchain,
463480
);
481+
print('payjoin proposal: $payjoin_proposal');
464482
try {
465483
final (postReq, ohttpCtx) = await payjoin_proposal.extractV2Req();
484+
print('extracted v2 req');
466485
final postRes = await http.post(
467486
Uri.parse(postReq.url.asString()),
468487
headers: {
469488
'Content-Type': postReq.contentType,
470489
},
471490
body: postReq.body,
472491
);
492+
print('processed res');
473493
await payjoin_proposal.processRes(
474494
res: postRes.bodyBytes,
475495
ohttpContext: ohttpCtx,
@@ -498,92 +518,104 @@ Future<PayjoinProposal> processPayjoinProposal(
498518
final fallbackTx = await proposal.extractTxToScheduleBroadcast();
499519
print('fallback tx (broadcast this if payjoin fails): $fallbackTx');
500520

501-
// Receive Check 1: can broadcast
502-
final pj1 = await proposal.assumeInteractiveReceiver();
503-
// Receive Check 2: original PSBT has no receiver-owned inputs
504-
final pj2 = await pj1.checkInputsNotOwned(
505-
isOwned: (inputScript) async {
506-
final address = await bdk.Address.fromScript(
507-
script: bdk.ScriptBuf(bytes: inputScript),
508-
network: isTestnet ? bdk.Network.testnet : bdk.Network.bitcoin,
509-
);
510-
return await addressExistsInWallet(address.toString(), wallet);
511-
},
512-
);
513-
// Receive Check 3: sender inputs have not been seen before (prevent probing attacks)
514-
final pj3 = await pj2.checkNoInputsSeenBefore(
515-
isKnown: (input) {
516-
// TODO: keep track of seen inputs in hive storage?
517-
return false;
518-
},
519-
);
521+
try {
522+
// Receive Check 1: can broadcast
523+
print('check1');
524+
final pj1 = await proposal.assumeInteractiveReceiver();
525+
print('check2');
526+
// Receive Check 2: original PSBT has no receiver-owned inputs
527+
final pj2 = await pj1.checkInputsNotOwned(
528+
isOwned: (inputScript) async {
529+
final address = await bdk.Address.fromScript(
530+
script: bdk.ScriptBuf(bytes: inputScript),
531+
network: isTestnet ? bdk.Network.testnet : bdk.Network.bitcoin,
532+
);
533+
return await addressExistsInWallet(address.toString(), wallet);
534+
},
535+
);
536+
// Receive Check 3: sender inputs have not been seen before (prevent probing attacks)
537+
print('check3');
538+
final pj3 = await pj2.checkNoInputsSeenBefore(
539+
isKnown: (input) {
540+
// TODO: keep track of seen inputs in hive storage?
541+
return false;
542+
},
543+
);
520544

521-
// Identify receiver outputs
522-
final pj4 = await pj3.identifyReceiverOutputs(
523-
isReceiverOutput: (outputScript) async {
524-
final address = await bdk.Address.fromScript(
525-
script: bdk.ScriptBuf(bytes: outputScript),
526-
network: isTestnet ? bdk.Network.testnet : bdk.Network.bitcoin,
527-
);
528-
return await addressExistsInWallet(address.toString(), wallet);
529-
},
530-
);
531-
final pj5 = await pj4.commitOutputs();
545+
// Identify receiver outputs
546+
print('check4');
547+
final pj4 = await pj3.identifyReceiverOutputs(
548+
isReceiverOutput: (outputScript) async {
549+
final address = await bdk.Address.fromScript(
550+
script: bdk.ScriptBuf(bytes: outputScript),
551+
network: isTestnet ? bdk.Network.testnet : bdk.Network.bitcoin,
552+
);
553+
return await addressExistsInWallet(address.toString(), wallet);
554+
},
555+
);
556+
final pj5 = await pj4.commitOutputs();
532557

533-
// Contribute receiver inputs
534-
final utxos = await getSpendableUtxosFromBdkWallet(
535-
wallet,
536-
isTestnet ? bdk.Network.testnet : bdk.Network.bitcoin,
537-
);
538-
final inputs = await Future.wait(
539-
utxos.map((utxo) => inputPairFromUtxo(utxo, isTestnet)),
540-
);
541-
final selected_utxo = await pj5.tryPreservingPrivacy(
542-
candidateInputs: inputs,
543-
);
544-
final pj6 = await pj5.contributeInputs(replacementInputs: [selected_utxo]);
545-
final pj7 = await pj6.commitInputs();
546-
547-
// Finalize proposal
548-
final payjoin_proposal = await pj7.finalizeProposal(
549-
processPsbt: (String psbt) async {
550-
// TODO: sign PSBT
551-
final psbtStruct = await bdk.PartiallySignedTransaction.fromString(psbt);
552-
await wallet.sign(
553-
psbt: psbtStruct,
554-
signOptions: const bdk.SignOptions(
555-
trustWitnessUtxo: false,
556-
allowAllSighashes: false,
557-
removePartialSigs: true,
558-
tryFinalize: true,
559-
signWithTapInternalKey: false,
560-
allowGrinding: true,
561-
),
562-
);
563-
return psbt;
564-
},
565-
maxFeeRateSatPerVb: BigInt.zero,
566-
);
567-
return payjoin_proposal;
558+
// Contribute receiver inputs
559+
print('get spendable utxos');
560+
final unspent = wallet.listUnspent();
561+
final inputs = await Future.wait(
562+
unspent.map((unspent) => inputPairFromUtxo(unspent, isTestnet)),
563+
);
564+
print('selected utxo');
565+
final selected_utxo = await pj5.tryPreservingPrivacy(
566+
candidateInputs: inputs,
567+
);
568+
print('contribute inputs');
569+
final pj6 = await pj5.contributeInputs(replacementInputs: [selected_utxo]);
570+
print('commit inputs');
571+
final pj7 = await pj6.commitInputs();
572+
573+
// Finalize proposal
574+
print('finalize proposal');
575+
final payjoin_proposal = await pj7.finalizeProposal(
576+
processPsbt: (String psbt) async {
577+
print('finalizeProposal psbt $psbt');
578+
// TODO: sign PSBT
579+
final psbtStruct =
580+
await bdk.PartiallySignedTransaction.fromString(psbt);
581+
print('unsigned psbtStruct $psbtStruct');
582+
final signed = await wallet.sign(
583+
psbt: psbtStruct,
584+
signOptions: const bdk.SignOptions(
585+
trustWitnessUtxo: false,
586+
allowAllSighashes: false,
587+
removePartialSigs: true,
588+
tryFinalize: true,
589+
signWithTapInternalKey: true,
590+
allowGrinding: true,
591+
),
592+
);
593+
print('signed $signed');
594+
final signedPsbt = psbtStruct.toString();
595+
print('signedPsbt $signedPsbt');
596+
return signedPsbt;
597+
},
598+
maxFeeRateSatPerVb: BigInt.from(10000),
599+
);
600+
return payjoin_proposal;
601+
} catch (e) {
602+
print('err: $e');
603+
throw Exception('Error occurred while finalizing proposal');
604+
}
568605
}
569606

570-
Future<InputPair> inputPairFromUtxo(UTXO utxo, bool isTestnet) async {
571-
// TODO: this seems like a roundabout way of getting the script pubkey
572-
final address = await bdk.Address.fromString(
573-
s: utxo.address.address,
574-
network: isTestnet ? bdk.Network.testnet : bdk.Network.bitcoin,
575-
);
576-
final spk = address.scriptPubkey().bytes;
607+
Future<InputPair> inputPairFromUtxo(bdk.LocalUtxo utxo, bool isTestnet) async {
577608
final psbtin = PsbtInput(
609+
// We should be able to merge these bdk & payjoin rust-bitcoin types with bitcoin-ffi eventually
578610
witnessUtxo: TxOut(
579-
value: BigInt.from(utxo.value),
580-
scriptPubkey: spk,
611+
value: utxo.txout.value,
612+
scriptPubkey: utxo.txout.scriptPubkey.bytes,
581613
),
582614
// TODO: redeem script/witness script?
583615
);
584-
// TODO: perhaps TxIn.default() should be exposed in payjoin_flutter api
585616
final txin = TxIn(
586-
previousOutput: OutPoint(txid: utxo.txid, vout: utxo.txIndex),
617+
previousOutput:
618+
OutPoint(txid: utxo.outpoint.txid, vout: utxo.outpoint.vout),
587619
scriptSig: await Script.newInstance(rawOutputScript: []),
588620
sequence: 0xFFFFFFFF,
589621
witness: [],

0 commit comments

Comments
 (0)