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 = [