Add Chainflip exchange provider (#1807) (#1970)

* Add Chainflip exchange provider (#1807) * 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 * fix: use correct retryDurationInBlocks * feat: use secrets for api key --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Add secrets to workflow * revert boostfee change * minor ui fix [skip ci] --------- Co-authored-by: David Cumps <david@cumps.be>

Omar Hatem committed Jan 24, 2025 at 20:17 UTC 9cda2c99e7edfe102eea411aa5b52e0ca242695c
21 files changed +413 -21
.github/workflows/pr_test_build_android.yml
+4
@@ -160,6 +160,10 @@ jobs:
160 echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
161 echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
162 echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart
163 + # end of test secrets
164 + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart
165 + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
166 +
167 - name: prepare monero_c and cache
168 run: |
169 export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }')
.github/workflows/pr_test_build_linux.yml
+5 -1
@@ -155,7 +155,11 @@ jobs:
155 echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
156 echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
157 echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
158 - echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart
158 + echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart
159 + # end of test secrets
160 + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart
161 + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
162 +
163 - name: prepare monero_c and cache
164 run: |
165 export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }')
assets/images/chainflip.png
Binary files /dev/null and b/assets/images/chainflip.png differ
assets/images/flip_icon.png
Binary files /dev/null and b/assets/images/flip_icon.png differ
cw_core/lib/crypto_currency.dart
+2
@@ -107,6 +107,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
107 CryptoCurrency.tbtc,
108 CryptoCurrency.wow,
109 CryptoCurrency.ton,
110 + CryptoCurrency.flip
111 ];
112
113 static const havenCurrencies = [
@@ -226,6 +227,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
227 static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11);
228 static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8);
229
230 + static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 96, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18);
231
232 static final Map<int, CryptoCurrency> _rawCurrencyMap =
233 [...all, ...havenCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) {
cw_ethereum/lib/default_ethereum_erc20_tokens.dart
+7
@@ -290,6 +290,13 @@ class DefaultEthereumErc20Tokens {
290 decimal: 6,
291 enabled: false,
292 ),
293 + Erc20Token(
294 + name: "Chainflip",
295 + symbol: "FLIP",
296 + contractAddress: "0x826180541412D574cf1336d22c0C0a287822678A",
297 + decimal: 18,
298 + enabled: false,
299 + ),
300 ];
301
302 List<Erc20Token> get initialErc20Tokens => _defaultTokens.map((token) {
lib/exchange/exchange_provider_description.dart
+11 -6
@@ -16,22 +16,25 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
16 ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png');
17 static const sideShift =
18 ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png');
19 - static const simpleSwap = ExchangeProviderDescription(
20 - title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png');
19 + static const simpleSwap =
20 + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png');
21 static const trocador =
22 ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
23 static const exolix =
24 ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
25 - static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
25 + static const all =
26 + ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
27 static const thorChain =
28 ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png');
29 static const quantex =
30 ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png');
31 static const letsExchange =
31 - ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg');
32 + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg');
33 static const stealthEx =
33 - ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
34 -
34 + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
35 + static const chainflip =
36 + ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png');
37 +
38 static ExchangeProviderDescription deserialize({required int raw}) {
39 switch (raw) {
40 case 0:
@@ -58,6 +61,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
61 return letsExchange;
62 case 11:
63 return stealthEx;
64 + case 12:
65 + return chainflip;
66 default:
67 throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
68 }
lib/exchange/provider/chainflip_exchange_provider.dart new
+315
@@ -0,0 +1,315 @@
1 +import 'dart:convert';
2 +import 'dart:math';
3 +
4 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
6 +import 'package:cake_wallet/exchange/limits.dart';
7 +import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
8 +import 'package:cake_wallet/exchange/trade.dart';
9 +import 'package:cake_wallet/exchange/trade_request.dart';
10 +import 'package:cake_wallet/exchange/trade_state.dart';
11 +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
12 +import 'package:cw_core/crypto_currency.dart';
13 +import 'package:cw_core/utils/print_verbose.dart';
14 +import 'package:hive/hive.dart';
15 +import 'package:http/http.dart' as http;
16 +
17 +class ChainflipExchangeProvider extends ExchangeProvider {
18 + ChainflipExchangeProvider({required this.tradesStore})
19 + : super(pairList: supportedPairs(_notSupported));
20 +
21 + static final List<CryptoCurrency> _notSupported = [
22 + ...(CryptoCurrency.all
23 + .where((element) => ![
24 + CryptoCurrency.btc,
25 + CryptoCurrency.eth,
26 + CryptoCurrency.usdc,
27 + CryptoCurrency.usdterc20,
28 + CryptoCurrency.flip,
29 + CryptoCurrency.sol,
30 + CryptoCurrency.usdcsol,
31 + // TODO: Add CryptoCurrency.etharb
32 + // TODO: Add CryptoCurrency.usdcarb
33 + // TODO: Add CryptoCurrency.dot
34 + ].contains(element))
35 + .toList())
36 + ];
37 +
38 + static const _baseURL = 'chainflip-broker.io';
39 + static const _assetsPath = '/assets';
40 + static const _quotePath = '/quote-native';
41 + static const _swapPath = '/swap';
42 + static const _txInfoPath = '/status-by-deposit-channel';
43 + static const _affiliateBps = secrets.chainflipAffiliateFee;
44 + static const _affiliateKey = secrets.chainflipApiKey;
45 +
46 + final Box<Trade> tradesStore;
47 +
48 + @override
49 + String get title => 'Chainflip';
50 +
51 + @override
52 + bool get isAvailable => true;
53 +
54 + @override
55 + bool get isEnabled => true;
56 +
57 + @override
58 + bool get supportsFixedRate => false;
59 +
60 + @override
61 + ExchangeProviderDescription get description =>
62 + ExchangeProviderDescription.chainflip;
63 +
64 + @override
65 + Future<bool> checkIsAvailable() async => true;
66 +
67 + @override
68 + Future<Limits> fetchLimits(
69 + {required CryptoCurrency from,
70 + required CryptoCurrency to,
71 + required bool isFixedRateMode}) async {
72 + final assetId = _normalizeCurrency(from);
73 +
74 + final assetsResponse = await _getAssets();
75 + final assets = assetsResponse['assets'] as List<dynamic>;
76 +
77 + final minAmount = assets.firstWhere(
78 + (asset) => asset['id'] == assetId,
79 + orElse: () => null)?['minimalAmountNative'] ?? '0';
80 +
81 + return Limits(min: _amountFromNative(minAmount.toString(), from));
82 + }
83 +
84 + @override
85 + Future<double> fetchRate(
86 + {required CryptoCurrency from,
87 + required CryptoCurrency to,
88 + required double amount,
89 + required bool isFixedRateMode,
90 + required bool isReceiveAmount}) async {
91 + // TODO: It seems this rate is getting cached, and re-used for different amounts, can we not do this?
92 +
93 + try {
94 + if (amount == 0) return 0.0;
95 +
96 + final quoteParams = {
97 + 'apiKey': _affiliateKey,
98 + 'sourceAsset': _normalizeCurrency(from),
99 + 'destinationAsset': _normalizeCurrency(to),
100 + 'amount': _amountToNative(amount, from),
101 + 'commissionBps': _affiliateBps
102 + };
103 +
104 + final quoteResponse = await _getSwapQuote(quoteParams);
105 +
106 + final expectedAmountOut =
107 + quoteResponse['egressAmountNative'] as String? ?? '0';
108 +
109 + return _amountFromNative(expectedAmountOut, to) / amount;
110 + } catch (e) {
111 + printV(e.toString());
112 + return 0.0;
113 + }
114 + }
115 +
116 + @override
117 + Future<Trade> createTrade(
118 + {required TradeRequest request,
119 + required bool isFixedRateMode,
120 + required bool isSendAll}) async {
121 + try {
122 + final maxSlippage = 2;
123 +
124 + final quoteParams = {
125 + 'apiKey': _affiliateKey,
126 + 'sourceAsset': _normalizeCurrency(request.fromCurrency),
127 + 'destinationAsset': _normalizeCurrency(request.toCurrency),
128 + 'amount': _amountToNative(double.parse(request.fromAmount), request.fromCurrency),
129 + 'commissionBps': _affiliateBps
130 + };
131 +
132 + final quoteResponse = await _getSwapQuote(quoteParams);
133 + final estimatedPrice = quoteResponse['estimatedPrice'] as double;
134 + final minimumPrice = estimatedPrice * (100 - maxSlippage) / 100;
135 +
136 + final swapParams = {
137 + 'apiKey': _affiliateKey,
138 + 'sourceAsset': _normalizeCurrency(request.fromCurrency),
139 + 'destinationAsset': _normalizeCurrency(request.toCurrency),
140 + 'destinationAddress': request.toAddress,
141 + 'commissionBps': _affiliateBps,
142 + 'minimumPrice': minimumPrice.toString(),
143 + 'refundAddress': request.refundAddress,
144 + 'boostFee': '6',
145 + 'retryDurationInBlocks': '150'
146 + };
147 +
148 + final swapResponse = await _openDepositChannel(swapParams);
149 +
150 + final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString().toUpperCase()}-${swapResponse['channelId']}';
151 +
152 + return Trade(
153 + id: id,
154 + from: request.fromCurrency,
155 + to: request.toCurrency,
156 + provider: description,
157 + inputAddress: swapResponse['address'].toString(),
158 + createdAt: DateTime.now(),
159 + amount: request.fromAmount,
160 + receiveAmount: request.toAmount,
161 + state: TradeState.waiting,
162 + payoutAddress: request.toAddress,
163 + isSendAll: isSendAll);
164 + } catch (e) {
165 + printV(e.toString());
166 + rethrow;
167 + }
168 + }
169 +
170 + @override
171 + Future<Trade> findTradeById({required String id}) async {
172 + try {
173 + final channelParts = id.split('-');
174 +
175 + final statusParams = {
176 + 'apiKey': _affiliateKey,
177 + 'issuedBlock': channelParts[0],
178 + 'network': channelParts[1],
179 + 'channelId': channelParts[2]
180 + };
181 +
182 + final statusResponse = await _getStatus(statusParams);
183 +
184 + if (statusResponse == null)
185 + throw Exception('Trade not found for id: $id');
186 +
187 + final status = statusResponse['status'];
188 + final currentState = _determineState(status['state'].toString());
189 +
190 + final depositAmount = status['deposit']?['amount']?.toString() ?? '0.0';
191 + final receiveAmount = status['swapEgress']?['amount']?.toString() ?? '0.0';
192 + final refundAmount = status['refundEgress']?['amount']?.toString() ?? '0.0';
193 + final isRefund = status['refundEgress'] != null;
194 + final amount = isRefund ? refundAmount : receiveAmount;
195 +
196 + final newTrade = Trade(
197 + id: id,
198 + from: _toCurrency(status['sourceAsset'].toString()),
199 + to: _toCurrency(status['destinationAsset'].toString()),
200 + provider: description,
201 + amount: depositAmount,
202 + receiveAmount: amount,
203 + state: currentState,
204 + payoutAddress: status['destinationAddress'].toString(),
205 + outputTransaction: status['swapEgress']?['transactionReference']?.toString(),
206 + isRefund: isRefund);
207 +
208 + // Find trade and update receiveAmount with the real value received
209 + final storedTrade = _getStoredTrade(id);
210 +
211 + if (storedTrade != null) {
212 + storedTrade.$2.receiveAmount = newTrade.receiveAmount;
213 + storedTrade.$2.outputTransaction = newTrade.outputTransaction;
214 + tradesStore.put(storedTrade.$1, storedTrade.$2);
215 + }
216 +
217 + return newTrade;
218 + } catch (e) {
219 + printV(e.toString());
220 + rethrow;
221 + }
222 + }
223 +
224 + String _normalizeCurrency(CryptoCurrency currency) {
225 + final network = switch (currency.tag) {
226 + 'ETH' => 'eth',
227 + 'SOL' => 'sol',
228 + _ => currency.title.toLowerCase()
229 + };
230 +
231 + return '${currency.title.toLowerCase()}.$network';
232 + }
233 +
234 + CryptoCurrency? _toCurrency(String name) {
235 + final currency = switch (name) {
236 + 'btc.btc' => CryptoCurrency.btc,
237 + 'eth.eth' => CryptoCurrency.eth,
238 + 'usdc.eth' => CryptoCurrency.usdc,
239 + 'usdt.eth' => CryptoCurrency.usdterc20,
240 + 'flip.eth' => CryptoCurrency.flip,
241 + 'sol.sol' => CryptoCurrency.sol,
242 + 'usdc.sol' => CryptoCurrency.usdcsol,
243 + _ => null
244 + };
245 +
246 + return currency;
247 + }
248 +
249 + (dynamic, Trade)? _getStoredTrade(String id) {
250 + for (var i = tradesStore.length -1; i >= 0; i--) {
251 + Trade? t = tradesStore.getAt(i);
252 +
253 + if (t != null && t.id == id)
254 + return (i, t);
255 + }
256 +
257 + return null;
258 + }
259 +
260 + String _amountToNative(double amount, CryptoCurrency currency) =>
261 + (amount * pow(10, currency.decimals)).toInt().toString();
262 +
263 + double _amountFromNative(String amount, CryptoCurrency currency) =>
264 + double.parse(amount) / pow(10, currency.decimals);
265 +
266 + Future<Map<String, dynamic>> _getAssets() async =>
267 + _getRequest(_assetsPath, {});
268 +
269 + Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async =>
270 + _getRequest(_quotePath, params);
271 +
272 + Future<Map<String, dynamic>> _openDepositChannel(Map<String, String> params) async =>
273 + _getRequest(_swapPath, params);
274 +
275 + Future<Map<String, dynamic>> _getRequest(String path, Map<String, String> params) async {
276 + final uri = Uri.https(_baseURL, path, params);
277 +
278 + final response = await http.get(uri);
279 +
280 + if ((response.statusCode != 200) || (response.body.contains('error'))) {
281 + throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}');
282 + }
283 +
284 + return json.decode(response.body) as Map<String, dynamic>;
285 + }
286 +
287 + Future<Map<String, dynamic>?> _getStatus(Map<String, String> params) async {
288 + final uri = Uri.https(_baseURL, _txInfoPath, params);
289 +
290 + final response = await http.get(uri);
291 +
292 + if (response.statusCode == 404) return null;
293 +
294 + if ((response.statusCode != 200) || (response.body.contains('error'))) {
295 + throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}');
296 + }
297 +
298 + return json.decode(response.body) as Map<String, dynamic>;
299 + }
300 +
301 + TradeState _determineState(String state) {
302 + final swapState = switch (state) {
303 + 'waiting' => TradeState.waiting,
304 + 'receiving' => TradeState.processing,
305 + 'swapping' => TradeState.processing,
306 + 'sending' => TradeState.processing,
307 + 'sent' => TradeState.processing,
308 + 'completed' => TradeState.success,
309 + 'failed' => TradeState.failed,
310 + _ => TradeState.notFound
311 + };
312 +
313 + return swapState;
314 + }
315 +}
lib/exchange/provider/exolix_exchange_provider.dart
+1 -1
@@ -203,7 +203,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
203 extraId: extraId,
204 createdAt: DateTime.now(),
205 amount: amount,
206 - receiveAmount:receiveAmount ?? request.toAmount,
206 + receiveAmount: receiveAmount ?? request.toAmount,
207 state: TradeState.created,
208 payoutAddress: payoutAddress,
209 isSendAll: isSendAll,
lib/exchange/trade.dart
+1
@@ -168,6 +168,7 @@ class Trade extends HiveObject {
168 }
169
170 String amountFormatted() => formatAmount(amount);
171 + String receiveAmountFormatted() => formatAmount(receiveAmount ?? '');
172 }
173
174 class TradeAdapter extends TypeAdapter<Trade> {
lib/src/screens/dashboard/pages/transactions_page.dart
+2 -1
@@ -160,7 +160,8 @@ class TransactionsPage extends StatelessWidget {
160 createdAtFormattedDate: trade.createdAt != null
161 ? DateFormat('HH:mm').format(trade.createdAt!)
162 : null,
163 - formattedAmount: item.tradeFormattedAmount,
163 + formattedAmount: item.tradeFormattedAmount,
164 + formattedReceiveAmount: item.tradeFormattedReceiveAmount
165 ),
166 );
167 }
lib/src/screens/dashboard/widgets/trade_row.dart
+18 -6
@@ -13,6 +13,7 @@ class TradeRow extends StatelessWidget {
13 required this.createdAtFormattedDate,
14 this.onTap,
15 this.formattedAmount,
16 + this.formattedReceiveAmount,
17 super.key,
18 });
19
@@ -22,10 +23,12 @@ class TradeRow extends StatelessWidget {
23 final CryptoCurrency to;
24 final String? createdAtFormattedDate;
25 final String? formattedAmount;
26 + final String? formattedReceiveAmount;
27
28 @override
29 Widget build(BuildContext context) {
30 final amountCrypto = from.toString();
31 + final receiveAmountCrypto = to.toString();
32
33 return InkWell(
34 onTap: onTap,
@@ -61,12 +64,21 @@ class TradeRow extends StatelessWidget {
64 : Container()
65 ]),
66 SizedBox(height: 5),
64 - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
65 - if (createdAtFormattedDate != null)
66 - Text(createdAtFormattedDate!,
67 - style: TextStyle(
68 - fontSize: 14,
69 - color: Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor))
67 + Row(
68 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
69 + children: <Widget>[
70 + createdAtFormattedDate != null
71 + ? Text(createdAtFormattedDate!,
72 + style: TextStyle(
73 + fontSize: 14,
74 + color: Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor))
75 + : Container(),
76 + formattedReceiveAmount != null
77 + ? Text(formattedReceiveAmount! + ' ' + receiveAmountCrypto,
78 + style: TextStyle(
79 + fontSize: 14,
80 + color: Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor))
81 + : Container(),
82 ])
83 ],
84 ))
lib/src/screens/exchange/exchange_page.dart
+6 -2
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2 +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
3 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
4 import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
5 import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
@@ -444,7 +445,8 @@ class ExchangePage extends BasePage {
445 }
446 if (state is TradeIsCreatedSuccessfully) {
447 exchangeViewModel.reset();
447 - (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain)
448 + (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain ||
449 + exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.chainflip)
450 ? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
451 : Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
452 }
@@ -495,8 +497,10 @@ class ExchangePage extends BasePage {
497 exchangeViewModel.isSendAllEnabled = false;
498 final isThorChain = exchangeViewModel.selectedProviders
499 .any((provider) => provider is ThorChainExchangeProvider);
500 + final isChainflip = exchangeViewModel.selectedProviders
501 + .any((provider) => provider is ChainflipExchangeProvider);
502
499 - _depositAmountDebounce = isThorChain
503 + _depositAmountDebounce = isThorChain || isChainflip
504 ? Debounce(Duration(milliseconds: 1000))
505 : Debounce(Duration(milliseconds: 500));
506
lib/src/screens/send/transaction_success_info_page.dart
+3 -2
@@ -27,6 +27,7 @@ class TransactionSuccessPage extends InfoPage {
27 Key? get buttonKey => ValueKey('transaction_success_info_page_button_key');
28
29 @override
30 - void Function(BuildContext) get onPressed =>
31 - (BuildContext context) => Navigator.of(context).pop();
30 + void Function(BuildContext) get onPressed => (BuildContext context) {
31 + if (context.mounted) Navigator.of(context).pop();
32 + };
33 }
lib/store/dashboard/trade_filter_store.dart
+14 -1
@@ -16,6 +16,7 @@ abstract class TradeFilterStoreBase with Store {
16 displaySimpleSwap = true,
17 displayTrocador = true,
18 displayExolix = true,
19 + displayChainflip = true,
20 displayThorChain = true,
21 displayLetsExchange = true,
22 displayStealthEx = true;
@@ -41,6 +42,9 @@ abstract class TradeFilterStoreBase with Store {
42 @observable
43 bool displayExolix;
44
45 + @observable
46 + bool displayChainflip;
47 +
48 @observable
49 bool displayThorChain;
50
@@ -56,7 +60,8 @@ abstract class TradeFilterStoreBase with Store {
60 displaySideShift &&
61 displaySimpleSwap &&
62 displayTrocador &&
59 - displayExolix &&
63 + displayExolix &&
64 + displayChainflip &&
65 displayThorChain &&
66 displayLetsExchange &&
67 displayStealthEx;
@@ -85,11 +90,15 @@ abstract class TradeFilterStoreBase with Store {
90 case ExchangeProviderDescription.exolix:
91 displayExolix = !displayExolix;
92 break;
93 + case ExchangeProviderDescription.chainflip:
94 + displayChainflip = !displayChainflip;
95 + break;
96 case ExchangeProviderDescription.thorChain:
97 displayThorChain = !displayThorChain;
98 break;
99 case ExchangeProviderDescription.letsExchange:
100 displayLetsExchange = !displayLetsExchange;
101 + break;
102 case ExchangeProviderDescription.stealthEx:
103 displayStealthEx = !displayStealthEx;
104 break;
@@ -102,6 +111,7 @@ abstract class TradeFilterStoreBase with Store {
111 displaySimpleSwap = false;
112 displayTrocador = false;
113 displayExolix = false;
114 + displayChainflip = false;
115 displayThorChain = false;
116 displayLetsExchange = false;
117 displayStealthEx = false;
@@ -113,6 +123,7 @@ abstract class TradeFilterStoreBase with Store {
123 displaySimpleSwap = true;
124 displayTrocador = true;
125 displayExolix = true;
126 + displayChainflip = true;
127 displayThorChain = true;
128 displayLetsExchange = true;
129 displayStealthEx = true;
@@ -141,6 +152,8 @@ abstract class TradeFilterStoreBase with Store {
152 item.trade.provider == ExchangeProviderDescription.simpleSwap) ||
153 (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) ||
154 (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) ||
155 + (displayChainflip &&
156 + item.trade.provider == ExchangeProviderDescription.chainflip) ||
157 (displayThorChain &&
158 item.trade.provider == ExchangeProviderDescription.thorChain) ||
159 (displayLetsExchange &&
lib/view_model/dashboard/dashboard_view_model.dart
+5
@@ -130,6 +130,11 @@ abstract class DashboardViewModelBase with Store {
130 caption: ExchangeProviderDescription.exolix.title,
131 onChanged: () =>
132 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
133 + FilterItem(
134 + value: () => tradeFilterStore.displayChainflip,
135 + caption: ExchangeProviderDescription.chainflip.title,
136 + onChanged: () =>
137 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)),
138 FilterItem(
139 value: () => tradeFilterStore.displayThorChain,
140 caption: ExchangeProviderDescription.thorChain.title,
lib/view_model/dashboard/trade_list_item.dart
+3
@@ -18,6 +18,9 @@ class TradeListItem extends ActionListItem {
18 String get tradeFormattedAmount =>
19 displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.amountFormatted();
20
21 + String get tradeFormattedReceiveAmount =>
22 + displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.receiveAmountFormatted();
23 +
24 @override
25 DateTime get date => trade.createdAt!;
26 }
lib/view_model/exchange/exchange_trade_view_model.dart
+5
@@ -1,6 +1,7 @@
1 import 'dart:async';
2
3 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
4 +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
5 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
6 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
7 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
@@ -56,9 +57,13 @@ abstract class ExchangeTradeViewModelBase with Store {
57 break;
58 case ExchangeProviderDescription.stealthEx:
59 _provider = StealthExExchangeProvider();
60 + break;
61 case ExchangeProviderDescription.thorChain:
62 _provider = ThorChainExchangeProvider(tradesStore: trades);
63 break;
64 + case ExchangeProviderDescription.chainflip:
65 + _provider = ChainflipExchangeProvider(tradesStore: trades);
66 + break;
67 }
68
69 _updateItems();
lib/view_model/exchange/exchange_view_model.dart
+4 -1
@@ -4,6 +4,7 @@ import 'dart:convert';
4
5 import 'package:bitcoin_base/bitcoin_base.dart';
6 import 'package:cake_wallet/core/create_trade_result.dart';
7 +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
8 import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
9 import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
10 import 'package:cw_core/crypto_currency.dart';
@@ -159,7 +160,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
160 wallet.type == WalletType.bitcoinCash;
161
162 bool get hideAddressAfterExchange =>
162 - wallet.type == WalletType.monero || wallet.type == WalletType.wownero;
163 + wallet.type == WalletType.monero ||
164 + wallet.type == WalletType.wownero;
165
166 bool _useTorOnly;
167 final Box<Trade> trades;
@@ -172,6 +174,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
174 SideShiftExchangeProvider(),
175 SimpleSwapExchangeProvider(),
176 ThorChainExchangeProvider(tradesStore: trades),
177 + ChainflipExchangeProvider(tradesStore: trades),
178 if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
179 QuantexExchangeProvider(),
180 LetsExchangeExchangeProvider(),
lib/view_model/trade_details_view_model.dart
+6
@@ -1,6 +1,7 @@
1 import 'dart:async';
2
3 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
4 +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
5 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
6 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
7 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
@@ -68,6 +69,9 @@ abstract class TradeDetailsViewModelBase with Store {
69 case ExchangeProviderDescription.stealthEx:
70 _provider = StealthExExchangeProvider();
71 break;
72 + case ExchangeProviderDescription.chainflip:
73 + _provider = ChainflipExchangeProvider(tradesStore: trades);
74 + break;
75 }
76
77 _updateItems();
@@ -98,6 +102,8 @@ abstract class TradeDetailsViewModelBase with Store {
102 return 'https://letsexchange.io/?transactionId=${trade.id}';
103 case ExchangeProviderDescription.stealthEx:
104 return 'https://stealthex.io/exchange/?id=${trade.id}';
105 + case ExchangeProviderDescription.chainflip:
106 + return 'https://scan.chainflip.io/channels/${trade.id}';
107 }
108 return null;
109 }
tool/utils/secret_key.dart
+1
@@ -75,6 +75,7 @@ class SecretKey {
75 SecretKey('stealthExBearerToken', () => ''),
76 SecretKey('stealthExAdditionalFeePercent', () => ''),
77 SecretKey('moneroTestWalletBlockHeight', () => ''),
78 + SecretKey('chainflipApiKey', () => ''),
79 ];
80
81 static final evmChainsSecrets = [