From 9e62b43d2d7e159849b6637eb266d3c8684d8aef Mon Sep 17 00:00:00 2001 From: J0J0XMR Date: Tue, 10 Dec 2024 09:07:22 -0600 Subject: [PATCH] API changes for v0.21 --- cw_bitcoin/lib/bitcoin_payjoin.dart | 490 ++++++++---------- cw_bitcoin/pubspec.lock | 6 +- cw_bitcoin/pubspec.yaml | 7 +- cw_core/pubspec.lock | 4 +- lib/bitcoin/bitcoin.dart | 23 +- lib/bitcoin/cw_bitcoin.dart | 34 +- .../wallet_address_list_view_model.dart | 63 ++- 7 files changed, 296 insertions(+), 331 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_payjoin.dart b/cw_bitcoin/lib/bitcoin_payjoin.dart index ec2195d2b9..8faa8b148d 100644 --- a/cw_bitcoin/lib/bitcoin_payjoin.dart +++ b/cw_bitcoin/lib/bitcoin_payjoin.dart @@ -1,27 +1,29 @@ import 'dart:convert'; -import 'dart:typed_data'; +import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_bitcoin/psbt_signer.dart'; +import 'package:flutter/foundation.dart'; import 'package:ledger_bitcoin/psbt.dart'; import 'package:payjoin_flutter/common.dart'; -import 'package:payjoin_flutter/receive/v2.dart' as v2; +import 'package:payjoin_flutter/receive.dart'; +import 'package:payjoin_flutter/uri.dart' as pjuri; import 'package:payjoin_flutter/uri.dart' as pj_uri; import 'package:http/http.dart' as http; -import 'package:payjoin_flutter/src/generated/utils/types.dart' as types; import 'package:payjoin_flutter/send.dart' as send; import 'pending_bitcoin_transaction.dart'; +import 'package:payjoin_flutter/bitcoin_ffi.dart' as script; -export 'package:payjoin_flutter/receive/v2.dart' - show ActiveSession, UncheckedProposal, PayjoinProposal; +export 'package:payjoin_flutter/receive.dart' + show Receiver, UncheckedProposal, PayjoinProposal; export 'package:payjoin_flutter/uri.dart' show Uri; -export 'package:payjoin_flutter/send.dart' show RequestContext; +export 'package:payjoin_flutter/send.dart' show Sender; export 'package:payjoin_flutter/src/exceptions.dart' show PayjoinException; @@ -42,8 +44,7 @@ class BitcoinPayjoin { } static const pjUrl = "https://payjo.in"; - static const ohttpRelay = "https://pj.bobspacebkk.com"; - static const payjoinDirectory = "https://payjo.in"; + static const relayUrl = "https://pj.bobspacebkk.com"; static const v2ContentType = "message/ohttp-req"; Network get testnet => Network.testnet; @@ -59,26 +60,39 @@ class BitcoinPayjoin { int? amount, required String address, required Network network, - required int expireAfter, + required BigInt expireAfter, }) async { - print( - '[+] BitcoinPayjoin || buildV2PjStr => address: $address \n amount: $amount \n network: $network'); - // Start a Payjoin receive session with the given parameters - final session = await _startV2ReceiveSession( + debugPrint( + '[+] BITCOINPAYJOIN => buildV2PjStr - address: $address \n amount: $amount \n network: $network'); + + final _payjoinDirectory = await pjuri.Url.fromStr(pjUrl); + final _ohttpRelay = await pjuri.Url.fromStr(relayUrl); + + final _ohttpKeys = await pjuri.fetchOhttpKeys( + ohttpRelay: _ohttpRelay, + payjoinDirectory: _payjoinDirectory, + ); + + debugPrint( + '[+] BITCOINPAYJOIN => buildV2PjStr - OHTTP KEYS FETCHED ${_ohttpKeys.toString()}'); + + final receiver = await Receiver.create( address: address, network: network, - expireAfter: expireAfter, + directory: _payjoinDirectory, + ohttpKeys: _ohttpKeys, + ohttpRelay: _ohttpRelay, + expireAfter: expireAfter, // 5 minutes ); String pjUriStr; - // Get the Payjoin URI builder from the session - final pjUriBuilder = session.pjUriBuilder(); + final pjUriBuilder = receiver.pjUriBuilder(); - // Build the URI if (amount != null) { + // ignore final pjUriBuilderWithAmount = - pjUriBuilder.amount(amount: BigInt.from(amount)); + pjUriBuilder.amountSats(amount: BigInt.from(amount)); final pjUri = pjUriBuilderWithAmount.build(); pjUriStr = pjUri.asString(); } else { @@ -86,241 +100,152 @@ class BitcoinPayjoin { pjUriStr = pjUri.asString(); } - return {'pjUri': pjUriStr, 'session': session}; + return {'pjUri': pjUriStr, 'session': receiver}; } - Future _startV2ReceiveSession({ - required String address, - required Network network, - required int expireAfter, - }) async { - // Convert the OHTTP relay URL string to a Url object - final ohttpRelayUrl = await pj_uri.Url.fromStr(ohttpRelay); - // Convert the Payjoin directory URL string to a Url object - final payjoinDirectoryUrl = await pj_uri.Url.fromStr(payjoinDirectory); - - // Fetch OHTTP keys using the relay and directory URLs - pj_uri.OhttpKeys ohttpKeys = await pj_uri.fetchOhttpKeys( - ohttpRelay: ohttpRelayUrl, - payjoinDirectory: payjoinDirectoryUrl, - ); - - // Initialize a Payjoin session with the provided parameters - final session = await v2.SessionInitializer.create( - address: address, - ohttpRelay: ohttpRelayUrl, - directory: payjoinDirectoryUrl, - ohttpKeys: ohttpKeys, - network: network, - expireAfter: BigInt.from(expireAfter), - ); - - // Extract the Payjoin request and context from the session - final extractReq = await session.extractReq(); - - // Send the Payjoin request to the server using HTTP POST - final response = await http.post( - Uri.parse(extractReq.$1.url.asString()), - body: extractReq.$1.body, - headers: { - 'Content-Type': v2ContentType, - }, - ); - - // Process the server's response to activate the Payjoin session - final activeSession = await session.processRes( - body: response.bodyBytes, - ctx: extractReq.$2, - ); - - return activeSession; - } + Future handleReceiverSession(Receiver session) async { + debugPrint("BITCOINPAYJOIN => pollV2Request"); + try { + final httpClient = HttpClient(); + UncheckedProposal? proposal; - Future pollV2Request(v2.ActiveSession session) async { - // Start an infinite loop to continuously poll for requests - while (true) { - try { - // Extract the request and context from the session. This may also throw a timeout error. + while (proposal == null) { final extractReq = await session.extractReq(); - - // Send the extracted request to the server using HTTP POST - final originalPsbt = await http.post( - Uri.parse(extractReq.$1.url.asString()), - body: extractReq.$1.body, - headers: { - 'Content-Type': v2ContentType, - }, - ); - - // Process the server's response to get an unchecked proposal - final uncheckedProposal = await session.processRes( - body: originalPsbt.bodyBytes, - ctx: extractReq.$2, - ); - - // If a valid unchecked proposal is received, return it and end the loop - if (uncheckedProposal != null) { - return uncheckedProposal; - } - - // Wait for 2 seconds before retrying to avoid overloading the server - await Future.delayed(const Duration(seconds: 2)); - } catch (e) { - // If an error occurs (like a timeout), rethrow the error to be handled by the caller - rethrow; + final request = extractReq.$1; + final clientResponse = extractReq.$2; + + final url = Uri.parse(request.url.asString()); + final httpRequest = await httpClient.postUrl(url); + httpRequest.headers.set('Content-Type', request.contentType); + httpRequest.add(request.body); + + final response = await httpRequest.close(); + final responseBody = await response.fold>( + [], (previous, element) => previous..addAll(element)); + final uint8Response = Uint8List.fromList(responseBody); + proposal = + await session.processRes(body: uint8Response, ctx: clientResponse); } + + return proposal; + } catch (e, st) { + debugPrint( + '[!] BITCOINPAYJOINERROR => buildV2PjStr - Error: ${e.toString()}, Stacktrace: $st'); + rethrow; } } - Future> handleV2Request({ - required v2.UncheckedProposal uncheckedProposal, + Future extractOriginalTransaction(UncheckedProposal proposal) async { + final originalTxBytes = await proposal.extractTxToScheduleBroadcast(); + final originalTx = getTxIdFromTxBytes(originalTxBytes); + return originalTx; + } + + Future processProposal({ + required UncheckedProposal proposal, required Object receiverWallet, }) async { - // Call the _handleV2Request function to process the unchecked proposal and - //get the original transaction and a Payjoin proposal - final request = await _handleV2Request( - proposal: uncheckedProposal, - receiverWallet: receiverWallet, - ); + final bitcoinWallet = receiverWallet as ElectrumWallet; - final originalTx = request['originalTx']; - final payjoinProposal = request['payjoinProposal'] as v2.PayjoinProposal; + final maybeInputsOwned = await proposal.assumeInteractiveReceiver(); - final extractReq = await payjoinProposal.extractV2Req(); + final maybeInputsSeen = await maybeInputsOwned.checkInputsNotOwned( + isOwned: (outpoint) async => + false // TODO Implement actual ownership check + ); - // Send the extracted V2 request to the server - final res = await http.post( - Uri.parse(extractReq.$1.url.asString()), - body: extractReq.$1.body, - headers: { - 'Content-Type': v2ContentType, - }, - ); + final outputsUnknown = await maybeInputsSeen.checkNoInputsSeenBefore( + isKnown: (outpoint) async => false // TODO Implement actual seen check + ); - // Process the server's response to update the Payjoin proposal with the result - await payjoinProposal.processRes( - res: res.bodyBytes, - ohttpContext: extractReq.$2, - ); + final wantsOutputs = await outputsUnknown.identifyReceiverOutputs( + isReceiverOutput: (script) async { + return receiverWallet.isMine(Script(script: script)); + }); - return {'originalTx': originalTx, 'payjoinProposal': payjoinProposal}; - } + var wantsInputs = await wantsOutputs.commitOutputs(); - String getTxIdFromTxBytes(Uint8List txBytes) { - final originalTx = BtcTransaction.fromRaw(BytesUtils.toHexString(txBytes)); - return originalTx.txId(); - } + // final unspent = receiverWallet.listUnspent(); + final List unspent = bitcoinWallet.unspentCoins; - Future> _handleV2Request({ - required v2.UncheckedProposal proposal, - required Object receiverWallet, - }) async { - final bitcoinWallet = receiverWallet as ElectrumWallet; - try { - // Extract the transaction bytes from the proposal to schedule it for broadcasting - final originalTxBytes = await proposal.extractTxToScheduleBroadcast(); - - // Convert the extracted bytes into a Bitcoin Transaction object - final originalTx = getTxIdFromTxBytes(originalTxBytes); - - // Check the suitability of the proposal for broadcasting - final ownedInputs = - await proposal.checkBroadcastSuitability(canBroadcast: (e) async { - return true; // Assume the transaction is suitable for broadcasting - }); - - // Ensure no mixed input scripts (i.e., inputs not owned by the wallet) - final mixedInputScripts = await ownedInputs.checkInputsNotOwned( - isOwned: (i) => _isOwned(i, bitcoinWallet)); - - // Check that no previously seen inputs are being reused in the transaction - final seenInputs = await mixedInputScripts.checkNoMixedInputScripts(); - - // Identify which outputs belong to the receiver's wallet - final payjoin = - await (await seenInputs.checkNoInputsSeenBefore(isKnown: (e) async { - return false; // Assume no inputs have been seen before - })) - .identifyReceiverOutputs( - isReceiverOutput: (i) => _isOwned(i, bitcoinWallet), - ); + List candidateInputs = []; - // TODO: List all unspent outputs (UTXOs) available in the receiver's wallet - final availableInputs = bitcoinWallet.unspentCoins; - - // Create a map of candidate inputs with their corresponding outpoints - Map candidateInputs = { - for (var input in availableInputs) - BigInt.from(input.value): types.OutPoint( - txid: input.hash.toString(), - vout: input.vout, - ) - }; - - // Try to select an outpoint that preserves the privacy of the transaction - final selectedOutpoint = await payjoin.tryPreservingPrivacy( - candidateInputs: candidateInputs, + for (BitcoinUnspent input in unspent) { + final scriptBytes = BitcoinBaseAddress.fromString(input.address) + .toScriptPubKey() + .toBytes(); + final txout = TxOut( + value: BigInt.from(input.value), + scriptPubkey: Uint8List.fromList(scriptBytes), ); - // Find the selected UTXO from the available inputs - var selectedUtxo = availableInputs.firstWhere( - (i) => - i.hash == selectedOutpoint.txid && - i.vout == selectedOutpoint.vout, - orElse: () => throw Exception('UTXO not found')); - - // Create a TxOut object representing the selected UTXO's output - var scriptList = P2trAddress.fromAddress( - address: selectedUtxo.address, - network: bitcoinWallet.network, - ).toScriptPubKey().script as List; - - var txoToContribute = types.TxOut( - value: BigInt.from(selectedUtxo.value), - scriptPubkey: Uint8List.fromList(scriptList), - ); + final psbtin = PsbtInput( + witnessUtxo: txout, redeemScript: null, witnessScript: null); - // Create an OutPoint object representing the selected UTXO's outpoint - var outpointToContribute = types.OutPoint( - txid: selectedUtxo.hash.toString(), - vout: selectedUtxo.vout, - ); + final previousOutput = OutPoint(txid: input.hash, vout: input.vout); - // Contribute the selected witness input to the Payjoin transaction - await payjoin.contributeWitnessInput( - txo: txoToContribute, - outpoint: outpointToContribute, + final txin = TxIn( + previousOutput: previousOutput, + scriptSig: await script.Script.newInstance(rawOutputScript: []), + witness: [], + sequence: 0, ); - // Finalize the Payjoin proposal by processing the PSBT (Partially Signed Bitcoin Transaction) - final payjoinProposal = await payjoin.finalizeProposal( - processPsbt: (i) => _processPsbt(i, bitcoinWallet)); + final ip = await InputPair.newInstance(txin, psbtin); - // Return the original transaction and the finalized Payjoin proposal - return {'originalTx': originalTx, 'payjoinProposal': payjoinProposal}; - } on Exception catch (e) { - // If an error occurs, log the error and rethrow it - print('[!] bitcoin_payjoin.dart || _handleV2Request() => e: $e'); - rethrow; + candidateInputs.add(ip); } + + final inputPair = await wantsInputs.tryPreservingPrivacy( + candidateInputs: candidateInputs); + + wantsInputs = + await wantsInputs.contributeInputs(replacementInputs: [inputPair]); + + final provisionalProposal = await wantsInputs.commitInputs(); + + final finalProposal = await provisionalProposal.finalizeProposal( + processPsbt: (i) => _processPsbt(i, receiverWallet), + maxFeeRateSatPerVb: BigInt.from(25)); + + return finalProposal; } - Future _isOwned(Uint8List bytes, ElectrumWallet wallet) async { - // Create a ScriptBuf object from the provided byte data - print('[+] BitcoinPayjoin || _isOwned'); - final script = Script(script: bytes); + Future sendFinalProposal(PayjoinProposal finalProposal) async { + final req = await finalProposal.extractV2Req(); + final proposalReq = req.$1; + final proposalCtx = req.$2; + + final httpClient = HttpClient(); + final httpRequest = await httpClient.postUrl( + Uri.parse(proposalReq.url.asString()), + ); + httpRequest.headers.set('content-type', 'message/ohttp-req'); + httpRequest.add(proposalReq.body); + + final response = await httpRequest.close(); + final responseBody = await response.fold>( + [], + (previous, element) => previous..addAll(element), + ); + await finalProposal.processRes( + res: responseBody, ohttpContext: proposalCtx); - final isMine = wallet.isMine(script); - print('[+] BitcoinPayjoin || _isOwned => isMine: $isMine'); + final proposalPsbt = await finalProposal.psbt(); - return isMine; + return await getTxIdFromPsbt(proposalPsbt); + } + + String getTxIdFromTxBytes(Uint8List txBytes) { + final originalTx = BtcTransaction.fromRaw(BytesUtils.toHexString(txBytes)); + return originalTx.txId(); } Future _processPsbt( - String preProcessed, ElectrumWallet wallet) async { + String preProcessed, + ElectrumWallet wallet, + ) async { final signedPsbt = wallet.signPsbt(preProcessed); - - // Return the string representation of the signed PSBT return signedPsbt; } @@ -331,7 +256,6 @@ class BitcoinPayjoin { final txId = hex.encode(revert.reversed.toList()); return txId; } - /* +-------------------------+ | Sender starts from here | @@ -340,9 +264,10 @@ class BitcoinPayjoin { Future stringToPjUri(String pj) async { try { - return await pj_uri.Uri.fromStr(pj); - } catch (e) { - print('[!] BitcoinPayjoin || stringToPjUri() => e: $e'); + return await pjuri.Uri.fromStr(pj); + } catch (e, st) { + debugPrint( + '[!] BITCOINPAYJOINERROR => stringToPjUri - Error: ${e.toString()}, Stacktrace: $st'); return null; } } @@ -358,83 +283,106 @@ class BitcoinPayjoin { final uri = pjUri as pj_uri.Uri; final bitcoinWallet = wallet as ElectrumWallet; - // final tx = await bitcoinWallet.createPayjoinTransaction( - // credentials, - // pjBtcAddress: uri.address(), - // ); - final psbtv2 = await bitcoinWallet.createPayjoinTransaction( credentials, pjBtcAddress: uri.address(), ); - print( - '[+] BitcoinPayjoin | buildOriginalPsbt => psbtv2: ${base64Encode(psbtv2.serialize())}'); + debugPrint( + '[+] BITCOINPAYJOIN => buildOriginalPsbt - psbtv2: ${base64Encode(psbtv2.serialize())}'); final psbtv0 = base64Encode(psbtv2.asPsbtV0()); - print('[+] BitcoinPayjoin | buildOriginalPsbt => psbtv0: $psbtv0'); + debugPrint('[+] BITCOINPAYJOIN => buildOriginalPsbt - psbtv0: $psbtv0'); return psbtv0; } - Future buildPayjoinRequest( + Future buildPayjoinRequest( String originalPsbt, dynamic pjUri, int fee, ) async { final uri = pjUri as pj_uri.Uri; - // Create a RequestBuilder from the given original PSBT and Payjoin URI - // The Payjoin URI is checked for Payjoin support before proceeding - final requestBuilder = await send.RequestBuilder.fromPsbtAndUri( + final senderBuilder = await send.SenderBuilder.fromPsbtAndUri( psbtBase64: originalPsbt, pjUri: uri.checkPjSupported(), ); - // Build a RequestContext using the RequestBuilder with a minimum fee rate - // Here, a minimum fee rate of 1 satoshi per byte is set - final requestContext = await requestBuilder.buildRecommended( - minFeeRate: BigInt.from(1), + final sender = await senderBuilder.buildRecommended( + minFeeRate: BigInt.from(250), ); - return requestContext; + return sender; } Future requestAndPollV2Proposal( - send.RequestContext requestContext, + send.Sender sender, ) async { - // Keep polling for a V2 proposal - while (true) { - try { - // Extract the V2 request and context using the requestContext - // The extraction includes specifying the OHTTP proxy URL for Payjoin - final extractV2 = await requestContext.extractV2( - ohttpProxyUrl: await pj_uri.Url.fromStr(payjoinDirectory), - ); + debugPrint( + '[+] BITCOINPAYJOIN => requestAndPollV2Proposal - Sending V2 Proposal Request...'); - // Post the extracted request to the server - final response = await http.post( - Uri.parse(extractV2.$1.url.asString()), - headers: { - 'Content-Type': v2ContentType, - }, - body: extractV2.$1.body, - ); - - // Process the response from the server using the context - final checkedPayjoinProposal = - await extractV2.$2.processResponse(response: response.bodyBytes); + try { + final extractV2 = await sender.extractV2( + ohttpProxyUrl: await pj_uri.Url.fromStr(relayUrl), + ); + final request = extractV2.$1; + final postCtx = extractV2.$2; + + final response = await http.post( + Uri.parse(request.url.asString()), + headers: { + 'Content-Type': v2ContentType, + }, + body: request.body, + ); - // If a valid Payjoin proposal is received, return it - if (checkedPayjoinProposal != null) { - return checkedPayjoinProposal; + final getCtx = + await postCtx.processResponse(response: response.bodyBytes); + + while (true) { + debugPrint( + '[+] BITCOINPAYJOIN => requestAndPollV2Proposal - Polling V2 Proposal Request...'); + + try { + final extractReq = await getCtx.extractReq( + ohttpRelay: await pj_uri.Url.fromStr(relayUrl), + ); + final getReq = extractReq.$1; + final ohttpCtx = extractReq.$2; + + final loopResponse = await http.post( + Uri.parse(getReq.url.asString()), + headers: { + 'Content-Type': v2ContentType, + }, + body: getReq.body, + ); + + final proposal = await getCtx.processResponse( + response: loopResponse.bodyBytes, ohttpCtx: ohttpCtx); + + if (proposal != null) { + debugPrint( + '[+] BITCOINPAYJOIN => requestAndPollV2Proposal - Received V2 proposal: $proposal'); + return proposal; + } + + debugPrint( + '[+] BITCOINPAYJOIN => requestAndPollV2Proposal - No valid proposal received, retrying after 2 seconds...'); + + await Future.delayed(const Duration(seconds: 2)); + } catch (e, st) { + // If the session times out or another error occurs, rethrow the error + debugPrint( + '[!] BITCOINPAYJOINERROR => stringToPjUri - Error: ${e.toString()}, Stacktrace: $st'); + rethrow; } - - // Add a 2-second delay before the next polling attempt - await Future.delayed(const Duration(seconds: 2)); - } catch (e) { - // If the session times out or another error occurs, rethrow the error - rethrow; } + } catch (e, st) { + // If the initial request fails, rethrow the error + debugPrint( + '[!] BITCOINPAYJOINERROR => stringToPjUri - Error: ${e.toString()}, Stacktrace: $st'); + rethrow; } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 072d23a0ef..aeabb832a1 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -668,9 +668,9 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: dc1685cb808205bd59f319ce2e6e67ea133b884d - url: "https://github.com/J0J0XMR/payjoin-flutter" + ref: payjoin-21 + resolved-ref: "8b7db6acaebbc036b947f28b2eb8b47a23fae1a3" + url: "https://github.com/DanGould/payjoin-flutter" source: git version: "0.20.0" platform: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 315776f612..d7041c67fa 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -39,12 +39,11 @@ dependencies: ref: sp_v4.0.0 payjoin_flutter: git: - url: https://github.com/J0J0XMR/payjoin-flutter - + url: https://github.com/DanGould/payjoin-flutter + ref: payjoin-21 # dangould payjoin-21 branch # payjoin_flutter: # git: - # url: https://github.com/kumulynja/payjoin-flutter - # ref: override-ffi-functions + # url: https://github.com/LtbLightning/payjoin-flutter dev_dependencies: diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index bbcb95208d..349bb3741c 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -709,10 +709,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/lib/bitcoin/bitcoin.dart b/lib/bitcoin/bitcoin.dart index bcfc53bca7..c7afa1cd7b 100644 --- a/lib/bitcoin/bitcoin.dart +++ b/lib/bitcoin/bitcoin.dart @@ -171,14 +171,22 @@ abstract class Bitcoin { int? amount, required String address, required bool isTestnet, - required int expireAfter, + required BigInt expireAfter, }); - Future pollV2Request(ActiveSession session); - Future> handleV2Request({ - required UncheckedProposal uncheckedProposal, + + Future handleReceiverSession(Receiver session); + + Future extractOriginalTransaction(UncheckedProposal proposal); + + Future processProposal({ + required UncheckedProposal proposal, required Object receiverWallet, }); + + Future sendFinalProposal(PayjoinProposal finalProposal); + Future getTxIdFromPsbt(String psbtBase64); + Future stringToPjUri(String pj); Future buildOriginalPsbt( Object wallet, @@ -187,14 +195,17 @@ abstract class Bitcoin { double amount, Object credentials, ); - Future buildPayjoinRequest( + + Future buildPayjoinRequest( String originalPsbt, dynamic pjUri, int fee, ); + Future requestAndPollV2Proposal( - RequestContext requestContext, + Sender sender, ); + Future extractPjTx( Object wallet, String psbtString, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 461fc8c3ff..a5d9171c4a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -589,7 +589,7 @@ class CWBitcoin extends Bitcoin { int? amount, required String address, required bool isTestnet, - required int expireAfter, + required BigInt expireAfter, }) async { final res = await payjoin.buildV2PjStr( amount: amount, @@ -601,20 +601,30 @@ class CWBitcoin extends Bitcoin { } @override - Future pollV2Request(ActiveSession session) async { - final res = await payjoin.pollV2Request(session); + Future handleReceiverSession(Receiver session) async { + final res = await payjoin.handleReceiverSession(session); return res; } @override - Future> handleV2Request({ - required UncheckedProposal uncheckedProposal, + Future extractOriginalTransaction(UncheckedProposal proposal) async { + final res = await payjoin.extractOriginalTransaction(proposal); + return res; + } + + @override + Future processProposal({ + required UncheckedProposal proposal, required Object receiverWallet, }) async { - final res = await payjoin.handleV2Request( - uncheckedProposal: uncheckedProposal, - receiverWallet: receiverWallet, - ); + final res = await payjoin.processProposal( + proposal: proposal, receiverWallet: receiverWallet); + return res; + } + + @override + Future sendFinalProposal(PayjoinProposal finalProposal) async { + final res = await payjoin.sendFinalProposal(finalProposal); return res; } @@ -650,7 +660,7 @@ class CWBitcoin extends Bitcoin { } @override - Future buildPayjoinRequest( + Future buildPayjoinRequest( String originalPsbt, dynamic pjUri, int fee, @@ -665,9 +675,9 @@ class CWBitcoin extends Bitcoin { @override Future requestAndPollV2Proposal( - RequestContext requestContext, + Sender sender, ) async { - final res = await payjoin.requestAndPollV2Proposal(requestContext); + final res = await payjoin.requestAndPollV2Proposal(sender); return res; } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index b76ba21ebd..0dc531b79b 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -6,18 +6,14 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/list_item.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; @@ -29,7 +25,6 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; - part 'wallet_address_list_view_model.g.dart'; class WalletAddressListViewModel = WalletAddressListViewModelBase @@ -625,10 +620,13 @@ abstract class WalletAddressListViewModelBase String payjoinUri = ''; @observable - ActiveSession? session; + Receiver? session; + + @observable + PayjoinProposal? payjoinProposal; @observable - RequestContext? reqCtx; + UncheckedProposal? uncheckedProposal; @observable PayjoinException? pjException; @@ -648,7 +646,8 @@ abstract class WalletAddressListViewModelBase '[+] wallet_address_list_view_model.dart || buildV2PjStr() => satsAmount: $satsAmount'); try { - final expireAfter = 60 * 5; // 5 minutes + final expireAfter = BigInt.from(60 * 5); // 5 minutes + final res = await bitcoin!.buildV2PjStr( amount: satsAmount, address: address.address, @@ -656,46 +655,46 @@ abstract class WalletAddressListViewModelBase expireAfter: expireAfter, ); payjoinUri = res['pjUri'] as String; - session = res['session'] as ActiveSession; + session = res['session'] as Receiver; print( '[+] wallet_address_list_view_model.dart || buildV2PjStr() => payjoinUri: $payjoinUri'); - // Poll for requests made by the sender to this payjoin uri - final requestProposal = await bitcoin!.pollV2Request(session!); + final proposal = await bitcoin!.handleReceiverSession(session!); + uncheckedProposal = proposal; + + final originalTx = await bitcoin!.extractOriginalTransaction(proposal); // Handle the request and send back the payjoin proposal - final handleV2 = await bitcoin!.handleV2Request( - uncheckedProposal: requestProposal, + final finalizedProposal = await bitcoin!.processProposal( + proposal: proposal, receiverWallet: wallet, ); - final proposedPayjoin = handleV2['payjoinProposal'] as PayjoinProposal; - final originalTxId = handleV2['originalTx'] as String; - - // Wait some time for the tx to be broadcasted - await Future.delayed(const Duration(seconds: 3)); + payjoinProposal = finalizedProposal; - // Wait for the original or payjoin tx to be broadcasted - final proposalTxId = - await bitcoin!.getTxIdFromPsbt(await proposedPayjoin.psbt()); + final proposalTxId = await bitcoin!.sendFinalProposal(finalizedProposal); final receivedTxId = await waitForTransaction( - originalTxId: originalTxId, + originalTxId: await originalTx, proposalTxId: proposalTxId, ); disposePayjoinSession(); + + if (receivedTxId.isNotEmpty) { + final msg = + '${receivedTxId == proposalTxId ? 'Payjoin' : 'Original'} tx received!'; + print('[+] wallet_address_list_vm.dart => msg: $msg'); + } } catch (e, st) { + debugPrint('[!] WALLETADDRESSLISTVM => buildV2PjStr() - ${e.toString()}'); + if (e is PayjoinException) { // TODO: Handle the error appropriately - print( - '[!] wallet_address_list_vm.dart || buildV2PjStr() => e: $e, st: $st'); + debugPrint( + '[!] WALLETADDRESSLISTVM => buildV2PjStr() - e: $e, st: $st'); pjException = e; disposePayjoinSession(); - } else { - print( - '[!] wallet_address_list_vm.dart || buildV2PjStr() => e: $e, st: $st'); - disposePayjoinSession(); } } } @@ -704,7 +703,8 @@ abstract class WalletAddressListViewModelBase void disposePayjoinSession() { // payjoinUri = ''; session = null; - reqCtx = null; + uncheckedProposal = null; + payjoinProposal = null; } Future waitForTransaction({ @@ -715,14 +715,11 @@ abstract class WalletAddressListViewModelBase final txs = wallet.transactionHistory.transactions; try { - // Search for the first transaction in the list that matches either the original or proposal transaction ID final tx = txs.values .firstWhere((tx) => tx.id == originalTxId || tx.id == proposalTxId); return tx.id; } catch (e) { - // Check if the request context (`reqCtx`) is null, which could indicate that the session was canceled - if (reqCtx == null) { - // If the session is canceled, return an empty string to stop the polling process + if (session == null) { return ''; }