From a741135520353c486f816599357b65dbc60ae209 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Fri, 15 Nov 2024 01:13:06 +0100 Subject: [PATCH] feat: add Chainflip exchange provider feat: add chainflip provider (fetchLimits and fetchRate) feat: add createTrade feat: add icon feat: add swap status feat: add FLIP to list feat: add to transaction list, with target amount feat: update receivedAmount with real values style: dart formatting feat: update received amount chore: cleanup space and typo --- assets/images/chainflip.png | Bin 0 -> 5693 bytes assets/images/flip_icon.png | Bin 0 -> 5693 bytes cw_core/lib/crypto_currency.dart | 2 + .../lib/default_ethereum_erc20_tokens.dart | 7 + .../exchange_provider_description.dart | 17 +- .../provider/chainflip_exchange_provider.dart | 313 ++++++++++++++++++ .../provider/exolix_exchange_provider.dart | 2 +- lib/exchange/trade.dart | 1 + .../dashboard/pages/transactions_page.dart | 3 +- .../screens/dashboard/widgets/trade_row.dart | 24 +- lib/src/screens/exchange/exchange_page.dart | 8 +- lib/store/dashboard/trade_filter_store.dart | 15 +- .../dashboard/dashboard_view_model.dart | 5 + lib/view_model/dashboard/trade_list_item.dart | 3 + .../exchange/exchange_trade_view_model.dart | 5 + .../exchange/exchange_view_model.dart | 2 + lib/view_model/trade_details_view_model.dart | 6 + tool/utils/secret_key.dart | 2 +- 18 files changed, 397 insertions(+), 18 deletions(-) create mode 100644 assets/images/chainflip.png create mode 100644 assets/images/flip_icon.png create mode 100644 lib/exchange/provider/chainflip_exchange_provider.dart diff --git a/assets/images/chainflip.png b/assets/images/chainflip.png new file mode 100644 index 0000000000000000000000000000000000000000..e588e63618a04fde69c0e6483ae13d122bc43fac GIT binary patch literal 5693 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 literal 0 HcmV?d00001 diff --git a/assets/images/flip_icon.png b/assets/images/flip_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e588e63618a04fde69c0e6483ae13d122bc43fac GIT binary patch literal 5693 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 literal 0 HcmV?d00001 diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 0280bb45af..d00e364ced 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -107,6 +107,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.tbtc, CryptoCurrency.wow, CryptoCurrency.ton, + CryptoCurrency.flip ]; static const havenCurrencies = [ @@ -226,6 +227,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11); static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); + static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 96, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index c26ee1efc6..ee60a3d6c5 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -290,6 +290,13 @@ class DefaultEthereumErc20Tokens { decimal: 6, enabled: false, ), + Erc20Token( + name: "Chainflip", + symbol: "FLIP", + contractAddress: "0x826180541412D574cf1336d22c0C0a287822678A", + decimal: 18, + enabled: false, + ), ]; List get initialErc20Tokens => _defaultTokens.map((token) { diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 9f37233564..249ae61bf4 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -16,22 +16,25 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png'); static const sideShift = ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png'); - static const simpleSwap = ExchangeProviderDescription( - title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); + static const simpleSwap = + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); static const trocador = 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 all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + static const all = + ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); static const thorChain = ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); static const quantex = ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); static const letsExchange = - ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); static const stealthEx = - ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); - + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); + static const chainflip = + ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png'); + static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { case 0: @@ -58,6 +61,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return letsExchange; case 11: return stealthEx; + case 12: + return chainflip; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart new file mode 100644 index 0000000000..5d06d61836 --- /dev/null +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -0,0 +1,313 @@ +import 'dart:convert'; +import 'dart:math'; + +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 ChainflipExchangeProvider extends ExchangeProvider { + ChainflipExchangeProvider({required this.tradesStore}) + : super(pairList: supportedPairs(_notSupported)); + + static final List _notSupported = [ + ...(CryptoCurrency.all + .where((element) => ![ + CryptoCurrency.btc, + CryptoCurrency.eth, + CryptoCurrency.usdc, + CryptoCurrency.usdterc20, + CryptoCurrency.flip, + CryptoCurrency.sol, + CryptoCurrency.usdcsol, + // TODO: Add CryptoCurrency.etharb + // TODO: Add CryptoCurrency.usdcarb + // TODO: Add CryptoCurrency.dot + ].contains(element)) + .toList()) + ]; + + static const _baseURL = 'chainflip-broker.io'; + static const _assetsPath = '/assets'; + static const _quotePath = '/quote-native'; + static const _swapPath = '/swap'; + static const _txInfoPath = '/status-by-deposit-channel'; + static const _affiliateBps = '170'; + static const _affiliateKey = '6ba154d4-e219-472a-9674-5fa5b1300ccf'; + // TODO: Example key, replace with CW key + + final Box tradesStore; + + @override + String get title => 'Chainflip'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => false; + + @override + ExchangeProviderDescription get description => + ExchangeProviderDescription.chainflip; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final assetId = _normalizeCurrency(from); + + final assetsResponse = await _getAssets(); + final assets = assetsResponse['assets'] as List; + + final minAmount = assets.firstWhere( + (asset) => asset['id'] == assetId, + orElse: () => null)?['minimalAmountNative'] ?? '0'; + + return Limits(min: _amountFromNative(minAmount.toString(), from)); + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + // TODO: It seems this rate is getting cached, and re-used for different amounts, can we not do this? + + try { + if (amount == 0) return 0.0; + + final quoteParams = { + 'apiKey': _affiliateKey, + 'sourceAsset': _normalizeCurrency(from), + 'destinationAsset': _normalizeCurrency(to), + 'amount': _amountToNative(amount, from), + 'commissionBps': _affiliateBps + }; + + final quoteResponse = await _getSwapQuote(quoteParams); + + final expectedAmountOut = + quoteResponse['egressAmountNative'] as String? ?? '0'; + + return _amountFromNative(expectedAmountOut, to) / amount; + } catch (e) { + print(e.toString()); + return 0.0; + } + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + try { + final maxSlippage = 2; + + final quoteParams = { + 'apiKey': _affiliateKey, + 'sourceAsset': _normalizeCurrency(request.fromCurrency), + 'destinationAsset': _normalizeCurrency(request.toCurrency), + 'amount': _amountToNative(double.parse(request.fromAmount), request.fromCurrency), + 'commissionBps': _affiliateBps + }; + + final quoteResponse = await _getSwapQuote(quoteParams); + final estimatedPrice = quoteResponse['estimatedPrice'] as double; + final minimumPrice = estimatedPrice * (100 - maxSlippage) / 100; + + final swapParams = { + 'apiKey': _affiliateKey, + 'sourceAsset': _normalizeCurrency(request.fromCurrency), + 'destinationAsset': _normalizeCurrency(request.toCurrency), + 'destinationAddress': request.toAddress, + 'commissionBps': _affiliateBps, + 'minimumPrice': minimumPrice.toString(), + 'refundAddress': request.refundAddress, + 'retryDurationInBlocks': '10' + }; + + final swapResponse = await _openDepositChannel(swapParams); + + final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString().toUpperCase()}-${swapResponse['channelId']}'; + + return Trade( + id: id, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + inputAddress: swapResponse['address'].toString(), + createdAt: DateTime.now(), + amount: request.fromAmount, + receiveAmount: request.toAmount, + state: TradeState.waiting, + payoutAddress: request.toAddress, + isSendAll: isSendAll); + } catch (e) { + print(e.toString()); + rethrow; + } + } + + @override + Future findTradeById({required String id}) async { + try { + final channelParts = id.split('-'); + + final statusParams = { + 'apiKey': _affiliateKey, + 'issuedBlock': channelParts[0], + 'network': channelParts[1], + 'channelId': channelParts[2] + }; + + final statusResponse = await _getStatus(statusParams); + + if (statusResponse == null) + throw Exception('Trade not found for id: $id'); + + final status = statusResponse['status']; + final currentState = _determineState(status['state'].toString()); + + final depositAmount = status['deposit']?['amount']?.toString() ?? '0.0'; + final receiveAmount = status['swapEgress']?['amount']?.toString() ?? '0.0'; + final refundAmount = status['refundEgress']?['amount']?.toString() ?? '0.0'; + final isRefund = status['refundEgress'] != null; + final amount = isRefund ? refundAmount : receiveAmount; + + final newTrade = Trade( + id: id, + from: _toCurrency(status['sourceAsset'].toString()), + to: _toCurrency(status['destinationAsset'].toString()), + provider: description, + amount: depositAmount, + receiveAmount: amount, + state: currentState, + payoutAddress: status['destinationAddress'].toString(), + outputTransaction: status['swapEgress']?['transactionReference']?.toString(), + isRefund: isRefund); + + // Find trade and update receiveAmount with the real value received + final storedTrade = _getStoredTrade(id); + + if (storedTrade != null) { + storedTrade.$2.receiveAmount = newTrade.receiveAmount; + storedTrade.$2.outputTransaction = newTrade.outputTransaction; + tradesStore.put(storedTrade.$1, storedTrade.$2); + } + + return newTrade; + } catch (e) { + print(e.toString()); + rethrow; + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + final network = switch (currency.tag) { + 'ETH' => 'eth', + 'SOL' => 'sol', + _ => currency.title.toLowerCase() + }; + + return '${currency.title.toLowerCase()}.$network'; + } + + CryptoCurrency? _toCurrency(String name) { + final currency = switch (name) { + 'btc.btc' => CryptoCurrency.btc, + 'eth.eth' => CryptoCurrency.eth, + 'usdc.eth' => CryptoCurrency.usdc, + 'usdt.eth' => CryptoCurrency.usdterc20, + 'flip.eth' => CryptoCurrency.flip, + 'sol.sol' => CryptoCurrency.sol, + 'usdc.sol' => CryptoCurrency.usdcsol, + _ => null + }; + + return currency; + } + + (dynamic, Trade)? _getStoredTrade(String id) { + for (var i = tradesStore.length -1; i >= 0; i--) { + Trade? t = tradesStore.getAt(i); + + if (t != null && t.id == id) + return (i, t); + } + + return null; + } + + String _amountToNative(double amount, CryptoCurrency currency) => + (amount * pow(10, currency.decimals)).toInt().toString(); + + double _amountFromNative(String amount, CryptoCurrency currency) => + double.parse(amount) / pow(10, currency.decimals); + + Future> _getAssets() async => + _getRequest(_assetsPath, {}); + + Future> _getSwapQuote(Map params) async => + _getRequest(_quotePath, params); + + Future> _openDepositChannel(Map params) async => + _getRequest(_swapPath, params); + + Future> _getRequest(String path, Map params) async { + final uri = Uri.https(_baseURL, path, params); + + final response = await http.get(uri); + + if ((response.statusCode != 200) || (response.body.contains('error'))) { + throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); + } + + return json.decode(response.body) as Map; + } + + Future?> _getStatus(Map params) async { + final uri = Uri.https(_baseURL, _txInfoPath, params); + + final response = await http.get(uri); + + if (response.statusCode == 404) return null; + + if ((response.statusCode != 200) || (response.body.contains('error'))) { + throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); + } + + return json.decode(response.body) as Map; + } + + TradeState _determineState(String state) { + final swapState = switch (state) { + 'waiting' => TradeState.waiting, + 'receiving' => TradeState.processing, + 'swapping' => TradeState.processing, + 'sending' => TradeState.processing, + 'sent' => TradeState.processing, + 'completed' => TradeState.success, + 'failed' => TradeState.failed, + _ => TradeState.notFound + }; + + return swapState; + } +} diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 5eeb6f9cf4..3bf783af83 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -184,7 +184,7 @@ class ExolixExchangeProvider extends ExchangeProvider { extraId: extraId, createdAt: DateTime.now(), amount: amount, - receiveAmount:receiveAmount ?? request.toAmount, + receiveAmount: receiveAmount ?? request.toAmount, state: TradeState.created, payoutAddress: payoutAddress, isSendAll: isSendAll, diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index a0c08aac7b..77cc0d567e 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -166,6 +166,7 @@ class Trade extends HiveObject { } String amountFormatted() => formatAmount(amount); + String receiveAmountFormatted() => formatAmount(receiveAmount ?? ''); } class TradeAdapter extends TypeAdapter { diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 0db9ac35b6..2eae0add7e 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -160,7 +160,8 @@ class TransactionsPage extends StatelessWidget { createdAtFormattedDate: trade.createdAt != null ? DateFormat('HH:mm').format(trade.createdAt!) : null, - formattedAmount: item.tradeFormattedAmount, + formattedAmount: item.tradeFormattedAmount, + formattedReceiveAmount: item.tradeFormattedReceiveAmount ), ); } diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 84a5d2beba..8eea77b192 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -13,6 +13,7 @@ class TradeRow extends StatelessWidget { required this.createdAtFormattedDate, this.onTap, this.formattedAmount, + this.formattedReceiveAmount, super.key, }); @@ -22,10 +23,12 @@ class TradeRow extends StatelessWidget { final CryptoCurrency to; final String? createdAtFormattedDate; final String? formattedAmount; + final String? formattedReceiveAmount; @override Widget build(BuildContext context) { final amountCrypto = from.toString(); + final receiveAmountCrypto = to.toString(); return InkWell( onTap: onTap, @@ -61,12 +64,21 @@ class TradeRow extends StatelessWidget { : Container() ]), SizedBox(height: 5), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (createdAtFormattedDate != null) - Text(createdAtFormattedDate!, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + createdAtFormattedDate != null + ? Text(createdAtFormattedDate!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.dateSectionRowColor)) + : Container(), + formattedReceiveAmount != null + ? Text(formattedReceiveAmount! + ' ' + receiveAmountCrypto, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.dateSectionRowColor)) + : Container(), ]) ], )) diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 2f8e3eb5ce..9bdf57f3dc 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.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'; @@ -442,7 +443,8 @@ class ExchangePage extends BasePage { } if (state is TradeIsCreatedSuccessfully) { exchangeViewModel.reset(); - (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain) + (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain || + exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.chainflip) ? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade) : Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm); } @@ -485,8 +487,10 @@ class ExchangePage extends BasePage { exchangeViewModel.isSendAllEnabled = false; final isThorChain = exchangeViewModel.selectedProviders .any((provider) => provider is ThorChainExchangeProvider); + final isChainflip = exchangeViewModel.selectedProviders + .any((provider) => provider is ChainflipExchangeProvider); - _depositAmountDebounce = isThorChain + _depositAmountDebounce = isThorChain || isChainflip ? Debounce(Duration(milliseconds: 1000)) : Debounce(Duration(milliseconds: 500)); diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index c1e462cd6c..a2c6e36460 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -16,6 +16,7 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true, displayTrocador = true, displayExolix = true, + displayChainflip = true, displayThorChain = true, displayLetsExchange = true, displayStealthEx = true; @@ -41,6 +42,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayExolix; + @observable + bool displayChainflip; + @observable bool displayThorChain; @@ -56,7 +60,8 @@ abstract class TradeFilterStoreBase with Store { displaySideShift && displaySimpleSwap && displayTrocador && - displayExolix && + displayExolix && + displayChainflip && displayThorChain && displayLetsExchange && displayStealthEx; @@ -85,11 +90,15 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.exolix: displayExolix = !displayExolix; break; + case ExchangeProviderDescription.chainflip: + displayChainflip = !displayChainflip; + break; case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; case ExchangeProviderDescription.letsExchange: displayLetsExchange = !displayLetsExchange; + break; case ExchangeProviderDescription.stealthEx: displayStealthEx = !displayStealthEx; break; @@ -102,6 +111,7 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = false; displayTrocador = false; displayExolix = false; + displayChainflip = false; displayThorChain = false; displayLetsExchange = false; displayStealthEx = false; @@ -113,6 +123,7 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true; displayTrocador = true; displayExolix = true; + displayChainflip = true; displayThorChain = true; displayLetsExchange = true; displayStealthEx = true; @@ -141,6 +152,8 @@ abstract class TradeFilterStoreBase with Store { item.trade.provider == ExchangeProviderDescription.simpleSwap) || (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) || (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || + (displayChainflip && + item.trade.provider == ExchangeProviderDescription.chainflip) || (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain) || (displayLetsExchange && diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 808657f66c..3dcd24efb3 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -132,6 +132,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.exolix.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)), + FilterItem( + value: () => tradeFilterStore.displayChainflip, + caption: ExchangeProviderDescription.chainflip.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)), FilterItem( value: () => tradeFilterStore.displayThorChain, caption: ExchangeProviderDescription.thorChain.title, diff --git a/lib/view_model/dashboard/trade_list_item.dart b/lib/view_model/dashboard/trade_list_item.dart index 55ae4e99f3..973f5b76f0 100644 --- a/lib/view_model/dashboard/trade_list_item.dart +++ b/lib/view_model/dashboard/trade_list_item.dart @@ -18,6 +18,9 @@ class TradeListItem extends ActionListItem { String get tradeFormattedAmount => displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.amountFormatted(); + String get tradeFormattedReceiveAmount => + displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.receiveAmountFormatted(); + @override DateTime get date => trade.createdAt!; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 4cb7e4cadc..5b01b946bb 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; @@ -55,9 +56,13 @@ abstract class ExchangeTradeViewModelBase with Store { break; case ExchangeProviderDescription.stealthEx: _provider = StealthExExchangeProvider(); + break; case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; + case ExchangeProviderDescription.chainflip: + _provider = ChainflipExchangeProvider(tradesStore: trades); + break; } _updateItems(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index d29b7df6b6..47a706fc84 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -170,6 +171,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with SideShiftExchangeProvider(), SimpleSwapExchangeProvider(), ThorChainExchangeProvider(tradesStore: trades), + ChainflipExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), QuantexExchangeProvider(), LetsExchangeExchangeProvider(), diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 19315f40d5..63a4f05904 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; @@ -67,6 +68,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.stealthEx: _provider = StealthExExchangeProvider(); break; + case ExchangeProviderDescription.chainflip: + _provider = ChainflipExchangeProvider(tradesStore: trades); + break; } _updateItems(); @@ -97,6 +101,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://letsexchange.io/?transactionId=${trade.id}'; case ExchangeProviderDescription.stealthEx: return 'https://stealthex.io/exchange/?id=${trade.id}'; + case ExchangeProviderDescription.chainflip: + return 'https://scan.chainflip.io/channels/${trade.id}'; } return null; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index affe4017c3..d67ab7605a 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -62,7 +62,7 @@ class SecretKey { SecretKey('bitcoinTestWalletReceiveAddress', () => ''), SecretKey('ethereumTestWalletReceiveAddress', () => ''), SecretKey('litecoinTestWalletReceiveAddress', () => ''), - SecretKey('bitco inCashTestWalletReceiveAddress', () => ''), + SecretKey('bitcoinCashTestWalletReceiveAddress', () => ''), SecretKey('polygonTestWalletReceiveAddress', () => ''), SecretKey('solanaTestWalletReceiveAddress', () => ''), SecretKey('tronTestWalletReceiveAddress', () => ''),