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>
This commit is contained in:
Konstantin Ullrich 2025-05-12 19:33:14 +02:00 committed by GitHub
parent 4a08e18f00
commit 82e3ebf4fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 2622 additions and 198 deletions

BIN
assets/images/payjoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -2,22 +2,36 @@ import 'package:bitcoin_base/bitcoin_base.dart';
String addressFromOutputScript(Script script, BasedUtxoNetwork network) {
try {
switch (script.getAddressType()) {
case P2pkhAddressType.p2pkh:
return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network);
case P2shAddressType.p2pkInP2sh:
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2wpkh:
return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network);
case P2shAddressType.p2pkhInP2sh:
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2wsh:
return P2wshAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2tr:
return P2trAddress.fromScriptPubkey(script: script).toAddress(network);
default:
}
return addressFromScript(script, network).toAddress(network);
} catch (_) {}
return '';
}
BitcoinBaseAddress addressFromScript(Script script,
[BasedUtxoNetwork network = BitcoinNetwork.mainnet]) {
final addressType = script.getAddressType();
if (addressType == null) {
throw ArgumentError("Invalid script");
}
switch (addressType) {
case P2pkhAddressType.p2pkh:
return P2pkhAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case P2shAddressType.p2pkhInP2sh:
return P2shAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case SegwitAddresType.p2wpkh:
return P2wpkhAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case SegwitAddresType.p2wsh:
return P2wshAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case SegwitAddresType.p2tr:
return P2trAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
}
throw ArgumentError("Invalid script");
}

View file

@ -3,11 +3,17 @@ import 'package:cw_core/output_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
class BitcoinTransactionCredentials {
BitcoinTransactionCredentials(this.outputs,
{required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any});
BitcoinTransactionCredentials(
this.outputs, {
required this.priority,
this.feeRate,
this.coinTypeToSpendFrom = UnspentCoinType.any,
this.payjoinUri,
});
final List<OutputInfo> outputs;
final BitcoinTransactionPriority? priority;
final int? feeRate;
final UnspentCoinType coinTypeToSpendFrom;
final String? payjoinUri;
}

View file

@ -3,22 +3,33 @@ import 'dart:convert';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/payjoin/storage.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/psbt/signer.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart';
@ -31,6 +42,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required EncryptionFileUtils encryptionFileUtils,
Uint8List? seedBytes,
String? mnemonic,
@ -71,8 +83,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// String derivationPath = walletInfo.derivationInfo!.derivationPath!;
// String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
// final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
walletAddresses = BitcoinWalletAddresses(
walletInfo,
payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
walletAddresses = BitcoinWalletAddresses(walletInfo,
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
@ -84,7 +97,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
masterHd:
seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
isHardwareWallet: walletInfo.isHardwareWallet,
);
payjoinManager: payjoinManager);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress =
@ -100,6 +113,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
@ -122,9 +136,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
seedBytes =
await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break;
}
return BitcoinWallet(
mnemonic: mnemonic,
passphrase: passphrase ?? "",
@ -141,6 +157,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
networkParam: network,
payjoinBox: payjoinBox,
);
}
@ -148,6 +165,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required String password,
required EncryptionFileUtils encryptionFileUtils,
required bool alwaysScan,
@ -204,7 +222,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (mnemonic != null) {
switch (walletInfo.derivationInfo!.derivationType) {
case DerivationType.electrum:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
seedBytes =
await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break;
case DerivationType.bip39:
default:
@ -234,7 +253,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
addressPageType: snp?.addressPageType,
networkParam: network,
alwaysScan: alwaysScan,
);
payjoinBox: payjoinBox);
}
LedgerConnection? _ledgerConnection;
@ -247,20 +266,25 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
derivationPath: walletInfo.derivationInfo!.derivationPath!);
}
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
late final PayjoinManager payjoinManager;
bool get isPayjoinAvailable => unspentCoinsInfo.values
.where((element) =>
element.walletId == id && element.isSending && !element.isFrozen)
.isNotEmpty;
Future<PsbtV2> buildPsbt({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
required Uint8List masterFingerprint,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx =
@ -278,13 +302,128 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
));
}
final psbt = PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
return PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF)
.psbt;
}
final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt);
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
final psbt = await buildPsbt(
outputs: outputs,
fee: fee,
network: network,
utxos: utxos,
publicKeys: publicKeys,
masterFingerprint: masterFingerprint,
memo: memo,
enableRBF: enableRBF,
inputOrdering: inputOrdering,
outputOrdering: outputOrdering,
);
final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
credentials = credentials as BitcoinTransactionCredentials;
final tx = (await super.createTransaction(credentials))
as PendingBitcoinTransaction;
final payjoinUri = credentials.payjoinUri;
if (payjoinUri == null) return tx;
final transaction = await buildPsbt(
utxos: tx.utxos,
outputs: tx.outputs
.map((e) => BitcoinOutput(
address: addressFromScript(e.scriptPubKey),
value: e.amount,
isSilentPayment: e.isSilentPayment,
isChange: e.isChange,
))
.toList(),
fee: BigInt.from(tx.fee),
network: network,
memo: credentials.outputs.first.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
publicKeys: tx.publicKeys!,
masterFingerprint: Uint8List(0));
final originalPsbt = await signPsbt(
base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
tx.commitOverride = () async {
final sender = await payjoinManager.initSender(
payjoinUri, originalPsbt, int.parse(tx.feeRate));
payjoinManager.spawnNewSender(
sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
};
return tx;
}
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
.where((e) => (e.isSending && !e.isFrozen))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
Future<void> commitPsbt(String finalizedPsbt) {
final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
final btcTx =
BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
return PendingBitcoinTransaction(
btcTx,
type,
electrumClient: electrumClient,
amount: 0,
fee: 0,
feeRate: "",
network: network,
hasChange: true,
).commit();
}
Future<String> signPsbt(
String preProcessedPsbt, List<UtxoWithPrivateKey> utxos) async {
final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt));
await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) {
return utxo.utxo.isP2tr()
? key.signTapRoot(
txDigest,
sighash: sighash,
tweak: utxo.utxo.isSilentPayment != true,
)
: key.signInput(txDigest, sigHash: sighash);
}, (txId, vout) async {
final txHex = await electrumClient.getTransactionHex(hash: txId);
final output = BtcTransaction.fromRaw(txHex).outputs[vout];
return TaprootAmountScriptPair(output.amount, output.scriptPubKey);
});
psbt.finalizeV0();
return base64Encode(psbt.asPsbtV0());
}
@override
Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) {

View file

@ -1,10 +1,13 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
import 'package:payjoin_flutter/receive.dart' as payjoin;
part 'bitcoin_wallet_addresses.g.dart';
@ -17,6 +20,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
required super.sideHd,
required super.network,
required super.isHardwareWallet,
required this.payjoinManager,
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
@ -25,6 +29,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
super.masterHd,
}) : super(walletInfo);
final PayjoinManager payjoinManager;
@observable
payjoin.Receiver? currentPayjoinReceiver;
@computed
String? get payjoinEndpoint =>
currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint();
@override
String getAddress(
{required int index,
@ -45,4 +58,17 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
return generateP2WPKHAddress(hd: hd, index: index, network: network);
}
Future<void> initPayjoin() async {
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
payjoinManager.resumeSessions();
}
Future<void> newPayjoinReceiver() async {
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
printV("Initializing new Payjoin Receiver");
payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!);
}
}

View file

@ -5,6 +5,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
@ -21,10 +22,12 @@ class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
this.payjoinSessionSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final Box<PayjoinSession> payjoinSessionSource;
final bool alwaysScan;
final bool isDirect;
@ -55,6 +58,7 @@ class BitcoinWalletService extends WalletService<
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -79,6 +83,7 @@ class BitcoinWalletService extends WalletService<
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -92,6 +97,7 @@ class BitcoinWalletService extends WalletService<
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -126,6 +132,7 @@ class BitcoinWalletService extends WalletService<
name: currentName,
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -147,7 +154,6 @@ class BitcoinWalletService extends WalletService<
credentials.walletInfo?.network = network.value;
credentials.walletInfo?.derivationInfo?.derivationPath =
credentials.hwAccountData.derivationPath;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.hwAccountData.xpub,
@ -155,6 +161,7 @@ class BitcoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
payjoinBox: payjoinSessionSource,
);
await wallet.save();
await wallet.init();
@ -182,6 +189,7 @@ class BitcoinWalletService extends WalletService<
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);

View file

@ -1188,6 +1188,7 @@ abstract class ElectrumWalletBase
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
publicKeys: estimatedTx.publicKeys
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
if (estimatedTx.spendsSilentPayment) {
@ -1965,6 +1966,11 @@ abstract class ElectrumWalletBase
}
}
bool isMine(Script script) {
final derivedAddress = addressFromOutputScript(script, network);
return addressesSet.contains(derivedAddress);
}
@override
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {

View file

@ -0,0 +1,298 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart';
import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_bitcoin/payjoin/storage.dart';
import 'package:cw_bitcoin/psbt/signer.dart';
import 'package:cw_bitcoin/psbt/utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:payjoin_flutter/common.dart';
import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/send.dart';
import 'package:payjoin_flutter/uri.dart' as PayjoinUri;
class PayjoinManager {
PayjoinManager(this._payjoinStorage, this._wallet);
final PayjoinStorage _payjoinStorage;
final BitcoinWalletBase _wallet;
final Map<String, PayjoinPollerSession> _activePollers = {};
static const List<String> ohttpRelayUrls = [
'https://pj.bobspacebkk.com',
'https://ohttp.achow101.com',
];
static Future<PayjoinUri.Url> randomOhttpRelayUrl() => PayjoinUri.Url.fromStr(
ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]);
static const payjoinDirectoryUrl = 'https://payjo.in';
Future<void> resumeSessions() async {
final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id);
final spawnedSessions = allSessions.map((session) {
if (session.isSenderSession) {
printV("Resuming Payjoin Sender Session ${session.pjUri!}");
return _spawnSender(
sender: Sender.fromJson(session.sender!),
pjUri: session.pjUri!,
);
}
final receiver = Receiver.fromJson(session.receiver!);
printV("Resuming Payjoin Receiver Session ${receiver.id()}");
return _spawnReceiver(receiver: receiver);
});
printV("Resumed ${spawnedSessions.length} Payjoin Sessions");
await Future.wait(spawnedSessions);
}
Future<Sender> initSender(
String pjUriString, String originalPsbt, int networkFeesSatPerVb) async {
try {
final pjUri =
(await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported();
final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250);
final senderBuilder = await SenderBuilder.fromPsbtAndUri(
psbtBase64: originalPsbt,
pjUri: pjUri,
);
return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu);
} catch (e) {
throw Exception('Error initializing Payjoin Sender: $e');
}
}
Future<void> spawnNewSender({
required Sender sender,
required String pjUrl,
required BigInt amount,
bool isTestnet = false,
}) async {
final pjUri = Uri.parse(pjUrl).queryParameters['pj']!;
await _payjoinStorage.insertSenderSession(
sender, pjUri, _wallet.id, amount);
return _spawnSender(isTestnet: isTestnet, sender: sender, pjUri: pjUri);
}
Future<void> _spawnSender({
required Sender sender,
required String pjUri,
bool isTestnet = false,
}) async {
final completer = Completer();
final receivePort = ReceivePort();
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
try {
switch (message['type'] as PayjoinSenderRequestTypes) {
case PayjoinSenderRequestTypes.requestPosted:
return;
case PayjoinSenderRequestTypes.psbtToSign:
final proposalPsbt = message['psbt'] as String;
final utxos = _wallet.getUtxoWithPrivateKeys();
final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos);
final txId = getTxIdFromPsbtV0(finalizedPsbt);
_wallet.commitPsbt(finalizedPsbt);
_cleanupSession(pjUri);
await _payjoinStorage.markSenderSessionComplete(pjUri, txId);
completer.complete();
}
} catch (e) {
_cleanupSession(pjUri);
printV(e);
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
completer.completeError(e);
}
} else if (message is PayjoinSessionError) {
_cleanupSession(pjUri);
if (message is UnrecoverableError) {
printV(message.message);
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
completer.complete();
} else if (message is RecoverableError) {
completer.complete();
} else {
completer.completeError(message);
}
}
});
final isolate = await Isolate.spawn(
PayjoinSenderWorker.run,
[receivePort.sendPort, sender.toJson(), pjUri],
);
_activePollers[pjUri] = PayjoinPollerSession(isolate, receivePort);
return completer.future;
}
Future<Receiver> initReceiver(String address,
[bool isTestnet = false]) async {
try {
final payjoinDirectory =
await PayjoinUri.Url.fromStr(payjoinDirectoryUrl);
final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
ohttpRelay: await randomOhttpRelayUrl(),
payjoinDirectory: payjoinDirectory,
);
final receiver = await Receiver.create(
address: address,
network: isTestnet ? Network.testnet : Network.bitcoin,
directory: payjoinDirectory,
ohttpKeys: ohttpKeys,
ohttpRelay: await randomOhttpRelayUrl(),
);
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
return receiver;
} catch (e) {
throw Exception('Error initializing Payjoin Receiver: $e');
}
}
Future<void> spawnNewReceiver({
required Receiver receiver,
bool isTestnet = false,
}) async {
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
return _spawnReceiver(isTestnet: isTestnet, receiver: receiver);
}
Future<void> _spawnReceiver({
required Receiver receiver,
bool isTestnet = false,
}) async {
final completer = Completer();
final receivePort = ReceivePort();
SendPort? mainToIsolateSendPort;
List<UtxoWithPrivateKey> utxos = [];
String rawAmount = '0';
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
try {
switch (message['type'] as PayjoinReceiverRequestTypes) {
case PayjoinReceiverRequestTypes.processOriginalTx:
final tx = message['tx'] as String;
rawAmount = getOutputAmountFromTx(tx, _wallet);
break;
case PayjoinReceiverRequestTypes.checkIsOwned:
(_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver();
_payjoinStorage.markReceiverSessionInProgress(receiver.id());
final inputScript = message['input_script'] as Uint8List;
final isOwned =
_wallet.isMine(Script.fromRaw(byteData: inputScript));
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': isOwned,
});
break;
case PayjoinReceiverRequestTypes.checkIsReceiverOutput:
final outputScript = message['output_script'] as Uint8List;
final isReceiverOutput =
_wallet.isMine(Script.fromRaw(byteData: outputScript));
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': isReceiverOutput,
});
break;
case PayjoinReceiverRequestTypes.getCandidateInputs:
utxos = _wallet.getUtxoWithPrivateKeys();
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': utxos,
});
break;
case PayjoinReceiverRequestTypes.processPsbt:
final psbt = message['psbt'] as String;
final signedPsbt = await _wallet.signPsbt(psbt, utxos);
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': signedPsbt,
});
break;
case PayjoinReceiverRequestTypes.proposalSent:
_cleanupSession(receiver.id());
final psbt = message['psbt'] as String;
await _payjoinStorage.markReceiverSessionComplete(
receiver.id(), getTxIdFromPsbtV0(psbt), rawAmount);
completer.complete();
}
} catch (e) {
_cleanupSession(receiver.id());
await _payjoinStorage.markReceiverSessionUnrecoverable(
receiver.id(), e.toString());
completer.completeError(e);
}
} else if (message is PayjoinSessionError) {
_cleanupSession(receiver.id());
if (message is UnrecoverableError) {
await _payjoinStorage.markReceiverSessionUnrecoverable(
receiver.id(), message.message);
completer.complete();
} else if (message is RecoverableError) {
completer.complete();
} else {
completer.completeError(message);
}
} else if (message is SendPort) {
mainToIsolateSendPort = message;
}
});
final isolate = await Isolate.spawn(
PayjoinReceiverWorker.run,
[receivePort.sendPort, receiver.toJson()],
);
_activePollers[receiver.id()] = PayjoinPollerSession(isolate, receivePort);
return completer.future;
}
void cleanupSessions() {
final sessionIds = _activePollers.keys.toList();
for (final sessionId in sessionIds) {
_cleanupSession(sessionId);
}
}
void _cleanupSession(String sessionId) {
_activePollers[sessionId]?.close();
_activePollers.remove(sessionId);
}
}
class PayjoinPollerSession {
final Isolate isolate;
final ReceivePort port;
PayjoinPollerSession(this.isolate, this.port);
void close() {
isolate.kill();
port.close();
}
}

View file

@ -0,0 +1,219 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_bitcoin/psbt/signer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:http/http.dart' as http;
import 'package:payjoin_flutter/bitcoin_ffi.dart';
import 'package:payjoin_flutter/common.dart';
import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj;
enum PayjoinReceiverRequestTypes {
processOriginalTx,
proposalSent,
getCandidateInputs,
checkIsOwned,
checkIsReceiverOutput,
processPsbt;
}
class PayjoinReceiverWorker {
final SendPort sendPort;
final pendingRequests = <String, Completer<dynamic>>{};
PayjoinReceiverWorker._(this.sendPort);
static Future<void> run(List<Object> args) async {
await pj.core.init();
final sendPort = args[0] as SendPort;
final receiverJson = args[1] as String;
final worker = PayjoinReceiverWorker._(sendPort);
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen(worker.handleMessage);
try {
final httpClient = http.Client();
final receiver = Receiver.fromJson(receiverJson);
final uncheckedProposal =
await worker.receiveUncheckedProposal(httpClient, receiver);
final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast();
sendPort.send({
'type': PayjoinReceiverRequestTypes.processOriginalTx,
'tx': BytesUtils.toHexString(originalTx),
});
final payjoinProposal = await worker.processPayjoinProposal(
uncheckedProposal,
);
final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal);
sendPort.send({
'type': PayjoinReceiverRequestTypes.proposalSent,
'psbt': psbt,
});
} catch (e) {
if (e is HttpException ||
(e is http.ClientException &&
e.message.contains("Software caused connection abort"))) {
sendPort.send(PayjoinSessionError.recoverable(e.toString()));
} else {
sendPort.send(PayjoinSessionError.unrecoverable(e.toString()));
}
}
}
void handleMessage(dynamic message) async {
if (message is Map<String, dynamic>) {
final requestId = message['requestId'] as String?;
if (requestId != null && pendingRequests.containsKey(requestId)) {
pendingRequests[requestId]!.complete(message['result']);
pendingRequests.remove(requestId);
}
}
}
Future<dynamic> _sendRequest(PayjoinReceiverRequestTypes type,
[Map<String, dynamic> data = const {}]) async {
final completer = Completer<dynamic>();
final requestId = DateTime.now().millisecondsSinceEpoch.toString();
pendingRequests[requestId] = completer;
sendPort.send({
...data,
'type': type,
'requestId': requestId,
});
return completer.future;
}
Future<UncheckedProposal> receiveUncheckedProposal(
http.Client httpClient, Receiver session) async {
while (true) {
printV("Polling for Proposal (${session.id()})");
final extractReq = await session.extractReq();
final request = extractReq.$1;
final url = Uri.parse(request.url.asString());
final httpRequest = await httpClient.post(url,
headers: {'Content-Type': request.contentType}, body: request.body);
final proposal = await session.processRes(
body: httpRequest.bodyBytes, ctx: extractReq.$2);
if (proposal != null) return proposal;
}
}
Future<String> sendFinalProposal(
http.Client httpClient, PayjoinProposal finalProposal) async {
final req = await finalProposal.extractV2Req();
final proposalReq = req.$1;
final proposalCtx = req.$2;
final request = await httpClient.post(
Uri.parse(proposalReq.url.asString()),
headers: {"Content-Type": proposalReq.contentType},
body: proposalReq.body,
);
await finalProposal.processRes(
res: request.bodyBytes,
ohttpContext: proposalCtx,
);
return await finalProposal.psbt();
}
Future<PayjoinProposal> processPayjoinProposal(
UncheckedProposal proposal) async {
await proposal.extractTxToScheduleBroadcast();
// TODO Handle this. send to the main port on a timer?
try {
// Receive Check 1: can broadcast
final pj1 = await proposal.assumeInteractiveReceiver();
// Receive Check 2: original PSBT has no receiver-owned inputs
final pj2 = await pj1.checkInputsNotOwned(
isOwned: (inputScript) async {
final result = await _sendRequest(
PayjoinReceiverRequestTypes.checkIsOwned,
{'input_script': inputScript},
);
return result as bool;
},
);
// Receive Check 3: sender inputs have not been seen before (prevent probing attacks)
final pj3 = await pj2.checkNoInputsSeenBefore(isKnown: (input) => false);
// Identify receiver outputs
final pj4 = await pj3.identifyReceiverOutputs(
isReceiverOutput: (outputScript) async {
final result = await _sendRequest(
PayjoinReceiverRequestTypes.checkIsReceiverOutput,
{'output_script': outputScript},
);
return result as bool;
},
);
final pj5 = await pj4.commitOutputs();
final listUnspent =
await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs);
final unspent = listUnspent as List<UtxoWithPrivateKey>;
if (unspent.isEmpty) throw Exception('No unspent outputs available');
final selectedUtxo = await _inputPairFromUtxo(unspent[0]);
final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]);
final pj7 = await pj6.commitInputs();
// Finalize proposal
final payjoinProposal = await pj7.finalizeProposal(
processPsbt: (String psbt) async {
final result = await _sendRequest(
PayjoinReceiverRequestTypes.processPsbt, {'psbt': psbt});
return result as String;
},
// TODO set maxFeeRateSatPerVb
maxFeeRateSatPerVb: BigInt.from(10000),
);
return payjoinProposal;
} catch (e) {
printV('Error occurred while finalizing proposal: $e');
rethrow;
}
}
Future<InputPair> _inputPairFromUtxo(UtxoWithPrivateKey utxo) async {
final txout = TxOut(
value: utxo.utxo.value,
scriptPubkey: Uint8List.fromList(
utxo.ownerDetails.address.toScriptPubKey().toBytes()),
);
final psbtin =
PsbtInput(witnessUtxo: txout, redeemScript: null, witnessScript: null);
final previousOutput =
OutPoint(txid: utxo.utxo.txHash, vout: utxo.utxo.vout);
final txin = TxIn(
previousOutput: previousOutput,
scriptSig: await Script.newInstance(rawOutputScript: []),
witness: [],
sequence: 0,
);
return InputPair.newInstance(txin, psbtin);
}
}

View file

@ -0,0 +1,119 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:http/http.dart' as http;
import 'package:payjoin_flutter/common.dart';
import 'package:payjoin_flutter/send.dart';
import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj;
enum PayjoinSenderRequestTypes {
requestPosted,
psbtToSign;
}
class PayjoinSenderWorker {
final SendPort sendPort;
final pendingRequests = <String, Completer<dynamic>>{};
final String pjUrl;
PayjoinSenderWorker._(this.sendPort, this.pjUrl);
static Future<void> run(List<Object> args) async {
await pj.core.init();
final sendPort = args[0] as SendPort;
final senderJson = args[1] as String;
final pjUrl = args[2] as String;
final sender = Sender.fromJson(senderJson);
final worker = PayjoinSenderWorker._(sendPort, pjUrl);
try {
final proposalPsbt = await worker.runSender(sender);
sendPort.send({
'type': PayjoinSenderRequestTypes.psbtToSign,
'psbt': proposalPsbt,
});
} catch (e) {
sendPort.send(e);
}
}
/// Run a payjoin sender (V2 protocol first, fallback to V1).
Future<String> runSender(Sender sender) async {
final httpClient = http.Client();
try {
return await _runSenderV2(sender, httpClient);
} catch (e) {
printV(e);
if (e is PayjoinException &&
// TODO condition on error type instead of message content
e.message?.contains('parse receiver public key') == true) {
return await _runSenderV1(sender, httpClient);
} else if (e is HttpException) {
printV(e);
throw Exception(PayjoinSessionError.recoverable(e.toString()));
} else {
throw Exception(PayjoinSessionError.unrecoverable(e.toString()));
}
}
}
/// Attempt to send payjoin using the V2 of the protocol.
Future<String> _runSenderV2(Sender sender, http.Client httpClient) async {
try {
final postRequest = await sender.extractV2(
ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(),
);
final postResult = await _postRequest(httpClient, postRequest.$1);
final getContext =
await postRequest.$2.processResponse(response: postResult);
sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted, "pj": pjUrl});
while (true) {
printV('Polling V2 Proposal Request (${pjUrl})');
final getRequest = await getContext.extractReq(
ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(),
);
final getRes = await _postRequest(httpClient, getRequest.$1);
final proposalPsbt = await getContext.processResponse(
response: getRes,
ohttpCtx: getRequest.$2,
);
printV("$proposalPsbt");
if (proposalPsbt != null) return proposalPsbt;
}
} catch (e) {
rethrow;
}
}
/// Attempt to send payjoin using the V1 of the protocol.
Future<String> _runSenderV1(Sender sender, http.Client httpClient) async {
try {
final postRequest = await sender.extractV1();
final response = await _postRequest(httpClient, postRequest.$1);
sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted});
return await postRequest.$2.processResponse(response: response);
} catch (e) {
throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e');
}
}
Future<List<int>> _postRequest(http.Client client, Request req) async {
final httpRequest = await client.post(Uri.parse(req.url.asString()),
headers: {'Content-Type': req.contentType}, body: req.body);
return httpRequest.bodyBytes;
}
}

View file

@ -0,0 +1,16 @@
class PayjoinSessionError {
final String message;
const PayjoinSessionError._(this.message);
factory PayjoinSessionError.recoverable(String message) = RecoverableError;
factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError;
}
class RecoverableError extends PayjoinSessionError {
const RecoverableError(super.message) : super._();
}
class UnrecoverableError extends PayjoinSessionError {
const UnrecoverableError(super.message) : super._();
}

View file

@ -0,0 +1,95 @@
import 'package:cw_core/payjoin_session.dart';
import 'package:hive/hive.dart';
import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/send.dart';
class PayjoinStorage {
PayjoinStorage(this._payjoinSessionSources);
final Box<PayjoinSession> _payjoinSessionSources;
static const String _receiverPrefix = 'pj_recv_';
static const String _senderPrefix = 'pj_send_';
Future<void> insertReceiverSession(
Receiver receiver,
String walletId,
) =>
_payjoinSessionSources.put(
"$_receiverPrefix${receiver.id()}",
PayjoinSession(
walletId: walletId,
receiver: receiver.toJson(),
),
);
Future<void> markReceiverSessionComplete(
String sessionId, String txId, String amount) async {
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
session.status = PayjoinSessionStatus.success.name;
session.txId = txId;
session.rawAmount = amount;
await session.save();
}
Future<void> markReceiverSessionUnrecoverable(
String sessionId, String reason) async {
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
session.status = PayjoinSessionStatus.unrecoverable.name;
session.error = reason;
await session.save();
}
Future<void> markReceiverSessionInProgress(String sessionId) async {
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
session.status = PayjoinSessionStatus.inProgress.name;
session.inProgressSince = DateTime.now();
await session.save();
}
Future<void> insertSenderSession(
Sender sender,
String pjUrl,
String walletId,
BigInt amount,
) =>
_payjoinSessionSources.put(
"$_senderPrefix$pjUrl",
PayjoinSession(
walletId: walletId,
pjUri: pjUrl,
sender: sender.toJson(),
status: PayjoinSessionStatus.inProgress.name,
inProgressSince: DateTime.now(),
rawAmount: amount.toString(),
),
);
Future<void> markSenderSessionComplete(String pjUrl, String txId) async {
final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
session.status = PayjoinSessionStatus.success.name;
session.txId = txId;
await session.save();
}
Future<void> markSenderSessionUnrecoverable(String pjUrl) async {
final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
session.status = PayjoinSessionStatus.unrecoverable.name;
await session.save();
}
List<PayjoinSession> readAllOpenSessions(String walletId) =>
_payjoinSessionSources.values
.where((session) =>
session.walletId == walletId &&
![
PayjoinSessionStatus.success.name,
PayjoinSessionStatus.unrecoverable.name
].contains(session.status))
.toList();
}

View file

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

View file

@ -0,0 +1,263 @@
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_bitcoin/src/utils/buffer_writer.dart';
extension PsbtSigner on PsbtV2 {
Uint8List extractUnsignedTX({bool getSegwit = true}) {
final tx = BufferWriter()..writeUInt32(getGlobalTxVersion());
final isSegwit = getInputWitnessUtxo(0) != null;
if (isSegwit && getSegwit) {
tx.writeSlice(Uint8List.fromList([0, 1]));
}
final inputCount = getGlobalInputCount();
tx.writeVarInt(inputCount);
for (var i = 0; i < inputCount; i++) {
tx
..writeSlice(getInputPreviousTxid(i))
..writeUInt32(getInputOutputIndex(i))
..writeVarSlice(Uint8List(0))
..writeUInt32(getInputSequence(i));
}
final outputCount = getGlobalOutputCount();
tx.writeVarInt(outputCount);
for (var i = 0; i < outputCount; i++) {
tx.writeUInt64(getOutputAmount(i));
tx.writeVarSlice(getOutputScript(i));
}
tx.writeUInt32(getGlobalFallbackLocktime() ?? 0);
return tx.buffer();
}
Future<void> signWithUTXO(
List<UtxoWithPrivateKey> utxos, UTXOSignerCallBack signer,
[UTXOGetterCallBack? getTaprootPair]) async {
final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false));
final tx = BtcTransaction.fromRaw(raw);
/// when the transaction is taproot and we must use getTaproot transaction
/// digest we need all of inputs amounts and owner script pub keys
List<BigInt> taprootAmounts = [];
List<Script> taprootScripts = [];
if (utxos.any((e) => e.utxo.isP2tr())) {
for (final input in tx.inputs) {
final utxo = utxos.firstWhereOrNull(
(u) => u.utxo.txHash == input.txId && u.utxo.vout == input.txIndex);
if (utxo == null) {
final trPair = await getTaprootPair!.call(input.txId, input.txIndex);
taprootAmounts.add(trPair.value);
taprootScripts.add(trPair.script);
continue;
}
taprootAmounts.add(utxo.utxo.value);
taprootScripts.add(_findLockingScript(utxo, true));
}
}
for (var i = 0; i < tx.inputs.length; i++) {
final utxo = utxos.firstWhereOrNull((e) =>
e.utxo.txHash == tx.inputs[i].txId &&
e.utxo.vout == tx.inputs[i].txIndex); // ToDo: More robust verify
if (utxo == null) continue;
/// We receive the owner's ScriptPubKey
final script = _findLockingScript(utxo, false);
final int sighash = utxo.utxo.isP2tr()
? BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL
: BitcoinOpCodeConst.SIGHASH_ALL;
/// We generate transaction digest for current input
final digest = _generateTransactionDigest(
script, i, utxo.utxo, tx, taprootAmounts, taprootScripts);
/// now we need sign the transaction digest
final sig = signer(digest, utxo, utxo.privateKey, sighash);
if (utxo.utxo.isP2tr()) {
setInputTapKeySig(i, Uint8List.fromList(BytesUtils.fromHexString(sig)));
} else {
setInputPartialSig(
i,
Uint8List.fromList(BytesUtils.fromHexString(utxo.public().toHex())),
Uint8List.fromList(BytesUtils.fromHexString(sig)));
}
}
}
List<int> _generateTransactionDigest(
Script scriptPubKeys,
int input,
BitcoinUtxo utxo,
BtcTransaction transaction,
List<BigInt> taprootAmounts,
List<Script> tapRootPubKeys) {
if (utxo.isSegwit()) {
if (utxo.isP2tr()) {
return transaction.getTransactionTaprootDigset(
txIndex: input,
scriptPubKeys: tapRootPubKeys,
amounts: taprootAmounts,
);
}
return transaction.getTransactionSegwitDigit(
txInIndex: input, script: scriptPubKeys, amount: utxo.value);
}
return transaction.getTransactionDigest(
txInIndex: input, script: scriptPubKeys);
}
Script _findLockingScript(UtxoWithAddress utxo, bool isTaproot) {
if (utxo.isMultiSig()) {
throw Exception("MultiSig is not supported yet");
}
final senderPub = utxo.public();
switch (utxo.utxo.scriptType) {
case PubKeyAddressType.p2pk:
return senderPub.toRedeemScript();
case SegwitAddresType.p2wsh:
if (isTaproot) {
return senderPub.toP2wshAddress().toScriptPubKey();
}
return senderPub.toP2wshRedeemScript();
case P2pkhAddressType.p2pkh:
return senderPub.toP2pkhAddress().toScriptPubKey();
case SegwitAddresType.p2wpkh:
if (isTaproot) {
return senderPub.toP2wpkhAddress().toScriptPubKey();
}
return senderPub.toP2pkhAddress().toScriptPubKey();
case SegwitAddresType.p2tr:
return senderPub
.toTaprootAddress(tweak: utxo.utxo.isSilentPayment != true)
.toScriptPubKey();
case SegwitAddresType.mweb:
return Script(script: []);
case P2shAddressType.p2pkhInP2sh:
if (isTaproot) {
return senderPub.toP2pkhInP2sh().toScriptPubKey();
}
return senderPub.toP2pkhAddress().toScriptPubKey();
case P2shAddressType.p2wpkhInP2sh:
if (isTaproot) {
return senderPub.toP2wpkhInP2sh().toScriptPubKey();
}
return senderPub.toP2pkhAddress().toScriptPubKey();
case P2shAddressType.p2wshInP2sh:
if (isTaproot) {
return senderPub.toP2wshInP2sh().toScriptPubKey();
}
return senderPub.toP2wshRedeemScript();
case P2shAddressType.p2pkInP2sh:
if (isTaproot) {
return senderPub.toP2pkInP2sh().toScriptPubKey();
}
return senderPub.toRedeemScript();
}
throw Exception("invalid bitcoin address type");
}
}
typedef UTXOSignerCallBack = String Function(List<int> trDigest,
UtxoWithAddress utxo, ECPrivate privateKey, int sighash);
typedef UTXOGetterCallBack = Future<TaprootAmountScriptPair> Function(
String txId, int vout);
class TaprootAmountScriptPair {
final BigInt value;
final Script script;
const TaprootAmountScriptPair(this.value, this.script);
}
class UtxoWithPrivateKey extends UtxoWithAddress {
final ECPrivate privateKey;
UtxoWithPrivateKey({
required super.utxo,
required super.ownerDetails,
required this.privateKey,
});
factory UtxoWithPrivateKey.fromUtxo(
UtxoWithAddress input, List<ECPrivateInfo> inputPrivateKeyInfos) {
ECPrivateInfo? key;
if (inputPrivateKeyInfos.isEmpty) {
throw Exception("No private keys generated.");
} else {
key = inputPrivateKeyInfos.firstWhereOrNull((element) {
final elemPubkey = element.privkey.getPublic().toHex();
if (elemPubkey == input.public().toHex()) {
return true;
} else {
return false;
}
});
}
if (key == null) {
throw Exception("${input.utxo.txHash} No Key found");
}
return UtxoWithPrivateKey(
utxo: input.utxo,
ownerDetails: input.ownerDetails,
privateKey: key.privkey);
}
factory UtxoWithPrivateKey.fromUnspent(
BitcoinUnspent input, BitcoinWalletBase wallet) {
final address =
RegexUtils.addressTypeFromStr(input.address, BitcoinNetwork.mainnet);
final newHd =
input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.hd;
ECPrivate privkey;
if (input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress =
input.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
privkey = wallet.walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
),
);
} else {
privkey = generateECPrivate(
hd: newHd,
index: input.bitcoinAddressRecord.index,
network: BitcoinNetwork.mainnet);
}
return UtxoWithPrivateKey(
utxo: BitcoinUtxo(
txHash: input.hash,
value: BigInt.from(input.value),
vout: input.vout,
scriptType: input.bitcoinAddressRecord.type,
isSilentPayment:
input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord,
),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
privateKey: privkey);
}
}

View file

@ -0,0 +1,41 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:ledger_bitcoin/psbt.dart';
String getTxIdFromPsbtV0(String psbt) {
final psbtV2 = PsbtV2()..deserializeV0(base64.decode(psbt));
return BtcTransaction.fromRaw(
BytesUtils.toHexString(psbtV2.extractUnsignedTX(false)))
.txId();
}
String getOutputAmountFromPsbt(String psbtV0, BitcoinWalletBase wallet) {
printV(psbtV0);
final psbt = PsbtV2()..deserializeV0(base64.decode(psbtV0));
int amount = 0;
for (var i = 0; i < psbt.getGlobalOutputCount(); i++) {
final script = psbt.getOutputScript(i);
if (wallet.isMine(Script.fromRaw(byteData: script))) {
amount += psbt.getOutputAmount(i);
}
}
return amount.toString();
}
String getOutputAmountFromTx(String originalTx, BitcoinWalletBase wallet) {
final tx = BtcTransaction.fromRaw(originalTx);
BigInt amount = BigInt.zero;
for (final output in tx.outputs) {
if (wallet.isMine(output.scriptPubKey)) {
amount += output.amount;
}
}
printV(amount);
return amount.toString();
}

View file

@ -0,0 +1,52 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_bitcoin/src/psbt/map_extension.dart';
import 'package:ledger_bitcoin/src/utils/buffer_reader.dart';
import 'package:ledger_bitcoin/src/utils/uint8list_extension.dart' as ext;
extension PsbtSigner on PsbtV2 {
void deserializeV0(Uint8List psbt) {
final bufferReader = BufferReader(psbt);
if (!listEquals(bufferReader.readSlice(5), Uint8List.fromList([0x70, 0x73, 0x62, 0x74, 0xff]))) {
throw Exception("Invalid magic bytes");
}
while (_readKeyPair(globalMap, bufferReader)) {}
final tx = BtcTransaction.fromRaw(BytesUtils.toHexString(globalMap['00']!));
setGlobalInputCount(tx.inputs.length);
setGlobalOutputCount(tx.outputs.length);
setGlobalTxVersion(Uint8List.fromList(tx.version).readUint32LE(0));
for (var i = 0; i < getGlobalInputCount(); i++) {
inputMaps.insert(i, <String, Uint8List>{});
while (_readKeyPair(inputMaps[i], bufferReader)) {}
final input = tx.inputs[i];
setInputOutputIndex(i, input.txIndex);
setInputPreviousTxId(i, Uint8List.fromList(BytesUtils.fromHexString(input.txId).reversed.toList()));
setInputSequence(i, Uint8List.fromList(input.sequence).readUint32LE(0));
}
for (var i = 0; i < getGlobalOutputCount(); i++) {
outputMaps.insert(i, <String, Uint8List>{});
while (_readKeyPair(outputMaps[i], bufferReader)) {}
final output = tx.outputs[i];
setOutputAmount(i, output.amount.toInt());
setOutputScript(i, Uint8List.fromList(output.scriptPubKey.toBytes()));
}
}
bool _readKeyPair(Map<String, Uint8List> map, BufferReader bufferReader) {
final keyLen = bufferReader.readVarInt();
if (keyLen == 0) return false;
final keyType = bufferReader.readUInt8();
final keyData = bufferReader.readSlice(keyLen - 1);
final value = bufferReader.readVarSlice();
map.set(keyType, keyData, value);
return true;
}
}

View file

@ -0,0 +1,143 @@
import "dart:typed_data";
import "package:ledger_bitcoin/src/psbt/constants.dart";
import "package:ledger_bitcoin/src/psbt/psbtv2.dart";
import "package:ledger_bitcoin/src/utils/buffer_writer.dart";
/// This roughly implements the "input finalizer" role of BIP370 (PSBTv2
/// https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However
/// the role is documented in BIP174 (PSBTv0
/// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
///
/// Verify that all inputs have a signature, and set inputFinalScriptwitness
/// and/or inputFinalScriptSig depending on the type of the spent outputs. Clean
/// fields that aren't useful anymore, partial signatures, redeem script and
/// derivation paths.
///
/// @param psbt The psbt with all signatures added as partial sigs, either
/// through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG
extension InputFinalizer on PsbtV2 {
void finalizeV0() {
// First check that each input has a signature
for (var i = 0; i < getGlobalInputCount(); i++) {
if (_isFinalized(i)) continue;
final legacyPubkeys = getInputKeyDatas(i, PSBTIn.partialSig);
final taprootSig = getInputTapKeySig(i);
if (legacyPubkeys.isEmpty && taprootSig == null) {
continue;
// throw Exception('No signature for input $i present');
}
if (legacyPubkeys.isNotEmpty) {
if (legacyPubkeys.length > 1) {
throw Exception(
'Expected exactly one signature, got ${legacyPubkeys.length}');
}
if (taprootSig != null) {
throw Exception('Both taproot and non-taproot signatures present.');
}
final isSegwitV0 = getInputWitnessUtxo(i) != null;
final redeemScript = getInputRedeemScript(i);
final isWrappedSegwit = redeemScript != null;
final signature = getInputPartialSig(i, legacyPubkeys[0]);
if (signature == null) {
throw Exception('Expected partial signature for input $i');
}
if (isSegwitV0) {
final witnessBuf = BufferWriter()
..writeVarInt(2)
..writeVarInt(signature.length)
..writeSlice(signature)
..writeVarInt(legacyPubkeys[0].length)
..writeSlice(legacyPubkeys[0]);
setInputFinalScriptwitness(i, witnessBuf.buffer());
if (isWrappedSegwit) {
if (redeemScript.isEmpty) {
throw Exception(
"Expected non-empty redeemscript. Can't finalize intput $i");
}
final scriptSigBuf = BufferWriter()
..writeUInt8(redeemScript.length) // Push redeemScript length
..writeSlice(redeemScript);
setInputFinalScriptsig(i, scriptSigBuf.buffer());
}
} else {
// Legacy input
final scriptSig = BufferWriter();
_writePush(scriptSig, signature);
_writePush(scriptSig, legacyPubkeys[0]);
setInputFinalScriptsig(i, scriptSig.buffer());
}
} else {
// Taproot input
final signature = getInputTapKeySig(i);
if (signature == null) {
throw Exception("No taproot signature found");
}
if (signature.length != 64 && signature.length != 65) {
throw Exception("Unexpected length of schnorr signature.");
}
final witnessBuf = BufferWriter()
..writeVarInt(1)
..writeVarSlice(signature);
setInputFinalScriptwitness(i, witnessBuf.buffer());
}
clearFinalizedInput(i);
}
}
/// Deletes fields that are no longer neccesary from the psbt.
///
/// Note, the spec doesn't say anything about removing ouput fields
/// like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them
/// without actually knowing why. I think we should remove them too.
void clearFinalizedInput(int inputIndex) {
final keyTypes = [
PSBTIn.bip32Derivation,
PSBTIn.partialSig,
PSBTIn.tapBip32Derivation,
PSBTIn.tapKeySig,
];
final witnessUtxoAvailable = getInputWitnessUtxo(inputIndex) != null;
final nonWitnessUtxoAvailable = getInputNonWitnessUtxo(inputIndex) != null;
if (witnessUtxoAvailable && nonWitnessUtxoAvailable) {
// Remove NON_WITNESS_UTXO for segwit v0 as it's only needed while signing.
// Segwit v1 doesn't have NON_WITNESS_UTXO set.
// See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7
keyTypes.add(PSBTIn.nonWitnessUTXO);
}
deleteInputEntries(inputIndex, keyTypes);
}
/// Writes a script push operation to buf, which looks different
/// depending on the size of the data. See
/// https://en.bitcoin.it/wiki/Script#finalants
///
/// [buf] the BufferWriter to write to
/// [data] the Buffer to be pushed.
void _writePush(BufferWriter buf, Uint8List data) {
if (data.length <= 75) {
buf.writeUInt8(data.length);
} else if (data.length <= 256) {
buf.writeUInt8(76);
buf.writeUInt8(data.length);
} else if (data.length <= 256 * 256) {
buf.writeUInt8(77);
final b = ByteData(2)..setUint16(0, data.length, Endian.little);
buf.writeSlice(b.buffer.asUint8List());
}
buf.writeSlice(data);
}
bool _isFinalized(int i) {
if (getInputFinalScriptsig(i) != null) return true;
try {
getInputFinalScriptwitness(i);
return true;
} catch (_) {
return false;
}
}
}

View file

@ -125,6 +125,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
url: "https://pub.dev"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
@ -377,6 +385,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -395,6 +411,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@ -559,7 +583,7 @@ packages:
dependency: "direct main"
description:
path: "packages/ledger-bitcoin"
ref: HEAD
ref: trunk
resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8
url: "https://github.com/cake-tech/ledger-flutter-plus-plugins"
source: git
@ -726,6 +750,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
payjoin_flutter:
dependency: "direct main"
description:
path: "."
ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c"
resolved-ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c"
url: "https://github.com/konstantinullrich/payjoin-flutter"
source: git
version: "0.21.0"
petitparser:
dependency: transitive
description:
@ -940,6 +973,14 @@ packages:
url: "https://github.com/cake-tech/sp_scanner"
source: git
version: "0.0.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@ -1036,6 +1077,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:
@ -1118,4 +1167,4 @@ packages:
version: "2.2.2"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -40,11 +40,16 @@ dependencies:
bech32:
git:
url: https://github.com/cake-tech/bech32.git
payjoin_flutter:
git:
url: https://github.com/konstantinullrich/payjoin-flutter
ref: 6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c #cake-v1
ledger_flutter_plus: ^1.4.1
ledger_bitcoin:
git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins
path: packages/ledger-bitcoin
ref: trunk
ledger_litecoin:
git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins

View file

@ -21,3 +21,4 @@ const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
const MWEB_UTXO_TYPE_ID = 20;
const HAVEN_SEED_STORE_TYPE_ID = 21;
const ZANO_ASSET_TYPE_ID = 22;
const PAYJOIN_SESSION_TYPE_ID = 23;

View file

@ -0,0 +1,67 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'payjoin_session.g.dart';
@HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
class PayjoinSession extends HiveObject {
PayjoinSession({
required this.walletId,
this.receiver,
this.sender,
this.pjUri,
this.status = "created",
this.inProgressSince,
this.rawAmount,
}) {
if (receiver == null) {
assert(sender != null);
assert(pjUri != null);
} else {
assert(receiver != null);
}
}
static const typeId = PAYJOIN_SESSION_TYPE_ID;
static const boxName = 'PayjoinSessions';
@HiveField(0)
final String walletId;
@HiveField(1)
final String? sender;
@HiveField(2)
final String? receiver;
@HiveField(3)
final String? pjUri;
@HiveField(4)
String status;
@HiveField(5)
DateTime? inProgressSince;
@HiveField(6)
String? txId;
@HiveField(7)
String? rawAmount;
@HiveField(8)
String? error;
bool get isSenderSession => sender != null;
BigInt get amount => BigInt.parse(rawAmount ?? "0");
set amount(BigInt amount) => rawAmount = amount.toString();
}
enum PayjoinSessionStatus {
created,
inProgress,
success,
unrecoverable,
}

View file

@ -810,4 +810,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -849,4 +849,4 @@ packages:
version: "2.2.2"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -978,4 +978,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -946,4 +946,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -845,4 +845,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -842,4 +842,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -109,9 +109,12 @@ class CWBitcoin extends Bitcoin {
required TransactionPriority priority,
int? feeRate,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
String? payjoinUri,
}) {
final bitcoinFeeRate =
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
priority == BitcoinTransactionPriority.custom && feeRate != null
? feeRate
: null;
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
@ -128,7 +131,7 @@ class CWBitcoin extends Bitcoin {
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
payjoinUri: payjoinUri);
}
@override
@ -224,9 +227,14 @@ class CWBitcoin extends Bitcoin {
await bitcoinWallet.updateAllUnspents();
}
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource,
Box<PayjoinSession> payjoinSessionSource,
bool alwaysScan,
bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource,
payjoinSessionSource, alwaysScan, isDirect);
}
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
@ -550,6 +558,10 @@ class CWBitcoin extends Bitcoin {
return option is BitcoinReceivePageOption;
}
@override
bool isPayjoinAvailable(Object wallet) =>
(wallet is BitcoinWallet) && (wallet as BitcoinWallet).isPayjoinAvailable;
@override
BitcoinAddressType getOptionToType(ReceivePageOption option) {
return (option as BitcoinReceivePageOption).toType();
@ -706,4 +718,34 @@ class CWBitcoin extends Bitcoin {
return null;
}
}
@override
String getPayjoinEndpoint(Object wallet) {
final _wallet = wallet as ElectrumWallet;
if (!isPayjoinAvailable(wallet)) return '';
return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? '';
}
@override
void updatePayjoinState(Object wallet, bool value) {
final _wallet = wallet as ElectrumWallet;
if (value) {
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
} else {
stopPayjoinSessions(wallet);
}
}
@override
void resumePayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
}
@override
void stopPayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
(_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions();
(_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
}
}

View file

@ -51,6 +51,7 @@ import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
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';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
@ -286,6 +290,7 @@ late Box<ExchangeTemplate> _exchangeTemplates;
late Box<TransactionDescription> _transactionDescriptionBox;
late Box<Order> _ordersSource;
late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
late Box<PayjoinSession> _payjoinSessionSource;
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
Future<void> setup({
@ -299,6 +304,7 @@ Future<void> setup({
required Box<TransactionDescription> transactionDescriptionBox,
required Box<Order> ordersSource,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource,
required SecureStorage secureStorage,
required GlobalKey<NavigatorState> navigatorKey,
@ -313,6 +319,7 @@ Future<void> setup({
_transactionDescriptionBox = transactionDescriptionBox;
_ordersSource = ordersSource;
_unspentCoinsInfoSource = unspentCoinsInfoSource;
_payjoinSessionSource = payjoinSessionSource;
_anonpayInvoiceInfoSource = anonpayInvoiceInfoSource;
if (!_isSetupFinished) {
@ -354,6 +361,8 @@ Future<void> setup({
TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerSingleton<OrdersStore>(
OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory(() =>
PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
@ -507,6 +516,7 @@ Future<void> setup({
yatStore: getIt.get<YatStore>(),
ordersStore: getIt.get<OrdersStore>(),
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
sharedPreferences: getIt.get<SharedPreferences>(),
keyService: getIt.get<KeyService>()));
@ -1095,6 +1105,7 @@ Future<void> setup({
return bitcoin!.createBitcoinWalletService(
_walletInfoSource,
_unspentCoinsInfoSource,
_payjoinSessionSource,
getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
SettingsStoreBase.walletPasswordDirectInput,
);
@ -1423,6 +1434,15 @@ Future<void> setup({
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<PayjoinDetailsViewModel, String, TransactionInfo?>(
(String sessionId, TransactionInfo? transactionInfo) =>
PayjoinDetailsViewModel(
sessionId,
transactionInfo,
payjoinSessionSource: _payjoinSessionSource,
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<AnonPayReceivePage, AnonpayInfoBase, void>(
(AnonpayInfoBase anonpayInvoiceInfo, _) =>
AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo));
@ -1431,6 +1451,11 @@ Future<void> setup({
(AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(
anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
getIt.registerFactoryParam<PayjoinDetailsPage, String, TransactionInfo?>(
(String sessionId, TransactionInfo? transactionInfo) => PayjoinDetailsPage(
payjoinDetailsViewModel: getIt.get<PayjoinDetailsViewModel>(
param1: sessionId, param2: transactionInfo)));
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));

View file

@ -82,6 +82,7 @@ class PreferencesKey {
static const lookupsOpenAlias = 'looks_up_open_alias';
static const lookupsENS = 'looks_up_ens';
static const lookupsWellKnown = 'looks_up_well_known';
static const usePayjoin = 'use_payjoin';
static const showCameraConsent = 'show_camera_consent';
static const showDecredInfoCard = 'show_decred_info_card';

View file

@ -35,6 +35,7 @@ import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
@ -178,6 +179,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
CakeHive.registerAdapter(MwebUtxoAdapter());
}
if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
CakeHive.registerAdapter(PayjoinSessionAdapter());
}
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@ -197,6 +202,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
final havenSeedStoreBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
@ -219,6 +225,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 49,
@ -241,6 +248,7 @@ Future<void> initialSetup(
required SecureStorage secureStorage,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<HavenSeedStore> havenSeedStore,
int initialMigrationVersion = 15, }) async {
LanguageService.loadLocaleList();
@ -266,6 +274,7 @@ Future<void> initialSetup(
ordersSource: ordersSource,
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
unspentCoinsInfoSource: unspentCoinsInfoSource,
payjoinSessionSource: payjoinSessionSource,
navigatorKey: navigatorKey,
secureStorage: secureStorage,
);

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
@ -78,6 +79,10 @@ void startCurrentWalletChangeReaction(
_setAutoGenerateSubaddressStatus(wallet, settingsStore);
}
if (wallet.type == WalletType.bitcoin) {
bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin);
}
await wallet.connectToNode(node: node);
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
final powNode = settingsStore.getCurrentPowNode(wallet.type);

View file

@ -58,6 +58,7 @@ import 'package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_de
import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
@ -719,6 +720,14 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData));
case Routes.payjoinDetails:
final arguments = settings.arguments as List;
final sessionId = arguments.first as String;
final transactionInfo = arguments[1] as TransactionInfo?;
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<PayjoinDetailsPage>(
param1: sessionId, param2: transactionInfo));
case Routes.desktop_actions:
return PageRouteBuilder(
opaque: false,

View file

@ -15,6 +15,7 @@ class Routes {
static const dashboard = '/dashboard';
static const send = '/send';
static const transactionDetails = '/transaction_info';
static const payjoinDetails = '/transaction_info/payjoin';
static const bumpFeePage = '/bump_fee_page';
static const receive = '/receive';
static const newSubaddress = '/new_subaddress';

View file

@ -1,12 +1,14 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart';
import 'package:cake_wallet/themes/extensions/placeholder_theme.dart';
import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
@ -143,6 +145,25 @@ class TransactionsPage extends StatelessWidget {
);
}
if (item is PayjoinTransactionListItem) {
final session = item.session;
return PayjoinTransactionRow(
key: item.key,
onTap: () => Navigator.of(context).pushNamed(
Routes.payjoinDetails,
arguments: [item.sessionId, item.transaction],
),
currency: "BTC",
state: item.status,
amount: bitcoin!.formatterBitcoinAmountToString(
amount: session.amount.toInt()),
createdAt: DateFormat('HH:mm')
.format(session.inProgressSince!),
isSending: session.isSenderSession,
);
}
if (item is TradeListItem) {
final trade = item.trade;

View file

@ -0,0 +1,99 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:flutter/material.dart';
class PayjoinTransactionRow extends StatelessWidget {
PayjoinTransactionRow({
required this.createdAt,
required this.currency,
required this.onTap,
required this.amount,
required this.state,
required this.isSending,
super.key,
});
final VoidCallback? onTap;
final String createdAt;
final String amount;
final String currency;
final String state;
final bool isSending;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getImage(),
SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"${isSending ? S.current.outgoing : S.current.incoming} Payjoin",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.textColor,
),
),
Text(
amount + ' ' + currency,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.textColor,
),
)
]),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
createdAt,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.dateSectionRowColor,
),
),
Text(
state,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.dateSectionRowColor,
),
),
])
],
))
],
),
));
}
Widget _getImage() => ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/payjoin.png', width: 36, height: 36));
}

View file

@ -0,0 +1,77 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_list_card.dart';
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PayjoinDetailsPage extends BasePage {
PayjoinDetailsPage({required this.payjoinDetailsViewModel});
@override
String get title => S.current.payjoin_details;
final PayjoinDetailsViewModel payjoinDetailsViewModel;
@override
Widget body(BuildContext context) => PayjoinDetailsPageBody(payjoinDetailsViewModel);
}
class PayjoinDetailsPageBody extends StatefulWidget {
PayjoinDetailsPageBody(this.payjoinDetailsViewModel);
final PayjoinDetailsViewModel payjoinDetailsViewModel;
@override
State<PayjoinDetailsPageBody> createState() => _PayjoinDetailsPageBodyState();
}
class _PayjoinDetailsPageBodyState extends State<PayjoinDetailsPageBody> {
@override
void dispose() {
super.dispose();
widget.payjoinDetailsViewModel.listener.cancel();
}
@override
Widget build(BuildContext context) {
return SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => widget.payjoinDetailsViewModel.items.length,
itemBuilder: (__, index) {
final item = widget.payjoinDetailsViewModel.items[index];
if (item is DetailsListStatusItem) {
return StandardListStatusRow(
title: item.title,
value: item.value,
status: item.status,
);
}
if (item is TradeDetailsListCardItem) {
return TradeDetailsStandardListCard(
id: item.id,
create: item.createdAt,
pair: item.pair,
currentTheme: widget.payjoinDetailsViewModel.settingsStore.currentTheme.type,
onTap: item.onTap,
);
}
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: '${item.title}:', value: item.value),
);
});
}
}

View file

@ -1,4 +1,6 @@
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/picker_theme.dart';
import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
import 'package:cake_wallet/routes.dart';
@ -91,26 +93,54 @@ class QRWidget extends StatelessWidget {
child: Hero(
tag: Key(heroTag ?? addressUri.toString()),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
padding: EdgeInsets.zero,
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.textColor,
),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 3,
border: Border(top: BorderSide.none),
borderRadius:
BorderRadius.all(Radius.circular(5)),
color: Colors.white,
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(3),
child: AspectRatio(
aspectRatio: 1.0,
child: QrImage(
data: addressUri.toString(),
),
child: QrImage(data: addressUri.toString())),
),
),
if (addressListViewModel
.payjoinEndpoint.isNotEmpty) ...[
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(
top: 4,
bottom: 4,
right: 4,
),
child: Image.asset(
'assets/images/payjoin.png',
width: 20,
),
),
Text(
S.of(context).payjoin_enabled,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
]
],
),
),
),
@ -179,7 +209,25 @@ class QRWidget extends StatelessWidget {
),
),
),
)
),
if (addressListViewModel.payjoinEndpoint.isNotEmpty) ...[
Padding(
padding: EdgeInsets.only(top: 12),
child: PrimaryImageButton(
onPressed: () {
Clipboard.setData(ClipboardData(
text: addressListViewModel.payjoinEndpoint));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
image: Image.asset('assets/images/payjoin.png', width: 25,),
text: S.of(context).copy_payjoin_url,
color: Theme.of(context).cardColor,
textColor: Theme.of(context)
.extension<CakeTextTheme>()!
.buttonTextColor,
),
),
],
],
),
),

View file

@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/utils/device_info.dart';
@ -136,6 +136,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
setState(() => _setInactive(true));
}
if (widget.appStore.wallet?.type == WalletType.bitcoin) {
bitcoin!.stopPayjoinSessions(widget.appStore.wallet!);
}
break;
case AppLifecycleState.resumed:
widget.authService.requireAuth().then((value) {
@ -145,6 +149,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
});
}
});
if (widget.appStore.wallet?.type == WalletType.bitcoin &&
widget.appStore.settingsStore.usePayjoin) {
bitcoin!.resumePayjoinSessions(widget.appStore.wallet!);
}
break;
default:
break;

View file

@ -463,7 +463,7 @@ class SendPage extends BasePage {
},
);
},
text: S.of(context).send,
text: sendViewModel.payjoinUri != null ? S.of(context).send_payjoin : S.of(context).send,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isLoading: sendViewModel.state is IsExecutingState ||

View file

@ -188,18 +188,22 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
sendViewModel.createOpenCryptoPayTransaction(uri.toString());
} else {
final paymentRequest = PaymentRequest.fromUri(uri);
if (sendViewModel.usePayjoin) {
sendViewModel.payjoinUri = paymentRequest.pjUri;
}
addressController.text = paymentRequest.address;
cryptoAmountController.text = paymentRequest.amount;
noteController.text = paymentRequest.note;
}
noteController.text = paymentRequest.note;}
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
buttonColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintStyle: TextStyle(

View file

@ -49,6 +49,14 @@ class PrivacyPage extends BasePage {
_privacySettingsViewModel.setExchangeApiMode(mode),
),
),
if (_privacySettingsViewModel.canUsePayjoin)
SettingsSwitcherCell(
title: S.of(context).use_payjoin,
value: _privacySettingsViewModel.usePayjoin,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setUsePayjoin(value);
},
),
SettingsSwitcherCell(
title: S.current.settings_save_recipient_address,
value: _privacySettingsViewModel.shouldSaveRecipientAddress,

View file

@ -2,6 +2,8 @@ import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.d
class DetailsListStatusItem extends StandartListItem {
DetailsListStatusItem(
{required String title, required String value})
{required String title, required String value, this.status})
: super(title: title, value: value);
final String? status; // waiting, action required, created, fetching, finished, success
}

View file

@ -1,21 +1,20 @@
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField<T extends Currency> extends StatelessWidget{
class AddressTextField<T extends Currency> extends StatelessWidget {
AddressTextField({
required this.controller,
this.isActive = true,
@ -234,9 +233,7 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
if (!isCameraPermissionGranted) return;
final code = await presentQRScanner(context);
if (code == null) return;
if (code.isEmpty) {
return;
}
if (code.isEmpty) return;
try {
final uri = Uri.parse(code);
@ -259,7 +256,8 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
}
Future<void> _presetWalletAddressPicker(BuildContext context) async {
final address = await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
final address =
await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
if (address is String) {
controller?.text = address;
@ -272,8 +270,14 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
final address = clipboard?.text ?? '';
if (address.isNotEmpty) {
try {
final uri = Uri.parse(address);
controller?.text = uri.path;
onURIScanned?.call(uri);
} catch (_) {
controller?.text = address;
}
}
onPushPasteButton?.call(context);
}

View file

@ -1,5 +1,4 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -7,10 +6,11 @@ import 'package:cake_wallet/themes/extensions/address_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class StandardListStatusRow extends StatelessWidget {
StandardListStatusRow({required this.title, required this.value});
StandardListStatusRow({required this.title, required this.value, this.status});
final String title;
final String value;
final String? status; // waiting, action required, created, fetching, finished, success
@override
Widget build(BuildContext context) {
@ -43,7 +43,7 @@ class StandardListStatusRow extends StatelessWidget {
children: <Widget>[
SyncIndicatorIcon(
boolMode: false,
value: value,
value: status ?? value,
size: 6,
),
SizedBox(

View file

@ -0,0 +1,47 @@
import 'dart:async';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
part 'payjoin_transactions_store.g.dart';
class PayjoinTransactionsStore = PayjoinTransactionsStoreBase
with _$PayjoinTransactionsStore;
abstract class PayjoinTransactionsStoreBase with Store {
PayjoinTransactionsStoreBase({
required this.payjoinSessionSource,
}) : transactions = <PayjoinTransactionListItem>[] {
payjoinSessionSource.watch().listen((_) => updateTransactionList());
updateTransactionList();
}
Box<PayjoinSession> payjoinSessionSource;
@observable
List<PayjoinTransactionListItem> transactions;
@action
Future<void> updateTransactionList() async {
final updatedTransactions = <PayjoinTransactionListItem>[];
payjoinSessionSource.toMap().forEach((dynamic key, PayjoinSession session) {
if ([
PayjoinSessionStatus.inProgress.name,
PayjoinSessionStatus.success.name,
PayjoinSessionStatus.unrecoverable.name
].contains(session.status) &&
session.inProgressSince != null) {
updatedTransactions.add(PayjoinTransactionListItem(
sessionId: key as String,
session: session,
key: ValueKey('payjoin_transaction_list_item_${key}_key'),
));
}
});
transactions = updatedTransactions;
}
}

View file

@ -121,6 +121,7 @@ abstract class SettingsStoreBase with Store {
required this.lookupsOpenAlias,
required this.lookupsENS,
required this.lookupsWellKnown,
required this.usePayjoin,
required this.customBitcoinFeeRate,
required this.silentPaymentsCardDisplay,
required this.silentPaymentsAlwaysScan,
@ -483,6 +484,11 @@ abstract class SettingsStoreBase with Store {
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => usePayjoin,
(bool usePayjoin) =>
_sharedPreferences.setBool(PreferencesKey.usePayjoin, usePayjoin));
// secure storage keys:
reaction(
(_) => allowBiometricalAuthentication,
@ -802,6 +808,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool lookupsWellKnown;
@observable
bool usePayjoin;
@observable
SyncMode currentSyncMode;
@ -1009,6 +1018,7 @@ abstract class SettingsStoreBase with Store {
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
final silentPaymentsCardDisplay =
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
@ -1311,6 +1321,7 @@ abstract class SettingsStoreBase with Store {
lookupsOpenAlias: lookupsOpenAlias,
lookupsENS: lookupsENS,
lookupsWellKnown: lookupsWellKnown,
usePayjoin: usePayjoin,
customBitcoinFeeRate: customBitcoinFeeRate,
silentPaymentsCardDisplay: silentPaymentsCardDisplay,
silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,

View file

@ -1,8 +1,8 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/nano/nano.dart';
class PaymentRequest {
PaymentRequest(this.address, this.amount, this.note, this.scheme, {this.callbackUrl, this.callbackMessage});
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
{this.callbackUrl, this.callbackMessage});
factory PaymentRequest.fromUri(Uri? uri) {
var address = "";
@ -12,8 +12,13 @@ class PaymentRequest {
String? walletType;
String? callbackUrl;
String? callbackMessage;
String? pjUri;
if (uri != null) {
if (uri.queryParameters['pj'] != null) {
pjUri = uri.toString();
}
address = uri.queryParameters['address'] ?? uri.path;
amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? "";
note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? "";
@ -42,6 +47,7 @@ class PaymentRequest {
amount,
note,
scheme,
pjUri,
callbackUrl: callbackUrl,
callbackMessage: callbackMessage,
);
@ -51,6 +57,7 @@ class PaymentRequest {
final String amount;
final String note;
final String scheme;
final String? pjUri;
final String? callbackUrl;
final String? callbackMessage;
}

View file

@ -13,11 +13,11 @@ import 'package:cake_wallet/entities/service_status.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart' as wow;
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/orders_store.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
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';
import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cake_wallet/wownero/wownero.dart' as wow;
import 'package:cryptography/cryptography.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/cake_hive.dart';
@ -70,6 +73,7 @@ abstract class DashboardViewModelBase with Store {
required this.yatStore,
required this.ordersStore,
required this.anonpayTransactionsStore,
required this.payjoinTransactionsStore,
required this.sharedPreferences,
required this.keyService})
: hasTradeAction = true,
@ -408,10 +412,17 @@ abstract class DashboardViewModelBase with Store {
ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
@computed
List<AnonpayTransactionListItem> get anonpayTransactons => anonpayTransactionsStore.transactions
List<AnonpayTransactionListItem> get anonpayTransactions =>
anonpayTransactionsStore.transactions
.where((item) => item.transaction.walletId == wallet.id)
.toList();
@computed
List<PayjoinTransactionListItem> get payjoinTransactions =>
payjoinTransactionsStore.transactions
.where((item) => item.session.walletId == wallet.id)
.toList();
@computed
double get price => balanceViewModel.price;
@ -423,11 +434,27 @@ abstract class DashboardViewModelBase with Store {
List<ActionListItem> get items {
final _items = <ActionListItem>[];
_items.addAll(
transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
_items.addAll(transactionFilterStore
.filtered(transactions: [...transactions, ...anonpayTransactions]));
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
_items.addAll(orders);
if (payjoinTransactions.isNotEmpty) {
final _payjoinTransactions = payjoinTransactions;
_items.forEach((e) {
if (e is TransactionListItem &&
_payjoinTransactions
.any((t) => t.session.txId == e.transaction.id)) {
_payjoinTransactions
.firstWhere((t) => t.session.txId == e.transaction.id)
.transaction = e.transaction;
}
});
_items.addAll(_payjoinTransactions);
_items.removeWhere((e) => (e is TransactionListItem &&
_payjoinTransactions.any((t) => t.session.txId == e.transaction.id)));
}
return formattedItemsList(_items);
}
@ -755,6 +782,8 @@ abstract class DashboardViewModelBase with Store {
TransactionFilterStore transactionFilterStore;
PayjoinTransactionsStore payjoinTransactionsStore;
Map<String, List<FilterItem>> filterItems;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;

View file

@ -0,0 +1,34 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/transaction_info.dart';
class PayjoinTransactionListItem extends ActionListItem {
PayjoinTransactionListItem({
required this.sessionId,
required this.session,
required super.key,
});
final String sessionId;
final PayjoinSession session;
TransactionInfo? transaction;
@override
DateTime get date => session.inProgressSince!;
String get status {
switch (session.status) {
case 'success':
if (transaction?.isPending == true)
return S.current.payjoin_request_awaiting_tx;
return S.current.successful;
case 'inProgress':
return S.current.payjoin_request_in_progress;
case 'unrecoverable':
return S.current.error;
default:
return session.status;
}
}
}

View file

@ -0,0 +1,122 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:mobx/mobx.dart';
part 'payjoin_details_view_model.g.dart';
class PayjoinDetailsViewModel = PayjoinDetailsViewModelBase
with _$PayjoinDetailsViewModel;
abstract class PayjoinDetailsViewModelBase with Store {
PayjoinDetailsViewModelBase(
this.payjoinSessionId,
this.transactionInfo, {
required this.payjoinSessionSource,
required this.settingsStore,
}) : items = ObservableList<StandartListItem>(),
payjoinSession = payjoinSessionSource.get(payjoinSessionId)! {
listener = payjoinSessionSource.watch().listen((e) {
if (e.key == payjoinSessionId) _updateItems();
});
_updateItems();
}
final Box<PayjoinSession> payjoinSessionSource;
final SettingsStore settingsStore;
final String payjoinSessionId;
final TransactionInfo? transactionInfo;
@observable
late PayjoinSession payjoinSession;
final ObservableList<StandartListItem> items;
late final StreamSubscription<BoxEvent> listener;
Timer? timer;
@action
void _updateItems() {
final dateFormat = DateFormatter.withCurrentLocal();
items.clear();
items.addAll([
DetailsListStatusItem(
title: S.current.status,
value: _getStatusString(),
status: payjoinSession.status,
),
TradeDetailsListCardItem(
id: "${payjoinSession.isSenderSession ? S.current.outgoing : S.current.incoming} Payjoin",
createdAt:
dateFormat.format(payjoinSession.inProgressSince!).toString(),
pair:
'${bitcoin!.formatterBitcoinAmountToString(amount: payjoinSession.amount.toInt())} BTC',
onTap: (_) {},
),
if (payjoinSession.error?.isNotEmpty == true)
StandartListItem(
title: S.current.error,
value: payjoinSession.error!,
),
if (payjoinSession.txId?.isNotEmpty == true)
StandartListItem(
title: S.current.transaction_details_transaction_id,
value: payjoinSession.txId!,
key: ValueKey('standard_list_item_transaction_details_id_key'),
)
]);
if (transactionInfo != null) {
items.addAll([
StandartListItem(
title: S.current.transaction_details_date,
value: dateFormat.format(transactionInfo!.date),
key: ValueKey('standard_list_item_transaction_details_date_key'),
),
StandartListItem(
title: S.current.confirmations,
value: transactionInfo!.confirmations.toString(),
key: ValueKey('standard_list_item_transaction_confirmations_key'),
),
StandartListItem(
title: S.current.transaction_details_height,
value: '${transactionInfo!.height}',
key: ValueKey('standard_list_item_transaction_details_height_key'),
),
if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(
title: S.current.transaction_details_fee,
value: transactionInfo!.feeFormatted()!,
key: ValueKey('standard_list_item_transaction_details_fee_key'),
),
]);
}
}
String _getStatusString() {
switch (payjoinSession.status) {
case 'success':
if (transactionInfo?.isPending == true)
return S.current.payjoin_request_awaiting_tx;
return S.current.successful;
case 'inProgress':
return S.current.payjoin_request_in_progress;
case 'unrecoverable':
return S.current.error;
default:
return payjoinSession.status;
}
}
}

View file

@ -619,6 +619,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
priority: priority!,
feeRate: feesViewModel.customBitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
payjoinUri: _settingsStore.usePayjoin ? payjoinUri : null,
);
case WalletType.litecoin:
return bitcoin!.createBitcoinTransactionCredentials(
@ -855,4 +856,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return false;
}
@computed
bool get usePayjoin => _settingsStore.usePayjoin;
@observable
String? payjoinUri;
}

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
@ -32,20 +33,19 @@ abstract class PrivacySettingsViewModelBase with Store {
@action
void setAutoGenerateSubaddresses(bool value) {
_wallet.isEnabledAutoGenerateSubaddress = value;
if (value) {
_settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled;
} else {
_settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled;
}
_settingsStore.autoGenerateSubaddressStatus = value
? AutoGenerateSubaddressStatus.enabled
: AutoGenerateSubaddressStatus.disabled;
}
bool get isAutoGenerateSubaddressesVisible =>
_wallet.type == WalletType.monero ||
_wallet.type == WalletType.wownero ||
_wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash ||
_wallet.type == WalletType.decred;
bool get isAutoGenerateSubaddressesVisible => [
WalletType.monero,
WalletType.wownero,
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.decred
].contains(_wallet.type);
bool get isMoneroWallet => _wallet.type == WalletType.monero;
@ -100,6 +100,9 @@ abstract class PrivacySettingsViewModelBase with Store {
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
@computed
bool get usePayjoin => _settingsStore.usePayjoin;
bool get canUseEtherscan => _wallet.type == WalletType.ethereum;
bool get canUsePolygonScan => _wallet.type == WalletType.polygon;
@ -108,6 +111,8 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin;
bool get canUsePayjoin => _wallet.type == WalletType.bitcoin;
@action
void setShouldSaveRecipientAddress(bool value) =>
_settingsStore.shouldSaveRecipientAddress = value;
@ -170,7 +175,12 @@ abstract class PrivacySettingsViewModelBase with Store {
}
@action
void setUseMempoolFeeAPI(bool value) {
void setUseMempoolFeeAPI(bool value) =>
_settingsStore.useMempoolFeeAPI = value;
@action
void setUsePayjoin(bool value) {
_settingsStore.usePayjoin = value;
bitcoin!.updatePayjoinState(_wallet, value);
}
}

View file

@ -1,4 +1,5 @@
import 'dart:developer' as dev;
import 'dart:core';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
@ -72,17 +73,21 @@ class HavenURI extends PaymentURI {
}
class BitcoinURI extends PaymentURI {
BitcoinURI({required super.amount, required super.address});
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
final String pjUri;
@override
String toString() {
var base = 'bitcoin:$address';
final qp = <String, String>{};
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
if (pjUri.isNotEmpty) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
}
return base;
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
}
}
@ -300,6 +305,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
WalletAddressListItem get address =>
WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false);
@computed
String get payjoinEndpoint => wallet.type == WalletType.bitcoin
? bitcoin!.getPayjoinEndpoint(wallet)
: "";
@computed
PaymentURI get uri {
switch (wallet.type) {
@ -308,7 +318,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
case WalletType.haven:
return HavenURI(amount: amount, address: address.address);
case WalletType.bitcoin:
return BitcoinURI(amount: amount, address: address.address);
return BitcoinURI(
amount: amount,
address: address.address,
pjUri: payjoinEndpoint);
case WalletType.litecoin:
return LitecoinURI(amount: amount, address: address.address);
case WalletType.ethereum:

View file

@ -202,6 +202,7 @@
"copy": "نسخ",
"copy_address": "نسخ العنوان",
"copy_id": "نسخ معرف العملية",
"copy_payjoin_url": "نسخ Payjoin url",
"copyWalletConnectLink": "ﺎﻨﻫ ﻪﻘﺼﻟﺍﻭ dApp ﻦﻣ WalletConnect ﻂﺑﺍﺭ ﺦﺴﻧﺍ",
"corrupted_seed_notice": "تالف ملفات هذه المحفظة ولا يمكن فتحها. يرجى الاطلاع على عبارة البذور وحفظها واستعادة المحفظة.\n\nإذا كانت القيمة فارغة ، لم تتمكن البذور من استردادها بشكل صحيح.",
"countries": "بلدان",
@ -546,6 +547,10 @@
"password": "كلمة المرور",
"paste": "لصق",
"pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ",
"payjoin_details": "Payjoin تفاصيل",
"payjoin_enabled": "Payjoin تمكين",
"payjoin_request_awaiting_tx": "في انتظار المعاملة",
"payjoin_request_in_progress": "في تَقَدم",
"payment_id": "معرف الدفع:",
"payment_was_received": "تم استلام الدفع الخاص بك.",
"pending": " (في الإنتظار)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "أرسل من محفظة خارجية",
"send_name": "الأسم",
"send_new": "جديد",
"send_payjoin": "يرسل Payjoin",
"send_payment_id": "معرف عملية الدفع (اختياري)",
"send_priority": "حاليًا ، تم تحديد الرسوم بأولوية ${transactionPriority}.\nيمكن تعديل أولوية المعاملة في الإعدادات",
"send_sending": "يتم الإرسال...",
@ -973,6 +979,7 @@
"use": "التبديل إلى",
"use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.",
"use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.",
"use_payjoin": "يستخدم Payjoin",
"use_ssl": "استخدم SSL",
"use_suggested": "استخدام المقترح",
"use_testnet": "استخدم testnet",

View file

@ -202,6 +202,7 @@
"copy": "Копиране",
"copy_address": "Copy Address",
"copy_id": "Копиране на ID",
"copy_payjoin_url": "Копиране Payjoin url",
"copyWalletConnectLink": "Копирайте връзката WalletConnect от dApp и я поставете тук",
"corrupted_seed_notice": "Файловете за този портфейл са повредени и не могат да бъдат отворени. Моля, прегледайте фразата за семена, запазете я и възстановете портфейла.\n\nАко стойността е празна, тогава семето не успя да бъде правилно възстановено.",
"countries": "Държави",
@ -546,6 +547,10 @@
"password": "Парола",
"paste": "Поставяне",
"pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.",
"payjoin_details": "Payjoin подробности",
"payjoin_enabled": "Payjoin enabled",
"payjoin_request_awaiting_tx": "В очакване на транзакция",
"payjoin_request_in_progress": "В ход",
"payment_id": "Payment ID: ",
"payment_was_received": "Плащането бе получено.",
"pending": " (чакащи)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Изпратете от външен портфейл",
"send_name": "Име",
"send_new": "Ново",
"send_payjoin": "Изпратете Payjoin",
"send_payment_id": "Payment ID (не е задължително)",
"send_priority": "В момента таксата е на ${transactionPriority} приоритетност.\nПриоритетността на транзакцията може да бъде променена в настройките",
"send_sending": "Изпращане...",
@ -973,6 +979,7 @@
"use": "Смяна на ",
"use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.",
"use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.",
"use_payjoin": "Използвайте Payjoin",
"use_ssl": "Използване на SSL",
"use_suggested": "Използване на предложеното",
"use_testnet": "Използвайте TestNet",

View file

@ -202,6 +202,7 @@
"copy": "Kopírovat",
"copy_address": "Zkopírovat adresu",
"copy_id": "Kopírovat ID",
"copy_payjoin_url": "Kopírovat Payjoin URL",
"copyWalletConnectLink": "Zkopírujte odkaz WalletConnect z dApp a vložte jej sem",
"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.",
"countries": "Země",
@ -546,6 +547,10 @@
"password": "Heslo",
"paste": "Vložit",
"pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.",
"payjoin_details": "%%TE podrobnosti",
"payjoin_enabled": "Payjoin povoleno",
"payjoin_request_awaiting_tx": "Čeká na transakci",
"payjoin_request_in_progress": "Probíhá",
"payment_id": "ID platby: ",
"payment_was_received": "Vaše platba byla přijata.",
"pending": " (čeká)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Odeslat z externí peněženky",
"send_name": "Název",
"send_new": "Nová",
"send_payjoin": "Odeslat Payjoin",
"send_payment_id": "ID platby (nepovinné)",
"send_priority": "Momentálně je poplatek nastaven na prioritu: ${transactionPriority}.\nPriorita transakce může být upravena v nastavení.",
"send_sending": "Odesílání...",
@ -973,6 +979,7 @@
"use": "Přepnout na ",
"use_card_info_three": "Použijte tuto digitální kartu online nebo bezkontaktními platebními metodami.",
"use_card_info_two": "Prostředky jsou převedeny na USD, když jsou drženy na předplaceném účtu, nikoliv na digitální měnu.",
"use_payjoin": "Použijte %%TE",
"use_ssl": "Použít SSL",
"use_suggested": "Použít doporučený",
"use_testnet": "Použijte testNet",

View file

@ -202,6 +202,7 @@
"copy": "Kopieren",
"copy_address": "Adresse kopieren",
"copy_id": "ID kopieren",
"copy_payjoin_url": "Payjoin URL kopieren",
"copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein",
"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.",
"countries": "Länder",
@ -546,6 +547,10 @@
"password": "Passwort",
"paste": "Einfügen",
"pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.",
"payjoin_details": "Payjoin Details",
"payjoin_enabled": "Payjoin aktiv",
"payjoin_request_awaiting_tx": "Warten auf die Transaktion",
"payjoin_request_in_progress": "Im Gange",
"payment_id": "Zahlungs-ID: ",
"payment_was_received": "Ihre Zahlung ist eingegangen.",
"pending": " (ausstehend)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Senden Sie aus der Außenschreibe",
"send_name": "Name",
"send_new": "Neu",
"send_payjoin": "Payjoin senden",
"send_payment_id": "Zahlungs-ID (optional)",
"send_priority": "Derzeit ist ${transactionPriority} als Gebührenpriorität eingestellt.\nDie Transaktionspriorität kann in den Einstellungen angepasst werden",
"send_sending": "Senden...",
@ -975,6 +981,7 @@
"use": "Wechsel zu ",
"use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.",
"use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.",
"use_payjoin": "Benutze Payjoin",
"use_ssl": "SSL verwenden",
"use_suggested": "Vorgeschlagen verwenden",
"use_testnet": "TESTNET verwenden",

View file

@ -202,6 +202,7 @@
"copy": "Copy",
"copy_address": "Copy Address",
"copy_id": "Copy ID",
"copy_payjoin_url": "Copy Payjoin URL",
"copyWalletConnectLink": "Copy the WalletConnect link from dApp and paste here",
"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.",
"countries": "Countries",
@ -547,6 +548,10 @@
"password": "Password",
"paste": "Paste",
"pause_wallet_creation": "Ability to create Haven Wallet is currently paused.",
"payjoin_details": "Payjoin details",
"payjoin_enabled": "Payjoin enabled",
"payjoin_request_awaiting_tx": "Awaiting Transaction",
"payjoin_request_in_progress": "In Progress",
"payment_id": "Payment ID: ",
"payment_was_received": "Your payment was received.",
"pending": " (pending)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Send from External Wallet",
"send_name": "Name",
"send_new": "New",
"send_payjoin": "Send Payjoin",
"send_payment_id": "Payment ID (optional)",
"send_priority": "Currently the fee is set at ${transactionPriority} priority.\nTransaction priority can be adjusted in the settings",
"send_sending": "Sending...",
@ -974,6 +980,7 @@
"use": "Switch to ",
"use_card_info_three": "Use the digital card online or with contactless payment methods.",
"use_card_info_two": "Funds are converted to USD when they're held in the prepaid account, not in digital currencies.",
"use_payjoin": "Use Payjoin",
"use_ssl": "Use SSL",
"use_suggested": "Use Suggested",
"use_testnet": "Use Testnet",

View file

@ -202,6 +202,7 @@
"copy": "Dupdo",
"copy_address": "Copiar dirección ",
"copy_id": "Copiar ID",
"copy_payjoin_url": "Copiar Payjoin url",
"copyWalletConnectLink": "Copie el enlace de WalletConnect de dApp y péguelo aquí",
"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.",
"countries": "Países",
@ -546,6 +547,10 @@
"password": "Contraseña",
"paste": "Pegar",
"pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.",
"payjoin_details": "Payjoin detalles",
"payjoin_enabled": "Payjoin activado",
"payjoin_request_awaiting_tx": "Esperando transacción",
"payjoin_request_in_progress": "En curso",
"payment_id": "ID de pago: ",
"payment_was_received": "Su pago fue recibido.",
"pending": " (pendiente)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Enviar desde la billetera externa",
"send_name": "Nombre",
"send_new": "Nuevo",
"send_payjoin": "Enviar Payjoin",
"send_payment_id": "ID de pago (opcional)",
"send_priority": "Actualmente la tarifa se establece en ${transactionPriority} prioridad.\nLa prioridad de la transacción se puede ajustar en la configuración",
"send_sending": "Enviando...",
@ -974,6 +980,7 @@
"use": "Utilizar a ",
"use_card_info_three": "Utiliza la tarjeta digital en línea o con métodos de pago sin contacto.",
"use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.",
"use_payjoin": "Usar Payjoin",
"use_ssl": "Utiliza SSL",
"use_suggested": "Usar sugerido",
"use_testnet": "Usar TestNet",

View file

@ -202,6 +202,7 @@
"copy": "Copier",
"copy_address": "Copier l'Adresse",
"copy_id": "Copier l'ID",
"copy_payjoin_url": "Copie Payjoin URL",
"copyWalletConnectLink": "Copiez le lien WalletConnect depuis l'application décentralisée (dApp) et collez-le ici",
"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.",
"countries": "Pays",
@ -546,6 +547,10 @@
"password": "Mot de passe",
"paste": "Coller",
"pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.",
"payjoin_details": "Payjoin détails",
"payjoin_enabled": "Payjoin activé",
"payjoin_request_awaiting_tx": "En attente de transaction",
"payjoin_request_in_progress": "En cours",
"payment_id": "ID de Paiement : ",
"payment_was_received": "Votre paiement a été reçu.",
"pending": " (en attente)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Envoyer du portefeuille externe",
"send_name": "Nom",
"send_new": "Nouveau",
"send_payjoin": "Envoyer Payjoin",
"send_payment_id": "ID de paiement (optionnel)",
"send_priority": "Actuellement les frais sont positionnés à la priorité ${transactionPriority}.\nLa priorité de la transaction peut être modifiée dans les réglages",
"send_sending": "Envoi...",
@ -973,6 +979,7 @@
"use": "Changer vers code PIN à ",
"use_card_info_three": "Utilisez la carte numérique en ligne ou avec des méthodes de paiement sans contact.",
"use_card_info_two": "Les fonds sont convertis en USD lorsqu'ils sont détenus sur le compte prépayé, et non en devises numériques.",
"use_payjoin": "Utiliser Payjoin",
"use_ssl": "Utiliser SSL",
"use_suggested": "Suivre la suggestion",
"use_testnet": "Utiliser TestNet",

View file

@ -202,6 +202,7 @@
"copy": "Kwafi",
"copy_address": "Kwafi Adireshin",
"copy_id": "Kwafi ID",
"copy_payjoin_url": "Kwafa Payjoin url",
"copyWalletConnectLink": "Kwafi hanyar haɗin WalletConnect daga dApp kuma liƙa a nan",
"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.",
"countries": "Kasashe",
@ -548,6 +549,10 @@
"password": "Kalmar wucewa",
"paste": "Manna",
"pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.",
"payjoin_details": "Payjoin LIT LOCEC LOcciya",
"payjoin_enabled": "Payjoin An kunna",
"payjoin_request_awaiting_tx": "Jiran ma'amala",
"payjoin_request_in_progress": "Ana kai",
"payment_id": "ID na biyan kuɗi:",
"payment_was_received": "An karɓi kuɗin ku.",
"pending": "(pending)",
@ -737,6 +742,7 @@
"send_from_external_wallet": "Aika daga walat na waje",
"send_name": "Sunan",
"send_new": "Sabon",
"send_payjoin": "Aika Payjoin",
"send_payment_id": "ID na biyan kuɗi (optional)",
"send_priority": "Yanzu haka fee yana set a ${transactionPriority} fifiko.\nAna iya daidaita fifikon ciniki a cikin saitunan",
"send_sending": "Aika...",
@ -975,6 +981,7 @@
"use": "Canja zuwa",
"use_card_info_three": "Yi amfani da katin dijital akan layi ko tare da hanyoyin biyan kuɗi mara lamba.",
"use_card_info_two": "Ana canza kuɗi zuwa dalar Amurka lokacin da ake riƙe su a cikin asusun da aka riga aka biya, ba cikin agogon dijital ba.",
"use_payjoin": "Yi amfani da Payjoin",
"use_ssl": "Yi amfani da SSL",
"use_suggested": "Amfani da Shawarwari",
"use_testnet": "Amfani da gwaji",

View file

@ -202,6 +202,7 @@
"copy": "प्रतिलिपि",
"copy_address": "पता कॉपी करें",
"copy_id": "प्रतिलिपि ID",
"copy_payjoin_url": "कॉपी Payjoin url",
"copyWalletConnectLink": "dApp से वॉलेटकनेक्ट लिंक को कॉपी करें और यहां पेस्ट करें",
"corrupted_seed_notice": "इस वॉलेट की फाइलें दूषित हैं और उन्हें खोलने में असमर्थ हैं। कृपया बीज वाक्यांश देखें, इसे बचाएं, और बटुए को पुनर्स्थापित करें।\n\nयदि मूल्य खाली है, तो बीज सही ढंग से पुनर्प्राप्त करने में असमर्थ था।",
"countries": "देशों",
@ -546,6 +547,10 @@
"password": "पारण शब्द",
"paste": "पेस्ट करें",
"pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।",
"payjoin_details": "Payjoin विवरण",
"payjoin_enabled": "Payjoin सक्षम",
"payjoin_request_awaiting_tx": "लेन -देन का इंतजार",
"payjoin_request_in_progress": "प्रगति पर है",
"payment_id": "भुगतान ID: ",
"Payment_was_received": "आपका भुगतान प्राप्त हो गया था।",
"payment_was_received": "आपका भुगतान प्राप्त हुआ था।",
@ -737,6 +742,7 @@
"send_from_external_wallet": "बाहरी बटुए से भेजें",
"send_name": "नाम",
"send_new": "नया",
"send_payjoin": "भेजना Payjoin",
"send_payment_id": "भुगतान ID (ऐच्छिक)",
"send_priority": "वर्तमान में शुल्क निर्धारित है ${transactionPriority} प्राथमिकता.\nलेन-देन की प्राथमिकता को सेटिंग्स में समायोजित किया जा सकता है",
"send_sending": "भेजना...",
@ -975,6 +981,7 @@
"use": "उपयोग ",
"use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।",
"use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।",
"use_payjoin": "उपयोग Payjoin",
"use_ssl": "उपयोग SSL",
"use_suggested": "सुझाए गए का प्रयोग करें",
"use_testnet": "टेस्टनेट का उपयोग करें",

View file

@ -202,6 +202,7 @@
"copy": "Kopiraj",
"copy_address": "Kopiraj adresu",
"copy_id": "Kopirati ID",
"copy_payjoin_url": "Kopirajte Payjoin url",
"copyWalletConnectLink": "Kopirajte vezu WalletConnect iz dApp-a i zalijepite je ovdje",
"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.",
"countries": "Zemalja",
@ -546,6 +547,10 @@
"password": "Lozinka",
"paste": "Zalijepi",
"pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.",
"payjoin_details": "Payjoin Pojedinosti",
"payjoin_enabled": "Payjoin Omogućeno",
"payjoin_request_awaiting_tx": "Čekajući transakciju",
"payjoin_request_in_progress": "U toku",
"payment_id": "ID plaćanja: ",
"payment_was_received": "Vaša uplata je primljena.",
"pending": " (u tijeku)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Pošaljite iz vanjskog novčanika",
"send_name": "Ime",
"send_new": "Novi",
"send_payjoin": "Pošaljite Payjoin",
"send_payment_id": "ID plaćanja (nije obvezno)",
"send_priority": "Trenutno se naknada nalazi na ${transactionPriority} mjestu prioriteta.\nPrioritet transakcije moguće je prilagoditi u postavkama",
"send_sending": "Slanje...",
@ -973,6 +979,7 @@
"use": "Prebaci na",
"use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.",
"use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.",
"use_payjoin": "Koristite Payjoin",
"use_ssl": "Koristi SSL",
"use_suggested": "Koristite predloženo",
"use_testnet": "Koristite TestNet",

View file

@ -202,6 +202,7 @@
"copy": "Պատճենել",
"copy_address": "Պատճենել հասցեն",
"copy_id": "Պատճենել ID",
"copy_payjoin_url": "Պատճենել Payjoin url",
"copyWalletConnectLink": "Պատճենել WalletConnect հղումը dApp-ից և տեղադրել այստեղ",
"corrupted_seed_notice": "Այս դրամապանակի համար ֆայլերը կոռումպացված են եւ չեն կարողանում բացվել: Խնդրում ենք դիտել սերմերի արտահայտությունը, պահպանել այն եւ վերականգնել դրամապանակը:\n\nԵթե ​​արժեքը դատարկ է, ապա սերմը չկարողացավ ճիշտ վերականգնվել:",
"countries": "Երկրներ",
@ -545,6 +546,10 @@
"password": "Գաղտնաբառ",
"paste": "Տեղադրել",
"pause_wallet_creation": "Հնարավորություն ստեղծել Haven Դրամապանակ ընթացիկ դադարեցված է",
"payjoin_details": "Payjoin Մանրամասն",
"payjoin_enabled": "Payjoin միացված",
"payjoin_request_awaiting_tx": "Սպասում է գործարքին",
"payjoin_request_in_progress": "Ընթացքի մեջ",
"payment_id": "Վճարման հերթական համար",
"payment_was_received": "Վճարումը ստացված է",
"pending": " (մշակվում է)",
@ -733,6 +738,7 @@
"send_from_external_wallet": "Ուղարկել արտաքին դրամապանակից",
"send_name": "Անվանում",
"send_new": "Նոր",
"send_payjoin": "Ուղարկել Payjoin",
"send_payment_id": "Վճարման ID (կամավոր)",
"send_priority": "Ներկայումս վարձը սահմանված է ${transactionPriority} առաջնահերթությամբ։ Գործարքի առաջնահերթությունը կարող է կարգավորվել կարգավորումներում",
"send_sending": "Ուղարկվում է...",
@ -971,6 +977,7 @@
"use": "Փոխեք ",
"use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։",
"use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։",
"use_payjoin": "Օգտագործեք Payjoin",
"use_ssl": "Օգտագործել SSL",
"use_suggested": "Օգտագործել առաջարկվածը",
"use_testnet": "Օգտագործել Testnet",

View file

@ -202,6 +202,7 @@
"copy": "Salin",
"copy_address": "Salin Alamat",
"copy_id": "Salin ID",
"copy_payjoin_url": "Salin Payjoin url",
"copyWalletConnectLink": "Salin tautan WalletConnect dari dApp dan tempel di sini",
"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.",
"countries": "Negara",
@ -548,6 +549,10 @@
"password": "Kata Sandi",
"paste": "Tempel",
"pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.",
"payjoin_details": "Payjoin detail",
"payjoin_enabled": "Payjoin diaktifkan",
"payjoin_request_awaiting_tx": "Menunggu transaksi",
"payjoin_request_in_progress": "Sedang berlangsung",
"payment_id": "ID Pembayaran: ",
"payment_was_received": "Pembayaran Anda telah diterima.",
"pending": " (pending)",
@ -738,6 +743,7 @@
"send_from_external_wallet": "Kirim dari dompet eksternal",
"send_name": "Nama",
"send_new": "Baru",
"send_payjoin": "Mengirim Payjoin",
"send_payment_id": "ID Pembayaran (opsional)",
"send_priority": "Saat ini biaya diatur dengan prioritas ${transactionPriority}.\nPrioritas transaksi dapat diubah pada pengaturan",
"send_sending": "Mengirim...",
@ -976,6 +982,7 @@
"use": "Beralih ke ",
"use_card_info_three": "Gunakan kartu digital secara online atau dengan metode pembayaran tanpa kontak.",
"use_card_info_two": "Dana dikonversi ke USD ketika disimpan dalam akun pra-bayar, bukan dalam mata uang digital.",
"use_payjoin": "Menggunakan Payjoin",
"use_ssl": "Gunakan SSL",
"use_suggested": "Gunakan yang Disarankan",
"use_testnet": "Gunakan TestNet",

View file

@ -202,6 +202,7 @@
"copy": "Copia",
"copy_address": "Copia Indirizzo",
"copy_id": "Copia ID",
"copy_payjoin_url": "Copia Payjoin url",
"copyWalletConnectLink": "Copia il collegamento WalletConnect dalla dApp e incollalo qui",
"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.",
"countries": "Paesi",
@ -547,6 +548,10 @@
"password": "Password",
"paste": "Incolla",
"pause_wallet_creation": "La possibilità di creare Wallet Haven è attualmente sospesa.",
"payjoin_details": "Payjoin dettagli",
"payjoin_enabled": "Payjoin abilitato",
"payjoin_request_awaiting_tx": "In attesa di transazione",
"payjoin_request_in_progress": "In corso",
"payment_id": "ID Pagamento: ",
"payment_was_received": "Il tuo pagamento è stato ricevuto.",
"pending": " (non confermati)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Invia dal portafoglio esterno",
"send_name": "Nome",
"send_new": "Nuovo",
"send_payjoin": "Inviare Payjoin",
"send_payment_id": "ID Pagamento (opzionale)",
"send_priority": "Attualmente la commissione è impostata a priorità ${transactionPriority} .\nLa priorità della transazione può essere modificata nelle impostazioni",
"send_sending": "Invio...",
@ -974,6 +980,7 @@
"use": "Passa a ",
"use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.",
"use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.",
"use_payjoin": "Utilizzo Payjoin",
"use_ssl": "Usa SSL",
"use_suggested": "Usa suggerito",
"use_testnet": "Usa TestNet",

View file

@ -202,6 +202,7 @@
"copy": "コピー",
"copy_address": "住所をコピー",
"copy_id": "IDをコピー",
"copy_payjoin_url": "Payjoin urlをコピーします",
"copyWalletConnectLink": "dApp から WalletConnect リンクをコピーし、ここに貼り付けます",
"corrupted_seed_notice": "このウォレットのファイルは破損しており、開くことができません。シードフレーズを表示し、保存し、財布を復元してください。\n\n値が空の場合、種子を正しく回復することができませんでした。",
"countries": "国",
@ -547,6 +548,10 @@
"password": "パスワード",
"paste": "ペースト",
"pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。",
"payjoin_details": "Payjoin 詳細",
"payjoin_enabled": "Payjoin enabled",
"payjoin_request_awaiting_tx": "トランザクションを待っています",
"payjoin_request_in_progress": "進行中",
"payment_id": "支払いID: ",
"payment_was_received": "お支払いを受け取りました。",
"pending": " (保留中)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "外部ウォレットから送信します",
"send_name": "名前",
"send_new": "新着",
"send_payjoin": "送信 Payjoin",
"send_payment_id": "支払いID (オプショナル)",
"send_priority": "現在、料金は ${transactionPriority} 優先度.\nトランザクションの優先度は設定で調整できます",
"send_sending": "送信...",
@ -974,6 +980,7 @@
"use": "使用する ",
"use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。",
"use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。",
"use_payjoin": "使用 Payjoin",
"use_ssl": "SSLを使用する",
"use_suggested": "推奨を使用",
"use_testnet": "テストネットを使用します",

View file

@ -202,6 +202,7 @@
"copy": "복사",
"copy_address": "주소 복사",
"copy_id": "ID 복사",
"copy_payjoin_url": "Payjoin url을 복사하십시오",
"copyWalletConnectLink": "dApp에서 WalletConnect 링크를 복사하여 여기에 붙여넣으세요",
"corrupted_seed_notice": "이 지갑의 파일이 손상되어 열 수 없습니다. 시드 구문을 보고 저장한 다음 지갑을 복구하세요.\n\n값이 비어 있으면 시드를 올바르게 복구할 수 없었습니다.",
"countries": "국가",
@ -547,6 +548,10 @@
"password": "비밀번호",
"paste": "붙여넣기",
"pause_wallet_creation": "현재 Haven 지갑 생성 기능이 일시 중지되었습니다.",
"payjoin_details": "Payjoin 세부 정보",
"payjoin_enabled": "Payjoin enabled",
"payjoin_request_awaiting_tx": "거래를 기다리고 있습니다",
"payjoin_request_in_progress": "진행 중",
"payment_id": "결제 ID: ",
"payment_was_received": "결제가 접수되었습니다.",
"pending": " (대기 중)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "외부 지갑에서 보내기",
"send_name": "이름",
"send_new": "새로 만들기",
"send_payjoin": "보내다 Payjoin",
"send_payment_id": "결제 ID (선택 사항)",
"send_priority": "현재 수수료는 ${transactionPriority} 우선순위로 설정되어 있습니다.\n트랜잭션 우선순위는 설정에서 조정할 수 있습니다.",
"send_sending": "보내는 중...",
@ -974,6 +980,7 @@
"use": "다음으로 전환 ",
"use_card_info_three": "디지털 카드를 온라인 또는 비접촉 결제 방법으로 사용하세요.",
"use_card_info_two": "자금은 디지털 통화가 아닌 선불 계정에 보관될 때 USD로 변환됩니다.",
"use_payjoin": "사용 Payjoin",
"use_ssl": "SSL 사용",
"use_suggested": "제안 사용",
"use_testnet": "테스트넷 사용",

View file

@ -202,6 +202,7 @@
"copy": "ကော်ပီ",
"copy_address": "လိပ်စာကို ကူးယူပါ။",
"copy_id": "ID ကူးယူပါ။",
"copy_payjoin_url": "Payjoin URL ကိုကူးယူပါ",
"copyWalletConnectLink": "dApp မှ WalletConnect လင့်ခ်ကို ကူးယူပြီး ဤနေရာတွင် ကူးထည့်ပါ။",
"corrupted_seed_notice": "ဤပိုက်ဆံအိတ်အတွက်ဖိုင်များသည်အကျင့်ပျက်ခြစားမှုများနှင့်မဖွင့်နိုင်ပါ။ ကျေးဇူးပြု. မျိုးစေ့များကိုကြည့်ပါ, ၎င်းကိုသိမ်းဆည်းပါ, ပိုက်ဆံအိတ်ကိုပြန်ယူပါ။\n\nအကယ်. တန်ဖိုးသည်အချည်းနှီးဖြစ်ပါကမျိုးစေ့ကိုမှန်ကန်စွာပြန်လည်ကောင်းမွန်မရရှိနိုင်ပါ။",
"countries": "နိုင်ငံများ",
@ -546,6 +547,10 @@
"password": "စကားဝှက်",
"paste": "ငါးပိ",
"pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။",
"payjoin_details": "Payjoin အသေးစိတ်အချက်အလက်များ %% အသေးစိတ်အချက်အလက်များ",
"payjoin_enabled": "Payjoin enabled",
"payjoin_request_awaiting_tx": "ငွေပေးငွေယူစောင့်ဆိုင်း",
"payjoin_request_in_progress": "ဆောင်ရွက်ဆဲဖြစ်သည်",
"payment_id": "ငွေပေးချေမှု ID:",
"payment_was_received": "သင့်ငွေပေးချေမှုကို လက်ခံရရှိခဲ့သည်။",
"pending": " (ဆိုင်းငံ့)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "ပြင်ပပိုက်ဆံအိတ်မှပေးပို့ပါ",
"send_name": "နာမည်",
"send_new": "အသစ်",
"send_payjoin": "Payjoin ကိုပို့ပါ",
"send_payment_id": "ငွေပေးချေမှု ID (ချန်လှပ်ထား)",
"send_priority": "လောလောဆယ်အခကြေးငွေကို ${transactionPriority} ဦးစားပေးတွင် သတ်မှတ်ထားပါသည်။\nငွေပေးငွေယူဦးစားပေးကို ဆက်တင်များတွင် ချိန်ညှိနိုင်ပါသည်။",
"send_sending": "ပို့နေသည်...",
@ -973,6 +979,7 @@
"use": "သို့ပြောင်းပါ။",
"use_card_info_three": "ဒစ်ဂျစ်တယ်ကတ်ကို အွန်လိုင်း သို့မဟုတ် ထိတွေ့မှုမဲ့ ငွေပေးချေမှုနည်းလမ်းများဖြင့် အသုံးပြုပါ။",
"use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။",
"use_payjoin": "Payjoin Chair ကိုအသုံးပြုပါ",
"use_ssl": "SSL ကိုသုံးပါ။",
"use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။",
"use_testnet": "testnet ကိုသုံးပါ",

View file

@ -202,6 +202,7 @@
"copy": "Kopiëren",
"copy_address": "Adres kopiëren",
"copy_id": "ID kopiëren",
"copy_payjoin_url": "Kopieer Payjoin url",
"copyWalletConnectLink": "Kopieer de WalletConnect-link van dApp en plak deze hier",
"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.",
"countries": "Landen",
@ -546,6 +547,10 @@
"password": "Wachtwoord",
"paste": "Plakken",
"pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.",
"payjoin_details": "Payjoin details",
"payjoin_enabled": "Payjoin ingeschakeld",
"payjoin_request_awaiting_tx": "In afwachting van transactie",
"payjoin_request_in_progress": "In uitvoering",
"payment_id": "Betaling ID: ",
"payment_was_received": "Uw betaling is ontvangen.",
"pending": " (in afwachting)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Stuur vanuit een externe portemonnee",
"send_name": "Naam",
"send_new": "Nieuw",
"send_payjoin": "Versturen Payjoin",
"send_payment_id": "Betaling ID (facultatief)",
"send_priority": "Momenteel is de vergoeding vastgesteld op ${transactionPriority} prioriteit.\nTransactieprioriteit kan worden aangepast in de instellingen",
"send_sending": "Bezig met verzenden...",
@ -973,6 +979,7 @@
"use": "Gebruik ",
"use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.",
"use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.",
"use_payjoin": "Gebruik Payjoin",
"use_ssl": "Gebruik SSL",
"use_suggested": "Gebruik aanbevolen",
"use_testnet": "Gebruik testnet",

View file

@ -202,6 +202,7 @@
"copy": "Kopiuj",
"copy_address": "Skopiuj adress",
"copy_id": "skopiuj ID",
"copy_payjoin_url": "Skopiuj Payjoin url",
"copyWalletConnectLink": "Skopiuj link do WalletConnect z dApp i wklej tutaj",
"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ć.",
"countries": "Kraje",
@ -546,6 +547,10 @@
"password": "Hasło",
"paste": "Wklej",
"pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.",
"payjoin_details": "Szczegóły Payjoin",
"payjoin_enabled": "Payjoin włączony",
"payjoin_request_awaiting_tx": "Oczekiwanie na transakcję",
"payjoin_request_in_progress": "W toku",
"payment_id": "ID Płatności: ",
"payment_was_received": "Twoja płatność została otrzymana.",
"pending": " (w oczekiwaniu)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Wyślij z portfela zewnętrznego",
"send_name": "Imię",
"send_new": "Nowy",
"send_payjoin": "Wyślij Payjoin",
"send_payment_id": "Identyfikator płatności (opcjonalny)",
"send_priority": "Obecnie opłata ustalona jest na ${transactionPriority} priorytet.\nPriorytet transakcji można zmienić w ustawieniach",
"send_sending": "Wysyłanie...",
@ -973,6 +979,7 @@
"use": "Użyj ",
"use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.",
"use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.",
"use_payjoin": "Używać Payjoin",
"use_ssl": "Użyj SSL",
"use_suggested": "Użyj sugerowane",
"use_testnet": "Użyj testne",

View file

@ -202,6 +202,7 @@
"copy": "Copiar",
"copy_address": "Copiar endereço",
"copy_id": "Copiar ID",
"copy_payjoin_url": "Copie Payjoin url",
"copyWalletConnectLink": "Copie o link WalletConnect do dApp e cole aqui",
"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.",
"countries": "Países",
@ -548,6 +549,10 @@
"password": "Senha",
"paste": "Colar",
"pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.",
"payjoin_details": "Payjoin detalhes",
"payjoin_enabled": "Payjoin habilitado",
"payjoin_request_awaiting_tx": "Aguardando transação",
"payjoin_request_in_progress": "Em andamento",
"payment_id": "ID de pagamento: ",
"payment_was_received": "Seu pagamento foi recebido.",
"pending": " (pendente)",
@ -737,6 +742,7 @@
"send_from_external_wallet": "Enviar da carteira externa",
"send_name": "Nome",
"send_new": "Novo",
"send_payjoin": "Enviar Payjoin",
"send_payment_id": "ID de pagamento (opcional)",
"send_priority": "Atualmente, a taxa está definida para a prioridade: ${transactionPriority}.\nA prioridade da transação pode ser ajustada nas configurações",
"send_sending": "Enviando...",
@ -975,6 +981,7 @@
"use": "Use PIN de ",
"use_card_info_three": "Use o cartão digital online ou com métodos de pagamento sem contato.",
"use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.",
"use_payjoin": "Usar Payjoin",
"use_ssl": "Use SSL",
"use_suggested": "Uso sugerido",
"use_testnet": "Use testNet",

View file

@ -202,6 +202,7 @@
"copy": "Скопировать",
"copy_address": "Cкопировать адрес",
"copy_id": "Скопировать ID",
"copy_payjoin_url": "Копировать Payjoin url",
"copyWalletConnectLink": "Скопируйте ссылку WalletConnect из dApp и вставьте сюда.",
"corrupted_seed_notice": "Файлы для этого кошелька повреждены и не могут быть открыты. Пожалуйста, просмотрите семенную фразу, сохраните ее и восстановите кошелек.\n\nЕсли значение пустое, то семя не смог правильно восстановить.",
"countries": "Страны",
@ -547,6 +548,10 @@
"password": "Пароль",
"paste": "Вставить",
"pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.",
"payjoin_details": "Payjoin подробности",
"payjoin_enabled": "Payjoin включено",
"payjoin_request_awaiting_tx": "В ожидании транзакции",
"payjoin_request_in_progress": "В ходе выполнения",
"payment_id": "ID платежа: ",
"payment_was_received": "Ваш платеж получен.",
"pending": " (в ожидании)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Отправить с внешнего кошелька",
"send_name": "Имя",
"send_new": "Новый",
"send_payjoin": "Отправлять Payjoin",
"send_payment_id": "ID платежа (опционально)",
"send_priority": "Комиссия установлена в зависимости от приоритета: ${transactionPriority}.\nПриоритет транзакции может быть изменён в настройках",
"send_sending": "Отправка...",
@ -974,6 +980,7 @@
"use": "Использовать ",
"use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.",
"use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.",
"use_payjoin": "Использовать Payjoin",
"use_ssl": "Использовать SSL",
"use_suggested": "Использовать предложенный",
"use_testnet": "Используйте Testnet",

View file

@ -202,6 +202,7 @@
"copy": "คัดลอก",
"copy_address": "คัดลอกที่อยู่",
"copy_id": "คัดลอก ID",
"copy_payjoin_url": "คัดลอก Payjoin url",
"copyWalletConnectLink": "คัดลอกลิงก์ WalletConnect จาก dApp แล้ววางที่นี่",
"corrupted_seed_notice": "ไฟล์สำหรับกระเป๋าเงินนี้เสียหายและไม่สามารถเปิดได้ โปรดดูวลีเมล็ดบันทึกและกู้คืนกระเป๋าเงิน\n\nหากค่าว่างเปล่าเมล็ดก็ไม่สามารถกู้คืนได้อย่างถูกต้อง",
"countries": "ประเทศ",
@ -546,6 +547,10 @@
"password": "รหัสผ่าน",
"paste": "วาง",
"pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว",
"payjoin_details": "Payjoin รายละเอียด",
"payjoin_enabled": "Payjoin เปิดใช้งาน",
"payjoin_request_awaiting_tx": "รอธุรกรรม",
"payjoin_request_in_progress": "การดำเนินการ",
"payment_id": "ID การชำระเงิน: ",
"payment_was_received": "การชำระเงินของคุณได้รับการรับทราบแล้ว",
"pending": " (อยู่ระหว่างดำเนินการ)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "ส่งจากกระเป๋าเงินภายนอก",
"send_name": "ชื่อ",
"send_new": "ใหม่",
"send_payjoin": "ส่ง Payjoin",
"send_payment_id": "ID การชำระเงิน (ไม่จำเป็น)",
"send_priority": "ในขณะนี้ค่าธรรมเนียมถูกตั้งค่าเป็นความสำคัญ ${transactionPriority} \nความสำคัญของธุรกรรมสามารถปรับได้ในการตั้งค่า",
"send_sending": "กำลังส่ง...",
@ -973,6 +979,7 @@
"use": "สลับไปที่ ",
"use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ",
"use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล",
"use_payjoin": "ใช้ Payjoin",
"use_ssl": "ใช้ SSL",
"use_suggested": "ใช้ที่แนะนำ",
"use_testnet": "ใช้ testnet",

View file

@ -202,6 +202,7 @@
"copy": "Kopyahin",
"copy_address": "Kopyahin ang Address",
"copy_id": "Kopyahin ang ID",
"copy_payjoin_url": "Kopyahin ang Payjoin url",
"copyWalletConnectLink": "Kopyahin ang link ng WalletConnect mula sa dApp at i-paste dito",
"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.",
"countries": "Mga bansa",
@ -546,6 +547,10 @@
"password": "Password",
"paste": "I-paste",
"pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.",
"payjoin_details": "Mga detalye ng Payjoin",
"payjoin_enabled": "Payjoin pinagana",
"payjoin_request_awaiting_tx": "Naghihintay ng transaksyon",
"payjoin_request_in_progress": "Sa pag -unlad",
"payment_id": "Payment ID: ",
"payment_was_received": "Natanggap ang iyong bayad.",
"pending": "(hindi pa tapos)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Magpadala mula sa panlabas na pitaka",
"send_name": "Pangalan",
"send_new": "Bago",
"send_payjoin": "Magpadala ng Payjoin",
"send_payment_id": "Payment ID (opsyonal)",
"send_priority": "Kasalukuyang nakatakda ang fee sa ${transactionPriority} priyoridad.\n Ang priyoridad ng transaksyon ay maaaring isaayos sa mga setting",
"send_sending": "Nagpapadala...",
@ -973,6 +979,7 @@
"use": "Lumipat sa ",
"use_card_info_three": "Gamitin ang digital card online o sa mga paraan ng pagbabayad na walang contact.",
"use_card_info_two": "Ang mga pondo ay na-convert sa USD kapag hawak sa prepaid account, hindi sa mga digital na pera.",
"use_payjoin": "Gumamit ng Payjoin",
"use_ssl": "Gumamit ng SSL",
"use_suggested": "Gumamit ng iminungkahing",
"use_testnet": "Gumamit ng testnet",

View file

@ -202,6 +202,7 @@
"copy": "Kopyala",
"copy_address": "Adresi kopyala",
"copy_id": "ID'yi kopyala",
"copy_payjoin_url": "Payjoin url kopyala",
"copyWalletConnectLink": "WalletConnect bağlantısını dApp'ten kopyalayıp buraya yapıştırın",
"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ı.",
"countries": "Ülkeler",
@ -546,6 +547,10 @@
"password": "Parola",
"paste": "Yapıştır",
"pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.",
"payjoin_details": "Payjoin detaylar",
"payjoin_enabled": "Payjoin etkinleştirilmiş",
"payjoin_request_awaiting_tx": "İşlem bekliyor",
"payjoin_request_in_progress": "Devam etmekte",
"payment_id": "Ödeme ID'si: ",
"payment_was_received": "Ödemeniz alındı.",
"pending": " (bekleyen)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "Harici cüzdandan gönder",
"send_name": "İsim",
"send_new": "Yeni",
"send_payjoin": "Göndermek Payjoin",
"send_payment_id": "Ödeme ID'si (isteğe bağlı)",
"send_priority": "Şu anda ücret ${transactionPriority} önceliğine ayarlanmıştır.\nİşlem önceliği ayarlardan değiştirilebilir",
"send_sending": "Gönderiliyor...",
@ -973,6 +979,7 @@
"use": "Şuna geç: ",
"use_card_info_three": "Dijital kartı çevrimiçi olarak veya temassız ödeme yöntemleriyle kullanın.",
"use_card_info_two": "Paralar, dijital para birimlerinde değil, ön ödemeli hesapta tutulduğunda USD'ye dönüştürülür.",
"use_payjoin": "Kullanmak Payjoin",
"use_ssl": "SSL kullan",
"use_suggested": "Önerileni Kullan",
"use_testnet": "TestNet kullanın",

View file

@ -202,6 +202,7 @@
"copy": "Скопіювати",
"copy_address": "Cкопіювати адресу",
"copy_id": "Скопіювати ID",
"copy_payjoin_url": "Скопіюйте Payjoin url",
"copyWalletConnectLink": "Скопіюйте посилання WalletConnect із dApp і вставте сюди",
"corrupted_seed_notice": "Файли для цього гаманця пошкоджені і не можуть бути відкриті. Перегляньте насіннєву фразу, збережіть її та відновіть гаманець.\n\nЯкщо значення порожнє, то насіння не могло бути правильно відновленим.",
"countries": "Країни",
@ -546,6 +547,10 @@
"password": "Пароль",
"paste": "Вставити",
"pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.",
"payjoin_details": "Payjoin деталей",
"payjoin_enabled": "Payjoin увімкнено",
"payjoin_request_awaiting_tx": "Чекає транзакції",
"payjoin_request_in_progress": "Триває",
"payment_id": "ID платежу: ",
"payment_was_received": "Ваш платіж отримано.",
"pending": " (в очікуванні)",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Надіслати із зовнішнього гаманця",
"send_name": "Ім'я",
"send_new": "Новий",
"send_payjoin": "Надіслати Payjoin",
"send_payment_id": "ID платежу (опційно)",
"send_priority": "Комісія встановлена в залежності від пріоритету: ${transactionPriority}.\nПріоритет транзакції може бути змінений в налаштуваннях",
"send_sending": "Відправлення...",
@ -974,6 +980,7 @@
"use": "Використати ",
"use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.",
"use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.",
"use_payjoin": "Використовуйте Payjoin",
"use_ssl": "Використати SSL",
"use_suggested": "Використати запропоноване",
"use_testnet": "Використовуйте тестову мережу",

View file

@ -202,6 +202,7 @@
"copy": "کاپی",
"copy_address": "ایڈریس کاپی کریں۔",
"copy_id": "کاپی ID",
"copy_payjoin_url": "کاپی کریں Payjoin url",
"copyWalletConnectLink": "dApp ﮯﺳ WalletConnect ۔ﮟﯾﺮﮐ ﭧﺴﯿﭘ ﮞﺎﮩﯾ ﺭﻭﺍ ﮟﯾﺮﮐ ﯽﭘﺎﮐ ﻮﮐ ﮏﻨﻟ",
"corrupted_seed_notice": "اس پرس کے لئے فائلیں خراب ہیں اور کھولنے سے قاصر ہیں۔ براہ کرم بیج کے فقرے کو دیکھیں ، اسے بچائیں ، اور بٹوے کو بحال کریں۔\n\nاگر قیمت خالی ہے ، تو بیج صحیح طور پر بازیافت کرنے سے قاصر تھا۔",
"countries": "ممالک",
@ -548,6 +549,10 @@
"password": "پاس ورڈ",
"paste": "چسپاں کریں۔",
"pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ",
"payjoin_details": "Payjoin تفصیلات",
"payjoin_enabled": "Payjoin فعال",
"payjoin_request_awaiting_tx": "لین دین کے منتظر",
"payjoin_request_in_progress": "پیشرفت میں",
"payment_id": "ادائیگی کی شناخت:",
"payment_was_received": "آپ کی ادائیگی موصول ہو گئی۔",
"pending": " (زیر التواء)",
@ -737,6 +742,7 @@
"send_from_external_wallet": "بیرونی پرس سے بھیجیں",
"send_name": "نام",
"send_new": "نئی",
"send_payjoin": "بھیجیں",
"send_payment_id": "ادائیگی کی شناخت (اختیاری)",
"send_priority": "فی الحال فیس ${transactionPriority} کی ترجیح پر سیٹ ہے۔\\nٹرانزیکشن کی ترجیح سیٹنگز میں ایڈجسٹ کی جا سکتی ہے۔",
"send_sending": "بھیج رہا ہے...",
@ -975,6 +981,7 @@
"use": "تبدیل کرنا",
"use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔",
"use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔",
"use_payjoin": "Payjoin کا استعمال کریں",
"use_ssl": "SSL استعمال کریں۔",
"use_suggested": "تجویز کردہ استعمال کریں۔",
"use_testnet": "ٹیسٹ نیٹ استعمال کریں",

View file

@ -201,6 +201,7 @@
"copy": "Sao chép",
"copy_address": "Sao chép Địa chỉ",
"copy_id": "Sao chép ID",
"copy_payjoin_url": "Sao chép Payjoin url",
"copyWalletConnectLink": "Sao chép liên kết WalletConnect từ dApp và dán vào đây",
"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.",
"countries": "Quốc gia",
@ -544,6 +545,10 @@
"password": "Mật khẩu",
"paste": "Dán",
"pause_wallet_creation": "Khả năng tạo ví Haven hiện đang bị tạm dừng.",
"payjoin_details": "Payjoin chi tiết",
"payjoin_enabled": "Payjoin Bật",
"payjoin_request_awaiting_tx": "Đang chờ giao dịch",
"payjoin_request_in_progress": "Trong tiến trình",
"payment_id": "ID thanh toán: ",
"payment_was_received": "Thanh toán của bạn đã được nhận.",
"pending": " (đang chờ)",
@ -732,6 +737,7 @@
"send_from_external_wallet": "Gửi từ ví bên ngoài",
"send_name": "Tên",
"send_new": "Mới",
"send_payjoin": "Gửi Payjoin",
"send_payment_id": "ID thanh toán (tùy chọn)",
"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",
"send_sending": "Đang gửi...",
@ -970,6 +976,7 @@
"use": "Chuyển sang",
"use_card_info_three": "Sử dụng thẻ kỹ thuật số trực tuyến hoặc với các phương thức thanh toán không tiếp xúc.",
"use_card_info_two": "Các khoản tiền được chuyển đổi thành USD khi chúng được giữ trong tài khoản trả trước, không phải trong các loại tiền kỹ thuật số.",
"use_payjoin": "Sử dụng Payjoin",
"use_ssl": "Sử dụng SSL",
"use_suggested": "Sử dụng đề xuất",
"use_testnet": "Sử dụng Testnet",

View file

@ -202,6 +202,7 @@
"copy": "Ṣẹ̀dà",
"copy_address": "Ṣẹ̀dà àdírẹ́sì",
"copy_id": "Ṣẹ̀dà àmì ìdánimọ̀",
"copy_payjoin_url": "Daakọ Payjoin url",
"copyWalletConnectLink": "Daakọ ọna asopọ WalletConnect lati dApp ki o si lẹẹmọ nibi",
"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.",
"countries": "Awọn orilẹ-ede",
@ -547,6 +548,10 @@
"password": "Ọ̀rọ̀ aṣínà",
"paste": "Fikún ẹ̀dà yín",
"pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.",
"payjoin_details": "Payjoin awọn alaye",
"payjoin_enabled": "Payjoin ṣiṣẹ",
"payjoin_request_awaiting_tx": "O duro de idunadura",
"payjoin_request_in_progress": "Ni ilọsiwaju",
"payment_id": "Àmì ìdánimọ̀ àránṣẹ́: ",
"payment_was_received": "Àránṣẹ́ yín ti dé.",
"pending": " pípẹ́",
@ -736,6 +741,7 @@
"send_from_external_wallet": "Firanṣẹ lati apamọwọ ita",
"send_name": "Orúkọ",
"send_new": "Títun",
"send_payjoin": "Firanṣẹ PayjoinPayjoin",
"send_payment_id": "Àmì ìdánimọ̀ àránṣẹ́ (ìyàn nìyí)",
"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ò",
"send_sending": "Ń Ránṣẹ́...",
@ -974,6 +980,7 @@
"use": "Lo",
"use_card_info_three": "Ẹ lo káàdí ayélujára lórí wẹ́ẹ̀bù tàbí ẹ lò ó lórí àwọn ẹ̀rọ̀ ìrajà tíwọn kò kò.",
"use_card_info_two": "A pààrọ̀ owó sí owó Amẹ́ríkà tó bá wà nínú àkanti t'á ti fikún tẹ́lẹ̀tẹ́lẹ̀. A kò kó owó náà nínú owó ayélujára.",
"use_payjoin": "Lo Payjoin",
"use_ssl": "Lo SSL",
"use_suggested": "Lo àbá",
"use_testnet": "Lo tele",

View file

@ -202,6 +202,7 @@
"copy": "复制",
"copy_address": "复制地址",
"copy_id": "复制ID",
"copy_payjoin_url": "复制Payjoin url",
"copyWalletConnectLink": "从 dApp 复制 WalletConnect 链接并粘贴到此处",
"corrupted_seed_notice": "该钱包的文件被损坏,无法打开。请查看种子短语,保存并恢复钱包。\n\n如果该值为空则种子无法正确恢复。",
"countries": "国家",
@ -546,6 +547,10 @@
"password": "密码",
"paste": "粘贴",
"pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。",
"payjoin_details": "Payjoin 细节",
"payjoin_enabled": "Payjoin启用",
"payjoin_request_awaiting_tx": "等待交易",
"payjoin_request_in_progress": "进行中",
"payment_id": "付款 ID: ",
"payment_was_received": "您的付款已收到。",
"pending": " (待定)",
@ -735,6 +740,7 @@
"send_from_external_wallet": "从外部钱包发送",
"send_name": "名称",
"send_new": "新建",
"send_payjoin": "发送 Payjoin",
"send_payment_id": "付款编号 (可选的)",
"send_priority": "目前,费用设置为 ${transactionPriority} 优先.\n交易优先级可以在设置中进行调整",
"send_sending": "正在发送...",
@ -973,6 +979,7 @@
"use": "切换使用",
"use_card_info_three": "在线使用电子卡或使用非接触式支付方式。",
"use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。",
"use_payjoin": "使用 Payjoin",
"use_ssl": "使用SSL",
"use_suggested": "使用建议",
"use_testnet": "使用TestNet",

View file

@ -87,6 +87,7 @@ import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/receive_page_option.dart';
@ -118,10 +119,12 @@ import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/bitcoin_wallet_service.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/litecoin_wallet_service.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
@ -171,7 +174,7 @@ abstract class Bitcoin {
int getFeeRate(Object wallet, TransactionPriority priority);
Future<void> generateNewAddress(Object wallet, String label);
Future<void> updateAddress(Object wallet,String address, String label);
Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any});
Object createBitcoinTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, String? payjoinUri});
String getAddress(Object wallet);
List<ElectrumSubAddress> getSilentPaymentAddresses(Object wallet);
@ -189,7 +192,7 @@ abstract class Bitcoin {
List<Unspent> getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any});
Future<void> updateUnspents(Object wallet);
WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, Box<PayjoinSession> payjoinSessionSource, bool alwaysScan, bool isDirect);
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect);
TransactionPriority getBitcoinTransactionPriorityMedium();
TransactionPriority getBitcoinTransactionPriorityCustom();
@ -206,6 +209,7 @@ abstract class Bitcoin {
List<ReceivePageOption> getBitcoinReceivePageOptions();
List<ReceivePageOption> getLitecoinReceivePageOptions();
BitcoinAddressType getBitcoinAddressType(ReceivePageOption option);
bool isPayjoinAvailable(Object wallet);
bool hasSelectedSilentPayments(Object wallet);
bool isBitcoinReceivePageOption(ReceivePageOption option);
BitcoinAddressType getOptionToType(ReceivePageOption option);
@ -240,6 +244,11 @@ abstract class Bitcoin {
bool getMwebEnabled(Object wallet);
String? getUnusedMwebAddress(Object wallet);
String? getUnusedSegwitAddress(Object wallet);
void updatePayjoinState(Object wallet, bool state);
String getPayjoinEndpoint(Object wallet);
void resumePayjoinSessions(Object wallet);
void stopPayjoinSessions(Object wallet);
}
""";