mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
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:
@@ -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: |
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ mixin PendingTransaction {
|
||||
String get feeFormattedValue;
|
||||
String? feeRate;
|
||||
String get hex;
|
||||
String? get evmTxHashFromRawHex => null;
|
||||
int? get outputCount => null;
|
||||
PendingChange? change;
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ abstract class EVMChainClient {
|
||||
sender: senderAddress,
|
||||
to: toAddress,
|
||||
value: value,
|
||||
data: data,
|
||||
);
|
||||
|
||||
return estimatedGas.toInt();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -82,6 +82,7 @@ class SecretKey {
|
||||
SecretKey('chainflipAffiliateFee', () => ''),
|
||||
SecretKey('kryptonimApiKey', () => ''),
|
||||
SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)),
|
||||
SecretKey('swapsXyzApiKey', () => ''),
|
||||
];
|
||||
|
||||
static final evmChainsSecrets = [
|
||||
|
||||
Reference in New Issue
Block a user