Merge branch 'new_versions' of https://github.com/cake-tech/cake_wallet into cw_linux_direct_input_password

 Conflicts:
	cw_solana/lib/solana_wallet_service.dart
	lib/core/key_service.dart
This commit is contained in:
OmarHatem 2024-03-29 22:42:13 +02:00
commit 59cf9d0acf
122 changed files with 2228 additions and 893 deletions

View file

@ -143,7 +143,7 @@ jobs:
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart

BIN
assets/images/thorchain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,4 +1,2 @@
Monero enhancements
In-App live status page for the app services
Add Exolix exchange provider
Bug fixes and enhancements
Exchange flow enhancements and fixes
Generic enhancements and bug fixes

View file

@ -1 +1,6 @@
Bug fixes and enhancements
Exchange flow enhancements and fixes
Add MoonPay to Buy options
Add THORChain to Exchange providers
Improve Bitcoin fee calculations
Fixes and enhancements for Solana
Generic enhancements and bug fixes

View file

@ -1,4 +1,8 @@
class BitcoinCommitTransactionException implements Exception {
String errorMessage;
BitcoinCommitTransactionException(this.errorMessage);
@override
String toString() => 'Transaction commit is failed.';
}
String toString() => errorMessage;
}

View file

@ -1,4 +0,0 @@
class BitcoinTransactionNoInputsException implements Exception {
@override
String toString() => 'Not enough inputs available. Please select more under Coin Control';
}

View file

@ -1,10 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
class BitcoinTransactionWrongBalanceException implements Exception {
BitcoinTransactionWrongBalanceException(this.currency);
final CryptoCurrency currency;
@override
String toString() => 'You do not have enough ${currency.title} to send this amount.';
}

View file

@ -7,10 +7,9 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:http/http.dart' as http;
String jsonrpcparams(List<Object> params) {
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
final _params = params.map((val) => '"${val.toString()}"').join(',');
return '[$_params]';
}
@ -34,6 +33,7 @@ class ElectrumClient {
: _id = 0,
_isConnected = false,
_tasks = {},
_errors = {},
unterminatedString = '';
static const connectionTimeout = Duration(seconds: 5);
@ -44,6 +44,7 @@ class ElectrumClient {
void Function(bool)? onConnectionStatusChange;
int _id;
final Map<String, SocketTask> _tasks;
final Map<String, String> _errors;
bool _isConnected;
Timer? _aliveTimer;
String unterminatedString;
@ -243,30 +244,20 @@ class ElectrumClient {
});
Future<String> broadcastTransaction(
{required String transactionRaw, BasedUtxoNetwork? network}) async {
if (network == BitcoinNetwork.testnet) {
return http
.post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'),
headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'},
body: transactionRaw)
.then((http.Response response) {
if (response.statusCode == 200) {
return response.body;
{required String transactionRaw,
BasedUtxoNetwork? network,
Function(int)? idCallback}) async =>
call(
method: 'blockchain.transaction.broadcast',
params: [transactionRaw],
idCallback: idCallback)
.then((dynamic result) {
if (result is String) {
return result;
}
throw Exception('Failed to broadcast transaction: ${response.body}');
return '';
});
}
return call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
.then((dynamic result) {
if (result is String) {
return result;
}
return '';
});
}
Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
@ -371,10 +362,12 @@ class ElectrumClient {
}
}
Future<dynamic> call({required String method, List<Object> params = const []}) async {
Future<dynamic> call(
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
idCallback?.call(id);
_registryTask(id, completer);
socket!.write(jsonrpc(method: method, id: id, params: params));
@ -456,6 +449,23 @@ class ElectrumClient {
final id = response['id'] as String?;
final result = response['result'];
try {
final error = response['error'] as Map<String, dynamic>?;
if (error != null) {
final errorMessage = error['message'] as String?;
if (errorMessage != null) {
_errors[id!] = errorMessage;
}
}
} catch (_) {}
try {
final error = response['error'] as String?;
if (error != null) {
_errors[id!] = error;
}
} catch (_) {}
if (method is String) {
_methodHandler(method: method, request: response);
return;
@ -465,6 +475,8 @@ class ElectrumClient {
_finish(id, result);
}
}
String getErrorMessage(int id) => _errors[id.toString()] ?? '';
}
// FIXME: move me

View file

@ -9,10 +9,9 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
import 'package:cw_bitcoin/electrum.dart';
@ -20,6 +19,7 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart';
@ -198,26 +198,27 @@ abstract class ElectrumWalletBase
}
}
Future<EstimatedTxResult> estimateTxFeeAndInputsToUse(
int credentialsAmount,
bool sendAll,
List<BitcoinBaseAddress> outputAddresses,
List<BitcoinOutput> outputs,
int? feeRate,
BitcoinTransactionPriority? priority,
{int? inputsCount}) async {
int _getDustAmount() {
return 546;
}
bool _isBelowDust(int amount) => amount <= _getDustAmount() && network != BitcoinNetwork.testnet;
Future<EstimatedTxResult> estimateSendAllTx(
List<BitcoinOutput> outputs,
int feeRate, {
String? memo,
int credentialsAmount = 0,
}) async {
final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = [];
var leftAmount = credentialsAmount;
var allInputsAmount = 0;
int allInputsAmount = 0;
for (int i = 0; i < unspentCoins.length; i++) {
final utx = unspentCoins[i];
if (utx.isSending) {
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
@ -235,15 +236,12 @@ abstract class ElectrumWalletBase
vout: utx.vout,
scriptType: _getScriptType(address),
),
ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
),
);
bool amountIsAcquired = !sendAll && leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
}
@ -251,120 +249,310 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionNoInputsException();
}
var changeValue = allInputsAmount - credentialsAmount;
if (!sendAll) {
if (changeValue > 0) {
final changeAddress = await walletAddresses.getChangeAddress();
final address = addressTypeFromStr(changeAddress, network);
outputAddresses.add(address);
outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue)));
}
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else {
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
}
final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos, outputs: outputs, network: network);
int fee = feeRate != null
? feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize)
: feeAmountForPriority(priority!, 0, 0, size: estimatedSize);
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency);
throw BitcoinTransactionNoFeeException();
}
var amount = credentialsAmount;
// Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
int amount = allInputsAmount - fee;
final lastOutput = outputs.last;
if (!sendAll) {
if (changeValue > fee) {
// Here, lastOutput is change, deduct the fee from it
outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
}
if (credentialsAmount > 0) {
final amountLeftForFee = amount - credentialsAmount;
if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
amount -= amountLeftForFee;
fee += amountLeftForFee;
}
}
outputs[outputs.length - 1] =
BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
return EstimatedTxResult(
utxos: utxos,
privateKeys: privateKeys,
fee: fee,
amount: amount,
isSendAll: true,
hasChange: false,
memo: memo,
);
}
Future<EstimatedTxResult> estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
int? inputsCount,
String? memo,
}) async {
final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = [];
int allInputsAmount = 0;
int leftAmount = credentialsAmount;
final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList();
for (int i = 0; i < sendingCoins.length; i++) {
final utx = sendingCoins[i];
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
privateKeys.add(privkey);
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utx.hash,
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: _getScriptType(address),
),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
),
);
bool amountIsAcquired = leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
if (utxos.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final spendingAllCoins = sendingCoins.length == utxos.length;
// How much is being spent - how much is being sent
int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
if (amountLeftForChangeAndFee <= 0) {
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress();
final address = addressTypeFromStr(changeAddress, network);
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(amountLeftForChangeAndFee),
));
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else {
// Here, if sendAll, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount for change
amount = allInputsAmount - fee;
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
}
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
}
int amount = credentialsAmount;
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (!_isBelowDust(amountLeftForChange)) {
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount));
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange));
} else {
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
outputs.removeLast();
// Still has inputs to spend before failing
if (!spendingAllCoins) {
return estimateTxForAmount(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
);
}
final estimatedSendAll = await estimateSendAllTx(
outputs,
feeRate,
memo: memo,
);
if (estimatedSendAll.amount == credentialsAmount) {
return estimatedSendAll;
}
// Estimate to user how much is needed to send to cover the fee
final maxAmountWithReturningChange = allInputsAmount - _getDustAmount() - fee - 1;
throw BitcoinTransactionNoDustOnChangeException(
bitcoinAmountToString(amount: maxAmountWithReturningChange),
bitcoinAmountToString(amount: estimatedSendAll.amount),
);
}
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
}
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed) {
throw BitcoinTransactionWrongBalanceException(currency);
throw BitcoinTransactionWrongBalanceException();
}
if (totalAmount > allInputsAmount) {
if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) {
throw BitcoinTransactionWrongBalanceException(currency);
if (spendingAllCoins) {
throw BitcoinTransactionWrongBalanceException();
} else {
if (changeValue > fee) {
outputAddresses.removeLast();
if (amountLeftForChangeAndFee > fee) {
outputs.removeLast();
}
return estimateTxFeeAndInputsToUse(
credentialsAmount, sendAll, outputAddresses, outputs, feeRate, priority,
inputsCount: utxos.length + 1);
return estimateTxForAmount(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
);
}
}
return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount);
return EstimatedTxResult(
utxos: utxos,
privateKeys: privateKeys,
fee: fee,
amount: amount,
hasChange: true,
isSendAll: false,
memo: memo,
);
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
final outputs = <BitcoinOutput>[];
final outputAddresses = <BitcoinBaseAddress>[];
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final hasMultiDestination = transactionCredentials.outputs.length > 1;
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
final memo = transactionCredentials.outputs.first.memo;
var credentialsAmount = 0;
int credentialsAmount = 0;
for (final out in transactionCredentials.outputs) {
final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
final address = addressTypeFromStr(outputAddress, network);
final outputAmount = out.formattedCryptoAmount!;
outputAddresses.add(address);
if (!sendAll && _isBelowDust(outputAmount)) {
throw BitcoinTransactionNoDustException();
}
if (hasMultiDestination) {
if (out.sendAll || out.formattedCryptoAmount! <= 0) {
throw BitcoinTransactionWrongBalanceException(currency);
if (out.sendAll) {
throw BitcoinTransactionWrongBalanceException();
}
}
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
final address =
addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
if (sendAll) {
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
} else {
if (!sendAll) {
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
} else {
// The value will be changed after estimating the Tx size and deducting the fee from the total
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
}
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
}
}
final estimatedTx = await estimateTxFeeAndInputsToUse(
credentialsAmount,
sendAll,
outputAddresses,
outputs,
transactionCredentials.feeRate,
transactionCredentials.priority,
);
final feeRateInt = transactionCredentials.feeRate != null
? transactionCredentials.feeRate!
: feeRate(transactionCredentials.priority!);
final txb = BitcoinTransactionBuilder(
EstimatedTxResult estimatedTx;
if (sendAll) {
estimatedTx = await estimateSendAllTx(
outputs,
feeRateInt,
memo: memo,
credentialsAmount: credentialsAmount,
);
} else {
estimatedTx = await estimateTxForAmount(
credentialsAmount,
outputs,
feeRateInt,
memo: memo,
);
}
BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network);
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
);
} else {
txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
);
}
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.privateKeys
@ -385,7 +573,10 @@ abstract class ElectrumWalletBase
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
network: network)
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll)
..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
@ -418,7 +609,7 @@ abstract class ElectrumWalletBase
}
}
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) =>
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
@ -898,16 +1089,35 @@ class EstimateTxParams {
}
class EstimatedTxResult {
EstimatedTxResult(
{required this.utxos, required this.privateKeys, required this.fee, required this.amount});
EstimatedTxResult({
required this.utxos,
required this.privateKeys,
required this.fee,
required this.amount,
required this.hasChange,
required this.isSendAll,
this.memo,
});
final List<UtxoWithAddress> utxos;
final List<ECPrivate> privateKeys;
final int fee;
final int amount;
final bool hasChange;
final bool isSendAll;
final String? memo;
}
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
if (network is BitcoinCashNetwork) {
if (!address.startsWith("bitcoincash:") &&
(address.startsWith("q") || address.startsWith("p"))) {
address = "bitcoincash:$address";
}
return BitcoinCashAddress(address).baseAddress;
}
if (P2pkhAddress.regex.hasMatch(address)) {
return P2pkhAddress.fromAddress(address: address, network: network);
} else if (P2shAddress.regex.hasMatch(address)) {

View file

@ -77,7 +77,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
String get address {
String receiveAddress;
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
final typeMatchingReceiveAddresses =
receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) {
@ -220,8 +221,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
allAddressesMap.clear();
_addresses.forEach((addressRecord) {
addressesMap[addressRecord.address] = addressRecord.name;
allAddressesMap[addressRecord.address] = addressRecord.name;
});
await saveAddressesInBox();
} catch (e) {

View file

@ -0,0 +1,27 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/exceptions.dart';
class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException {
BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc);
}
class BitcoinTransactionNoInputsException extends TransactionNoInputsException {}
class BitcoinTransactionNoFeeException extends TransactionNoFeeException {}
class BitcoinTransactionNoDustException extends TransactionNoDustException {}
class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException {
BitcoinTransactionNoDustOnChangeException(super.max, super.min);
}
class BitcoinTransactionCommitFailed extends TransactionCommitFailed {}
class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {}
class BitcoinTransactionCommitFailedDustOutput extends TransactionCommitFailedDustOutput {}
class BitcoinTransactionCommitFailedDustOutputSendAll
extends TransactionCommitFailedDustOutputSendAll {}
class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {}

View file

@ -1,4 +1,4 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
@ -9,7 +9,13 @@ import 'package:cw_core/wallet_type.dart';
class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(this._tx, this.type,
{required this.electrumClient, required this.amount, required this.fee, this.network})
{required this.electrumClient,
required this.amount,
required this.fee,
required this.feeRate,
this.network,
required this.hasChange,
required this.isSendAll})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@ -17,7 +23,10 @@ class PendingBitcoinTransaction with PendingTransaction {
final ElectrumClient electrumClient;
final int amount;
final int fee;
final String feeRate;
final BasedUtxoNetwork? network;
final bool hasChange;
final bool isSendAll;
@override
String get id => _tx.txId();
@ -31,14 +40,37 @@ class PendingBitcoinTransaction with PendingTransaction {
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
@override
int? get outputCount => _tx.outputs.length;
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
final result = await electrumClient.broadcastTransaction(transactionRaw: hex, network: network);
int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
if (result.isEmpty) {
throw BitcoinCommitTransactionException();
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
}
throw BitcoinTransactionCommitFailed();
}
_listeners.forEach((listener) => listener(transactionInfo()));

View file

@ -5,15 +5,10 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin_cash/src/pending_bitcoin_cash_transaction.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart';
@ -137,184 +132,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
);
}
@override
Future<PendingBitcoinCashTransaction> createTransaction(Object credentials) async {
const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
final outputs = transactionCredentials.outputs;
final hasMultiDestination = outputs.length > 1;
var allInputsAmount = 0;
if (unspentCoins.isEmpty) await updateUnspent();
for (final utx in unspentCoins) {
if (utx.isSending) {
allInputsAmount += utx.value;
inputs.add(utx);
}
}
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
final allAmountFee = transactionCredentials.feeRate != null
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
final allAmount = allInputsAmount - allAmountFee;
var credentialsAmount = 0;
var amount = 0;
var fee = 0;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
throw BitcoinTransactionWrongBalanceException(currency);
}
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
if (allAmount - credentialsAmount < minAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = credentialsAmount;
if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
outputsCount: outputs.length + 1);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
outputsCount: outputs.length + 1);
}
} else {
final output = outputs.first;
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
if (credentialsAmount > allAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = output.sendAll || allAmount - credentialsAmount < minAmount
? allAmount
: credentialsAmount;
if (output.sendAll || amount == allAmount) {
fee = allAmountFee;
} else if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
}
}
if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final txb = bitbox.Bitbox.transactionBuilder(testnet: false);
final changeAddress = await walletAddresses.getChangeAddress();
var leftAmount = totalAmount;
var totalInputAmount = 0;
inputs.clear();
for (final utx in unspentCoins) {
if (utx.isSending) {
leftAmount = leftAmount - utx.value;
totalInputAmount += utx.value;
inputs.add(utx);
if (leftAmount <= 0) {
break;
}
}
}
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
if (amount <= 0 || totalInputAmount < totalAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
inputs.forEach((input) {
txb.addInput(input.hash, input.vout);
});
final String bchPrefix = "bitcoincash:";
outputs.forEach((item) {
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
String outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
if (!outputAddress.startsWith(bchPrefix)) {
outputAddress = "$bchPrefix$outputAddress";
}
bool isP2sh = outputAddress.startsWith("p", bchPrefix.length);
if (isP2sh) {
final p2sh = P2shAddress.fromAddress(
address: outputAddress,
network: BitcoinCashNetwork.mainnet,
);
txb.addOutput(Uint8List.fromList(p2sh.toScriptPubKey().toBytes()), outputAmount!);
return;
}
txb.addOutput(outputAddress, outputAmount!);
});
final estimatedSize = bitbox.BitcoinCash.getByteCount(inputs.length, outputs.length + 1);
var feeAmount = 0;
if (transactionCredentials.feeRate != null) {
feeAmount = transactionCredentials.feeRate! * estimatedSize;
} else {
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
}
final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) {
txb.addOutput(changeAddress, changeValue);
}
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
final keyPair = generateKeyPair(
hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index);
txb.sign(i, keyPair, input.value);
}
// Build the transaction
final tx = txb.build();
return PendingBitcoinCashTransaction(tx, type,
electrumClient: electrumClient, amount: amount, fee: fee);
}
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
bitbox.ECPair.fromWIF(hd.derive(index).wif!);
@override
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) =>
feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0;
int totalValue = 0;

View file

@ -1,4 +1,4 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
@ -11,7 +11,9 @@ class PendingBitcoinCashTransaction with PendingTransaction {
PendingBitcoinCashTransaction(this._tx, this.type,
{required this.electrumClient,
required this.amount,
required this.fee})
required this.fee,
required this.hasChange,
required this.isSendAll})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@ -19,6 +21,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
final ElectrumClient electrumClient;
final int amount;
final int fee;
final bool hasChange;
final bool isSendAll;
@override
String get id => _tx.getId();
@ -36,18 +40,36 @@ class PendingBitcoinCashTransaction with PendingTransaction {
@override
Future<void> commit() async {
final result =
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, idCallback: (id) => callId = id);
if (result.isEmpty) {
throw BitcoinCommitTransactionException();
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
}
throw BitcoinTransactionCommitFailed();
}
_listeners?.forEach((listener) => listener(transactionInfo()));
_listeners.forEach((listener) => listener(transactionInfo()));
}
void addListener(
void Function(ElectrumTransactionInfo transaction) listener) =>
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,

View file

@ -38,6 +38,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.trx,
CryptoCurrency.usdt,
CryptoCurrency.usdterc20,
CryptoCurrency.sol,
CryptoCurrency.maticpoly,
CryptoCurrency.xlm,
CryptoCurrency.xrp,
CryptoCurrency.xhv,
@ -50,7 +52,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.usdttrc20,
CryptoCurrency.hbar,
CryptoCurrency.sc,
CryptoCurrency.sol,
CryptoCurrency.usdc,
CryptoCurrency.usdcsol,
CryptoCurrency.zaddr,
@ -61,7 +62,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.dcr,
CryptoCurrency.kmd,
CryptoCurrency.mana,
CryptoCurrency.maticpoly,
CryptoCurrency.matic,
CryptoCurrency.mkr,
CryptoCurrency.near,

View file

@ -0,0 +1,30 @@
import 'package:cw_core/crypto_currency.dart';
class TransactionWrongBalanceException implements Exception {
TransactionWrongBalanceException(this.currency);
final CryptoCurrency currency;
}
class TransactionNoInputsException implements Exception {}
class TransactionNoFeeException implements Exception {}
class TransactionNoDustException implements Exception {}
class TransactionNoDustOnChangeException implements Exception {
TransactionNoDustOnChangeException(this.max, this.min);
final String max;
final String min;
}
class TransactionCommitFailed implements Exception {}
class TransactionCommitFailedDustChange implements Exception {}
class TransactionCommitFailedDustOutput implements Exception {}
class TransactionCommitFailedDustOutputSendAll implements Exception {}
class TransactionCommitFailedVoutNegative implements Exception {}

View file

@ -7,7 +7,8 @@ class OutputInfo {
this.formattedCryptoAmount,
this.fiatAmount,
this.note,
this.extractedAddress,});
this.extractedAddress,
this.memo});
final String? fiatAmount;
final String? cryptoAmount;
@ -17,4 +18,5 @@ class OutputInfo {
final bool sendAll;
final bool isParsedAddress;
final int? formattedCryptoAmount;
final String? memo;
}

View file

@ -2,7 +2,9 @@ mixin PendingTransaction {
String get id;
String get amountFormatted;
String get feeFormatted;
String? feeRate;
String get hex;
int? get outputCount => null;
Future<void> commit();
}
}

View file

@ -3,8 +3,9 @@ import 'package:cw_core/wallet_info.dart';
abstract class WalletAddresses {
WalletAddresses(this.walletInfo)
: addressesMap = {},
addressInfos = {};
: addressesMap = {},
allAddressesMap = {},
addressInfos = {};
final WalletInfo walletInfo;
@ -15,6 +16,7 @@ abstract class WalletAddresses {
set address(String address);
Map<String, String> addressesMap;
Map<String, String> allAddressesMap;
Map<int, List<AddressInfo>> addressInfos;
@ -39,5 +41,5 @@ abstract class WalletAddresses {
}
}
bool containsAddress(String address) => addressesMap.containsKey(address);
bool containsAddress(String address) => allAddressesMap.containsKey(address);
}

View file

@ -41,4 +41,29 @@ class EthereumClient extends EVMChainClient {
return [];
}
}
@override
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
try {
final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", {
"module": "account",
"action": "txlistinternal",
"address": address,
"apikey": secrets.etherScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'ETH'))
.toList();
}
return [];
} catch (e) {
log(e.toString());
return [];
}
}
}

View file

@ -14,6 +14,7 @@ import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:erc20/erc20.dart';
import 'package:web3dart/web3dart.dart';
import 'package:hex/hex.dart' as hex;
abstract class EVMChainClient {
final httpClient = Client();
@ -26,6 +27,8 @@ abstract class EVMChainClient {
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress});
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address);
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
//! Common methods across all child classes
@ -79,12 +82,13 @@ abstract class EVMChainClient {
Future<PendingEVMChainTransaction> signTransaction({
required EthPrivateKey privateKey,
required String toAddress,
required String amount,
required BigInt amount,
required int gas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
required int exponent,
String? contractAddress,
String? data,
}) async {
assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly ||
@ -99,7 +103,8 @@ abstract class EVMChainClient {
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null,
);
final signedTransaction =
@ -119,7 +124,7 @@ abstract class EVMChainClient {
_sendTransaction = () async {
await erc20.transfer(
EthereumAddress.fromHex(toAddress),
BigInt.parse(amount),
amount,
credentials: privateKey,
transaction: transaction,
);
@ -128,7 +133,7 @@ abstract class EVMChainClient {
return PendingEVMChainTransaction(
signedTransaction: signedTransaction,
amount: amount,
amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei,
sendTransaction: _sendTransaction,
exponent: exponent,
@ -140,12 +145,14 @@ abstract class EVMChainClient {
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) {
return Transaction(
from: from,
to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
data: data,
);
}
@ -222,6 +229,10 @@ abstract class EVMChainClient {
}
}
Uint8List hexToBytes(String hexString) {
return Uint8List.fromList(hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
}
void stop() {
_client?.dispose();
}

View file

@ -9,3 +9,14 @@ class EVMChainTransactionCreationException implements Exception {
@override
String toString() => exceptionMessage;
}
class EVMChainTransactionFeesException implements Exception {
final String exceptionMessage;
EVMChainTransactionFeesException()
: exceptionMessage = 'Current balance is less than the estimated fees for this transaction.';
@override
String toString() => exceptionMessage;
}

View file

@ -32,15 +32,15 @@ class EVMChainTransactionModel {
factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) =>
EVMChainTransactionModel(
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
hash: json["hash"],
from: json["from"],
to: json["to"],
amount: BigInt.parse(json["value"]),
gasUsed: int.parse(json["gasUsed"]),
gasPrice: BigInt.parse(json["gasPrice"]),
contractAddress: json["contractAddress"],
confirmations: int.parse(json["confirmations"]),
blockNumber: int.parse(json["blockNumber"]),
hash: json["hash"] ?? "",
from: json["from"] ?? "",
to: json["to"] ?? "",
amount: BigInt.parse(json["value"] ?? "0"),
gasUsed: int.parse(json["gasUsed"] ?? "0"),
gasPrice: BigInt.parse(json["gasPrice"] ?? "0"),
contractAddress: json["contractAddress"] ?? "",
confirmations: int.parse(json["confirmations"] ?? "0"),
blockNumber: int.parse(json["blockNumber"] ?? "0"),
tokenSymbol: json["tokenSymbol"] ?? defaultSymbol,
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
isError: json["isError"] == "1",

View file

@ -230,10 +230,17 @@ abstract class EVMChainWalletBase
final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1;
final String? opReturnMemo = outputs.first.memo;
String? hexOpReturnMemo;
if (opReturnMemo != null) {
hexOpReturnMemo = '0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}';
}
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final _erc20Balance = balance[transactionCurrency]!;
final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent);
@ -248,7 +255,7 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) {
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
} else {
@ -257,18 +264,27 @@ abstract class EVMChainWalletBase
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = _erc20Balance.balance;
allAmount = erc20Balance.balance;
} else {
allAmount = _erc20Balance.balance -
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
}
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = output.sendAll
? allAmount
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
if (_erc20Balance.balance < totalAmount) {
if (estimatedFee > erc20Balance.balance) {
throw EVMChainTransactionFeesException();
}
allAmount = erc20Balance.balance - estimatedFee;
}
if (output.sendAll) {
totalAmount = allAmount;
} else {
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
}
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
}
@ -278,13 +294,14 @@ abstract class EVMChainWalletBase
toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount.toString(),
amount: totalAmount,
gas: _estimatedGas!,
priority: _credentials.priority!,
currency: transactionCurrency,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
data: hexOpReturnMemo,
);
return pendingEVMChainTransaction;
@ -316,6 +333,7 @@ abstract class EVMChainWalletBase
Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async {
final address = _evmChainPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address);
final internalTransactions = await _client.fetchInternalTransactions(address);
final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = [];
@ -330,6 +348,7 @@ abstract class EVMChainWalletBase
final tokensTransaction = await Future.wait(erc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element));
transactions.addAll(internalTransactions);
final Map<String, EVMChainTransactionInfo> result = {};
@ -490,7 +509,7 @@ abstract class EVMChainWalletBase
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_updateTransactions();
_updateBalance();
});

View file

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:cw_core/pending_transaction.dart';
import 'package:web3dart/crypto.dart';
import 'package:hex/hex.dart' as Hex;
class PendingEVMChainTransaction with PendingTransaction {
final Function sendTransaction;
@ -38,5 +39,12 @@ class PendingEVMChainTransaction with PendingTransaction {
String get hex => bytesToHex(signedTransaction, include0x: true);
@override
String get id => '';
String get id {
final String eip1559Hex = '0x02${hex.substring(2)}';
final Uint8List bytes = Uint8List.fromList(Hex.HEX.decode(eip1559Hex.substring(2)));
var txid = keccak256(bytes);
return '0x${Hex.HEX.encode(txid)}';
}
}

View file

@ -13,6 +13,7 @@ class PolygonClient extends EVMChainClient {
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) {
return Transaction(
from: from,
@ -54,4 +55,28 @@ class PolygonClient extends EVMChainClient {
return [];
}
}
@override
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
try {
final response = await httpClient.get(Uri.https("api.polygonscan.io", "/api", {
"module": "account",
"action": "txlistinternal",
"address": address,
"apikey": secrets.polygonScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'MATIC'))
.toList();
}
return [];
} catch (_) {
return [];
}
}
}

View file

@ -96,16 +96,30 @@ class SolanaWalletClient {
return SolanaBalance(totalBalance);
}
Future<double> getGasForMessage(String message) async {
Future<double> getFeeForMessage(String message, Commitment commitment) async {
try {
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
final fee = gasPrice / lamportsPerSol;
final feeForMessage =
await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
return fee;
} catch (_) {
return 0;
return 0.0;
}
}
Future<double> getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async {
const commitment = Commitment.confirmed;
final message =
_getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol);
final recentBlockhash = await _getRecentBlockhash(commitment);
final estimatedFee =
_getFeeFromCompiledMessage(message, ownerKeypair.publicKey, recentBlockhash, commitment);
return estimatedFee;
}
/// Load the Address's transactions into the account
Future<List<SolanaTransactionModel>> fetchTransactions(
Ed25519HDPublicKey publicKey, {
@ -257,24 +271,15 @@ class SolanaWalletClient {
Future<PendingSolanaTransaction> signSolanaTransaction({
required String tokenTitle,
required int tokenDecimals,
String? tokenMint,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required bool isSendAll,
String? tokenMint,
List<String> references = const [],
}) async {
const commitment = Commitment.confirmed;
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(
lamportsPerSignature: 500,
),
);
if (tokenTitle == CryptoCurrency.sol.title) {
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
tokenTitle: tokenTitle,
@ -282,8 +287,8 @@ class SolanaWalletClient {
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment,
isSendAll: isSendAll,
);
return pendingNativeTokenTransaction;
} else {
@ -294,25 +299,29 @@ class SolanaWalletClient {
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment,
);
return pendingSPLTokenTransaction;
}
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Future<RecentBlockhash> _getRecentBlockhash(Commitment commitment) async {
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(lamportsPerSignature: 500),
);
return recentBlockhash;
}
Message _getMessageForNativeTransaction(
Ed25519HDKeyPair ownerKeypair,
String destinationAddress,
int lamports,
) {
final instructions = [
SystemInstruction.transfer(
fundingAccount: ownerKeypair.publicKey,
@ -322,21 +331,75 @@ class SolanaWalletClient {
];
final message = Message(instructions: instructions);
return message;
}
Future<double> _getFeeFromCompiledMessage(
Message message,
Ed25519HDPublicKey feePayer,
RecentBlockhash recentBlockhash,
Commitment commitment,
) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getFeeForMessage(base64Message, commitment);
return fee;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required bool isSendAll,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports);
final signers = [ownerKeypair];
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
recentBlockhash,
commitment,
);
SignedTx signedTx;
if (isSendAll) {
final feeInLamports = (fee * lamportsPerSol).toInt();
final updatedLamports = lamports - feeInLamports;
final updatedMessage =
_getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports);
signedTx = await _signTransactionInternal(
message: updatedMessage,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
} else {
signedTx = await _signTransactionInternal(
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
}
sendTx() async => await sendTransaction(
signedTransaction: signedTx,
commitment: commitment,
@ -360,7 +423,6 @@ class SolanaWalletClient {
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
@ -408,8 +470,18 @@ class SolanaWalletClient {
);
final message = Message(instructions: [instruction]);
final signers = [ownerKeypair];
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
signers.first.publicKey,
recentBlockhash,
commitment,
);
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,
@ -417,12 +489,6 @@ class SolanaWalletClient {
recentBlockhash: recentBlockhash,
);
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
);
sendTx() async => await sendTransaction(
signedTransaction: signedTx,
commitment: commitment,
@ -438,19 +504,6 @@ class SolanaWalletClient {
return pendingTransaction;
}
Future<double> _getFeeFromCompiledMessage(
Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getGasForMessage(base64Message);
return fee;
}
Future<SignedTx> _signTransactionInternal({
required Message message,
required List<Ed25519HDKeyPair> signers,
@ -466,13 +519,18 @@ class SolanaWalletClient {
required SignedTx signedTransaction,
required Commitment commitment,
}) async {
final signature = await _client!.rpcClient.sendTransaction(
signedTransaction.encode(),
preflightCommitment: commitment,
);
try {
final signature = await _client!.rpcClient.sendTransaction(
signedTransaction.encode(),
preflightCommitment: commitment,
);
_client!.waitForSignatureStatus(signature, status: commitment);
_client!.waitForSignatureStatus(signature, status: commitment);
return signature;
return signature;
} catch (e) {
print('Error while sending transaction: ${e.toString()}');
throw Exception(e);
}
}
}

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
@ -30,7 +29,6 @@ import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
import 'package:web3dart/crypto.dart';
part 'solana_wallet.g.dart';
@ -83,6 +81,9 @@ abstract class SolanaWalletBase
late SolanaWalletClient _client;
@observable
double? estimatedFee;
Timer? _transactionsUpdateTimer;
late final Box<SPLToken> splTokensBox;
@ -140,7 +141,7 @@ abstract class SolanaWalletBase
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
final privateKeyBytes = hexToBytes(privateKey);
final privateKeyBytes = HEX.decode(privateKey);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
}
@ -179,6 +180,14 @@ abstract class SolanaWalletBase
}
}
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
} catch (e) {
estimatedFee = 0.0;
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
@ -196,6 +205,8 @@ abstract class SolanaWalletBase
double totalAmount = 0.0;
bool isSendAll = false;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -212,9 +223,15 @@ abstract class SolanaWalletBase
} else {
final output = outputs.first;
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
isSendAll = output.sendAll;
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
if (isSendAll) {
totalAmount = walletBalanceForCurrency;
} else {
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = totalOriginalAmount;
}
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -236,6 +253,7 @@ abstract class SolanaWalletBase
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
isSendAll: isSendAll,
);
return pendingSolanaTransaction;
@ -277,7 +295,10 @@ abstract class SolanaWalletBase
Future<void> _updateSPLTokenTransactions() async {
List<SolanaTransactionModel> splTokenTransactions = [];
for (var token in balance.keys) {
// Make a copy of keys to avoid concurrent modification
var tokenKeys = List<CryptoCurrency>.from(balance.keys);
for (var token in tokenKeys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
@ -334,6 +355,7 @@ abstract class SolanaWalletBase
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
_getEstimatedFees(),
]);
syncStatus = SyncedSyncStatus();
@ -443,18 +465,22 @@ abstract class SolanaWalletBase
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
// Fetch token's metadata account
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
try {
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) {
if (token == null) {
return null;
}
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
);
} catch (e) {
return null;
}
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
);
}
@override
@ -485,9 +511,9 @@ abstract class SolanaWalletBase
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
_updateSPLTokenTransactions();
_updateNativeSOLTransactions();
_updateBalance();
_updateNativeSOLTransactions();
_updateSPLTokenTransactions();
});
}
@ -499,7 +525,7 @@ abstract class SolanaWalletBase
final signature = await _walletKeyPair!.sign(messageBytes);
// Convert the signature to a hexadecimal string
final hex = bytesToHex(signature.bytes);
final hex = HEX.encode(signature.bytes);
return hex;
}

View file

@ -35,6 +35,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@ -49,17 +50,33 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
Future<SolanaWallet> openWallet(String name, String password) async {
final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
await wallet.save();
try {
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
return wallet;
await wallet.init();
await wallet.save();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
await wallet.save();
return wallet;
}
}
@override
@ -120,6 +137,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());

View file

@ -19,7 +19,6 @@ dependencies:
bip39: ^1.0.6
mobx: ^2.3.0+1
shared_preferences: ^2.0.15
web3dart: ^2.7.1
bip32: ^2.0.0
hex: ^0.2.0
@ -34,4 +33,4 @@ dev_dependencies:
flutter:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# - images/a_dot_ham.jpeg

View file

@ -58,16 +58,16 @@ post_install do |installer|
'PERMISSION_CONTACTS=0',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=0',
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=0',
'PERMISSION_MICROPHONE=1',
## dart: PermissionGroup.speech
'PERMISSION_SPEECH_RECOGNIZER=0',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=0',
'PERMISSION_PHOTOS=1',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_LOCATION=0',

View file

@ -300,6 +300,6 @@ SPEC CHECKSUMS:
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca
PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3
COCOAPODS: 1.15.2

View file

@ -85,7 +85,8 @@ class CWBitcoin extends Bitcoin {
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: feeRate);
@ -122,23 +123,30 @@ class CWBitcoin extends Bitcoin {
@override
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
final electrumWallet = wallet as ElectrumWallet;
final sk = ECPrivate.random();
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
final p2wpkhAddr = sk.getPublic().toP2wpkhAddress();
try {
final estimatedTx = await electrumWallet.estimateTxFeeAndInputsToUse(
0,
true,
// Deposit address + change address
[p2shAddr, p2wpkhAddr],
[
BitcoinOutput(address: p2shAddr, value: BigInt.zero),
BitcoinOutput(address: p2wpkhAddr, value: BigInt.zero)
],
null,
priority as BitcoinTransactionPriority);
final sk = ECPrivate.random();
final electrumWallet = wallet as ElectrumWallet;
if (wallet.type == WalletType.bitcoinCash) {
final p2pkhAddr = sk.getPublic().toP2pkhAddress();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2pkhAddr, value: BigInt.zero)],
getFeeRate(wallet, priority as BitcoinCashTransactionPriority),
);
return estimatedTx.amount;
}
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
getFeeRate(
wallet,
wallet.type == WalletType.litecoin
? priority as LitecoinTransactionPriority
: priority as BitcoinTransactionPriority,
),
);
return estimatedTx.amount;
} catch (_) {

View file

@ -1,4 +1,13 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@ -6,34 +15,31 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:crypto/crypto.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:url_launcher/url_launcher.dart';
class MoonPaySellProvider extends BuyProvider {
MoonPaySellProvider({
class MoonPayProvider extends BuyProvider {
MoonPayProvider({
required SettingsStore settingsStore,
required WalletBase wallet,
bool isTestEnvironment = false,
}) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
}) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
this._settingsStore = settingsStore,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
final SettingsStore _settingsStore;
static const _baseTestUrl = 'sell-sandbox.moonpay.com';
static const _baseProductUrl = 'sell.moonpay.com';
static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
static const _baseSellProductUrl = 'sell.moonpay.com';
static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
static const _baseBuyProductUrl = 'buy.moonpay.com';
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
static const _apiUrl = 'https://api.moonpay.com';
@override
String get providerDescription =>
@ -60,154 +66,121 @@ class MoonPaySellProvider extends BuyProvider {
static String get _apiKey => secrets.moonPayApiKey;
static String get _secretKey => secrets.moonPaySecretKey;
final String baseUrl;
final String baseBuyUrl;
final String baseSellUrl;
Future<Uri> requestMoonPayUrl({
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
Future<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
final response = await post(
uri,
headers: {
'Content-Type': 'application/json',
'x-api-key': _exchangeHelperApiKey,
},
body: json.encode({'query': query}),
);
if (response.statusCode == 200) {
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
}
}
Future<Uri> requestSellMoonPayUrl({
required CryptoCurrency currency,
required String refundWalletAddress,
required SettingsStore settingsStore,
}) async {
final customParams = {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'refundWalletAddress': refundWalletAddress,
};
final originalUri = Uri.https(
baseUrl,
'',
<String, dynamic>{
'apiKey': _apiKey,
'defaultBaseCurrencyCode': _normalizeCurrency(currency),
'refundWalletAddress': refundWalletAddress,
}..addAll(customParams),
);
if (_apiKey.isNotEmpty) {
params['apiKey'] = _apiKey;
}
final messageBytes = utf8.encode('?${originalUri.query}');
final key = utf8.encode(_secretKey);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
final originalUri = Uri.https(
baseSellUrl,
'',
params,
);
if (isTestEnvironment) {
return originalUri;
}
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
final uri = await requestMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
String _normalizeCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.maticpoly) {
return "MATIC_POLYGON";
}
return currency.toString().toLowerCase();
}
}
class MoonPayBuyProvider extends BuyProvider {
MoonPayBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseTestUrl = 'https://buy-staging.moonpay.com';
static const _baseProductUrl = 'https://buy.moonpay.com';
static const _apiUrl = 'https://api.moonpay.com';
// BUY:
static const _currenciesSuffix = '/v3/currencies';
static const _quoteSuffix = '/buy_quote';
static const _transactionsSuffix = '/v1/transactions';
static const _ipAddressSuffix = '/v4/ip_address';
static const _apiKey = secrets.moonPayApiKey;
static const _secretKey = secrets.moonPaySecretKey;
@override
String get title => 'MoonPay';
Future<Uri> requestBuyMoonPayUrl({
required CryptoCurrency currency,
required SettingsStore settingsStore,
required String walletAddress,
String? amount,
}) async {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'baseCurrencyCode': _normalizeCurrency(currency),
'baseCurrencyAmount': amount ?? '0',
'currencyCode': currencyCode,
'walletAddress': walletAddress,
'lockAmount': 'true',
'showAllCurrencies': 'false',
'showWalletAddressForm': 'false',
'enabledPaymentMethods':
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
};
@override
String get providerDescription =>
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
if (_apiKey.isNotEmpty) {
params['apiKey'] = _apiKey;
}
@override
String get lightIcon => 'assets/images/moonpay_light.png';
final originalUri = Uri.https(
baseBuyUrl,
'',
params,
);
@override
String get darkIcon => 'assets/images/moonpay_dark.png';
if (isTestEnvironment) {
return originalUri;
}
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseUrl + '/transaction_receipt?transactionId=';
String baseUrl;
Future<String> requestUrl(String amount, String sourceCurrency) async {
final enabledPaymentMethods = 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
'%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment';
final suffix = '?apiKey=' +
_apiKey +
'&currencyCode=' +
currencyCode +
'&enabledPaymentMethods=' +
enabledPaymentMethods +
'&walletAddress=' +
wallet.walletAddresses.address +
'&baseCurrencyCode=' +
sourceCurrency.toLowerCase() +
'&baseCurrencyAmount=' +
amount +
'&lockAmount=true' +
'&showAllCurrencies=false' +
'&showWalletAddressForm=false';
final originalUrl = baseUrl + suffix;
final messageBytes = utf8.encode(suffix);
final key = utf8.encode(_secretKey);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
final urlWithSignature = originalUrl + '&signature=${Uri.encodeComponent(signature)}';
return isTestEnvironment ? originalUrl : urlWithSignature;
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
@ -282,6 +255,52 @@ class MoonPayBuyProvider extends BuyProvider {
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) =>
throw UnimplementedError();
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
// try {
late final Uri uri;
if (isBuyAction ?? true) {
uri = await requestBuyMoonPayUrl(
currency: wallet.currency,
walletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
} else {
uri = await requestSellMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
}
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
// } catch (e) {
// await showDialog<void>(
// context: context,
// builder: (BuildContext context) {
// return AlertWithOneAction(
// alertTitle: 'MoonPay',
// alertContent: 'The MoonPay service is currently unavailable: $e',
// buttonText: S.of(context).ok,
// buttonAction: () => Navigator.of(context).pop(),
// );
// },
// );
// }
}
String _normalizeCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.maticpoly) {
return "MATIC_POLYGON";
}
return currency.toString().toLowerCase();
}
}

View file

@ -32,11 +32,12 @@ class RobinhoodBuyProvider extends BuyProvider {
String get _applicationId => secrets.robinhoodApplicationId;
String get _apiSecret => secrets.robinhoodCIdApiSecret;
String get _apiSecret => secrets.exchangeHelperApiKey;
String getSignature(String message) {
switch (wallet.type) {
case WalletType.ethereum:
case WalletType.polygon:
return wallet.signMessage(message);
case WalletType.litecoin:
case WalletType.bitcoin:

View file

@ -34,6 +34,10 @@ class AmountValidator extends TextValidator {
late final DecimalAmountValidator decimalAmountValidator;
String? call(String? value) {
if (value == null || value.isEmpty) {
return S.current.error_text_amount;
}
//* Validate for Text(length, symbols, decimals etc)
final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value);

View file

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@ -64,7 +66,7 @@ class AuthService with Store {
Future<bool> authenticate(String pin) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final encodedPin = await secureStorage.read(key: key);
final encodedPin = await readSecureStorage(secureStorage, key);
final decodedPin = decodedPinCode(pin: encodedPin!);
return decodedPin == pin;
@ -76,7 +78,8 @@ class AuthService with Store {
}
Future<bool> requireAuth() async {
final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
final timestamp =
int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
final duration = _durationToRequireAuth(timestamp ?? 0);
final requiredPinInterval = settingsStore.pinTimeOutDuration;

View file

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// For now, we can create a utility function to handle this.
//
// However, we could look into abstracting the entire FlutterSecureStorage package
// so the app doesn't depend on the package directly but an absraction.
// It'll make these kind of modifications to read/write come from a single point.
Future<String?> readSecureStorage(FlutterSecureStorage secureStorage, String key) async {
String? result;
const maxWait = Duration(seconds: 3);
const checkInterval = Duration(milliseconds: 200);
DateTime start = DateTime.now();
while (result == null && DateTime.now().difference(start) < maxWait) {
result = await secureStorage.read(key: key);
if (result != null) {
break;
}
await Future.delayed(checkInterval);
}
return result;
}

View file

@ -201,6 +201,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
@ -807,8 +808,11 @@ Future<void> setup({
getIt
.registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactory<MoonPaySellProvider>(() => MoonPaySellProvider(
settingsStore: getIt.get<AppStore>().settingsStore, wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
settingsStore: getIt.get<AppStore>().settingsStore,
wallet: getIt.get<AppStore>().wallet!,
isTestEnvironment: kDebugMode,
));
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
getIt.get<AppStore>().settingsStore,

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
@ -107,7 +108,7 @@ class BackgroundTasks {
final SyncMode syncMode = settingsStore.currentSyncMode;
final bool syncAll = settingsStore.currentSyncAll;
if (syncMode.type == SyncType.disabled) {
if (syncMode.type == SyncType.disabled || !FeatureFlag.isBackgroundSyncEnabled) {
cancelSyncTask();
return;
}

View file

@ -10,6 +10,7 @@ class BiometricAuth {
return await _localAuth.authenticate(
localizedReason: S.current.biometric_auth_reason,
options: AuthenticationOptions(
biometricOnly: true,
useErrorDialogs: true,
stickyAuth: false));
} on PlatformException catch (e) {

View file

@ -20,6 +20,7 @@ class PreferencesKey {
static const isAppSecureKey = 'is_app_secure';
static const disableBuyKey = 'disable_buy';
static const disableSellKey = 'disable_sell';
static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order';
static const walletListAscending = 'wallet_list_ascending';

View file

@ -11,7 +11,7 @@ enum ProviderType {
robinhood,
dfx,
onramper,
moonpaySell,
moonpay,
}
extension ProviderTypeName on ProviderType {
@ -25,7 +25,7 @@ extension ProviderTypeName on ProviderType {
return 'DFX Connect';
case ProviderType.onramper:
return 'Onramper';
case ProviderType.moonpaySell:
case ProviderType.moonpay:
return 'MoonPay';
}
}
@ -40,7 +40,7 @@ extension ProviderTypeName on ProviderType {
return 'dfx_connect_provider';
case ProviderType.onramper:
return 'onramper_provider';
case ProviderType.moonpaySell:
case ProviderType.moonpay:
return 'moonpay_provider';
}
}
@ -55,18 +55,18 @@ class ProvidersHelper {
case WalletType.monero:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
case WalletType.bitcoin:
case WalletType.polygon:
case WalletType.ethereum:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.dfx,
ProviderType.robinhood,
ProviderType.moonpay,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.none:
@ -79,28 +79,22 @@ class ProvidersHelper {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.ethereum:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.moonpaySell,
ProviderType.dfx,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.moonpaySell];
case WalletType.polygon:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.moonpaySell,
ProviderType.moonpay,
ProviderType.dfx,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.moonpay];
case WalletType.solana:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpaySell,
ProviderType.moonpay,
];
case WalletType.monero:
case WalletType.nano:
@ -119,10 +113,10 @@ class ProvidersHelper {
return getIt.get<DFXBuyProvider>();
case ProviderType.onramper:
return getIt.get<OnRamperBuyProvider>();
case ProviderType.moonpay:
return getIt.get<MoonPayProvider>();
case ProviderType.askEachTime:
return null;
case ProviderType.moonpaySell:
return getIt.get<MoonPaySellProvider>();
}
}
}

View file

@ -77,7 +77,8 @@ class CWEthereum extends Ethereum {
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as EVMChainTransactionPriority,
currency: currency,

View file

@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
static const exolix =
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
static const thorChain =
ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png');
static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
@ -41,6 +43,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return trocador;
case 6:
return exolix;
case 8:
return thorChain;
case 7:
return all;
default:

View file

@ -133,7 +133,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final distributionPath = await DistributionInfo.instance.getDistributionPath();
final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0;
final payload = {
@ -202,7 +206,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(),
amount: responseJSON['fromAmount']?.toString() ?? request.fromAmount,
state: TradeState.created,
payoutAddress: payoutAddress);
payoutAddress: payoutAddress,
isSendAll: isSendAll);
}
@override

View file

@ -28,7 +28,8 @@ abstract class ExchangeProvider {
Future<Limits> fetchLimits(
{required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode});
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode});
Future<Trade> createTrade(
{required TradeRequest request, required bool isFixedRateMode, required bool isSendAll});
Future<Trade> findTradeById({required String id});

View file

@ -130,7 +130,11 @@ class ExolixExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final headers = {'Content-Type': 'application/json'};
final body = {
'coinFrom': _normalizeCurrency(request.fromCurrency),
@ -180,7 +184,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(),
amount: amount,
state: TradeState.created,
payoutAddress: payoutAddress);
payoutAddress: payoutAddress,
isSendAll: isSendAll);
}
@override

View file

@ -144,7 +144,11 @@ class SideShiftExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
String url = '';
final body = {
'affiliateId': affiliateId,
@ -197,6 +201,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
amount: depositAmount ?? request.fromAmount,
payoutAddress: settleAddress,
createdAt: DateTime.now(),
isSendAll: isSendAll,
);
}

View file

@ -117,7 +117,11 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final headers = {'Content-Type': 'application/json'};
final params = {'api_key': apiKey};
final body = <String, dynamic>{
@ -162,6 +166,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
amount: request.fromAmount,
payoutAddress: payoutAddress,
createdAt: DateTime.now(),
isSendAll: isSendAll,
);
}

View file

@ -0,0 +1,255 @@
import 'dart:convert';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
class ThorChainExchangeProvider extends ExchangeProvider {
ThorChainExchangeProvider({required this.tradesStore})
: super(pairList: supportedPairs(_notSupported));
static final List<CryptoCurrency> _notSupported = [
...(CryptoCurrency.all
.where((element) => ![
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.ltc,
CryptoCurrency.bch,
CryptoCurrency.aave,
CryptoCurrency.dai,
CryptoCurrency.gusd,
CryptoCurrency.usdc,
CryptoCurrency.usdterc20,
CryptoCurrency.wbtc,
].contains(element))
.toList())
];
static final isRefundAddressSupported = [CryptoCurrency.eth];
static const _baseURL = 'thornode.ninerealms.com';
static const _quotePath = '/thorchain/quote/swap';
static const _txInfoPath = '/thorchain/tx/status/';
static const _affiliateName = 'cakewallet';
static const _affiliateBps = '175';
final Box<Trade> tradesStore;
@override
String get title => 'THORChain';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => false;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain;
@override
Future<bool> checkIsAvailable() async => true;
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
try {
if (amount == 0) return 0.0;
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(amount),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
return _thorChainAmountToDouble(expectedAmountOut) / amount;
} catch (e) {
print(e.toString());
return 0.0;
}
}
@override
Future<Limits> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(1),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final minAmountIn = responseJSON['recommended_min_amount_in'] as String? ?? '0.0';
return Limits(min: _thorChainAmountToDouble(minAmountIn));
}
@override
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
String formattedToAddress = request.toAddress.startsWith('bitcoincash:')
? request.toAddress.replaceFirst('bitcoincash:', '')
: request.toAddress;
final formattedFromAmount = double.parse(request.fromAmount);
final params = {
'from_asset': _normalizeCurrency(request.fromCurrency),
'to_asset': _normalizeCurrency(request.toCurrency),
'amount': _doubleToThorChainString(formattedFromAmount),
'destination': formattedToAddress,
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps,
'refund_address':
isRefundAddressSupported.contains(request.fromCurrency) ? request.refundAddress : '',
};
final responseJSON = await _getSwapQuote(params);
final inputAddress = responseJSON['inbound_address'] as String?;
final memo = responseJSON['memo'] as String?;
return Trade(
id: '',
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
inputAddress: inputAddress,
createdAt: DateTime.now(),
amount: request.fromAmount,
state: TradeState.notFound,
payoutAddress: request.toAddress,
memo: memo,
isSendAll: isSendAll);
}
@override
Future<Trade> findTradeById({required String id}) async {
if (id.isEmpty) throw Exception('Trade id is empty');
final formattedId = id.startsWith('0x') ? id.substring(2) : id;
final uri = Uri.https(_baseURL, '$_txInfoPath$formattedId');
final response = await http.get(uri);
if (response.statusCode == 404) {
throw Exception('Trade not found for id: $formattedId');
} else if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body);
final Map<String, dynamic> stagesJson = responseJSON['stages'] as Map<String, dynamic>;
final inboundObservedStarted = stagesJson['inbound_observed']?['started'] as bool? ?? true;
if (!inboundObservedStarted) {
throw Exception('Trade has not started for id: $formattedId');
}
final currentState = _updateStateBasedOnStages(stagesJson) ?? TradeState.notFound;
final tx = responseJSON['tx'];
final String fromAddress = tx['from_address'] as String? ?? '';
final String toAddress = tx['to_address'] as String? ?? '';
final List<dynamic> coins = tx['coins'] as List<dynamic>;
final String? memo = tx['memo'] as String?;
final parts = memo?.split(':') ?? [];
final String toChain = parts.length > 1 ? parts[1].split('.')[0] : '';
final String toAsset = parts.length > 1 && parts[1].split('.').length > 1
? parts[1].split('.')[1].split('-')[0]
: '';
final formattedToChain = CryptoCurrency.fromString(toChain);
final toAssetWithChain = CryptoCurrency.fromString(toAsset, walletCurrency: formattedToChain);
final plannedOutTxs = responseJSON['planned_out_txs'] as List<dynamic>?;
final isRefund = plannedOutTxs?.any((tx) => tx['refund'] == true) ?? false;
return Trade(
id: id,
from: CryptoCurrency.fromString(tx['chain'] as String? ?? ''),
to: toAssetWithChain,
provider: description,
inputAddress: fromAddress,
payoutAddress: toAddress,
amount: coins.first['amount'] as String? ?? '0.0',
state: currentState,
memo: memo,
isRefund: isRefund,
);
}
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
Uri uri = Uri.https(_baseURL, _quotePath, params);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
if (response.body.contains('error')) {
throw Exception('Unexpected response: ${response.body}');
}
return json.decode(response.body) as Map<String, dynamic>;
}
String _normalizeCurrency(CryptoCurrency currency) {
final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.title;
return '$networkTitle.${currency.title}';
}
String _doubleToThorChainString(double amount) => (amount * 1e8).toInt().toString();
double _thorChainAmountToDouble(String amount) => double.parse(amount) / 1e8;
TradeState? _updateStateBasedOnStages(Map<String, dynamic> stages) {
TradeState? currentState;
if (stages['inbound_observed']['completed'] as bool? ?? false) {
currentState = TradeState.confirmation;
}
if (stages['inbound_confirmation_counted']['completed'] as bool? ?? false) {
currentState = TradeState.confirmed;
}
if (stages['inbound_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.processing;
}
if (stages['swap_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.traded;
}
if (stages['outbound_signed']['completed'] as bool? ?? false) {
currentState = TradeState.success;
}
return currentState;
}
}

View file

@ -13,7 +13,8 @@ import 'package:http/http.dart';
class TrocadorExchangeProvider extends ExchangeProvider {
TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}})
: _lastUsedRateId = '', _provider = [],
: _lastUsedRateId = '',
_provider = [],
super(pairList: supportedPairs(_notSupported));
bool useTorOnly;
@ -23,7 +24,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
'Swapter',
'StealthEx',
'Simpleswap',
'Swapuz'
'Swapuz',
'ChangeNow',
'Changehero',
'FixedFloat',
@ -144,8 +145,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final params = {
'api_key': apiKey,
'ticker_from': _normalizeCurrency(request.fromCurrency),
@ -172,7 +176,6 @@ class TrocadorExchangeProvider extends ExchangeProvider {
params['id'] = _lastUsedRateId;
}
String firstAvailableProvider = '';
for (var provider in _provider) {
@ -225,7 +228,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
providerName: providerName,
createdAt: DateTime.tryParse(date)?.toLocal(),
amount: responseJSON['amount_from']?.toString() ?? request.fromAmount,
payoutAddress: payoutAddress);
payoutAddress: payoutAddress,
isSendAll: isSendAll);
}
@override

View file

@ -27,7 +27,11 @@ class Trade extends HiveObject {
this.password,
this.providerId,
this.providerName,
this.fromWalletAddress
this.fromWalletAddress,
this.memo,
this.txId,
this.isRefund,
this.isSendAll,
}) {
if (provider != null) providerRaw = provider.raw;
@ -105,6 +109,18 @@ class Trade extends HiveObject {
@HiveField(17)
String? fromWalletAddress;
@HiveField(18)
String? memo;
@HiveField(19)
String? txId;
@HiveField(20)
bool? isRefund;
@HiveField(21)
bool? isSendAll;
static Trade fromMap(Map<String, Object?> map) {
return Trade(
id: map['id'] as String,
@ -115,8 +131,11 @@ class Trade extends HiveObject {
map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null,
amount: map['amount'] as String,
walletId: map['wallet_id'] as String,
fromWalletAddress: map['from_wallet_address'] as String?
);
fromWalletAddress: map['from_wallet_address'] as String?,
memo: map['memo'] as String?,
txId: map['tx_id'] as String?,
isRefund: map['isRefund'] as bool?,
isSendAll: map['isSendAll'] as bool?);
}
Map<String, dynamic> toMap() {
@ -128,7 +147,11 @@ class Trade extends HiveObject {
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
'amount': amount,
'wallet_id': walletId,
'from_wallet_address': fromWalletAddress
'from_wallet_address': fromWalletAddress,
'memo': memo,
'tx_id': txId,
'isRefund': isRefund,
'isSendAll': isSendAll,
};
}

View file

@ -41,6 +41,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
static const success = TradeState(raw: 'success', title: 'Success');
static TradeState deserialize({required String raw}) {
switch (raw) {
case 'NOT_FOUND':
return notFound;
case 'pending':
return pending;
case 'confirming':
@ -98,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'sending':
return sending;
case 'success':
case 'done':
return success;
default:
throw Exception('Unexpected token: $raw in TradeState deserialize');

View file

@ -75,8 +75,22 @@ class CWSolana extends Solana {
}
@override
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
Future<void> addSPLToken(
WalletBase wallet,
CryptoCurrency token,
String contractAddress,
) async {
final splToken = SPLToken(
name: token.name,
symbol: token.title,
mintAddress: contractAddress,
decimal: token.decimals,
mint: token.name.toUpperCase(),
enabled: token.enabled,
);
await (wallet as SolanaWallet).addSPLToken(splToken);
}
@override
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
@ -116,4 +130,9 @@ class CWSolana extends Solana {
return null;
}
@override
double? getEstimateFees(WalletBase wallet) {
return (wallet as SolanaWallet).estimatedFee;
}
}

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
@ -25,45 +26,46 @@ class BuySellOptionsPage extends BasePage {
? dashboardViewModel.availableBuyProviders
: dashboardViewModel.availableSellProviders;
return Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return ScrollableWithBottomSection(
content: Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
Spacer(),
Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),
],
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
],
),
),
),
),
bottomSection: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),

View file

@ -60,7 +60,7 @@ class BuyWebViewPageBodyState extends State<BuyWebViewPageBody> {
_saveOrder(keyword: 'completed', splitSymbol: '/');
}
if (widget.buyViewModel.selectedProvider is MoonPayBuyProvider) {
if (widget.buyViewModel.selectedProvider is MoonPayProvider) {
_saveOrder(keyword: 'transactionId', splitSymbol: '=');
}
}

View file

@ -103,7 +103,16 @@ class _DashboardPageView extends BasePage {
Widget get endDrawer => MenuWidget(dashboardViewModel);
@override
Widget leading(BuildContext context) => ServicesUpdatesWidget(dashboardViewModel.getServicesStatus());
Widget leading(BuildContext context) {
return Observer(
builder: (context) {
if (dashboardViewModel.isEnabledBulletinAction) {
return ServicesUpdatesWidget(dashboardViewModel.getServicesStatus());
}
return const SizedBox();
},
);
}
@override
Widget middle(BuildContext context) {

View file

@ -195,12 +195,14 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
onPressed: () async {
if (_formKey.currentState!.validate() &&
(!_showDisclaimer || _disclaimerChecked)) {
await widget.homeSettingsViewModel.addToken(Erc20Token(
name: _tokenNameController.text,
symbol: _tokenSymbolController.text,
await widget.homeSettingsViewModel.addToken(
token: CryptoCurrency(
name: _tokenNameController.text,
title: _tokenSymbolController.text.toUpperCase(),
decimals: int.parse(_tokenDecimalController.text),
),
contractAddress: _contractAddressController.text,
decimal: int.parse(_tokenDecimalController.text),
));
);
if (context.mounted) {
Navigator.pop(context);
}

View file

@ -9,7 +9,7 @@ class FilterTile extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 24.0),
child: child,
);
}

View file

@ -20,6 +20,7 @@ class SyncIndicatorIcon extends StatelessWidget {
static const String created = 'created';
static const String fetching = 'fetching';
static const String finished = 'finished';
static const String success = 'success';
@override
Widget build(BuildContext context) {
@ -45,6 +46,7 @@ class SyncIndicatorIcon extends StatelessWidget {
indicatorColor = Colors.red;
break;
case finished:
case success:
indicatorColor = PaletteDark.brightGreen;
break;
default:

View file

@ -34,7 +34,9 @@ class TradeRow extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getPoweredImage(provider)!,
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset(provider.image, width: 36, height: 36)),
SizedBox(width: 12),
Expanded(
child: Column(
@ -69,38 +71,4 @@ class TradeRow extends StatelessWidget {
),
));
}
Widget? _getPoweredImage(ExchangeProviderDescription provider) {
Widget? image;
switch (provider) {
case ExchangeProviderDescription.xmrto:
image = Image.asset('assets/images/xmrto.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.changeNow:
image = Image.asset('assets/images/changenow.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.morphToken:
image = Image.asset('assets/images/morph.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.sideShift:
image = Image.asset('assets/images/sideshift.png', width: 36, height: 36);
break;
case ExchangeProviderDescription.simpleSwap:
image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36);
break;
case ExchangeProviderDescription.trocador:
image = ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/trocador.png', width: 36, height: 36));
break;
case ExchangeProviderDescription.exolix:
image = Image.asset('assets/images/exolix.png', width: 36, height: 36);
break;
default:
image = null;
}
return image;
}
}

View file

@ -1,3 +1,5 @@
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/core/auth_service.dart';
@ -60,7 +62,7 @@ class ExchangePage extends BasePage {
final _receiveAmountFocus = FocusNode();
final _receiveAddressFocus = FocusNode();
final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
final _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
@ -184,7 +186,13 @@ class ExchangePage extends BasePage {
StandardCheckbox(
value: exchangeViewModel.isFixedRateMode,
caption: S.of(context).fixed_rate,
onChanged: (value) => exchangeViewModel.isFixedRateMode = value,
onChanged: (value) {
if (value) {
exchangeViewModel.enableFixedRateMode();
} else {
exchangeViewModel.isFixedRateMode = false;
}
},
),
],
)),
@ -431,7 +439,9 @@ class ExchangePage extends BasePage {
}
if (state is TradeIsCreatedSuccessfully) {
exchangeViewModel.reset();
Navigator.of(context).pushNamed(Routes.exchangeConfirm);
(exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain)
? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
: Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
}
});
@ -470,6 +480,13 @@ class ExchangePage extends BasePage {
if (depositAmountController.text != exchangeViewModel.depositAmount &&
depositAmountController.text != S.of(context).all) {
exchangeViewModel.isSendAllEnabled = false;
final isThorChain = exchangeViewModel.selectedProviders
.any((provider) => provider is ThorChainExchangeProvider);
_depositAmountDebounce = isThorChain
? Debounce(Duration(milliseconds: 1000))
: Debounce(Duration(milliseconds: 500));
_depositAmountDebounce.run(() {
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
exchangeViewModel.isReceiveAmountEntered = false;
@ -517,7 +534,7 @@ class ExchangePage extends BasePage {
_receiveAmountFocus.addListener(() {
if (_receiveAmountFocus.hasFocus) {
exchangeViewModel.isFixedRateMode = true;
exchangeViewModel.enableFixedRateMode();
}
// exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
});

View file

@ -485,14 +485,14 @@ class ExchangeCardState extends State<ExchangeCard> {
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).overwrite_amount,
alertContent: S.of(context).qr_payment_amount,
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
alertTitle: S.of(dialogContext).overwrite_amount,
alertContent: S.of(dialogContext).qr_payment_amount,
rightButtonText: S.of(dialogContext).ok,
leftButtonText: S.of(dialogContext).cancel,
actionRightButton: () {
widget.amountFocusNode?.requestFocus();
amountController.text = paymentRequest.amount;
Navigator.of(context).pop();
Navigator.of(dialogContext).pop();
},
actionLeftButton: () => Navigator.of(dialogContext).pop());
});

View file

@ -262,6 +262,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
fee: S.of(popupContext).send_fee,
feeValue: widget.exchangeTradeViewModel.sendViewModel
.pendingTransaction!.feeFormatted,
feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate,
rightButtonText: S.of(popupContext).send,
leftButtonText: S.of(popupContext).cancel,
actionRightButton: () async {

View file

@ -100,6 +100,12 @@ class _AdvancedPrivacySettingsBodyState extends State<AdvancedPrivacySettingsBod
Observer(builder: (_) {
return Column(
children: [
SettingsSwitcherCell(
title: S.current.disable_bulletin,
value: widget.privacySettingsViewModel.disableBulletin,
onValueChange: (BuildContext _, bool value) {
widget.privacySettingsViewModel.setDisableBulletin(value);
}),
SettingsSwitcherCell(
title: S.current.add_custom_node,
value: widget.privacySettingsViewModel.addCustomNode,

View file

@ -426,6 +426,7 @@ class SendPage extends BasePage {
fee: isEVMCompatibleChain(sendViewModel.walletType)
? S.of(_dialogContext).send_estimated_fee
: S.of(_dialogContext).send_fee,
feeRate: sendViewModel.pendingTransaction!.feeRate,
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: sendViewModel.outputs,

View file

@ -16,6 +16,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
@ -36,6 +37,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -90,6 +92,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
amountValue: amountValue,
fiatAmountValue: fiatAmountValue,
fee: fee,
feeRate: feeRate,
feeValue: feeValue,
feeFiatAmount: feeFiatAmount,
outputs: outputs);
@ -103,6 +106,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs});
@ -113,6 +117,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -125,6 +130,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
amountValue: amountValue,
fiatAmountValue: fiatAmountValue,
fee: fee,
feeRate: feeRate,
feeValue: feeValue,
feeFiatAmount: feeFiatAmount,
outputs: outputs);
@ -138,6 +144,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs})
@ -153,6 +160,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -183,7 +191,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
return Stack(alignment: Alignment.center, clipBehavior: Clip.none, children: [
Container(
height: 200,
height: feeRate != null ? 250 : 200,
child: SingleChildScrollView(
controller: controller,
child: Column(
@ -311,6 +319,36 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
)
],
)),
if (feeRate != null && feeRate!.isNotEmpty)
Padding(
padding: EdgeInsets.only(top: 16),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S.current.send_estimated_fee,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
Text(
"$feeRate sat/byte",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
)
],
)),
Padding(
padding: EdgeInsets.only(top: 16),
child: Column(

View file

@ -323,8 +323,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
),
if (!sendViewModel.isBatchSending &&
sendViewModel.shouldDisplaySendALL)
if (!sendViewModel.isBatchSending)
Positioned(
top: 2,
right: 0,
@ -456,7 +455,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (sendViewModel.hasFees)
Observer(
builder: (_) => GestureDetector(
onTap: () => _setTransactionPriority(context),
onTap: sendViewModel.hasFeesPriority
? () => _setTransactionPriority(context)
: () {},
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(

View file

@ -15,7 +15,6 @@ import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -43,7 +42,7 @@ class ConnectionSyncPage extends BasePage {
title: S.current.rescan,
handler: (context) => Navigator.of(context).pushNamed(Routes.rescan),
),
if (DeviceInfo.instance.isMobile) ...[
if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[
Observer(builder: (context) {
return SettingsPickerCell<SyncMode>(
title: S.current.background_sync_mode,

View file

@ -80,6 +80,12 @@ class PrivacyPage extends BasePage {
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableSell(value);
}),
SettingsSwitcherCell(
title: S.current.disable_bulletin,
value: _privacySettingsViewModel.disableBulletin,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBulletin(value);
}),
if (_privacySettingsViewModel.canUseEtherscan)
SettingsSwitcherCell(
title: S.current.etherscan_history,

View file

@ -46,9 +46,6 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
itemBuilder: (_, int index) {
return Observer(builder: (_) {
final item = unspentCoinsListViewModel.items[index];
final address = unspentCoinsListViewModel.wallet.type == WalletType.bitcoinCash
? bitcoinCash!.getCashAddrFormat(item.address)
: item.address;
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails,
@ -56,7 +53,7 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
child: UnspentCoinsListItem(
note: item.note,
amount: item.amount,
address: address,
address: item.address,
isSending: item.isSending,
isFrozen: item.isFrozen,
isChange: item.isChange,

View file

@ -27,10 +27,12 @@ class UnspentCoinsListItem extends StatelessWidget {
Widget build(BuildContext context) {
final unselectedItemColor = Theme.of(context).cardColor;
final selectedItemColor = Theme.of(context).primaryColor;
final itemColor = isSending ? selectedItemColor : unselectedItemColor;
final amountColor =
isSending ? Colors.white : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final itemColor = isSending
? selectedItemColor
: unselectedItemColor;
final amountColor = isSending
? Colors.white
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final addressColor = isSending
? Colors.white.withOpacity(0.5)
: Theme.of(context).extension<CakeTextTheme>()!.buttonSecondaryTextColor;
@ -85,7 +87,7 @@ class UnspentCoinsListItem extends StatelessWidget {
child: Text(
S.of(context).frozen,
style: TextStyle(
color: amountColor, fontSize: 7, fontWeight: FontWeight.w600),
color: Colors.black, fontSize: 7, fontWeight: FontWeight.w600),
)),
],
),

View file

@ -111,7 +111,7 @@ class _ServicesUpdatesWidgetState extends State<ServicesUpdatesWidget> {
color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
width: 30,
),
if (state.hasData && state.data!.hasUpdates)
if (state.hasData && state.data!.hasUpdates && !wasOpened)
Container(
height: 7,
width: 7,

View file

@ -3,18 +3,20 @@ import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
part'trade_filter_store.g.dart';
part 'trade_filter_store.g.dart';
class TradeFilterStore = TradeFilterStoreBase with _$TradeFilterStore;
abstract class TradeFilterStoreBase with Store {
TradeFilterStoreBase() : displayXMRTO = true,
TradeFilterStoreBase()
: displayXMRTO = true,
displayChangeNow = true,
displaySideShift = true,
displayMorphToken = true,
displaySimpleSwap = true,
displayTrocador = true,
displayExolix = true;
displayExolix = true,
displayThorChain = true;
@observable
bool displayXMRTO;
@ -37,8 +39,17 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayExolix;
@observable
bool displayThorChain;
@computed
bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador && displayExolix;
bool get displayAllTrades =>
displayChangeNow &&
displaySideShift &&
displaySimpleSwap &&
displayTrocador &&
displayExolix &&
displayThorChain;
@action
void toggleDisplayExchange(ExchangeProviderDescription provider) {
@ -64,6 +75,9 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.exolix:
displayExolix = !displayExolix;
break;
case ExchangeProviderDescription.thorChain:
displayThorChain = !displayThorChain;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
@ -73,6 +87,7 @@ abstract class TradeFilterStoreBase with Store {
displaySimpleSwap = false;
displayTrocador = false;
displayExolix = false;
displayThorChain = false;
} else {
displayChangeNow = true;
displaySideShift = true;
@ -81,6 +96,7 @@ abstract class TradeFilterStoreBase with Store {
displaySimpleSwap = true;
displayTrocador = true;
displayExolix = true;
displayThorChain = true;
}
break;
}
@ -96,16 +112,13 @@ abstract class TradeFilterStoreBase with Store {
? _trades
.where((item) =>
(displayXMRTO && item.trade.provider == ExchangeProviderDescription.xmrto) ||
(displaySideShift &&
item.trade.provider == ExchangeProviderDescription.sideShift) ||
(displayChangeNow &&
item.trade.provider == ExchangeProviderDescription.changeNow) ||
(displayMorphToken &&
item.trade.provider == ExchangeProviderDescription.morphToken) ||
(displaySimpleSwap &&
item.trade.provider == ExchangeProviderDescription.simpleSwap) ||
(displaySideShift && item.trade.provider == ExchangeProviderDescription.sideShift) ||
(displayChangeNow && item.trade.provider == ExchangeProviderDescription.changeNow) ||
(displayMorphToken && item.trade.provider == ExchangeProviderDescription.morphToken) ||
(displaySimpleSwap && item.trade.provider == ExchangeProviderDescription.simpleSwap) ||
(displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) ||
(displayExolix && item.trade.provider == ExchangeProviderDescription.exolix))
(displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) ||
(displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain))
.toList()
: _trades;
}

View file

@ -59,6 +59,7 @@ abstract class SettingsStoreBase with Store {
required bool initialAppSecure,
required bool initialDisableBuy,
required bool initialDisableSell,
required bool initialDisableBulletin,
required WalletListOrderType initialWalletListOrder,
required bool initialWalletListAscending,
required FiatApiMode initialFiatMode,
@ -130,6 +131,7 @@ abstract class SettingsStoreBase with Store {
isAppSecure = initialAppSecure,
disableBuy = initialDisableBuy,
disableSell = initialDisableSell,
disableBulletin = initialDisableBulletin,
walletListOrder = initialWalletListOrder,
walletListAscending = initialWalletListAscending,
shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
@ -291,6 +293,11 @@ abstract class SettingsStoreBase with Store {
(bool disableSell) =>
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
reaction(
(_) => disableBulletin,
(bool disableBulletin) =>
sharedPreferences.setBool(PreferencesKey.disableBulletinKey, disableBulletin));
reaction(
(_) => walletListOrder,
(WalletListOrderType walletListOrder) =>
@ -554,6 +561,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool disableSell;
@observable
bool disableBulletin;
@observable
WalletListOrderType walletListOrder;
@ -778,6 +788,7 @@ abstract class SettingsStoreBase with Store {
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
final walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
final walletListAscending =
@ -1030,6 +1041,7 @@ abstract class SettingsStoreBase with Store {
initialAppSecure: isAppSecure,
initialDisableBuy: disableBuy,
initialDisableSell: disableSell,
initialDisableBulletin: disableBulletin,
initialWalletListOrder: walletListOrder,
initialWalletListAscending: walletListAscending,
initialFiatMode: currentFiatApiMode,
@ -1148,6 +1160,7 @@ abstract class SettingsStoreBase with Store {
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;

View file

@ -2,4 +2,5 @@ class FeatureFlag {
static const bool isCakePayEnabled = false;
static const bool isExolixEnabled = true;
static const bool isInAppTorEnabled = false;
static const bool isBackgroundSyncEnabled = false;
}

View file

@ -20,6 +20,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
@computed
FiatApiMode get fiatApiMode => _settingsStore.fiatApiMode;
@computed
bool get disableBulletin => _settingsStore.disableBulletin;
@observable
bool _addCustomNode = false;
@ -64,6 +67,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
@action
void setExchangeApiMode(ExchangeApiMode value) => _settingsStore.exchangeStatus = value;
@action
void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value;
@action
void toggleAddCustomNode() => _addCustomNode = !_addCustomNode;

View file

@ -71,7 +71,7 @@ abstract class AnonpayDetailsViewModelBase with Store {
]);
items.add(TrackTradeListItem(
title: 'Track',
title: S.current.track,
value: invoiceDetail.clearnetStatusUrl,
onTap: () => launchUrlString(invoiceDetail.clearnetStatusUrl)));
}

View file

@ -93,18 +93,6 @@ abstract class BuyViewModelBase with Store {
_providerList.add(WyreBuyProvider(wallet: wallet));
}
var isMoonPayEnabled = false;
try {
isMoonPayEnabled = await MoonPayBuyProvider.onEnabled();
} catch (e) {
isMoonPayEnabled = false;
print(e.toString());
}
if (isMoonPayEnabled) {
_providerList.add(MoonPayBuyProvider(wallet: wallet));
}
items = _providerList.map((provider) =>
BuyItem(provider: provider, buyAmountViewModel: buyAmountViewModel))
.toList();

View file

@ -46,6 +46,8 @@ abstract class ContactListViewModelBase with Store {
name,
walletTypeToCryptoCurrency(info.type),
));
// Only one contact address per wallet
return;
});
} else if (info.address != null) {
walletContacts.add(WalletContact(

View file

@ -120,6 +120,11 @@ abstract class DashboardViewModelBase with Store {
caption: ExchangeProviderDescription.exolix.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
FilterItem(
value: () => tradeFilterStore.displayThorChain,
caption: ExchangeProviderDescription.thorChain.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
]
},
subname = '',
@ -355,6 +360,9 @@ abstract class DashboardViewModelBase with Store {
@observable
bool hasSellAction;
@computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
ReactionDisposer? _onMoneroAccountChangeReaction;
ReactionDisposer? _onMoneroBalanceChangeReaction;

View file

@ -44,17 +44,37 @@ abstract class HomeSettingsViewModelBase with Store {
@action
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
Future<void> addToken(CryptoCurrency token) async {
Future<void> addToken({
required String contractAddress,
required CryptoCurrency token,
}) async {
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
await ethereum!.addErc20Token(_balanceViewModel.wallet, token);
final erc20token = Erc20Token(
name: token.name,
symbol: token.title,
decimal: token.decimals,
contractAddress: contractAddress,
);
await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token);
}
if (_balanceViewModel.wallet.type == WalletType.polygon) {
await polygon!.addErc20Token(_balanceViewModel.wallet, token);
final polygonToken = Erc20Token(
name: token.name,
symbol: token.title,
decimal: token.decimals,
contractAddress: contractAddress,
);
await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken);
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
await solana!.addSPLToken(_balanceViewModel.wallet, token);
await solana!.addSPLToken(
_balanceViewModel.wallet,
token,
contractAddress,
);
}
_updateTokensList();
@ -117,7 +137,8 @@ abstract class HomeSettingsViewModelBase with Store {
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
solana!.addSPLToken(_balanceViewModel.wallet, token);
final address = solana!.getTokenAddress(token);
solana!.addSPLToken(_balanceViewModel.wallet, token, address);
}
_refreshTokensList();

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
@ -47,6 +48,9 @@ abstract class ExchangeTradeViewModelBase with Store {
case ExchangeProviderDescription.exolix:
_provider = ExolixExchangeProvider();
break;
case ExchangeProviderDescription.thorChain:
_provider = ThorChainExchangeProvider(tradesStore: trades);
break;
}
_updateItems();
@ -100,13 +104,21 @@ abstract class ExchangeTradeViewModelBase with Store {
final output = sendViewModel.outputs.first;
output.address = trade.inputAddress ?? '';
output.setCryptoAmount(trade.amount);
if (_provider is ThorChainExchangeProvider) output.memo = trade.memo;
if (trade.isSendAll == true) output.sendAll = true;
sendViewModel.selectedCryptoCurrency = trade.from;
await sendViewModel.createTransaction();
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
if (_provider is ThorChainExchangeProvider) {
trade.id = pendingTransaction?.id ?? '';
trades.add(trade);
}
}
@action
Future<void> _updateTrade() async {
try {
final agreedAmount = tradesStore.trade!.amount;
final isSendAll = tradesStore.trade!.isSendAll;
final updatedTrade = await _provider!.findTradeById(id: trade.id);
if (updatedTrade.createdAt == null && trade.createdAt != null)
@ -115,6 +127,8 @@ abstract class ExchangeTradeViewModelBase with Store {
if (updatedTrade.amount.isEmpty) updatedTrade.amount = trade.amount;
trade = updatedTrade;
trade.amount = agreedAmount;
trade.isSendAll = isSendAll;
_updateItems();
} catch (e) {
@ -127,8 +141,10 @@ abstract class ExchangeTradeViewModelBase with Store {
tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : '';
final tagTo = tradesStore.trade!.to.tag != null ? '${tradesStore.trade!.to.tag}' + ' ' : '';
items.clear();
items.add(ExchangeTradeItem(
title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true));
if (trade.provider != ExchangeProviderDescription.thorChain)
items.add(ExchangeTradeItem(
title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true));
if (trade.extraId != null) {
final title = trade.from == CryptoCurrency.xrp

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
@ -9,6 +10,7 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/exchange/limits.dart';
@ -18,6 +20,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
@ -96,7 +99,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
/// if the provider is not in the user settings (user's first time or newly added provider)
/// then use its default value decided by us
selectedProviders = ObservableList.of(providersForCurrentPair()
selectedProviders = ObservableList.of(providerList
.where((element) => exchangeProvidersSelection[element.title] == null
? element.isEnabled
: (exchangeProvidersSelection[element.title] as bool))
@ -148,6 +151,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
SimpleSwapExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
ThorChainExchangeProvider(tradesStore: trades),
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
];
@ -466,6 +470,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> createTrade() async {
if (isSendAllEnabled) {
await calculateDepositAllAmount();
final amount = double.tryParse(depositAmount);
if (limits.min != null && amount != null && amount < limits.min!) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.amount_is_below_minimum_limit(limits.min!.toString()));
return;
}
}
try {
for (var provider in _sortedAvailableProviders.values) {
if (!(await provider.checkIsAvailable())) continue;
@ -492,12 +508,23 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
else {
try {
tradeState = TradeIsCreating();
final trade =
await provider.createTrade(request: request, isFixedRateMode: isFixedRateMode);
final trade = await provider.createTrade(
request: request,
isFixedRateMode: isFixedRateMode,
isSendAll: isSendAllEnabled,
);
trade.walletId = wallet.id;
trade.fromWalletAddress = wallet.walletAddresses.address;
if (!isCanCreateTrade(trade)) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.thorchain_taproot_address_not_supported);
return;
}
tradesStore.setTrade(trade);
await trades.add(trade);
if (trade.provider != ExchangeProviderDescription.thorChain) await trades.add(trade);
tradeState = TradeIsCreatedSuccessfully(trade: trade);
/// return after the first successful trade
@ -539,25 +566,24 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void enableSendAllAmount() {
isSendAllEnabled = true;
isFixedRateMode = false;
calculateDepositAllAmount();
}
@action
void enableFixedRateMode() {
isSendAllEnabled = false;
isFixedRateMode = true;
}
@action
Future<void> calculateDepositAllAmount() async {
if (wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash) {
final availableBalance = wallet.balance[wallet.currency]!.available;
final priority = _settingsStore.priority[wallet.type]!;
final fee = wallet.calculateEstimatedFee(priority, null);
if (availableBalance < fee || availableBalance == 0) return;
final amount = availableBalance - fee;
changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
} else if (wallet.type == WalletType.bitcoin) {
if (wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.bitcoinCash) {
final priority = _settingsStore.priority[wallet.type]!;
final amount = await bitcoin!.estimateFakeSendAllTxAmount(
wallet, bitcoin!.deserializeBitcoinTransactionPriority(priority.raw));
final amount = await bitcoin!.estimateFakeSendAllTxAmount(wallet, priority);
changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
}
@ -749,4 +775,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
int get depositMaxDigits => depositCurrency.decimals;
int get receiveMaxDigits => receiveCurrency.decimals;
bool isCanCreateTrade(Trade trade) {
if (trade.provider == ExchangeProviderDescription.thorChain) {
final payoutAddress = trade.payoutAddress ?? '';
final fromWalletAddress = trade.fromWalletAddress ?? '';
final tapRootPattern = RegExp(P2trAddress.regex.pattern);
if (tapRootPattern.hasMatch(payoutAddress) || tapRootPattern.hasMatch(fromWalletAddress)) {
return false;
}
}
return true;
}
}

View file

@ -27,7 +27,7 @@ abstract class OrderDetailsViewModelBase with Store {
_provider = WyreBuyProvider(wallet: wallet);
break;
case BuyProviderDescription.moonPay:
_provider = MoonPayBuyProvider(wallet: wallet);
// _provider = MoonPayProvider(wallet: wallet);// TODO: CW-521
break;
}
}
@ -50,9 +50,9 @@ abstract class OrderDetailsViewModelBase with Store {
@action
Future<void> _updateOrder() async {
try {
if (_provider != null && (_provider is MoonPayBuyProvider || _provider is WyreBuyProvider)) {
final updatedOrder = _provider is MoonPayBuyProvider
? await (_provider as MoonPayBuyProvider).findOrderById(order.id)
if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
final updatedOrder = _provider is MoonPayProvider
? await (_provider as MoonPayProvider).findOrderById(order.id)
: await (_provider as WyreBuyProvider).findOrderById(order.id);
updatedOrder.from = order.from;
updatedOrder.to = order.to;
@ -89,17 +89,17 @@ abstract class OrderDetailsViewModelBase with Store {
value: order.provider.title)
);
if (_provider != null && (_provider is MoonPayBuyProvider || _provider is WyreBuyProvider)) {
if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
final trackUrl = _provider is MoonPayBuyProvider
? (_provider as MoonPayBuyProvider).trackUrl
final trackUrl = _provider is MoonPayProvider
? (_provider as MoonPayProvider).trackUrl
: (_provider as WyreBuyProvider).trackUrl;
if (trackUrl.isNotEmpty ?? false) {
final buildURL = trackUrl + '${order.transferId}';
items.add(
TrackTradeListItem(
title: 'Track',
title: S.current.track,
value: buildURL,
onTap: () {
try {

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
@ -66,6 +67,8 @@ abstract class OutputBase with Store {
@observable
String extractedAddress;
String? memo;
@computed
bool get isParsedAddress =>
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
@ -114,6 +117,10 @@ abstract class OutputBase with Store {
@computed
double get estimatedFee {
try {
if (_wallet.type == WalletType.solana) {
return solana!.getEstimateFees(_wallet) ?? 0.0;
}
final fee = _wallet.calculateEstimatedFee(
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
@ -175,6 +182,7 @@ abstract class OutputBase with Store {
fiatAmount = '';
address = '';
note = '';
memo = null;
resetParsedAddress();
}

View file

@ -2,6 +2,8 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/contact_record.dart';
@ -12,6 +14,7 @@ import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
@ -103,8 +106,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed
bool get isBatchSending => outputs.length > 1;
bool get shouldDisplaySendALL => walletType != WalletType.solana;
@computed
String get pendingTransactionFiatAmount {
if (pendingTransaction == null) {
@ -205,6 +206,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed
bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano;
@computed
bool get hasFeesPriority =>
wallet.type != WalletType.nano &&
wallet.type != WalletType.banano &&
wallet.type != WalletType.solana;
@observable
CryptoCurrency selectedCryptoCurrency;
@ -296,14 +302,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
@action
Future<void> createTransaction() async {
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
try {
state = IsExecutingState();
pendingTransaction = await wallet.createTransaction(_credentials());
if (provider is ThorChainExchangeProvider) {
final outputCount = pendingTransaction?.outputCount ?? 0;
if (outputCount > 10) throw Exception("ThorChain does not support more than 10 outputs");
}
state = ExecutedSuccessfullyState();
return pendingTransaction;
} catch (e) {
print('Failed with ${e.toString()}');
state = FailureState(e.toString());
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
}
}
@ -345,8 +355,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
state = TransactionCommitted();
} catch (e) {
String translatedError = translateErrorMessage(e.toString(), wallet.type, wallet.currency);
state = FailureState(translatedError);
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
}
}
@ -421,11 +430,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
ContactRecord? newContactAddress () {
ContactRecord? newContactAddress() {
final Set<String> contactAddresses =
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
for (var output in outputs) {
String address;
@ -436,7 +444,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
if (address.isNotEmpty && !contactAddresses.contains(address)) {
return ContactRecord(
contactListViewModel.contactSource,
Contact(
@ -450,22 +457,59 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
String translateErrorMessage(
String error,
Object error,
WalletType walletType,
CryptoCurrency currency,
) {
String errorMessage = error.toString();
if (walletType == WalletType.ethereum ||
walletType == WalletType.polygon ||
walletType == WalletType.solana ||
walletType == WalletType.haven) {
if (error.contains('gas required exceeds allowance') ||
error.contains('insufficient funds')) {
if (errorMessage.contains('gas required exceeds allowance') ||
errorMessage.contains('insufficient funds')) {
return S.current.do_not_have_enough_gas_asset(currency.toString());
}
return error;
return errorMessage;
}
return error;
if (walletType == WalletType.bitcoin ||
walletType == WalletType.litecoin ||
walletType == WalletType.bitcoinCash) {
if (error is TransactionWrongBalanceException) {
return S.current.tx_wrong_balance_exception(currency.toString());
}
if (error is TransactionNoInputsException) {
return S.current.tx_not_enough_inputs_exception;
}
if (error is TransactionNoFeeException) {
return S.current.tx_zero_fee_exception;
}
if (error is TransactionNoDustException) {
return S.current.tx_no_dust_exception;
}
if (error is TransactionCommitFailed) {
return S.current.tx_commit_failed;
}
if (error is TransactionCommitFailedDustChange) {
return S.current.tx_rejected_dust_change;
}
if (error is TransactionCommitFailedDustOutput) {
return S.current.tx_rejected_dust_output;
}
if (error is TransactionCommitFailedDustOutputSendAll) {
return S.current.tx_rejected_dust_output_send_all;
}
if (error is TransactionCommitFailedVoutNegative) {
return S.current.tx_rejected_vout_negative;
}
if (error is TransactionNoDustOnChangeException) {
return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max);
}
}
return errorMessage;
}
}

View file

@ -59,6 +59,9 @@ abstract class PrivacySettingsViewModelBase with Store {
@computed
bool get disableSell => _settingsStore.disableSell;
@computed
bool get disableBulletin => _settingsStore.disableBulletin;
@computed
bool get useEtherscan => _settingsStore.useEtherscan;
@ -106,6 +109,9 @@ abstract class PrivacySettingsViewModelBase with Store {
@action
void setDisableSell(bool value) => _settingsStore.disableSell = value;
@action
void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value;
@action
void setLookupsTwitter(bool value) => _settingsStore.lookupsTwitter = value;

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
@ -52,6 +53,9 @@ abstract class TradeDetailsViewModelBase with Store {
case ExchangeProviderDescription.exolix:
_provider = ExolixExchangeProvider();
break;
case ExchangeProviderDescription.thorChain:
_provider = ThorChainExchangeProvider(tradesStore: trades);
break;
}
_updateItems();
@ -62,6 +66,24 @@ abstract class TradeDetailsViewModelBase with Store {
}
}
static String? getTrackUrl(ExchangeProviderDescription provider, Trade trade) {
switch (provider) {
case ExchangeProviderDescription.changeNow:
return 'https://changenow.io/exchange/txs/${trade.id}';
case ExchangeProviderDescription.sideShift:
return 'https://sideshift.ai/orders/${trade.id}';
case ExchangeProviderDescription.simpleSwap:
return 'https://simpleswap.io/exchange?id=${trade.id}';
case ExchangeProviderDescription.trocador:
return 'https://trocador.app/en/checkout/${trade.id}';
case ExchangeProviderDescription.exolix:
return 'https://exolix.com/transaction/${trade.id}';
case ExchangeProviderDescription.thorChain:
return 'https://track.ninerealms.com/${trade.id}';
}
return null;
}
final Box<Trade> trades;
@observable
@ -125,46 +147,26 @@ abstract class TradeDetailsViewModelBase with Store {
items.add(StandartListItem(
title: S.current.trade_details_provider, value: trade.provider.toString()));
if (trade.provider == ExchangeProviderDescription.changeNow) {
final buildURL = 'https://changenow.io/exchange/txs/${trade.id.toString()}';
final trackUrl = TradeDetailsViewModelBase.getTrackUrl(trade.provider, trade);
if (trackUrl != null) {
items.add(TrackTradeListItem(
title: 'Track',
value: buildURL,
onTap: () {
_launchUrl(buildURL);
}));
title: S.current.track, value: trackUrl, onTap: () => _launchUrl(trackUrl)));
}
if (trade.provider == ExchangeProviderDescription.sideShift) {
final buildURL = 'https://sideshift.ai/orders/${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
}
if (trade.provider == ExchangeProviderDescription.simpleSwap) {
final buildURL = 'https://simpleswap.io/exchange?id=${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
if (trade.isRefund == true) {
items.add(StandartListItem(
title: 'Refund', value: trade.refundAddress ?? ''));
}
if (trade.provider == ExchangeProviderDescription.trocador) {
final buildURL = 'https://trocador.app/en/checkout/${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
items.add(StandartListItem(
title: '${trade.providerName} ${S.current.id.toUpperCase()}',
value: trade.providerId ?? ''));
if (trade.password != null && trade.password!.isNotEmpty)
if (trade.password != null && trade.password!.isNotEmpty) {
items.add(StandartListItem(
title: '${trade.providerName} ${S.current.password}', value: trade.password ?? ''));
}
if (trade.provider == ExchangeProviderDescription.exolix) {
final buildURL = 'https://exolix.com/transaction/${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
}
}
}

View file

@ -100,7 +100,5 @@ abstract class UnspentCoinsDetailsViewModelBase with Store {
final WalletType _type;
List<TransactionDetailsListItem> items;
String get formattedAddress => WalletType.bitcoinCash == _type
? bitcoinCash!.getCashAddrFormat(unspentCoinsItem.address)
: unspentCoinsItem.address;
String get formattedAddress => unspentCoinsItem.address;
}

View file

@ -43,6 +43,7 @@
"already_have_account": "لديك حساب؟",
"always": "دائماً",
"amount": "مقدار:",
"amount_is_below_minimum_limit": "سيكون رصيدك بعد الرسوم أقل من الحد الأدنى للمبلغ اللازم للتبادل (${min})",
"amount_is_estimate": "المبلغ المستلم هو تقدير",
"amount_is_guaranteed": "مبلغ الاستلام مضمون",
"and": "و",
@ -186,6 +187,7 @@
"digit_pin": "-رقم PIN",
"digital_and_physical_card": " بطاقة ائتمان رقمية ومادية مسبقة الدفع",
"disable": "إبطال",
"disable_bulletin": "تعطيل نشرة حالة الخدمة",
"disable_buy": "تعطيل إجراء الشراء",
"disable_cake_2fa": "تعطيل 2 عامل المصادقة",
"disable_exchange": "تعطيل التبادل",
@ -646,6 +648,7 @@
"template_name": "اسم القالب",
"third_intro_content": "يعيش Yats خارج Cake Wallet أيضًا. يمكن استبدال أي عنوان محفظة على وجه الأرض بـ Yat!",
"third_intro_title": "يتماشي Yat بلطف مع الآخرين",
"thorchain_taproot_address_not_supported": "لا يدعم مزود Thorchain عناوين Taproot. يرجى تغيير العنوان أو تحديد مزود مختلف.",
"time": "${minutes}د ${seconds}س",
"tip": "بقشيش:",
"today": "اليوم",
@ -663,6 +666,7 @@
"totp_code": "كود TOTP",
"totp_secret_code": "كود TOTP السري",
"totp_verification_success": "تم التحقق بنجاح!",
"track": " ﺭﺎﺴﻣ",
"trade_details_copied": "تم نسخ ${title} إلى الحافظة",
"trade_details_created_at": "أنشئت في",
"trade_details_fetching": "جار الجلب",
@ -713,6 +717,16 @@
"transactions": "المعاملات",
"transactions_by_date": "المعاملات حسب التاريخ",
"trusted": "موثوق به",
"tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.",
"tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.",
"tx_no_dust_exception": "يتم رفض المعاملة عن طريق إرسال مبلغ صغير جدًا. يرجى محاولة زيادة المبلغ.",
"tx_not_enough_inputs_exception": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة",
"tx_rejected_dust_change": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، ومبلغ التغيير المنخفض (الغبار). حاول إرسال كل أو تقليل المبلغ.",
"tx_rejected_dust_output": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، وكمية الإخراج المنخفض (الغبار). يرجى زيادة المبلغ.",
"tx_rejected_dust_output_send_all": "المعاملة التي يتم رفضها بموجب قواعد الشبكة ، وكمية الإخراج المنخفض (الغبار). يرجى التحقق من رصيد العملات المعدنية المحددة تحت التحكم في العملة.",
"tx_rejected_vout_negative": "لا يوجد ما يكفي من الرصيد لدفع رسوم هذه الصفقة. يرجى التحقق من رصيد العملات المعدنية تحت السيطرة على العملة.",
"tx_wrong_balance_exception": "ليس لديك ما يكفي من ${currency} لإرسال هذا المبلغ.",
"tx_zero_fee_exception": "لا يمكن إرسال معاملة مع 0 رسوم. حاول زيادة المعدل أو التحقق من اتصالك للحصول على أحدث التقديرات.",
"unavailable_balance": "ﺮﻓﻮﺘﻣ ﺮﻴﻏ ﺪﻴﺻﺭ",
"unavailable_balance_description": ".ﺎﻫﺪﻴﻤﺠﺗ ءﺎﻐﻟﺇ ﺭﺮﻘﺗ ﻰﺘﺣ ﺕﻼﻣﺎﻌﻤﻠﻟ ﻝﻮﺻﻮﻠﻟ ﺔﻠﺑﺎﻗ ﺮﻴﻏ ﺓﺪﻤﺠﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﻞﻈﺗ ﺎﻤﻨﻴﺑ ،ﺎﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻣﺎﻌﻤﻟﺍ ﻝﺎﻤﺘﻛﺍ ﺩﺮﺠﻤﺑ ﺔﺣﺎﺘﻣ ﺔﻠﻔﻘﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﺢﺒﺼﺘﺳ .ﻚﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻤﻌﻟﺍ ﻲﻓ ﻢﻜﺤﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻂﺸﻧ ﻞﻜﺸﺑ ﺎﻫﺪﻴﻤﺠﺘﺑ ﺖﻤﻗ",
"unconfirmed": "رصيد غير مؤكد",

View file

@ -43,6 +43,7 @@
"already_have_account": "Вече имате профил?",
"always": "Винаги",
"amount": "Сума: ",
"amount_is_below_minimum_limit": "Вашето салдо след такси ще бъде по -малко от минималната сума, необходима за борсата (${min})",
"amount_is_estimate": "Сумата за получаване е ",
"amount_is_guaranteed": "Сумата за получаване е гарантирана",
"and": "и",
@ -186,6 +187,7 @@
"digit_pin": "-цифрен PIN",
"digital_and_physical_card": " дигитална или физическа предплатена дебитна карта",
"disable": "Деактивиране",
"disable_bulletin": "Деактивирайте бюлетина за състоянието на услугата",
"disable_buy": "Деактивирайте действието за покупка",
"disable_cake_2fa": "Деактивирайте Cake 2FA",
"disable_exchange": "Деактивиране на борса",
@ -646,6 +648,7 @@
"template_name": "Име на шаблон",
"third_intro_content": "Yats също живее извън Cake Wallet. Всеки адрес на портфейл може да бъде заменен с Yat!",
"third_intro_title": "Yat добре се сработва с други",
"thorchain_taproot_address_not_supported": "Доставчикът на Thorchain не поддържа адреси на TapRoot. Моля, променете адреса или изберете друг доставчик.",
"time": "${minutes} мин ${seconds} сек",
"tip": "Tip:",
"today": "Днес",
@ -663,6 +666,7 @@
"totp_code": "TOTP код",
"totp_secret_code": "TOTP таен код",
"totp_verification_success": "Проверката е успешна!",
"track": "Писта",
"trade_details_copied": "${title} копирано",
"trade_details_created_at": "Създадено",
"trade_details_fetching": "Обработка",
@ -713,6 +717,16 @@
"transactions": "Транзакции",
"transactions_by_date": "Транзакции по дата",
"trusted": "Надежден",
"tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.",
"tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.",
"tx_no_dust_exception": "Сделката се отхвърля чрез изпращане на сума твърде малка. Моля, опитайте да увеличите сумата.",
"tx_not_enough_inputs_exception": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети",
"tx_rejected_dust_change": "Транзакция, отхвърлена от мрежови правила, ниска сума на промяна (прах). Опитайте да изпратите всички или да намалите сумата.",
"tx_rejected_dust_output": "Транзакция, отхвърлена от мрежови правила, ниска стойност на изхода (прах). Моля, увеличете сумата.",
"tx_rejected_dust_output_send_all": "Транзакция, отхвърлена от мрежови правила, ниска стойност на изхода (прах). Моля, проверете баланса на монетите, избрани под контрол на монети.",
"tx_rejected_vout_negative": "Няма достатъчно баланс, за да платите за таксите на тази транзакция. Моля, проверете баланса на монетите под контрол на монетите.",
"tx_wrong_balance_exception": "Нямате достатъчно ${currency}, за да изпратите тази сума.",
"tx_zero_fee_exception": "Не може да изпраща транзакция с 0 такса. Опитайте да увеличите скоростта или да проверите връзката си за най -новите оценки.",
"unavailable_balance": "Неналично салдо",
"unavailable_balance_description": "Неналично салдо: Тази обща сума включва средства, които са заключени в чакащи транзакции и тези, които сте замразили активно в настройките за контрол на монетите. Заключените баланси ще станат достъпни, след като съответните им транзакции бъдат завършени, докато замразените баланси остават недостъпни за транзакции, докато не решите да ги размразите.",
"unconfirmed": "Непотвърден баланс",

View file

@ -43,6 +43,7 @@
"already_have_account": "Máte už účet?",
"always": "Vždy",
"amount": "Částka: ",
"amount_is_below_minimum_limit": "Váš zůstatek po poplatcích by byl menší než minimální částka potřebná pro burzu (${min})",
"amount_is_estimate": "Částka, kterou dostanete, je jen odhad.",
"amount_is_guaranteed": "Částka, kterou dostanete, je konečná",
"and": "a",
@ -186,6 +187,7 @@
"digit_pin": "-číselný PIN",
"digital_and_physical_card": " digitální a fyzické předplacené debetní karty,",
"disable": "Zakázat",
"disable_bulletin": "Zakázat status servisního stavu",
"disable_buy": "Zakázat akci nákupu",
"disable_cake_2fa": "Zakázat Cake 2FA",
"disable_exchange": "Zakázat směnárny",
@ -646,6 +648,7 @@
"template_name": "Název šablony",
"third_intro_content": "Yat existuje i mimo Cake Wallet. Jakákoliv adresa peněženky na světě může být nahrazena Yatem!",
"third_intro_title": "Yat dobře spolupracuje s ostatními",
"thorchain_taproot_address_not_supported": "Poskytovatel Thorchain nepodporuje adresy Taproot. Změňte adresu nebo vyberte jiného poskytovatele.",
"time": "${minutes}m ${seconds}s",
"tip": "Spropitné:",
"today": "Dnes",
@ -663,6 +666,7 @@
"totp_code": "Kód TOTP",
"totp_secret_code": "Tajný kód TOTP",
"totp_verification_success": "Ověření proběhlo úspěšně!",
"track": "Dráha",
"trade_details_copied": "${title} zkopírováno do schránky",
"trade_details_created_at": "Vytvořeno v",
"trade_details_fetching": "Získávám",
@ -713,6 +717,16 @@
"transactions": "Transakce",
"transactions_by_date": "Transakce podle data",
"trusted": "Důvěřovat",
"tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.",
"tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.",
"tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.",
"tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí",
"tx_rejected_dust_change": "Transakce zamítnuta podle síťových pravidel, množství nízké změny (prach). Zkuste odeslat vše nebo snížit částku.",
"tx_rejected_dust_output": "Transakce zamítnuta síťovými pravidly, nízkým množstvím výstupu (prach). Zvyšte prosím částku.",
"tx_rejected_dust_output_send_all": "Transakce zamítnuta síťovými pravidly, nízkým množstvím výstupu (prach). Zkontrolujte prosím zůstatek mincí vybraných pod kontrolou mincí.",
"tx_rejected_vout_negative": "Nedostatek zůstatek na zaplacení poplatků za tuto transakci. Zkontrolujte prosím zůstatek mincí pod kontrolou mincí.",
"tx_wrong_balance_exception": "Nemáte dost ${currency} pro odeslání této částky.",
"tx_zero_fee_exception": "Nelze odeslat transakci s 0 poplatkem. Zkuste zvýšit sazbu nebo zkontrolovat připojení pro nejnovější odhady.",
"unavailable_balance": "Nedostupný zůstatek",
"unavailable_balance_description": "Nedostupný zůstatek: Tento součet zahrnuje prostředky, které jsou uzamčeny v nevyřízených transakcích a ty, které jste aktivně zmrazili v nastavení kontroly mincí. Uzamčené zůstatky budou k dispozici po dokončení příslušných transakcí, zatímco zmrazené zůstatky zůstanou pro transakce nepřístupné, dokud se nerozhodnete je uvolnit.",
"unconfirmed": "Nepotvrzený zůstatek",

View file

@ -43,6 +43,7 @@
"already_have_account": "Sie haben bereits ein Konto?",
"always": "immer",
"amount": "Betrag: ",
"amount_is_below_minimum_limit": "Ihr Saldo nach Gebühren wäre geringer als der für den Austausch benötigte Mindestbetrag (${min})",
"amount_is_estimate": "Der empfangene Betrag ist eine Schätzung",
"amount_is_guaranteed": "Der Empfangsbetrag ist garantiert",
"and": "Und",
@ -186,6 +187,7 @@
"digit_pin": "-stellige PIN",
"digital_and_physical_card": "digitale und physische Prepaid-Debitkarte",
"disable": "Deaktivieren",
"disable_bulletin": "Deaktivieren Sie das Bulletin des Service Status",
"disable_buy": "Kaufaktion deaktivieren",
"disable_cake_2fa": "Cake 2FA deaktivieren",
"disable_exchange": "Exchange deaktivieren",
@ -413,8 +415,8 @@
"placeholder_transactions": "Ihre Transaktionen werden hier angezeigt",
"please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist",
"please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.",
"Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
"please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.",
"Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
"please_select": "Bitte auswählen:",
"please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.",
"please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden",
@ -647,6 +649,7 @@
"template_name": "Vorlagenname",
"third_intro_content": "Yats leben auch außerhalb von Cake Wallet. Jede Wallet-Adresse auf der Welt kann durch ein Yat ersetzt werden!",
"third_intro_title": "Yat spielt gut mit anderen",
"thorchain_taproot_address_not_supported": "Der Thorchain -Anbieter unterstützt keine Taproot -Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.",
"time": "${minutes}m ${seconds}s",
"tip": "Hinweis:",
"today": "Heute",
@ -664,6 +667,7 @@
"totp_code": "TOTP-Code",
"totp_secret_code": "TOTP-Geheimcode",
"totp_verification_success": "Verifizierung erfolgreich!",
"track": "Schiene",
"trade_details_copied": "${title} in die Zwischenablage kopiert",
"trade_details_created_at": "Erzeugt am",
"trade_details_fetching": "Wird ermittelt",
@ -714,6 +718,16 @@
"transactions": "Transaktionen",
"transactions_by_date": "Transaktionen nach Datum",
"trusted": "Vertrauenswürdige",
"tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.",
"tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.",
"tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.",
"tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus",
"tx_rejected_dust_change": "Transaktion abgelehnt durch Netzwerkregeln, niedriger Änderungsbetrag (Staub). Versuchen Sie, alle zu senden oder die Menge zu reduzieren.",
"tx_rejected_dust_output": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte erhöhen Sie den Betrag.",
"tx_rejected_dust_output_send_all": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte überprüfen Sie den Gleichgewicht der unter Münzkontrolle ausgewählten Münzen.",
"tx_rejected_vout_negative": "Nicht genug Guthaben, um die Gebühren dieser Transaktion zu bezahlen. Bitte überprüfen Sie den Restbetrag der Münzen unter Münzkontrolle.",
"tx_wrong_balance_exception": "Sie haben nicht genug ${currency}, um diesen Betrag zu senden.",
"tx_zero_fee_exception": "Transaktion kann nicht mit 0 Gebühren gesendet werden. Versuchen Sie, die Rate zu erhöhen oder Ihre Verbindung auf die neuesten Schätzungen zu überprüfen.",
"unavailable_balance": "Nicht verfügbares Guthaben",
"unavailable_balance_description": "Nicht verfügbares Guthaben: Diese Summe umfasst Gelder, die in ausstehenden Transaktionen gesperrt sind, und solche, die Sie in Ihren Münzkontrolleinstellungen aktiv eingefroren haben. Gesperrte Guthaben werden verfügbar, sobald die entsprechenden Transaktionen abgeschlossen sind, während eingefrorene Guthaben für Transaktionen nicht zugänglich bleiben, bis Sie sich dazu entschließen, sie wieder freizugeben.",
"unconfirmed": "Unbestätigter Saldo",

View file

@ -43,6 +43,7 @@
"already_have_account": "Already have an account?",
"always": "Always",
"amount": "Amount: ",
"amount_is_below_minimum_limit": "Your balance after fees would be less than the minimum amount needed for the exchange (${min})",
"amount_is_estimate": "The receive amount is an estimate",
"amount_is_guaranteed": "The receive amount is guaranteed",
"and": "and",
@ -186,6 +187,7 @@
"digit_pin": "-digit PIN",
"digital_and_physical_card": " digital and physical prepaid debit card",
"disable": "Disable",
"disable_bulletin": "Disable service status bulletin",
"disable_buy": "Disable buy action",
"disable_cake_2fa": "Disable Cake 2FA",
"disable_exchange": "Disable exchange",
@ -646,6 +648,7 @@
"template_name": "Template Name",
"third_intro_content": "Yats live outside of Cake Wallet, too. Any wallet address on earth can be replaced with a Yat!",
"third_intro_title": "Yat plays nicely with others",
"thorchain_taproot_address_not_supported": "The ThorChain provider does not support Taproot addresses. Please change the address or select a different provider.",
"time": "${minutes}m ${seconds}s",
"tip": "Tip:",
"today": "Today",
@ -663,6 +666,7 @@
"totp_code": "TOTP Code",
"totp_secret_code": "TOTP Secret Code",
"totp_verification_success": "Verification Successful!",
"track": "Track",
"trade_details_copied": "${title} copied to Clipboard",
"trade_details_created_at": "Created at",
"trade_details_fetching": "Fetching",
@ -713,6 +717,16 @@
"transactions": "Transactions",
"transactions_by_date": "Transactions by date",
"trusted": "Trusted",
"tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.",
"tx_commit_failed": "Transaction commit failed. Please contact support.",
"tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.",
"tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control",
"tx_rejected_dust_change": "Transaction rejected by network rules, low change amount (dust). Try sending ALL or reducing the amount.",
"tx_rejected_dust_output": "Transaction rejected by network rules, low output amount (dust). Please increase the amount.",
"tx_rejected_dust_output_send_all": "Transaction rejected by network rules, low output amount (dust). Please check the balance of coins selected under Coin Control.",
"tx_rejected_vout_negative": "Not enough balance to pay for this transaction's fees. Please check the balance of coins under Coin Control.",
"tx_wrong_balance_exception": "You do not have enough ${currency} to send this amount.",
"tx_zero_fee_exception": "Cannot send transaction with 0 fee. Try increasing the rate or checking your connection for latest estimates.",
"unavailable_balance": "Unavailable balance",
"unavailable_balance_description": "Unavailable Balance: This total includes funds that are locked in pending transactions and those you have actively frozen in your coin control settings. Locked balances will become available once their respective transactions are completed, while frozen balances remain inaccessible for transactions until you decide to unfreeze them.",
"unconfirmed": "Unconfirmed Balance",

View file

@ -43,6 +43,7 @@
"already_have_account": "¿Ya tienes una cuenta?",
"always": "siempre",
"amount": "Cantidad: ",
"amount_is_below_minimum_limit": "Su saldo después de las tarifas sería menor que la cantidad mínima necesaria para el intercambio (${min})",
"amount_is_estimate": "El monto recibido es un estimado",
"amount_is_guaranteed": "La cantidad recibida está garantizada",
"and": "y",
@ -186,6 +187,7 @@
"digit_pin": "-dígito PIN",
"digital_and_physical_card": " tarjeta de débito prepago digital y física",
"disable": "Desactivar",
"disable_bulletin": "Desactivar el boletín de estado del servicio",
"disable_buy": "Desactivar acción de compra",
"disable_cake_2fa": "Desactivar pastel 2FA",
"disable_exchange": "Deshabilitar intercambio",
@ -647,6 +649,7 @@
"template_name": "Nombre de la plantilla",
"third_intro_content": "Los Yats también viven fuera de Cake Wallet. Cualquier dirección de billetera en la tierra se puede reemplazar con un Yat!",
"third_intro_title": "Yat juega muy bien con otras",
"thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambie la dirección o seleccione un proveedor diferente.",
"time": "${minutes}m ${seconds}s",
"tip": "Consejo:",
"today": "Hoy",
@ -664,6 +667,7 @@
"totp_code": "Código TOTP",
"totp_secret_code": "Código secreto TOTP",
"totp_verification_success": "¡Verificación exitosa!",
"track": "Pista",
"trade_details_copied": "${title} Copiado al portapapeles",
"trade_details_created_at": "Creado en",
"trade_details_fetching": "Cargando",
@ -714,6 +718,16 @@
"transactions": "Actas",
"transactions_by_date": "Transacciones por fecha",
"trusted": "de confianza",
"tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.",
"tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.",
"tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.",
"tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas",
"tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intente enviar todo o reducir la cantidad.",
"tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumente la cantidad.",
"tx_rejected_dust_output_send_all": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Verifique el saldo de monedas seleccionadas bajo control de monedas.",
"tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifique el saldo de monedas bajo control de monedas.",
"tx_wrong_balance_exception": "No tiene suficiente ${currency} para enviar esta cantidad.",
"tx_zero_fee_exception": "No se puede enviar transacciones con 0 tarifa. Intente aumentar la tasa o verificar su conexión para las últimas estimaciones.",
"unavailable_balance": "Saldo no disponible",
"unavailable_balance_description": "Saldo no disponible: este total incluye fondos que están bloqueados en transacciones pendientes y aquellos que usted ha congelado activamente en su configuración de control de monedas. Los saldos bloqueados estarán disponibles una vez que se completen sus respectivas transacciones, mientras que los saldos congelados permanecerán inaccesibles para las transacciones hasta que usted decida descongelarlos.",
"unconfirmed": "Saldo no confirmado",

View file

@ -43,6 +43,7 @@
"already_have_account": "Vous avez déjà un compte ?",
"always": "toujours",
"amount": "Montant : ",
"amount_is_below_minimum_limit": "Votre solde après les frais serait inférieur au montant minimum nécessaire à l'échange (${min})",
"amount_is_estimate": "Le montant reçu est estimé",
"amount_is_guaranteed": "Le montant reçu est garanti",
"and": "et",
@ -186,6 +187,7 @@
"digit_pin": " chiffres",
"digital_and_physical_card": "carte de débit prépayée numérique et physique",
"disable": "Désactiver",
"disable_bulletin": "Désactiver le bulletin de statut de service",
"disable_buy": "Désactiver l'action d'achat",
"disable_cake_2fa": "Désactiver Cake 2FA",
"disable_exchange": "Désactiver l'échange",
@ -646,6 +648,7 @@
"template_name": "Nom du modèle",
"third_intro_content": "Les Yats existent aussi en dehors de Cake Wallet. Toute adresse sur terre peut être remplacée par un Yat !",
"third_intro_title": "Yat est universel",
"thorchain_taproot_address_not_supported": "Le fournisseur de Thorchain ne prend pas en charge les adresses de tapoot. Veuillez modifier l'adresse ou sélectionner un autre fournisseur.",
"time": "${minutes}m ${seconds}s",
"tip": "Pourboire :",
"today": "Aujourd'hui",
@ -663,6 +666,7 @@
"totp_code": "Code TOTP",
"totp_secret_code": "Secret TOTP",
"totp_verification_success": "Vérification réussie !",
"track": "Piste",
"trade_details_copied": "${title} copié vers le presse-papier",
"trade_details_created_at": "Créé le",
"trade_details_fetching": "Récupération",
@ -713,6 +717,16 @@
"transactions": "Transactions",
"transactions_by_date": "Transactions par date",
"trusted": "de confiance",
"tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.",
"tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.",
"tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.",
"tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control",
"tx_rejected_dust_change": "Transaction rejetée par les règles du réseau, montant de faible variation (poussière). Essayez d'envoyer tout ou de réduire le montant.",
"tx_rejected_dust_output": "Transaction rejetée par les règles du réseau, faible quantité de sortie (poussière). Veuillez augmenter le montant.",
"tx_rejected_dust_output_send_all": "Transaction rejetée par les règles du réseau, faible quantité de sortie (poussière). Veuillez vérifier le solde des pièces sélectionnées sous le contrôle des pièces de monnaie.",
"tx_rejected_vout_negative": "Pas assez de solde pour payer les frais de cette transaction. Veuillez vérifier le solde des pièces sous le contrôle des pièces.",
"tx_wrong_balance_exception": "Vous n'avez pas assez ${currency} pour envoyer ce montant.",
"tx_zero_fee_exception": "Impossible d'envoyer une transaction avec 0 frais. Essayez d'augmenter le taux ou de vérifier votre connexion pour les dernières estimations.",
"unavailable_balance": "Solde indisponible",
"unavailable_balance_description": "Solde indisponible : ce total comprend les fonds bloqués dans les transactions en attente et ceux que vous avez activement gelés dans vos paramètres de contrôle des pièces. Les soldes bloqués deviendront disponibles une fois leurs transactions respectives terminées, tandis que les soldes gelés resteront inaccessibles aux transactions jusqu'à ce que vous décidiez de les débloquer.",
"unconfirmed": "Solde non confirmé",

View file

@ -43,6 +43,7 @@
"already_have_account": "Kuna da asusu?",
"always": "Koyaushe",
"amount": "Adadi:",
"amount_is_below_minimum_limit": "Daidaitarku bayan kudade zai zama ƙasa da mafi ƙarancin adadin da ake buƙata don musayar (${min}",
"amount_is_estimate": "Adadin da aka karɓa shine kimantawa",
"amount_is_guaranteed": "Adadin da aka karɓa yana da garanti",
"and": "kuma",
@ -186,6 +187,7 @@
"digit_pin": "-lambar PIN",
"digital_and_physical_card": "katin zare kudi na dijital da na zahiri",
"disable": "Kashe",
"disable_bulletin": "Musaki ma'aunin sabis na sabis",
"disable_buy": "Kashe alama",
"disable_cake_2fa": "Musaki Cake 2FA",
"disable_exchange": "Kashe musanya",
@ -648,6 +650,7 @@
"template_name": "Sunan Samfura",
"third_intro_content": "Yats suna zaune a wajen Kek Wallet, kuma. Ana iya maye gurbin kowane adireshin walat a duniya da Yat!",
"third_intro_title": "Yat yana wasa da kyau tare da wasu",
"thorchain_taproot_address_not_supported": "Mai ba da tallafi na ThorChain baya goyan bayan adreshin taproot. Da fatan za a canza adireshin ko zaɓi mai bayarwa daban.",
"time": "${minutes}m ${seconds}s",
"tip": "Tukwici:",
"today": "Yau",
@ -665,6 +668,7 @@
"totp_code": "Lambar totp",
"totp_secret_code": "Lambar sirri",
"totp_verification_success": "Tabbatar cin nasara!",
"track": "Waƙa",
"trade_details_copied": "${title} an kwafa zuwa cikin kwafin",
"trade_details_created_at": "An ƙirƙira a",
"trade_details_fetching": "Daukewa",
@ -715,6 +719,16 @@
"transactions": "Ma'amaloli",
"transactions_by_date": "Ma'amaloli ta kwanan wata",
"trusted": "Amintacce",
"tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.",
"tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.",
"tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.",
"tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin",
"tx_rejected_dust_change": "Ma'amala ta ƙi ta dokokin cibiyar sadarwa, ƙarancin canji (ƙura). Gwada aikawa da duka ko rage adadin.",
"tx_rejected_dust_output": "Ma'adar da aka ƙi ta dokokin cibiyar sadarwa, ƙananan fitarwa (ƙura). Da fatan za a ƙara adadin.",
"tx_rejected_dust_output_send_all": "Ma'adar da aka ƙi ta dokokin cibiyar sadarwa, ƙananan fitarwa (ƙura). Da fatan za a duba daidaiton tsabar kudi a ƙarƙashin ikon tsabar kudin.",
"tx_rejected_vout_negative": "Bai isa daidai ba don biyan wannan kudin ma'amala. Da fatan za a duba daidaiton tsabar kudi a ƙarƙashin ikon tsabar kudin.",
"tx_wrong_balance_exception": "Ba ku da isasshen ${currency} don aika wannan adadin.",
"tx_zero_fee_exception": "Ba zai iya aika ma'amala da kuɗi 0 ba. Gwada ƙara ƙimar ko bincika haɗin ku don mahimmin ƙididdiga.",
"unavailable_balance": "Ma'aunin da ba ya samuwa",
"unavailable_balance_description": "Ma'auni Babu: Wannan jimlar ya haɗa da kuɗi waɗanda ke kulle a cikin ma'amaloli da ke jiran aiki da waɗanda kuka daskare sosai a cikin saitunan sarrafa kuɗin ku. Ma'auni da aka kulle za su kasance da zarar an kammala ma'amalolinsu, yayin da daskararrun ma'auni ba za su iya samun damar yin ciniki ba har sai kun yanke shawarar cire su.",
"unconfirmed": "Ba a tabbatar ba",

View file

@ -43,6 +43,7 @@
"already_have_account": "क्या आपके पास पहले से एक खाता मौजूद है?",
"always": "हमेशा",
"amount": "रकम: ",
"amount_is_below_minimum_limit": "फीस के बाद आपका संतुलन विनिमय के लिए आवश्यक न्यूनतम राशि से कम होगा (${min})",
"amount_is_estimate": "प्राप्त राशि एक अनुमान है",
"amount_is_guaranteed": "प्राप्त राशि की गारंटी है",
"and": "और",
@ -186,6 +187,7 @@
"digit_pin": "-अंक पिन",
"digital_and_physical_card": "डिजिटल और भौतिक प्रीपेड डेबिट कार्ड",
"disable": "अक्षम करना",
"disable_bulletin": "सेवा स्थिति बुलेटिन अक्षम करें",
"disable_buy": "खरीद कार्रवाई अक्षम करें",
"disable_cake_2fa": "केक 2FA अक्षम करें",
"disable_exchange": "एक्सचेंज अक्षम करें",
@ -648,6 +650,7 @@
"template_name": "टेम्पलेट नाम",
"third_intro_content": "Yats Cake Wallet के बाहर भी रहता है। धरती पर किसी भी वॉलेट पते को Yat से बदला जा सकता है!",
"third_intro_title": "Yat दूसरों के साथ अच्छा खेलता है",
"thorchain_taproot_address_not_supported": "थोरचेन प्रदाता टैपरोट पते का समर्थन नहीं करता है। कृपया पता बदलें या एक अलग प्रदाता का चयन करें।",
"time": "${minutes}m ${seconds}s",
"tip": "टिप:",
"today": "आज",
@ -665,6 +668,7 @@
"totp_code": "टीओटीपी कोड",
"totp_secret_code": "टीओटीपी गुप्त कोड",
"totp_verification_success": "सत्यापन सफल!",
"track": "रास्ता",
"trade_details_copied": "${title} क्लिपबोर्ड पर नकल",
"trade_details_created_at": "पर बनाया गया",
"trade_details_fetching": "ला रहा है",
@ -715,6 +719,16 @@
"transactions": "लेन-देन",
"transactions_by_date": "तारीख से लेन-देन",
"trusted": "भरोसा",
"tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।",
"tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।",
"tx_no_dust_exception": "लेनदेन को बहुत छोटी राशि भेजकर अस्वीकार कर दिया जाता है। कृपया राशि बढ़ाने का प्रयास करें।",
"tx_not_enough_inputs_exception": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें",
"tx_rejected_dust_change": "नेटवर्क नियमों, कम परिवर्तन राशि (धूल) द्वारा खारिज किए गए लेनदेन। सभी भेजने या राशि को कम करने का प्रयास करें।",
"tx_rejected_dust_output": "नेटवर्क नियमों, कम आउटपुट राशि (धूल) द्वारा खारिज किए गए लेनदेन। कृपया राशि बढ़ाएं।",
"tx_rejected_dust_output_send_all": "नेटवर्क नियमों, कम आउटपुट राशि (धूल) द्वारा खारिज किए गए लेनदेन। कृपया सिक्का नियंत्रण के तहत चुने गए सिक्कों के संतुलन की जाँच करें।",
"tx_rejected_vout_negative": "इस लेनदेन की फीस के लिए भुगतान करने के लिए पर्याप्त शेष राशि नहीं है। कृपया सिक्के नियंत्रण के तहत सिक्कों के संतुलन की जाँच करें।",
"tx_wrong_balance_exception": "इस राशि को भेजने के लिए आपके पास पर्याप्त ${currency} नहीं है।",
"tx_zero_fee_exception": "0 शुल्क के साथ लेनदेन नहीं भेज सकते। नवीनतम अनुमानों के लिए दर बढ़ाने या अपने कनेक्शन की जांच करने का प्रयास करें।",
"unavailable_balance": "अनुपलब्ध शेष",
"unavailable_balance_description": "अनुपलब्ध शेष राशि: इस कुल में वे धनराशि शामिल हैं जो लंबित लेनदेन में बंद हैं और जिन्हें आपने अपनी सिक्का नियंत्रण सेटिंग्स में सक्रिय रूप से जमा कर रखा है। लॉक किए गए शेष उनके संबंधित लेन-देन पूरे होने के बाद उपलब्ध हो जाएंगे, जबकि जमे हुए शेष लेन-देन के लिए अप्राप्य रहेंगे जब तक कि आप उन्हें अनफ्रीज करने का निर्णय नहीं लेते।",
"unconfirmed": "अपुष्ट शेष राशि",

Some files were not shown because too many files have changed in this diff Show more