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 3aa1386ecf.

* fix thorchain exchange rate

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii 2024-03-28 14:41:11 +02:00 committed by GitHub
parent b9e803f3bd
commit cdf081edfd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 534 additions and 102 deletions

BIN
assets/images/thorchain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -195,7 +195,8 @@ abstract class ElectrumWalletBase
List<BitcoinOutput> outputs,
int? feeRate,
BitcoinTransactionPriority? priority,
{int? inputsCount}) async {
{int? inputsCount,
String? memo}) async {
final utxos = <UtxoWithAddress>[];
List<ECPrivate> 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<UtxoWithAddress> utxos;
final List<ECPrivate> privateKeys;
final int fee;
final int amount;
final String? memo;
}
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {

View file

@ -31,6 +31,9 @@ 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

View file

@ -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,

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

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

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();
@ -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();
}

View file

@ -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;

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,8 @@ class PolygonClient extends EVMChainClient {
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) {
return Transaction(
from: from,

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);

View file

@ -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,

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

@ -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<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}) 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<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

@ -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<String, Object?> 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
};
}

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

@ -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(
@ -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;

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

@ -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

@ -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 = '',

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,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));

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(),
];
@ -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;
}
}

View file

@ -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 {

View file

@ -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();
}

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';
@ -296,14 +298,20 @@ 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());
return null;
}
}

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

@ -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": "جار الجلب",

View file

@ -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": "Обработка",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "ला रहा है",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "フェッチング",

View file

@ -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": "가져 오는 중",

View file

@ -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": "ခေါ်ယူခြင်း။",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Получение",

View file

@ -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": "กำลังเรียกข้อมูล",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Отримання",

View file

@ -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": "لا رہا ہے۔",

View file

@ -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á",

View file

@ -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": "正在获取",