mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-04-27 14:04:46 +00:00
Payjoin Sender are now isolated
This commit is contained in:
parent
4c4fd8e645
commit
8f4dad9134
45 changed files with 452 additions and 295 deletions
cw_bitcoin/lib
bitcoin_payjoin.dartbitcoin_wallet.dartbitcoin_wallet_addresses.dartelectrum_wallet.dart
payjoin
manager.dartpayjoin_receive_worker.dartpayjoin_send_worker.dartpayjoin_session_errors.dartstorage.dart
pending_bitcoin_transaction.dartlib
bitcoin
reactions
src/screens/settings
view_model
send
settings
wallet_address_list
res/values
strings_ar.arbstrings_bg.arbstrings_cs.arbstrings_de.arbstrings_en.arbstrings_es.arbstrings_fr.arbstrings_ha.arbstrings_hi.arbstrings_hr.arbstrings_hy.arbstrings_id.arbstrings_it.arbstrings_ja.arbstrings_ko.arbstrings_my.arbstrings_nl.arbstrings_pl.arbstrings_pt.arbstrings_ru.arbstrings_th.arbstrings_tl.arbstrings_tr.arbstrings_uk.arbstrings_ur.arbstrings_vi.arbstrings_yo.arbstrings_zh.arb
tool
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>>{};
|
111
cw_bitcoin/lib/payjoin/payjoin_send_worker.dart
Normal file
111
cw_bitcoin/lib/payjoin/payjoin_send_worker.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
16
cw_bitcoin/lib/payjoin/payjoin_session_errors.dart
Normal file
16
cw_bitcoin/lib/payjoin/payjoin_session_errors.dart
Normal 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._();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -905,6 +905,7 @@
|
|||
"use": "التبديل إلى",
|
||||
"use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.",
|
||||
"use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.",
|
||||
"use_payjoin": "يستخدم ٪٪٪",
|
||||
"use_ssl": "استخدم SSL",
|
||||
"use_suggested": "استخدام المقترح",
|
||||
"use_testnet": "استخدم testnet",
|
||||
|
|
|
@ -905,6 +905,7 @@
|
|||
"use": "Смяна на ",
|
||||
"use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.",
|
||||
"use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.",
|
||||
"use_payjoin": "Използвайте %%",
|
||||
"use_ssl": "Използване на SSL",
|
||||
"use_suggested": "Използване на предложеното",
|
||||
"use_testnet": "Използвайте TestNet",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -907,6 +907,7 @@
|
|||
"use": "उपयोग ",
|
||||
"use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।",
|
||||
"use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।",
|
||||
"use_payjoin": "उपयोग %%%",
|
||||
"use_ssl": "उपयोग SSL",
|
||||
"use_suggested": "सुझाए गए का प्रयोग करें",
|
||||
"use_testnet": "टेस्टनेट का उपयोग करें",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "ՏՏ"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -906,6 +906,7 @@
|
|||
"use": "使用する ",
|
||||
"use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。",
|
||||
"use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。",
|
||||
"use_payjoin": "使用 %%%",
|
||||
"use_ssl": "SSLを使用する",
|
||||
"use_suggested": "推奨を使用",
|
||||
"use_testnet": "テストネットを使用します",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ကိုသုံးပါ",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -906,6 +906,7 @@
|
|||
"use": "Использовать ",
|
||||
"use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.",
|
||||
"use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.",
|
||||
"use_payjoin": "Использовать %%%",
|
||||
"use_ssl": "Использовать SSL",
|
||||
"use_suggested": "Использовать предложенный",
|
||||
"use_testnet": "Используйте Testnet",
|
||||
|
|
|
@ -905,6 +905,7 @@
|
|||
"use": "สลับไปที่ ",
|
||||
"use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ",
|
||||
"use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล",
|
||||
"use_payjoin": "ใช้ %%%",
|
||||
"use_ssl": "ใช้ SSL",
|
||||
"use_suggested": "ใช้ที่แนะนำ",
|
||||
"use_testnet": "ใช้ testnet",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -906,6 +906,7 @@
|
|||
"use": "Використати ",
|
||||
"use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.",
|
||||
"use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.",
|
||||
"use_payjoin": "Використовуйте %%",
|
||||
"use_ssl": "Використати SSL",
|
||||
"use_suggested": "Використати запропоноване",
|
||||
"use_testnet": "Використовуйте тестову мережу",
|
||||
|
|
|
@ -907,6 +907,7 @@
|
|||
"use": "تبدیل کرنا",
|
||||
"use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔",
|
||||
"use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔",
|
||||
"use_payjoin": "٪٪٪ کا استعمال کریں",
|
||||
"use_ssl": "SSL استعمال کریں۔",
|
||||
"use_suggested": "تجویز کردہ استعمال کریں۔",
|
||||
"use_testnet": "ٹیسٹ نیٹ استعمال کریں",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -905,6 +905,7 @@
|
|||
"use": "切换使用",
|
||||
"use_card_info_three": "在线使用电子卡或使用非接触式支付方式。",
|
||||
"use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。",
|
||||
"use_payjoin": "使用 %%%",
|
||||
"use_ssl": "使用SSL",
|
||||
"use_suggested": "使用建议",
|
||||
"use_testnet": "使用TestNet",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
""";
|
||||
|
||||
|
|
Loading…
Reference in a new issue