CW-1175-Implement-swaps.xyz-swap-provider (#2526)

* Integrate SwapsXYZ

* Remove print

* enhance SwapsXyz EVM token swap handling

* Disable fixed rate support in SwapsXyz provider

* enable fixed rate swap

* fix timestamp parsing

* remove sender and recipient address from fetchRate

* Improve SwapsXYZ trade approval logic

* fix trade ID overflow

* add Base chain support for  swaps xyz

* Update lib/view_model/exchange/exchange_trade_view_model.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* add missing support for Base

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2025-10-20 23:22:01 +02:00
committed by GitHub
co-authored by Omar Hatem
parent 697ec0ab82
commit ee0996f535
37 changed files with 1557 additions and 77 deletions
@@ -190,6 +190,7 @@ jobs:
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
echo "const swapsXyzApiKey = '${{ secrets.SWAPSXYZ_API_KEY }}';" >> lib/.secrets.g.dart
- name: prepare monero_c and cache
run: |
@@ -180,6 +180,7 @@ jobs:
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
echo "const swapsXyzApiKey = '${{ secrets.SWAPSXYZ_API_KEY }}';" >> lib/.secrets.g.dart
- name: Generate KeyStore
run: |
@@ -176,6 +176,7 @@ jobs:
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
echo "const swapsXyzApiKey = '${{ secrets.SWAPSXYZ_API_KEY }}';" >> lib/.secrets.g.dart
- name: Fetch prebuilt Torch
run: |
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.468 0.614 498.759 500.158" width="500
px" height="500px">
<defs>
<linearGradient id="paint0_linear_306_24" x1="66.49" y1="0.0200195" x2="66.49" y2="133" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.875312, 0, 0, 1.880575, 0.468736, 0.576712)">
<stop stop-color="#D5A2FF"/>
<stop offset="0.5" stop-color="#4B68B3"/>
<stop offset="1" stop-color="#30001A"/>
</linearGradient>
<linearGradient id="paint1_linear_306_24" x1="199.47" y1="0.0200195" x2="199.47" y2="126.85" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.875312, 0, 0, 1.880575, 0.468736, 0.576712)">
<stop stop-color="#D5A2FF"/>
<stop offset="0.5" stop-color="#4B68B3"/>
<stop offset="1" stop-color="#30001A"/>
</linearGradient>
<linearGradient id="paint2_linear_306_24" x1="199.47" y1="133" x2="199.47" y2="265.98" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.875312, 0, 0, 1.880575, 0.468736, 0.576712)">
<stop stop-color="#30001A"/>
<stop offset="0.5" stop-color="#4B68B3"/>
<stop offset="1" stop-color="#D5A2FF"/>
</linearGradient>
<linearGradient id="paint3_linear_306_24" x1="66.49" y1="139.15" x2="66.49" y2="265.98" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.875312, 0, 0, 1.880575, 0.468736, 0.576712)">
<stop stop-color="#30001A"/>
<stop offset="0.5" stop-color="#4B68B3"/>
<stop offset="1" stop-color="#D5A2FF"/>
</linearGradient>
<linearGradient id="paint4_linear_306_24" x1="196.7" y1="85.6736" x2="133.028" y2="133.067" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.875312, 0, 0, 1.880575, 0.468736, 0.576712)">
<stop stop-color="#D5A2FF"/>
<stop offset="0.5" stop-color="#4B68B3"/>
<stop offset="1" stop-color="#30001A"/>
</linearGradient>
<linearGradient id="paint5_linear_306_24" x1="69.2504" y1="180.326" x2="132.922" y2="132.933" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.875312, 0, 0, 1.880575, 0.468736, 0.576712)">
<stop stop-color="#D5A2FF"/>
<stop offset="0.5" stop-color="#4B68B3"/>
<stop offset="1" stop-color="#30001A"/>
</linearGradient>
</defs>
<path d="M 249.847 0.614 L 249.847 250.694 L 125.158 250.694 C 90.728 250.694 59.559 236.683 36.981 214.078 C 14.421 191.454 0.468 160.181 0.468 125.652 C 0.468 91.127 14.421 59.852 36.981 37.23 C 59.559 14.625 90.728 0.614 125.158 0.614 L 249.847 0.614 Z" fill="url(#paint0_linear_306_24)" style="stroke-width: 1;" transform="matrix(1, 0, 0, 1, 0, 1.4210854715202004e-14)"/>
<path d="M 499.227 125.652 C 499.227 175.94 469.634 219.287 426.952 239.127 L 249.847 0.614 L 374.537 0.614 C 408.967 0.614 440.135 14.625 462.713 37.23 C 485.293 59.852 499.227 91.108 499.227 125.652 Z" fill="url(#paint1_linear_306_24)" style="stroke-width: 1;" transform="matrix(1, 0, 0, 1, 0, 1.4210854715202004e-14)"/>
<path d="M 499.227 375.732 C 499.227 444.786 443.398 500.772 374.537 500.772 L 249.847 500.772 L 249.847 250.694 L 374.537 250.694 C 408.967 250.694 440.135 264.702 462.713 287.308 C 485.293 309.931 499.227 341.187 499.227 375.732 Z" fill="url(#paint2_linear_306_24)" style="stroke-width: 1;" transform="matrix(1, 0, 0, 1, 0, 1.4210854715202004e-14)"/>
<path d="M 249.847 500.772 L 125.158 500.772 C 90.728 500.772 59.559 486.761 36.981 464.138 C 14.421 441.534 0.468 410.259 0.468 375.732 C 0.468 325.446 30.061 282.099 72.744 262.259 L 249.829 500.772 L 249.847 500.772 Z" fill="url(#paint3_linear_306_24)" style="stroke-width: 1;" transform="matrix(1, 0, 0, 1, 0, 1.4210854715202004e-14)"/>
<path d="M 462.695 287.308 C 440.117 264.684 408.949 250.694 374.518 250.694 L 249.829 250.694 L 249.829 0.614 L 426.914 239.127 L 462.676 287.308 L 462.695 287.308 Z" fill="url(#paint4_linear_306_24)" style="stroke-width: 1;" transform="matrix(1, 0, 0, 1, 0, 1.4210854715202004e-14)"/>
<path d="M 36.981 214.078 C 59.559 236.701 90.728 250.694 125.158 250.694 L 249.847 250.694 L 249.847 500.772 L 72.762 262.259 L 36.999 214.078 L 36.981 214.078 Z" fill="url(#paint5_linear_306_24)" style="stroke-width: 1;" transform="matrix(1, 0, 0, 1, 0, 1.4210854715202004e-14)"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+50
View File
@@ -71,4 +71,54 @@ class AmountConverter {
static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat.format(
DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider)));
static String toBaseUnits(String amount, int decimals) {
amount = amount.trim();
if (amount.isEmpty) return '0';
final neg = amount.startsWith('-');
if (neg) amount = amount.substring(1);
amount = amount.replaceAll(',', '');
final parts = amount.split('.');
final whole = parts[0].isEmpty ? '0' : parts[0];
final frac = parts.length > 1 ? parts[1] : '';
final fracPadded = (frac + '0' * decimals).substring(0, decimals);
final pow10 = BigInt.from(10).pow(decimals);
final wholeBI = BigInt.parse(whole);
final fracBI = fracPadded.isEmpty ? BigInt.zero : BigInt.parse(fracPadded);
final res = wholeBI * pow10 + fracBI;
return neg ? '-${res.toString()}' : res.toString();
}
static String fromBaseUnits(String units, int decimals) {
units = units.trim();
if (units.isEmpty) return '0';
final neg = units.startsWith('-');
if (neg) units = units.substring(1);
if (decimals == 0) return neg ? '-$units' : units;
// pad if shorter than decimals
if (units.length <= decimals) {
final s = units.padLeft(decimals + 1, '0'); // ensures at least "0xxxx"
final intPart = s.substring(0, s.length - decimals);
var fracPart = s.substring(s.length - decimals);
fracPart = fracPart.replaceFirst(RegExp(r'0+$'), '');
final out = fracPart.isEmpty ? intPart : '$intPart.$fracPart';
return neg ? '-$out' : out;
}
final intPart = units.substring(0, units.length - decimals);
var fracPart = units.substring(units.length - decimals);
fracPart = fracPart.replaceFirst(RegExp(r'0+$'), '');
final out = fracPart.isEmpty ? intPart : '$intPart.$fracPart';
return neg ? '-$out' : out;
}
}
+1
View File
@@ -12,6 +12,7 @@ mixin PendingTransaction {
String get feeFormattedValue;
String? feeRate;
String get hex;
String? get evmTxHashFromRawHex => null;
int? get outputCount => null;
PendingChange? change;
+1
View File
@@ -111,6 +111,7 @@ abstract class EVMChainClient {
sender: senderAddress,
to: toAddress,
value: value,
data: data,
);
return estimatedGas.toInt();
+76
View File
@@ -40,6 +40,7 @@ import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'contract/erc20.dart';
import 'evm_chain_transaction_info.dart';
import 'evm_erc20_balance.dart';
@@ -611,6 +612,50 @@ abstract class EVMChainWalletBase
return pendingEVMChainTransaction;
}
Future<PendingTransaction> createCallDataTransaction(
String to,
String dataHex,
BigInt valueWei,
EVMChainTransactionPriority priority,
) async {
// Estimate gas with the SAME call (sender, to, value, data)
final gas = await calculateActualEstimatedFeeForCreateTransaction(
amount: valueWei, // native value (usually 0 for ERC20 transfer)
receivingAddressHex: to,
priority: priority,
contractAddress: null,
data: _client.hexToBytes(dataHex),
);
final nativeCurrency = switch (_client.chainId) {
137 => CryptoCurrency.maticpoly,
8453 => CryptoCurrency.baseEth,
_ => CryptoCurrency.eth,
};
// Fallback for nodes that fail estimate (non-zero)
final gasUnits = gas.estimatedGasUnits == 0 ? 65000 : gas.estimatedGasUnits;
// Sign raw (native) tx with callData
return _client.signTransaction(
privateKey: _evmChainPrivateKey,
toAddress: to,
amount: valueWei,
gasFee: BigInt.from(gas.estimatedGasFee),
estimatedGasUnits: gasUnits,
maxFeePerGas: gas.maxFeePerGas,
priority: priority,
currency: nativeCurrency,
feeCurrency: nativeCurrency.title,
exponent: 18,
contractAddress: null,
data: dataHex,
gasPrice: gas.gasPrice,
);
}
Future<PendingTransaction> createApprovalTransaction(BigInt amount, String spender,
CryptoCurrency token, EVMChainTransactionPriority priority, String feeCurrency) async {
final CryptoCurrency transactionCurrency =
@@ -793,6 +838,37 @@ abstract class EVMChainWalletBase
}
}
Future<bool> isApprovalRequired(
String tokenContract,
String spender,
BigInt requiredAmount) async {
const zero = '0x0000000000000000000000000000000000000000';
const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
final token = tokenContract.toLowerCase();
if (token == zero || token == evmNative.toLowerCase()) return false;
if (requiredAmount <= BigInt.zero) return false;
try {
final owner = _evmChainPrivateKey.address;
final erc20 = ERC20(
client: _client.getWeb3Client()!,
address: EthereumAddress.fromHex(tokenContract),
chainId: _client.chainId,
);
final allowance = await erc20.allowance(
owner,
EthereumAddress.fromHex(spender));
return allowance < requiredAmount;
} catch (e) {
printV('approval-check error: $e');
return true;
}
}
Future<EthPrivateKey> getPrivateKey({
String? mnemonic,
String? privateKey,
@@ -51,7 +51,15 @@ class PendingEVMChainTransaction with PendingTransaction {
return '0x${Hex.HEX.encode(txid)}';
}
@override
String? get evmTxHashFromRawHex {
final no0x = hex.startsWith('0x') ? hex.substring(2) : hex;
final bytes = Uint8List.fromList(Hex.HEX.decode(no0x));
final digest = keccak256(bytes);
return '0x${Hex.HEX.encode(digest)}';
}
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
+10
View File
@@ -189,6 +189,10 @@ class CWBase extends Base {
@override
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
@override
Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) =>
(wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
@override
Future<PendingTransaction> createTokenApproval(
WalletBase wallet,
@@ -205,6 +209,12 @@ class CWBase extends Base {
"BASE",
);
@override
Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei,
TransactionPriority priority) =>
(wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority);
@override
void setHardwareWalletService(WalletBase wallet, HardwareWalletService service) {
if (service is EVMChainLedgerService) {
+3
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
@@ -62,6 +63,8 @@ class TradeMonitor {
return ChainflipExchangeProvider(tradesStore: trades);
case ExchangeProviderDescription.xoSwap:
return XOSwapExchangeProvider();
case ExchangeProviderDescription.swapsXyz:
return SwapsXyzExchangeProvider();
}
return null;
}
+10
View File
@@ -217,11 +217,21 @@ class CWEthereum extends Ethereum {
return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase());
}
@override
Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) =>
(wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
@override
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender,
CryptoCurrency token, TransactionPriority priority) =>
(wallet as EVMChainWallet).createApprovalTransaction(
amount, spender, token, priority as EVMChainTransactionPriority, "ETH");
@override
Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei,
TransactionPriority priority) =>
(wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority);
// Integrations
@override
Future<BigInt> getDEuroSavingsBalance(WalletBase wallet) =>
@@ -36,6 +36,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png');
static const xoSwap =
ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg');
static const swapsXyz =
ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg');
static ExchangeProviderDescription deserialize({required int raw}) {
switch (raw) {
@@ -67,6 +69,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return chainflip;
case 13:
return xoSwap;
case 14:
return swapsXyz;
default:
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
}
@@ -91,12 +91,13 @@ class ChainflipExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
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 {
@@ -91,12 +91,13 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
+7 -6
View File
@@ -33,12 +33,13 @@ abstract class ExchangeProvider {
Future<Trade> findTradeById({required String id});
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount});
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
});
Future<bool> checkIsAvailable();
}
@@ -112,12 +112,13 @@ class ExolixExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
@@ -78,12 +78,13 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
final networkFrom = _getNetworkType(from);
final networkTo = _getNetworkType(to);
try {
@@ -120,12 +120,13 @@ class SideShiftExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
@@ -89,12 +89,13 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
@@ -0,0 +1,823 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_not_created_exception.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/amount_converter.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
class SwapsXyzExchangeProvider extends ExchangeProvider {
SwapsXyzExchangeProvider() : super(pairList: supportedPairs(_notSupported));
static const List<CryptoCurrency> _notSupported = [];
static final _apiKey = secrets.swapsXyzApiKey;
static const _baseUrl = 'api-v2.swaps.xyz';
static const _getChainList = 'api/getChainList';
static const _getPaths = 'api/getPaths';
static const _getQuotePaths = 'api/getQuote';
static const _getAction = 'api/getAction';
static const _registerTxs = 'api/registerTxs';
static const _getStatus = 'api/getStatus';
static final _headers = {'x-api-key': _apiKey};
static final _supportedChainList = <Chain>[];
final Map<int, List<TokenPathInfo>> _tokensCache = {};
@override
String get title => 'Swaps.XYZ';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => true;
@override
ExchangeProviderDescription get description =>
ExchangeProviderDescription.swapsXyz;
@override
Future<bool> checkIsAvailable() async => true;
@override
Future<Limits> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
}) async {
try {
final chains = await _geSupportedChain();
if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
final srcChain = _findChainByCurrency(from, chains);
final dstChain = _findChainByCurrency(to, chains);
await _ensureTokensCached(
fromChain: srcChain, toChain: dstChain, from: from, to: to);
final srcToken = _getTokenAddress(currency: from, chain: srcChain);
final dstToken = _getTokenAddress(currency: to, chain: dstChain);
// parameters to and from are swapped in this endpoint
// i.e. to get limits for FROM -> TO, we request paths for TO -> FROM
final params = {
'srcChainId': '${dstChain.chainId}',
'srcToken': dstToken,
'dstChainId': '${srcChain.chainId}',
'dstToken': srcToken,
};
final uri = Uri.https(_baseUrl, _getPaths, params);
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (res.statusCode != 200) {
throw Exception('Unexpected http status: ${res.statusCode}');
}
final body = json.decode(res.body) as Map<String, dynamic>;
final paths = (body['paths'] as List?) ?? const [];
if (paths.isEmpty) {
throw Exception('No paths for ${from.title} -> ${to.title}');
}
final path0 = paths.first as Map<String, dynamic>;
final tokens = path0['tokens'];
// If tokens == "all", use path-level amountLimits
if (tokens is String && tokens == 'all') {
final amountLimits = path0['amountLimits'] as Map<String, dynamic>?;
final min =
double.tryParse(amountLimits?['minAmount']?.toString() ?? '');
final max =
double.tryParse(amountLimits?['maxAmount']?.toString() ?? '');
return Limits(min: min, max: max);
}
// Otherwise tokens is a list -> find the specific token entry
final tokenList = (tokens as List?) ?? const [];
final token = findTokenBySymbol(title: from.title, tokens: tokenList);
if (token == null) {
throw Exception('Token info not found for ${from.title}');
}
final min = double.tryParse(token['minAmount']?.toString() ?? '');
final max = double.tryParse(token['maxAmount']?.toString() ?? '');
return Limits(min: min, max: max);
} catch (e) {
printV('fetchLimits error: $e');
throw Exception('Error fetching limits: $e');
}
}
Future<_PathInfo?> _pickPath({
required int srcChainId,
required String srcToken,
required int dstChainId,
required String dstToken,
}) async {
final uri = Uri.https(_baseUrl, _getPaths, {
'srcChainId': '$srcChainId',
'srcToken': srcToken,
'dstChainId': '$dstChainId',
'dstToken': dstToken,
});
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (res.statusCode != 200) return null;
final body = json.decode(res.body) as Map<String, dynamic>;
final paths =
(body['paths'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
if (paths.isEmpty) return null;
final p = paths.first;
return _PathInfo(
supportsExactOut: p['supportsExactAmountOut'] == true,
minToAmountHuman: (p['amountLimits']?['minAmount'] as String?) ?? '0',
);
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
try {
final chains = await _geSupportedChain();
if (chains.isEmpty) return 0.0;
final srcChain = _findChainByCurrency(from, chains);
final dstChain = _findChainByCurrency(to, chains);
await _ensureTokensCached(
fromChain: srcChain, toChain: dstChain, from: from, to: to);
final srcToken = _getTokenAddress(currency: from, chain: srcChain);
final dstToken = _getTokenAddress(currency: to, chain: dstChain);
if (isReceiveAmount) {
final path = await _pickPath(
srcChainId: srcChain.chainId,
srcToken: srcToken,
dstChainId: dstChain.chainId,
dstToken: dstToken,
);
if (path == null || !path.supportsExactOut) {
printV(
'fetchRate: route does not support exact-amount-out for ${from.title} -> ${to.title}');
return 0.0;
}
}
final humanAmountStr = amount.toString();
final formattedAmount = AmountConverter.toBaseUnits(
humanAmountStr,
isReceiveAmount ? to.decimals : from.decimals,
);
final params = {
'swapDirection':
isReceiveAmount ? 'exact-amount-out' : 'exact-amount-in',
'srcToken': srcToken,
'dstToken': dstToken,
'srcChainId': '${srcChain.chainId}',
'dstChainId': '${dstChain.chainId}',
'amount': formattedAmount,
};
final uri = Uri.https(_baseUrl, _getQuotePaths, params);
final response =
await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (response.statusCode != 200) {
printV('fetchRate failed: ${response.body}');
return 0.0;
}
final data = json.decode(response.body) as Map<String, dynamic>;
final exchangeRate = (data['exchangeRate'] as num?)?.toDouble() ?? 0.0;
return exchangeRate;
} catch (e) {
printV('fetchRate error: $e');
return 0.0;
}
}
@override
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
try {
final sender = request.refundAddress.trim();
final recipient = request.toAddress.trim();
if (sender.isEmpty || recipient.isEmpty) {
throw Exception(
'Sender (refundAddress) or recipient (toAddress) is empty');
}
final chains = await _geSupportedChain();
if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
final srcChain = _findChainByCurrency(request.fromCurrency, chains);
final dstChain = _findChainByCurrency(request.toCurrency, chains);
await _ensureTokensCached(
fromChain: srcChain,
toChain: dstChain,
from: request.fromCurrency,
to: request.toCurrency,
);
final srcToken =
_getTokenAddress(currency: request.fromCurrency, chain: srcChain);
final dstToken =
_getTokenAddress(currency: request.toCurrency, chain: dstChain);
// Optional: ensure path supports exact-out before attempting fixed rate.
if (isFixedRateMode) {
final path = await _pickPath(
srcChainId: srcChain.chainId,
srcToken: srcToken,
dstChainId: dstChain.chainId,
dstToken: dstToken,
);
if (path == null || !path.supportsExactOut) {
throw Exception(
'This route does not support fixed receive (exact-amount-out)');
}
}
final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount;
final rawAmount = double.tryParse(amountStr) ?? 0.0;
if (rawAmount <= 0) throw Exception('Invalid amount');
final formattedAmount = AmountConverter.toBaseUnits(
amountStr,
isFixedRateMode
? request.toCurrency.decimals
: request.fromCurrency.decimals,
);
final params = {
'actionType': 'swap-action',
'sender': sender,
'srcChainId': '${srcChain.chainId}',
'srcToken': srcToken,
'dstChainId': '${dstChain.chainId}',
'dstToken': dstToken,
'slippage': '100',
'swapDirection':
isFixedRateMode ? 'exact-amount-out' : 'exact-amount-in',
'amount': formattedAmount,
'recipient': recipient,
};
final uri = Uri.https(_baseUrl, _getAction, params);
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (res.statusCode != 200) {
throw Exception('getAction failed: ${res.statusCode} ${res.body}');
}
final data = json.decode(res.body) as Map<String, dynamic>;
final txId = data['txId'] as String? ?? '';
final vmId = data['vmId'] as String? ?? '';
final txObj = (data['tx'] as Map?) ?? const {};
final txTo = txObj['to']?.toString();
final chainId = txObj['chainId']?.toString();
final routerData = txObj['data']?.toString();
final txValue = txObj['value']?.toString() ?? '0';
final bridgeIds = (data['bridgeIds'] as List?) ?? const [];
if (txId.isEmpty) throw Exception('No txId returned by getAction');
final amtIn = (data['amountIn'] as Map?) ?? const {};
final amtInMax = (data['amountInMax'] as Map?) ?? const {};
final srcTokenAddr = amtIn['address']?.toString();
final srcTokenDecs =
(amtIn['decimals'] as num?)?.toInt() ?? request.fromCurrency.decimals;
final requiresTokenApproval =
data['requiresTokenApproval'] as bool? ?? false;
final reqAmountStr =
(amtInMax['amount'] ?? amtIn['amount'])?.toString() ?? '0';
final reqAmountRaw = reqAmountStr.replaceAll('n', '');
final needToRegisterInSwapXyz =
vmId == 'alt-vm' || bridgeIds.contains('alt-vm');
final trade = Trade(
id: txId,
router: chainId,
providerId: vmId,
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
inputAddress: txTo,
refundAddress: request.refundAddress,
state: TradeState.created,
providerName: title,
createdAt: DateTime.now(),
amount: request.fromAmount,
receiveAmount: request.toAmount,
payoutAddress: request.toAddress,
isSendAll: isSendAll,
needToRegisterInSwapXyz: needToRegisterInSwapXyz,
sourceTokenAddress: srcTokenAddr ?? srcToken,
sourceTokenDecimals: srcTokenDecs,
sourceTokenAmountRaw: reqAmountRaw,
requiresTokenApproval: requiresTokenApproval,
routerData: routerData,
routerValue: txValue,
userCurrencyFromRaw:
'${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
userCurrencyToRaw:
'${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
);
return trade;
} catch (e) {
printV('createTrade error: $e');
throw TradeNotCreatedException(description, description: e.toString());
}
}
/// Register a broadcasted tx with Swaps.xyz (required for alt-vm).
Future<bool> registerAltVmTx({
required String txId,
required String txHash,
required int chainId,
required String vmId,
}) async {
try {
final uri = Uri.https(_baseUrl, _registerTxs);
final payload = {
'txId': txId,
'vmId': vmId,
'txHash': txHash,
'chainId': chainId,
};
final res = await ProxyWrapper().post(
clearnetUri: uri,
headers: {
..._headers,
'content-type': 'application/json',
},
body: jsonEncode(payload),
);
if (res.statusCode != 200) {
printV('registerTxs failed: ${res.statusCode} ${res.body}');
return false;
}
final List<dynamic> body = json.decode(res.body) as List<dynamic>;
if (body.isEmpty) return false;
final isSuccess =
(body[0] as Map<String, dynamic>)['success'] as bool? ?? false;
return isSuccess;
} catch (e) {
printV('registerAltVmTx error: $e');
return false;
}
}
@override
Future<Trade> findTradeById({required String id}) async {
if (id.isEmpty) {
throw Exception('Trade id is empty');
}
final uri = Uri.https(_baseUrl, _getStatus, {'txId': id});
final resp = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (resp.statusCode != 200) {
throw Exception('getStatus failed: ${resp.statusCode} ${resp.body}');
}
final data = json.decode(resp.body) as Map<String, dynamic>;
final isSuccess = (data['success'] as bool?);
if (isSuccess != null && !isSuccess) {
final error = data['error'] as Map<String, dynamic>?;
if (error != null) {
final code = error['code']?.toString() ?? 'unknown';
throw Exception(
'SwapXyzExchangeProvider findTradeById error: ($id) $code');
}
}
final statusStr = (data['status'] as String?)?.toLowerCase() ?? 'NOT_FOUND';
final state = _mapSwapsStatusToTradeState(statusStr);
final refundAddress = data['sender']?.toString();
final srcTransaction = (data['srcTx'] as Map?)?.cast<String, dynamic>();
final dstTransaction = (data['dstTx'] as Map?)?.cast<String, dynamic>();
final inputAddress = srcTransaction?['toAddress']?.toString();
final payoutAddress = dstTransaction?['toAddress']?.toString();
final srcPaymentToken =
(srcTransaction?['paymentToken'] as Map?)?.cast<String, dynamic>();
final dstPaymentToken =
(dstTransaction?['paymentToken'] as Map?)?.cast<String, dynamic>();
final fromSymbol = (srcPaymentToken?['symbol'] as String?) ?? '';
final toSymbol = (dstPaymentToken?['symbol'] as String?) ?? '';
CryptoCurrency? toCurrency;
if (toSymbol.isNotEmpty) {
toCurrency = CryptoCurrency.safeParseCurrencyFromString(toSymbol);
}
final srcDecimals = (srcPaymentToken?['decimals'] as num?)?.toInt() ?? 0;
final dstDecimals = (dstPaymentToken?['decimals'] as num?)?.toInt() ?? 0;
final txHash = srcTransaction?['txHash'] as String?;
// Minimal-unit amounts like "12000n"
final srcAmountRaw = srcPaymentToken?['amount']?.toString();
final dstAmountRaw = dstPaymentToken?['amount']?.toString();
String? receiveAmount;
if (dstAmountRaw != null) {
final dstAmountMinimal = _stripN(dstAmountRaw);
receiveAmount =
AmountConverter.fromBaseUnits(dstAmountMinimal, dstDecimals);
}
final srcAmountMinimal = _stripN(srcAmountRaw);
final amount = AmountConverter.fromBaseUnits(srcAmountMinimal, srcDecimals);
final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromSymbol);
// Timestamps can be num or "123n" handle both
final srcTs = _parseUnixSeconds(srcTransaction?['timestamp']);
final dstTs = _parseUnixSeconds(dstTransaction?['timestamp']);
final timestamp = srcTs ?? dstTs;
final createdAt = timestamp != null
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true)
.toLocal()
: null;
return Trade(
id: (data['txId'] as String?) ?? id,
from: fromCurrency,
to: toCurrency,
provider: description,
inputAddress: inputAddress,
payoutAddress: payoutAddress,
amount: amount,
receiveAmount: receiveAmount,
txId: txHash,
state: state,
createdAt: createdAt,
refundAddress: refundAddress,
userCurrencyFromRaw: '${fromSymbol.toUpperCase()}' + '_',
userCurrencyToRaw: '${toSymbol.toUpperCase()}' + '_',
);
}
TradeState _mapSwapsStatusToTradeState(String s) {
switch (s) {
case 'pending':
case 'processing':
return TradeState.pending;
case 'success':
case 'completed':
case 'complete':
return TradeState.finished;
case 'failed':
case 'cancelled':
case 'canceled':
case 'error':
return TradeState.failed;
default:
return TradeState.pending;
}
}
// Load & cache supported chains once
Future<List<Chain>> _geSupportedChain() async {
if (_supportedChainList.isNotEmpty) return _supportedChainList;
try {
final uri = Uri.https(_baseUrl, _getChainList);
final response =
await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (response.statusCode != 200) return [];
final data = json.decode(response.body) as List<dynamic>;
_supportedChainList
..clear()
..addAll(data.map((e) => Chain.fromJson(e as Map<String, dynamic>)));
return _supportedChainList;
} catch (e) {
printV(e);
return [];
}
}
// Ensure tokens for this pair are cached (fills both src & dst chain entries)
// Replace your _ensureTokensCached with this one
Future<void> _ensureTokensCached({
required Chain fromChain,
required Chain toChain,
required CryptoCurrency from,
required CryptoCurrency to,
}) async {
bool needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
(_tokensCache[fromChain.chainId]?.isEmpty ?? true);
bool needDst = !_tokensCache.containsKey(toChain.chainId) ||
(_tokensCache[toChain.chainId]?.isEmpty ?? true);
if (!needSrc && !needDst) return;
// First try: from -> to
await _fetchAndCacheTokens(
srcChainId: fromChain.chainId,
srcToken: from.title.toUpperCase(),
dstChainId: toChain.chainId,
dstToken: to.title.toUpperCase(),
);
// Re-check after first call
needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
(_tokensCache[fromChain.chainId]?.isEmpty ?? true);
needDst = !_tokensCache.containsKey(toChain.chainId) ||
(_tokensCache[toChain.chainId]?.isEmpty ?? true);
// If any still missing, try swapped: to -> from
if (needSrc || needDst) {
await _fetchAndCacheTokens(
srcChainId: toChain.chainId,
srcToken: to.title.toUpperCase(),
dstChainId: fromChain.chainId,
dstToken: from.title.toUpperCase(),
);
}
}
// call getPaths and merge tokens into cache keyed by the chainId
Future<void> _fetchAndCacheTokens({
required int srcChainId,
required String srcToken,
required int dstChainId,
required String dstToken,
}) async {
final params = <String, String>{
'srcChainId': '$srcChainId',
'srcToken': srcToken,
'dstChainId': '$dstChainId',
};
final uri = Uri.https(_baseUrl, _getPaths, params);
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
if (res.statusCode != 200) {
printV('getPaths failed: ${res.statusCode} ${res.body}');
return;
}
final body = json.decode(res.body) as Map<String, dynamic>;
final paths = (body['paths'] as List?) ?? const [];
if (paths.isEmpty) return;
final path0 = paths.first as Map<String, dynamic>;
final pathChainId = (path0['chainId'] as num?)?.toInt();
final tokens = (path0['tokens'] as List?) ?? const [];
if (pathChainId == null || tokens.isEmpty) return;
final parsed = tokens
.map((t) => TokenPathInfo.fromJson(t as Map<String, dynamic>))
.toList();
_mergeCache(pathChainId, parsed);
}
// Merge by symbol, prefer entries that have a non-empty address/decimals
void _mergeCache(int chainId, List<TokenPathInfo> incoming) {
final existing = _tokensCache[chainId] ?? const <TokenPathInfo>[];
final bySymbol = <String, TokenPathInfo>{
for (final t in existing) t.symbol: t
};
for (final t in incoming) {
final cur = bySymbol[t.symbol];
if (cur == null) {
bySymbol[t.symbol] = t;
} else {
// If incoming has a real address/decimals, prefer it
final hasBetterAddr = (t.address != null && t.address!.isNotEmpty) &&
(cur.address == null || cur.address!.isEmpty);
final hasBetterDec = (t.decimals != null) && (cur.decimals == null);
if (hasBetterAddr || hasBetterDec) {
bySymbol[t.symbol] = t;
}
}
}
_tokensCache[chainId] = bySymbol.values.toList();
}
String _nativeSymbolForChain(Chain chain) {
final network = chain.name.toUpperCase();
switch (network) {
case 'BTC':
case 'BITCOIN':
return 'BTC';
case 'XMR':
case 'MONERO':
return 'XMR';
case 'TRX':
case 'TRON':
return 'TRX';
case 'ETH':
case 'ETHEREUM':
return 'ETH';
case 'LTC':
case 'LITECOIN':
return 'LTC';
case 'DOGE':
case 'DOGECOIN':
return 'DOGE';
case 'XRP':
case 'RIPPLE':
return 'XRP';
case 'XLM':
case 'STELLAR':
return 'XLM';
case 'SOL':
case 'SOLANA':
return 'SOL';
case 'CARDANO':
return 'ADA';
case 'Bitcoin Cash':
return 'BCH';
case 'POLYGON':
return 'POL';
default:
return network;
}
}
String _getTokenAddress({
required CryptoCurrency currency,
required Chain chain,
}) {
final symbol = currency.title.toUpperCase();
final list = _tokensCache[chain.chainId];
// Try cache hit
if (list != null && list.isNotEmpty) {
for (final t in list) {
if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) {
return t.address!;
}
}
}
// If it's the native coin for this chain, return native placeholder
final nativeSym = _nativeSymbolForChain(chain);
if (nativeSym == symbol) {
return _nativePlaceholderForVm(chain.vmId);
}
// May fail for non-native Alt-VM assets
return symbol;
}
String _nativePlaceholderForVm(String vmId) {
switch (vmId.toLowerCase()) {
case 'evm':
return '0x0000000000000000000000000000000000000000';
case 'alt-vm':
return '0x0000000000000000000000000000000000000000';
default:
return '0x0000000000000000000000000000000000000000';
}
}
Map<String, dynamic>? findTokenBySymbol(
{required String title, required List<dynamic> tokens}) {
final reqSymbol = title.toUpperCase();
for (final token in tokens) {
final map = token as Map<String, dynamic>;
final symbol = (map['symbol'] as String?)?.toUpperCase();
if (symbol == reqSymbol) return map;
}
return null;
}
Chain _findChainByCurrency(CryptoCurrency cur, List<Chain> chains) {
final network = _normalizeCakeNetwork(cur.tag ?? cur.title);
return chains.firstWhere(
(c) => c.name.toUpperCase() == network,
orElse: () => throw Exception('Unsupported chain for ${cur.title}'),
);
}
String _normalizeCakeNetwork(String network) {
return switch (network.toUpperCase()) {
'ETH' => 'ETHEREUM',
'BSC' => 'BNB SMART CHAIN',
'POL' => 'POLYGON',
'AVAXC' => 'AVALANCHE',
'TRX' => 'TRON',
'SOL' => 'SOLANA',
'CRO' => 'CRONOS',
'ADA' => 'CARDANO',
'KAS' => 'KASPA',
'TON' => 'TONCOIN',
'BCH' => 'BITCOIN CASH',
_ => network.toUpperCase(),
};
}
int? _parseUnixSeconds(dynamic value) {
if (value == null) return null;
if (value is num) return value.toInt();
if (value is String) {
final clean = _stripN(value);
return int.tryParse(clean);
}
return null;
}
String _stripN(String? str) {
final s = str ?? '0';
return s.endsWith('n') ? s.substring(0, s.length - 1) : s;
}
}
class TokenPathInfo {
final String symbol;
final String? address;
final int? decimals;
final String? minAmount;
final String? maxAmount;
TokenPathInfo({
required this.symbol,
required this.address,
required this.decimals,
required this.minAmount,
required this.maxAmount,
});
factory TokenPathInfo.fromJson(Map<String, dynamic> json) => TokenPathInfo(
symbol: (json['symbol'] as String?)?.toUpperCase() ?? '',
address: json['address'] as String?,
decimals: json['decimals'] as int?,
minAmount: json['minAmount']?.toString(),
maxAmount: json['maxAmount']?.toString(),
);
}
class Chain {
final int chainId;
final String name;
final String vmId;
Chain({
required this.chainId,
required this.name,
required this.vmId,
});
factory Chain.fromJson(Map<String, dynamic> json) {
return Chain(
chainId: json['chainId'] as int,
name: json['name'] as String,
vmId: json['vmId'] as String,
);
}
}
class _PathInfo {
final bool supportsExactOut;
final String minToAmountHuman;
_PathInfo({required this.supportsExactOut, required this.minToAmountHuman});
}
@@ -105,7 +105,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
@@ -66,12 +66,13 @@ class ThorChainExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
@@ -120,12 +120,13 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount
}) async {
try {
if (amount == 0) return 0.0;
@@ -142,12 +142,13 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}
}
@override
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount,
required bool isReceiveAmount
}) async {
try {
final rates = await getRatesForPair(from: from, to: to);
+59 -1
View File
@@ -33,6 +33,15 @@ class Trade extends HiveObject {
this.router,
this.userCurrencyFromRaw,
this.userCurrencyToRaw,
// The following fields are used for SwapXyz trades only
this.needToRegisterInSwapXyz,
this.sourceTokenAddress,
this.sourceTokenDecimals,
this.routerData,
this.routerValue,
this.routerChainId,
this.sourceTokenAmountRaw,
this.requiresTokenApproval
}) {
if (provider != null) providerRaw = provider.raw;
@@ -133,6 +142,31 @@ class Trade extends HiveObject {
@HiveField(25, defaultValue: '')
String? userCurrencyToRaw;
// The following fields are used for SwapXyz trades only
@HiveField(26)
bool? needToRegisterInSwapXyz;
@HiveField(27)
String? sourceTokenAddress;
@HiveField(28)
int? sourceTokenDecimals;
@HiveField(29)
String? routerData;
@HiveField(30)
String? routerValue;
@HiveField(31)
int? routerChainId;
@HiveField(32)
String? sourceTokenAmountRaw;
@HiveField(33, defaultValue: false)
bool? requiresTokenApproval;
CryptoCurrency? get userCurrencyFrom {
if (userCurrencyFromRaw == null || userCurrencyFromRaw!.isEmpty) {
return null;
@@ -249,6 +283,14 @@ class TradeAdapter extends TypeAdapter<Trade> {
router: fields[22] as String?,
userCurrencyFromRaw: fields[24] as String?,
userCurrencyToRaw: fields[25] as String?,
needToRegisterInSwapXyz: fields[26] as bool?,
sourceTokenAddress: fields[27] as String?,
sourceTokenDecimals: fields[28] as int?,
routerData: fields[29] as String?,
routerValue: fields[30] as String?,
routerChainId: fields[31] as int?,
sourceTokenAmountRaw: fields[32] as String?,
requiresTokenApproval: fields[33] as bool?,
)
..providerRaw = fields[1] == null ? 0 : fields[1] as int
..fromRaw = (fields[2] as int?) ?? -1
@@ -311,7 +353,23 @@ class TradeAdapter extends TypeAdapter<Trade> {
..writeByte(24)
..write(obj.userCurrencyFromRaw)
..writeByte(25)
..write(obj.userCurrencyToRaw);
..write(obj.userCurrencyToRaw)
..writeByte(26)
..write(obj.needToRegisterInSwapXyz)
..writeByte(27)
..write(obj.sourceTokenAddress)
..writeByte(28)
..write(obj.sourceTokenDecimals)
..writeByte(29)
..write(obj.routerData)
..writeByte(30)
..write(obj.routerValue)
..writeByte(31)
..write(obj.routerChainId)
..writeByte(32)
..write(obj.sourceTokenAmountRaw)
..writeByte(33)
..write(obj.requiresTokenApproval);
}
@override
+7
View File
@@ -183,6 +183,13 @@ class CWPolygon extends Polygon {
@override
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) =>
(wallet as PolygonWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei,
TransactionPriority priority) =>
(wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority);
@override
Future<PendingTransaction> createTokenApproval(
WalletBase wallet,
@@ -65,15 +65,16 @@ class ExchangeConfirmPage extends BasePage {
fontWeight: FontWeight.w500,
),
),
Text(
trade.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
Expanded(
child: Text(
trade.id,
overflow: TextOverflow.visible,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
@@ -1,3 +1,4 @@
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
@@ -173,6 +174,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
textColor: widget.exchangeTradeViewModel.isSendable
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onPrimary,
isDisabled: widget.exchangeTradeViewModel.isSwapsXyzSendingEVMTokenSwap,
),
SizedBox(height: 16),
Observer(
@@ -265,6 +267,14 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final trade = widget.exchangeTradeViewModel.trade;
final isSwapsXyz = trade.provider == ExchangeProviderDescription.swapsXyz;
final isEVMWallet = widget.exchangeTradeViewModel.sendViewModel.isEVMWallet;
final amountValue = isSwapsXyz && isEVMWallet
? trade.amount
: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted;
if (context.mounted) {
final result = await showModalBottomSheet<bool>(
context: context,
@@ -280,8 +290,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount,
amountValue: widget
.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted,
amountValue: amountValue,
fiatAmountValue: widget
.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee:
@@ -298,6 +307,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
Navigator.of(bottomSheetContext).pop(true);
}
widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context);
widget.exchangeTradeViewModel.registerSwapsXyzTransaction();
},
);
},
+15 -3
View File
@@ -21,7 +21,8 @@ abstract class TradeFilterStoreBase with Store {
displayLetsExchange = true,
displayStealthEx = true,
displayXOSwap = true,
displaySwapTrade = true;
displaySwapTrade = true,
displaySwapXyz = true;
@observable
bool displayXMRTO;
@@ -62,6 +63,9 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displaySwapTrade;
@observable
bool displaySwapXyz;
@computed
bool get displayAllTrades =>
displayChangeNow &&
@@ -74,7 +78,8 @@ abstract class TradeFilterStoreBase with Store {
displayLetsExchange &&
displayStealthEx &&
displayXOSwap &&
displaySwapTrade;
displaySwapTrade &&
displaySwapXyz;
@action
void toggleDisplayExchange(ExchangeProviderDescription provider) {
@@ -118,6 +123,9 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.swapTrade:
displaySwapTrade = !displaySwapTrade;
break;
case ExchangeProviderDescription.swapsXyz:
displaySwapXyz = !displaySwapXyz;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
@@ -133,6 +141,7 @@ abstract class TradeFilterStoreBase with Store {
displayStealthEx = false;
displayXOSwap = false;
displaySwapTrade = false;
displaySwapXyz = false;
} else {
displayChangeNow = true;
displaySideShift = true;
@@ -147,6 +156,7 @@ abstract class TradeFilterStoreBase with Store {
displayStealthEx = true;
displayXOSwap = true;
displaySwapTrade = true;
displaySwapXyz = true;
}
break;
}
@@ -180,7 +190,9 @@ abstract class TradeFilterStoreBase with Store {
item.trade.provider == ExchangeProviderDescription.letsExchange) ||
(displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) ||
(displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) ||
(displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade))
(displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade) ||
(displaySwapXyz &&
item.trade.provider == ExchangeProviderDescription.swapsXyz))
.toList()
: _trades;
}
@@ -181,6 +181,11 @@ abstract class DashboardViewModelBase with Store {
caption: ExchangeProviderDescription.swapTrade.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)),
FilterItem(
value: () => tradeFilterStore.displaySwapXyz,
caption: ExchangeProviderDescription.swapsXyz.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
]
},
subname = '',
@@ -8,6 +8,7 @@ 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';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
@@ -80,6 +81,10 @@ abstract class ExchangeTradeViewModelBase with Store {
break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
break;
case ExchangeProviderDescription.swapsXyz:
_provider = SwapsXyzExchangeProvider();
break;
}
_updateItems();
@@ -104,6 +109,15 @@ abstract class ExchangeTradeViewModelBase with Store {
@observable
bool isSendable;
bool get isSwapsXyzSendingEVMTokenSwap => (_provider is SwapsXyzExchangeProvider) &&
(wallet.type == WalletType.ethereum &&
wallet.currency != trade.from ||
(wallet.type == WalletType.polygon &&
wallet.currency != trade.from) ||
(wallet.type == WalletType.base &&
wallet.currency != trade.from));
String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty
? '\n\n' + S.current.exchange_extra_info
: '';
@@ -189,7 +203,19 @@ abstract class ExchangeTradeViewModelBase with Store {
sendViewModel.selectedCryptoCurrency = selected;
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider, trade: trade);
if (_provider is SwapsXyzExchangeProvider) {
final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? '';
trade.txId = hash;
if (trade.isInBox) {
await trade.save();
} else {
await trades.add(trade);
}
}
if (_provider is ThorChainExchangeProvider) {
trade.id = pendingTransaction?.id ?? '';
trades.add(trade);
@@ -308,7 +334,7 @@ abstract class ExchangeTradeViewModelBase with Store {
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
bool _isBaseToken() =>
wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.title;
wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag;
bool _isTronToken() =>
wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
@@ -324,6 +350,53 @@ abstract class ExchangeTradeViewModelBase with Store {
_isTronToken() ||
_isBaseToken();
}
Future<void> registerSwapsXyzTransaction() async {
try {
if (!(_provider is SwapsXyzExchangeProvider)) return;
final swaps = _provider as SwapsXyzExchangeProvider;
// register only for vmId is alt-vm or bridgeId is alt-vm (trade.needToRegisterInSwapXyz)
final needToRegister = trade.needToRegisterInSwapXyz ?? false;
if (!needToRegister) return;
final vmId = (trade.providerId ?? '').toLowerCase();
if (vmId.isEmpty) {
printV('SwapsXyz: transaction register: skipped (vmId empty)');
return;
}
final txHash = sendViewModel.pendingTransaction?.evmTxHashFromRawHex ?? sendViewModel.pendingTransaction?.id ?? '';
if (txHash.isEmpty) {
printV('SwapsXyz: transaction register: skipped (txHash empty)');
return;
}
final chainId = int.tryParse(trade.router ?? '') ?? 0;
if (chainId <= 0) {
printV('SwapsXyz: transaction register: skipped (invalid chainId)');
return;
}
printV('SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId');
final registered = await swaps.registerAltVmTx(
txId: trade.id,
txHash: txHash,
chainId: chainId,
vmId: vmId,
);
if (!registered) {
printV('SwapsXyz: transaction register: failed');
} else {
printV('SwapsXyz: transaction register: success');
}
} catch (e) {
printV('registerSwapsXyzTransaction error: $e');
}
}
PaymentURI? get paymentUri {
final inputAddress = trade.inputAddress;
@@ -27,6 +27,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
@@ -214,6 +215,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
LetsExchangeExchangeProvider(),
StealthExExchangeProvider(),
XOSwapExchangeProvider(),
SwapsXyzExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
];
@@ -536,7 +538,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
to: receiveCurrency,
amount: amount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: isFixedRateMode,
isReceiveAmount: isFixedRateMode
)
.timeout(
Duration(seconds: 7),
+251 -1
View File
@@ -22,7 +22,9 @@ import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart';
@@ -46,6 +48,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_mod
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/pending_transaction.dart';
@@ -119,6 +122,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
});
}
PendingTransaction? _pendingApprovalTx;
bool _isSwapsXYZCallDataTx = false;
@observable
ExecutionState state;
@@ -133,6 +139,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get isMwebEnabled => balanceViewModel.mwebEnabled;
bool get isEVMWallet => walletType == WalletType.ethereum || walletType == WalletType.polygon ||
walletType == WalletType.base;
@action
void setShowAddressBookPopup(bool value) {
_settingsStore.showAddressBookPopupEnabled = value;
@@ -490,7 +499,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Timer? _ledgerTxStateTimer;
@action
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
try {
if (!(state is IsExecutingState)) state = IsExecutingState();
@@ -505,6 +514,151 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
});
}
// Swaps.xyz (EVM) path
if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) {
final routerTo = trade.inputAddress;
final routerData = trade.routerData;
final routerValueWei =
BigInt.tryParse((trade.routerValue ?? '0').toString()) ?? BigInt.zero;
if (routerTo?.isNotEmpty == true && routerData?.isNotEmpty == true) {
// detect prepared ERC-20 transfer(...) (alt-vm deposit pattern)
String _selector(String s) =>
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
const _transferSig = '0xa9059cbb';
final _sel = _selector(routerData!);
final _isPreparedTransfer = _sel == _transferSig &&
(trade.sourceTokenAddress ?? '').toLowerCase() == (routerTo ?? '').toLowerCase();
_pendingApprovalTx = null;
// Optionally prebuild approval (SKIP for prepared transfer)
final tokenContract = trade.sourceTokenAddress ?? '';
final requiredAmount = BigInt.tryParse(
(trade.sourceTokenAmountRaw ?? '0').replaceAll('n', ''),
) ?? BigInt.zero;
// Only do approval when NOT a prepared transfer, and only if the API hinted we might need it
final requiresTokenApproval = (trade.requiresTokenApproval ?? false) && !_isPreparedTransfer;
if (requiresTokenApproval && tokenContract.isNotEmpty && requiredAmount > BigInt.zero) {
if (walletType == WalletType.ethereum) {
final priority = _settingsStore.priority[WalletType.ethereum]!;
_pendingApprovalTx = await buildApprovalIfNeeded(
spender: routerTo!, // if API provides a specific spender, use that instead
tokenContract: tokenContract,
requiredAmount: requiredAmount,
sourceTokenDecimals: trade.sourceTokenDecimals,
);
// Build the callData tx
pendingTransaction = await ethereum!.createRawCallDataTransaction(
wallet,
routerTo,
routerData,
routerValueWei,
priority,
);
_isSwapsXYZCallDataTx = true;
state = ExecutedSuccessfullyState();
return pendingTransaction; // do NOT fall back to regular flow
}
if (walletType == WalletType.polygon) {
final priority = _settingsStore.priority[WalletType.polygon]!;
_pendingApprovalTx = await buildApprovalIfNeeded(
spender: routerTo!,
tokenContract: tokenContract,
requiredAmount: requiredAmount,
sourceTokenDecimals: trade.sourceTokenDecimals,
);
// Build the callData tx
pendingTransaction = await polygon!.createRawCallDataTransaction(
wallet,
routerTo,
routerData,
routerValueWei,
priority,
);
_isSwapsXYZCallDataTx = true;
state = ExecutedSuccessfullyState();
return pendingTransaction; // do NOT fall back to regular flow
}
if (walletType == WalletType.base) {
final priority = _settingsStore.priority[WalletType.base]!;
_pendingApprovalTx = await buildApprovalIfNeeded(
spender: routerTo!,
tokenContract: tokenContract,
requiredAmount: requiredAmount,
sourceTokenDecimals: trade.sourceTokenDecimals,
);
// Build the callData tx
pendingTransaction = await base!.createRawCallDataTransaction(
wallet,
routerTo,
routerData,
routerValueWei,
priority,
);
_isSwapsXYZCallDataTx = true;
state = ExecutedSuccessfullyState();
return pendingTransaction; // do NOT fall back to regular flow
}
}
// No approval needed (or prepared transfer): send exactly what backend prepared
if (walletType == WalletType.ethereum) {
final priority = _settingsStore.priority[WalletType.ethereum]!;
pendingTransaction = await ethereum!.createRawCallDataTransaction(
wallet,
routerTo!,
routerData,
routerValueWei,
priority,
);
_isSwapsXYZCallDataTx = true;
state = ExecutedSuccessfullyState();
return pendingTransaction;
}
if (walletType == WalletType.polygon) {
final priority = _settingsStore.priority[WalletType.polygon]!;
pendingTransaction = await polygon!.createRawCallDataTransaction(
wallet,
routerTo!,
routerData,
routerValueWei,
priority,
);
_isSwapsXYZCallDataTx = true;
state = ExecutedSuccessfullyState();
return pendingTransaction;
}
if (walletType == WalletType.base) {
final priority = _settingsStore.priority[WalletType.polygon]!;
pendingTransaction = await base!.createRawCallDataTransaction(
wallet,
routerTo!,
routerData,
routerValueWei,
priority,
);
_isSwapsXYZCallDataTx = true;
state = ExecutedSuccessfullyState();
return pendingTransaction;
}
}
}
// Regular flow
pendingTransaction = await wallet.createTransaction(_credentials(provider));
if (provider is ThorChainExchangeProvider) {
@@ -602,6 +756,37 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return;
}
// Swaps.xyz approval (if any), then commit the prebuilt router tx
if (_isSwapsXYZCallDataTx) {
if (_pendingApprovalTx != null) {
await _pendingApprovalTx!.commit();
_pendingApprovalTx = null;
// Small pause to ensure allowance is indexed
await Future.delayed(const Duration(milliseconds: 300));
}
await pendingTransaction!.commit();
_isSwapsXYZCallDataTx = false;
state = TransactionCommitted();
return; // skip the regular flow below
}
// Regular flow (non-Swaps)
if (pendingTransaction!.shouldCommitUR()) {
final urstr = await pendingTransaction!.commitUR();
final result = await Navigator.of(context).pushNamed(
Routes.urqrAnimatedPage,
arguments: urstr,
);
if (result == null) {
state = FailureState("Canceled by user");
return;
}
} else {
await pendingTransaction!.commit();
}
String address = outputs.fold('', (acc, value) {
return value.isParsedAddress
? '$acc${value.address}\n${value.extractedAddress}\n\n'
@@ -933,6 +1118,71 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return false;
}
Future<PendingTransaction?> buildApprovalIfNeeded({
required String spender,
required String tokenContract,
required BigInt requiredAmount,
int? sourceTokenDecimals,
}) async {
// Only EVM chains support ERC20 approvals
if (!isEVMWallet) return null;
const zero = '0x0000000000000000000000000000000000000000';
const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
final tokenLc = tokenContract.toLowerCase();
if (tokenLc == zero || tokenLc == evmNative.toLowerCase()) return null;
if (requiredAmount <= BigInt.zero) return null;
bool needsApproval = false;
if (walletType == WalletType.ethereum) {
needsApproval = await ethereum!.isApprovalRequired(
wallet, tokenContract, spender, requiredAmount,
);
} else if (walletType == WalletType.polygon) {
needsApproval = await polygon!.isApprovalRequired(
wallet, tokenContract, spender, requiredAmount,
);
} else if (walletType == WalletType.base) {
needsApproval = await base!.isApprovalRequired(
wallet, tokenContract, spender, requiredAmount,
);
}
if (!needsApproval) return null;
final erc20Token = wallet.balance.keys.whereType<Erc20Token>().firstWhere(
(t) => t.contractAddress.toLowerCase() == tokenLc,
orElse: () => Erc20Token(
name: '',
symbol: '',
contractAddress: tokenContract,
decimal: sourceTokenDecimals ?? 18,
enabled: true,
),
);
if (walletType == WalletType.ethereum) {
final priority = _settingsStore.priority[WalletType.ethereum]!;
return await ethereum!.createTokenApproval(
wallet, requiredAmount, spender, erc20Token, priority,
);
} else if (walletType == WalletType.polygon) {
final priority = _settingsStore.priority[WalletType.polygon]!;
return await polygon!.createTokenApproval(
wallet, requiredAmount, spender, erc20Token, priority,
);
} else if (walletType == WalletType.base) {
final priority = _settingsStore.priority[WalletType.base]!;
return await base!.createTokenApproval(
wallet, requiredAmount, spender, erc20Token, priority,
);
}
return null;
}
@computed
bool get usePayjoin => _settingsStore.usePayjoin;
+15 -1
View File
@@ -6,6 +6,7 @@ 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';
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
@@ -77,6 +78,9 @@ abstract class TradeDetailsViewModelBase with Store {
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
break;
case ExchangeProviderDescription.swapsXyz:
_provider = SwapsXyzExchangeProvider();
break;
}
_updateItems();
@@ -134,8 +138,13 @@ abstract class TradeDetailsViewModelBase with Store {
try {
final updatedTrade = await _provider!.findTradeById(id: trade.id);
if (updatedTrade.createdAt == null && trade.createdAt != null)
if (updatedTrade.createdAt == null && trade.createdAt != null) {
updatedTrade.createdAt = trade.createdAt;
}
if (updatedTrade.toRaw == -1 && trade.toRaw != -1) {
updatedTrade.toRaw = trade.toRaw;
}
Trade? foundElement = trades.values.firstWhereOrNull((element) => element.id == trade.id);
if (foundElement != null) {
@@ -205,6 +214,11 @@ abstract class TradeDetailsViewModelBase with Store {
title: '${trade.providerName} ${S.current.password}', value: trade.password ?? ''));
}
}
if (trade.provider == ExchangeProviderDescription.swapsXyz && trade.txId != null && trade.txId!.isNotEmpty) {
items.add(StandartListItem(
title: 'Transaction ID', value: trade.txId!));
}
}
void _launchUrl(String url) {
+6
View File
@@ -776,7 +776,9 @@ abstract class Ethereum {
Web3Client? getWeb3Client(WalletBase wallet);
String getTokenAddress(CryptoCurrency asset);
Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount);
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority);
Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, TransactionPriority priority);
Future<BigInt> getDEuroSavingsBalance(WalletBase wallet);
Future<BigInt> getDEuroAccruedInterest(WalletBase wallet);
@@ -899,7 +901,9 @@ abstract class Polygon {
Future<void> removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token);
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress);
Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount);
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority);
Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, TransactionPriority priority);
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled);
@@ -1595,8 +1599,10 @@ abstract class Base {
Future<void> removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token);
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress);
Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount);
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender,
CryptoCurrency token, TransactionPriority priority);
Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, TransactionPriority priority);
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
void updateBaseScanUsageState(WalletBase wallet, bool isEnabled);
+1
View File
@@ -82,6 +82,7 @@ class SecretKey {
SecretKey('chainflipAffiliateFee', () => ''),
SecretKey('kryptonimApiKey', () => ''),
SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)),
SecretKey('swapsXyzApiKey', () => ''),
];
static final evmChainsSecrets = [