From cdf081edfdab1b4e05e3a8e83e709e624784cafc Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 28 Mar 2024 14:41:11 +0200 Subject: [PATCH] Cw 537 integrate thor chain swaps (#1280) * thorChain btc to eth swap * eth to btc swap * update the UI * update localization * Update thorchain_exchange.provider.dart * minor fixes * minor fix * fix min amount bug * revert amount_converter changes * fetching thorChain traid info * resolve evm related merge conflicts * minor fix * Fix eth transaction hash for Thorchain Integration * add new status endpoint and refund address for eth * Adjust affiliate fee * Fix conflicts with main * review comments + transaction filter item * taproot addresses check * added 10 outputs check * Update thorchain_exchange.provider.dart * minor fixes * update thorchain title * fix fetching rate for thorchain * Revert "fix fetching rate for thorchain" This reverts commit 3aa1386ecfbca14271bf01a73b424de19c4fd484. * fix thorchain exchange rate --------- Co-authored-by: OmarHatem --- assets/images/thorchain.png | Bin 0 -> 5478 bytes cw_bitcoin/lib/electrum_wallet.dart | 39 ++- .../lib/pending_bitcoin_transaction.dart | 3 + .../lib/src/bitcoin_cash_wallet.dart | 5 +- cw_core/lib/output_info.dart | 4 +- cw_core/lib/pending_transaction.dart | 1 + cw_evm/lib/evm_chain_client.dart | 9 + cw_evm/lib/evm_chain_wallet.dart | 8 + cw_evm/lib/pending_evm_chain_transaction.dart | 10 +- cw_polygon/lib/polygon_client.dart | 2 + lib/bitcoin/cw_bitcoin.dart | 3 +- lib/ethereum/cw_ethereum.dart | 3 +- .../exchange_provider_description.dart | 4 + .../provider/thorchain_exchange.provider.dart | 248 ++++++++++++++++++ lib/exchange/trade.dart | 24 +- lib/exchange/trade_state.dart | 3 + .../dashboard/widgets/filter_tile.dart | 2 +- .../widgets/sync_indicator_icon.dart | 2 + .../screens/dashboard/widgets/trade_row.dart | 38 +-- lib/src/screens/exchange/exchange_page.dart | 15 +- lib/store/dashboard/trade_filter_store.dart | 39 ++- .../anonpay_details_view_model.dart | 2 +- .../dashboard/dashboard_view_model.dart | 5 + .../exchange/exchange_trade_view_model.dart | 13 +- .../exchange/exchange_view_model.dart | 29 +- lib/view_model/order_details_view_model.dart | 2 +- lib/view_model/send/output.dart | 3 + lib/view_model/send/send_view_model.dart | 10 +- lib/view_model/trade_details_view_model.dart | 58 ++-- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 2 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 55 files changed, 534 insertions(+), 102 deletions(-) create mode 100644 assets/images/thorchain.png create mode 100644 lib/exchange/provider/thorchain_exchange.provider.dart diff --git a/assets/images/thorchain.png b/assets/images/thorchain.png new file mode 100644 index 0000000000000000000000000000000000000000..674b60f824c16848aaef27b347588ffeb8051deb GIT binary patch literal 5478 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 aYt7G+@%vUxf9L outputs, int? feeRate, BitcoinTransactionPriority? priority, - {int? inputsCount}) async { + {int? inputsCount, + String? memo}) async { final utxos = []; List privateKeys = []; @@ -253,7 +254,11 @@ abstract class ElectrumWalletBase } final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, outputs: outputs, network: network); + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); int fee = feeRate != null ? feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize) @@ -300,7 +305,13 @@ abstract class ElectrumWalletBase } } - return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount); + return EstimatedTxResult( + utxos: utxos, + privateKeys: privateKeys, + fee: fee, + amount: amount, + memo: memo, + ); } @override @@ -348,13 +359,17 @@ abstract class ElectrumWalletBase outputs, transactionCredentials.feeRate, transactionCredentials.priority, + memo: transactionCredentials.outputs.first.memo, ); final txb = BitcoinTransactionBuilder( - utxos: estimatedTx.utxos, - outputs: outputs, - fee: BigInt.from(estimatedTx.fee), - network: network); + 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 @@ -888,13 +903,19 @@ 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, + this.memo, + }); final List utxos; final List privateKeys; final int fee; final int amount; + final String? memo; } BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index fa413febd..b45931133 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -31,6 +31,9 @@ class PendingBitcoinTransaction with PendingTransaction { @override String get feeFormatted => bitcoinAmountToString(amount: fee); + @override + int? get outputCount => _tx.outputs.length; + final List _listeners; @override diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index f5835e728..40ddcfa57 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -140,6 +140,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { var allInputsAmount = 0; + final String? opReturnMemo = outputs.first.memo; + if (unspentCoins.isEmpty) await updateUnspent(); for (final utx in unspentCoins) { @@ -282,6 +284,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { txb.addOutput(changeAddress, changeValue); } + if (opReturnMemo != null) txb.addOutputData(opReturnMemo); + for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; final keyPair = generateKeyPair( @@ -290,7 +294,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { txb.sign(i, keyPair, input.value); } - // Build the transaction final tx = txb.build(); return PendingBitcoinCashTransaction(tx, type, diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart index e2b1201a8..9e3ac4ffc 100644 --- a/cw_core/lib/output_info.dart +++ b/cw_core/lib/output_info.dart @@ -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; } \ No newline at end of file diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index cc5686fc9..042b0ca2b 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -3,6 +3,7 @@ mixin PendingTransaction { String get amountFormatted; String get feeFormatted; String get hex; + int? get outputCount => null; Future commit(); } \ No newline at end of file diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index de5b3874a..5e84ec796 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -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(); @@ -85,6 +86,7 @@ abstract class EVMChainClient { required CryptoCurrency currency, required int exponent, String? contractAddress, + String? data, }) async { assert(currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || @@ -100,6 +102,7 @@ abstract class EVMChainClient { to: EthereumAddress.fromHex(toAddress), maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + data: data != null ? hexToBytes(data) : null, ); final signedTransaction = @@ -140,12 +143,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 +227,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(); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0fb282960..2d58e95ab 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -224,6 +224,13 @@ 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); @@ -279,6 +286,7 @@ abstract class EVMChainWalletBase exponent: exponent, contractAddress: transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, + data: hexOpReturnMemo, ); return pendingEVMChainTransaction; diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 8129de728..0b367da68 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -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)}'; + } } diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart index 055b42f87..35fbe5072 100644 --- a/cw_polygon/lib/polygon_client.dart +++ b/cw_polygon/lib/polygon_client.dart @@ -13,6 +13,8 @@ class PolygonClient extends EVMChainClient { required EthereumAddress to, required EtherAmount amount, EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + }) { return Transaction( from: from, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 78423a8c3..4db51fbc2 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -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); diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 6e658788e..52839d68a 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -76,7 +76,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, diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index abfac3a6b..4d9691035 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem 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 with Serializable< return trocador; case 6: return exolix; + case 8: + return thorChain; case 7: return all; default: diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart new file mode 100644 index 000000000..2ab04b742 --- /dev/null +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -0,0 +1,248 @@ +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 _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 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 checkIsAvailable() async => true; + + @override + Future 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 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 createTrade({required TradeRequest request, required bool isFixedRateMode}) 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); + } + + @override + Future 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 stagesJson = responseJSON['stages'] as Map; + + 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 coins = tx['coins'] as List; + 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?; + 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> _getSwapQuote(Map 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 _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 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; + } +} diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 4eb48c248..4bfb335b8 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -27,7 +27,10 @@ class Trade extends HiveObject { this.password, this.providerId, this.providerName, - this.fromWalletAddress + this.fromWalletAddress, + this.memo, + this.txId, + this.isRefund, }) { if (provider != null) providerRaw = provider.raw; @@ -105,6 +108,15 @@ class Trade extends HiveObject { @HiveField(17) String? fromWalletAddress; + @HiveField(18) + String? memo; + + @HiveField(19) + String? txId; + + @HiveField(20) + bool? isRefund; + static Trade fromMap(Map map) { return Trade( id: map['id'] as String, @@ -115,7 +127,10 @@ 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? ); } @@ -128,7 +143,10 @@ 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 }; } diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index ed56d9845..2c58a96f4 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -41,6 +41,8 @@ class TradeState extends EnumerableItem with Serializable { 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 with Serializable { case 'sending': return sending; case 'success': + case 'done': return success; default: throw Exception('Unexpected token: $raw in TradeState deserialize'); diff --git a/lib/src/screens/dashboard/widgets/filter_tile.dart b/lib/src/screens/dashboard/widgets/filter_tile.dart index 3be96073a..d2f824806 100644 --- a/lib/src/screens/dashboard/widgets/filter_tile.dart +++ b/lib/src/screens/dashboard/widgets/filter_tile.dart @@ -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, ); } diff --git a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart index 11bde6dfa..21133a438 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart @@ -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: diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 7f570b98e..caccb8047 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -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; - } } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 1a5ab24e6..d8e5a6a4a 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -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( @@ -431,7 +433,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 +474,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; diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 4e901aa5e..c05839578 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -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; } diff --git a/lib/view_model/anonpay_details_view_model.dart b/lib/view_model/anonpay_details_view_model.dart index 6c528f495..fe4b9da3d 100644 --- a/lib/view_model/anonpay_details_view_model.dart +++ b/lib/view_model/anonpay_details_view_model.dart @@ -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))); } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 9ee0647fc..66d179523 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -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 = '', diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 0d40ae240..3eb07460b 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -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,8 +104,13 @@ 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; 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 @@ -127,6 +136,8 @@ 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(); + + if(trade.provider != ExchangeProviderDescription.thorChain) items.add(ExchangeTradeItem( title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true)); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 1f86847f4..01ac8e942 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -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(), ]; @@ -496,8 +500,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with await provider.createTrade(request: request, isFixedRateMode: isFixedRateMode); 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 @@ -749,4 +761,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; + } } diff --git a/lib/view_model/order_details_view_model.dart b/lib/view_model/order_details_view_model.dart index 9b00bbb46..384c0541d 100644 --- a/lib/view_model/order_details_view_model.dart +++ b/lib/view_model/order_details_view_model.dart @@ -99,7 +99,7 @@ abstract class OrderDetailsViewModelBase with Store { final buildURL = trackUrl + '${order.transferId}'; items.add( TrackTradeListItem( - title: 'Track', + title: S.current.track, value: buildURL, onTap: () { try { diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index cc39aca8b..c881284b3 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -66,6 +66,8 @@ abstract class OutputBase with Store { @observable String extractedAddress; + String? memo; + @computed bool get isParsedAddress => parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; @@ -175,6 +177,7 @@ abstract class OutputBase with Store { fiatAmount = ''; address = ''; note = ''; + memo = null; resetParsedAddress(); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 7636c485a..451f2210d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -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'; @@ -296,14 +298,20 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } @action - Future createTransaction() async { + Future 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()); + return null; } } diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 45502fd74..1da322778 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -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 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))); + } } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 66af15dfa..44d55e0ed 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -643,6 +643,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": "اليوم", @@ -660,6 +661,7 @@ "totp_code": "كود TOTP", "totp_secret_code": "كود TOTP السري", "totp_verification_success": "تم التحقق بنجاح!", + "track": " ﺭﺎﺴﻣ", "trade_details_copied": "تم نسخ ${title} إلى الحافظة", "trade_details_created_at": "أنشئت في", "trade_details_fetching": "جار الجلب", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 061506b86..1817b1807 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -643,6 +643,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": "Днес", @@ -660,6 +661,7 @@ "totp_code": "TOTP код", "totp_secret_code": "TOTP таен код", "totp_verification_success": "Проверката е успешна!", + "track": "Писта", "trade_details_copied": "${title} копирано", "trade_details_created_at": "Създадено", "trade_details_fetching": "Обработка", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 2896c11a9..9bd416f45 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -643,6 +643,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", @@ -660,6 +661,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", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 9dfa4f4db..109df5ef2 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -644,6 +644,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", @@ -661,6 +662,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", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 902cb062f..f9e67d475 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -643,6 +643,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", @@ -660,6 +661,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", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 4b9a9d26b..f84f892dd 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -644,6 +644,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", @@ -661,6 +662,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", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 384bb9585..048b97f66 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -643,6 +643,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", @@ -660,6 +661,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", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 7c0a7df12..4eadc6333 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -645,6 +645,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", @@ -662,6 +663,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", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 932fb25e7..500df31f7 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -645,6 +645,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": "आज", @@ -662,6 +663,7 @@ "totp_code": "टीओटीपी कोड", "totp_secret_code": "टीओटीपी गुप्त कोड", "totp_verification_success": "सत्यापन सफल!", + "track": "रास्ता", "trade_details_copied": "${title} क्लिपबोर्ड पर नकल", "trade_details_created_at": "पर बनाया गया", "trade_details_fetching": "ला रहा है", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index c8c9a00bd..9a6f74adb 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -643,6 +643,7 @@ "template_name": "Naziv predloška", "third_intro_content": "Yats žive i izvan Cake Wallet -a. Bilo koja adresa novčanika na svijetu može se zamijeniti Yat!", "third_intro_title": "Yat se lijepo igra s drugima", + "thorchain_taproot_address_not_supported": "Thorchain pružatelj ne podržava Taproot adrese. Promijenite adresu ili odaberite drugog davatelja usluga.", "time": "${minutes}m ${seconds}s", "tip": "Savjet:", "today": "Danas", @@ -660,6 +661,7 @@ "totp_code": "TOTP kod", "totp_secret_code": "TOTP tajni kod", "totp_verification_success": "Provjera uspješna!", + "track": "Staza", "trade_details_copied": "${title} kopiran u međuspremnik", "trade_details_created_at": "Stvoreno u", "trade_details_fetching": "Dohvaćanje", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 2168de910..b0c4276c3 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -646,6 +646,7 @@ "template_name": "Nama Templat", "third_intro_content": "Yats hidup di luar Cake Wallet juga. Setiap alamat dompet di dunia dapat diganti dengan Yat!", "third_intro_title": "Yat bermain baik dengan yang lain", + "thorchain_taproot_address_not_supported": "Penyedia Thorchain tidak mendukung alamat Taproot. Harap ubah alamatnya atau pilih penyedia yang berbeda.", "time": "${minutes}m ${seconds}s", "tip": "Tip:", "today": "Hari ini", @@ -663,6 +664,7 @@ "totp_code": "Kode TOTP", "totp_secret_code": "Kode Rahasia TOTP", "totp_verification_success": "Verifikasi Berhasil!", + "track": "Melacak", "trade_details_copied": "${title} disalin ke Clipboard", "trade_details_created_at": "Dibuat pada", "trade_details_fetching": "Mengambil", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 2700203a0..b96acc951 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -645,6 +645,7 @@ "template_name": "Nome modello", "third_intro_content": "Yat può funzionare anche fuori da Cake Wallet. Qualsiasi indirizzo di portafoglio sulla terra può essere sostituito con uno Yat!", "third_intro_title": "Yat gioca bene con gli altri", + "thorchain_taproot_address_not_supported": "Il provider di Thorchain non supporta gli indirizzi di TapRoot. Si prega di modificare l'indirizzo o selezionare un fornitore diverso.", "time": "${minutes}m ${seconds}s", "tip": "Suggerimento:", "today": "Oggi", @@ -662,6 +663,7 @@ "totp_code": "Codice TOTP", "totp_secret_code": "TOTP codice segreto", "totp_verification_success": "Verifica riuscita!", + "track": "Traccia", "trade_details_copied": "${title} copiati negli Appunti", "trade_details_created_at": "Creato alle", "trade_details_fetching": "Recupero", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 443ce6658..9f8ddbe15 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -644,6 +644,7 @@ "template_name": "テンプレート名", "third_intro_content": "YatsはCakeWalletの外にも住んでいます。 地球上のどのウォレットアドレスもYatに置き換えることができます!", "third_intro_title": "Yatは他の人とうまく遊ぶ", + "thorchain_taproot_address_not_supported": "Thorchainプロバイダーは、TapRootアドレスをサポートしていません。アドレスを変更するか、別のプロバイダーを選択してください。", "time": "${minutes}m ${seconds}s", "tip": "ヒント: ", "today": "今日", @@ -661,6 +662,7 @@ "totp_code": "TOTP コード", "totp_secret_code": "TOTPシークレットコード", "totp_verification_success": "検証成功!", + "track": "追跡", "trade_details_copied": "${title} クリップボードにコピーしました", "trade_details_created_at": "で作成", "trade_details_fetching": "フェッチング", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 3affd803b..757dc6cb3 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -644,6 +644,7 @@ "template_name": "템플릿 이름", "third_intro_content": "Yats는 Cake Wallet 밖에서도 살고 있습니다. 지구상의 모든 지갑 주소는 Yat!", "third_intro_title": "Yat는 다른 사람들과 잘 놉니다.", + "thorchain_taproot_address_not_supported": "Thorchain 제공 업체는 Taproot 주소를 지원하지 않습니다. 주소를 변경하거나 다른 공급자를 선택하십시오.", "time": "${minutes}m ${seconds}s", "tip": "팁:", "today": "오늘", @@ -661,6 +662,7 @@ "totp_code": "TOTP 코드", "totp_secret_code": "TOTP 비밀 코드", "totp_verification_success": "확인 성공!", + "track": "길", "trade_details_copied": "${title} 클립 보드에 복사", "trade_details_created_at": "에 작성", "trade_details_fetching": "가져 오는 중", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c52d65c06..5b622c234 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -643,6 +643,7 @@ "template_name": "နမူနာပုံစံ", "third_intro_content": "Yats သည် Cake Wallet အပြင်ဘက်တွင် နေထိုင်ပါသည်။ ကမ္ဘာပေါ်ရှိ မည်သည့်ပိုက်ဆံအိတ်လိပ်စာကို Yat ဖြင့် အစားထိုးနိုင်ပါသည်။", "third_intro_title": "Yat သည် အခြားသူများနှင့် ကောင်းစွာကစားသည်။", + "thorchain_taproot_address_not_supported": "Thorchain Provider သည် Taproot လိပ်စာများကိုမထောက်ခံပါ။ ကျေးဇူးပြု. လိပ်စာကိုပြောင်းပါသို့မဟုတ်အခြားပံ့ပိုးပေးသူကိုရွေးချယ်ပါ။", "time": "${minutes}m ${seconds}s", "tip": "အကြံပြုချက်-", "today": "ဒီနေ့", @@ -660,6 +661,7 @@ "totp_code": "TOTP ကုဒ်", "totp_secret_code": "TOTP လျှို့ဝှက်ကုဒ်", "totp_verification_success": "အတည်ပြုခြင်း အောင်မြင်ပါသည်။", + "track": "တစ်ပုဒ်", "trade_details_copied": "${title} ကို Clipboard သို့ ကူးယူထားသည်။", "trade_details_created_at": "တွင်ဖန်တီးခဲ့သည်။", "trade_details_fetching": "ခေါ်ယူခြင်း။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index fa7825809..121ade760 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -643,6 +643,7 @@ "template_name": "Sjabloonnaam", "third_intro_content": "Yats wonen ook buiten Cake Wallet. Elk portemonnee-adres op aarde kan worden vervangen door een Yat!", "third_intro_title": "Yat speelt leuk met anderen", + "thorchain_taproot_address_not_supported": "De Thorchain -provider ondersteunt geen Taprooot -adressen. Wijzig het adres of selecteer een andere provider.", "time": "${minutes}m ${seconds}s", "tip": "Tip:", "today": "Vandaag", @@ -660,6 +661,7 @@ "totp_code": "TOTP-code", "totp_secret_code": "TOTP-geheime code", "totp_verification_success": "Verificatie geslaagd!", + "track": "Spoor", "trade_details_copied": "${title} gekopieerd naar het klembord", "trade_details_created_at": "Gemaakt bij", "trade_details_fetching": "Ophalen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 16dc8696a..88fbfb6e5 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -643,6 +643,7 @@ "template_name": "Nazwa szablonu", "third_intro_content": "Yats mieszkają również poza Cake Wallet. Każdy adres portfela na ziemi można zastąpić Yat!", "third_intro_title": "Yat ładnie bawi się z innymi", + "thorchain_taproot_address_not_supported": "Dostawca Thorchain nie obsługuje adresów TAPROOT. Zmień adres lub wybierz innego dostawcę.", "time": "${minutes}m ${seconds}s", "tip": "wskazówka:", "today": "Dzisiaj", @@ -660,6 +661,7 @@ "totp_code": "Kod TOTP", "totp_secret_code": "Tajny kod TOTP", "totp_verification_success": "Weryfikacja powiodła się!", + "track": "Ścieżka", "trade_details_copied": "${title} skopiowane do schowka", "trade_details_created_at": "Utworzono ", "trade_details_fetching": "Pobieranie", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8516ecf3f..7487499ef 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -645,6 +645,7 @@ "template_name": "Nome do modelo", "third_intro_content": "Yats também mora fora da Cake Wallet. Qualquer endereço de carteira na Terra pode ser substituído por um Yat!", "third_intro_title": "Yat joga bem com os outros", + "thorchain_taproot_address_not_supported": "O provedor de Thorchain não suporta endereços de raiz de Tap. Altere o endereço ou selecione um provedor diferente.", "time": "${minutes}m ${seconds}s", "tip": "Dica:", "today": "Hoje", @@ -662,6 +663,7 @@ "totp_code": "Código TOTP", "totp_secret_code": "Código Secreto TOTP", "totp_verification_success": "Verificação bem-sucedida!", + "track": "Acompanhar", "trade_details_copied": "${title} copiados para a área de transferência", "trade_details_created_at": "Criada em", "trade_details_fetching": "Buscando", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 2bff24414..443daa588 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -644,6 +644,7 @@ "template_name": "Имя Шаблона", "third_intro_content": "Yat находятся за пределами Cake Wallet. Любой адрес кошелька на земле можно заменить на Yat!", "third_intro_title": "Yat хорошо взаимодействует с другими", + "thorchain_taproot_address_not_supported": "Поставщик Thorchain не поддерживает адреса taproot. Пожалуйста, измените адрес или выберите другого поставщика.", "time": "${minutes}мин ${seconds}сек", "tip": "Совет:", "today": "Сегодня", @@ -661,6 +662,7 @@ "totp_code": "TOTP-код", "totp_secret_code": "Секретный код ТОТП", "totp_verification_success": "Проверка прошла успешно!", + "track": "Отслеживать", "trade_details_copied": "${title} скопировано в буфер обмена", "trade_details_created_at": "Создано", "trade_details_fetching": "Получение", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 9c37ff8bb..dc864c39c 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -643,6 +643,7 @@ "template_name": "ชื่อแม่แบบ", "third_intro_content": "Yat อาศัยอยู่นอก Cake Wallet ด้วย ที่อยู่กระเป๋าใดๆ ทั่วโลกสามารถแทนด้วย Yat ได้อีกด้วย!", "third_intro_title": "Yat ปฏิบัติตนอย่างดีกับผู้อื่น", + "thorchain_taproot_address_not_supported": "ผู้ให้บริการ Thorchain ไม่รองรับที่อยู่ taproot โปรดเปลี่ยนที่อยู่หรือเลือกผู้ให้บริการอื่น", "time": "${minutes}m ${seconds}s", "tip": "เพิ่มค่าตอบแทน:", "today": "วันนี้", @@ -660,6 +661,7 @@ "totp_code": "รหัสทีโอพี", "totp_secret_code": "รหัสลับ TOTP", "totp_verification_success": "การยืนยันสำเร็จ!", + "track": "ติดตาม", "trade_details_copied": "${title} คัดลอกไปยัง Clipboard", "trade_details_created_at": "สร้างเมื่อ", "trade_details_fetching": "กำลังเรียกข้อมูล", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index d4be21741..4abd6472f 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -643,6 +643,7 @@ "template_name": "Pangalan ng Template", "third_intro_content": "Ang mga yats ay nakatira sa labas ng cake wallet, din. Ang anumang address ng pitaka sa mundo ay maaaring mapalitan ng isang yat!", "third_intro_title": "Si Yat ay mahusay na gumaganap sa iba", + "thorchain_taproot_address_not_supported": "Ang Tagabigay ng Thorchain ay hindi sumusuporta sa mga address ng taproot. Mangyaring baguhin ang address o pumili ng ibang provider.", "time": "${minutes} m ${seconds} s", "tip": "Tip:", "today": "Ngayon", @@ -660,6 +661,7 @@ "totp_code": "TOTP code", "totp_secret_code": "TOTP Secret Code", "totp_verification_success": "Matagumpay ang pagpapatunay!", + "track": "Subaybayan", "trade_details_copied": "${title} kinopya sa clipboard", "trade_details_created_at": "Nilikha sa", "trade_details_fetching": "Pagkuha", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index a6209ed33..c615864a7 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -643,6 +643,7 @@ "template_name": "şablon adı", "third_intro_content": "Yat'lar Cake Wallet'ın dışında da çalışabilir. Dünya üzerindeki herhangi bir cüzdan adresi Yat ile değiştirilebilir!", "third_intro_title": "Yat diğerleriyle iyi çalışır", + "thorchain_taproot_address_not_supported": "Thorchain sağlayıcısı Taproot adreslerini desteklemiyor. Lütfen adresi değiştirin veya farklı bir sağlayıcı seçin.", "time": "${minutes}d ${seconds}s", "tip": "Bahşiş:", "today": "Bugün", @@ -660,6 +661,7 @@ "totp_code": "TOTP Kodu", "totp_secret_code": "TOTP Gizli Kodu", "totp_verification_success": "Doğrulama Başarılı!", + "track": "İzlemek", "trade_details_copied": "${title} panoya kopyalandı", "trade_details_created_at": "'da oluşturuldu", "trade_details_fetching": "Getiriliyor", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 3d60b9e8e..0fe324d11 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -644,6 +644,7 @@ "template_name": "Назва шаблону", "third_intro_content": "Yat знаходиться за межами Cake Wallet. Будь-яку адресу гаманця на землі можна замінити на Yat!", "third_intro_title": "Yat добре взаємодіє з іншими", + "thorchain_taproot_address_not_supported": "Постачальник Thorchain не підтримує адреси Taproot. Будь ласка, змініть адресу або виберіть іншого постачальника.", "time": "${minutes}хв ${seconds}сек", "tip": "Порада:", "today": "Сьогодні", @@ -661,6 +662,7 @@ "totp_code": "Код TOTP", "totp_secret_code": "Секретний код TOTP", "totp_verification_success": "Перевірка успішна!", + "track": "Відслідковувати", "trade_details_copied": "${title} скопійовано в буфер обміну", "trade_details_created_at": "Створено", "trade_details_fetching": "Отримання", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e0e74e6de..9de85a683 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -645,6 +645,7 @@ "template_name": "ٹیمپلیٹ کا نام", "third_intro_content": "Yats بھی Cake والیٹ سے باہر رہتے ہیں۔ زمین پر کسی بھی بٹوے کے پتے کو Yat سے تبدیل کیا جا سکتا ہے!", "third_intro_title": "Yat دوسروں کے ساتھ اچھی طرح کھیلتا ہے۔", + "thorchain_taproot_address_not_supported": "تھورچین فراہم کنندہ ٹیپروٹ پتے کی حمایت نہیں کرتا ہے۔ براہ کرم پتہ تبدیل کریں یا ایک مختلف فراہم کنندہ کو منتخب کریں۔", "time": "${minutes}m ${seconds}s", "tip": "ٹپ:", "today": "آج", @@ -662,6 +663,7 @@ "totp_code": "TOTP کوڈ", "totp_secret_code": "TOTP خفیہ کوڈ", "totp_verification_success": "توثیق کامیاب!", + "track": " ﮏﯾﺮﭨ", "trade_details_copied": "${title} کو کلپ بورڈ پر کاپی کیا گیا۔", "trade_details_created_at": "پر تخلیق کیا گیا۔", "trade_details_fetching": "لا رہا ہے۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 54df5dbf2..8d85f0d6e 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -644,6 +644,7 @@ "template_name": "Orukọ Awoṣe", "third_intro_content": "A sì lè lo Yats níta Cake Wallet. A lè rọ́pò Àdírẹ́sì kankan àpamọ́wọ́ fún Yat!", "third_intro_title": "Àlàáfíà ni Yat àti àwọn ìmíìn jọ wà", + "thorchain_taproot_address_not_supported": "Olupese Trockchain ko ṣe atilẹyin awọn adirẹsi Taproot. Jọwọ yi adirẹsi pada tabi yan olupese ti o yatọ.", "time": "${minutes}ìṣj ${seconds}ìṣs", "tip": "Owó àfikún:", "today": "Lénìí", @@ -661,6 +662,7 @@ "totp_code": "Koodu TOTP", "totp_secret_code": "Koodu iye TOTP", "totp_verification_success": "Ìbẹrẹ dọkita!", + "track": "Orin", "trade_details_copied": "Ti ṣeda ${title} sí àtẹ àkọsílẹ̀", "trade_details_created_at": "Ṣíṣe ní", "trade_details_fetching": "Ń mú wá", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index cb1f012fb..4087d96e8 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -643,6 +643,7 @@ "template_name": "模板名称", "third_intro_content": "Yats 也住在 Cake Wallet 之外。 地球上任何一個錢包地址都可以用一個Yat來代替!", "third_intro_title": "Yat 和別人玩得很好", + "thorchain_taproot_address_not_supported": "Thorchain提供商不支持Taproot地址。请更改地址或选择其他提供商。", "time": "${minutes}m ${seconds}s", "tip": "提示:", "today": "今天", @@ -660,6 +661,7 @@ "totp_code": "TOTP代码", "totp_secret_code": "TOTP密码", "totp_verification_success": "验证成功!", + "track": "追踪", "trade_details_copied": "${title} 复制到剪贴板", "trade_details_created_at": "创建于", "trade_details_fetching": "正在获取",