implement-payjoin (#1949)

* Initial Payjoin * Initial Payjoin * More payjoin stuff * Minor fixes * Minor fixes * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix minor bug causes by data inconsistency in the btc utxos * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Initial Payjoin * Initial Payjoin * More payjoin stuff * Minor fixes * Minor fixes * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix minor bug causes by data inconsistency in the btc utxos * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix Rebase issues * Move PJ Receiver to isolate * Add Payjoin Setting * Payjoin Sender are now isolated * Added Payjoin sessions to tx overview. Fix Fee issue with payjoin * Clean up code * Fix taproot for payjoin * Fix CI Errors * Add Payjoin UI elements and details page * Add Payjoin UI elements and details page * Fix Translations * feat: Detect Payjoin URIs in pasted text and show to the User sending Payjoin * feat: rename pjUri to payjoinURI for more code clarity * Update res/values/strings_pl.arb Co-authored-by: cyan <cyjan@mrcyjanek.net> * Update cw_bitcoin/lib/payjoin/manager.dart Co-authored-by: cyan <cyjan@mrcyjanek.net> * Update cw_bitcoin/lib/payjoin/manager.dart Co-authored-by: cyan <cyjan@mrcyjanek.net> * feat: Disable Payjoin per default * feat: Disable Payjoin fully if disabled or no Inputs available * feat: Resume Payjoin if app comes back to foreground * chore: Revert overly aggressive code formats * feat: show correct Payjoin amount for receivers * feat: Improved payjoin status * feat: Show payjoin errors on payjoin details screen * deps: update flutter to 3.27.4 * feat: Revert localisations * bug: Remove duplicate transaction id on payjoin details * style: remove double await in payjoin sender * refactor(cw_bitcoin): Refactor method signatures and convert constructor to factory * refactor(cw_bitcoin): Refactor wallet service and PSBT signer for cleaner code Removed unnecessary `CakeHive` dependency and refactored `BitcoinWallet` initialization to use `payjoinSessionSource`. Improved code readability in `PsbtSigner` by reformatting lines and simplifying constructor methods for `UtxoWithPrivateKey`. * fix: Resume Payjoin Sessions and load PJUri after sleep * feat: Add "Copy Payjoin URL button" to receive screen * fix: Add "Payjoin enabled"-Box below QR Code on the receive screen * fix: Set payjoin_enabled color to black independent of the theme * refactor: Payjoin session management and cleanup unused code. --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: cyan <cyjan@mrcyjanek.net>

Konstantin Ullrich committed May 12, 2025 at 19:33 UTC 82e3ebf4fa9839e56da02ca53e55c64098c66394
84 files changed +2622 -198
assets/images/payjoin.png
Binary files /dev/null and b/assets/images/payjoin.png differ
cw_bitcoin/lib/address_from_output.dart
+29 -15
@@ -2,22 +2,36 @@ import 'package:bitcoin_base/bitcoin_base.dart';
2
3 String addressFromOutputScript(Script script, BasedUtxoNetwork network) {
4 try {
5 - switch (script.getAddressType()) {
6 - case P2pkhAddressType.p2pkh:
7 - return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network);
8 - case P2shAddressType.p2pkInP2sh:
9 - return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
10 - case SegwitAddresType.p2wpkh:
11 - return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network);
12 - case P2shAddressType.p2pkhInP2sh:
13 - return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
14 - case SegwitAddresType.p2wsh:
15 - return P2wshAddress.fromScriptPubkey(script: script).toAddress(network);
16 - case SegwitAddresType.p2tr:
17 - return P2trAddress.fromScriptPubkey(script: script).toAddress(network);
18 - default:
19 - }
5 + return addressFromScript(script, network).toAddress(network);
6 } catch (_) {}
7
8 return '';
9 }
10 +
11 +BitcoinBaseAddress addressFromScript(Script script,
12 + [BasedUtxoNetwork network = BitcoinNetwork.mainnet]) {
13 + final addressType = script.getAddressType();
14 + if (addressType == null) {
15 + throw ArgumentError("Invalid script");
16 + }
17 +
18 + switch (addressType) {
19 + case P2pkhAddressType.p2pkh:
20 + return P2pkhAddress.fromScriptPubkey(
21 + script: script, network: BitcoinNetwork.mainnet);
22 + case P2shAddressType.p2pkhInP2sh:
23 + return P2shAddress.fromScriptPubkey(
24 + script: script, network: BitcoinNetwork.mainnet);
25 + case SegwitAddresType.p2wpkh:
26 + return P2wpkhAddress.fromScriptPubkey(
27 + script: script, network: BitcoinNetwork.mainnet);
28 + case SegwitAddresType.p2wsh:
29 + return P2wshAddress.fromScriptPubkey(
30 + script: script, network: BitcoinNetwork.mainnet);
31 + case SegwitAddresType.p2tr:
32 + return P2trAddress.fromScriptPubkey(
33 + script: script, network: BitcoinNetwork.mainnet);
34 + }
35 +
36 + throw ArgumentError("Invalid script");
37 +}
cw_bitcoin/lib/bitcoin_transaction_credentials.dart
+8 -2
@@ -3,11 +3,17 @@ import 'package:cw_core/output_info.dart';
3 import 'package:cw_core/unspent_coin_type.dart';
4
5 class BitcoinTransactionCredentials {
6 - BitcoinTransactionCredentials(this.outputs,
7 - {required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any});
6 + BitcoinTransactionCredentials(
7 + this.outputs, {
8 + required this.priority,
9 + this.feeRate,
10 + this.coinTypeToSpendFrom = UnspentCoinType.any,
11 + this.payjoinUri,
12 + });
13
14 final List<OutputInfo> outputs;
15 final BitcoinTransactionPriority? priority;
16 final int? feeRate;
17 final UnspentCoinType coinTypeToSpendFrom;
18 + final String? payjoinUri;
19 }
cw_bitcoin/lib/bitcoin_wallet.dart
+183 -44
@@ -3,22 +3,33 @@ import 'dart:convert';
3 import 'package:bip39/bip39.dart' as bip39;
4 import 'package:bitcoin_base/bitcoin_base.dart';
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 +import 'package:cw_bitcoin/address_from_output.dart';
7 import 'package:cw_bitcoin/bitcoin_address_record.dart';
8 import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
8 -import 'package:cw_bitcoin/psbt_transaction_builder.dart';
9 -import 'package:cw_core/encryption_file_utils.dart';
10 -import 'package:cw_bitcoin/electrum_derivations.dart';
9 +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
10 import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
11 import 'package:cw_bitcoin/electrum_balance.dart';
12 +import 'package:cw_bitcoin/electrum_derivations.dart';
13 import 'package:cw_bitcoin/electrum_wallet.dart';
14 import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
15 +import 'package:cw_bitcoin/payjoin/manager.dart';
16 +import 'package:cw_bitcoin/payjoin/storage.dart';
17 +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
18 +import 'package:cw_bitcoin/psbt/signer.dart';
19 +import 'package:cw_bitcoin/psbt/transaction_builder.dart';
20 +import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
21 +import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
22 import 'package:cw_core/crypto_currency.dart';
23 +import 'package:cw_core/encryption_file_utils.dart';
24 +import 'package:cw_core/payjoin_session.dart';
25 +import 'package:cw_core/pending_transaction.dart';
26 import 'package:cw_core/unspent_coins_info.dart';
27 import 'package:cw_core/wallet_info.dart';
28 import 'package:cw_core/wallet_keys_file.dart';
29 import 'package:flutter/foundation.dart';
30 import 'package:hive/hive.dart';
31 import 'package:ledger_bitcoin/ledger_bitcoin.dart';
32 +import 'package:ledger_bitcoin/psbt.dart';
33 import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
34 import 'package:mobx/mobx.dart';
35
@@ -31,6 +42,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
42 required String password,
43 required WalletInfo walletInfo,
44 required Box<UnspentCoinsInfo> unspentCoinsInfo,
45 + required Box<PayjoinSession> payjoinBox,
46 required EncryptionFileUtils encryptionFileUtils,
47 Uint8List? seedBytes,
48 String? mnemonic,
@@ -71,20 +83,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
83 // String derivationPath = walletInfo.derivationInfo!.derivationPath!;
84 // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
85 // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
74 - walletAddresses = BitcoinWalletAddresses(
75 - walletInfo,
76 - initialAddresses: initialAddresses,
77 - initialRegularAddressIndex: initialRegularAddressIndex,
78 - initialChangeAddressIndex: initialChangeAddressIndex,
79 - initialSilentAddresses: initialSilentAddresses,
80 - initialSilentAddressIndex: initialSilentAddressIndex,
81 - mainHd: hd,
82 - sideHd: accountHD.childKey(Bip32KeyIndex(1)),
83 - network: networkParam ?? network,
84 - masterHd:
85 - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
86 - isHardwareWallet: walletInfo.isHardwareWallet,
87 - );
86 +
87 + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
88 + walletAddresses = BitcoinWalletAddresses(walletInfo,
89 + initialAddresses: initialAddresses,
90 + initialRegularAddressIndex: initialRegularAddressIndex,
91 + initialChangeAddressIndex: initialChangeAddressIndex,
92 + initialSilentAddresses: initialSilentAddresses,
93 + initialSilentAddressIndex: initialSilentAddressIndex,
94 + mainHd: hd,
95 + sideHd: accountHD.childKey(Bip32KeyIndex(1)),
96 + network: networkParam ?? network,
97 + masterHd:
98 + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
99 + isHardwareWallet: walletInfo.isHardwareWallet,
100 + payjoinManager: payjoinManager);
101
102 autorun((_) {
103 this.walletAddresses.isEnabledAutoGenerateSubaddress =
@@ -100,6 +113,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
113 required String password,
114 required WalletInfo walletInfo,
115 required Box<UnspentCoinsInfo> unspentCoinsInfo,
116 + required Box<PayjoinSession> payjoinBox,
117 required EncryptionFileUtils encryptionFileUtils,
118 String? passphrase,
119 String? addressPageType,
@@ -122,9 +136,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
136 break;
137 case DerivationType.electrum:
138 default:
125 - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
139 + seedBytes =
140 + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
141 break;
142 }
143 +
144 return BitcoinWallet(
145 mnemonic: mnemonic,
146 passphrase: passphrase ?? "",
@@ -141,6 +157,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
157 initialChangeAddressIndex: initialChangeAddressIndex,
158 addressPageType: addressPageType,
159 networkParam: network,
160 + payjoinBox: payjoinBox,
161 );
162 }
163
@@ -148,6 +165,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
165 required String name,
166 required WalletInfo walletInfo,
167 required Box<UnspentCoinsInfo> unspentCoinsInfo,
168 + required Box<PayjoinSession> payjoinBox,
169 required String password,
170 required EncryptionFileUtils encryptionFileUtils,
171 required bool alwaysScan,
@@ -204,7 +222,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
222 if (mnemonic != null) {
223 switch (walletInfo.derivationInfo!.derivationType) {
224 case DerivationType.electrum:
207 - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
225 + seedBytes =
226 + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
227 break;
228 case DerivationType.bip39:
229 default:
@@ -217,24 +236,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
236 }
237
238 return BitcoinWallet(
220 - mnemonic: mnemonic,
221 - xpub: keysData.xPub,
222 - password: password,
223 - passphrase: passphrase,
224 - walletInfo: walletInfo,
225 - unspentCoinsInfo: unspentCoinsInfo,
226 - initialAddresses: snp?.addresses,
227 - initialSilentAddresses: snp?.silentAddresses,
228 - initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
229 - initialBalance: snp?.balance,
230 - encryptionFileUtils: encryptionFileUtils,
231 - seedBytes: seedBytes,
232 - initialRegularAddressIndex: snp?.regularAddressIndex,
233 - initialChangeAddressIndex: snp?.changeAddressIndex,
234 - addressPageType: snp?.addressPageType,
235 - networkParam: network,
236 - alwaysScan: alwaysScan,
237 - );
239 + mnemonic: mnemonic,
240 + xpub: keysData.xPub,
241 + password: password,
242 + passphrase: passphrase,
243 + walletInfo: walletInfo,
244 + unspentCoinsInfo: unspentCoinsInfo,
245 + initialAddresses: snp?.addresses,
246 + initialSilentAddresses: snp?.silentAddresses,
247 + initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
248 + initialBalance: snp?.balance,
249 + encryptionFileUtils: encryptionFileUtils,
250 + seedBytes: seedBytes,
251 + initialRegularAddressIndex: snp?.regularAddressIndex,
252 + initialChangeAddressIndex: snp?.changeAddressIndex,
253 + addressPageType: snp?.addressPageType,
254 + networkParam: network,
255 + alwaysScan: alwaysScan,
256 + payjoinBox: payjoinBox);
257 }
258
259 LedgerConnection? _ledgerConnection;
@@ -247,20 +266,25 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
266 derivationPath: walletInfo.derivationInfo!.derivationPath!);
267 }
268
250 - @override
251 - Future<BtcTransaction> buildHardwareWalletTransaction({
269 + late final PayjoinManager payjoinManager;
270 +
271 + bool get isPayjoinAvailable => unspentCoinsInfo.values
272 + .where((element) =>
273 + element.walletId == id && element.isSending && !element.isFrozen)
274 + .isNotEmpty;
275 +
276 + Future<PsbtV2> buildPsbt({
277 required List<BitcoinBaseOutput> outputs,
278 required BigInt fee,
279 required BasedUtxoNetwork network,
280 required List<UtxoWithAddress> utxos,
281 required Map<String, PublicKeyWithDerivationPath> publicKeys,
282 + required Uint8List masterFingerprint,
283 String? memo,
284 bool enableRBF = false,
285 BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
286 BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
287 }) async {
262 - final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
263 -
288 final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
289 for (final utxo in utxos) {
290 final rawTx =
@@ -278,13 +302,128 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
302 ));
303 }
304
281 - final psbt = PSBTTransactionBuild(
282 - inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
305 + return PSBTTransactionBuild(
306 + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF)
307 + .psbt;
308 + }
309 +
310 + @override
311 + Future<BtcTransaction> buildHardwareWalletTransaction({
312 + required List<BitcoinBaseOutput> outputs,
313 + required BigInt fee,
314 + required BasedUtxoNetwork network,
315 + required List<UtxoWithAddress> utxos,
316 + required Map<String, PublicKeyWithDerivationPath> publicKeys,
317 + String? memo,
318 + bool enableRBF = false,
319 + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
320 + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
321 + }) async {
322 + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
323 +
324 + final psbt = await buildPsbt(
325 + outputs: outputs,
326 + fee: fee,
327 + network: network,
328 + utxos: utxos,
329 + publicKeys: publicKeys,
330 + masterFingerprint: masterFingerprint,
331 + memo: memo,
332 + enableRBF: enableRBF,
333 + inputOrdering: inputOrdering,
334 + outputOrdering: outputOrdering,
335 + );
336
284 - final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt);
337 + final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt);
338 return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
339 }
340
341 + @override
342 + Future<PendingTransaction> createTransaction(Object credentials) async {
343 + credentials = credentials as BitcoinTransactionCredentials;
344 +
345 + final tx = (await super.createTransaction(credentials))
346 + as PendingBitcoinTransaction;
347 +
348 + final payjoinUri = credentials.payjoinUri;
349 + if (payjoinUri == null) return tx;
350 +
351 + final transaction = await buildPsbt(
352 + utxos: tx.utxos,
353 + outputs: tx.outputs
354 + .map((e) => BitcoinOutput(
355 + address: addressFromScript(e.scriptPubKey),
356 + value: e.amount,
357 + isSilentPayment: e.isSilentPayment,
358 + isChange: e.isChange,
359 + ))
360 + .toList(),
361 + fee: BigInt.from(tx.fee),
362 + network: network,
363 + memo: credentials.outputs.first.memo,
364 + outputOrdering: BitcoinOrdering.none,
365 + enableRBF: true,
366 + publicKeys: tx.publicKeys!,
367 + masterFingerprint: Uint8List(0));
368 +
369 + final originalPsbt = await signPsbt(
370 + base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
371 +
372 + tx.commitOverride = () async {
373 + final sender = await payjoinManager.initSender(
374 + payjoinUri, originalPsbt, int.parse(tx.feeRate));
375 + payjoinManager.spawnNewSender(
376 + sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
377 + };
378 +
379 + return tx;
380 + }
381 +
382 + List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
383 + .where((e) => (e.isSending && !e.isFrozen))
384 + .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
385 + .toList();
386 +
387 + Future<void> commitPsbt(String finalizedPsbt) {
388 + final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
389 +
390 + final btcTx =
391 + BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
392 +
393 + return PendingBitcoinTransaction(
394 + btcTx,
395 + type,
396 + electrumClient: electrumClient,
397 + amount: 0,
398 + fee: 0,
399 + feeRate: "",
400 + network: network,
401 + hasChange: true,
402 + ).commit();
403 + }
404 +
405 + Future<String> signPsbt(
406 + String preProcessedPsbt, List<UtxoWithPrivateKey> utxos) async {
407 + final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt));
408 +
409 + await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) {
410 + return utxo.utxo.isP2tr()
411 + ? key.signTapRoot(
412 + txDigest,
413 + sighash: sighash,
414 + tweak: utxo.utxo.isSilentPayment != true,
415 + )
416 + : key.signInput(txDigest, sigHash: sighash);
417 + }, (txId, vout) async {
418 + final txHex = await electrumClient.getTransactionHex(hash: txId);
419 + final output = BtcTransaction.fromRaw(txHex).outputs[vout];
420 + return TaprootAmountScriptPair(output.amount, output.scriptPubKey);
421 + });
422 +
423 + psbt.finalizeV0();
424 + return base64Encode(psbt.asPsbtV0());
425 + }
426 +
427 @override
428 Future<String> signMessage(String message, {String? address = null}) async {
429 if (walletInfo.isHardwareWallet) {
cw_bitcoin/lib/bitcoin_wallet_addresses.dart
+26
@@ -1,10 +1,13 @@
1 import 'package:bitcoin_base/bitcoin_base.dart';
2 import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
3 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
4 +import 'package:cw_bitcoin/payjoin/manager.dart';
5 import 'package:cw_bitcoin/utils.dart';
6 import 'package:cw_core/unspent_coin_type.dart';
7 +import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:cw_core/wallet_info.dart';
9 import 'package:mobx/mobx.dart';
10 +import 'package:payjoin_flutter/receive.dart' as payjoin;
11
12 part 'bitcoin_wallet_addresses.g.dart';
13
@@ -17,6 +20,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
20 required super.sideHd,
21 required super.network,
22 required super.isHardwareWallet,
23 + required this.payjoinManager,
24 super.initialAddresses,
25 super.initialRegularAddressIndex,
26 super.initialChangeAddressIndex,
@@ -25,6 +29,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
29 super.masterHd,
30 }) : super(walletInfo);
31
32 + final PayjoinManager payjoinManager;
33 +
34 + @observable
35 + payjoin.Receiver? currentPayjoinReceiver;
36 +
37 + @computed
38 + String? get payjoinEndpoint =>
39 + currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint();
40 +
41 @override
42 String getAddress(
43 {required int index,
@@ -45,4 +58,17 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
58
59 return generateP2WPKHAddress(hd: hd, index: index, network: network);
60 }
61 +
62 + Future<void> initPayjoin() async {
63 + currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
64 +
65 + payjoinManager.resumeSessions();
66 + }
67 +
68 + Future<void> newPayjoinReceiver() async {
69 + currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
70 +
71 + printV("Initializing new Payjoin Receiver");
72 + payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!);
73 + }
74 }
cw_bitcoin/lib/bitcoin_wallet_service.dart
+10 -2
@@ -5,6 +5,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
5 import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
6 import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
7 import 'package:cw_core/encryption_file_utils.dart';
8 +import 'package:cw_core/payjoin_session.dart';
9 import 'package:cw_core/unspent_coins_info.dart';
10 import 'package:cw_core/wallet_base.dart';
11 import 'package:cw_core/wallet_service.dart';
@@ -21,10 +22,12 @@ class BitcoinWalletService extends WalletService<
22 BitcoinRestoreWalletFromSeedCredentials,
23 BitcoinRestoreWalletFromWIFCredentials,
24 BitcoinRestoreWalletFromHardware> {
24 - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
25 + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
26 + this.payjoinSessionSource, this.alwaysScan, this.isDirect);
27
28 final Box<WalletInfo> walletInfoSource;
29 final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
30 + final Box<PayjoinSession> payjoinSessionSource;
31 final bool alwaysScan;
32 final bool isDirect;
33
@@ -55,6 +58,7 @@ class BitcoinWalletService extends WalletService<
58 passphrase: credentials.passphrase,
59 walletInfo: credentials.walletInfo!,
60 unspentCoinsInfo: unspentCoinsInfoSource,
61 + payjoinBox: payjoinSessionSource,
62 network: network,
63 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
64 );
@@ -79,6 +83,7 @@ class BitcoinWalletService extends WalletService<
83 name: name,
84 walletInfo: walletInfo,
85 unspentCoinsInfo: unspentCoinsInfoSource,
86 + payjoinBox: payjoinSessionSource,
87 alwaysScan: alwaysScan,
88 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
89 );
@@ -92,6 +97,7 @@ class BitcoinWalletService extends WalletService<
97 name: name,
98 walletInfo: walletInfo,
99 unspentCoinsInfo: unspentCoinsInfoSource,
100 + payjoinBox: payjoinSessionSource,
101 alwaysScan: alwaysScan,
102 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
103 );
@@ -126,6 +132,7 @@ class BitcoinWalletService extends WalletService<
132 name: currentName,
133 walletInfo: currentWalletInfo,
134 unspentCoinsInfo: unspentCoinsInfoSource,
135 + payjoinBox: payjoinSessionSource,
136 alwaysScan: alwaysScan,
137 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
138 );
@@ -147,7 +154,6 @@ class BitcoinWalletService extends WalletService<
154 credentials.walletInfo?.network = network.value;
155 credentials.walletInfo?.derivationInfo?.derivationPath =
156 credentials.hwAccountData.derivationPath;
150 -
157 final wallet = await BitcoinWallet(
158 password: credentials.password!,
159 xpub: credentials.hwAccountData.xpub,
@@ -155,6 +161,7 @@ class BitcoinWalletService extends WalletService<
161 unspentCoinsInfo: unspentCoinsInfoSource,
162 networkParam: network,
163 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
164 + payjoinBox: payjoinSessionSource,
165 );
166 await wallet.save();
167 await wallet.init();
@@ -182,6 +189,7 @@ class BitcoinWalletService extends WalletService<
189 mnemonic: credentials.mnemonic,
190 walletInfo: credentials.walletInfo!,
191 unspentCoinsInfo: unspentCoinsInfoSource,
192 + payjoinBox: payjoinSessionSource,
193 network: network,
194 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
195 );
cw_bitcoin/lib/electrum_wallet.dart
+6
@@ -1188,6 +1188,7 @@ abstract class ElectrumWalletBase
1188 isSendAll: estimatedTx.isSendAll,
1189 hasTaprootInputs: hasTaprootInputs,
1190 utxos: estimatedTx.utxos,
1191 + publicKeys: estimatedTx.publicKeys
1192 )..addListener((transaction) async {
1193 transactionHistory.addOne(transaction);
1194 if (estimatedTx.spendsSilentPayment) {
@@ -1965,6 +1966,11 @@ abstract class ElectrumWalletBase
1966 }
1967 }
1968
1969 + bool isMine(Script script) {
1970 + final derivedAddress = addressFromOutputScript(script, network);
1971 + return addressesSet.contains(derivedAddress);
1972 + }
1973 +
1974 @override
1975 Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
1976 try {
cw_bitcoin/lib/payjoin/manager.dart new
+298
@@ -0,0 +1,298 @@
1 +import 'dart:async';
2 +import 'dart:isolate';
3 +import 'dart:math';
4 +import 'dart:typed_data';
5 +
6 +import 'package:bitcoin_base/bitcoin_base.dart';
7 +import 'package:cw_bitcoin/bitcoin_wallet.dart';
8 +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
9 +import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart';
10 +import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart';
11 +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
12 +import 'package:cw_bitcoin/payjoin/storage.dart';
13 +import 'package:cw_bitcoin/psbt/signer.dart';
14 +import 'package:cw_bitcoin/psbt/utils.dart';
15 +import 'package:cw_core/utils/print_verbose.dart';
16 +import 'package:payjoin_flutter/common.dart';
17 +import 'package:payjoin_flutter/receive.dart';
18 +import 'package:payjoin_flutter/send.dart';
19 +import 'package:payjoin_flutter/uri.dart' as PayjoinUri;
20 +
21 +class PayjoinManager {
22 + PayjoinManager(this._payjoinStorage, this._wallet);
23 +
24 + final PayjoinStorage _payjoinStorage;
25 + final BitcoinWalletBase _wallet;
26 + final Map<String, PayjoinPollerSession> _activePollers = {};
27 +
28 + static const List<String> ohttpRelayUrls = [
29 + 'https://pj.bobspacebkk.com',
30 + 'https://ohttp.achow101.com',
31 + ];
32 +
33 + static Future<PayjoinUri.Url> randomOhttpRelayUrl() => PayjoinUri.Url.fromStr(
34 + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]);
35 +
36 + static const payjoinDirectoryUrl = 'https://payjo.in';
37 +
38 + Future<void> resumeSessions() async {
39 + final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id);
40 +
41 + final spawnedSessions = allSessions.map((session) {
42 + if (session.isSenderSession) {
43 + printV("Resuming Payjoin Sender Session ${session.pjUri!}");
44 + return _spawnSender(
45 + sender: Sender.fromJson(session.sender!),
46 + pjUri: session.pjUri!,
47 + );
48 + }
49 + final receiver = Receiver.fromJson(session.receiver!);
50 + printV("Resuming Payjoin Receiver Session ${receiver.id()}");
51 + return _spawnReceiver(receiver: receiver);
52 + });
53 +
54 + printV("Resumed ${spawnedSessions.length} Payjoin Sessions");
55 + await Future.wait(spawnedSessions);
56 + }
57 +
58 + Future<Sender> initSender(
59 + String pjUriString, String originalPsbt, int networkFeesSatPerVb) async {
60 + try {
61 + final pjUri =
62 + (await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported();
63 + final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250);
64 + final senderBuilder = await SenderBuilder.fromPsbtAndUri(
65 + psbtBase64: originalPsbt,
66 + pjUri: pjUri,
67 + );
68 + return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu);
69 + } catch (e) {
70 + throw Exception('Error initializing Payjoin Sender: $e');
71 + }
72 + }
73 +
74 + Future<void> spawnNewSender({
75 + required Sender sender,
76 + required String pjUrl,
77 + required BigInt amount,
78 + bool isTestnet = false,
79 + }) async {
80 + final pjUri = Uri.parse(pjUrl).queryParameters['pj']!;
81 + await _payjoinStorage.insertSenderSession(
82 + sender, pjUri, _wallet.id, amount);
83 +
84 + return _spawnSender(isTestnet: isTestnet, sender: sender, pjUri: pjUri);
85 + }
86 +
87 + Future<void> _spawnSender({
88 + required Sender sender,
89 + required String pjUri,
90 + bool isTestnet = false,
91 + }) async {
92 + final completer = Completer();
93 + final receivePort = ReceivePort();
94 +
95 + receivePort.listen((message) async {
96 + if (message is Map<String, dynamic>) {
97 + try {
98 + switch (message['type'] as PayjoinSenderRequestTypes) {
99 + case PayjoinSenderRequestTypes.requestPosted:
100 + return;
101 + case PayjoinSenderRequestTypes.psbtToSign:
102 + final proposalPsbt = message['psbt'] as String;
103 + final utxos = _wallet.getUtxoWithPrivateKeys();
104 + final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos);
105 + final txId = getTxIdFromPsbtV0(finalizedPsbt);
106 + _wallet.commitPsbt(finalizedPsbt);
107 +
108 + _cleanupSession(pjUri);
109 + await _payjoinStorage.markSenderSessionComplete(pjUri, txId);
110 + completer.complete();
111 + }
112 + } catch (e) {
113 + _cleanupSession(pjUri);
114 + printV(e);
115 + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
116 + completer.completeError(e);
117 + }
118 + } else if (message is PayjoinSessionError) {
119 + _cleanupSession(pjUri);
120 + if (message is UnrecoverableError) {
121 + printV(message.message);
122 + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
123 + completer.complete();
124 + } else if (message is RecoverableError) {
125 + completer.complete();
126 + } else {
127 + completer.completeError(message);
128 + }
129 + }
130 + });
131 +
132 + final isolate = await Isolate.spawn(
133 + PayjoinSenderWorker.run,
134 + [receivePort.sendPort, sender.toJson(), pjUri],
135 + );
136 +
137 + _activePollers[pjUri] = PayjoinPollerSession(isolate, receivePort);
138 +
139 + return completer.future;
140 + }
141 +
142 + Future<Receiver> initReceiver(String address,
143 + [bool isTestnet = false]) async {
144 + try {
145 + final payjoinDirectory =
146 + await PayjoinUri.Url.fromStr(payjoinDirectoryUrl);
147 +
148 + final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
149 + ohttpRelay: await randomOhttpRelayUrl(),
150 + payjoinDirectory: payjoinDirectory,
151 + );
152 +
153 + final receiver = await Receiver.create(
154 + address: address,
155 + network: isTestnet ? Network.testnet : Network.bitcoin,
156 + directory: payjoinDirectory,
157 + ohttpKeys: ohttpKeys,
158 + ohttpRelay: await randomOhttpRelayUrl(),
159 + );
160 +
161 + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
162 +
163 + return receiver;
164 + } catch (e) {
165 + throw Exception('Error initializing Payjoin Receiver: $e');
166 + }
167 + }
168 +
169 + Future<void> spawnNewReceiver({
170 + required Receiver receiver,
171 + bool isTestnet = false,
172 + }) async {
173 + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
174 + return _spawnReceiver(isTestnet: isTestnet, receiver: receiver);
175 + }
176 +
177 + Future<void> _spawnReceiver({
178 + required Receiver receiver,
179 + bool isTestnet = false,
180 + }) async {
181 + final completer = Completer();
182 + final receivePort = ReceivePort();
183 +
184 + SendPort? mainToIsolateSendPort;
185 + List<UtxoWithPrivateKey> utxos = [];
186 + String rawAmount = '0';
187 +
188 + receivePort.listen((message) async {
189 + if (message is Map<String, dynamic>) {
190 + try {
191 + switch (message['type'] as PayjoinReceiverRequestTypes) {
192 + case PayjoinReceiverRequestTypes.processOriginalTx:
193 + final tx = message['tx'] as String;
194 + rawAmount = getOutputAmountFromTx(tx, _wallet);
195 + break;
196 + case PayjoinReceiverRequestTypes.checkIsOwned:
197 + (_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver();
198 + _payjoinStorage.markReceiverSessionInProgress(receiver.id());
199 +
200 + final inputScript = message['input_script'] as Uint8List;
201 + final isOwned =
202 + _wallet.isMine(Script.fromRaw(byteData: inputScript));
203 + mainToIsolateSendPort?.send({
204 + 'requestId': message['requestId'],
205 + 'result': isOwned,
206 + });
207 + break;
208 +
209 + case PayjoinReceiverRequestTypes.checkIsReceiverOutput:
210 + final outputScript = message['output_script'] as Uint8List;
211 + final isReceiverOutput =
212 + _wallet.isMine(Script.fromRaw(byteData: outputScript));
213 + mainToIsolateSendPort?.send({
214 + 'requestId': message['requestId'],
215 + 'result': isReceiverOutput,
216 + });
217 + break;
218 +
219 + case PayjoinReceiverRequestTypes.getCandidateInputs:
220 + utxos = _wallet.getUtxoWithPrivateKeys();
221 + mainToIsolateSendPort?.send({
222 + 'requestId': message['requestId'],
223 + 'result': utxos,
224 + });
225 + break;
226 +
227 + case PayjoinReceiverRequestTypes.processPsbt:
228 + final psbt = message['psbt'] as String;
229 + final signedPsbt = await _wallet.signPsbt(psbt, utxos);
230 + mainToIsolateSendPort?.send({
231 + 'requestId': message['requestId'],
232 + 'result': signedPsbt,
233 + });
234 + break;
235 +
236 + case PayjoinReceiverRequestTypes.proposalSent:
237 + _cleanupSession(receiver.id());
238 + final psbt = message['psbt'] as String;
239 + await _payjoinStorage.markReceiverSessionComplete(
240 + receiver.id(), getTxIdFromPsbtV0(psbt), rawAmount);
241 + completer.complete();
242 + }
243 + } catch (e) {
244 + _cleanupSession(receiver.id());
245 + await _payjoinStorage.markReceiverSessionUnrecoverable(
246 + receiver.id(), e.toString());
247 + completer.completeError(e);
248 + }
249 + } else if (message is PayjoinSessionError) {
250 + _cleanupSession(receiver.id());
251 + if (message is UnrecoverableError) {
252 + await _payjoinStorage.markReceiverSessionUnrecoverable(
253 + receiver.id(), message.message);
254 + completer.complete();
255 + } else if (message is RecoverableError) {
256 + completer.complete();
257 + } else {
258 + completer.completeError(message);
259 + }
260 + } else if (message is SendPort) {
261 + mainToIsolateSendPort = message;
262 + }
263 + });
264 +
265 + final isolate = await Isolate.spawn(
266 + PayjoinReceiverWorker.run,
267 + [receivePort.sendPort, receiver.toJson()],
268 + );
269 +
270 + _activePollers[receiver.id()] = PayjoinPollerSession(isolate, receivePort);
271 +
272 + return completer.future;
273 + }
274 +
275 + void cleanupSessions() {
276 + final sessionIds = _activePollers.keys.toList();
277 + for (final sessionId in sessionIds) {
278 + _cleanupSession(sessionId);
279 + }
280 + }
281 +
282 + void _cleanupSession(String sessionId) {
283 + _activePollers[sessionId]?.close();
284 + _activePollers.remove(sessionId);
285 + }
286 +}
287 +
288 +class PayjoinPollerSession {
289 + final Isolate isolate;
290 + final ReceivePort port;
291 +
292 + PayjoinPollerSession(this.isolate, this.port);
293 +
294 + void close() {
295 + isolate.kill();
296 + port.close();
297 + }
298 +}
cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart new
+219
@@ -0,0 +1,219 @@
1 +import 'dart:async';
2 +import 'dart:io';
3 +import 'dart:isolate';
4 +import 'dart:typed_data';
5 +
6 +import 'package:blockchain_utils/blockchain_utils.dart';
7 +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
8 +import 'package:cw_bitcoin/psbt/signer.dart';
9 +import 'package:cw_core/utils/print_verbose.dart';
10 +import 'package:http/http.dart' as http;
11 +import 'package:payjoin_flutter/bitcoin_ffi.dart';
12 +import 'package:payjoin_flutter/common.dart';
13 +import 'package:payjoin_flutter/receive.dart';
14 +import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj;
15 +
16 +enum PayjoinReceiverRequestTypes {
17 + processOriginalTx,
18 + proposalSent,
19 + getCandidateInputs,
20 + checkIsOwned,
21 + checkIsReceiverOutput,
22 + processPsbt;
23 +}
24 +
25 +class PayjoinReceiverWorker {
26 + final SendPort sendPort;
27 + final pendingRequests = <String, Completer<dynamic>>{};
28 +
29 + PayjoinReceiverWorker._(this.sendPort);
30 +
31 + static Future<void> run(List<Object> args) async {
32 + await pj.core.init();
33 +
34 + final sendPort = args[0] as SendPort;
35 + final receiverJson = args[1] as String;
36 +
37 + final worker = PayjoinReceiverWorker._(sendPort);
38 + final receivePort = ReceivePort();
39 +
40 + sendPort.send(receivePort.sendPort);
41 + receivePort.listen(worker.handleMessage);
42 +
43 + try {
44 + final httpClient = http.Client();
45 + final receiver = Receiver.fromJson(receiverJson);
46 +
47 + final uncheckedProposal =
48 + await worker.receiveUncheckedProposal(httpClient, receiver);
49 +
50 + final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast();
51 + sendPort.send({
52 + 'type': PayjoinReceiverRequestTypes.processOriginalTx,
53 + 'tx': BytesUtils.toHexString(originalTx),
54 + });
55 +
56 + final payjoinProposal = await worker.processPayjoinProposal(
57 + uncheckedProposal,
58 + );
59 + final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal);
60 + sendPort.send({
61 + 'type': PayjoinReceiverRequestTypes.proposalSent,
62 + 'psbt': psbt,
63 + });
64 + } catch (e) {
65 + if (e is HttpException ||
66 + (e is http.ClientException &&
67 + e.message.contains("Software caused connection abort"))) {
68 + sendPort.send(PayjoinSessionError.recoverable(e.toString()));
69 + } else {
70 + sendPort.send(PayjoinSessionError.unrecoverable(e.toString()));
71 + }
72 + }
73 + }
74 +
75 + void handleMessage(dynamic message) async {
76 + if (message is Map<String, dynamic>) {
77 + final requestId = message['requestId'] as String?;
78 + if (requestId != null && pendingRequests.containsKey(requestId)) {
79 + pendingRequests[requestId]!.complete(message['result']);
80 + pendingRequests.remove(requestId);
81 + }
82 + }
83 + }
84 +
85 + Future<dynamic> _sendRequest(PayjoinReceiverRequestTypes type,
86 + [Map<String, dynamic> data = const {}]) async {
87 + final completer = Completer<dynamic>();
88 + final requestId = DateTime.now().millisecondsSinceEpoch.toString();
89 + pendingRequests[requestId] = completer;
90 +
91 + sendPort.send({
92 + ...data,
93 + 'type': type,
94 + 'requestId': requestId,
95 + });
96 +
97 + return completer.future;
98 + }
99 +
100 + Future<UncheckedProposal> receiveUncheckedProposal(
101 + http.Client httpClient, Receiver session) async {
102 + while (true) {
103 + printV("Polling for Proposal (${session.id()})");
104 + final extractReq = await session.extractReq();
105 + final request = extractReq.$1;
106 +
107 + final url = Uri.parse(request.url.asString());
108 + final httpRequest = await httpClient.post(url,
109 + headers: {'Content-Type': request.contentType}, body: request.body);
110 +
111 + final proposal = await session.processRes(
112 + body: httpRequest.bodyBytes, ctx: extractReq.$2);
113 + if (proposal != null) return proposal;
114 + }
115 + }
116 +
117 + Future<String> sendFinalProposal(
118 + http.Client httpClient, PayjoinProposal finalProposal) async {
119 + final req = await finalProposal.extractV2Req();
120 + final proposalReq = req.$1;
121 + final proposalCtx = req.$2;
122 +
123 + final request = await httpClient.post(
124 + Uri.parse(proposalReq.url.asString()),
125 + headers: {"Content-Type": proposalReq.contentType},
126 + body: proposalReq.body,
127 + );
128 +
129 + await finalProposal.processRes(
130 + res: request.bodyBytes,
131 + ohttpContext: proposalCtx,
132 + );
133 +
134 + return await finalProposal.psbt();
135 + }
136 +
137 + Future<PayjoinProposal> processPayjoinProposal(
138 + UncheckedProposal proposal) async {
139 + await proposal.extractTxToScheduleBroadcast();
140 + // TODO Handle this. send to the main port on a timer?
141 +
142 + try {
143 + // Receive Check 1: can broadcast
144 + final pj1 = await proposal.assumeInteractiveReceiver();
145 +
146 + // Receive Check 2: original PSBT has no receiver-owned inputs
147 + final pj2 = await pj1.checkInputsNotOwned(
148 + isOwned: (inputScript) async {
149 + final result = await _sendRequest(
150 + PayjoinReceiverRequestTypes.checkIsOwned,
151 + {'input_script': inputScript},
152 + );
153 + return result as bool;
154 + },
155 + );
156 + // Receive Check 3: sender inputs have not been seen before (prevent probing attacks)
157 + final pj3 = await pj2.checkNoInputsSeenBefore(isKnown: (input) => false);
158 +
159 + // Identify receiver outputs
160 + final pj4 = await pj3.identifyReceiverOutputs(
161 + isReceiverOutput: (outputScript) async {
162 + final result = await _sendRequest(
163 + PayjoinReceiverRequestTypes.checkIsReceiverOutput,
164 + {'output_script': outputScript},
165 + );
166 + return result as bool;
167 + },
168 + );
169 + final pj5 = await pj4.commitOutputs();
170 +
171 + final listUnspent =
172 + await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs);
173 + final unspent = listUnspent as List<UtxoWithPrivateKey>;
174 + if (unspent.isEmpty) throw Exception('No unspent outputs available');
175 +
176 + final selectedUtxo = await _inputPairFromUtxo(unspent[0]);
177 + final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]);
178 + final pj7 = await pj6.commitInputs();
179 +
180 + // Finalize proposal
181 + final payjoinProposal = await pj7.finalizeProposal(
182 + processPsbt: (String psbt) async {
183 + final result = await _sendRequest(
184 + PayjoinReceiverRequestTypes.processPsbt, {'psbt': psbt});
185 + return result as String;
186 + },
187 + // TODO set maxFeeRateSatPerVb
188 + maxFeeRateSatPerVb: BigInt.from(10000),
189 + );
190 + return payjoinProposal;
191 + } catch (e) {
192 + printV('Error occurred while finalizing proposal: $e');
193 + rethrow;
194 + }
195 + }
196 +
197 + Future<InputPair> _inputPairFromUtxo(UtxoWithPrivateKey utxo) async {
198 + final txout = TxOut(
199 + value: utxo.utxo.value,
200 + scriptPubkey: Uint8List.fromList(
201 + utxo.ownerDetails.address.toScriptPubKey().toBytes()),
202 + );
203 +
204 + final psbtin =
205 + PsbtInput(witnessUtxo: txout, redeemScript: null, witnessScript: null);
206 +
207 + final previousOutput =
208 + OutPoint(txid: utxo.utxo.txHash, vout: utxo.utxo.vout);
209 +
210 + final txin = TxIn(
211 + previousOutput: previousOutput,
212 + scriptSig: await Script.newInstance(rawOutputScript: []),
213 + witness: [],
214 + sequence: 0,
215 + );
216 +
217 + return InputPair.newInstance(txin, psbtin);
218 + }
219 +}
cw_bitcoin/lib/payjoin/payjoin_send_worker.dart new
+119
@@ -0,0 +1,119 @@
1 +import 'dart:async';
2 +import 'dart:io';
3 +import 'dart:isolate';
4 +
5 +import 'package:cw_bitcoin/payjoin/manager.dart';
6 +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
7 +import 'package:cw_core/utils/print_verbose.dart';
8 +import 'package:http/http.dart' as http;
9 +import 'package:payjoin_flutter/common.dart';
10 +import 'package:payjoin_flutter/send.dart';
11 +import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj;
12 +
13 +enum PayjoinSenderRequestTypes {
14 + requestPosted,
15 + psbtToSign;
16 +}
17 +
18 +class PayjoinSenderWorker {
19 + final SendPort sendPort;
20 + final pendingRequests = <String, Completer<dynamic>>{};
21 + final String pjUrl;
22 +
23 + PayjoinSenderWorker._(this.sendPort, this.pjUrl);
24 +
25 + static Future<void> run(List<Object> args) async {
26 + await pj.core.init();
27 +
28 + final sendPort = args[0] as SendPort;
29 + final senderJson = args[1] as String;
30 + final pjUrl = args[2] as String;
31 +
32 + final sender = Sender.fromJson(senderJson);
33 + final worker = PayjoinSenderWorker._(sendPort, pjUrl);
34 +
35 + try {
36 + final proposalPsbt = await worker.runSender(sender);
37 + sendPort.send({
38 + 'type': PayjoinSenderRequestTypes.psbtToSign,
39 + 'psbt': proposalPsbt,
40 + });
41 + } catch (e) {
42 + sendPort.send(e);
43 + }
44 + }
45 +
46 + /// Run a payjoin sender (V2 protocol first, fallback to V1).
47 + Future<String> runSender(Sender sender) async {
48 + final httpClient = http.Client();
49 +
50 + try {
51 + return await _runSenderV2(sender, httpClient);
52 + } catch (e) {
53 + printV(e);
54 + if (e is PayjoinException &&
55 + // TODO condition on error type instead of message content
56 + e.message?.contains('parse receiver public key') == true) {
57 + return await _runSenderV1(sender, httpClient);
58 + } else if (e is HttpException) {
59 + printV(e);
60 + throw Exception(PayjoinSessionError.recoverable(e.toString()));
61 + } else {
62 + throw Exception(PayjoinSessionError.unrecoverable(e.toString()));
63 + }
64 + }
65 + }
66 +
67 + /// Attempt to send payjoin using the V2 of the protocol.
68 + Future<String> _runSenderV2(Sender sender, http.Client httpClient) async {
69 + try {
70 + final postRequest = await sender.extractV2(
71 + ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(),
72 + );
73 +
74 + final postResult = await _postRequest(httpClient, postRequest.$1);
75 + final getContext =
76 + await postRequest.$2.processResponse(response: postResult);
77 +
78 + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted, "pj": pjUrl});
79 +
80 + while (true) {
81 + printV('Polling V2 Proposal Request (${pjUrl})');
82 +
83 + final getRequest = await getContext.extractReq(
84 + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(),
85 + );
86 + final getRes = await _postRequest(httpClient, getRequest.$1);
87 + final proposalPsbt = await getContext.processResponse(
88 + response: getRes,
89 + ohttpCtx: getRequest.$2,
90 + );
91 + printV("$proposalPsbt");
92 + if (proposalPsbt != null) return proposalPsbt;
93 + }
94 + } catch (e) {
95 + rethrow;
96 + }
97 + }
98 +
99 + /// Attempt to send payjoin using the V1 of the protocol.
100 + Future<String> _runSenderV1(Sender sender, http.Client httpClient) async {
101 + try {
102 + final postRequest = await sender.extractV1();
103 + final response = await _postRequest(httpClient, postRequest.$1);
104 +
105 + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted});
106 +
107 + return await postRequest.$2.processResponse(response: response);
108 + } catch (e) {
109 + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e');
110 + }
111 + }
112 +
113 + Future<List<int>> _postRequest(http.Client client, Request req) async {
114 + final httpRequest = await client.post(Uri.parse(req.url.asString()),
115 + headers: {'Content-Type': req.contentType}, body: req.body);
116 +
117 + return httpRequest.bodyBytes;
118 + }
119 +}
cw_bitcoin/lib/payjoin/payjoin_session_errors.dart new
+16
@@ -0,0 +1,16 @@
1 +class PayjoinSessionError {
2 + final String message;
3 +
4 + const PayjoinSessionError._(this.message);
5 +
6 + factory PayjoinSessionError.recoverable(String message) = RecoverableError;
7 + factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError;
8 +}
9 +
10 +class RecoverableError extends PayjoinSessionError {
11 + const RecoverableError(super.message) : super._();
12 +}
13 +
14 +class UnrecoverableError extends PayjoinSessionError {
15 + const UnrecoverableError(super.message) : super._();
16 +}
cw_bitcoin/lib/payjoin/storage.dart new
+95
@@ -0,0 +1,95 @@
1 +import 'package:cw_core/payjoin_session.dart';
2 +import 'package:hive/hive.dart';
3 +import 'package:payjoin_flutter/receive.dart';
4 +import 'package:payjoin_flutter/send.dart';
5 +
6 +class PayjoinStorage {
7 + PayjoinStorage(this._payjoinSessionSources);
8 +
9 + final Box<PayjoinSession> _payjoinSessionSources;
10 +
11 + static const String _receiverPrefix = 'pj_recv_';
12 + static const String _senderPrefix = 'pj_send_';
13 +
14 + Future<void> insertReceiverSession(
15 + Receiver receiver,
16 + String walletId,
17 + ) =>
18 + _payjoinSessionSources.put(
19 + "$_receiverPrefix${receiver.id()}",
20 + PayjoinSession(
21 + walletId: walletId,
22 + receiver: receiver.toJson(),
23 + ),
24 + );
25 +
26 + Future<void> markReceiverSessionComplete(
27 + String sessionId, String txId, String amount) async {
28 + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
29 +
30 + session.status = PayjoinSessionStatus.success.name;
31 + session.txId = txId;
32 + session.rawAmount = amount;
33 + await session.save();
34 + }
35 +
36 + Future<void> markReceiverSessionUnrecoverable(
37 + String sessionId, String reason) async {
38 + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
39 +
40 + session.status = PayjoinSessionStatus.unrecoverable.name;
41 + session.error = reason;
42 + await session.save();
43 + }
44 +
45 + Future<void> markReceiverSessionInProgress(String sessionId) async {
46 + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
47 +
48 + session.status = PayjoinSessionStatus.inProgress.name;
49 + session.inProgressSince = DateTime.now();
50 + await session.save();
51 + }
52 +
53 + Future<void> insertSenderSession(
54 + Sender sender,
55 + String pjUrl,
56 + String walletId,
57 + BigInt amount,
58 + ) =>
59 + _payjoinSessionSources.put(
60 + "$_senderPrefix$pjUrl",
61 + PayjoinSession(
62 + walletId: walletId,
63 + pjUri: pjUrl,
64 + sender: sender.toJson(),
65 + status: PayjoinSessionStatus.inProgress.name,
66 + inProgressSince: DateTime.now(),
67 + rawAmount: amount.toString(),
68 + ),
69 + );
70 +
71 + Future<void> markSenderSessionComplete(String pjUrl, String txId) async {
72 + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
73 +
74 + session.status = PayjoinSessionStatus.success.name;
75 + session.txId = txId;
76 + await session.save();
77 + }
78 +
79 + Future<void> markSenderSessionUnrecoverable(String pjUrl) async {
80 + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
81 +
82 + session.status = PayjoinSessionStatus.unrecoverable.name;
83 + await session.save();
84 + }
85 +
86 + List<PayjoinSession> readAllOpenSessions(String walletId) =>
87 + _payjoinSessionSources.values
88 + .where((session) =>
89 + session.walletId == walletId &&
90 + ![
91 + PayjoinSessionStatus.success.name,
92 + PayjoinSessionStatus.unrecoverable.name
93 + ].contains(session.status))
94 + .toList();
95 +}
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+9
@@ -1,3 +1,4 @@
1 +import 'package:cw_bitcoin/electrum_wallet.dart';
2 import 'package:grpc/grpc.dart';
3 import 'package:cw_bitcoin/exceptions.dart';
4 import 'package:bitcoin_base/bitcoin_base.dart';
@@ -25,6 +26,8 @@ class PendingBitcoinTransaction with PendingTransaction {
26 this.hasTaprootInputs = false,
27 this.isMweb = false,
28 this.utxos = const [],
29 + this.publicKeys,
30 + this.commitOverride,
31 }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
32
33 final WalletType type;
@@ -43,6 +46,8 @@ class PendingBitcoinTransaction with PendingTransaction {
46 String? idOverride;
47 String? hexOverride;
48 List<String>? outputAddresses;
49 + final Map<String, PublicKeyWithDerivationPath>? publicKeys;
50 + Future<void> Function()? commitOverride;
51
52 @override
53 String get id => idOverride ?? _tx.txId();
@@ -129,6 +134,10 @@ class PendingBitcoinTransaction with PendingTransaction {
134
135 @override
136 Future<void> commit() async {
137 + if (commitOverride != null) {
138 + return commitOverride?.call();
139 + }
140 +
141 if (isMweb) {
142 await _ltcCommit();
143 } else {
cw_bitcoin/lib/psbt/signer.dart new
+263
@@ -0,0 +1,263 @@
1 +import 'dart:typed_data';
2 +
3 +import 'package:bitcoin_base/bitcoin_base.dart';
4 +import 'package:blockchain_utils/blockchain_utils.dart';
5 +import 'package:collection/collection.dart';
6 +import 'package:cw_bitcoin/bitcoin_address_record.dart';
7 +import 'package:cw_bitcoin/bitcoin_unspent.dart';
8 +import 'package:cw_bitcoin/bitcoin_wallet.dart';
9 +import 'package:cw_bitcoin/utils.dart';
10 +import 'package:ledger_bitcoin/psbt.dart';
11 +import 'package:ledger_bitcoin/src/utils/buffer_writer.dart';
12 +
13 +extension PsbtSigner on PsbtV2 {
14 + Uint8List extractUnsignedTX({bool getSegwit = true}) {
15 + final tx = BufferWriter()..writeUInt32(getGlobalTxVersion());
16 +
17 + final isSegwit = getInputWitnessUtxo(0) != null;
18 + if (isSegwit && getSegwit) {
19 + tx.writeSlice(Uint8List.fromList([0, 1]));
20 + }
21 +
22 + final inputCount = getGlobalInputCount();
23 + tx.writeVarInt(inputCount);
24 +
25 + for (var i = 0; i < inputCount; i++) {
26 + tx
27 + ..writeSlice(getInputPreviousTxid(i))
28 + ..writeUInt32(getInputOutputIndex(i))
29 + ..writeVarSlice(Uint8List(0))
30 + ..writeUInt32(getInputSequence(i));
31 + }
32 +
33 + final outputCount = getGlobalOutputCount();
34 + tx.writeVarInt(outputCount);
35 + for (var i = 0; i < outputCount; i++) {
36 + tx.writeUInt64(getOutputAmount(i));
37 + tx.writeVarSlice(getOutputScript(i));
38 + }
39 + tx.writeUInt32(getGlobalFallbackLocktime() ?? 0);
40 + return tx.buffer();
41 + }
42 +
43 + Future<void> signWithUTXO(
44 + List<UtxoWithPrivateKey> utxos, UTXOSignerCallBack signer,
45 + [UTXOGetterCallBack? getTaprootPair]) async {
46 + final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false));
47 + final tx = BtcTransaction.fromRaw(raw);
48 +
49 + /// when the transaction is taproot and we must use getTaproot transaction
50 + /// digest we need all of inputs amounts and owner script pub keys
51 + List<BigInt> taprootAmounts = [];
52 + List<Script> taprootScripts = [];
53 +
54 + if (utxos.any((e) => e.utxo.isP2tr())) {
55 + for (final input in tx.inputs) {
56 + final utxo = utxos.firstWhereOrNull(
57 + (u) => u.utxo.txHash == input.txId && u.utxo.vout == input.txIndex);
58 +
59 + if (utxo == null) {
60 + final trPair = await getTaprootPair!.call(input.txId, input.txIndex);
61 + taprootAmounts.add(trPair.value);
62 + taprootScripts.add(trPair.script);
63 + continue;
64 + }
65 + taprootAmounts.add(utxo.utxo.value);
66 + taprootScripts.add(_findLockingScript(utxo, true));
67 + }
68 + }
69 +
70 + for (var i = 0; i < tx.inputs.length; i++) {
71 + final utxo = utxos.firstWhereOrNull((e) =>
72 + e.utxo.txHash == tx.inputs[i].txId &&
73 + e.utxo.vout == tx.inputs[i].txIndex); // ToDo: More robust verify
74 + if (utxo == null) continue;
75 +
76 + /// We receive the owner's ScriptPubKey
77 + final script = _findLockingScript(utxo, false);
78 +
79 + final int sighash = utxo.utxo.isP2tr()
80 + ? BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL
81 + : BitcoinOpCodeConst.SIGHASH_ALL;
82 +
83 + /// We generate transaction digest for current input
84 + final digest = _generateTransactionDigest(
85 + script, i, utxo.utxo, tx, taprootAmounts, taprootScripts);
86 +
87 + /// now we need sign the transaction digest
88 + final sig = signer(digest, utxo, utxo.privateKey, sighash);
89 +
90 + if (utxo.utxo.isP2tr()) {
91 + setInputTapKeySig(i, Uint8List.fromList(BytesUtils.fromHexString(sig)));
92 + } else {
93 + setInputPartialSig(
94 + i,
95 + Uint8List.fromList(BytesUtils.fromHexString(utxo.public().toHex())),
96 + Uint8List.fromList(BytesUtils.fromHexString(sig)));
97 + }
98 + }
99 + }
100 +
101 + List<int> _generateTransactionDigest(
102 + Script scriptPubKeys,
103 + int input,
104 + BitcoinUtxo utxo,
105 + BtcTransaction transaction,
106 + List<BigInt> taprootAmounts,
107 + List<Script> tapRootPubKeys) {
108 + if (utxo.isSegwit()) {
109 + if (utxo.isP2tr()) {
110 + return transaction.getTransactionTaprootDigset(
111 + txIndex: input,
112 + scriptPubKeys: tapRootPubKeys,
113 + amounts: taprootAmounts,
114 + );
115 + }
116 + return transaction.getTransactionSegwitDigit(
117 + txInIndex: input, script: scriptPubKeys, amount: utxo.value);
118 + }
119 + return transaction.getTransactionDigest(
120 + txInIndex: input, script: scriptPubKeys);
121 + }
122 +
123 + Script _findLockingScript(UtxoWithAddress utxo, bool isTaproot) {
124 + if (utxo.isMultiSig()) {
125 + throw Exception("MultiSig is not supported yet");
126 + }
127 +
128 + final senderPub = utxo.public();
129 + switch (utxo.utxo.scriptType) {
130 + case PubKeyAddressType.p2pk:
131 + return senderPub.toRedeemScript();
132 + case SegwitAddresType.p2wsh:
133 + if (isTaproot) {
134 + return senderPub.toP2wshAddress().toScriptPubKey();
135 + }
136 + return senderPub.toP2wshRedeemScript();
137 + case P2pkhAddressType.p2pkh:
138 + return senderPub.toP2pkhAddress().toScriptPubKey();
139 + case SegwitAddresType.p2wpkh:
140 + if (isTaproot) {
141 + return senderPub.toP2wpkhAddress().toScriptPubKey();
142 + }
143 + return senderPub.toP2pkhAddress().toScriptPubKey();
144 + case SegwitAddresType.p2tr:
145 + return senderPub
146 + .toTaprootAddress(tweak: utxo.utxo.isSilentPayment != true)
147 + .toScriptPubKey();
148 + case SegwitAddresType.mweb:
149 + return Script(script: []);
150 + case P2shAddressType.p2pkhInP2sh:
151 + if (isTaproot) {
152 + return senderPub.toP2pkhInP2sh().toScriptPubKey();
153 + }
154 + return senderPub.toP2pkhAddress().toScriptPubKey();
155 + case P2shAddressType.p2wpkhInP2sh:
156 + if (isTaproot) {
157 + return senderPub.toP2wpkhInP2sh().toScriptPubKey();
158 + }
159 + return senderPub.toP2pkhAddress().toScriptPubKey();
160 + case P2shAddressType.p2wshInP2sh:
161 + if (isTaproot) {
162 + return senderPub.toP2wshInP2sh().toScriptPubKey();
163 + }
164 + return senderPub.toP2wshRedeemScript();
165 + case P2shAddressType.p2pkInP2sh:
166 + if (isTaproot) {
167 + return senderPub.toP2pkInP2sh().toScriptPubKey();
168 + }
169 + return senderPub.toRedeemScript();
170 + }
171 + throw Exception("invalid bitcoin address type");
172 + }
173 +}
174 +
175 +typedef UTXOSignerCallBack = String Function(List<int> trDigest,
176 + UtxoWithAddress utxo, ECPrivate privateKey, int sighash);
177 +
178 +typedef UTXOGetterCallBack = Future<TaprootAmountScriptPair> Function(
179 + String txId, int vout);
180 +
181 +class TaprootAmountScriptPair {
182 + final BigInt value;
183 + final Script script;
184 +
185 + const TaprootAmountScriptPair(this.value, this.script);
186 +}
187 +
188 +class UtxoWithPrivateKey extends UtxoWithAddress {
189 + final ECPrivate privateKey;
190 +
191 + UtxoWithPrivateKey({
192 + required super.utxo,
193 + required super.ownerDetails,
194 + required this.privateKey,
195 + });
196 +
197 + factory UtxoWithPrivateKey.fromUtxo(
198 + UtxoWithAddress input, List<ECPrivateInfo> inputPrivateKeyInfos) {
199 + ECPrivateInfo? key;
200 +
201 + if (inputPrivateKeyInfos.isEmpty) {
202 + throw Exception("No private keys generated.");
203 + } else {
204 + key = inputPrivateKeyInfos.firstWhereOrNull((element) {
205 + final elemPubkey = element.privkey.getPublic().toHex();
206 + if (elemPubkey == input.public().toHex()) {
207 + return true;
208 + } else {
209 + return false;
210 + }
211 + });
212 + }
213 +
214 + if (key == null) {
215 + throw Exception("${input.utxo.txHash} No Key found");
216 + }
217 +
218 + return UtxoWithPrivateKey(
219 + utxo: input.utxo,
220 + ownerDetails: input.ownerDetails,
221 + privateKey: key.privkey);
222 + }
223 +
224 + factory UtxoWithPrivateKey.fromUnspent(
225 + BitcoinUnspent input, BitcoinWalletBase wallet) {
226 + final address =
227 + RegexUtils.addressTypeFromStr(input.address, BitcoinNetwork.mainnet);
228 +
229 + final newHd =
230 + input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.hd;
231 +
232 + ECPrivate privkey;
233 + if (input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
234 + final unspentAddress =
235 + input.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
236 + privkey = wallet.walletAddresses.silentAddress!.b_spend.tweakAdd(
237 + BigintUtils.fromBytes(
238 + BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
239 + ),
240 + );
241 + } else {
242 + privkey = generateECPrivate(
243 + hd: newHd,
244 + index: input.bitcoinAddressRecord.index,
245 + network: BitcoinNetwork.mainnet);
246 + }
247 +
248 + return UtxoWithPrivateKey(
249 + utxo: BitcoinUtxo(
250 + txHash: input.hash,
251 + value: BigInt.from(input.value),
252 + vout: input.vout,
253 + scriptType: input.bitcoinAddressRecord.type,
254 + isSilentPayment:
255 + input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord,
256 + ),
257 + ownerDetails: UtxoAddressDetails(
258 + publicKey: privkey.getPublic().toHex(),
259 + address: address,
260 + ),
261 + privateKey: privkey);
262 + }
263 +}
cw_bitcoin/lib/psbt/transaction_builder.dart renamed
cw_bitcoin/lib/psbt/utils.dart new
+41
@@ -0,0 +1,41 @@
1 +import 'dart:convert';
2 +
3 +import 'package:bitcoin_base/bitcoin_base.dart';
4 +import 'package:blockchain_utils/blockchain_utils.dart';
5 +import 'package:cw_bitcoin/bitcoin_wallet.dart';
6 +import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
7 +import 'package:cw_core/utils/print_verbose.dart';
8 +import 'package:ledger_bitcoin/psbt.dart';
9 +
10 +String getTxIdFromPsbtV0(String psbt) {
11 + final psbtV2 = PsbtV2()..deserializeV0(base64.decode(psbt));
12 +
13 + return BtcTransaction.fromRaw(
14 + BytesUtils.toHexString(psbtV2.extractUnsignedTX(false)))
15 + .txId();
16 +}
17 +
18 +String getOutputAmountFromPsbt(String psbtV0, BitcoinWalletBase wallet) {
19 + printV(psbtV0);
20 + final psbt = PsbtV2()..deserializeV0(base64.decode(psbtV0));
21 + int amount = 0;
22 + for (var i = 0; i < psbt.getGlobalOutputCount(); i++) {
23 + final script = psbt.getOutputScript(i);
24 + if (wallet.isMine(Script.fromRaw(byteData: script))) {
25 + amount += psbt.getOutputAmount(i);
26 + }
27 + }
28 + return amount.toString();
29 +}
30 +
31 +String getOutputAmountFromTx(String originalTx, BitcoinWalletBase wallet) {
32 + final tx = BtcTransaction.fromRaw(originalTx);
33 + BigInt amount = BigInt.zero;
34 + for (final output in tx.outputs) {
35 + if (wallet.isMine(output.scriptPubKey)) {
36 + amount += output.amount;
37 + }
38 + }
39 + printV(amount);
40 + return amount.toString();
41 +}
cw_bitcoin/lib/psbt/v0_deserialize.dart new
+52
@@ -0,0 +1,52 @@
1 +import 'package:bitcoin_base/bitcoin_base.dart';
2 +import 'package:blockchain_utils/blockchain_utils.dart';
3 +import 'package:flutter/foundation.dart';
4 +import 'package:ledger_bitcoin/psbt.dart';
5 +import 'package:ledger_bitcoin/src/psbt/map_extension.dart';
6 +import 'package:ledger_bitcoin/src/utils/buffer_reader.dart';
7 +import 'package:ledger_bitcoin/src/utils/uint8list_extension.dart' as ext;
8 +
9 +extension PsbtSigner on PsbtV2 {
10 +
11 + void deserializeV0(Uint8List psbt) {
12 + final bufferReader = BufferReader(psbt);
13 + if (!listEquals(bufferReader.readSlice(5), Uint8List.fromList([0x70, 0x73, 0x62, 0x74, 0xff]))) {
14 + throw Exception("Invalid magic bytes");
15 + }
16 + while (_readKeyPair(globalMap, bufferReader)) {}
17 +
18 + final tx = BtcTransaction.fromRaw(BytesUtils.toHexString(globalMap['00']!));
19 +
20 + setGlobalInputCount(tx.inputs.length);
21 + setGlobalOutputCount(tx.outputs.length);
22 + setGlobalTxVersion(Uint8List.fromList(tx.version).readUint32LE(0));
23 +
24 + for (var i = 0; i < getGlobalInputCount(); i++) {
25 + inputMaps.insert(i, <String, Uint8List>{});
26 + while (_readKeyPair(inputMaps[i], bufferReader)) {}
27 + final input = tx.inputs[i];
28 + setInputOutputIndex(i, input.txIndex);
29 + setInputPreviousTxId(i, Uint8List.fromList(BytesUtils.fromHexString(input.txId).reversed.toList()));
30 + setInputSequence(i, Uint8List.fromList(input.sequence).readUint32LE(0));
31 + }
32 + for (var i = 0; i < getGlobalOutputCount(); i++) {
33 + outputMaps.insert(i, <String, Uint8List>{});
34 + while (_readKeyPair(outputMaps[i], bufferReader)) {}
35 + final output = tx.outputs[i];
36 + setOutputAmount(i, output.amount.toInt());
37 + setOutputScript(i, Uint8List.fromList(output.scriptPubKey.toBytes()));
38 + }
39 + }
40 +
41 + bool _readKeyPair(Map<String, Uint8List> map, BufferReader bufferReader) {
42 + final keyLen = bufferReader.readVarInt();
43 + if (keyLen == 0) return false;
44 +
45 + final keyType = bufferReader.readUInt8();
46 + final keyData = bufferReader.readSlice(keyLen - 1);
47 + final value = bufferReader.readVarSlice();
48 +
49 + map.set(keyType, keyData, value);
50 + return true;
51 + }
52 +}
cw_bitcoin/lib/psbt/v0_finalizer.dart new
+143
@@ -0,0 +1,143 @@
1 +import "dart:typed_data";
2 +
3 +import "package:ledger_bitcoin/src/psbt/constants.dart";
4 +import "package:ledger_bitcoin/src/psbt/psbtv2.dart";
5 +import "package:ledger_bitcoin/src/utils/buffer_writer.dart";
6 +
7 +/// This roughly implements the "input finalizer" role of BIP370 (PSBTv2
8 +/// https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However
9 +/// the role is documented in BIP174 (PSBTv0
10 +/// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
11 +///
12 +/// Verify that all inputs have a signature, and set inputFinalScriptwitness
13 +/// and/or inputFinalScriptSig depending on the type of the spent outputs. Clean
14 +/// fields that aren't useful anymore, partial signatures, redeem script and
15 +/// derivation paths.
16 +///
17 +/// @param psbt The psbt with all signatures added as partial sigs, either
18 +/// through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG
19 +extension InputFinalizer on PsbtV2 {
20 + void finalizeV0() {
21 +
22 + // First check that each input has a signature
23 + for (var i = 0; i < getGlobalInputCount(); i++) {
24 + if (_isFinalized(i)) continue;
25 +
26 + final legacyPubkeys = getInputKeyDatas(i, PSBTIn.partialSig);
27 + final taprootSig = getInputTapKeySig(i);
28 + if (legacyPubkeys.isEmpty && taprootSig == null) {
29 + continue;
30 + // throw Exception('No signature for input $i present');
31 + }
32 + if (legacyPubkeys.isNotEmpty) {
33 + if (legacyPubkeys.length > 1) {
34 + throw Exception(
35 + 'Expected exactly one signature, got ${legacyPubkeys.length}');
36 + }
37 + if (taprootSig != null) {
38 + throw Exception('Both taproot and non-taproot signatures present.');
39 + }
40 +
41 + final isSegwitV0 = getInputWitnessUtxo(i) != null;
42 + final redeemScript = getInputRedeemScript(i);
43 + final isWrappedSegwit = redeemScript != null;
44 + final signature = getInputPartialSig(i, legacyPubkeys[0]);
45 + if (signature == null) {
46 + throw Exception('Expected partial signature for input $i');
47 + }
48 + if (isSegwitV0) {
49 + final witnessBuf = BufferWriter()
50 + ..writeVarInt(2)
51 + ..writeVarInt(signature.length)
52 + ..writeSlice(signature)
53 + ..writeVarInt(legacyPubkeys[0].length)
54 + ..writeSlice(legacyPubkeys[0]);
55 + setInputFinalScriptwitness(i, witnessBuf.buffer());
56 + if (isWrappedSegwit) {
57 + if (redeemScript.isEmpty) {
58 + throw Exception(
59 + "Expected non-empty redeemscript. Can't finalize intput $i");
60 + }
61 + final scriptSigBuf = BufferWriter()
62 + ..writeUInt8(redeemScript.length) // Push redeemScript length
63 + ..writeSlice(redeemScript);
64 + setInputFinalScriptsig(i, scriptSigBuf.buffer());
65 + }
66 + } else {
67 + // Legacy input
68 + final scriptSig = BufferWriter();
69 + _writePush(scriptSig, signature);
70 + _writePush(scriptSig, legacyPubkeys[0]);
71 + setInputFinalScriptsig(i, scriptSig.buffer());
72 + }
73 + } else {
74 + // Taproot input
75 + final signature = getInputTapKeySig(i);
76 + if (signature == null) {
77 + throw Exception("No taproot signature found");
78 + }
79 + if (signature.length != 64 && signature.length != 65) {
80 + throw Exception("Unexpected length of schnorr signature.");
81 + }
82 + final witnessBuf = BufferWriter()
83 + ..writeVarInt(1)
84 + ..writeVarSlice(signature);
85 + setInputFinalScriptwitness(i, witnessBuf.buffer());
86 + }
87 + clearFinalizedInput(i);
88 + }
89 + }
90 +
91 + /// Deletes fields that are no longer neccesary from the psbt.
92 + ///
93 + /// Note, the spec doesn't say anything about removing ouput fields
94 + /// like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them
95 + /// without actually knowing why. I think we should remove them too.
96 + void clearFinalizedInput(int inputIndex) {
97 + final keyTypes = [
98 + PSBTIn.bip32Derivation,
99 + PSBTIn.partialSig,
100 + PSBTIn.tapBip32Derivation,
101 + PSBTIn.tapKeySig,
102 + ];
103 + final witnessUtxoAvailable = getInputWitnessUtxo(inputIndex) != null;
104 + final nonWitnessUtxoAvailable = getInputNonWitnessUtxo(inputIndex) != null;
105 + if (witnessUtxoAvailable && nonWitnessUtxoAvailable) {
106 + // Remove NON_WITNESS_UTXO for segwit v0 as it's only needed while signing.
107 + // Segwit v1 doesn't have NON_WITNESS_UTXO set.
108 + // See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7
109 + keyTypes.add(PSBTIn.nonWitnessUTXO);
110 + }
111 + deleteInputEntries(inputIndex, keyTypes);
112 + }
113 +
114 + /// Writes a script push operation to buf, which looks different
115 + /// depending on the size of the data. See
116 + /// https://en.bitcoin.it/wiki/Script#finalants
117 + ///
118 + /// [buf] the BufferWriter to write to
119 + /// [data] the Buffer to be pushed.
120 + void _writePush(BufferWriter buf, Uint8List data) {
121 + if (data.length <= 75) {
122 + buf.writeUInt8(data.length);
123 + } else if (data.length <= 256) {
124 + buf.writeUInt8(76);
125 + buf.writeUInt8(data.length);
126 + } else if (data.length <= 256 * 256) {
127 + buf.writeUInt8(77);
128 + final b = ByteData(2)..setUint16(0, data.length, Endian.little);
129 + buf.writeSlice(b.buffer.asUint8List());
130 + }
131 + buf.writeSlice(data);
132 + }
133 +
134 + bool _isFinalized(int i) {
135 + if (getInputFinalScriptsig(i) != null) return true;
136 + try {
137 + getInputFinalScriptwitness(i);
138 + return true;
139 + } catch (_) {
140 + return false;
141 + }
142 + }
143 +}
cw_bitcoin/pubspec.lock
+51 -2
@@ -125,6 +125,14 @@ packages:
125 url: "https://pub.dev"
126 source: hosted
127 version: "2.4.1"
128 + build_cli_annotations:
129 + dependency: transitive
130 + description:
131 + name: build_cli_annotations
132 + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
133 + url: "https://pub.dev"
134 + source: hosted
135 + version: "2.1.0"
136 build_config:
137 dependency: transitive
138 description:
@@ -377,6 +385,14 @@ packages:
385 url: "https://pub.dev"
386 source: hosted
387 version: "2.3.0"
388 + flutter_rust_bridge:
389 + dependency: transitive
390 + description:
391 + name: flutter_rust_bridge
392 + sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00"
393 + url: "https://pub.dev"
394 + source: hosted
395 + version: "2.7.1"
396 flutter_test:
397 dependency: "direct dev"
398 description: flutter
@@ -395,6 +411,14 @@ packages:
411 description: flutter
412 source: sdk
413 version: "0.0.0"
414 + freezed_annotation:
415 + dependency: transitive
416 + description:
417 + name: freezed_annotation
418 + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
419 + url: "https://pub.dev"
420 + source: hosted
421 + version: "2.4.4"
422 frontend_server_client:
423 dependency: transitive
424 description:
@@ -559,7 +583,7 @@ packages:
583 dependency: "direct main"
584 description:
585 path: "packages/ledger-bitcoin"
562 - ref: HEAD
586 + ref: trunk
587 resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8
588 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins"
589 source: git
@@ -726,6 +750,15 @@ packages:
750 url: "https://pub.dev"
751 source: hosted
752 version: "2.3.0"
753 + payjoin_flutter:
754 + dependency: "direct main"
755 + description:
756 + path: "."
757 + ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c"
758 + resolved-ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c"
759 + url: "https://github.com/konstantinullrich/payjoin-flutter"
760 + source: git
761 + version: "0.21.0"
762 petitparser:
763 dependency: transitive
764 description:
@@ -940,6 +973,14 @@ packages:
973 url: "https://github.com/cake-tech/sp_scanner"
974 source: git
975 version: "0.0.1"
976 + sprintf:
977 + dependency: transitive
978 + description:
979 + name: sprintf
980 + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
981 + url: "https://pub.dev"
982 + source: hosted
983 + version: "7.0.0"
984 stack_trace:
985 dependency: transitive
986 description:
@@ -1036,6 +1077,14 @@ packages:
1077 url: "https://pub.dev"
1078 source: hosted
1079 version: "0.3.0"
1080 + uuid:
1081 + dependency: transitive
1082 + description:
1083 + name: uuid
1084 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
1085 + url: "https://pub.dev"
1086 + source: hosted
1087 + version: "4.5.1"
1088 vector_math:
1089 dependency: transitive
1090 description:
@@ -1118,4 +1167,4 @@ packages:
1167 version: "2.2.2"
1168 sdks:
1169 dart: ">=3.5.0 <4.0.0"
1121 - flutter: ">=3.24.0"
1170 + flutter: ">=3.27.4"
cw_bitcoin/pubspec.yaml
+5
@@ -40,11 +40,16 @@ dependencies:
40 bech32:
41 git:
42 url: https://github.com/cake-tech/bech32.git
43 + payjoin_flutter:
44 + git:
45 + url: https://github.com/konstantinullrich/payjoin-flutter
46 + ref: 6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c #cake-v1
47 ledger_flutter_plus: ^1.4.1
48 ledger_bitcoin:
49 git:
50 url: https://github.com/cake-tech/ledger-flutter-plus-plugins
51 path: packages/ledger-bitcoin
52 + ref: trunk
53 ledger_litecoin:
54 git:
55 url: https://github.com/cake-tech/ledger-flutter-plus-plugins
cw_core/lib/hive_type_ids.dart
+1
@@ -21,3 +21,4 @@ const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
21 const MWEB_UTXO_TYPE_ID = 20;
22 const HAVEN_SEED_STORE_TYPE_ID = 21;
23 const ZANO_ASSET_TYPE_ID = 22;
24 +const PAYJOIN_SESSION_TYPE_ID = 23;
cw_core/lib/payjoin_session.dart new
+67
@@ -0,0 +1,67 @@
1 +import 'package:cw_core/hive_type_ids.dart';
2 +import 'package:hive/hive.dart';
3 +
4 +part 'payjoin_session.g.dart';
5 +
6 +@HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
7 +class PayjoinSession extends HiveObject {
8 + PayjoinSession({
9 + required this.walletId,
10 + this.receiver,
11 + this.sender,
12 + this.pjUri,
13 + this.status = "created",
14 + this.inProgressSince,
15 + this.rawAmount,
16 + }) {
17 + if (receiver == null) {
18 + assert(sender != null);
19 + assert(pjUri != null);
20 + } else {
21 + assert(receiver != null);
22 + }
23 + }
24 +
25 + static const typeId = PAYJOIN_SESSION_TYPE_ID;
26 + static const boxName = 'PayjoinSessions';
27 +
28 + @HiveField(0)
29 + final String walletId;
30 +
31 + @HiveField(1)
32 + final String? sender;
33 +
34 + @HiveField(2)
35 + final String? receiver;
36 +
37 + @HiveField(3)
38 + final String? pjUri;
39 +
40 + @HiveField(4)
41 + String status;
42 +
43 + @HiveField(5)
44 + DateTime? inProgressSince;
45 +
46 + @HiveField(6)
47 + String? txId;
48 +
49 + @HiveField(7)
50 + String? rawAmount;
51 +
52 + @HiveField(8)
53 + String? error;
54 +
55 + bool get isSenderSession => sender != null;
56 +
57 + BigInt get amount => BigInt.parse(rawAmount ?? "0");
58 + set amount(BigInt amount) => rawAmount = amount.toString();
59 +
60 +}
61 +
62 +enum PayjoinSessionStatus {
63 + created,
64 + inProgress,
65 + success,
66 + unrecoverable,
67 +}
cw_core/pubspec.lock
+1 -1
@@ -810,4 +810,4 @@ packages:
810 version: "3.1.3"
811 sdks:
812 dart: ">=3.5.0 <4.0.0"
813 - flutter: ">=3.24.0"
813 + flutter: ">=3.27.4"
cw_decred/pubspec.lock
+1 -1
@@ -849,4 +849,4 @@ packages:
849 version: "2.2.2"
850 sdks:
851 dart: ">=3.5.0 <4.0.0"
852 - flutter: ">=3.24.0"
852 + flutter: ">=3.27.4"
cw_monero/pubspec.lock
+1 -1
@@ -978,4 +978,4 @@ packages:
978 version: "3.1.3"
979 sdks:
980 dart: ">=3.6.0 <4.0.0"
981 - flutter: ">=3.24.0"
981 + flutter: ">=3.27.4"
cw_nano/pubspec.lock
+1 -1
@@ -946,4 +946,4 @@ packages:
946 version: "3.1.3"
947 sdks:
948 dart: ">=3.5.0 <4.0.0"
949 - flutter: ">=3.24.0"
949 + flutter: ">=3.27.4"
cw_wownero/pubspec.lock
+1 -1
@@ -845,4 +845,4 @@ packages:
845 version: "3.1.3"
846 sdks:
847 dart: ">=3.5.0 <4.0.0"
848 - flutter: ">=3.24.0"
848 + flutter: ">=3.27.4"
cw_zano/pubspec.lock
+1 -1
@@ -842,4 +842,4 @@ packages:
842 version: "3.1.3"
843 sdks:
844 dart: ">=3.5.0 <4.0.0"
845 - flutter: ">=3.24.0"
845 + flutter: ">=3.27.4"
lib/bitcoin/cw_bitcoin.dart
+62 -20
@@ -109,26 +109,29 @@ class CWBitcoin extends Bitcoin {
109 required TransactionPriority priority,
110 int? feeRate,
111 UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
112 + String? payjoinUri,
113 }) {
114 final bitcoinFeeRate =
114 - priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
115 + priority == BitcoinTransactionPriority.custom && feeRate != null
116 + ? feeRate
117 + : null;
118 return BitcoinTransactionCredentials(
116 - outputs
117 - .map((out) => OutputInfo(
118 - fiatAmount: out.fiatAmount,
119 - cryptoAmount: out.cryptoAmount,
120 - address: out.address,
121 - note: out.note,
122 - sendAll: out.sendAll,
123 - extractedAddress: out.extractedAddress,
124 - isParsedAddress: out.isParsedAddress,
125 - formattedCryptoAmount: out.formattedCryptoAmount,
126 - memo: out.memo))
127 - .toList(),
128 - priority: priority as BitcoinTransactionPriority,
129 - feeRate: bitcoinFeeRate,
130 - coinTypeToSpendFrom: coinTypeToSpendFrom,
131 - );
119 + outputs
120 + .map((out) => OutputInfo(
121 + fiatAmount: out.fiatAmount,
122 + cryptoAmount: out.cryptoAmount,
123 + address: out.address,
124 + note: out.note,
125 + sendAll: out.sendAll,
126 + extractedAddress: out.extractedAddress,
127 + isParsedAddress: out.isParsedAddress,
128 + formattedCryptoAmount: out.formattedCryptoAmount,
129 + memo: out.memo))
130 + .toList(),
131 + priority: priority as BitcoinTransactionPriority,
132 + feeRate: bitcoinFeeRate,
133 + coinTypeToSpendFrom: coinTypeToSpendFrom,
134 + payjoinUri: payjoinUri);
135 }
136
137 @override
@@ -224,9 +227,14 @@ class CWBitcoin extends Bitcoin {
227 await bitcoinWallet.updateAllUnspents();
228 }
229
227 - WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource,
228 - Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
229 - return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
230 + WalletService createBitcoinWalletService(
231 + Box<WalletInfo> walletInfoSource,
232 + Box<UnspentCoinsInfo> unspentCoinSource,
233 + Box<PayjoinSession> payjoinSessionSource,
234 + bool alwaysScan,
235 + bool isDirect) {
236 + return BitcoinWalletService(walletInfoSource, unspentCoinSource,
237 + payjoinSessionSource, alwaysScan, isDirect);
238 }
239
240 WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
@@ -550,6 +558,10 @@ class CWBitcoin extends Bitcoin {
558 return option is BitcoinReceivePageOption;
559 }
560
561 + @override
562 + bool isPayjoinAvailable(Object wallet) =>
563 + (wallet is BitcoinWallet) && (wallet as BitcoinWallet).isPayjoinAvailable;
564 +
565 @override
566 BitcoinAddressType getOptionToType(ReceivePageOption option) {
567 return (option as BitcoinReceivePageOption).toType();
@@ -706,4 +718,34 @@ class CWBitcoin extends Bitcoin {
718 return null;
719 }
720 }
721 +
722 + @override
723 + String getPayjoinEndpoint(Object wallet) {
724 + final _wallet = wallet as ElectrumWallet;
725 + if (!isPayjoinAvailable(wallet)) return '';
726 + return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? '';
727 + }
728 +
729 + @override
730 + void updatePayjoinState(Object wallet, bool value) {
731 + final _wallet = wallet as ElectrumWallet;
732 + if (value) {
733 + (_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
734 + } else {
735 + stopPayjoinSessions(wallet);
736 + }
737 + }
738 +
739 + @override
740 + void resumePayjoinSessions(Object wallet) {
741 + final _wallet = wallet as ElectrumWallet;
742 + (_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
743 + }
744 +
745 + @override
746 + void stopPayjoinSessions(Object wallet) {
747 + final _wallet = wallet as ElectrumWallet;
748 + (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions();
749 + (_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
750 + }
751 }
lib/di.dart
+25
@@ -51,6 +51,7 @@ import 'package:cake_wallet/entities/wallet_manager.dart';
51 import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
52 import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
53 import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
54 +import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
55 import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
56 import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
57 import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
@@ -58,7 +59,10 @@ import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
59 import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
60 import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
61 import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
62 +import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
63 import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
64 +import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
65 +import 'package:cw_core/payjoin_session.dart';
66 import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
67 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
68 import 'package:cake_wallet/entities/preferences_key.dart';
@@ -286,6 +290,7 @@ late Box<ExchangeTemplate> _exchangeTemplates;
290 late Box<TransactionDescription> _transactionDescriptionBox;
291 late Box<Order> _ordersSource;
292 late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
293 +late Box<PayjoinSession> _payjoinSessionSource;
294 late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
295
296 Future<void> setup({
@@ -299,6 +304,7 @@ Future<void> setup({
304 required Box<TransactionDescription> transactionDescriptionBox,
305 required Box<Order> ordersSource,
306 required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
307 + required Box<PayjoinSession> payjoinSessionSource,
308 required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource,
309 required SecureStorage secureStorage,
310 required GlobalKey<NavigatorState> navigatorKey,
@@ -313,6 +319,7 @@ Future<void> setup({
319 _transactionDescriptionBox = transactionDescriptionBox;
320 _ordersSource = ordersSource;
321 _unspentCoinsInfoSource = unspentCoinsInfoSource;
322 + _payjoinSessionSource = payjoinSessionSource;
323 _anonpayInvoiceInfoSource = anonpayInvoiceInfoSource;
324
325 if (!_isSetupFinished) {
@@ -354,6 +361,8 @@ Future<void> setup({
361 TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get<SettingsStore>()));
362 getIt.registerSingleton<OrdersStore>(
363 OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
364 + getIt.registerFactory(() =>
365 + PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
366 getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
367 getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
368 getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
@@ -507,6 +516,7 @@ Future<void> setup({
516 yatStore: getIt.get<YatStore>(),
517 ordersStore: getIt.get<OrdersStore>(),
518 anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
519 + payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
520 sharedPreferences: getIt.get<SharedPreferences>(),
521 keyService: getIt.get<KeyService>()));
522
@@ -1095,6 +1105,7 @@ Future<void> setup({
1105 return bitcoin!.createBitcoinWalletService(
1106 _walletInfoSource,
1107 _unspentCoinsInfoSource,
1108 + _payjoinSessionSource,
1109 getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
1110 SettingsStoreBase.walletPasswordDirectInput,
1111 );
@@ -1423,6 +1434,15 @@ Future<void> setup({
1434 settingsStore: getIt.get<SettingsStore>(),
1435 ));
1436
1437 + getIt.registerFactoryParam<PayjoinDetailsViewModel, String, TransactionInfo?>(
1438 + (String sessionId, TransactionInfo? transactionInfo) =>
1439 + PayjoinDetailsViewModel(
1440 + sessionId,
1441 + transactionInfo,
1442 + payjoinSessionSource: _payjoinSessionSource,
1443 + settingsStore: getIt.get<SettingsStore>(),
1444 + ));
1445 +
1446 getIt.registerFactoryParam<AnonPayReceivePage, AnonpayInfoBase, void>(
1447 (AnonpayInfoBase anonpayInvoiceInfo, _) =>
1448 AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo));
@@ -1431,6 +1451,11 @@ Future<void> setup({
1451 (AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(
1452 anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
1453
1454 + getIt.registerFactoryParam<PayjoinDetailsPage, String, TransactionInfo?>(
1455 + (String sessionId, TransactionInfo? transactionInfo) => PayjoinDetailsPage(
1456 + payjoinDetailsViewModel: getIt.get<PayjoinDetailsViewModel>(
1457 + param1: sessionId, param2: transactionInfo)));
1458 +
1459 getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
1460 HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
1461
lib/entities/preferences_key.dart
+1
@@ -82,6 +82,7 @@ class PreferencesKey {
82 static const lookupsOpenAlias = 'looks_up_open_alias';
83 static const lookupsENS = 'looks_up_ens';
84 static const lookupsWellKnown = 'looks_up_well_known';
85 + static const usePayjoin = 'use_payjoin';
86 static const showCameraConsent = 'show_camera_consent';
87 static const showDecredInfoCard = 'show_decred_info_card';
88
lib/main.dart
+9
@@ -35,6 +35,7 @@ import 'package:cw_core/cake_hive.dart';
35 import 'package:cw_core/hive_type_ids.dart';
36 import 'package:cw_core/mweb_utxo.dart';
37 import 'package:cw_core/node.dart';
38 +import 'package:cw_core/payjoin_session.dart';
39 import 'package:cw_core/unspent_coins_info.dart';
40 import 'package:cw_core/utils/print_verbose.dart';
41 import 'package:cw_core/wallet_info.dart';
@@ -178,6 +179,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
179 CakeHive.registerAdapter(MwebUtxoAdapter());
180 }
181
182 + if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
183 + CakeHive.registerAdapter(PayjoinSessionAdapter());
184 + }
185 +
186 final secureStorage = secureStorageShared;
187 final transactionDescriptionsBoxKey =
188 await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@@ -197,6 +202,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
202 final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
203 final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
204 final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
205 + final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
206
207 final havenSeedStoreBoxKey =
208 await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
@@ -219,6 +225,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
225 exchangeTemplates: exchangeTemplates,
226 transactionDescriptions: transactionDescriptions,
227 secureStorage: secureStorage,
228 + payjoinSessionSource: payjoinSessionSource,
229 anonpayInvoiceInfo: anonpayInvoiceInfo,
230 havenSeedStore: havenSeedStore,
231 initialMigrationVersion: 49,
@@ -241,6 +248,7 @@ Future<void> initialSetup(
248 required SecureStorage secureStorage,
249 required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
250 required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
251 + required Box<PayjoinSession> payjoinSessionSource,
252 required Box<HavenSeedStore> havenSeedStore,
253 int initialMigrationVersion = 15, }) async {
254 LanguageService.loadLocaleList();
@@ -266,6 +274,7 @@ Future<void> initialSetup(
274 ordersSource: ordersSource,
275 anonpayInvoiceInfoSource: anonpayInvoiceInfo,
276 unspentCoinsInfoSource: unspentCoinsInfoSource,
277 + payjoinSessionSource: payjoinSessionSource,
278 navigatorKey: navigatorKey,
279 secureStorage: secureStorage,
280 );
lib/reactions/on_current_wallet_change.dart
+5
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/di.dart';
3 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
4 import 'package:cake_wallet/entities/fiat_api_mode.dart';
@@ -78,6 +79,10 @@ void startCurrentWalletChangeReaction(
79 _setAutoGenerateSubaddressStatus(wallet, settingsStore);
80 }
81
82 + if (wallet.type == WalletType.bitcoin) {
83 + bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin);
84 + }
85 +
86 await wallet.connectToNode(node: node);
87 if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
88 final powNode = settingsStore.getCurrentPowNode(wallet.type);
lib/router.dart
+9
@@ -58,6 +58,7 @@ import 'package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_de
58 import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart';
59 import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
60 import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
61 +import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
62 import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
63 import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
64 import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
@@ -719,6 +720,14 @@ Route<dynamic> createRoute(RouteSettings settings) {
720 return CupertinoPageRoute<void>(
721 builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData));
722
723 + case Routes.payjoinDetails:
724 + final arguments = settings.arguments as List;
725 + final sessionId = arguments.first as String;
726 + final transactionInfo = arguments[1] as TransactionInfo?;
727 + return CupertinoPageRoute<void>(
728 + builder: (_) => getIt.get<PayjoinDetailsPage>(
729 + param1: sessionId, param2: transactionInfo));
730 +
731 case Routes.desktop_actions:
732 return PageRouteBuilder(
733 opaque: false,
lib/routes.dart
+1
@@ -15,6 +15,7 @@ class Routes {
15 static const dashboard = '/dashboard';
16 static const send = '/send';
17 static const transactionDetails = '/transaction_info';
18 + static const payjoinDetails = '/transaction_info/payjoin';
19 static const bumpFeePage = '/bump_fee_page';
20 static const receive = '/receive';
21 static const newSubaddress = '/new_subaddress';
lib/src/screens/dashboard/pages/transactions_page.dart
+21
@@ -1,12 +1,14 @@
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
3 import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
4 +import 'package:cake_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
5 import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart';
6 import 'package:cake_wallet/themes/extensions/placeholder_theme.dart';
7 import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
8 import 'package:cake_wallet/utils/responsive_layout_util.dart';
9 import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
10 import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
11 +import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
12 import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
13 import 'package:cw_core/crypto_currency.dart';
14 import 'package:cw_core/sync_status.dart';
@@ -143,6 +145,25 @@ class TransactionsPage extends StatelessWidget {
145 );
146 }
147
148 + if (item is PayjoinTransactionListItem) {
149 + final session = item.session;
150 +
151 + return PayjoinTransactionRow(
152 + key: item.key,
153 + onTap: () => Navigator.of(context).pushNamed(
154 + Routes.payjoinDetails,
155 + arguments: [item.sessionId, item.transaction],
156 + ),
157 + currency: "BTC",
158 + state: item.status,
159 + amount: bitcoin!.formatterBitcoinAmountToString(
160 + amount: session.amount.toInt()),
161 + createdAt: DateFormat('HH:mm')
162 + .format(session.inProgressSince!),
163 + isSending: session.isSenderSession,
164 + );
165 + }
166 +
167 if (item is TradeListItem) {
168 final trade = item.trade;
169
lib/src/screens/dashboard/widgets/payjoin_transaction_row.dart new
+99
@@ -0,0 +1,99 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
3 +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
4 +import 'package:flutter/material.dart';
5 +
6 +class PayjoinTransactionRow extends StatelessWidget {
7 + PayjoinTransactionRow({
8 + required this.createdAt,
9 + required this.currency,
10 + required this.onTap,
11 + required this.amount,
12 + required this.state,
13 + required this.isSending,
14 + super.key,
15 + });
16 +
17 + final VoidCallback? onTap;
18 + final String createdAt;
19 + final String amount;
20 + final String currency;
21 + final String state;
22 + final bool isSending;
23 +
24 + @override
25 + Widget build(BuildContext context) {
26 + return InkWell(
27 + onTap: onTap,
28 + child: Container(
29 + padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
30 + color: Colors.transparent,
31 + child: Row(
32 + mainAxisSize: MainAxisSize.max,
33 + crossAxisAlignment: CrossAxisAlignment.center,
34 + children: [
35 + _getImage(),
36 + SizedBox(width: 12),
37 + Expanded(
38 + child: Column(
39 + mainAxisSize: MainAxisSize.min,
40 + children: [
41 + Row(
42 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
43 + children: <Widget>[
44 + Text(
45 + "${isSending ? S.current.outgoing : S.current.incoming} Payjoin",
46 + style: TextStyle(
47 + fontSize: 16,
48 + fontWeight: FontWeight.w500,
49 + color: Theme.of(context)
50 + .extension<DashboardPageTheme>()!
51 + .textColor,
52 + ),
53 + ),
54 + Text(
55 + amount + ' ' + currency,
56 + style: TextStyle(
57 + fontSize: 16,
58 + fontWeight: FontWeight.w500,
59 + color: Theme.of(context)
60 + .extension<DashboardPageTheme>()!
61 + .textColor,
62 + ),
63 + )
64 + ]),
65 + SizedBox(height: 5),
66 + Row(
67 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
68 + children: <Widget>[
69 + Text(
70 + createdAt,
71 + style: TextStyle(
72 + fontSize: 14,
73 + color: Theme.of(context)
74 + .extension<CakeTextTheme>()!
75 + .dateSectionRowColor,
76 + ),
77 + ),
78 + Text(
79 + state,
80 + style: TextStyle(
81 + fontSize: 14,
82 + color: Theme.of(context)
83 + .extension<CakeTextTheme>()!
84 + .dateSectionRowColor,
85 + ),
86 + ),
87 + ])
88 + ],
89 + ))
90 + ],
91 + ),
92 + ));
93 + }
94 +
95 + Widget _getImage() => ClipRRect(
96 + borderRadius: BorderRadius.circular(50),
97 + child: Image.asset('assets/images/payjoin.png', width: 36, height: 36));
98 +
99 +}
lib/src/screens/payjoin_details/payjoin_details_page.dart new
+77
@@ -0,0 +1,77 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/base_page.dart';
3 +import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
4 +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
5 +import 'package:cake_wallet/src/widgets/list_row.dart';
6 +import 'package:cake_wallet/src/widgets/standard_list.dart';
7 +import 'package:cake_wallet/src/widgets/standard_list_card.dart';
8 +import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
9 +import 'package:cake_wallet/utils/show_bar.dart';
10 +import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
11 +import 'package:flutter/material.dart';
12 +import 'package:flutter/services.dart';
13 +
14 +class PayjoinDetailsPage extends BasePage {
15 + PayjoinDetailsPage({required this.payjoinDetailsViewModel});
16 +
17 + @override
18 + String get title => S.current.payjoin_details;
19 +
20 + final PayjoinDetailsViewModel payjoinDetailsViewModel;
21 +
22 + @override
23 + Widget body(BuildContext context) => PayjoinDetailsPageBody(payjoinDetailsViewModel);
24 +}
25 +
26 +class PayjoinDetailsPageBody extends StatefulWidget {
27 + PayjoinDetailsPageBody(this.payjoinDetailsViewModel);
28 +
29 + final PayjoinDetailsViewModel payjoinDetailsViewModel;
30 +
31 + @override
32 + State<PayjoinDetailsPageBody> createState() => _PayjoinDetailsPageBodyState();
33 +}
34 +
35 +class _PayjoinDetailsPageBodyState extends State<PayjoinDetailsPageBody> {
36 + @override
37 + void dispose() {
38 + super.dispose();
39 + widget.payjoinDetailsViewModel.listener.cancel();
40 + }
41 +
42 + @override
43 + Widget build(BuildContext context) {
44 + return SectionStandardList(
45 + sectionCount: 1,
46 + itemCounter: (int _) => widget.payjoinDetailsViewModel.items.length,
47 + itemBuilder: (__, index) {
48 + final item = widget.payjoinDetailsViewModel.items[index];
49 +
50 + if (item is DetailsListStatusItem) {
51 + return StandardListStatusRow(
52 + title: item.title,
53 + value: item.value,
54 + status: item.status,
55 + );
56 + }
57 +
58 + if (item is TradeDetailsListCardItem) {
59 + return TradeDetailsStandardListCard(
60 + id: item.id,
61 + create: item.createdAt,
62 + pair: item.pair,
63 + currentTheme: widget.payjoinDetailsViewModel.settingsStore.currentTheme.type,
64 + onTap: item.onTap,
65 + );
66 + }
67 +
68 + return GestureDetector(
69 + onTap: () {
70 + Clipboard.setData(ClipboardData(text: item.value));
71 + showBar<void>(context, S.of(context).transaction_details_copied(item.title));
72 + },
73 + child: ListRow(title: '${item.title}:', value: item.value),
74 + );
75 + });
76 + }
77 +}
lib/src/screens/receive/widgets/qr_widget.dart
+67 -19
@@ -1,4 +1,6 @@
1 import 'package:cake_wallet/entities/qr_view_data.dart';
2 +import 'package:cake_wallet/src/widgets/primary_button.dart';
3 +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
4 import 'package:cake_wallet/themes/extensions/picker_theme.dart';
5 import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
6 import 'package:cake_wallet/routes.dart';
@@ -91,26 +93,54 @@ class QRWidget extends StatelessWidget {
93 child: Hero(
94 tag: Key(heroTag ?? addressUri.toString()),
95 child: Center(
94 - child: AspectRatio(
95 - aspectRatio: 1.0,
96 - child: Container(
97 - padding: EdgeInsets.all(5),
98 - decoration: BoxDecoration(
99 - border: Border.all(
100 - width: 3,
101 - color: Theme.of(context)
102 - .extension<DashboardPageTheme>()!
103 - .textColor,
104 - ),
105 - ),
106 - child: Container(
107 - decoration: BoxDecoration(
108 - border: Border.all(
109 - width: 3,
110 - color: Colors.white,
96 + child: Container(
97 + padding: EdgeInsets.zero,
98 + decoration: BoxDecoration(
99 + border: Border(top: BorderSide.none),
100 + borderRadius:
101 + BorderRadius.all(Radius.circular(5)),
102 + color: Colors.white,
103 + ),
104 + child: Column(
105 + children: [
106 + Container(
107 + padding: EdgeInsets.all(3),
108 + child: AspectRatio(
109 + aspectRatio: 1.0,
110 + child: QrImage(
111 + data: addressUri.toString(),
112 ),
113 ),
113 - child: QrImage(data: addressUri.toString())),
114 + ),
115 + if (addressListViewModel
116 + .payjoinEndpoint.isNotEmpty) ...[
117 + Row(
118 + mainAxisAlignment:
119 + MainAxisAlignment.center,
120 + children: [
121 + Padding(
122 + padding: EdgeInsets.only(
123 + top: 4,
124 + bottom: 4,
125 + right: 4,
126 + ),
127 + child: Image.asset(
128 + 'assets/images/payjoin.png',
129 + width: 20,
130 + ),
131 + ),
132 + Text(
133 + S.of(context).payjoin_enabled,
134 + style: TextStyle(
135 + fontSize: 12,
136 + fontWeight: FontWeight.w600,
137 + color: Colors.black,
138 + ),
139 + ),
140 + ],
141 + ),
142 + ]
143 + ],
144 ),
145 ),
146 ),
@@ -179,7 +209,25 @@ class QRWidget extends StatelessWidget {
209 ),
210 ),
211 ),
182 - )
212 + ),
213 + if (addressListViewModel.payjoinEndpoint.isNotEmpty) ...[
214 + Padding(
215 + padding: EdgeInsets.only(top: 12),
216 + child: PrimaryImageButton(
217 + onPressed: () {
218 + Clipboard.setData(ClipboardData(
219 + text: addressListViewModel.payjoinEndpoint));
220 + showBar<void>(context, S.of(context).copied_to_clipboard);
221 + },
222 + image: Image.asset('assets/images/payjoin.png', width: 25,),
223 + text: S.of(context).copy_payjoin_url,
224 + color: Theme.of(context).cardColor,
225 + textColor: Theme.of(context)
226 + .extension<CakeTextTheme>()!
227 + .buttonTextColor,
228 + ),
229 + ),
230 + ],
231 ],
232 ),
233 ),
lib/src/screens/root/root.dart
+9 -1
@@ -1,5 +1,5 @@
1 import 'dart:async';
2 -import 'dart:io';
2 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
3 import 'package:cake_wallet/core/auth_service.dart';
4 import 'package:cake_wallet/core/totp_request_details.dart';
5 import 'package:cake_wallet/utils/device_info.dart';
@@ -136,6 +136,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
136 setState(() => _setInactive(true));
137 }
138
139 + if (widget.appStore.wallet?.type == WalletType.bitcoin) {
140 + bitcoin!.stopPayjoinSessions(widget.appStore.wallet!);
141 + }
142 +
143 break;
144 case AppLifecycleState.resumed:
145 widget.authService.requireAuth().then((value) {
@@ -145,6 +149,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
149 });
150 }
151 });
152 + if (widget.appStore.wallet?.type == WalletType.bitcoin &&
153 + widget.appStore.settingsStore.usePayjoin) {
154 + bitcoin!.resumePayjoinSessions(widget.appStore.wallet!);
155 + }
156 break;
157 default:
158 break;
lib/src/screens/send/send_page.dart
+1 -1
@@ -463,7 +463,7 @@ class SendPage extends BasePage {
463 },
464 );
465 },
466 - text: S.of(context).send,
466 + text: sendViewModel.payjoinUri != null ? S.of(context).send_payjoin : S.of(context).send,
467 color: Theme.of(context).primaryColor,
468 textColor: Colors.white,
469 isLoading: sendViewModel.state is IsExecutingState ||
lib/src/screens/send/widgets/send_card.dart
+17 -13
@@ -188,19 +188,23 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
188 sendViewModel.createOpenCryptoPayTransaction(uri.toString());
189 } else {
190 final paymentRequest = PaymentRequest.fromUri(uri);
191 - addressController.text = paymentRequest.address;
192 - cryptoAmountController.text = paymentRequest.amount;
193 - noteController.text = paymentRequest.note;
194 - }
195 - },
196 - options: [
197 - AddressTextFieldOption.paste,
198 - AddressTextFieldOption.qrCode,
199 - AddressTextFieldOption.addressBook
200 - ],
201 - buttonColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
202 - borderColor: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
203 - textStyle:
191 + if (sendViewModel.usePayjoin) {
192 + sendViewModel.payjoinUri = paymentRequest.pjUri;
193 + }
194 + addressController.text = paymentRequest.address;
195 + cryptoAmountController.text = paymentRequest.amount;
196 + noteController.text = paymentRequest.note;}
197 + },
198 + options: [
199 + AddressTextFieldOption.paste,
200 + AddressTextFieldOption.qrCode,
201 + AddressTextFieldOption.addressBook
202 + ],
203 + buttonColor:
204 + Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
205 + borderColor:
206 + Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
207 + textStyle:
208 TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
209 hintStyle: TextStyle(
210 fontSize: 14,
lib/src/screens/settings/privacy_page.dart
+8
@@ -49,6 +49,14 @@ class PrivacyPage extends BasePage {
49 _privacySettingsViewModel.setExchangeApiMode(mode),
50 ),
51 ),
52 + if (_privacySettingsViewModel.canUsePayjoin)
53 + SettingsSwitcherCell(
54 + title: S.of(context).use_payjoin,
55 + value: _privacySettingsViewModel.usePayjoin,
56 + onValueChange: (BuildContext _, bool value) {
57 + _privacySettingsViewModel.setUsePayjoin(value);
58 + },
59 + ),
60 SettingsSwitcherCell(
61 title: S.current.settings_save_recipient_address,
62 value: _privacySettingsViewModel.shouldSaveRecipientAddress,
lib/src/screens/trade_details/trade_details_status_item.dart
+3 -1
@@ -2,6 +2,8 @@ import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.d
2
3 class DetailsListStatusItem extends StandartListItem {
4 DetailsListStatusItem(
5 - {required String title, required String value})
5 + {required String title, required String value, this.status})
6 : super(title: title, value: value);
7 +
8 + final String? status; // waiting, action required, created, fetching, finished, success
9 }
lib/src/widgets/address_text_field.dart
+19 -15
@@ -1,21 +1,20 @@
1 -import 'package:cake_wallet/utils/device_info.dart';
1 +import 'package:cake_wallet/entities/contact_base.dart';
2 +import 'package:cake_wallet/entities/qr_scanner.dart';
3 +import 'package:cake_wallet/generated/i18n.dart';
4 +import 'package:cake_wallet/routes.dart';
5 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
6 +import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
7 +import 'package:cake_wallet/utils/device_info.dart';
8 +import 'package:cake_wallet/utils/permission_handler.dart';
9 import 'package:cake_wallet/utils/responsive_layout_util.dart';
10 import 'package:cw_core/currency.dart';
5 -import 'package:flutter/services.dart';
11 import 'package:flutter/material.dart';
7 -import 'package:cake_wallet/routes.dart';
8 -import 'package:cake_wallet/generated/i18n.dart';
9 -import 'package:cake_wallet/entities/qr_scanner.dart';
10 -import 'package:cake_wallet/entities/contact_base.dart';
11 -import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
12 -import 'package:cake_wallet/utils/permission_handler.dart';
12 +import 'package:flutter/services.dart';
13 import 'package:permission_handler/permission_handler.dart';
14
15 enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
16
17 -
18 -class AddressTextField<T extends Currency> extends StatelessWidget{
17 +class AddressTextField<T extends Currency> extends StatelessWidget {
18 AddressTextField({
19 required this.controller,
20 this.isActive = true,
@@ -234,9 +233,7 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
233 if (!isCameraPermissionGranted) return;
234 final code = await presentQRScanner(context);
235 if (code == null) return;
237 - if (code.isEmpty) {
238 - return;
239 - }
236 + if (code.isEmpty) return;
237
238 try {
239 final uri = Uri.parse(code);
@@ -259,7 +256,8 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
256 }
257
258 Future<void> _presetWalletAddressPicker(BuildContext context) async {
262 - final address = await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
259 + final address =
260 + await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
261
262 if (address is String) {
263 controller?.text = address;
@@ -272,7 +270,13 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
270 final address = clipboard?.text ?? '';
271
272 if (address.isNotEmpty) {
275 - controller?.text = address;
273 + try {
274 + final uri = Uri.parse(address);
275 + controller?.text = uri.path;
276 + onURIScanned?.call(uri);
277 + } catch (_) {
278 + controller?.text = address;
279 + }
280 }
281
282 onPushPasteButton?.call(context);
lib/src/widgets/standard_list_status_row.dart
+3 -3
@@ -1,5 +1,4 @@
1 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
2 -import 'package:cake_wallet/palette.dart';
2 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
3 import 'package:flutter/cupertino.dart';
4 import 'package:flutter/material.dart';
@@ -7,10 +6,11 @@ import 'package:cake_wallet/themes/extensions/address_theme.dart';
6 import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
7
8 class StandardListStatusRow extends StatelessWidget {
10 - StandardListStatusRow({required this.title, required this.value});
9 + StandardListStatusRow({required this.title, required this.value, this.status});
10
11 final String title;
12 final String value;
13 + final String? status; // waiting, action required, created, fetching, finished, success
14
15 @override
16 Widget build(BuildContext context) {
@@ -43,7 +43,7 @@ class StandardListStatusRow extends StatelessWidget {
43 children: <Widget>[
44 SyncIndicatorIcon(
45 boolMode: false,
46 - value: value,
46 + value: status ?? value,
47 size: 6,
48 ),
49 SizedBox(
lib/store/dashboard/payjoin_transactions_store.dart new
+47
@@ -0,0 +1,47 @@
1 +import 'dart:async';
2 +
3 +import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
4 +import 'package:cw_core/payjoin_session.dart';
5 +import 'package:flutter/foundation.dart';
6 +import 'package:hive/hive.dart';
7 +import 'package:mobx/mobx.dart';
8 +
9 +part 'payjoin_transactions_store.g.dart';
10 +
11 +class PayjoinTransactionsStore = PayjoinTransactionsStoreBase
12 + with _$PayjoinTransactionsStore;
13 +
14 +abstract class PayjoinTransactionsStoreBase with Store {
15 + PayjoinTransactionsStoreBase({
16 + required this.payjoinSessionSource,
17 + }) : transactions = <PayjoinTransactionListItem>[] {
18 + payjoinSessionSource.watch().listen((_) => updateTransactionList());
19 + updateTransactionList();
20 + }
21 +
22 + Box<PayjoinSession> payjoinSessionSource;
23 +
24 + @observable
25 + List<PayjoinTransactionListItem> transactions;
26 +
27 + @action
28 + Future<void> updateTransactionList() async {
29 + final updatedTransactions = <PayjoinTransactionListItem>[];
30 + payjoinSessionSource.toMap().forEach((dynamic key, PayjoinSession session) {
31 + if ([
32 + PayjoinSessionStatus.inProgress.name,
33 + PayjoinSessionStatus.success.name,
34 + PayjoinSessionStatus.unrecoverable.name
35 + ].contains(session.status) &&
36 + session.inProgressSince != null) {
37 + updatedTransactions.add(PayjoinTransactionListItem(
38 + sessionId: key as String,
39 + session: session,
40 + key: ValueKey('payjoin_transaction_list_item_${key}_key'),
41 + ));
42 + }
43 + });
44 +
45 + transactions = updatedTransactions;
46 + }
47 +}
lib/store/settings_store.dart
+11
@@ -121,6 +121,7 @@ abstract class SettingsStoreBase with Store {
121 required this.lookupsOpenAlias,
122 required this.lookupsENS,
123 required this.lookupsWellKnown,
124 + required this.usePayjoin,
125 required this.customBitcoinFeeRate,
126 required this.silentPaymentsCardDisplay,
127 required this.silentPaymentsAlwaysScan,
@@ -483,6 +484,11 @@ abstract class SettingsStoreBase with Store {
484 (bool looksUpWellKnown) =>
485 _sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
486
487 + reaction(
488 + (_) => usePayjoin,
489 + (bool usePayjoin) =>
490 + _sharedPreferences.setBool(PreferencesKey.usePayjoin, usePayjoin));
491 +
492 // secure storage keys:
493 reaction(
494 (_) => allowBiometricalAuthentication,
@@ -802,6 +808,9 @@ abstract class SettingsStoreBase with Store {
808 @observable
809 bool lookupsWellKnown;
810
811 + @observable
812 + bool usePayjoin;
813 +
814 @observable
815 SyncMode currentSyncMode;
816
@@ -1009,6 +1018,7 @@ abstract class SettingsStoreBase with Store {
1018 final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
1019 final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
1020 final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
1021 + final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
1022 final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
1023 final silentPaymentsCardDisplay =
1024 sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
@@ -1311,6 +1321,7 @@ abstract class SettingsStoreBase with Store {
1321 lookupsOpenAlias: lookupsOpenAlias,
1322 lookupsENS: lookupsENS,
1323 lookupsWellKnown: lookupsWellKnown,
1324 + usePayjoin: usePayjoin,
1325 customBitcoinFeeRate: customBitcoinFeeRate,
1326 silentPaymentsCardDisplay: silentPaymentsCardDisplay,
1327 silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,
lib/utils/payment_request.dart
+9 -2
@@ -1,8 +1,8 @@
1 -import 'package:cake_wallet/generated/i18n.dart';
1 import 'package:cake_wallet/nano/nano.dart';
2
3 class PaymentRequest {
5 - PaymentRequest(this.address, this.amount, this.note, this.scheme, {this.callbackUrl, this.callbackMessage});
4 + PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
5 + {this.callbackUrl, this.callbackMessage});
6
7 factory PaymentRequest.fromUri(Uri? uri) {
8 var address = "";
@@ -12,8 +12,13 @@ class PaymentRequest {
12 String? walletType;
13 String? callbackUrl;
14 String? callbackMessage;
15 + String? pjUri;
16
17 if (uri != null) {
18 + if (uri.queryParameters['pj'] != null) {
19 + pjUri = uri.toString();
20 + }
21 +
22 address = uri.queryParameters['address'] ?? uri.path;
23 amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? "";
24 note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? "";
@@ -42,6 +47,7 @@ class PaymentRequest {
47 amount,
48 note,
49 scheme,
50 + pjUri,
51 callbackUrl: callbackUrl,
52 callbackMessage: callbackMessage,
53 );
@@ -51,6 +57,7 @@ class PaymentRequest {
57 final String amount;
58 final String note;
59 final String scheme;
60 + final String? pjUri;
61 final String? callbackUrl;
62 final String? callbackMessage;
63 }
lib/view_model/dashboard/dashboard_view_model.dart
+35 -6
@@ -13,11 +13,11 @@ import 'package:cake_wallet/entities/service_status.dart';
13 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
14 import 'package:cake_wallet/generated/i18n.dart';
15 import 'package:cake_wallet/monero/monero.dart';
16 -import 'package:cake_wallet/wownero/wownero.dart' as wow;
16 import 'package:cake_wallet/nano/nano.dart';
17 import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
18 import 'package:cake_wallet/store/app_store.dart';
19 import 'package:cake_wallet/store/dashboard/orders_store.dart';
20 +import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
21 import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
22 import 'package:cake_wallet/store/dashboard/trades_store.dart';
23 import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
@@ -29,9 +29,12 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
29 import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
30 import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
31 import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
32 +import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
33 import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
34 import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
35 import 'package:cake_wallet/view_model/settings/sync_mode.dart';
36 +import 'package:cake_wallet/wallet_type_utils.dart';
37 +import 'package:cake_wallet/wownero/wownero.dart' as wow;
38 import 'package:cryptography/cryptography.dart';
39 import 'package:cw_core/balance.dart';
40 import 'package:cw_core/cake_hive.dart';
@@ -70,6 +73,7 @@ abstract class DashboardViewModelBase with Store {
73 required this.yatStore,
74 required this.ordersStore,
75 required this.anonpayTransactionsStore,
76 + required this.payjoinTransactionsStore,
77 required this.sharedPreferences,
78 required this.keyService})
79 : hasTradeAction = true,
@@ -408,9 +412,16 @@ abstract class DashboardViewModelBase with Store {
412 ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
413
414 @computed
411 - List<AnonpayTransactionListItem> get anonpayTransactons => anonpayTransactionsStore.transactions
412 - .where((item) => item.transaction.walletId == wallet.id)
413 - .toList();
415 + List<AnonpayTransactionListItem> get anonpayTransactions =>
416 + anonpayTransactionsStore.transactions
417 + .where((item) => item.transaction.walletId == wallet.id)
418 + .toList();
419 +
420 + @computed
421 + List<PayjoinTransactionListItem> get payjoinTransactions =>
422 + payjoinTransactionsStore.transactions
423 + .where((item) => item.session.walletId == wallet.id)
424 + .toList();
425
426 @computed
427 double get price => balanceViewModel.price;
@@ -423,11 +434,27 @@ abstract class DashboardViewModelBase with Store {
434 List<ActionListItem> get items {
435 final _items = <ActionListItem>[];
436
426 - _items.addAll(
427 - transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
437 + _items.addAll(transactionFilterStore
438 + .filtered(transactions: [...transactions, ...anonpayTransactions]));
439 _items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
440 _items.addAll(orders);
441
442 + if (payjoinTransactions.isNotEmpty) {
443 + final _payjoinTransactions = payjoinTransactions;
444 + _items.forEach((e) {
445 + if (e is TransactionListItem &&
446 + _payjoinTransactions
447 + .any((t) => t.session.txId == e.transaction.id)) {
448 + _payjoinTransactions
449 + .firstWhere((t) => t.session.txId == e.transaction.id)
450 + .transaction = e.transaction;
451 + }
452 + });
453 + _items.addAll(_payjoinTransactions);
454 + _items.removeWhere((e) => (e is TransactionListItem &&
455 + _payjoinTransactions.any((t) => t.session.txId == e.transaction.id)));
456 + }
457 +
458 return formattedItemsList(_items);
459 }
460
@@ -755,6 +782,8 @@ abstract class DashboardViewModelBase with Store {
782
783 TransactionFilterStore transactionFilterStore;
784
785 + PayjoinTransactionsStore payjoinTransactionsStore;
786 +
787 Map<String, List<FilterItem>> filterItems;
788
789 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
lib/view_model/dashboard/payjoin_transaction_list_item.dart new
+34
@@ -0,0 +1,34 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
3 +import 'package:cw_core/payjoin_session.dart';
4 +import 'package:cw_core/transaction_info.dart';
5 +
6 +class PayjoinTransactionListItem extends ActionListItem {
7 + PayjoinTransactionListItem({
8 + required this.sessionId,
9 + required this.session,
10 + required super.key,
11 + });
12 +
13 + final String sessionId;
14 + final PayjoinSession session;
15 + TransactionInfo? transaction;
16 +
17 + @override
18 + DateTime get date => session.inProgressSince!;
19 +
20 + String get status {
21 + switch (session.status) {
22 + case 'success':
23 + if (transaction?.isPending == true)
24 + return S.current.payjoin_request_awaiting_tx;
25 + return S.current.successful;
26 + case 'inProgress':
27 + return S.current.payjoin_request_in_progress;
28 + case 'unrecoverable':
29 + return S.current.error;
30 + default:
31 + return session.status;
32 + }
33 + }
34 +}
lib/view_model/payjoin_details_view_model.dart new
+122
@@ -0,0 +1,122 @@
1 +import 'dart:async';
2 +
3 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 +import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
6 +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
7 +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
8 +import 'package:cake_wallet/store/settings_store.dart';
9 +import 'package:cake_wallet/utils/date_formatter.dart';
10 +import 'package:cw_core/payjoin_session.dart';
11 +import 'package:cw_core/transaction_info.dart';
12 +import 'package:cw_core/utils/print_verbose.dart';
13 +import 'package:flutter/widgets.dart';
14 +import 'package:hive_flutter/hive_flutter.dart';
15 +import 'package:mobx/mobx.dart';
16 +
17 +part 'payjoin_details_view_model.g.dart';
18 +
19 +class PayjoinDetailsViewModel = PayjoinDetailsViewModelBase
20 + with _$PayjoinDetailsViewModel;
21 +
22 +abstract class PayjoinDetailsViewModelBase with Store {
23 + PayjoinDetailsViewModelBase(
24 + this.payjoinSessionId,
25 + this.transactionInfo, {
26 + required this.payjoinSessionSource,
27 + required this.settingsStore,
28 + }) : items = ObservableList<StandartListItem>(),
29 + payjoinSession = payjoinSessionSource.get(payjoinSessionId)! {
30 + listener = payjoinSessionSource.watch().listen((e) {
31 + if (e.key == payjoinSessionId) _updateItems();
32 + });
33 + _updateItems();
34 + }
35 +
36 + final Box<PayjoinSession> payjoinSessionSource;
37 + final SettingsStore settingsStore;
38 + final String payjoinSessionId;
39 + final TransactionInfo? transactionInfo;
40 +
41 + @observable
42 + late PayjoinSession payjoinSession;
43 +
44 + final ObservableList<StandartListItem> items;
45 +
46 + late final StreamSubscription<BoxEvent> listener;
47 +
48 + Timer? timer;
49 +
50 + @action
51 + void _updateItems() {
52 + final dateFormat = DateFormatter.withCurrentLocal();
53 + items.clear();
54 + items.addAll([
55 + DetailsListStatusItem(
56 + title: S.current.status,
57 + value: _getStatusString(),
58 + status: payjoinSession.status,
59 + ),
60 + TradeDetailsListCardItem(
61 + id: "${payjoinSession.isSenderSession ? S.current.outgoing : S.current.incoming} Payjoin",
62 + createdAt:
63 + dateFormat.format(payjoinSession.inProgressSince!).toString(),
64 + pair:
65 + '${bitcoin!.formatterBitcoinAmountToString(amount: payjoinSession.amount.toInt())} BTC',
66 + onTap: (_) {},
67 + ),
68 + if (payjoinSession.error?.isNotEmpty == true)
69 + StandartListItem(
70 + title: S.current.error,
71 + value: payjoinSession.error!,
72 + ),
73 + if (payjoinSession.txId?.isNotEmpty == true)
74 + StandartListItem(
75 + title: S.current.transaction_details_transaction_id,
76 + value: payjoinSession.txId!,
77 + key: ValueKey('standard_list_item_transaction_details_id_key'),
78 + )
79 + ]);
80 +
81 + if (transactionInfo != null) {
82 + items.addAll([
83 + StandartListItem(
84 + title: S.current.transaction_details_date,
85 + value: dateFormat.format(transactionInfo!.date),
86 + key: ValueKey('standard_list_item_transaction_details_date_key'),
87 + ),
88 + StandartListItem(
89 + title: S.current.confirmations,
90 + value: transactionInfo!.confirmations.toString(),
91 + key: ValueKey('standard_list_item_transaction_confirmations_key'),
92 + ),
93 + StandartListItem(
94 + title: S.current.transaction_details_height,
95 + value: '${transactionInfo!.height}',
96 + key: ValueKey('standard_list_item_transaction_details_height_key'),
97 + ),
98 + if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false)
99 + StandartListItem(
100 + title: S.current.transaction_details_fee,
101 + value: transactionInfo!.feeFormatted()!,
102 + key: ValueKey('standard_list_item_transaction_details_fee_key'),
103 + ),
104 + ]);
105 + }
106 + }
107 +
108 + String _getStatusString() {
109 + switch (payjoinSession.status) {
110 + case 'success':
111 + if (transactionInfo?.isPending == true)
112 + return S.current.payjoin_request_awaiting_tx;
113 + return S.current.successful;
114 + case 'inProgress':
115 + return S.current.payjoin_request_in_progress;
116 + case 'unrecoverable':
117 + return S.current.error;
118 + default:
119 + return payjoinSession.status;
120 + }
121 + }
122 +}
lib/view_model/send/send_view_model.dart
+7
@@ -619,6 +619,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
619 priority: priority!,
620 feeRate: feesViewModel.customBitcoinFeeRate,
621 coinTypeToSpendFrom: coinTypeToSpendFrom,
622 + payjoinUri: _settingsStore.usePayjoin ? payjoinUri : null,
623 );
624 case WalletType.litecoin:
625 return bitcoin!.createBitcoinTransactionCredentials(
@@ -855,4 +856,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
856
857 return false;
858 }
859 +
860 + @computed
861 + bool get usePayjoin => _settingsStore.usePayjoin;
862 +
863 + @observable
864 + String? payjoinUri;
865 }
lib/view_model/settings/privacy_settings_view_model.dart
+24 -14
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
3 import 'package:cake_wallet/entities/exchange_api_mode.dart';
4 import 'package:cake_wallet/ethereum/ethereum.dart';
@@ -32,20 +33,19 @@ abstract class PrivacySettingsViewModelBase with Store {
33 @action
34 void setAutoGenerateSubaddresses(bool value) {
35 _wallet.isEnabledAutoGenerateSubaddress = value;
35 - if (value) {
36 - _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled;
37 - } else {
38 - _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled;
39 - }
36 + _settingsStore.autoGenerateSubaddressStatus = value
37 + ? AutoGenerateSubaddressStatus.enabled
38 + : AutoGenerateSubaddressStatus.disabled;
39 }
40
42 - bool get isAutoGenerateSubaddressesVisible =>
43 - _wallet.type == WalletType.monero ||
44 - _wallet.type == WalletType.wownero ||
45 - _wallet.type == WalletType.bitcoin ||
46 - _wallet.type == WalletType.litecoin ||
47 - _wallet.type == WalletType.bitcoinCash ||
48 - _wallet.type == WalletType.decred;
41 + bool get isAutoGenerateSubaddressesVisible => [
42 + WalletType.monero,
43 + WalletType.wownero,
44 + WalletType.bitcoin,
45 + WalletType.litecoin,
46 + WalletType.bitcoinCash,
47 + WalletType.decred
48 + ].contains(_wallet.type);
49
50 bool get isMoneroWallet => _wallet.type == WalletType.monero;
51
@@ -100,6 +100,9 @@ abstract class PrivacySettingsViewModelBase with Store {
100 @computed
101 bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
102
103 + @computed
104 + bool get usePayjoin => _settingsStore.usePayjoin;
105 +
106 bool get canUseEtherscan => _wallet.type == WalletType.ethereum;
107
108 bool get canUsePolygonScan => _wallet.type == WalletType.polygon;
@@ -108,6 +111,8 @@ abstract class PrivacySettingsViewModelBase with Store {
111
112 bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin;
113
114 + bool get canUsePayjoin => _wallet.type == WalletType.bitcoin;
115 +
116 @action
117 void setShouldSaveRecipientAddress(bool value) =>
118 _settingsStore.shouldSaveRecipientAddress = value;
@@ -170,7 +175,12 @@ abstract class PrivacySettingsViewModelBase with Store {
175 }
176
177 @action
173 - void setUseMempoolFeeAPI(bool value) {
174 - _settingsStore.useMempoolFeeAPI = value;
178 + void setUseMempoolFeeAPI(bool value) =>
179 + _settingsStore.useMempoolFeeAPI = value;
180 +
181 + @action
182 + void setUsePayjoin(bool value) {
183 + _settingsStore.usePayjoin = value;
184 + bitcoin!.updatePayjoinState(_wallet, value);
185 }
186 }
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+19 -6
@@ -1,4 +1,5 @@
1 import 'dart:developer' as dev;
2 +import 'dart:core';
3
4 import 'package:cake_wallet/bitcoin/bitcoin.dart';
5 import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -72,17 +73,21 @@ class HavenURI extends PaymentURI {
73 }
74
75 class BitcoinURI extends PaymentURI {
75 - BitcoinURI({required super.amount, required super.address});
76 + BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
77 +
78 + final String pjUri;
79
80 @override
81 String toString() {
79 - var base = 'bitcoin:$address';
82 + final qp = <String, String>{};
83
81 - if (amount.isNotEmpty) {
82 - base += '?amount=${amount.replaceAll(',', '.')}';
84 + if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
85 + if (pjUri.isNotEmpty) {
86 + qp['pjos'] = '0';
87 + qp['pj'] = pjUri;
88 }
89
85 - return base;
90 + return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
91 }
92 }
93
@@ -300,6 +305,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
305 WalletAddressListItem get address =>
306 WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false);
307
308 + @computed
309 + String get payjoinEndpoint => wallet.type == WalletType.bitcoin
310 + ? bitcoin!.getPayjoinEndpoint(wallet)
311 + : "";
312 +
313 @computed
314 PaymentURI get uri {
315 switch (wallet.type) {
@@ -308,7 +318,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
318 case WalletType.haven:
319 return HavenURI(amount: amount, address: address.address);
320 case WalletType.bitcoin:
311 - return BitcoinURI(amount: amount, address: address.address);
321 + return BitcoinURI(
322 + amount: amount,
323 + address: address.address,
324 + pjUri: payjoinEndpoint);
325 case WalletType.litecoin:
326 return LitecoinURI(amount: amount, address: address.address);
327 case WalletType.ethereum:
res/values/strings_ar.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "نسخ",
203 "copy_address": "نسخ العنوان",
204 "copy_id": "نسخ معرف العملية",
205 + "copy_payjoin_url": "نسخ Payjoin url",
206 "copyWalletConnectLink": "ﺎﻨﻫ ﻪﻘﺼﻟﺍﻭ dApp ﻦﻣ WalletConnect ﻂﺑﺍﺭ ﺦﺴﻧﺍ",
207 "corrupted_seed_notice": "تالف ملفات هذه المحفظة ولا يمكن فتحها. يرجى الاطلاع على عبارة البذور وحفظها واستعادة المحفظة.\n\nإذا كانت القيمة فارغة ، لم تتمكن البذور من استردادها بشكل صحيح.",
208 "countries": "بلدان",
@@ -546,6 +547,10 @@
547 "password": "كلمة المرور",
548 "paste": "لصق",
549 "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ",
550 + "payjoin_details": "Payjoin تفاصيل",
551 + "payjoin_enabled": "Payjoin تمكين",
552 + "payjoin_request_awaiting_tx": "في انتظار المعاملة",
553 + "payjoin_request_in_progress": "في تَقَدم",
554 "payment_id": "معرف الدفع:",
555 "payment_was_received": "تم استلام الدفع الخاص بك.",
556 "pending": " (في الإنتظار)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "أرسل من محفظة خارجية",
741 "send_name": "الأسم",
742 "send_new": "جديد",
743 + "send_payjoin": "يرسل Payjoin",
744 "send_payment_id": "معرف عملية الدفع (اختياري)",
745 "send_priority": "حاليًا ، تم تحديد الرسوم بأولوية ${transactionPriority}.\nيمكن تعديل أولوية المعاملة في الإعدادات",
746 "send_sending": "يتم الإرسال...",
@@ -973,6 +979,7 @@
979 "use": "التبديل إلى",
980 "use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.",
981 "use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.",
982 + "use_payjoin": "يستخدم Payjoin",
983 "use_ssl": "استخدم SSL",
984 "use_suggested": "استخدام المقترح",
985 "use_testnet": "استخدم testnet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "تحويل من",
1071 "youCanGoBackToYourDapp": "يمكنك العودة إلى DAPP الخاص بك الآن",
1072 "yy": "YY"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_bg.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Копиране",
203 "copy_address": "Copy Address",
204 "copy_id": "Копиране на ID",
205 + "copy_payjoin_url": "Копиране Payjoin url",
206 "copyWalletConnectLink": "Копирайте връзката WalletConnect от dApp и я поставете тук",
207 "corrupted_seed_notice": "Файловете за този портфейл са повредени и не могат да бъдат отворени. Моля, прегледайте фразата за семена, запазете я и възстановете портфейла.\n\nАко стойността е празна, тогава семето не успя да бъде правилно възстановено.",
208 "countries": "Държави",
@@ -546,6 +547,10 @@
547 "password": "Парола",
548 "paste": "Поставяне",
549 "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.",
550 + "payjoin_details": "Payjoin подробности",
551 + "payjoin_enabled": "Payjoin enabled",
552 + "payjoin_request_awaiting_tx": "В очакване на транзакция",
553 + "payjoin_request_in_progress": "В ход",
554 "payment_id": "Payment ID: ",
555 "payment_was_received": "Плащането бе получено.",
556 "pending": " (чакащи)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Изпратете от външен портфейл",
741 "send_name": "Име",
742 "send_new": "Ново",
743 + "send_payjoin": "Изпратете Payjoin",
744 "send_payment_id": "Payment ID (не е задължително)",
745 "send_priority": "В момента таксата е на ${transactionPriority} приоритетност.\nПриоритетността на транзакцията може да бъде променена в настройките",
746 "send_sending": "Изпращане...",
@@ -973,6 +979,7 @@
979 "use": "Смяна на ",
980 "use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.",
981 "use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.",
982 + "use_payjoin": "Използвайте Payjoin",
983 "use_ssl": "Използване на SSL",
984 "use_suggested": "Използване на предложеното",
985 "use_testnet": "Използвайте TestNet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "Обръщане от",
1071 "youCanGoBackToYourDapp": "Можете да се върнете при вашия Dapp сега",
1072 "yy": "гг"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_cs.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kopírovat",
203 "copy_address": "Zkopírovat adresu",
204 "copy_id": "Kopírovat ID",
205 + "copy_payjoin_url": "Kopírovat Payjoin URL",
206 "copyWalletConnectLink": "Zkopírujte odkaz WalletConnect z dApp a vložte jej sem",
207 "corrupted_seed_notice": "Soubory pro tuto peněženku jsou poškozeny a nemohou být otevřeny. Podívejte se prosím na osivo, uložte ji a obnovte peněženku.\n\nPokud je hodnota prázdná, pak semeno nebylo možné správně obnovit.",
208 "countries": "Země",
@@ -546,6 +547,10 @@
547 "password": "Heslo",
548 "paste": "Vložit",
549 "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.",
550 + "payjoin_details": "%%TE podrobnosti",
551 + "payjoin_enabled": "Payjoin povoleno",
552 + "payjoin_request_awaiting_tx": "Čeká na transakci",
553 + "payjoin_request_in_progress": "Probíhá",
554 "payment_id": "ID platby: ",
555 "payment_was_received": "Vaše platba byla přijata.",
556 "pending": " (čeká)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Odeslat z externí peněženky",
741 "send_name": "Název",
742 "send_new": "Nová",
743 + "send_payjoin": "Odeslat Payjoin",
744 "send_payment_id": "ID platby (nepovinné)",
745 "send_priority": "Momentálně je poplatek nastaven na prioritu: ${transactionPriority}.\nPriorita transakce může být upravena v nastavení.",
746 "send_sending": "Odesílání...",
@@ -973,6 +979,7 @@
979 "use": "Přepnout na ",
980 "use_card_info_three": "Použijte tuto digitální kartu online nebo bezkontaktními platebními metodami.",
981 "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.",
982 + "use_payjoin": "Použijte %%TE",
983 "use_ssl": "Použít SSL",
984 "use_suggested": "Použít doporučený",
985 "use_testnet": "Použijte testNet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "Směnit z",
1071 "youCanGoBackToYourDapp": "Nyní se můžete vrátit do svého dappu",
1072 "yy": "YY"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_de.arb
+7
@@ -202,6 +202,7 @@
202 "copy": "Kopieren",
203 "copy_address": "Adresse kopieren",
204 "copy_id": "ID kopieren",
205 + "copy_payjoin_url": "Payjoin URL kopieren",
206 "copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein",
207 "corrupted_seed_notice": "Die Dateien für diese Wallet sind beschädigt und können nicht geöffnet werden. Bitte sehen Sie sich die Seeds an, speichern Sie sie und stellen Sie die Wallet wieder her.\n\nWenn der Wert leer ist, konnte der Seed nicht korrekt wiederhergestellt werden.",
208 "countries": "Länder",
@@ -546,6 +547,10 @@
547 "password": "Passwort",
548 "paste": "Einfügen",
549 "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.",
550 + "payjoin_details": "Payjoin Details",
551 + "payjoin_enabled": "Payjoin aktiv",
552 + "payjoin_request_awaiting_tx": "Warten auf die Transaktion",
553 + "payjoin_request_in_progress": "Im Gange",
554 "payment_id": "Zahlungs-ID: ",
555 "payment_was_received": "Ihre Zahlung ist eingegangen.",
556 "pending": " (ausstehend)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Senden Sie aus der Außenschreibe",
742 "send_name": "Name",
743 "send_new": "Neu",
744 + "send_payjoin": "Payjoin senden",
745 "send_payment_id": "Zahlungs-ID (optional)",
746 "send_priority": "Derzeit ist ${transactionPriority} als Gebührenpriorität eingestellt.\nDie Transaktionspriorität kann in den Einstellungen angepasst werden",
747 "send_sending": "Senden...",
@@ -975,6 +981,7 @@
981 "use": "Wechsel zu ",
982 "use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.",
983 "use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.",
984 + "use_payjoin": "Benutze Payjoin",
985 "use_ssl": "SSL verwenden",
986 "use_suggested": "Vorgeschlagen verwenden",
987 "use_testnet": "TESTNET verwenden",
res/values/strings_en.arb
+7
@@ -202,6 +202,7 @@
202 "copy": "Copy",
203 "copy_address": "Copy Address",
204 "copy_id": "Copy ID",
205 + "copy_payjoin_url": "Copy Payjoin URL",
206 "copyWalletConnectLink": "Copy the WalletConnect link from dApp and paste here",
207 "corrupted_seed_notice": "The files for this wallet are corrupted and are unable to be opened. Please view the seed phrase, save it, and restore the wallet.\n\nIf the value is empty, then the seed was unable to be correctly recovered.",
208 "countries": "Countries",
@@ -547,6 +548,10 @@
548 "password": "Password",
549 "paste": "Paste",
550 "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.",
551 + "payjoin_details": "Payjoin details",
552 + "payjoin_enabled": "Payjoin enabled",
553 + "payjoin_request_awaiting_tx": "Awaiting Transaction",
554 + "payjoin_request_in_progress": "In Progress",
555 "payment_id": "Payment ID: ",
556 "payment_was_received": "Your payment was received.",
557 "pending": " (pending)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Send from External Wallet",
742 "send_name": "Name",
743 "send_new": "New",
744 + "send_payjoin": "Send Payjoin",
745 "send_payment_id": "Payment ID (optional)",
746 "send_priority": "Currently the fee is set at ${transactionPriority} priority.\nTransaction priority can be adjusted in the settings",
747 "send_sending": "Sending...",
@@ -974,6 +980,7 @@
980 "use": "Switch to ",
981 "use_card_info_three": "Use the digital card online or with contactless payment methods.",
982 "use_card_info_two": "Funds are converted to USD when they're held in the prepaid account, not in digital currencies.",
983 + "use_payjoin": "Use Payjoin",
984 "use_ssl": "Use SSL",
985 "use_suggested": "Use Suggested",
986 "use_testnet": "Use Testnet",
res/values/strings_es.arb
+7
@@ -202,6 +202,7 @@
202 "copy": "Dupdo",
203 "copy_address": "Copiar dirección ",
204 "copy_id": "Copiar ID",
205 + "copy_payjoin_url": "Copiar Payjoin url",
206 "copyWalletConnectLink": "Copie el enlace de WalletConnect de dApp y péguelo aquí",
207 "corrupted_seed_notice": "Los archivos para esta billetera están dañados y no pueden abrirse. Vea la frase de semillas, guárdela y restaura la billetera.\n\nSi el valor está vacío, entonces la semilla no pudo recuperarse correctamente.",
208 "countries": "Países",
@@ -546,6 +547,10 @@
547 "password": "Contraseña",
548 "paste": "Pegar",
549 "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.",
550 + "payjoin_details": "Payjoin detalles",
551 + "payjoin_enabled": "Payjoin activado",
552 + "payjoin_request_awaiting_tx": "Esperando transacción",
553 + "payjoin_request_in_progress": "En curso",
554 "payment_id": "ID de pago: ",
555 "payment_was_received": "Su pago fue recibido.",
556 "pending": " (pendiente)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Enviar desde la billetera externa",
742 "send_name": "Nombre",
743 "send_new": "Nuevo",
744 + "send_payjoin": "Enviar Payjoin",
745 "send_payment_id": "ID de pago (opcional)",
746 "send_priority": "Actualmente la tarifa se establece en ${transactionPriority} prioridad.\nLa prioridad de la transacción se puede ajustar en la configuración",
747 "send_sending": "Enviando...",
@@ -974,6 +980,7 @@
980 "use": "Utilizar a ",
981 "use_card_info_three": "Utiliza la tarjeta digital en línea o con métodos de pago sin contacto.",
982 "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.",
983 + "use_payjoin": "Usar Payjoin",
984 "use_ssl": "Utiliza SSL",
985 "use_suggested": "Usar sugerido",
986 "use_testnet": "Usar TestNet",
res/values/strings_fr.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Copier",
203 "copy_address": "Copier l'Adresse",
204 "copy_id": "Copier l'ID",
205 + "copy_payjoin_url": "Copie Payjoin URL",
206 "copyWalletConnectLink": "Copiez le lien WalletConnect depuis l'application décentralisée (dApp) et collez-le ici",
207 "corrupted_seed_notice": "Les fichiers de ce portefeuille sont corrompus et ne peuvent pas être ouverts. Veuillez consulter la phrase de graines, sauver et restaurer le portefeuille.\n\nSi la valeur est vide, la graine n'a pas pu être correctement récupérée.",
208 "countries": "Pays",
@@ -546,6 +547,10 @@
547 "password": "Mot de passe",
548 "paste": "Coller",
549 "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.",
550 + "payjoin_details": "Payjoin détails",
551 + "payjoin_enabled": "Payjoin activé",
552 + "payjoin_request_awaiting_tx": "En attente de transaction",
553 + "payjoin_request_in_progress": "En cours",
554 "payment_id": "ID de Paiement : ",
555 "payment_was_received": "Votre paiement a été reçu.",
556 "pending": " (en attente)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Envoyer du portefeuille externe",
741 "send_name": "Nom",
742 "send_new": "Nouveau",
743 + "send_payjoin": "Envoyer Payjoin",
744 "send_payment_id": "ID de paiement (optionnel)",
745 "send_priority": "Actuellement les frais sont positionnés à la priorité ${transactionPriority}.\nLa priorité de la transaction peut être modifiée dans les réglages",
746 "send_sending": "Envoi...",
@@ -973,6 +979,7 @@
979 "use": "Changer vers code PIN à ",
980 "use_card_info_three": "Utilisez la carte numérique en ligne ou avec des méthodes de paiement sans contact.",
981 "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.",
982 + "use_payjoin": "Utiliser Payjoin",
983 "use_ssl": "Utiliser SSL",
984 "use_suggested": "Suivre la suggestion",
985 "use_testnet": "Utiliser TestNet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "Convertir depuis",
1071 "youCanGoBackToYourDapp": "Vous pouvez retourner à votre Dapp maintenant",
1072 "yy": "AA"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_ha.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kwafi",
203 "copy_address": "Kwafi Adireshin",
204 "copy_id": "Kwafi ID",
205 + "copy_payjoin_url": "Kwafa Payjoin url",
206 "copyWalletConnectLink": "Kwafi hanyar haɗin WalletConnect daga dApp kuma liƙa a nan",
207 "corrupted_seed_notice": "Fayilolin don wannan walat ɗin sun lalata kuma ba za a iya buɗe su ba. Da fatan za a duba kalmar iri, adana shi, da dawo da walat.\n\nIdan darajar ta kasance fanko, to sai zuriyar da ba ta iya murmurewa daidai ba.",
208 "countries": "Kasashe",
@@ -548,6 +549,10 @@
549 "password": "Kalmar wucewa",
550 "paste": "Manna",
551 "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.",
552 + "payjoin_details": "Payjoin LIT LOCEC LOcciya",
553 + "payjoin_enabled": "Payjoin An kunna",
554 + "payjoin_request_awaiting_tx": "Jiran ma'amala",
555 + "payjoin_request_in_progress": "Ana kai",
556 "payment_id": "ID na biyan kuɗi:",
557 "payment_was_received": "An karɓi kuɗin ku.",
558 "pending": "(pending)",
@@ -737,6 +742,7 @@
742 "send_from_external_wallet": "Aika daga walat na waje",
743 "send_name": "Sunan",
744 "send_new": "Sabon",
745 + "send_payjoin": "Aika Payjoin",
746 "send_payment_id": "ID na biyan kuɗi (optional)",
747 "send_priority": "Yanzu haka fee yana set a ${transactionPriority} fifiko.\nAna iya daidaita fifikon ciniki a cikin saitunan",
748 "send_sending": "Aika...",
@@ -975,6 +981,7 @@
981 "use": "Canja zuwa",
982 "use_card_info_three": "Yi amfani da katin dijital akan layi ko tare da hanyoyin biyan kuɗi mara lamba.",
983 "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.",
984 + "use_payjoin": "Yi amfani da Payjoin",
985 "use_ssl": "Yi amfani da SSL",
986 "use_suggested": "Amfani da Shawarwari",
987 "use_testnet": "Amfani da gwaji",
@@ -1065,4 +1072,4 @@
1072 "you_will_send": "Maida daga",
1073 "youCanGoBackToYourDapp": "Kuna iya komawa zuwa DPP ɗinku yanzu",
1074 "yy": "YY"
1068 -}
\ No newline at end of file
1075 +}
res/values/strings_hi.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "प्रतिलिपि",
203 "copy_address": "पता कॉपी करें",
204 "copy_id": "प्रतिलिपि ID",
205 + "copy_payjoin_url": "कॉपी Payjoin url",
206 "copyWalletConnectLink": "dApp से वॉलेटकनेक्ट लिंक को कॉपी करें और यहां पेस्ट करें",
207 "corrupted_seed_notice": "इस वॉलेट की फाइलें दूषित हैं और उन्हें खोलने में असमर्थ हैं। कृपया बीज वाक्यांश देखें, इसे बचाएं, और बटुए को पुनर्स्थापित करें।\n\nयदि मूल्य खाली है, तो बीज सही ढंग से पुनर्प्राप्त करने में असमर्थ था।",
208 "countries": "देशों",
@@ -546,6 +547,10 @@
547 "password": "पारण शब्द",
548 "paste": "पेस्ट करें",
549 "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।",
550 + "payjoin_details": "Payjoin विवरण",
551 + "payjoin_enabled": "Payjoin सक्षम",
552 + "payjoin_request_awaiting_tx": "लेन -देन का इंतजार",
553 + "payjoin_request_in_progress": "प्रगति पर है",
554 "payment_id": "भुगतान ID: ",
555 "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।",
556 "payment_was_received": "आपका भुगतान प्राप्त हुआ था।",
@@ -737,6 +742,7 @@
742 "send_from_external_wallet": "बाहरी बटुए से भेजें",
743 "send_name": "नाम",
744 "send_new": "नया",
745 + "send_payjoin": "भेजना Payjoin",
746 "send_payment_id": "भुगतान ID (ऐच्छिक)",
747 "send_priority": "वर्तमान में शुल्क निर्धारित है ${transactionPriority} प्राथमिकता.\nलेन-देन की प्राथमिकता को सेटिंग्स में समायोजित किया जा सकता है",
748 "send_sending": "भेजना...",
@@ -975,6 +981,7 @@
981 "use": "उपयोग ",
982 "use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।",
983 "use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।",
984 + "use_payjoin": "उपयोग Payjoin",
985 "use_ssl": "उपयोग SSL",
986 "use_suggested": "सुझाए गए का प्रयोग करें",
987 "use_testnet": "टेस्टनेट का उपयोग करें",
@@ -1065,4 +1072,4 @@
1072 "you_will_send": "से रूपांतरित करें",
1073 "youCanGoBackToYourDapp": "अब आप अपने DAPP पर वापस जा सकते हैं",
1074 "yy": "वाईवाई"
1068 -}
\ No newline at end of file
1075 +}
res/values/strings_hr.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kopiraj",
203 "copy_address": "Kopiraj adresu",
204 "copy_id": "Kopirati ID",
205 + "copy_payjoin_url": "Kopirajte Payjoin url",
206 "copyWalletConnectLink": "Kopirajte vezu WalletConnect iz dApp-a i zalijepite je ovdje",
207 "corrupted_seed_notice": "Datoteke za ovaj novčanik su oštećene i nisu u mogućnosti otvoriti. Molimo pogledajte sjemensku frazu, spremite je i vratite novčanik.\n\nAko je vrijednost prazna, tada sjeme nije bilo u stanju ispravno oporaviti.",
208 "countries": "Zemalja",
@@ -546,6 +547,10 @@
547 "password": "Lozinka",
548 "paste": "Zalijepi",
549 "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.",
550 + "payjoin_details": "Payjoin Pojedinosti",
551 + "payjoin_enabled": "Payjoin Omogućeno",
552 + "payjoin_request_awaiting_tx": "Čekajući transakciju",
553 + "payjoin_request_in_progress": "U toku",
554 "payment_id": "ID plaćanja: ",
555 "payment_was_received": "Vaša uplata je primljena.",
556 "pending": " (u tijeku)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Pošaljite iz vanjskog novčanika",
741 "send_name": "Ime",
742 "send_new": "Novi",
743 + "send_payjoin": "Pošaljite Payjoin",
744 "send_payment_id": "ID plaćanja (nije obvezno)",
745 "send_priority": "Trenutno se naknada nalazi na ${transactionPriority} mjestu prioriteta.\nPrioritet transakcije moguće je prilagoditi u postavkama",
746 "send_sending": "Slanje...",
@@ -973,6 +979,7 @@
979 "use": "Prebaci na",
980 "use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.",
981 "use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.",
982 + "use_payjoin": "Koristite Payjoin",
983 "use_ssl": "Koristi SSL",
984 "use_suggested": "Koristite predloženo",
985 "use_testnet": "Koristite TestNet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "Razmijeni iz",
1071 "youCanGoBackToYourDapp": "Sada se možete vratiti na svoj dapp",
1072 "yy": "GG"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_hy.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Պատճենել",
203 "copy_address": "Պատճենել հասցեն",
204 "copy_id": "Պատճենել ID",
205 + "copy_payjoin_url": "Պատճենել Payjoin url",
206 "copyWalletConnectLink": "Պատճենել WalletConnect հղումը dApp-ից և տեղադրել այստեղ",
207 "corrupted_seed_notice": "Այս դրամապանակի համար ֆայլերը կոռումպացված են եւ չեն կարողանում բացվել: Խնդրում ենք դիտել սերմերի արտահայտությունը, պահպանել այն եւ վերականգնել դրամապանակը:\n\nԵթե ​​արժեքը դատարկ է, ապա սերմը չկարողացավ ճիշտ վերականգնվել:",
208 "countries": "Երկրներ",
@@ -545,6 +546,10 @@
546 "password": "Գաղտնաբառ",
547 "paste": "Տեղադրել",
548 "pause_wallet_creation": "Հնարավորություն ստեղծել Haven Դրամապանակ ընթացիկ դադարեցված է",
549 + "payjoin_details": "Payjoin Մանրամասն",
550 + "payjoin_enabled": "Payjoin միացված",
551 + "payjoin_request_awaiting_tx": "Սպասում է գործարքին",
552 + "payjoin_request_in_progress": "Ընթացքի մեջ",
553 "payment_id": "Վճարման հերթական համար",
554 "payment_was_received": "Վճարումը ստացված է",
555 "pending": " (մշակվում է)",
@@ -733,6 +738,7 @@
738 "send_from_external_wallet": "Ուղարկել արտաքին դրամապանակից",
739 "send_name": "Անվանում",
740 "send_new": "Նոր",
741 + "send_payjoin": "Ուղարկել Payjoin",
742 "send_payment_id": "Վճարման ID (կամավոր)",
743 "send_priority": "Ներկայումս վարձը սահմանված է ${transactionPriority} առաջնահերթությամբ։ Գործարքի առաջնահերթությունը կարող է կարգավորվել կարգավորումներում",
744 "send_sending": "Ուղարկվում է...",
@@ -971,6 +977,7 @@
977 "use": "Փոխեք ",
978 "use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։",
979 "use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։",
980 + "use_payjoin": "Օգտագործեք Payjoin",
981 "use_ssl": "Օգտագործել SSL",
982 "use_suggested": "Օգտագործել առաջարկվածը",
983 "use_testnet": "Օգտագործել Testnet",
@@ -1061,4 +1068,4 @@
1068 "you_will_send": "Փոխանակեք",
1069 "youCanGoBackToYourDapp": "Այժմ կարող եք վերադառնալ ձեր DAPP- ին",
1070 "yy": "ՏՏ"
1064 -}
\ No newline at end of file
1071 +}
res/values/strings_id.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Salin",
203 "copy_address": "Salin Alamat",
204 "copy_id": "Salin ID",
205 + "copy_payjoin_url": "Salin Payjoin url",
206 "copyWalletConnectLink": "Salin tautan WalletConnect dari dApp dan tempel di sini",
207 "corrupted_seed_notice": "File untuk dompet ini rusak dan tidak dapat dibuka. Silakan lihat frasa benih, simpan, dan kembalikan dompet.\n\nJika nilainya kosong, maka benih tidak dapat dipulihkan dengan benar.",
208 "countries": "Negara",
@@ -548,6 +549,10 @@
549 "password": "Kata Sandi",
550 "paste": "Tempel",
551 "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.",
552 + "payjoin_details": "Payjoin detail",
553 + "payjoin_enabled": "Payjoin diaktifkan",
554 + "payjoin_request_awaiting_tx": "Menunggu transaksi",
555 + "payjoin_request_in_progress": "Sedang berlangsung",
556 "payment_id": "ID Pembayaran: ",
557 "payment_was_received": "Pembayaran Anda telah diterima.",
558 "pending": " (pending)",
@@ -738,6 +743,7 @@
743 "send_from_external_wallet": "Kirim dari dompet eksternal",
744 "send_name": "Nama",
745 "send_new": "Baru",
746 + "send_payjoin": "Mengirim Payjoin",
747 "send_payment_id": "ID Pembayaran (opsional)",
748 "send_priority": "Saat ini biaya diatur dengan prioritas ${transactionPriority}.\nPrioritas transaksi dapat diubah pada pengaturan",
749 "send_sending": "Mengirim...",
@@ -976,6 +982,7 @@
982 "use": "Beralih ke ",
983 "use_card_info_three": "Gunakan kartu digital secara online atau dengan metode pembayaran tanpa kontak.",
984 "use_card_info_two": "Dana dikonversi ke USD ketika disimpan dalam akun pra-bayar, bukan dalam mata uang digital.",
985 + "use_payjoin": "Menggunakan Payjoin",
986 "use_ssl": "Gunakan SSL",
987 "use_suggested": "Gunakan yang Disarankan",
988 "use_testnet": "Gunakan TestNet",
@@ -1066,4 +1073,4 @@
1073 "you_will_send": "Konversi dari",
1074 "youCanGoBackToYourDapp": "Anda dapat kembali ke dapp Anda sekarang",
1075 "yy": "YY"
1069 -}
\ No newline at end of file
1076 +}
res/values/strings_it.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Copia",
203 "copy_address": "Copia Indirizzo",
204 "copy_id": "Copia ID",
205 + "copy_payjoin_url": "Copia Payjoin url",
206 "copyWalletConnectLink": "Copia il collegamento WalletConnect dalla dApp e incollalo qui",
207 "corrupted_seed_notice": "I file per questo portafoglio sono corrotti e non è possibile aprirli. Visualizza la frase del seme, salvala e ripristina il portafoglio.\n\nSe il valore è vuoto, non è stato possibile recuperare correttamente il seme.",
208 "countries": "Paesi",
@@ -547,6 +548,10 @@
548 "password": "Password",
549 "paste": "Incolla",
550 "pause_wallet_creation": "La possibilità di creare Wallet Haven è attualmente sospesa.",
551 + "payjoin_details": "Payjoin dettagli",
552 + "payjoin_enabled": "Payjoin abilitato",
553 + "payjoin_request_awaiting_tx": "In attesa di transazione",
554 + "payjoin_request_in_progress": "In corso",
555 "payment_id": "ID Pagamento: ",
556 "payment_was_received": "Il tuo pagamento è stato ricevuto.",
557 "pending": " (non confermati)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Invia dal portafoglio esterno",
742 "send_name": "Nome",
743 "send_new": "Nuovo",
744 + "send_payjoin": "Inviare Payjoin",
745 "send_payment_id": "ID Pagamento (opzionale)",
746 "send_priority": "Attualmente la commissione è impostata a priorità ${transactionPriority} .\nLa priorità della transazione può essere modificata nelle impostazioni",
747 "send_sending": "Invio...",
@@ -974,6 +980,7 @@
980 "use": "Passa a ",
981 "use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.",
982 "use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.",
983 + "use_payjoin": "Utilizzo Payjoin",
984 "use_ssl": "Usa SSL",
985 "use_suggested": "Usa suggerito",
986 "use_testnet": "Usa TestNet",
@@ -1065,4 +1072,4 @@
1072 "you_will_send": "Conveti da",
1073 "youCanGoBackToYourDapp": "Puoi tornare al tuo DApp ora",
1074 "yy": "YY"
1068 -}
\ No newline at end of file
1075 +}
res/values/strings_ja.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "コピー",
203 "copy_address": "住所をコピー",
204 "copy_id": "IDをコピー",
205 + "copy_payjoin_url": "Payjoin urlをコピーします",
206 "copyWalletConnectLink": "dApp から WalletConnect リンクをコピーし、ここに貼り付けます",
207 "corrupted_seed_notice": "このウォレットのファイルは破損しており、開くことができません。シードフレーズを表示し、保存し、財布を復元してください。\n\n値が空の場合、種子を正しく回復することができませんでした。",
208 "countries": "国",
@@ -547,6 +548,10 @@
548 "password": "パスワード",
549 "paste": "ペースト",
550 "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。",
551 + "payjoin_details": "Payjoin 詳細",
552 + "payjoin_enabled": "Payjoin enabled",
553 + "payjoin_request_awaiting_tx": "トランザクションを待っています",
554 + "payjoin_request_in_progress": "進行中",
555 "payment_id": "支払いID: ",
556 "payment_was_received": "お支払いを受け取りました。",
557 "pending": " (保留中)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "外部ウォレットから送信します",
742 "send_name": "名前",
743 "send_new": "新着",
744 + "send_payjoin": "送信 Payjoin",
745 "send_payment_id": "支払いID (オプショナル)",
746 "send_priority": "現在、料金は ${transactionPriority} 優先度.\nトランザクションの優先度は設定で調整できます",
747 "send_sending": "送信...",
@@ -974,6 +980,7 @@
980 "use": "使用する ",
981 "use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。",
982 "use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。",
983 + "use_payjoin": "使用 Payjoin",
984 "use_ssl": "SSLを使用する",
985 "use_suggested": "推奨を使用",
986 "use_testnet": "テストネットを使用します",
@@ -1064,4 +1071,4 @@
1071 "you_will_send": "から変換",
1072 "youCanGoBackToYourDapp": "あなたは今あなたのダップに戻ることができます",
1073 "yy": "YY"
1067 -}
\ No newline at end of file
1074 +}
res/values/strings_ko.arb
+7
@@ -202,6 +202,7 @@
202 "copy": "복사",
203 "copy_address": "주소 복사",
204 "copy_id": "ID 복사",
205 + "copy_payjoin_url": "Payjoin url을 복사하십시오",
206 "copyWalletConnectLink": "dApp에서 WalletConnect 링크를 복사하여 여기에 붙여넣으세요",
207 "corrupted_seed_notice": "이 지갑의 파일이 손상되어 열 수 없습니다. 시드 구문을 보고 저장한 다음 지갑을 복구하세요.\n\n값이 비어 있으면 시드를 올바르게 복구할 수 없었습니다.",
208 "countries": "국가",
@@ -547,6 +548,10 @@
548 "password": "비밀번호",
549 "paste": "붙여넣기",
550 "pause_wallet_creation": "현재 Haven 지갑 생성 기능이 일시 중지되었습니다.",
551 + "payjoin_details": "Payjoin 세부 정보",
552 + "payjoin_enabled": "Payjoin enabled",
553 + "payjoin_request_awaiting_tx": "거래를 기다리고 있습니다",
554 + "payjoin_request_in_progress": "진행 중",
555 "payment_id": "결제 ID: ",
556 "payment_was_received": "결제가 접수되었습니다.",
557 "pending": " (대기 중)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "외부 지갑에서 보내기",
742 "send_name": "이름",
743 "send_new": "새로 만들기",
744 + "send_payjoin": "보내다 Payjoin",
745 "send_payment_id": "결제 ID (선택 사항)",
746 "send_priority": "현재 수수료는 ${transactionPriority} 우선순위로 설정되어 있습니다.\n트랜잭션 우선순위는 설정에서 조정할 수 있습니다.",
747 "send_sending": "보내는 중...",
@@ -974,6 +980,7 @@
980 "use": "다음으로 전환 ",
981 "use_card_info_three": "디지털 카드를 온라인 또는 비접촉 결제 방법으로 사용하세요.",
982 "use_card_info_two": "자금은 디지털 통화가 아닌 선불 계정에 보관될 때 USD로 변환됩니다.",
983 + "use_payjoin": "사용 Payjoin",
984 "use_ssl": "SSL 사용",
985 "use_suggested": "제안 사용",
986 "use_testnet": "테스트넷 사용",
res/values/strings_my.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "ကော်ပီ",
203 "copy_address": "လိပ်စာကို ကူးယူပါ။",
204 "copy_id": "ID ကူးယူပါ။",
205 + "copy_payjoin_url": "Payjoin URL ကိုကူးယူပါ",
206 "copyWalletConnectLink": "dApp မှ WalletConnect လင့်ခ်ကို ကူးယူပြီး ဤနေရာတွင် ကူးထည့်ပါ။",
207 "corrupted_seed_notice": "ဤပိုက်ဆံအိတ်အတွက်ဖိုင်များသည်အကျင့်ပျက်ခြစားမှုများနှင့်မဖွင့်နိုင်ပါ။ ကျေးဇူးပြု. မျိုးစေ့များကိုကြည့်ပါ, ၎င်းကိုသိမ်းဆည်းပါ, ပိုက်ဆံအိတ်ကိုပြန်ယူပါ။\n\nအကယ်. တန်ဖိုးသည်အချည်းနှီးဖြစ်ပါကမျိုးစေ့ကိုမှန်ကန်စွာပြန်လည်ကောင်းမွန်မရရှိနိုင်ပါ။",
208 "countries": "နိုင်ငံများ",
@@ -546,6 +547,10 @@
547 "password": "စကားဝှက်",
548 "paste": "ငါးပိ",
549 "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။",
550 + "payjoin_details": "Payjoin အသေးစိတ်အချက်အလက်များ %% အသေးစိတ်အချက်အလက်များ",
551 + "payjoin_enabled": "Payjoin enabled",
552 + "payjoin_request_awaiting_tx": "ငွေပေးငွေယူစောင့်ဆိုင်း",
553 + "payjoin_request_in_progress": "ဆောင်ရွက်ဆဲဖြစ်သည်",
554 "payment_id": "ငွေပေးချေမှု ID:",
555 "payment_was_received": "သင့်ငွေပေးချေမှုကို လက်ခံရရှိခဲ့သည်။",
556 "pending": " (ဆိုင်းငံ့)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "ပြင်ပပိုက်ဆံအိတ်မှပေးပို့ပါ",
741 "send_name": "နာမည်",
742 "send_new": "အသစ်",
743 + "send_payjoin": "Payjoin ကိုပို့ပါ",
744 "send_payment_id": "ငွေပေးချေမှု ID (ချန်လှပ်ထား)",
745 "send_priority": "လောလောဆယ်အခကြေးငွေကို ${transactionPriority} ဦးစားပေးတွင် သတ်မှတ်ထားပါသည်။\nငွေပေးငွေယူဦးစားပေးကို ဆက်တင်များတွင် ချိန်ညှိနိုင်ပါသည်။",
746 "send_sending": "ပို့နေသည်...",
@@ -973,6 +979,7 @@
979 "use": "သို့ပြောင်းပါ။",
980 "use_card_info_three": "ဒစ်ဂျစ်တယ်ကတ်ကို အွန်လိုင်း သို့မဟုတ် ထိတွေ့မှုမဲ့ ငွေပေးချေမှုနည်းလမ်းများဖြင့် အသုံးပြုပါ။",
981 "use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။",
982 + "use_payjoin": "Payjoin Chair ကိုအသုံးပြုပါ",
983 "use_ssl": "SSL ကိုသုံးပါ။",
984 "use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။",
985 "use_testnet": "testnet ကိုသုံးပါ",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "မှပြောင်းပါ။",
1071 "youCanGoBackToYourDapp": "သငျသညျယခုသင်၏ dapp ကိုပြန်သွားနိုင်ပါတယ်",
1072 "yy": "YY"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_nl.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kopiëren",
203 "copy_address": "Adres kopiëren",
204 "copy_id": "ID kopiëren",
205 + "copy_payjoin_url": "Kopieer Payjoin url",
206 "copyWalletConnectLink": "Kopieer de WalletConnect-link van dApp en plak deze hier",
207 "corrupted_seed_notice": "De bestanden voor deze portemonnee zijn beschadigd en kunnen niet worden geopend. Bekijk de zaadzin, bewaar deze en herstel de portemonnee.\n\nAls de waarde leeg is, kon het zaad niet correct worden hersteld.",
208 "countries": "Landen",
@@ -546,6 +547,10 @@
547 "password": "Wachtwoord",
548 "paste": "Plakken",
549 "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.",
550 + "payjoin_details": "Payjoin details",
551 + "payjoin_enabled": "Payjoin ingeschakeld",
552 + "payjoin_request_awaiting_tx": "In afwachting van transactie",
553 + "payjoin_request_in_progress": "In uitvoering",
554 "payment_id": "Betaling ID: ",
555 "payment_was_received": "Uw betaling is ontvangen.",
556 "pending": " (in afwachting)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Stuur vanuit een externe portemonnee",
741 "send_name": "Naam",
742 "send_new": "Nieuw",
743 + "send_payjoin": "Versturen Payjoin",
744 "send_payment_id": "Betaling ID (facultatief)",
745 "send_priority": "Momenteel is de vergoeding vastgesteld op ${transactionPriority} prioriteit.\nTransactieprioriteit kan worden aangepast in de instellingen",
746 "send_sending": "Bezig met verzenden...",
@@ -973,6 +979,7 @@
979 "use": "Gebruik ",
980 "use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.",
981 "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.",
982 + "use_payjoin": "Gebruik Payjoin",
983 "use_ssl": "Gebruik SSL",
984 "use_suggested": "Gebruik aanbevolen",
985 "use_testnet": "Gebruik testnet",
@@ -1064,4 +1071,4 @@
1071 "you_will_send": "Converteren van",
1072 "youCanGoBackToYourDapp": "U kunt nu terug naar uw DApp gaan",
1073 "yy": "JJ"
1067 -}
\ No newline at end of file
1074 +}
res/values/strings_pl.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kopiuj",
203 "copy_address": "Skopiuj adress",
204 "copy_id": "skopiuj ID",
205 + "copy_payjoin_url": "Skopiuj Payjoin url",
206 "copyWalletConnectLink": "Skopiuj link do WalletConnect z dApp i wklej tutaj",
207 "corrupted_seed_notice": "Pliki dla tego portfela są uszkodzone i nie można ich otworzyć. Zobacz frazę seed, zapisz je i przywróć portfel.\n\nJeśli wartość jest pusta, frazy seed nie można było poprawnie odzyskać.",
208 "countries": "Kraje",
@@ -546,6 +547,10 @@
547 "password": "Hasło",
548 "paste": "Wklej",
549 "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.",
550 + "payjoin_details": "Szczegóły Payjoin",
551 + "payjoin_enabled": "Payjoin włączony",
552 + "payjoin_request_awaiting_tx": "Oczekiwanie na transakcję",
553 + "payjoin_request_in_progress": "W toku",
554 "payment_id": "ID Płatności: ",
555 "payment_was_received": "Twoja płatność została otrzymana.",
556 "pending": " (w oczekiwaniu)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Wyślij z portfela zewnętrznego",
741 "send_name": "Imię",
742 "send_new": "Nowy",
743 + "send_payjoin": "Wyślij Payjoin",
744 "send_payment_id": "Identyfikator płatności (opcjonalny)",
745 "send_priority": "Obecnie opłata ustalona jest na ${transactionPriority} priorytet.\nPriorytet transakcji można zmienić w ustawieniach",
746 "send_sending": "Wysyłanie...",
@@ -973,6 +979,7 @@
979 "use": "Użyj ",
980 "use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.",
981 "use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.",
982 + "use_payjoin": "Używać Payjoin",
983 "use_ssl": "Użyj SSL",
984 "use_suggested": "Użyj sugerowane",
985 "use_testnet": "Użyj testne",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "Konwertuj z",
1071 "youCanGoBackToYourDapp": "Możesz teraz wrócić do swojego dapp",
1072 "yy": "RR"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_pt.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Copiar",
203 "copy_address": "Copiar endereço",
204 "copy_id": "Copiar ID",
205 + "copy_payjoin_url": "Copie Payjoin url",
206 "copyWalletConnectLink": "Copie o link WalletConnect do dApp e cole aqui",
207 "corrupted_seed_notice": "Os arquivos para esta carteira estão corrompidos e não podem ser abertos. Veja a frase das sementes, salve -a e restaure a carteira.\n\nSe o valor estiver vazio, a semente não pôde ser recuperada corretamente.",
208 "countries": "Países",
@@ -548,6 +549,10 @@
549 "password": "Senha",
550 "paste": "Colar",
551 "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.",
552 + "payjoin_details": "Payjoin detalhes",
553 + "payjoin_enabled": "Payjoin habilitado",
554 + "payjoin_request_awaiting_tx": "Aguardando transação",
555 + "payjoin_request_in_progress": "Em andamento",
556 "payment_id": "ID de pagamento: ",
557 "payment_was_received": "Seu pagamento foi recebido.",
558 "pending": " (pendente)",
@@ -737,6 +742,7 @@
742 "send_from_external_wallet": "Enviar da carteira externa",
743 "send_name": "Nome",
744 "send_new": "Novo",
745 + "send_payjoin": "Enviar Payjoin",
746 "send_payment_id": "ID de pagamento (opcional)",
747 "send_priority": "Atualmente, a taxa está definida para a prioridade: ${transactionPriority}.\nA prioridade da transação pode ser ajustada nas configurações",
748 "send_sending": "Enviando...",
@@ -975,6 +981,7 @@
981 "use": "Use PIN de ",
982 "use_card_info_three": "Use o cartão digital online ou com métodos de pagamento sem contato.",
983 "use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.",
984 + "use_payjoin": "Usar Payjoin",
985 "use_ssl": "Use SSL",
986 "use_suggested": "Uso sugerido",
987 "use_testnet": "Use testNet",
@@ -1066,4 +1073,4 @@
1073 "you_will_send": "Converter de",
1074 "youCanGoBackToYourDapp": "Você pode voltar para o seu dapp agora",
1075 "yy": "aa"
1069 -}
\ No newline at end of file
1076 +}
res/values/strings_ru.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Скопировать",
203 "copy_address": "Cкопировать адрес",
204 "copy_id": "Скопировать ID",
205 + "copy_payjoin_url": "Копировать Payjoin url",
206 "copyWalletConnectLink": "Скопируйте ссылку WalletConnect из dApp и вставьте сюда.",
207 "corrupted_seed_notice": "Файлы для этого кошелька повреждены и не могут быть открыты. Пожалуйста, просмотрите семенную фразу, сохраните ее и восстановите кошелек.\n\nЕсли значение пустое, то семя не смог правильно восстановить.",
208 "countries": "Страны",
@@ -547,6 +548,10 @@
548 "password": "Пароль",
549 "paste": "Вставить",
550 "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.",
551 + "payjoin_details": "Payjoin подробности",
552 + "payjoin_enabled": "Payjoin включено",
553 + "payjoin_request_awaiting_tx": "В ожидании транзакции",
554 + "payjoin_request_in_progress": "В ходе выполнения",
555 "payment_id": "ID платежа: ",
556 "payment_was_received": "Ваш платеж получен.",
557 "pending": " (в ожидании)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Отправить с внешнего кошелька",
742 "send_name": "Имя",
743 "send_new": "Новый",
744 + "send_payjoin": "Отправлять Payjoin",
745 "send_payment_id": "ID платежа (опционально)",
746 "send_priority": "Комиссия установлена в зависимости от приоритета: ${transactionPriority}.\nПриоритет транзакции может быть изменён в настройках",
747 "send_sending": "Отправка...",
@@ -974,6 +980,7 @@
980 "use": "Использовать ",
981 "use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.",
982 "use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.",
983 + "use_payjoin": "Использовать Payjoin",
984 "use_ssl": "Использовать SSL",
985 "use_suggested": "Использовать предложенный",
986 "use_testnet": "Используйте Testnet",
@@ -1064,4 +1071,4 @@
1071 "you_will_send": "Конвертировать из",
1072 "youCanGoBackToYourDapp": "Вы можете вернуться к своему даппу сейчас",
1073 "yy": "ГГ"
1067 -}
\ No newline at end of file
1074 +}
res/values/strings_th.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "คัดลอก",
203 "copy_address": "คัดลอกที่อยู่",
204 "copy_id": "คัดลอก ID",
205 + "copy_payjoin_url": "คัดลอก Payjoin url",
206 "copyWalletConnectLink": "คัดลอกลิงก์ WalletConnect จาก dApp แล้ววางที่นี่",
207 "corrupted_seed_notice": "ไฟล์สำหรับกระเป๋าเงินนี้เสียหายและไม่สามารถเปิดได้ โปรดดูวลีเมล็ดบันทึกและกู้คืนกระเป๋าเงิน\n\nหากค่าว่างเปล่าเมล็ดก็ไม่สามารถกู้คืนได้อย่างถูกต้อง",
208 "countries": "ประเทศ",
@@ -546,6 +547,10 @@
547 "password": "รหัสผ่าน",
548 "paste": "วาง",
549 "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว",
550 + "payjoin_details": "Payjoin รายละเอียด",
551 + "payjoin_enabled": "Payjoin เปิดใช้งาน",
552 + "payjoin_request_awaiting_tx": "รอธุรกรรม",
553 + "payjoin_request_in_progress": "การดำเนินการ",
554 "payment_id": "ID การชำระเงิน: ",
555 "payment_was_received": "การชำระเงินของคุณได้รับการรับทราบแล้ว",
556 "pending": " (อยู่ระหว่างดำเนินการ)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "ส่งจากกระเป๋าเงินภายนอก",
741 "send_name": "ชื่อ",
742 "send_new": "ใหม่",
743 + "send_payjoin": "ส่ง Payjoin",
744 "send_payment_id": "ID การชำระเงิน (ไม่จำเป็น)",
745 "send_priority": "ในขณะนี้ค่าธรรมเนียมถูกตั้งค่าเป็นความสำคัญ ${transactionPriority} \nความสำคัญของธุรกรรมสามารถปรับได้ในการตั้งค่า",
746 "send_sending": "กำลังส่ง...",
@@ -973,6 +979,7 @@
979 "use": "สลับไปที่ ",
980 "use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ",
981 "use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล",
982 + "use_payjoin": "ใช้ Payjoin",
983 "use_ssl": "ใช้ SSL",
984 "use_suggested": "ใช้ที่แนะนำ",
985 "use_testnet": "ใช้ testnet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "แปลงจาก",
1071 "youCanGoBackToYourDapp": "คุณสามารถกลับไปที่ dapp ของคุณได้ทันที",
1072 "yy": "ปี"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_tl.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kopyahin",
203 "copy_address": "Kopyahin ang Address",
204 "copy_id": "Kopyahin ang ID",
205 + "copy_payjoin_url": "Kopyahin ang Payjoin url",
206 "copyWalletConnectLink": "Kopyahin ang link ng WalletConnect mula sa dApp at i-paste dito",
207 "corrupted_seed_notice": "Ang mga file para sa pitaka na ito ay nasira at hindi mabubuksan. Mangyaring tingnan ang parirala ng binhi, i -save ito, at ibalik ang pitaka.\n\nKung ang halaga ay walang laman, kung gayon ang binhi ay hindi ma -recover nang tama.",
208 "countries": "Mga bansa",
@@ -546,6 +547,10 @@
547 "password": "Password",
548 "paste": "I-paste",
549 "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.",
550 + "payjoin_details": "Mga detalye ng Payjoin",
551 + "payjoin_enabled": "Payjoin pinagana",
552 + "payjoin_request_awaiting_tx": "Naghihintay ng transaksyon",
553 + "payjoin_request_in_progress": "Sa pag -unlad",
554 "payment_id": "Payment ID: ",
555 "payment_was_received": "Natanggap ang iyong bayad.",
556 "pending": "(hindi pa tapos)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Magpadala mula sa panlabas na pitaka",
741 "send_name": "Pangalan",
742 "send_new": "Bago",
743 + "send_payjoin": "Magpadala ng Payjoin",
744 "send_payment_id": "Payment ID (opsyonal)",
745 "send_priority": "Kasalukuyang nakatakda ang fee sa ${transactionPriority} priyoridad.\n Ang priyoridad ng transaksyon ay maaaring isaayos sa mga setting",
746 "send_sending": "Nagpapadala...",
@@ -973,6 +979,7 @@
979 "use": "Lumipat sa ",
980 "use_card_info_three": "Gamitin ang digital card online o sa mga paraan ng pagbabayad na walang contact.",
981 "use_card_info_two": "Ang mga pondo ay na-convert sa USD kapag hawak sa prepaid account, hindi sa mga digital na pera.",
982 + "use_payjoin": "Gumamit ng Payjoin",
983 "use_ssl": "Gumamit ng SSL",
984 "use_suggested": "Gumamit ng iminungkahing",
985 "use_testnet": "Gumamit ng testnet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "I-convert mula sa",
1071 "youCanGoBackToYourDapp": "Maaari kang bumalik sa iyong dapp ngayon",
1072 "yy": "YY"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_tr.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Kopyala",
203 "copy_address": "Adresi kopyala",
204 "copy_id": "ID'yi kopyala",
205 + "copy_payjoin_url": "Payjoin url kopyala",
206 "copyWalletConnectLink": "WalletConnect bağlantısını dApp'ten kopyalayıp buraya yapıştırın",
207 "corrupted_seed_notice": "Bu cüzdanın dosyaları bozuk ve açılamıyor. Lütfen tohum ifadesini görüntüleyin, kaydedin ve cüzdanı geri yükleyin.\n\nDeğer boşsa, tohum doğru bir şekilde geri kazanılamadı.",
208 "countries": "Ülkeler",
@@ -546,6 +547,10 @@
547 "password": "Parola",
548 "paste": "Yapıştır",
549 "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.",
550 + "payjoin_details": "Payjoin detaylar",
551 + "payjoin_enabled": "Payjoin etkinleştirilmiş",
552 + "payjoin_request_awaiting_tx": "İşlem bekliyor",
553 + "payjoin_request_in_progress": "Devam etmekte",
554 "payment_id": "Ödeme ID'si: ",
555 "payment_was_received": "Ödemeniz alındı.",
556 "pending": " (bekleyen)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "Harici cüzdandan gönder",
741 "send_name": "İsim",
742 "send_new": "Yeni",
743 + "send_payjoin": "Göndermek Payjoin",
744 "send_payment_id": "Ödeme ID'si (isteğe bağlı)",
745 "send_priority": "Şu anda ücret ${transactionPriority} önceliğine ayarlanmıştır.\nİşlem önceliği ayarlardan değiştirilebilir",
746 "send_sending": "Gönderiliyor...",
@@ -973,6 +979,7 @@
979 "use": "Şuna geç: ",
980 "use_card_info_three": "Dijital kartı çevrimiçi olarak veya temassız ödeme yöntemleriyle kullanın.",
981 "use_card_info_two": "Paralar, dijital para birimlerinde değil, ön ödemeli hesapta tutulduğunda USD'ye dönüştürülür.",
982 + "use_payjoin": "Kullanmak Payjoin",
983 "use_ssl": "SSL kullan",
984 "use_suggested": "Önerileni Kullan",
985 "use_testnet": "TestNet kullanın",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "Biçiminden dönüştür:",
1071 "youCanGoBackToYourDapp": "Şimdi Dapp'ınıza geri dönebilirsin",
1072 "yy": "YY"
1066 -}
\ No newline at end of file
1073 +}
res/values/strings_uk.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Скопіювати",
203 "copy_address": "Cкопіювати адресу",
204 "copy_id": "Скопіювати ID",
205 + "copy_payjoin_url": "Скопіюйте Payjoin url",
206 "copyWalletConnectLink": "Скопіюйте посилання WalletConnect із dApp і вставте сюди",
207 "corrupted_seed_notice": "Файли для цього гаманця пошкоджені і не можуть бути відкриті. Перегляньте насіннєву фразу, збережіть її та відновіть гаманець.\n\nЯкщо значення порожнє, то насіння не могло бути правильно відновленим.",
208 "countries": "Країни",
@@ -546,6 +547,10 @@
547 "password": "Пароль",
548 "paste": "Вставити",
549 "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.",
550 + "payjoin_details": "Payjoin деталей",
551 + "payjoin_enabled": "Payjoin увімкнено",
552 + "payjoin_request_awaiting_tx": "Чекає транзакції",
553 + "payjoin_request_in_progress": "Триває",
554 "payment_id": "ID платежу: ",
555 "payment_was_received": "Ваш платіж отримано.",
556 "pending": " (в очікуванні)",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Надіслати із зовнішнього гаманця",
742 "send_name": "Ім'я",
743 "send_new": "Новий",
744 + "send_payjoin": "Надіслати Payjoin",
745 "send_payment_id": "ID платежу (опційно)",
746 "send_priority": "Комісія встановлена в залежності від пріоритету: ${transactionPriority}.\nПріоритет транзакції може бути змінений в налаштуваннях",
747 "send_sending": "Відправлення...",
@@ -974,6 +980,7 @@
980 "use": "Використати ",
981 "use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.",
982 "use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.",
983 + "use_payjoin": "Використовуйте Payjoin",
984 "use_ssl": "Використати SSL",
985 "use_suggested": "Використати запропоноване",
986 "use_testnet": "Використовуйте тестову мережу",
@@ -1064,4 +1071,4 @@
1071 "you_will_send": "Конвертувати з",
1072 "youCanGoBackToYourDapp": "Ви можете повернутися до свого DAPP зараз",
1073 "yy": "YY"
1067 -}
\ No newline at end of file
1074 +}
res/values/strings_ur.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "کاپی",
203 "copy_address": "ایڈریس کاپی کریں۔",
204 "copy_id": "کاپی ID",
205 + "copy_payjoin_url": "کاپی کریں Payjoin url",
206 "copyWalletConnectLink": "dApp ﮯﺳ WalletConnect ۔ﮟﯾﺮﮐ ﭧﺴﯿﭘ ﮞﺎﮩﯾ ﺭﻭﺍ ﮟﯾﺮﮐ ﯽﭘﺎﮐ ﻮﮐ ﮏﻨﻟ",
207 "corrupted_seed_notice": "اس پرس کے لئے فائلیں خراب ہیں اور کھولنے سے قاصر ہیں۔ براہ کرم بیج کے فقرے کو دیکھیں ، اسے بچائیں ، اور بٹوے کو بحال کریں۔\n\nاگر قیمت خالی ہے ، تو بیج صحیح طور پر بازیافت کرنے سے قاصر تھا۔",
208 "countries": "ممالک",
@@ -548,6 +549,10 @@
549 "password": "پاس ورڈ",
550 "paste": "چسپاں کریں۔",
551 "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ",
552 + "payjoin_details": "Payjoin تفصیلات",
553 + "payjoin_enabled": "Payjoin فعال",
554 + "payjoin_request_awaiting_tx": "لین دین کے منتظر",
555 + "payjoin_request_in_progress": "پیشرفت میں",
556 "payment_id": "ادائیگی کی شناخت:",
557 "payment_was_received": "آپ کی ادائیگی موصول ہو گئی۔",
558 "pending": " (زیر التواء)",
@@ -737,6 +742,7 @@
742 "send_from_external_wallet": "بیرونی پرس سے بھیجیں",
743 "send_name": "نام",
744 "send_new": "نئی",
745 + "send_payjoin": "بھیجیں",
746 "send_payment_id": "ادائیگی کی شناخت (اختیاری)",
747 "send_priority": "فی الحال فیس ${transactionPriority} کی ترجیح پر سیٹ ہے۔\\nٹرانزیکشن کی ترجیح سیٹنگز میں ایڈجسٹ کی جا سکتی ہے۔",
748 "send_sending": "بھیج رہا ہے...",
@@ -975,6 +981,7 @@
981 "use": "تبدیل کرنا",
982 "use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔",
983 "use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔",
984 + "use_payjoin": "Payjoin کا استعمال کریں",
985 "use_ssl": "SSL استعمال کریں۔",
986 "use_suggested": "تجویز کردہ استعمال کریں۔",
987 "use_testnet": "ٹیسٹ نیٹ استعمال کریں",
@@ -1065,4 +1072,4 @@
1072 "you_will_send": "سے تبدیل کریں۔",
1073 "youCanGoBackToYourDapp": "اب آپ اپنے ڈی اے پی پی پر واپس جاسکتے ہیں",
1074 "yy": "YY"
1068 -}
\ No newline at end of file
1075 +}
res/values/strings_vi.arb
+8 -1
@@ -201,6 +201,7 @@
201 "copy": "Sao chép",
202 "copy_address": "Sao chép Địa chỉ",
203 "copy_id": "Sao chép ID",
204 + "copy_payjoin_url": "Sao chép Payjoin url",
205 "copyWalletConnectLink": "Sao chép liên kết WalletConnect từ dApp và dán vào đây",
206 "corrupted_seed_notice": "Các tệp cho ví này bị hỏng và không thể mở. Vui lòng xem cụm từ hạt giống, lưu nó và khôi phục ví.\n\nNếu giá trị trống, thì hạt giống không thể được phục hồi chính xác.",
207 "countries": "Quốc gia",
@@ -544,6 +545,10 @@
545 "password": "Mật khẩu",
546 "paste": "Dán",
547 "pause_wallet_creation": "Khả năng tạo ví Haven hiện đang bị tạm dừng.",
548 + "payjoin_details": "Payjoin chi tiết",
549 + "payjoin_enabled": "Payjoin Bật",
550 + "payjoin_request_awaiting_tx": "Đang chờ giao dịch",
551 + "payjoin_request_in_progress": "Trong tiến trình",
552 "payment_id": "ID thanh toán: ",
553 "payment_was_received": "Thanh toán của bạn đã được nhận.",
554 "pending": " (đang chờ)",
@@ -732,6 +737,7 @@
737 "send_from_external_wallet": "Gửi từ ví bên ngoài",
738 "send_name": "Tên",
739 "send_new": "Mới",
740 + "send_payjoin": "Gửi Payjoin",
741 "send_payment_id": "ID thanh toán (tùy chọn)",
742 "send_priority": "Hiện tại phí được đặt ở mức ưu tiên ${transactionPriority}.\nƯu tiên giao dịch có thể được điều chỉnh trong cài đặt",
743 "send_sending": "Đang gửi...",
@@ -970,6 +976,7 @@
976 "use": "Chuyển sang",
977 "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.",
978 "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ố.",
979 + "use_payjoin": "Sử dụng Payjoin",
980 "use_ssl": "Sử dụng SSL",
981 "use_suggested": "Sử dụng đề xuất",
982 "use_testnet": "Sử dụng Testnet",
@@ -1060,4 +1067,4 @@
1067 "you_will_send": "Chuyển đổi từ",
1068 "youCanGoBackToYourDapp": "Bạn có thể quay lại DAPP của mình ngay bây giờ",
1069 "yy": "YY"
1063 -}
\ No newline at end of file
1070 +}
res/values/strings_yo.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "Ṣẹ̀dà",
203 "copy_address": "Ṣẹ̀dà àdírẹ́sì",
204 "copy_id": "Ṣẹ̀dà àmì ìdánimọ̀",
205 + "copy_payjoin_url": "Daakọ Payjoin url",
206 "copyWalletConnectLink": "Daakọ ọna asopọ WalletConnect lati dApp ki o si lẹẹmọ nibi",
207 "corrupted_seed_notice": "Awọn faili fun apamọwọ yii jẹ ibajẹ ati pe ko lagbara lati ṣii. Jọwọ wo ọrọ iseda, fipamọ rẹ, ki o mu apamọwọ naa pada.\n\nTi iye ba ṣofo, lẹhinna irugbin naa ko lagbara lati gba pada ni deede.",
208 "countries": "Awọn orilẹ-ede",
@@ -547,6 +548,10 @@
548 "password": "Ọ̀rọ̀ aṣínà",
549 "paste": "Fikún ẹ̀dà yín",
550 "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.",
551 + "payjoin_details": "Payjoin awọn alaye",
552 + "payjoin_enabled": "Payjoin ṣiṣẹ",
553 + "payjoin_request_awaiting_tx": "O duro de idunadura",
554 + "payjoin_request_in_progress": "Ni ilọsiwaju",
555 "payment_id": "Àmì ìdánimọ̀ àránṣẹ́: ",
556 "payment_was_received": "Àránṣẹ́ yín ti dé.",
557 "pending": " pípẹ́",
@@ -736,6 +741,7 @@
741 "send_from_external_wallet": "Firanṣẹ lati apamọwọ ita",
742 "send_name": "Orúkọ",
743 "send_new": "Títun",
744 + "send_payjoin": "Firanṣẹ PayjoinPayjoin",
745 "send_payment_id": "Àmì ìdánimọ̀ àránṣẹ́ (ìyàn nìyí)",
746 "send_priority": "${transactionPriority} agbára ni owó àfikún lọ́wọ́lọ́wọ́.\nẸ lè pààrọ̀ iye agbára t'ẹ fikún àránṣẹ́ lórí àwọn ààtò",
747 "send_sending": "Ń Ránṣẹ́...",
@@ -974,6 +980,7 @@
980 "use": "Lo",
981 "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ò.",
982 "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.",
983 + "use_payjoin": "Lo Payjoin",
984 "use_ssl": "Lo SSL",
985 "use_suggested": "Lo àbá",
986 "use_testnet": "Lo tele",
@@ -1064,4 +1071,4 @@
1071 "you_will_send": "Ṣe pàṣípààrọ̀ láti",
1072 "youCanGoBackToYourDapp": "O le pada si tapla rẹ bayi",
1073 "yy": "Ọd"
1067 -}
\ No newline at end of file
1074 +}
res/values/strings_zh.arb
+8 -1
@@ -202,6 +202,7 @@
202 "copy": "复制",
203 "copy_address": "复制地址",
204 "copy_id": "复制ID",
205 + "copy_payjoin_url": "复制Payjoin url",
206 "copyWalletConnectLink": "从 dApp 复制 WalletConnect 链接并粘贴到此处",
207 "corrupted_seed_notice": "该钱包的文件被损坏,无法打开。请查看种子短语,保存并恢复钱包。\n\n如果该值为空,则种子无法正确恢复。",
208 "countries": "国家",
@@ -546,6 +547,10 @@
547 "password": "密码",
548 "paste": "粘贴",
549 "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。",
550 + "payjoin_details": "Payjoin 细节",
551 + "payjoin_enabled": "Payjoin启用",
552 + "payjoin_request_awaiting_tx": "等待交易",
553 + "payjoin_request_in_progress": "进行中",
554 "payment_id": "付款 ID: ",
555 "payment_was_received": "您的付款已收到。",
556 "pending": " (待定)",
@@ -735,6 +740,7 @@
740 "send_from_external_wallet": "从外部钱包发送",
741 "send_name": "名称",
742 "send_new": "新建",
743 + "send_payjoin": "发送 Payjoin",
744 "send_payment_id": "付款编号 (可选的)",
745 "send_priority": "目前,费用设置为 ${transactionPriority} 优先.\n交易优先级可以在设置中进行调整",
746 "send_sending": "正在发送...",
@@ -973,6 +979,7 @@
979 "use": "切换使用",
980 "use_card_info_three": "在线使用电子卡或使用非接触式支付方式。",
981 "use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。",
982 + "use_payjoin": "使用 Payjoin",
983 "use_ssl": "使用SSL",
984 "use_suggested": "使用建议",
985 "use_testnet": "使用TestNet",
@@ -1063,4 +1070,4 @@
1070 "you_will_send": "转换自",
1071 "youCanGoBackToYourDapp": "您现在可以回到DAPP",
1072 "yy": "YY"
1066 -}
\ No newline at end of file
1073 +}
tool/configure.dart
+11 -2
@@ -87,6 +87,7 @@ import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
87 import 'package:cake_wallet/view_model/send/output.dart';
88 import 'package:cw_core/hardware/hardware_account_data.dart';
89 import 'package:cw_core/node.dart';
90 +import 'package:cw_core/payjoin_session.dart';
91 import 'package:cw_core/output_info.dart';
92 import 'package:cw_core/pending_transaction.dart';
93 import 'package:cw_core/receive_page_option.dart';
@@ -118,10 +119,12 @@ import 'package:cw_bitcoin/electrum_wallet.dart';
119 import 'package:cw_bitcoin/bitcoin_unspent.dart';
120 import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
121 import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
122 +import 'package:cw_bitcoin/bitcoin_wallet.dart';
123 import 'package:cw_bitcoin/bitcoin_wallet_service.dart';
124 import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
125 import 'package:cw_bitcoin/bitcoin_amount_format.dart';
126 import 'package:cw_bitcoin/bitcoin_address_record.dart';
127 +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
128 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
129 import 'package:cw_bitcoin/litecoin_wallet_service.dart';
130 import 'package:cw_bitcoin/litecoin_wallet.dart';
@@ -171,7 +174,7 @@ abstract class Bitcoin {
174 int getFeeRate(Object wallet, TransactionPriority priority);
175 Future<void> generateNewAddress(Object wallet, String label);
176 Future<void> updateAddress(Object wallet,String address, String label);
174 - Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any});
177 + Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, String? payjoinUri});
178
179 String getAddress(Object wallet);
180 List<ElectrumSubAddress> getSilentPaymentAddresses(Object wallet);
@@ -189,7 +192,7 @@ abstract class Bitcoin {
192 List<Unspent> getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any});
193 Future<void> updateUnspents(Object wallet);
194 WalletService createBitcoinWalletService(
192 - Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
195 + Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, Box<PayjoinSession> payjoinSessionSource, bool alwaysScan, bool isDirect);
196 WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
197 TransactionPriority getBitcoinTransactionPriorityMedium();
198 TransactionPriority getBitcoinTransactionPriorityCustom();
@@ -206,6 +209,7 @@ abstract class Bitcoin {
209 List<ReceivePageOption> getBitcoinReceivePageOptions();
210 List<ReceivePageOption> getLitecoinReceivePageOptions();
211 BitcoinAddressType getBitcoinAddressType(ReceivePageOption option);
212 + bool isPayjoinAvailable(Object wallet);
213 bool hasSelectedSilentPayments(Object wallet);
214 bool isBitcoinReceivePageOption(ReceivePageOption option);
215 BitcoinAddressType getOptionToType(ReceivePageOption option);
@@ -240,6 +244,11 @@ abstract class Bitcoin {
244 bool getMwebEnabled(Object wallet);
245 String? getUnusedMwebAddress(Object wallet);
246 String? getUnusedSegwitAddress(Object wallet);
247 +
248 + void updatePayjoinState(Object wallet, bool state);
249 + String getPayjoinEndpoint(Object wallet);
250 + void resumePayjoinSessions(Object wallet);
251 + void stopPayjoinSessions(Object wallet);
252 }
253 """;
254