-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
1 parent
2fb07dd
commit 591446f
Showing
18 changed files
with
401 additions
and
18 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
import 'dart:convert'; | ||
import 'dart:math'; | ||
|
||
import 'package:cake_wallet/.secrets.g.dart' as secrets; | ||
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:cw_core/utils/print_verbose.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<CryptoCurrency> _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 = secrets.chainflipApiKey; | ||
|
||
final Box<Trade> 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<bool> checkIsAvailable() async => true; | ||
|
||
@override | ||
Future<Limits> 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<dynamic>; | ||
|
||
final minAmount = assets.firstWhere( | ||
(asset) => asset['id'] == assetId, | ||
orElse: () => null)?['minimalAmountNative'] ?? '0'; | ||
|
||
return Limits(min: _amountFromNative(minAmount.toString(), from)); | ||
} | ||
|
||
@override | ||
Future<double> 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) { | ||
printV(e.toString()); | ||
return 0.0; | ||
} | ||
} | ||
|
||
@override | ||
Future<Trade> 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, | ||
'boostFee': '6', | ||
'retryDurationInBlocks': '150' | ||
}; | ||
|
||
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) { | ||
printV(e.toString()); | ||
rethrow; | ||
} | ||
} | ||
|
||
@override | ||
Future<Trade> 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) { | ||
printV(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<Map<String, dynamic>> _getAssets() async => | ||
_getRequest(_assetsPath, {}); | ||
|
||
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async => | ||
_getRequest(_quotePath, params); | ||
|
||
Future<Map<String, dynamic>> _openDepositChannel(Map<String, String> params) async => | ||
_getRequest(_swapPath, params); | ||
|
||
Future<Map<String, dynamic>> _getRequest(String path, Map<String, String> 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<String, dynamic>; | ||
} | ||
|
||
Future<Map<String, dynamic>?> _getStatus(Map<String, String> 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<String, dynamic>; | ||
} | ||
|
||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.