diff --git a/cw_bitcoin/lib/bitcoin_payjoin.dart b/cw_bitcoin/lib/bitcoin_payjoin.dart index c400ba457..166e78cd7 100644 --- a/cw_bitcoin/lib/bitcoin_payjoin.dart +++ b/cw_bitcoin/lib/bitcoin_payjoin.dart @@ -143,7 +143,6 @@ class BitcoinPayjoin { } } - Future<PendingBitcoinTransaction> extractPjTx( - Object wallet, String psbtString, Object credentials) async => - (wallet as BitcoinWallet).psbtToPendingTx(psbtString, credentials); + Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString) async => + (wallet as BitcoinWallet).psbtToPendingTx(psbtString); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 9d89da466..ec6b0ec45 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -85,7 +85,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); - payjoinManager = PayjoinManager(PayjoinStorage(payjoinSessionSources: payjoinBox), this); + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); walletAddresses = BitcoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -339,43 +339,36 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @override Future<PendingTransaction> createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - final tx = - super.createTransaction(credentials) as PendingBitcoinTransaction; - // if (credentials.payjoinUri == null) return tx; - // - // final transaction = await buildPsbt( - // utxos: tx.utxos, - // outputs: tx.outputs.map((e) => BitcoinOutput( - // address: BitcoinBaseAddress.fromString(e.scriptPubKey.toAddress()), - // value: e.amount, - // isSilentPayment: e.isSilentPayment, - // isChange: e.isChange, - // )).toList(), - // fee: BigInt.from(tx.fee), - // network: network, - // memo: credentials.outputs.first.memo, - // outputOrdering: BitcoinOrdering.none, - // enableRBF: true, - // publicKeys: tx.publicKeys, - // masterFingerprint: Uint8List(0) - // ); - // - // transaction.signWithUTXO( - // tx.utxos - // .map((e) => - // UtxoWithPrivateKey.fromUtxo(e, tx.inputPrivKeyInfos)) - // .toList(), (txDigest, utxo, key, sighash) { - // if (utxo.utxo.isP2tr()) { - // return key.signTapRoot( - // txDigest, - // sighash: sighash, - // tweak: utxo.utxo.isSilentPayment != true, - // ); - // } else { - // return key.signInput(txDigest, sigHash: sighash); - // } - // }); + final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; + + final payjoinUri = credentials.payjoinUri; + if (payjoinUri == null) return tx; + + final transaction = await buildPsbt( + utxos: tx.utxos, + outputs: tx.outputs.map((e) => BitcoinOutput( + address: BitcoinBaseAddress.fromString(e.scriptPubKey.toAddress()), + value: e.amount, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )).toList(), + fee: BigInt.from(tx.fee), + network: network, + memo: credentials.outputs.first.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + publicKeys: tx.publicKeys!, + masterFingerprint: Uint8List(0) + ); + + final originalPsbt = await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); + + tx.commitOverride = () async { + final sender = + await payjoinManager.initSender(payjoinUri, originalPsbt, tx.fee); + await payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri); + }; return tx; } @@ -497,8 +490,25 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this)) .toList(); - Future<PendingBitcoinTransaction> psbtToPendingTx( - String preProcessedPsbt, Object credentials) async { + Future<void> commitPsbt(String finalizedPsbt) { + final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); + + final btcTx = + BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + + return PendingBitcoinTransaction( + btcTx, + type, + electrumClient: electrumClient, + amount: 0, + fee: 0, + feeRate: "", + network: network, + hasChange: true, + ).commit(); + } + + Future<PendingBitcoinTransaction> psbtToPendingTx(String preProcessedPsbt) async { final psbt = PsbtV2()..deserializeV0(base64.decode(preProcessedPsbt)); final inputCount = psbt.getGlobalInputCount(); @@ -575,17 +585,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return base64Encode(psbt.asPsbtV0()); } - Future<String> getPJURl() async { - final payjoinBox = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName); - final manager = PayjoinManager(PayjoinStorage(payjoinSessionSources: payjoinBox), this); - - final receiver = await manager.initReceiver(walletAddresses.primaryAddress); - - manager.spawnNewReceiver(receiver: receiver); - - return receiver.pjUriBuilder().build().pjEndpoint(); - } - @override Future<String> signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 83e895f90..cf6407248 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -57,13 +57,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override Future<void> init() async { await super.init(); - - refreshPayjoinReceiver(); } - Future<void> refreshPayjoinReceiver() async { + Future<void> initPayjoin() async { currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); - + + await payjoinManager.resumeSessions(); await payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 09f200183..cf1729d50 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1181,6 +1181,7 @@ abstract class ElectrumWalletBase isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index c9ab062c6..fc8a47120 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -5,12 +5,16 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; -import 'package:cw_bitcoin/payjoin/payjoin_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; import 'package:cw_bitcoin/psbt_signer.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; -import 'package:payjoin_flutter/uri.dart'; +import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/uri.dart' as PayjoinUri; class PayjoinManager { PayjoinManager(this._payjoinStorage, this._wallet); @@ -19,20 +23,133 @@ class PayjoinManager { final BitcoinWalletBase _wallet; final Map<String, PayjoinPollerSession> _activePollers = {}; - static const List<String> _ohttpRelayUrls = [ + static const List<String> ohttpRelayUrls = [ 'https://pj.bobspacebkk.com', 'https://ohttp.achow101.com', ]; + static Future<PayjoinUri.Url> randomOhttpRelayUrl() => PayjoinUri.Url.fromStr( + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]); + static const payjoinDirectoryUrl = 'https://payjo.in'; + Future<void> resumeSessions() async { + final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); + + final spawnedSessions = allSessions.map((session) { + if (session.isSenderSession) { + return spawnSender( + sender: Sender.fromJson(session.sender!), + pjUri: session.pjUri!, + ); + } + return spawnReceiver( + receiver: Receiver.fromJson(session.receiver!), + ); + }); + + await Future.wait(spawnedSessions); + } + + Future<Sender> initSender( + String pjUriString, String originalPsbt, int networkFeesSatPerVb) async { + try { + final pjUri = + (await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported(); + final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250); + final senderBuilder = await SenderBuilder.fromPsbtAndUri( + psbtBase64: originalPsbt, + pjUri: pjUri, + ); + return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + } catch (e) { + throw Exception('Error initializing Payjoin Sender: $e'); + } + } + + Future<void> spawnNewSender({ + required Sender sender, + required String pjUrl, + bool isTestnet = false, + }) async { + await _payjoinStorage.insertSenderSession( + sender, + pjUrl, + _wallet.id, + ); + + return spawnSender( + isTestnet: isTestnet, + sender: sender, + pjUri: pjUrl, + ); + } + + Future<void> spawnSender({ + required Sender sender, + required String pjUri, + bool isTestnet = false, + }) async { + final completer = Completer(); + final receivePort = ReceivePort(); + + receivePort.listen((message) async { + print('Sender isolate: $message'); + if (message is Map<String, dynamic>) { + try { + switch (message['type']) { + case PayjoinSenderRequestTypes.requestPosted: + //ToDo: Update frontend + return; + case PayjoinSenderRequestTypes.psbtToSign: + final proposalPsbt = message['psbt'] as String; + final utxos = _wallet.getUtxoWithPrivateKeys(); + final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos); + _wallet.commitPsbt(finalizedPsbt); + + //ToDo: Update frontend + _cleanupSession(pjUri); + await _payjoinStorage.markSenderSessionComplete(pjUri); + completer.complete(); + } + } catch (e) { + _cleanupSession(pjUri); + await _payjoinStorage.markReceiverSessionUnrecoverable(pjUri); + completer.completeError(e); + } + } else if (message is PayjoinSessionError) { + _cleanupSession(pjUri); + if (message is UnrecoverableError) { + printV(message.message); + await _payjoinStorage.markReceiverSessionUnrecoverable(pjUri); + } + completer.completeError(message); + } + }); + + final args = [ + receivePort.sendPort, + sender.toJson(), + ]; + + final isolate = await Isolate.spawn( + PayjoinSenderWorker.run, + args, + ); + + _activePollers[pjUri] = PayjoinPollerSession(isolate, receivePort); + + return completer.future; + } + Future<Receiver> initReceiver(String address, [bool isTestnet = false]) async { try { - final payjoinDirectory = await Url.fromStr(payjoinDirectoryUrl); + final payjoinDirectory = + await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); - final ohttpKeys = await fetchOhttpKeys( - ohttpRelay: await _randomOhttpRelayUrl(), + final ohttpKeys = await PayjoinUri.fetchOhttpKeys( + ohttpRelay: await randomOhttpRelayUrl(), payjoinDirectory: payjoinDirectory, ); @@ -41,10 +158,10 @@ class PayjoinManager { network: isTestnet ? Network.testnet : Network.bitcoin, directory: payjoinDirectory, ohttpKeys: ohttpKeys, - ohttpRelay: await _randomOhttpRelayUrl(), + ohttpRelay: await randomOhttpRelayUrl(), ); } catch (e) { - throw Exception('Error initializing payjoin Receiver: $e'); + throw Exception('Error initializing Payjoin Receiver: $e'); } } @@ -115,18 +232,19 @@ class PayjoinManager { break; case PayjoinReceiverRequestTypes.proposalSent: - await _cleanupSession(receiver.id()); + _cleanupSession(receiver.id()); await _payjoinStorage.markReceiverSessionComplete(receiver.id()); completer.complete(); } } catch (e) { - await _cleanupSession(receiver.id()); + _cleanupSession(receiver.id()); await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id()); completer.completeError(e); } } else if (message is PayjoinSessionError) { - await _cleanupSession(receiver.id()); + _cleanupSession(receiver.id()); if (message is UnrecoverableError) { + printV(message.message); await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id()); } completer.completeError(message); @@ -150,16 +268,16 @@ class PayjoinManager { return completer.future; } + void cleanupSessions() { + for (final sessionId in _activePollers.keys) { + _cleanupSession(sessionId); + } + } - Future<void> _cleanupSession(String sessionId) async { + void _cleanupSession(String sessionId) { _activePollers[sessionId]?.close(); _activePollers.remove(sessionId); } - - // Top-level function to generate random OHTTP relay URL - Future<Url> _randomOhttpRelayUrl() => Url.fromStr( - _ohttpRelayUrls[Random.secure().nextInt(_ohttpRelayUrls.length)], - ); } class PayjoinPollerSession { diff --git a/cw_bitcoin/lib/payjoin/payjoin_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart similarity index 93% rename from cw_bitcoin/lib/payjoin/payjoin_worker.dart rename to cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart index 74fd5a43a..819ed3166 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/psbt_signer.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:payjoin_flutter/bitcoin_ffi.dart'; @@ -19,23 +20,6 @@ enum PayjoinReceiverRequestTypes { processPsbt; } -class PayjoinSessionError { - final String message; - - const PayjoinSessionError._(this.message); - - factory PayjoinSessionError.recoverable(String message) = RecoverableError; - factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError; -} - -class RecoverableError extends PayjoinSessionError { - const RecoverableError(super.message) : super._(); -} - -class UnrecoverableError extends PayjoinSessionError { - const UnrecoverableError(super.message) : super._(); -} - class PayjoinReceiverWorker { final SendPort sendPort; final pendingRequests = <String, Completer<dynamic>>{}; diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart new file mode 100644 index 000000000..9e0ef7a37 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -0,0 +1,111 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:cw_bitcoin/payjoin/manager.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; +import 'package:payjoin_flutter/common.dart'; +import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; + +enum PayjoinSenderRequestTypes { + requestPosted, + psbtToSign; +} + +class PayjoinSenderWorker { + final SendPort sendPort; + final pendingRequests = <String, Completer<dynamic>>{}; + + PayjoinSenderWorker._(this.sendPort); + + static Future<void> run(List<Object> args) async { + await pj.core.init(); + + final sendPort = args[0] as SendPort; + final senderJson = args[1] as String; + + final sender = Sender.fromJson(senderJson); + final worker = PayjoinSenderWorker._(sendPort); + + try { + final proposalPsbt = await worker.runSender(sender); + sendPort.send({ + 'type': PayjoinSenderRequestTypes.psbtToSign, + 'psbt': proposalPsbt, + }); + } catch (e) { + sendPort.send(e); + } + } + + /// Run a payjoin sender (V2 protocol first, fallback to V1). + Future<String> runSender(Sender sender) async { + final httpClient = HttpClient(); + + try { + return await _runSenderV2(sender, httpClient); + } catch (e) { + if (e is PayjoinException && + // TODO condition on error type instead of message content + e.message?.contains('parse receiver public key') == true) { + return await _runSenderV1(sender, httpClient); + } else if (e is HttpException) { + throw Exception(PayjoinSessionError.recoverable(e.toString())); + } else { + throw Exception(PayjoinSessionError.unrecoverable(e.toString())); + } + } + } + + /// Attempt to send payjoin using the V2 of the protocol. + Future<String> _runSenderV2(Sender sender, HttpClient httpClient) async { + final postRequest = await sender.extractV2( + ohttpProxyUrl: await await PayjoinManager.randomOhttpRelayUrl(), + ); + + final postResult = await _postRequest(httpClient, postRequest.$1); + final getContext = + await postRequest.$2.processResponse(response: postResult); + + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); + + while (true) { + final getRequest = await getContext.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); + final getRes = await _postRequest(httpClient, getRequest.$1); + final proposalPsbt = await getContext.processResponse( + response: getRes, + ohttpCtx: getRequest.$2, + ); + if (proposalPsbt != null) return proposalPsbt; + } + } + + /// Attempt to send payjoin using the V1 of the protocol. + Future<String> _runSenderV1(Sender sender, HttpClient httpClient) async { + try { + final postRequest = await sender.extractV1(); + final response = await _postRequest(httpClient, postRequest.$1); + + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); + + return await postRequest.$2.processResponse(response: response); + } catch (e) { + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e'); + } + } + + Future<List<int>> _postRequest(HttpClient client, Request req) async { + final httpRequest = await client.postUrl(Uri.parse(req.url.asString())); + httpRequest.headers.set('Content-Type', req.contentType); + httpRequest.add(req.body); + + final response = await httpRequest.close(); + return response.fold<List<int>>( + [], + (previous, element) => previous..addAll(element), + ); + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart b/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart new file mode 100644 index 000000000..06e0a5431 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart @@ -0,0 +1,16 @@ +class PayjoinSessionError { + final String message; + + const PayjoinSessionError._(this.message); + + factory PayjoinSessionError.recoverable(String message) = RecoverableError; + factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError; +} + +class RecoverableError extends PayjoinSessionError { + const RecoverableError(super.message) : super._(); +} + +class UnrecoverableError extends PayjoinSessionError { + const UnrecoverableError(super.message) : super._(); +} diff --git a/cw_bitcoin/lib/payjoin/storage.dart b/cw_bitcoin/lib/payjoin/storage.dart index d88ba8516..6dc77a62a 100644 --- a/cw_bitcoin/lib/payjoin/storage.dart +++ b/cw_bitcoin/lib/payjoin/storage.dart @@ -1,194 +1,75 @@ import 'package:cw_core/payjoin_session.dart'; import 'package:hive/hive.dart'; import 'package:payjoin_flutter/receive.dart'; +import 'package:payjoin_flutter/send.dart'; class PayjoinStorage { - PayjoinStorage({required Box<PayjoinSession> payjoinSessionSources}) - : _payjoinSessionSources = payjoinSessionSources; + PayjoinStorage(this._payjoinSessionSources); + final Box<PayjoinSession> _payjoinSessionSources; - static const String receiverPrefix = 'pj_recv_'; - static const String senderPrefix = 'pj_send_'; + static const String _receiverPrefix = 'pj_recv_'; + static const String _senderPrefix = 'pj_send_'; Future<void> insertReceiverSession( Receiver receiver, String walletId, - ) async { - final receiverSession = - PayjoinSession(walletId: walletId, receiver: receiver.toJson()); - - await _payjoinSessionSources.put( - "$receiverPrefix${receiver.id()}", receiverSession); - } - -// Future<(RecvSession?, Err?)> readReceiverSession(String sessionId) async { -// try { -// final (jsn, err) = -// await _hiveStorage.getValue(receiverPrefix + sessionId); -// if (err != null) throw err; -// final obj = jsonDecode(jsn!) as Map<String, dynamic>; -// final session = RecvSession.fromJson(obj); -// return (session, null); -// } catch (e) { -// return ( -// null, -// Err( -// e.toString(), -// expected: e.toString() == 'No Receiver with id $sessionId', -// ) -// ); -// } -// } + ) => + _payjoinSessionSources.put( + "$_receiverPrefix${receiver.id()}", + PayjoinSession( + walletId: walletId, + receiver: receiver.toJson(), + ), + ); Future<void> markReceiverSessionComplete(String sessionId) async { - final session = - await _payjoinSessionSources.get("$receiverPrefix${sessionId}")!; + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; session.status = "success"; await session.save(); } Future<void> markReceiverSessionUnrecoverable(String sessionId) async { - final session = - await _payjoinSessionSources.get("$receiverPrefix${sessionId}")!; + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; session.status = "unrecoverable"; await session.save(); } -// Future<(List<RecvSession>, Err?)> readAllReceivers() async { -// //deleteAllSessions(); -// try { -// final (allData, err) = await _hiveStorage.getAll(); -// if (err != null) return (List<RecvSession>.empty(), err); -// -// final List<RecvSession> receivers = []; -// allData!.forEach((key, value) { -// if (key.startsWith(receiverPrefix)) { -// try { -// final obj = jsonDecode(value) as Map<String, dynamic>; -// receivers.add(RecvSession.fromJson(obj)); -// } catch (e) { -// // Skip invalid entries -// debugPrint('Error: $e'); -// } -// } -// }); -// return (receivers, null); -// } catch (e) { -// return (List<RecvSession>.empty(), Err(e.toString())); -// } -// } -// -// Future<Err?> insertSenderSession( -// Sender sender, -// String pjUrl, -// String walletId, -// bool isTestnet, -// ) async { -// try { -// final sendSession = SendSession( -// isTestnet, -// sender, -// walletId, -// pjUrl, -// PayjoinSessionStatus.pending, -// ); -// -// await _hiveStorage.saveValue( -// key: senderPrefix + pjUrl, -// value: jsonEncode(sendSession.toJson()), -// ); -// return null; -// } catch (e) { -// return Err(e.toString()); -// } -// } -// -// Future<(SendSession?, Err?)> readSenderSession(String pjUrl) async { -// try { -// final (jsn, err) = await _hiveStorage.getValue(senderPrefix + pjUrl); -// if (err != null) throw err; -// final obj = jsonDecode(jsn!) as Map<String, dynamic>; -// final session = SendSession.fromJson(obj); -// return (session, null); -// } catch (e) { -// return ( -// null, -// Err( -// e.toString(), -// expected: e.toString() == 'No Sender with id $pjUrl', -// ) -// ); -// } -// } -// -// Future<Err?> markSenderSessionComplete(String pjUrl) async { -// try { -// final (session, err) = await readSenderSession(pjUrl); -// if (err != null) return err; -// -// final updatedSession = SendSession( -// session!.isTestnet, -// session.sender, -// session.walletId, -// session.pjUri, -// PayjoinSessionStatus.success, -// ); -// -// await _hiveStorage.saveValue( -// key: senderPrefix + pjUrl, -// value: jsonEncode(updatedSession.toJson()), -// ); -// return null; -// } catch (e) { -// return Err(e.toString()); -// } -// } -// -// Future<Err?> markSenderSessionUnrecoverable(String pjUri) async { -// try { -// final (session, err) = await readSenderSession(pjUri); -// if (err != null) return err; -// -// final updatedSession = SendSession( -// session!.isTestnet, -// session.sender, -// session.walletId, -// session.pjUri, -// PayjoinSessionStatus.unrecoverable, -// ); -// -// await _hiveStorage.saveValue( -// key: senderPrefix + pjUri, -// value: jsonEncode(updatedSession.toJson()), -// ); -// return null; -// } catch (e) { -// return Err(e.toString()); -// } -// } -// -// Future<(List<SendSession>, Err?)> readAllSenders() async { -// try { -// final (allData, err) = await _hiveStorage.getAll(); -// if (err != null) return (List<SendSession>.empty(), err); -// -// final List<SendSession> senders = []; -// allData!.forEach((key, value) { -// if (key.startsWith(senderPrefix)) { -// try { -// final obj = jsonDecode(value) as Map<String, dynamic>; -// senders.add(SendSession.fromJson(obj)); -// } catch (e) { -// // Skip invalid entries -// debugPrint('Error: $e'); -// } -// } -// }); -// return (senders, null); -// } catch (e) { -// return (List<SendSession>.empty(), Err(e.toString())); -// } -// } + Future<void> insertSenderSession( + Sender sender, + String pjUrl, + String walletId, + ) => + _payjoinSessionSources.put( + "$_senderPrefix$pjUrl", + PayjoinSession( + walletId: walletId, + pjUri: pjUrl, + sender: sender.toJson(), + ), + ); + + Future<void> markSenderSessionComplete(String pjUrl) async { + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; + + session.status = "success"; + await session.save(); + } + + Future<void> markSenderSessionUnrecoverable(String pjUrl) async { + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; + + session.status = "unrecoverable"; + await session.save(); + } + + List<PayjoinSession> readAllOpenSessions(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + session.status != "success" && + session.status != "unrecoverable") + .toList(); } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 411c7de16..6930524eb 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,4 @@ +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -25,6 +26,8 @@ class PendingBitcoinTransaction with PendingTransaction { this.hasTaprootInputs = false, this.isMweb = false, this.utxos = const [], + this.publicKeys, + this.commitOverride, }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[]; final WalletType type; @@ -43,6 +46,8 @@ class PendingBitcoinTransaction with PendingTransaction { String? idOverride; String? hexOverride; List<String>? outputAddresses; + final Map<String, PublicKeyWithDerivationPath>? publicKeys; + Future<void> Function()? commitOverride; @override String get id => idOverride ?? _tx.txId(); @@ -129,6 +134,10 @@ class PendingBitcoinTransaction with PendingTransaction { @override Future<void> commit() async { + if (commitOverride != null) { + return commitOverride?.call(); + } + if (isMweb) { await _ltcCommit(); } else { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e4b894c4a..864d41367 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -721,11 +721,20 @@ class CWBitcoin extends Bitcoin { } @override - String buildV2PjStr({ - required Object receiverWallet - }) { - final wallet = receiverWallet as BitcoinWallet; - return (wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? ''; + String getPayjoinEndpoint(Object wallet) { + final _wallet = wallet as BitcoinWallet; + return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? ''; + } + + @override + void updatePayjoinState(Object wallet, bool value) { + final _wallet = wallet as ElectrumWallet; + if (value) { + (_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin(); + } else { + (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions(); + (_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null; + } } @override @@ -755,12 +764,10 @@ class CWBitcoin extends Bitcoin { } @override - Future<PendingBitcoinTransaction> extractPjTx( - Object wallet, String psbtString, Object credentials) { + Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString) { return payjoin.extractPjTx( wallet, - psbtString, - credentials + psbtString ); } } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 3840b042e..c21b89564 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; @@ -74,6 +75,10 @@ void startCurrentWalletChangeReaction( _setAutoGenerateSubaddressStatus(wallet, settingsStore); } + if (wallet.type == WalletType.bitcoin) { + bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin); + } + await wallet.connectToNode(node: node); if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { final powNode = settingsStore.getCurrentPowNode(wallet.type); diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 16bd54a35..0d1ad7668 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -51,7 +51,7 @@ class PrivacyPage extends BasePage { ), if (_privacySettingsViewModel.canUsePayjoin) SettingsSwitcherCell( - title: 'Use Payjoin', // ToDo: localize + title: S.of(context).use_payjoin, value: _privacySettingsViewModel.usePayjoin, onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setUsePayjoin(value); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 6ecaebdc1..88b6f7bfb 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -395,7 +395,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState(); - pendingTransaction = await (pjUri != null ? performPayjoinSend() : wallet.createTransaction(_credentials())); // ToDo: Remove move Payjoin into create tx + pendingTransaction = await wallet.createTransaction(_credentials()); if (provider is ThorChainExchangeProvider) { final outputCount = pendingTransaction?.outputCount ?? 0; @@ -850,10 +850,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } // If a proposal is received, finalize the payjoin - return bitcoin!.extractPjTx( - wallet, - psbt, - _credentials() - ); + return bitcoin!.extractPjTx(wallet, psbt); } } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 6ff0fb501..b9141af6f 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -172,5 +172,8 @@ abstract class PrivacySettingsViewModelBase with Store { _settingsStore.useMempoolFeeAPI = value; @action - void setUsePayjoin(bool value) => _settingsStore.usePayjoin = value; + void setUsePayjoin(bool value) { + _settingsStore.usePayjoin = value; + bitcoin!.updatePayjoinState(_wallet, value); + } } 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 94afe9e68..c66705d06 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 @@ -284,7 +284,7 @@ abstract class WalletAddressListViewModelBase case WalletType.haven: return HavenURI(amount: amount, address: address.address); case WalletType.bitcoin: - final pjEndpoint = bitcoin!.buildV2PjStr(receiverWallet: wallet); + final pjEndpoint = bitcoin!.getPayjoinEndpoint(wallet); return BitcoinURI(amount: amount, address: address.address, pjUri: pjEndpoint); case WalletType.litecoin: return LitecoinURI(amount: amount, address: address.address); diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index b0955b6d7..6e6bd130c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -905,6 +905,7 @@ "use": "التبديل إلى", "use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.", "use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.", + "use_payjoin": "يستخدم ٪٪٪", "use_ssl": "استخدم SSL", "use_suggested": "استخدام المقترح", "use_testnet": "استخدم testnet", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e1fc3bd87..d42275e49 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -905,6 +905,7 @@ "use": "Смяна на ", "use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.", "use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.", + "use_payjoin": "Използвайте %%", "use_ssl": "Използване на SSL", "use_suggested": "Използване на предложеното", "use_testnet": "Използвайте TestNet", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 29c04496d..25797b255 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -905,6 +905,7 @@ "use": "Přepnout na ", "use_card_info_three": "Použijte tuto digitální kartu online nebo bezkontaktními platebními metodami.", "use_card_info_two": "Prostředky jsou převedeny na USD, když jsou drženy na předplaceném účtu, nikoliv na digitální měnu.", + "use_payjoin": "Použijte %%TE", "use_ssl": "Použít SSL", "use_suggested": "Použít doporučený", "use_testnet": "Použijte testNet", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 54c3a70e7..45ab89cbc 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -907,6 +907,7 @@ "use": "Wechsel zu ", "use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.", "use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.", + "use_payjoin": "Benutze Payjoin", "use_ssl": "SSL verwenden", "use_suggested": "Vorgeschlagen verwenden", "use_testnet": "TESTNET verwenden", @@ -991,4 +992,4 @@ "you_will_get": "Konvertieren zu", "you_will_send": "Konvertieren von", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 995fb5595..d4532217d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -905,6 +905,7 @@ "use": "Switch to ", "use_card_info_three": "Use the digital card online or with contactless payment methods.", "use_card_info_two": "Funds are converted to USD when they're held in the prepaid account, not in digital currencies.", + "use_payjoin": "Use Payjoin", "use_ssl": "Use SSL", "use_suggested": "Use Suggested", "use_testnet": "Use Testnet", @@ -988,4 +989,4 @@ "you_will_get": "Convert to", "you_will_send": "Convert from", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 011206fbf..a8d444733 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -906,6 +906,7 @@ "use": "Utilizar a ", "use_card_info_three": "Utiliza la tarjeta digital en línea o con métodos de pago sin contacto.", "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.", + "use_payjoin": "Usar Payjoin", "use_ssl": "Utiliza SSL", "use_suggested": "Usar sugerido", "use_testnet": "Usar TestNet", @@ -989,4 +990,4 @@ "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 7e2907c8f..8fed315af 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -905,6 +905,7 @@ "use": "Changer vers code PIN à ", "use_card_info_three": "Utilisez la carte numérique en ligne ou avec des méthodes de paiement sans contact.", "use_card_info_two": "Les fonds sont convertis en USD lorsqu'ils sont détenus sur le compte prépayé, et non en devises numériques.", + "use_payjoin": "Utiliser Payjoin", "use_ssl": "Utiliser SSL", "use_suggested": "Suivre la suggestion", "use_testnet": "Utiliser TestNet", @@ -988,4 +989,4 @@ "you_will_get": "Convertir vers", "you_will_send": "Convertir depuis", "yy": "AA" -} \ No newline at end of file +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index d06210e3d..a41606d58 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -907,6 +907,7 @@ "use": "Canja zuwa", "use_card_info_three": "Yi amfani da katin dijital akan layi ko tare da hanyoyin biyan kuɗi mara lamba.", "use_card_info_two": "Ana canza kuɗi zuwa dalar Amurka lokacin da ake riƙe su a cikin asusun da aka riga aka biya, ba cikin agogon dijital ba.", + "use_payjoin": "Yi amfani da %%", "use_ssl": "Yi amfani da SSL", "use_suggested": "Amfani da Shawarwari", "use_testnet": "Amfani da gwaji", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 74de1126e..811fb57c7 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -907,6 +907,7 @@ "use": "उपयोग ", "use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।", "use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।", + "use_payjoin": "उपयोग %%%", "use_ssl": "उपयोग SSL", "use_suggested": "सुझाए गए का प्रयोग करें", "use_testnet": "टेस्टनेट का उपयोग करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 41768bd4f..65f9720e7 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -905,6 +905,7 @@ "use": "Prebaci na", "use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.", "use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.", + "use_payjoin": "Koristite Payjoin", "use_ssl": "Koristi SSL", "use_suggested": "Koristite predloženo", "use_testnet": "Koristite TestNet", @@ -988,4 +989,4 @@ "you_will_get": "Razmijeni u", "you_will_send": "Razmijeni iz", "yy": "GG" -} \ No newline at end of file +} diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 2a3aeed6d..a97e0c50d 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -905,6 +905,7 @@ "use": "Փոխեք ", "use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։", "use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։", + "use_payjoin": "Օգտագործեք Payjoin", "use_ssl": "Օգտագործել SSL", "use_suggested": "Օգտագործել առաջարկվածը", "use_testnet": "Օգտագործել Testnet", @@ -988,4 +989,4 @@ "you_will_get": "Ստացեք", "you_will_send": "Փոխանակեք", "yy": "ՏՏ" -} \ No newline at end of file +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 90bf4d806..fe0af5afa 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -908,6 +908,7 @@ "use": "Beralih ke ", "use_card_info_three": "Gunakan kartu digital secara online atau dengan metode pembayaran tanpa kontak.", "use_card_info_two": "Dana dikonversi ke USD ketika disimpan dalam akun pra-bayar, bukan dalam mata uang digital.", + "use_payjoin": "Menggunakan Payjoin", "use_ssl": "Gunakan SSL", "use_suggested": "Gunakan yang Disarankan", "use_testnet": "Gunakan TestNet", @@ -991,4 +992,4 @@ "you_will_get": "Konversi ke", "you_will_send": "Konversi dari", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index c3796be5a..332b4e9b7 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -907,6 +907,7 @@ "use": "Passa a ", "use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.", "use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.", + "use_payjoin": "Utilizzo Payjoin", "use_ssl": "Usa SSL", "use_suggested": "Usa suggerito", "use_testnet": "Usa TestNet", @@ -991,4 +992,4 @@ "you_will_get": "Converti a", "you_will_send": "Conveti da", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 4ccd2d830..9fad19670 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -906,6 +906,7 @@ "use": "使用する ", "use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。", "use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。", + "use_payjoin": "使用 %%%", "use_ssl": "SSLを使用する", "use_suggested": "推奨を使用", "use_testnet": "テストネットを使用します", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 67a69e26b..88d9126d6 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -906,6 +906,7 @@ "use": "사용하다 ", "use_card_info_three": "디지털 카드를 온라인 또는 비접촉식 결제 수단으로 사용하십시오.", "use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", + "use_payjoin": "사용 Payjoin", "use_ssl": "SSL 사용", "use_suggested": "추천 사용", "use_testnet": "TestNet을 사용하십시오", @@ -990,4 +991,4 @@ "you_will_send": "다음에서 변환", "YY": "YY", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index dd2909d3f..d775d4b78 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -905,6 +905,7 @@ "use": "သို့ပြောင်းပါ။", "use_card_info_three": "ဒစ်ဂျစ်တယ်ကတ်ကို အွန်လိုင်း သို့မဟုတ် ထိတွေ့မှုမဲ့ ငွေပေးချေမှုနည်းလမ်းများဖြင့် အသုံးပြုပါ။", "use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။", + "use_payjoin": "%% Chair ကိုအသုံးပြုပါ", "use_ssl": "SSL ကိုသုံးပါ။", "use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။", "use_testnet": "testnet ကိုသုံးပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 8b32d669f..d8c86fb4a 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -905,6 +905,7 @@ "use": "Gebruik ", "use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.", "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.", + "use_payjoin": "Gebruik Payjoin", "use_ssl": "Gebruik SSL", "use_suggested": "Gebruik aanbevolen", "use_testnet": "Gebruik testnet", @@ -989,4 +990,4 @@ "you_will_get": "Converteren naar", "you_will_send": "Converteren van", "yy": "JJ" -} \ No newline at end of file +} diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 2f2c19546..66d075a0b 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -905,6 +905,7 @@ "use": "Użyj ", "use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.", "use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.", + "use_payjoin": "Używać Payjoin", "use_ssl": "Użyj SSL", "use_suggested": "Użyj sugerowane", "use_testnet": "Użyj testne", @@ -988,4 +989,4 @@ "you_will_get": "Konwertuj na", "you_will_send": "Konwertuj z", "yy": "RR" -} \ No newline at end of file +} diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index cf3adcf82..d6a157239 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -907,6 +907,7 @@ "use": "Use PIN de ", "use_card_info_three": "Use o cartão digital online ou com métodos de pagamento sem contato.", "use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.", + "use_payjoin": "Usar Payjoin", "use_ssl": "Use SSL", "use_suggested": "Uso sugerido", "use_testnet": "Use testNet", @@ -991,4 +992,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} \ No newline at end of file +} diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 0ba732a1c..992ad83cf 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -906,6 +906,7 @@ "use": "Использовать ", "use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.", "use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.", + "use_payjoin": "Использовать %%%", "use_ssl": "Использовать SSL", "use_suggested": "Использовать предложенный", "use_testnet": "Используйте Testnet", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 29764b302..aa1c8974a 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -905,6 +905,7 @@ "use": "สลับไปที่ ", "use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ", "use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล", + "use_payjoin": "ใช้ %%%", "use_ssl": "ใช้ SSL", "use_suggested": "ใช้ที่แนะนำ", "use_testnet": "ใช้ testnet", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 772ed2cc0..96a78859c 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -905,6 +905,7 @@ "use": "Lumipat sa ", "use_card_info_three": "Gamitin ang digital card online o sa mga paraan ng pagbabayad na walang contact.", "use_card_info_two": "Ang mga pondo ay na-convert sa USD kapag hawak sa prepaid account, hindi sa mga digital na pera.", + "use_payjoin": "Gumamit ng Payjoin", "use_ssl": "Gumamit ng SSL", "use_suggested": "Gumamit ng iminungkahing", "use_testnet": "Gumamit ng testnet", @@ -988,4 +989,4 @@ "you_will_get": "I-convert sa", "you_will_send": "I-convert mula sa", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 34fc7da8c..16b37d733 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -905,6 +905,7 @@ "use": "Şuna geç: ", "use_card_info_three": "Dijital kartı çevrimiçi olarak veya temassız ödeme yöntemleriyle kullanın.", "use_card_info_two": "Paralar, dijital para birimlerinde değil, ön ödemeli hesapta tutulduğunda USD'ye dönüştürülür.", + "use_payjoin": "Kullanmak Payjoin", "use_ssl": "SSL kullan", "use_suggested": "Önerileni Kullan", "use_testnet": "TestNet kullanın", @@ -988,4 +989,4 @@ "you_will_get": "Biçimine dönüştür:", "you_will_send": "Biçiminden dönüştür:", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 35d12cbbb..3e0dee67d 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -906,6 +906,7 @@ "use": "Використати ", "use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.", "use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.", + "use_payjoin": "Використовуйте %%", "use_ssl": "Використати SSL", "use_suggested": "Використати запропоноване", "use_testnet": "Використовуйте тестову мережу", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 874b9913e..b145f94b7 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -907,6 +907,7 @@ "use": "تبدیل کرنا", "use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔", "use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔", + "use_payjoin": "٪٪٪ کا استعمال کریں", "use_ssl": "SSL استعمال کریں۔", "use_suggested": "تجویز کردہ استعمال کریں۔", "use_testnet": "ٹیسٹ نیٹ استعمال کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 5b3b08a6b..5aba4fef6 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -904,6 +904,7 @@ "use": "Chuyển sang", "use_card_info_three": "Sử dụng thẻ kỹ thuật số trực tuyến hoặc với các phương thức thanh toán không tiếp xúc.", "use_card_info_two": "Các khoản tiền được chuyển đổi thành USD khi chúng được giữ trong tài khoản trả trước, không phải trong các loại tiền kỹ thuật số.", + "use_payjoin": "Sử dụng Payjoin", "use_ssl": "Sử dụng SSL", "use_suggested": "Sử dụng đề xuất", "use_testnet": "Sử dụng Testnet", @@ -987,4 +988,4 @@ "you_will_get": "Chuyển đổi thành", "you_will_send": "Chuyển đổi từ", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 360676a7e..4401ae992 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -906,6 +906,7 @@ "use": "Lo", "use_card_info_three": "Ẹ lo káàdí ayélujára lórí wẹ́ẹ̀bù tàbí ẹ lò ó lórí àwọn ẹ̀rọ̀ ìrajà tíwọn kò kò.", "use_card_info_two": "A pààrọ̀ owó sí owó Amẹ́ríkà tó bá wà nínú àkanti t'á ti fikún tẹ́lẹ̀tẹ́lẹ̀. A kò kó owó náà nínú owó ayélujára.", + "use_payjoin": "Lo %%", "use_ssl": "Lo SSL", "use_suggested": "Lo àbá", "use_testnet": "Lo tele", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index e032c40c4..ee698fdb8 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -905,6 +905,7 @@ "use": "切换使用", "use_card_info_three": "在线使用电子卡或使用非接触式支付方式。", "use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。", + "use_payjoin": "使用 %%%", "use_ssl": "使用SSL", "use_suggested": "使用建议", "use_testnet": "使用TestNet", diff --git a/tool/configure.dart b/tool/configure.dart index fa5d7da92..b9a72ad0b 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -240,11 +240,12 @@ abstract class Bitcoin { String? getUnusedMwebAddress(Object wallet); String? getUnusedSegwitAddress(Object wallet); - String buildV2PjStr({required Object receiverWallet}); + void updatePayjoinState(Object wallet, bool state); + String getPayjoinEndpoint(Object wallet); Future<Sender> buildPayjoinRequest(String originalPsbt, dynamic pjUri, int fee); Future<String> buildOriginalPsbt(Object wallet, int fee, double amount, Object credentials); Future<String> requestAndPollV2Proposal(Sender sender); - Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString, Object credentials); + Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString); } """;