Payjoin Sender are now isolated

This commit is contained in:
Konstantin Ullrich 2025-01-27 22:32:14 +01:00
parent 4c4fd8e645
commit 8f4dad9134
No known key found for this signature in database
GPG key ID: 6B3199AD9B3D23B8
45 changed files with 452 additions and 295 deletions

View file

@ -143,7 +143,6 @@ class BitcoinPayjoin {
} }
} }
Future<PendingBitcoinTransaction> extractPjTx( Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString) async =>
Object wallet, String psbtString, Object credentials) async => (wallet as BitcoinWallet).psbtToPendingTx(psbtString);
(wallet as BitcoinWallet).psbtToPendingTx(psbtString, credentials);
} }

View file

@ -85,7 +85,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
// final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
payjoinManager = PayjoinManager(PayjoinStorage(payjoinSessionSources: payjoinBox), this); payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
walletAddresses = BitcoinWalletAddresses( walletAddresses = BitcoinWalletAddresses(
walletInfo, walletInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
@ -339,43 +339,36 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
@override @override
Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
credentials = credentials as BitcoinTransactionCredentials; credentials = credentials as BitcoinTransactionCredentials;
final tx =
super.createTransaction(credentials) as PendingBitcoinTransaction;
// if (credentials.payjoinUri == null) return tx; final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
//
// final transaction = await buildPsbt( final payjoinUri = credentials.payjoinUri;
// utxos: tx.utxos, if (payjoinUri == null) return tx;
// outputs: tx.outputs.map((e) => BitcoinOutput(
// address: BitcoinBaseAddress.fromString(e.scriptPubKey.toAddress()), final transaction = await buildPsbt(
// value: e.amount, utxos: tx.utxos,
// isSilentPayment: e.isSilentPayment, outputs: tx.outputs.map((e) => BitcoinOutput(
// isChange: e.isChange, address: BitcoinBaseAddress.fromString(e.scriptPubKey.toAddress()),
// )).toList(), value: e.amount,
// fee: BigInt.from(tx.fee), isSilentPayment: e.isSilentPayment,
// network: network, isChange: e.isChange,
// memo: credentials.outputs.first.memo, )).toList(),
// outputOrdering: BitcoinOrdering.none, fee: BigInt.from(tx.fee),
// enableRBF: true, network: network,
// publicKeys: tx.publicKeys, memo: credentials.outputs.first.memo,
// masterFingerprint: Uint8List(0) outputOrdering: BitcoinOrdering.none,
// ); enableRBF: true,
// publicKeys: tx.publicKeys!,
// transaction.signWithUTXO( masterFingerprint: Uint8List(0)
// tx.utxos );
// .map((e) =>
// UtxoWithPrivateKey.fromUtxo(e, tx.inputPrivKeyInfos)) final originalPsbt = await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
// .toList(), (txDigest, utxo, key, sighash) {
// if (utxo.utxo.isP2tr()) { tx.commitOverride = () async {
// return key.signTapRoot( final sender =
// txDigest, await payjoinManager.initSender(payjoinUri, originalPsbt, tx.fee);
// sighash: sighash, await payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri);
// tweak: utxo.utxo.isSilentPayment != true, };
// );
// } else {
// return key.signInput(txDigest, sigHash: sighash);
// }
// });
return tx; return tx;
} }
@ -497,8 +490,25 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this)) .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList(); .toList();
Future<PendingBitcoinTransaction> psbtToPendingTx( Future<void> commitPsbt(String finalizedPsbt) {
String preProcessedPsbt, Object credentials) async { 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 psbt = PsbtV2()..deserializeV0(base64.decode(preProcessedPsbt));
final inputCount = psbt.getGlobalInputCount(); final inputCount = psbt.getGlobalInputCount();
@ -575,17 +585,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return base64Encode(psbt.asPsbtV0()); 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 @override
Future<String> signMessage(String message, {String? address = null}) async { Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) { if (walletInfo.isHardwareWallet) {

View file

@ -57,13 +57,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
@override @override
Future<void> init() async { Future<void> init() async {
await super.init(); await super.init();
refreshPayjoinReceiver();
} }
Future<void> refreshPayjoinReceiver() async { Future<void> initPayjoin() async {
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
await payjoinManager.resumeSessions();
await payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); await payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!);
} }
} }

View file

@ -1181,6 +1181,7 @@ abstract class ElectrumWalletBase
isSendAll: estimatedTx.isSendAll, isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs, hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos, utxos: estimatedTx.utxos,
publicKeys: estimatedTx.publicKeys
)..addListener((transaction) async { )..addListener((transaction) async {
transactionHistory.addOne(transaction); transactionHistory.addOne(transaction);
if (estimatedTx.spendsSilentPayment) { if (estimatedTx.spendsSilentPayment) {

View file

@ -5,12 +5,16 @@ import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_wallet.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/payjoin/storage.dart';
import 'package:cw_bitcoin/psbt_signer.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/common.dart';
import 'package:payjoin_flutter/receive.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 { class PayjoinManager {
PayjoinManager(this._payjoinStorage, this._wallet); PayjoinManager(this._payjoinStorage, this._wallet);
@ -19,20 +23,133 @@ class PayjoinManager {
final BitcoinWalletBase _wallet; final BitcoinWalletBase _wallet;
final Map<String, PayjoinPollerSession> _activePollers = {}; final Map<String, PayjoinPollerSession> _activePollers = {};
static const List<String> _ohttpRelayUrls = [ static const List<String> ohttpRelayUrls = [
'https://pj.bobspacebkk.com', 'https://pj.bobspacebkk.com',
'https://ohttp.achow101.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'; 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, Future<Receiver> initReceiver(String address,
[bool isTestnet = false]) async { [bool isTestnet = false]) async {
try { try {
final payjoinDirectory = await Url.fromStr(payjoinDirectoryUrl); final payjoinDirectory =
await PayjoinUri.Url.fromStr(payjoinDirectoryUrl);
final ohttpKeys = await fetchOhttpKeys( final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
ohttpRelay: await _randomOhttpRelayUrl(), ohttpRelay: await randomOhttpRelayUrl(),
payjoinDirectory: payjoinDirectory, payjoinDirectory: payjoinDirectory,
); );
@ -41,10 +158,10 @@ class PayjoinManager {
network: isTestnet ? Network.testnet : Network.bitcoin, network: isTestnet ? Network.testnet : Network.bitcoin,
directory: payjoinDirectory, directory: payjoinDirectory,
ohttpKeys: ohttpKeys, ohttpKeys: ohttpKeys,
ohttpRelay: await _randomOhttpRelayUrl(), ohttpRelay: await randomOhttpRelayUrl(),
); );
} catch (e) { } catch (e) {
throw Exception('Error initializing payjoin Receiver: $e'); throw Exception('Error initializing Payjoin Receiver: $e');
} }
} }
@ -115,18 +232,19 @@ class PayjoinManager {
break; break;
case PayjoinReceiverRequestTypes.proposalSent: case PayjoinReceiverRequestTypes.proposalSent:
await _cleanupSession(receiver.id()); _cleanupSession(receiver.id());
await _payjoinStorage.markReceiverSessionComplete(receiver.id()); await _payjoinStorage.markReceiverSessionComplete(receiver.id());
completer.complete(); completer.complete();
} }
} catch (e) { } catch (e) {
await _cleanupSession(receiver.id()); _cleanupSession(receiver.id());
await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id()); await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id());
completer.completeError(e); completer.completeError(e);
} }
} else if (message is PayjoinSessionError) { } else if (message is PayjoinSessionError) {
await _cleanupSession(receiver.id()); _cleanupSession(receiver.id());
if (message is UnrecoverableError) { if (message is UnrecoverableError) {
printV(message.message);
await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id()); await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id());
} }
completer.completeError(message); completer.completeError(message);
@ -150,16 +268,16 @@ class PayjoinManager {
return completer.future; 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[sessionId]?.close();
_activePollers.remove(sessionId); _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 { class PayjoinPollerSession {

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_bitcoin/psbt_signer.dart'; import 'package:cw_bitcoin/psbt_signer.dart';
import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/print_verbose.dart';
import 'package:payjoin_flutter/bitcoin_ffi.dart'; import 'package:payjoin_flutter/bitcoin_ffi.dart';
@ -19,23 +20,6 @@ enum PayjoinReceiverRequestTypes {
processPsbt; 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 { class PayjoinReceiverWorker {
final SendPort sendPort; final SendPort sendPort;
final pendingRequests = <String, Completer<dynamic>>{}; final pendingRequests = <String, Completer<dynamic>>{};

View file

@ -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),
);
}
}

View file

@ -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._();
}

View file

@ -1,194 +1,75 @@
import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/payjoin_session.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/send.dart';
class PayjoinStorage { class PayjoinStorage {
PayjoinStorage({required Box<PayjoinSession> payjoinSessionSources}) PayjoinStorage(this._payjoinSessionSources);
: _payjoinSessionSources = payjoinSessionSources;
final Box<PayjoinSession> _payjoinSessionSources; final Box<PayjoinSession> _payjoinSessionSources;
static const String receiverPrefix = 'pj_recv_'; static const String _receiverPrefix = 'pj_recv_';
static const String senderPrefix = 'pj_send_'; static const String _senderPrefix = 'pj_send_';
Future<void> insertReceiverSession( Future<void> insertReceiverSession(
Receiver receiver, Receiver receiver,
String walletId, String walletId,
) async { ) =>
final receiverSession = _payjoinSessionSources.put(
PayjoinSession(walletId: walletId, receiver: receiver.toJson()); "$_receiverPrefix${receiver.id()}",
PayjoinSession(
await _payjoinSessionSources.put( walletId: walletId,
"$receiverPrefix${receiver.id()}", receiverSession); receiver: receiver.toJson(),
} ),
);
// 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',
// )
// );
// }
// }
Future<void> markReceiverSessionComplete(String sessionId) async { Future<void> markReceiverSessionComplete(String sessionId) async {
final session = final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
await _payjoinSessionSources.get("$receiverPrefix${sessionId}")!;
session.status = "success"; session.status = "success";
await session.save(); await session.save();
} }
Future<void> markReceiverSessionUnrecoverable(String sessionId) async { Future<void> markReceiverSessionUnrecoverable(String sessionId) async {
final session = final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
await _payjoinSessionSources.get("$receiverPrefix${sessionId}")!;
session.status = "unrecoverable"; session.status = "unrecoverable";
await session.save(); await session.save();
} }
// Future<(List<RecvSession>, Err?)> readAllReceivers() async { Future<void> insertSenderSession(
// //deleteAllSessions(); Sender sender,
// try { String pjUrl,
// final (allData, err) = await _hiveStorage.getAll(); String walletId,
// if (err != null) return (List<RecvSession>.empty(), err); ) =>
// _payjoinSessionSources.put(
// final List<RecvSession> receivers = []; "$_senderPrefix$pjUrl",
// allData!.forEach((key, value) { PayjoinSession(
// if (key.startsWith(receiverPrefix)) { walletId: walletId,
// try { pjUri: pjUrl,
// final obj = jsonDecode(value) as Map<String, dynamic>; sender: sender.toJson(),
// receivers.add(RecvSession.fromJson(obj)); ),
// } catch (e) { );
// // Skip invalid entries
// debugPrint('Error: $e'); Future<void> markSenderSessionComplete(String pjUrl) async {
// } final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
// }
// }); session.status = "success";
// return (receivers, null); await session.save();
// } catch (e) { }
// return (List<RecvSession>.empty(), Err(e.toString()));
// } Future<void> markSenderSessionUnrecoverable(String pjUrl) async {
// } final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
//
// Future<Err?> insertSenderSession( session.status = "unrecoverable";
// Sender sender, await session.save();
// String pjUrl, }
// String walletId,
// bool isTestnet, List<PayjoinSession> readAllOpenSessions(String walletId) =>
// ) async { _payjoinSessionSources.values
// try { .where((session) =>
// final sendSession = SendSession( session.walletId == walletId &&
// isTestnet, session.status != "success" &&
// sender, session.status != "unrecoverable")
// walletId, .toList();
// 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()));
// }
// }
} }

View file

@ -1,3 +1,4 @@
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:grpc/grpc.dart'; import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
@ -25,6 +26,8 @@ class PendingBitcoinTransaction with PendingTransaction {
this.hasTaprootInputs = false, this.hasTaprootInputs = false,
this.isMweb = false, this.isMweb = false,
this.utxos = const [], this.utxos = const [],
this.publicKeys,
this.commitOverride,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[]; }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type; final WalletType type;
@ -43,6 +46,8 @@ class PendingBitcoinTransaction with PendingTransaction {
String? idOverride; String? idOverride;
String? hexOverride; String? hexOverride;
List<String>? outputAddresses; List<String>? outputAddresses;
final Map<String, PublicKeyWithDerivationPath>? publicKeys;
Future<void> Function()? commitOverride;
@override @override
String get id => idOverride ?? _tx.txId(); String get id => idOverride ?? _tx.txId();
@ -129,6 +134,10 @@ class PendingBitcoinTransaction with PendingTransaction {
@override @override
Future<void> commit() async { Future<void> commit() async {
if (commitOverride != null) {
return commitOverride?.call();
}
if (isMweb) { if (isMweb) {
await _ltcCommit(); await _ltcCommit();
} else { } else {

View file

@ -721,11 +721,20 @@ class CWBitcoin extends Bitcoin {
} }
@override @override
String buildV2PjStr({ String getPayjoinEndpoint(Object wallet) {
required Object receiverWallet final _wallet = wallet as BitcoinWallet;
}) { return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? '';
final wallet = receiverWallet 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 @override
@ -755,12 +764,10 @@ class CWBitcoin extends Bitcoin {
} }
@override @override
Future<PendingBitcoinTransaction> extractPjTx( Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString) {
Object wallet, String psbtString, Object credentials) {
return payjoin.extractPjTx( return payjoin.extractPjTx(
wallet, wallet,
psbtString, psbtString
credentials
); );
} }
} }

View file

@ -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/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart';
@ -74,6 +75,10 @@ void startCurrentWalletChangeReaction(
_setAutoGenerateSubaddressStatus(wallet, settingsStore); _setAutoGenerateSubaddressStatus(wallet, settingsStore);
} }
if (wallet.type == WalletType.bitcoin) {
bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin);
}
await wallet.connectToNode(node: node); await wallet.connectToNode(node: node);
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
final powNode = settingsStore.getCurrentPowNode(wallet.type); final powNode = settingsStore.getCurrentPowNode(wallet.type);

View file

@ -51,7 +51,7 @@ class PrivacyPage extends BasePage {
), ),
if (_privacySettingsViewModel.canUsePayjoin) if (_privacySettingsViewModel.canUsePayjoin)
SettingsSwitcherCell( SettingsSwitcherCell(
title: 'Use Payjoin', // ToDo: localize title: S.of(context).use_payjoin,
value: _privacySettingsViewModel.usePayjoin, value: _privacySettingsViewModel.usePayjoin,
onValueChange: (BuildContext _, bool value) { onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setUsePayjoin(value); _privacySettingsViewModel.setUsePayjoin(value);

View file

@ -395,7 +395,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState(); 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) { if (provider is ThorChainExchangeProvider) {
final outputCount = pendingTransaction?.outputCount ?? 0; final outputCount = pendingTransaction?.outputCount ?? 0;
@ -850,10 +850,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
} }
// If a proposal is received, finalize the payjoin // If a proposal is received, finalize the payjoin
return bitcoin!.extractPjTx( return bitcoin!.extractPjTx(wallet, psbt);
wallet,
psbt,
_credentials()
);
} }
} }

View file

@ -172,5 +172,8 @@ abstract class PrivacySettingsViewModelBase with Store {
_settingsStore.useMempoolFeeAPI = value; _settingsStore.useMempoolFeeAPI = value;
@action @action
void setUsePayjoin(bool value) => _settingsStore.usePayjoin = value; void setUsePayjoin(bool value) {
_settingsStore.usePayjoin = value;
bitcoin!.updatePayjoinState(_wallet, value);
}
} }

View file

@ -284,7 +284,7 @@ abstract class WalletAddressListViewModelBase
case WalletType.haven: case WalletType.haven:
return HavenURI(amount: amount, address: address.address); return HavenURI(amount: amount, address: address.address);
case WalletType.bitcoin: case WalletType.bitcoin:
final pjEndpoint = bitcoin!.buildV2PjStr(receiverWallet: wallet); final pjEndpoint = bitcoin!.getPayjoinEndpoint(wallet);
return BitcoinURI(amount: amount, address: address.address, pjUri: pjEndpoint); return BitcoinURI(amount: amount, address: address.address, pjUri: pjEndpoint);
case WalletType.litecoin: case WalletType.litecoin:
return LitecoinURI(amount: amount, address: address.address); return LitecoinURI(amount: amount, address: address.address);

View file

@ -905,6 +905,7 @@
"use": "التبديل إلى", "use": "التبديل إلى",
"use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.", "use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.",
"use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.", "use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.",
"use_payjoin": "يستخدم ٪٪٪",
"use_ssl": "استخدم SSL", "use_ssl": "استخدم SSL",
"use_suggested": "استخدام المقترح", "use_suggested": "استخدام المقترح",
"use_testnet": "استخدم testnet", "use_testnet": "استخدم testnet",

View file

@ -905,6 +905,7 @@
"use": "Смяна на ", "use": "Смяна на ",
"use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.", "use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.",
"use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.", "use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.",
"use_payjoin": "Използвайте %%",
"use_ssl": "Използване на SSL", "use_ssl": "Използване на SSL",
"use_suggested": "Използване на предложеното", "use_suggested": "Използване на предложеното",
"use_testnet": "Използвайте TestNet", "use_testnet": "Използвайте TestNet",

View file

@ -905,6 +905,7 @@
"use": "Přepnout na ", "use": "Přepnout na ",
"use_card_info_three": "Použijte tuto digitální kartu online nebo bezkontaktními platebními metodami.", "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_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_ssl": "Použít SSL",
"use_suggested": "Použít doporučený", "use_suggested": "Použít doporučený",
"use_testnet": "Použijte testNet", "use_testnet": "Použijte testNet",

View file

@ -907,6 +907,7 @@
"use": "Wechsel zu ", "use": "Wechsel zu ",
"use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.", "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_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_ssl": "SSL verwenden",
"use_suggested": "Vorgeschlagen verwenden", "use_suggested": "Vorgeschlagen verwenden",
"use_testnet": "TESTNET verwenden", "use_testnet": "TESTNET verwenden",
@ -991,4 +992,4 @@
"you_will_get": "Konvertieren zu", "you_will_get": "Konvertieren zu",
"you_will_send": "Konvertieren von", "you_will_send": "Konvertieren von",
"yy": "YY" "yy": "YY"
} }

View file

@ -905,6 +905,7 @@
"use": "Switch to ", "use": "Switch to ",
"use_card_info_three": "Use the digital card online or with contactless payment methods.", "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_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_ssl": "Use SSL",
"use_suggested": "Use Suggested", "use_suggested": "Use Suggested",
"use_testnet": "Use Testnet", "use_testnet": "Use Testnet",
@ -988,4 +989,4 @@
"you_will_get": "Convert to", "you_will_get": "Convert to",
"you_will_send": "Convert from", "you_will_send": "Convert from",
"yy": "YY" "yy": "YY"
} }

View file

@ -906,6 +906,7 @@
"use": "Utilizar a ", "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_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_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_ssl": "Utiliza SSL",
"use_suggested": "Usar sugerido", "use_suggested": "Usar sugerido",
"use_testnet": "Usar TestNet", "use_testnet": "Usar TestNet",
@ -989,4 +990,4 @@
"you_will_get": "Convertir a", "you_will_get": "Convertir a",
"you_will_send": "Convertir de", "you_will_send": "Convertir de",
"yy": "YY" "yy": "YY"
} }

View file

@ -905,6 +905,7 @@
"use": "Changer vers code PIN à ", "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_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_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_ssl": "Utiliser SSL",
"use_suggested": "Suivre la suggestion", "use_suggested": "Suivre la suggestion",
"use_testnet": "Utiliser TestNet", "use_testnet": "Utiliser TestNet",
@ -988,4 +989,4 @@
"you_will_get": "Convertir vers", "you_will_get": "Convertir vers",
"you_will_send": "Convertir depuis", "you_will_send": "Convertir depuis",
"yy": "AA" "yy": "AA"
} }

View file

@ -907,6 +907,7 @@
"use": "Canja zuwa", "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_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_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_ssl": "Yi amfani da SSL",
"use_suggested": "Amfani da Shawarwari", "use_suggested": "Amfani da Shawarwari",
"use_testnet": "Amfani da gwaji", "use_testnet": "Amfani da gwaji",

View file

@ -907,6 +907,7 @@
"use": "उपयोग ", "use": "उपयोग ",
"use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।", "use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।",
"use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।", "use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।",
"use_payjoin": "उपयोग %%%",
"use_ssl": "उपयोग SSL", "use_ssl": "उपयोग SSL",
"use_suggested": "सुझाए गए का प्रयोग करें", "use_suggested": "सुझाए गए का प्रयोग करें",
"use_testnet": "टेस्टनेट का उपयोग करें", "use_testnet": "टेस्टनेट का उपयोग करें",

View file

@ -905,6 +905,7 @@
"use": "Prebaci na", "use": "Prebaci na",
"use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.", "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_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_ssl": "Koristi SSL",
"use_suggested": "Koristite predloženo", "use_suggested": "Koristite predloženo",
"use_testnet": "Koristite TestNet", "use_testnet": "Koristite TestNet",
@ -988,4 +989,4 @@
"you_will_get": "Razmijeni u", "you_will_get": "Razmijeni u",
"you_will_send": "Razmijeni iz", "you_will_send": "Razmijeni iz",
"yy": "GG" "yy": "GG"
} }

View file

@ -905,6 +905,7 @@
"use": "Փոխեք ", "use": "Փոխեք ",
"use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։", "use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։",
"use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։", "use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։",
"use_payjoin": "Օգտագործեք Payjoin",
"use_ssl": "Օգտագործել SSL", "use_ssl": "Օգտագործել SSL",
"use_suggested": "Օգտագործել առաջարկվածը", "use_suggested": "Օգտագործել առաջարկվածը",
"use_testnet": "Օգտագործել Testnet", "use_testnet": "Օգտագործել Testnet",
@ -988,4 +989,4 @@
"you_will_get": "Ստացեք", "you_will_get": "Ստացեք",
"you_will_send": "Փոխանակեք", "you_will_send": "Փոխանակեք",
"yy": "ՏՏ" "yy": "ՏՏ"
} }

View file

@ -908,6 +908,7 @@
"use": "Beralih ke ", "use": "Beralih ke ",
"use_card_info_three": "Gunakan kartu digital secara online atau dengan metode pembayaran tanpa kontak.", "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_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_ssl": "Gunakan SSL",
"use_suggested": "Gunakan yang Disarankan", "use_suggested": "Gunakan yang Disarankan",
"use_testnet": "Gunakan TestNet", "use_testnet": "Gunakan TestNet",
@ -991,4 +992,4 @@
"you_will_get": "Konversi ke", "you_will_get": "Konversi ke",
"you_will_send": "Konversi dari", "you_will_send": "Konversi dari",
"yy": "YY" "yy": "YY"
} }

View file

@ -907,6 +907,7 @@
"use": "Passa a ", "use": "Passa a ",
"use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.", "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_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_ssl": "Usa SSL",
"use_suggested": "Usa suggerito", "use_suggested": "Usa suggerito",
"use_testnet": "Usa TestNet", "use_testnet": "Usa TestNet",
@ -991,4 +992,4 @@
"you_will_get": "Converti a", "you_will_get": "Converti a",
"you_will_send": "Conveti da", "you_will_send": "Conveti da",
"yy": "YY" "yy": "YY"
} }

View file

@ -906,6 +906,7 @@
"use": "使用する ", "use": "使用する ",
"use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。", "use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。",
"use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。", "use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。",
"use_payjoin": "使用 ",
"use_ssl": "SSLを使用する", "use_ssl": "SSLを使用する",
"use_suggested": "推奨を使用", "use_suggested": "推奨を使用",
"use_testnet": "テストネットを使用します", "use_testnet": "テストネットを使用します",

View file

@ -906,6 +906,7 @@
"use": "사용하다 ", "use": "사용하다 ",
"use_card_info_three": "디지털 카드를 온라인 또는 비접촉식 결제 수단으로 사용하십시오.", "use_card_info_three": "디지털 카드를 온라인 또는 비접촉식 결제 수단으로 사용하십시오.",
"use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", "use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.",
"use_payjoin": "사용 Payjoin",
"use_ssl": "SSL 사용", "use_ssl": "SSL 사용",
"use_suggested": "추천 사용", "use_suggested": "추천 사용",
"use_testnet": "TestNet을 사용하십시오", "use_testnet": "TestNet을 사용하십시오",
@ -990,4 +991,4 @@
"you_will_send": "다음에서 변환", "you_will_send": "다음에서 변환",
"YY": "YY", "YY": "YY",
"yy": "YY" "yy": "YY"
} }

View file

@ -905,6 +905,7 @@
"use": "သို့ပြောင်းပါ။", "use": "သို့ပြောင်းပါ။",
"use_card_info_three": "ဒစ်ဂျစ်တယ်ကတ်ကို အွန်လိုင်း သို့မဟုတ် ထိတွေ့မှုမဲ့ ငွေပေးချေမှုနည်းလမ်းများဖြင့် အသုံးပြုပါ။", "use_card_info_three": "ဒစ်ဂျစ်တယ်ကတ်ကို အွန်လိုင်း သို့မဟုတ် ထိတွေ့မှုမဲ့ ငွေပေးချေမှုနည်းလမ်းများဖြင့် အသုံးပြုပါ။",
"use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။", "use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။",
"use_payjoin": "%% Chair ကိုအသုံးပြုပါ",
"use_ssl": "SSL ကိုသုံးပါ။", "use_ssl": "SSL ကိုသုံးပါ။",
"use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။", "use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။",
"use_testnet": "testnet ကိုသုံးပါ", "use_testnet": "testnet ကိုသုံးပါ",

View file

@ -905,6 +905,7 @@
"use": "Gebruik ", "use": "Gebruik ",
"use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.", "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_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_ssl": "Gebruik SSL",
"use_suggested": "Gebruik aanbevolen", "use_suggested": "Gebruik aanbevolen",
"use_testnet": "Gebruik testnet", "use_testnet": "Gebruik testnet",
@ -989,4 +990,4 @@
"you_will_get": "Converteren naar", "you_will_get": "Converteren naar",
"you_will_send": "Converteren van", "you_will_send": "Converteren van",
"yy": "JJ" "yy": "JJ"
} }

View file

@ -905,6 +905,7 @@
"use": "Użyj ", "use": "Użyj ",
"use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.", "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_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_ssl": "Użyj SSL",
"use_suggested": "Użyj sugerowane", "use_suggested": "Użyj sugerowane",
"use_testnet": "Użyj testne", "use_testnet": "Użyj testne",
@ -988,4 +989,4 @@
"you_will_get": "Konwertuj na", "you_will_get": "Konwertuj na",
"you_will_send": "Konwertuj z", "you_will_send": "Konwertuj z",
"yy": "RR" "yy": "RR"
} }

View file

@ -907,6 +907,7 @@
"use": "Use PIN de ", "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_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_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_ssl": "Use SSL",
"use_suggested": "Uso sugerido", "use_suggested": "Uso sugerido",
"use_testnet": "Use testNet", "use_testnet": "Use testNet",
@ -991,4 +992,4 @@
"you_will_get": "Converter para", "you_will_get": "Converter para",
"you_will_send": "Converter de", "you_will_send": "Converter de",
"yy": "aa" "yy": "aa"
} }

View file

@ -906,6 +906,7 @@
"use": "Использовать ", "use": "Использовать ",
"use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.", "use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.",
"use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.", "use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.",
"use_payjoin": "Использовать %%%",
"use_ssl": "Использовать SSL", "use_ssl": "Использовать SSL",
"use_suggested": "Использовать предложенный", "use_suggested": "Использовать предложенный",
"use_testnet": "Используйте Testnet", "use_testnet": "Используйте Testnet",

View file

@ -905,6 +905,7 @@
"use": "สลับไปที่ ", "use": "สลับไปที่ ",
"use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ", "use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ",
"use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล", "use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล",
"use_payjoin": "ใช้ %%%",
"use_ssl": "ใช้ SSL", "use_ssl": "ใช้ SSL",
"use_suggested": "ใช้ที่แนะนำ", "use_suggested": "ใช้ที่แนะนำ",
"use_testnet": "ใช้ testnet", "use_testnet": "ใช้ testnet",

View file

@ -905,6 +905,7 @@
"use": "Lumipat sa ", "use": "Lumipat sa ",
"use_card_info_three": "Gamitin ang digital card online o sa mga paraan ng pagbabayad na walang contact.", "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_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_ssl": "Gumamit ng SSL",
"use_suggested": "Gumamit ng iminungkahing", "use_suggested": "Gumamit ng iminungkahing",
"use_testnet": "Gumamit ng testnet", "use_testnet": "Gumamit ng testnet",
@ -988,4 +989,4 @@
"you_will_get": "I-convert sa", "you_will_get": "I-convert sa",
"you_will_send": "I-convert mula sa", "you_will_send": "I-convert mula sa",
"yy": "YY" "yy": "YY"
} }

View file

@ -905,6 +905,7 @@
"use": "Şuna geç: ", "use": "Şuna geç: ",
"use_card_info_three": "Dijital kartı çevrimiçi olarak veya temassız ödeme yöntemleriyle kullanın.", "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_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_ssl": "SSL kullan",
"use_suggested": "Önerileni Kullan", "use_suggested": "Önerileni Kullan",
"use_testnet": "TestNet kullanın", "use_testnet": "TestNet kullanın",
@ -988,4 +989,4 @@
"you_will_get": "Biçimine dönüştür:", "you_will_get": "Biçimine dönüştür:",
"you_will_send": "Biçiminden dönüştür:", "you_will_send": "Biçiminden dönüştür:",
"yy": "YY" "yy": "YY"
} }

View file

@ -906,6 +906,7 @@
"use": "Використати ", "use": "Використати ",
"use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.", "use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.",
"use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.", "use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.",
"use_payjoin": "Використовуйте %%",
"use_ssl": "Використати SSL", "use_ssl": "Використати SSL",
"use_suggested": "Використати запропоноване", "use_suggested": "Використати запропоноване",
"use_testnet": "Використовуйте тестову мережу", "use_testnet": "Використовуйте тестову мережу",

View file

@ -907,6 +907,7 @@
"use": "تبدیل کرنا", "use": "تبدیل کرنا",
"use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔", "use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔",
"use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔", "use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔",
"use_payjoin": "٪٪٪ کا استعمال کریں",
"use_ssl": "SSL استعمال کریں۔", "use_ssl": "SSL استعمال کریں۔",
"use_suggested": "تجویز کردہ استعمال کریں۔", "use_suggested": "تجویز کردہ استعمال کریں۔",
"use_testnet": "ٹیسٹ نیٹ استعمال کریں", "use_testnet": "ٹیسٹ نیٹ استعمال کریں",

View file

@ -904,6 +904,7 @@
"use": "Chuyển sang", "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_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_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_ssl": "Sử dụng SSL",
"use_suggested": "Sử dụng đề xuất", "use_suggested": "Sử dụng đề xuất",
"use_testnet": "Sử dụng Testnet", "use_testnet": "Sử dụng Testnet",
@ -987,4 +988,4 @@
"you_will_get": "Chuyển đổi thành", "you_will_get": "Chuyển đổi thành",
"you_will_send": "Chuyển đổi từ", "you_will_send": "Chuyển đổi từ",
"yy": "YY" "yy": "YY"
} }

View file

@ -906,6 +906,7 @@
"use": "Lo", "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_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_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_ssl": "Lo SSL",
"use_suggested": "Lo àbá", "use_suggested": "Lo àbá",
"use_testnet": "Lo tele", "use_testnet": "Lo tele",

View file

@ -905,6 +905,7 @@
"use": "切换使用", "use": "切换使用",
"use_card_info_three": "在线使用电子卡或使用非接触式支付方式。", "use_card_info_three": "在线使用电子卡或使用非接触式支付方式。",
"use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。", "use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。",
"use_payjoin": "使用 ",
"use_ssl": "使用SSL", "use_ssl": "使用SSL",
"use_suggested": "使用建议", "use_suggested": "使用建议",
"use_testnet": "使用TestNet", "use_testnet": "使用TestNet",

View file

@ -240,11 +240,12 @@ abstract class Bitcoin {
String? getUnusedMwebAddress(Object wallet); String? getUnusedMwebAddress(Object wallet);
String? getUnusedSegwitAddress(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<Sender> buildPayjoinRequest(String originalPsbt, dynamic pjUri, int fee);
Future<String> buildOriginalPsbt(Object wallet, int fee, double amount, Object credentials); Future<String> buildOriginalPsbt(Object wallet, int fee, double amount, Object credentials);
Future<String> requestAndPollV2Proposal(Sender sender); Future<String> requestAndPollV2Proposal(Sender sender);
Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString, Object credentials); Future<PendingBitcoinTransaction> extractPjTx(Object wallet, String psbtString);
} }
"""; """;