Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Chainflip exchange provider #1807

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/chainflip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/flip_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions cw_core/lib/crypto_currency.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.tbtc,
CryptoCurrency.wow,
CryptoCurrency.ton,
CryptoCurrency.flip
];

static const havenCurrencies = [
Expand Down Expand Up @@ -226,6 +227,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> 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<int, CryptoCurrency> _rawCurrencyMap =
[...all, ...havenCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) {
Expand Down
7 changes: 7 additions & 0 deletions cw_ethereum/lib/default_ethereum_erc20_tokens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,13 @@ class DefaultEthereumErc20Tokens {
decimal: 6,
enabled: false,
),
Erc20Token(
name: "Chainflip",
symbol: "FLIP",
contractAddress: "0x826180541412D574cf1336d22c0C0a287822678A",
decimal: 18,
enabled: false,
),
];

List<Erc20Token> get initialErc20Tokens => _defaultTokens.map((token) {
Expand Down
17 changes: 11 additions & 6 deletions lib/exchange/exchange_provider_description.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ class ExchangeProviderDescription extends EnumerableItem<int> 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:
Expand All @@ -58,6 +61,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return letsExchange;
case 11:
return stealthEx;
case 12:
return chainflip;
default:
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
}
Expand Down
313 changes: 313 additions & 0 deletions lib/exchange/provider/chainflip_exchange_provider.dart
Original file line number Diff line number Diff line change
@@ -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<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 = '6ba154d4-e219-472a-9674-5fa5b1300ccf';
// TODO: Example key, replace with CW key

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) {
print(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,
'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<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) {
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<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;
}
}
2 changes: 1 addition & 1 deletion lib/exchange/provider/exolix_exchange_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading