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>
Serhii committed
Oct 21, 2025 at 00:22 UTC
ee0996f535ba31fc07850db2a6a4090739ba1490
37 files changed
+1557
-77
.github/workflows/automated_integration_test.yml
+1
@@ -190,6 +190,7 @@ jobs:
190
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
191
echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
192
echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
193
+ echo "const swapsXyzApiKey = '${{ secrets.SWAPSXYZ_API_KEY }}';" >> lib/.secrets.g.dart
194
195
- name: prepare monero_c and cache
196
run: |
.github/workflows/pr_test_build_android.yml
+1
@@ -180,6 +180,7 @@ jobs:
180
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
181
echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
182
echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
183
+ echo "const swapsXyzApiKey = '${{ secrets.SWAPSXYZ_API_KEY }}';" >> lib/.secrets.g.dart
184
185
- name: Generate KeyStore
186
run: |
.github/workflows/pr_test_build_linux.yml
+1
@@ -176,6 +176,7 @@ jobs:
176
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
177
echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
178
echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
179
+ echo "const swapsXyzApiKey = '${{ secrets.SWAPSXYZ_API_KEY }}';" >> lib/.secrets.g.dart
180
181
- name: Fetch prebuilt Torch
182
run: |
assets/images/swaps_xyz.svg
new
+42
@@ -0,0 +1,42 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.468 0.614 498.759 500.158" width="500
3
+px" height="500px">
4
+ <defs>
5
+ <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)">
6
+ <stop stop-color="#D5A2FF"/>
7
+ <stop offset="0.5" stop-color="#4B68B3"/>
8
+ <stop offset="1" stop-color="#30001A"/>
9
+ </linearGradient>
10
+ <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)">
11
+ <stop stop-color="#D5A2FF"/>
12
+ <stop offset="0.5" stop-color="#4B68B3"/>
13
+ <stop offset="1" stop-color="#30001A"/>
14
+ </linearGradient>
15
+ <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)">
16
+ <stop stop-color="#30001A"/>
17
+ <stop offset="0.5" stop-color="#4B68B3"/>
18
+ <stop offset="1" stop-color="#D5A2FF"/>
19
+ </linearGradient>
20
+ <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)">
21
+ <stop stop-color="#30001A"/>
22
+ <stop offset="0.5" stop-color="#4B68B3"/>
23
+ <stop offset="1" stop-color="#D5A2FF"/>
24
+ </linearGradient>
25
+ <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)">
26
+ <stop stop-color="#D5A2FF"/>
27
+ <stop offset="0.5" stop-color="#4B68B3"/>
28
+ <stop offset="1" stop-color="#30001A"/>
29
+ </linearGradient>
30
+ <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)">
31
+ <stop stop-color="#D5A2FF"/>
32
+ <stop offset="0.5" stop-color="#4B68B3"/>
33
+ <stop offset="1" stop-color="#30001A"/>
34
+ </linearGradient>
35
+ </defs>
36
+ <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)"/>
37
+ <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)"/>
38
+ <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)"/>
39
+ <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)"/>
40
+ <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)"/>
41
+ <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)"/>
42
+</svg>
\ No newline at end of file
cw_core/lib/amount_converter.dart
+50
@@ -71,4 +71,54 @@ class AmountConverter {
71
72
static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat.format(
73
DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider)));
74
+
75
+
76
+ static String toBaseUnits(String amount, int decimals) {
77
+ amount = amount.trim();
78
+ if (amount.isEmpty) return '0';
79
+
80
+ final neg = amount.startsWith('-');
81
+ if (neg) amount = amount.substring(1);
82
+
83
+ amount = amount.replaceAll(',', '');
84
+ final parts = amount.split('.');
85
+ final whole = parts[0].isEmpty ? '0' : parts[0];
86
+ final frac = parts.length > 1 ? parts[1] : '';
87
+
88
+ final fracPadded = (frac + '0' * decimals).substring(0, decimals);
89
+
90
+ final pow10 = BigInt.from(10).pow(decimals);
91
+ final wholeBI = BigInt.parse(whole);
92
+ final fracBI = fracPadded.isEmpty ? BigInt.zero : BigInt.parse(fracPadded);
93
+
94
+ final res = wholeBI * pow10 + fracBI;
95
+ return neg ? '-${res.toString()}' : res.toString();
96
+ }
97
+
98
+
99
+ static String fromBaseUnits(String units, int decimals) {
100
+ units = units.trim();
101
+ if (units.isEmpty) return '0';
102
+
103
+ final neg = units.startsWith('-');
104
+ if (neg) units = units.substring(1);
105
+
106
+ if (decimals == 0) return neg ? '-$units' : units;
107
+
108
+ // pad if shorter than decimals
109
+ if (units.length <= decimals) {
110
+ final s = units.padLeft(decimals + 1, '0'); // ensures at least "0xxxx"
111
+ final intPart = s.substring(0, s.length - decimals);
112
+ var fracPart = s.substring(s.length - decimals);
113
+ fracPart = fracPart.replaceFirst(RegExp(r'0+$'), '');
114
+ final out = fracPart.isEmpty ? intPart : '$intPart.$fracPart';
115
+ return neg ? '-$out' : out;
116
+ }
117
+
118
+ final intPart = units.substring(0, units.length - decimals);
119
+ var fracPart = units.substring(units.length - decimals);
120
+ fracPart = fracPart.replaceFirst(RegExp(r'0+$'), '');
121
+ final out = fracPart.isEmpty ? intPart : '$intPart.$fracPart';
122
+ return neg ? '-$out' : out;
123
+ }
124
}
cw_core/lib/pending_transaction.dart
+1
@@ -12,6 +12,7 @@ mixin PendingTransaction {
12
String get feeFormattedValue;
13
String? feeRate;
14
String get hex;
15
+ String? get evmTxHashFromRawHex => null;
16
int? get outputCount => null;
17
PendingChange? change;
18
cw_evm/lib/evm_chain_client.dart
+1
@@ -111,6 +111,7 @@ abstract class EVMChainClient {
111
sender: senderAddress,
112
to: toAddress,
113
value: value,
114
+ data: data,
115
);
116
117
return estimatedGas.toInt();
cw_evm/lib/evm_chain_wallet.dart
+76
@@ -40,6 +40,7 @@ import 'package:web3dart/crypto.dart';
40
import 'package:web3dart/web3dart.dart';
41
import 'package:eth_sig_util/eth_sig_util.dart';
42
43
+import 'contract/erc20.dart';
44
import 'evm_chain_transaction_info.dart';
45
import 'evm_erc20_balance.dart';
46
@@ -611,6 +612,50 @@ abstract class EVMChainWalletBase
612
return pendingEVMChainTransaction;
613
}
614
615
+ Future<PendingTransaction> createCallDataTransaction(
616
+ String to,
617
+ String dataHex,
618
+ BigInt valueWei,
619
+ EVMChainTransactionPriority priority,
620
+ ) async {
621
+
622
+ // Estimate gas with the SAME call (sender, to, value, data)
623
+ final gas = await calculateActualEstimatedFeeForCreateTransaction(
624
+ amount: valueWei, // native value (usually 0 for ERC20 transfer)
625
+ receivingAddressHex: to,
626
+ priority: priority,
627
+ contractAddress: null,
628
+ data: _client.hexToBytes(dataHex),
629
+ );
630
+
631
+ final nativeCurrency = switch (_client.chainId) {
632
+ 137 => CryptoCurrency.maticpoly,
633
+ 8453 => CryptoCurrency.baseEth,
634
+ _ => CryptoCurrency.eth,
635
+ };
636
+
637
+ // Fallback for nodes that fail estimate (non-zero)
638
+ final gasUnits = gas.estimatedGasUnits == 0 ? 65000 : gas.estimatedGasUnits;
639
+
640
+ // Sign raw (native) tx with callData
641
+ return _client.signTransaction(
642
+ privateKey: _evmChainPrivateKey,
643
+ toAddress: to,
644
+ amount: valueWei,
645
+ gasFee: BigInt.from(gas.estimatedGasFee),
646
+ estimatedGasUnits: gasUnits,
647
+ maxFeePerGas: gas.maxFeePerGas,
648
+ priority: priority,
649
+ currency: nativeCurrency,
650
+ feeCurrency: nativeCurrency.title,
651
+ exponent: 18,
652
+ contractAddress: null,
653
+ data: dataHex,
654
+ gasPrice: gas.gasPrice,
655
+ );
656
+ }
657
+
658
+
659
Future<PendingTransaction> createApprovalTransaction(BigInt amount, String spender,
660
CryptoCurrency token, EVMChainTransactionPriority priority, String feeCurrency) async {
661
final CryptoCurrency transactionCurrency =
@@ -793,6 +838,37 @@ abstract class EVMChainWalletBase
838
}
839
}
840
841
+ Future<bool> isApprovalRequired(
842
+ String tokenContract,
843
+ String spender,
844
+ BigInt requiredAmount) async {
845
+
846
+ const zero = '0x0000000000000000000000000000000000000000';
847
+ const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
848
+
849
+ final token = tokenContract.toLowerCase();
850
+ if (token == zero || token == evmNative.toLowerCase()) return false;
851
+ if (requiredAmount <= BigInt.zero) return false;
852
+
853
+ try {
854
+ final owner = _evmChainPrivateKey.address;
855
+ final erc20 = ERC20(
856
+ client: _client.getWeb3Client()!,
857
+ address: EthereumAddress.fromHex(tokenContract),
858
+ chainId: _client.chainId,
859
+ );
860
+
861
+ final allowance = await erc20.allowance(
862
+ owner,
863
+ EthereumAddress.fromHex(spender));
864
+
865
+ return allowance < requiredAmount;
866
+ } catch (e) {
867
+ printV('approval-check error: $e');
868
+ return true;
869
+ }
870
+ }
871
+
872
Future<EthPrivateKey> getPrivateKey({
873
String? mnemonic,
874
String? privateKey,
cw_evm/lib/pending_evm_chain_transaction.dart
+9
-1
@@ -51,7 +51,15 @@ class PendingEVMChainTransaction with PendingTransaction {
51
52
return '0x${Hex.HEX.encode(txid)}';
53
}
54
-
54
+
55
+ @override
56
+ String? get evmTxHashFromRawHex {
57
+ final no0x = hex.startsWith('0x') ? hex.substring(2) : hex;
58
+ final bytes = Uint8List.fromList(Hex.HEX.decode(no0x));
59
+ final digest = keccak256(bytes);
60
+ return '0x${Hex.HEX.encode(digest)}';
61
+ }
62
+
63
@override
64
Future<Map<String, String>> commitUR() {
65
throw UnimplementedError();
lib/base/cw_base.dart
+10
@@ -189,6 +189,10 @@ class CWBase extends Base {
189
@override
190
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
191
192
+ @override
193
+ Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) =>
194
+ (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
195
+
196
@override
197
Future<PendingTransaction> createTokenApproval(
198
WalletBase wallet,
@@ -205,6 +209,12 @@ class CWBase extends Base {
209
"BASE",
210
);
211
212
+ @override
213
+ Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei,
214
+ TransactionPriority priority) =>
215
+ (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority);
216
+
217
+
218
@override
219
void setHardwareWalletService(WalletBase wallet, HardwareWalletService service) {
220
if (service is EVMChainLedgerService) {
lib/core/trade_monitor.dart
+3
@@ -1,5 +1,6 @@
1
import 'dart:async';
2
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
3
+import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
4
import 'package:cake_wallet/exchange/trade.dart';
5
import 'package:cake_wallet/exchange/trade_state.dart';
6
import 'package:cake_wallet/store/dashboard/trades_store.dart';
@@ -62,6 +63,8 @@ class TradeMonitor {
63
return ChainflipExchangeProvider(tradesStore: trades);
64
case ExchangeProviderDescription.xoSwap:
65
return XOSwapExchangeProvider();
66
+ case ExchangeProviderDescription.swapsXyz:
67
+ return SwapsXyzExchangeProvider();
68
}
69
return null;
70
}
lib/ethereum/cw_ethereum.dart
+10
@@ -217,11 +217,21 @@ class CWEthereum extends Ethereum {
217
return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase());
218
}
219
220
+ @override
221
+ Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) =>
222
+ (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
223
+
224
+ @override
225
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender,
226
CryptoCurrency token, TransactionPriority priority) =>
227
(wallet as EVMChainWallet).createApprovalTransaction(
228
amount, spender, token, priority as EVMChainTransactionPriority, "ETH");
229
230
+ @override
231
+ Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei,
232
+ TransactionPriority priority) =>
233
+ (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority);
234
+
235
// Integrations
236
@override
237
Future<BigInt> getDEuroSavingsBalance(WalletBase wallet) =>
lib/exchange/exchange_provider_description.dart
+4
@@ -36,6 +36,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
36
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png');
37
static const xoSwap =
38
ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg');
39
+ static const swapsXyz =
40
+ ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg');
41
42
static ExchangeProviderDescription deserialize({required int raw}) {
43
switch (raw) {
@@ -67,6 +69,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
69
return chainflip;
70
case 13:
71
return xoSwap;
72
+ case 14:
73
+ return swapsXyz;
74
default:
75
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
76
}
lib/exchange/provider/chainflip_exchange_provider.dart
+7
-6
@@ -91,12 +91,13 @@ class ChainflipExchangeProvider extends ExchangeProvider {
91
}
92
93
@override
94
- Future<double> fetchRate(
95
- {required CryptoCurrency from,
96
- required CryptoCurrency to,
97
- required double amount,
98
- required bool isFixedRateMode,
99
- required bool isReceiveAmount}) async {
94
+ Future<double> fetchRate({
95
+ required CryptoCurrency from,
96
+ required CryptoCurrency to,
97
+ required double amount,
98
+ required bool isFixedRateMode,
99
+ required bool isReceiveAmount
100
+ }) async {
101
// TODO: It seems this rate is getting cached, and re-used for different amounts, can we not do this?
102
103
try {
lib/exchange/provider/changenow_exchange_provider.dart
+7
-6
@@ -91,12 +91,13 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
91
}
92
93
@override
94
- Future<double> fetchRate(
95
- {required CryptoCurrency from,
96
- required CryptoCurrency to,
97
- required double amount,
98
- required bool isFixedRateMode,
99
- required bool isReceiveAmount}) async {
94
+ Future<double> fetchRate({
95
+ required CryptoCurrency from,
96
+ required CryptoCurrency to,
97
+ required double amount,
98
+ required bool isFixedRateMode,
99
+ required bool isReceiveAmount
100
+ }) async {
101
try {
102
if (amount == 0) return 0.0;
103
lib/exchange/provider/exchange_provider.dart
+7
-6
@@ -33,12 +33,13 @@ abstract class ExchangeProvider {
33
34
Future<Trade> findTradeById({required String id});
35
36
- Future<double> fetchRate(
37
- {required CryptoCurrency from,
38
- required CryptoCurrency to,
39
- required double amount,
40
- required bool isFixedRateMode,
41
- required bool isReceiveAmount});
36
+ Future<double> fetchRate({
37
+ required CryptoCurrency from,
38
+ required CryptoCurrency to,
39
+ required double amount,
40
+ required bool isFixedRateMode,
41
+ required bool isReceiveAmount
42
+ });
43
44
Future<bool> checkIsAvailable();
45
}
lib/exchange/provider/exolix_exchange_provider.dart
+7
-6
@@ -112,12 +112,13 @@ class ExolixExchangeProvider extends ExchangeProvider {
112
}
113
114
@override
115
- Future<double> fetchRate(
116
- {required CryptoCurrency from,
117
- required CryptoCurrency to,
118
- required double amount,
119
- required bool isFixedRateMode,
120
- required bool isReceiveAmount}) async {
115
+ Future<double> fetchRate({
116
+ required CryptoCurrency from,
117
+ required CryptoCurrency to,
118
+ required double amount,
119
+ required bool isFixedRateMode,
120
+ required bool isReceiveAmount
121
+ }) async {
122
try {
123
if (amount == 0) return 0.0;
124
lib/exchange/provider/letsexchange_exchange_provider.dart
+7
-6
@@ -78,12 +78,13 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
78
}
79
80
@override
81
- Future<double> fetchRate(
82
- {required CryptoCurrency from,
83
- required CryptoCurrency to,
84
- required double amount,
85
- required bool isFixedRateMode,
86
- required bool isReceiveAmount}) async {
81
+ Future<double> fetchRate({
82
+ required CryptoCurrency from,
83
+ required CryptoCurrency to,
84
+ required double amount,
85
+ required bool isFixedRateMode,
86
+ required bool isReceiveAmount
87
+ }) async {
88
final networkFrom = _getNetworkType(from);
89
final networkTo = _getNetworkType(to);
90
try {
lib/exchange/provider/sideshift_exchange_provider.dart
+7
-6
@@ -120,12 +120,13 @@ class SideShiftExchangeProvider extends ExchangeProvider {
120
}
121
122
@override
123
- Future<double> fetchRate(
124
- {required CryptoCurrency from,
125
- required CryptoCurrency to,
126
- required double amount,
127
- required bool isFixedRateMode,
128
- required bool isReceiveAmount}) async {
123
+ Future<double> fetchRate({
124
+ required CryptoCurrency from,
125
+ required CryptoCurrency to,
126
+ required double amount,
127
+ required bool isFixedRateMode,
128
+ required bool isReceiveAmount
129
+ }) async {
130
try {
131
if (amount == 0) return 0.0;
132
lib/exchange/provider/simpleswap_exchange_provider.dart
+7
-6
@@ -89,12 +89,13 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
89
}
90
91
@override
92
- Future<double> fetchRate(
93
- {required CryptoCurrency from,
94
- required CryptoCurrency to,
95
- required double amount,
96
- required bool isFixedRateMode,
97
- required bool isReceiveAmount}) async {
92
+ Future<double> fetchRate({
93
+ required CryptoCurrency from,
94
+ required CryptoCurrency to,
95
+ required double amount,
96
+ required bool isFixedRateMode,
97
+ required bool isReceiveAmount
98
+ }) async {
99
try {
100
if (amount == 0) return 0.0;
101
lib/exchange/provider/swapsxyz_exchange_provider.dart
new
+823
@@ -0,0 +1,823 @@
1
+import 'dart:convert';
2
+
3
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
4
+import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
5
+import 'package:cake_wallet/exchange/exchange_provider_description.dart';
6
+import 'package:cake_wallet/exchange/limits.dart';
7
+import 'package:cake_wallet/exchange/trade.dart';
8
+import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
9
+import 'package:cake_wallet/exchange/trade_request.dart';
10
+import 'package:cake_wallet/exchange/trade_state.dart';
11
+import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
12
+import 'package:cw_core/amount_converter.dart';
13
+import 'package:cw_core/utils/print_verbose.dart';
14
+import 'package:cw_core/utils/proxy_wrapper.dart';
15
+import 'package:cw_core/crypto_currency.dart';
16
+
17
+class SwapsXyzExchangeProvider extends ExchangeProvider {
18
+ SwapsXyzExchangeProvider() : super(pairList: supportedPairs(_notSupported));
19
+
20
+ static const List<CryptoCurrency> _notSupported = [];
21
+
22
+ static final _apiKey = secrets.swapsXyzApiKey;
23
+ static const _baseUrl = 'api-v2.swaps.xyz';
24
+ static const _getChainList = 'api/getChainList';
25
+ static const _getPaths = 'api/getPaths';
26
+ static const _getQuotePaths = 'api/getQuote';
27
+ static const _getAction = 'api/getAction';
28
+ static const _registerTxs = 'api/registerTxs';
29
+ static const _getStatus = 'api/getStatus';
30
+
31
+ static final _headers = {'x-api-key': _apiKey};
32
+
33
+ static final _supportedChainList = <Chain>[];
34
+ final Map<int, List<TokenPathInfo>> _tokensCache = {};
35
+
36
+ @override
37
+ String get title => 'Swaps.XYZ';
38
+
39
+ @override
40
+ bool get isAvailable => true;
41
+
42
+ @override
43
+ bool get isEnabled => true;
44
+
45
+ @override
46
+ bool get supportsFixedRate => true;
47
+
48
+ @override
49
+ ExchangeProviderDescription get description =>
50
+ ExchangeProviderDescription.swapsXyz;
51
+
52
+ @override
53
+ Future<bool> checkIsAvailable() async => true;
54
+
55
+ @override
56
+ Future<Limits> fetchLimits({
57
+ required CryptoCurrency from,
58
+ required CryptoCurrency to,
59
+ required bool isFixedRateMode,
60
+ }) async {
61
+ try {
62
+ final chains = await _geSupportedChain();
63
+ if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
64
+
65
+ final srcChain = _findChainByCurrency(from, chains);
66
+ final dstChain = _findChainByCurrency(to, chains);
67
+
68
+ await _ensureTokensCached(
69
+ fromChain: srcChain, toChain: dstChain, from: from, to: to);
70
+
71
+ final srcToken = _getTokenAddress(currency: from, chain: srcChain);
72
+ final dstToken = _getTokenAddress(currency: to, chain: dstChain);
73
+
74
+ // parameters to and from are swapped in this endpoint
75
+ // i.e. to get limits for FROM -> TO, we request paths for TO -> FROM
76
+ final params = {
77
+ 'srcChainId': '${dstChain.chainId}',
78
+ 'srcToken': dstToken,
79
+ 'dstChainId': '${srcChain.chainId}',
80
+ 'dstToken': srcToken,
81
+ };
82
+
83
+ final uri = Uri.https(_baseUrl, _getPaths, params);
84
+ final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
85
+ if (res.statusCode != 200) {
86
+ throw Exception('Unexpected http status: ${res.statusCode}');
87
+ }
88
+
89
+ final body = json.decode(res.body) as Map<String, dynamic>;
90
+
91
+ final paths = (body['paths'] as List?) ?? const [];
92
+ if (paths.isEmpty) {
93
+ throw Exception('No paths for ${from.title} -> ${to.title}');
94
+ }
95
+
96
+ final path0 = paths.first as Map<String, dynamic>;
97
+ final tokens = path0['tokens'];
98
+
99
+ // If tokens == "all", use path-level amountLimits
100
+ if (tokens is String && tokens == 'all') {
101
+ final amountLimits = path0['amountLimits'] as Map<String, dynamic>?;
102
+
103
+ final min =
104
+ double.tryParse(amountLimits?['minAmount']?.toString() ?? '');
105
+ final max =
106
+ double.tryParse(amountLimits?['maxAmount']?.toString() ?? '');
107
+
108
+ return Limits(min: min, max: max);
109
+ }
110
+
111
+ // Otherwise tokens is a list -> find the specific token entry
112
+ final tokenList = (tokens as List?) ?? const [];
113
+ final token = findTokenBySymbol(title: from.title, tokens: tokenList);
114
+
115
+ if (token == null) {
116
+ throw Exception('Token info not found for ${from.title}');
117
+ }
118
+
119
+ final min = double.tryParse(token['minAmount']?.toString() ?? '');
120
+ final max = double.tryParse(token['maxAmount']?.toString() ?? '');
121
+
122
+ return Limits(min: min, max: max);
123
+ } catch (e) {
124
+ printV('fetchLimits error: $e');
125
+ throw Exception('Error fetching limits: $e');
126
+ }
127
+ }
128
+
129
+ Future<_PathInfo?> _pickPath({
130
+ required int srcChainId,
131
+ required String srcToken,
132
+ required int dstChainId,
133
+ required String dstToken,
134
+ }) async {
135
+ final uri = Uri.https(_baseUrl, _getPaths, {
136
+ 'srcChainId': '$srcChainId',
137
+ 'srcToken': srcToken,
138
+ 'dstChainId': '$dstChainId',
139
+ 'dstToken': dstToken,
140
+ });
141
+ final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
142
+ if (res.statusCode != 200) return null;
143
+ final body = json.decode(res.body) as Map<String, dynamic>;
144
+ final paths =
145
+ (body['paths'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
146
+ if (paths.isEmpty) return null;
147
+ final p = paths.first;
148
+ return _PathInfo(
149
+ supportsExactOut: p['supportsExactAmountOut'] == true,
150
+ minToAmountHuman: (p['amountLimits']?['minAmount'] as String?) ?? '0',
151
+ );
152
+ }
153
+
154
+ @override
155
+ Future<double> fetchRate(
156
+ {required CryptoCurrency from,
157
+ required CryptoCurrency to,
158
+ required double amount,
159
+ required bool isFixedRateMode,
160
+ required bool isReceiveAmount}) async {
161
+ try {
162
+ final chains = await _geSupportedChain();
163
+ if (chains.isEmpty) return 0.0;
164
+
165
+ final srcChain = _findChainByCurrency(from, chains);
166
+ final dstChain = _findChainByCurrency(to, chains);
167
+
168
+ await _ensureTokensCached(
169
+ fromChain: srcChain, toChain: dstChain, from: from, to: to);
170
+
171
+ final srcToken = _getTokenAddress(currency: from, chain: srcChain);
172
+ final dstToken = _getTokenAddress(currency: to, chain: dstChain);
173
+
174
+ if (isReceiveAmount) {
175
+ final path = await _pickPath(
176
+ srcChainId: srcChain.chainId,
177
+ srcToken: srcToken,
178
+ dstChainId: dstChain.chainId,
179
+ dstToken: dstToken,
180
+ );
181
+ if (path == null || !path.supportsExactOut) {
182
+ printV(
183
+ 'fetchRate: route does not support exact-amount-out for ${from.title} -> ${to.title}');
184
+ return 0.0;
185
+ }
186
+ }
187
+
188
+ final humanAmountStr = amount.toString();
189
+ final formattedAmount = AmountConverter.toBaseUnits(
190
+ humanAmountStr,
191
+ isReceiveAmount ? to.decimals : from.decimals,
192
+ );
193
+
194
+ final params = {
195
+ 'swapDirection':
196
+ isReceiveAmount ? 'exact-amount-out' : 'exact-amount-in',
197
+ 'srcToken': srcToken,
198
+ 'dstToken': dstToken,
199
+ 'srcChainId': '${srcChain.chainId}',
200
+ 'dstChainId': '${dstChain.chainId}',
201
+ 'amount': formattedAmount,
202
+ };
203
+
204
+ final uri = Uri.https(_baseUrl, _getQuotePaths, params);
205
+ final response =
206
+ await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
207
+
208
+ if (response.statusCode != 200) {
209
+ printV('fetchRate failed: ${response.body}');
210
+ return 0.0;
211
+ }
212
+
213
+ final data = json.decode(response.body) as Map<String, dynamic>;
214
+ final exchangeRate = (data['exchangeRate'] as num?)?.toDouble() ?? 0.0;
215
+ return exchangeRate;
216
+ } catch (e) {
217
+ printV('fetchRate error: $e');
218
+ return 0.0;
219
+ }
220
+ }
221
+
222
+ @override
223
+ Future<Trade> createTrade({
224
+ required TradeRequest request,
225
+ required bool isFixedRateMode,
226
+ required bool isSendAll,
227
+ }) async {
228
+ try {
229
+ final sender = request.refundAddress.trim();
230
+ final recipient = request.toAddress.trim();
231
+ if (sender.isEmpty || recipient.isEmpty) {
232
+ throw Exception(
233
+ 'Sender (refundAddress) or recipient (toAddress) is empty');
234
+ }
235
+
236
+ final chains = await _geSupportedChain();
237
+ if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
238
+ final srcChain = _findChainByCurrency(request.fromCurrency, chains);
239
+ final dstChain = _findChainByCurrency(request.toCurrency, chains);
240
+
241
+ await _ensureTokensCached(
242
+ fromChain: srcChain,
243
+ toChain: dstChain,
244
+ from: request.fromCurrency,
245
+ to: request.toCurrency,
246
+ );
247
+
248
+ final srcToken =
249
+ _getTokenAddress(currency: request.fromCurrency, chain: srcChain);
250
+ final dstToken =
251
+ _getTokenAddress(currency: request.toCurrency, chain: dstChain);
252
+
253
+ // Optional: ensure path supports exact-out before attempting fixed rate.
254
+ if (isFixedRateMode) {
255
+ final path = await _pickPath(
256
+ srcChainId: srcChain.chainId,
257
+ srcToken: srcToken,
258
+ dstChainId: dstChain.chainId,
259
+ dstToken: dstToken,
260
+ );
261
+ if (path == null || !path.supportsExactOut) {
262
+ throw Exception(
263
+ 'This route does not support fixed receive (exact-amount-out)');
264
+ }
265
+ }
266
+
267
+ final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount;
268
+ final rawAmount = double.tryParse(amountStr) ?? 0.0;
269
+ if (rawAmount <= 0) throw Exception('Invalid amount');
270
+
271
+ final formattedAmount = AmountConverter.toBaseUnits(
272
+ amountStr,
273
+ isFixedRateMode
274
+ ? request.toCurrency.decimals
275
+ : request.fromCurrency.decimals,
276
+ );
277
+
278
+ final params = {
279
+ 'actionType': 'swap-action',
280
+ 'sender': sender,
281
+ 'srcChainId': '${srcChain.chainId}',
282
+ 'srcToken': srcToken,
283
+ 'dstChainId': '${dstChain.chainId}',
284
+ 'dstToken': dstToken,
285
+ 'slippage': '100',
286
+ 'swapDirection':
287
+ isFixedRateMode ? 'exact-amount-out' : 'exact-amount-in',
288
+ 'amount': formattedAmount,
289
+ 'recipient': recipient,
290
+ };
291
+
292
+ final uri = Uri.https(_baseUrl, _getAction, params);
293
+ final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
294
+
295
+ if (res.statusCode != 200) {
296
+ throw Exception('getAction failed: ${res.statusCode} ${res.body}');
297
+ }
298
+
299
+ final data = json.decode(res.body) as Map<String, dynamic>;
300
+
301
+ final txId = data['txId'] as String? ?? '';
302
+ final vmId = data['vmId'] as String? ?? '';
303
+ final txObj = (data['tx'] as Map?) ?? const {};
304
+
305
+ final txTo = txObj['to']?.toString();
306
+ final chainId = txObj['chainId']?.toString();
307
+ final routerData = txObj['data']?.toString();
308
+ final txValue = txObj['value']?.toString() ?? '0';
309
+
310
+ final bridgeIds = (data['bridgeIds'] as List?) ?? const [];
311
+ if (txId.isEmpty) throw Exception('No txId returned by getAction');
312
+
313
+ final amtIn = (data['amountIn'] as Map?) ?? const {};
314
+ final amtInMax = (data['amountInMax'] as Map?) ?? const {};
315
+ final srcTokenAddr = amtIn['address']?.toString();
316
+ final srcTokenDecs =
317
+ (amtIn['decimals'] as num?)?.toInt() ?? request.fromCurrency.decimals;
318
+ final requiresTokenApproval =
319
+ data['requiresTokenApproval'] as bool? ?? false;
320
+
321
+ final reqAmountStr =
322
+ (amtInMax['amount'] ?? amtIn['amount'])?.toString() ?? '0';
323
+ final reqAmountRaw = reqAmountStr.replaceAll('n', '');
324
+
325
+ final needToRegisterInSwapXyz =
326
+ vmId == 'alt-vm' || bridgeIds.contains('alt-vm');
327
+
328
+ final trade = Trade(
329
+ id: txId,
330
+ router: chainId,
331
+ providerId: vmId,
332
+ from: request.fromCurrency,
333
+ to: request.toCurrency,
334
+ provider: description,
335
+ inputAddress: txTo,
336
+ refundAddress: request.refundAddress,
337
+ state: TradeState.created,
338
+ providerName: title,
339
+ createdAt: DateTime.now(),
340
+ amount: request.fromAmount,
341
+ receiveAmount: request.toAmount,
342
+ payoutAddress: request.toAddress,
343
+ isSendAll: isSendAll,
344
+ needToRegisterInSwapXyz: needToRegisterInSwapXyz,
345
+ sourceTokenAddress: srcTokenAddr ?? srcToken,
346
+ sourceTokenDecimals: srcTokenDecs,
347
+ sourceTokenAmountRaw: reqAmountRaw,
348
+ requiresTokenApproval: requiresTokenApproval,
349
+ routerData: routerData,
350
+ routerValue: txValue,
351
+ userCurrencyFromRaw:
352
+ '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
353
+ userCurrencyToRaw:
354
+ '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
355
+ );
356
+
357
+ return trade;
358
+ } catch (e) {
359
+ printV('createTrade error: $e');
360
+ throw TradeNotCreatedException(description, description: e.toString());
361
+ }
362
+ }
363
+
364
+ /// Register a broadcasted tx with Swaps.xyz (required for alt-vm).
365
+ Future<bool> registerAltVmTx({
366
+ required String txId,
367
+ required String txHash,
368
+ required int chainId,
369
+ required String vmId,
370
+ }) async {
371
+ try {
372
+ final uri = Uri.https(_baseUrl, _registerTxs);
373
+ final payload = {
374
+ 'txId': txId,
375
+ 'vmId': vmId,
376
+ 'txHash': txHash,
377
+ 'chainId': chainId,
378
+ };
379
+
380
+ final res = await ProxyWrapper().post(
381
+ clearnetUri: uri,
382
+ headers: {
383
+ ..._headers,
384
+ 'content-type': 'application/json',
385
+ },
386
+ body: jsonEncode(payload),
387
+ );
388
+
389
+ if (res.statusCode != 200) {
390
+ printV('registerTxs failed: ${res.statusCode} ${res.body}');
391
+ return false;
392
+ }
393
+ final List<dynamic> body = json.decode(res.body) as List<dynamic>;
394
+ if (body.isEmpty) return false;
395
+
396
+ final isSuccess =
397
+ (body[0] as Map<String, dynamic>)['success'] as bool? ?? false;
398
+
399
+ return isSuccess;
400
+ } catch (e) {
401
+ printV('registerAltVmTx error: $e');
402
+ return false;
403
+ }
404
+ }
405
+
406
+ @override
407
+ Future<Trade> findTradeById({required String id}) async {
408
+ if (id.isEmpty) {
409
+ throw Exception('Trade id is empty');
410
+ }
411
+
412
+ final uri = Uri.https(_baseUrl, _getStatus, {'txId': id});
413
+ final resp = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
414
+
415
+ if (resp.statusCode != 200) {
416
+ throw Exception('getStatus failed: ${resp.statusCode} ${resp.body}');
417
+ }
418
+
419
+ final data = json.decode(resp.body) as Map<String, dynamic>;
420
+ final isSuccess = (data['success'] as bool?);
421
+
422
+ if (isSuccess != null && !isSuccess) {
423
+ final error = data['error'] as Map<String, dynamic>?;
424
+ if (error != null) {
425
+ final code = error['code']?.toString() ?? 'unknown';
426
+ throw Exception(
427
+ 'SwapXyzExchangeProvider findTradeById error: ($id) $code');
428
+ }
429
+ }
430
+
431
+ final statusStr = (data['status'] as String?)?.toLowerCase() ?? 'NOT_FOUND';
432
+ final state = _mapSwapsStatusToTradeState(statusStr);
433
+
434
+ final refundAddress = data['sender']?.toString();
435
+
436
+ final srcTransaction = (data['srcTx'] as Map?)?.cast<String, dynamic>();
437
+ final dstTransaction = (data['dstTx'] as Map?)?.cast<String, dynamic>();
438
+
439
+ final inputAddress = srcTransaction?['toAddress']?.toString();
440
+
441
+ final payoutAddress = dstTransaction?['toAddress']?.toString();
442
+
443
+ final srcPaymentToken =
444
+ (srcTransaction?['paymentToken'] as Map?)?.cast<String, dynamic>();
445
+ final dstPaymentToken =
446
+ (dstTransaction?['paymentToken'] as Map?)?.cast<String, dynamic>();
447
+
448
+ final fromSymbol = (srcPaymentToken?['symbol'] as String?) ?? '';
449
+ final toSymbol = (dstPaymentToken?['symbol'] as String?) ?? '';
450
+
451
+ CryptoCurrency? toCurrency;
452
+ if (toSymbol.isNotEmpty) {
453
+ toCurrency = CryptoCurrency.safeParseCurrencyFromString(toSymbol);
454
+ }
455
+
456
+ final srcDecimals = (srcPaymentToken?['decimals'] as num?)?.toInt() ?? 0;
457
+ final dstDecimals = (dstPaymentToken?['decimals'] as num?)?.toInt() ?? 0;
458
+
459
+ final txHash = srcTransaction?['txHash'] as String?;
460
+
461
+ // Minimal-unit amounts like "12000n"
462
+ final srcAmountRaw = srcPaymentToken?['amount']?.toString();
463
+ final dstAmountRaw = dstPaymentToken?['amount']?.toString();
464
+ String? receiveAmount;
465
+ if (dstAmountRaw != null) {
466
+ final dstAmountMinimal = _stripN(dstAmountRaw);
467
+ receiveAmount =
468
+ AmountConverter.fromBaseUnits(dstAmountMinimal, dstDecimals);
469
+ }
470
+
471
+ final srcAmountMinimal = _stripN(srcAmountRaw);
472
+ final amount = AmountConverter.fromBaseUnits(srcAmountMinimal, srcDecimals);
473
+ final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromSymbol);
474
+
475
+ // Timestamps can be num or "123n" handle both
476
+ final srcTs = _parseUnixSeconds(srcTransaction?['timestamp']);
477
+ final dstTs = _parseUnixSeconds(dstTransaction?['timestamp']);
478
+ final timestamp = srcTs ?? dstTs;
479
+
480
+ final createdAt = timestamp != null
481
+ ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true)
482
+ .toLocal()
483
+ : null;
484
+
485
+ return Trade(
486
+ id: (data['txId'] as String?) ?? id,
487
+ from: fromCurrency,
488
+ to: toCurrency,
489
+ provider: description,
490
+ inputAddress: inputAddress,
491
+ payoutAddress: payoutAddress,
492
+ amount: amount,
493
+ receiveAmount: receiveAmount,
494
+ txId: txHash,
495
+ state: state,
496
+ createdAt: createdAt,
497
+ refundAddress: refundAddress,
498
+ userCurrencyFromRaw: '${fromSymbol.toUpperCase()}' + '_',
499
+ userCurrencyToRaw: '${toSymbol.toUpperCase()}' + '_',
500
+ );
501
+ }
502
+
503
+ TradeState _mapSwapsStatusToTradeState(String s) {
504
+ switch (s) {
505
+ case 'pending':
506
+ case 'processing':
507
+ return TradeState.pending;
508
+ case 'success':
509
+ case 'completed':
510
+ case 'complete':
511
+ return TradeState.finished;
512
+ case 'failed':
513
+ case 'cancelled':
514
+ case 'canceled':
515
+ case 'error':
516
+ return TradeState.failed;
517
+ default:
518
+ return TradeState.pending;
519
+ }
520
+ }
521
+
522
+ // Load & cache supported chains once
523
+ Future<List<Chain>> _geSupportedChain() async {
524
+ if (_supportedChainList.isNotEmpty) return _supportedChainList;
525
+ try {
526
+ final uri = Uri.https(_baseUrl, _getChainList);
527
+ final response =
528
+ await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
529
+ if (response.statusCode != 200) return [];
530
+
531
+ final data = json.decode(response.body) as List<dynamic>;
532
+ _supportedChainList
533
+ ..clear()
534
+ ..addAll(data.map((e) => Chain.fromJson(e as Map<String, dynamic>)));
535
+ return _supportedChainList;
536
+ } catch (e) {
537
+ printV(e);
538
+ return [];
539
+ }
540
+ }
541
+
542
+ // Ensure tokens for this pair are cached (fills both src & dst chain entries)
543
+ // Replace your _ensureTokensCached with this one
544
+ Future<void> _ensureTokensCached({
545
+ required Chain fromChain,
546
+ required Chain toChain,
547
+ required CryptoCurrency from,
548
+ required CryptoCurrency to,
549
+ }) async {
550
+ bool needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
551
+ (_tokensCache[fromChain.chainId]?.isEmpty ?? true);
552
+ bool needDst = !_tokensCache.containsKey(toChain.chainId) ||
553
+ (_tokensCache[toChain.chainId]?.isEmpty ?? true);
554
+
555
+ if (!needSrc && !needDst) return;
556
+
557
+ // First try: from -> to
558
+ await _fetchAndCacheTokens(
559
+ srcChainId: fromChain.chainId,
560
+ srcToken: from.title.toUpperCase(),
561
+ dstChainId: toChain.chainId,
562
+ dstToken: to.title.toUpperCase(),
563
+ );
564
+
565
+ // Re-check after first call
566
+ needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
567
+ (_tokensCache[fromChain.chainId]?.isEmpty ?? true);
568
+ needDst = !_tokensCache.containsKey(toChain.chainId) ||
569
+ (_tokensCache[toChain.chainId]?.isEmpty ?? true);
570
+
571
+ // If any still missing, try swapped: to -> from
572
+ if (needSrc || needDst) {
573
+ await _fetchAndCacheTokens(
574
+ srcChainId: toChain.chainId,
575
+ srcToken: to.title.toUpperCase(),
576
+ dstChainId: fromChain.chainId,
577
+ dstToken: from.title.toUpperCase(),
578
+ );
579
+ }
580
+ }
581
+
582
+ // call getPaths and merge tokens into cache keyed by the chainId
583
+ Future<void> _fetchAndCacheTokens({
584
+ required int srcChainId,
585
+ required String srcToken,
586
+ required int dstChainId,
587
+ required String dstToken,
588
+ }) async {
589
+ final params = <String, String>{
590
+ 'srcChainId': '$srcChainId',
591
+ 'srcToken': srcToken,
592
+ 'dstChainId': '$dstChainId',
593
+ };
594
+
595
+ final uri = Uri.https(_baseUrl, _getPaths, params);
596
+ final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
597
+ if (res.statusCode != 200) {
598
+ printV('getPaths failed: ${res.statusCode} ${res.body}');
599
+ return;
600
+ }
601
+
602
+ final body = json.decode(res.body) as Map<String, dynamic>;
603
+ final paths = (body['paths'] as List?) ?? const [];
604
+ if (paths.isEmpty) return;
605
+
606
+ final path0 = paths.first as Map<String, dynamic>;
607
+ final pathChainId = (path0['chainId'] as num?)?.toInt();
608
+ final tokens = (path0['tokens'] as List?) ?? const [];
609
+
610
+ if (pathChainId == null || tokens.isEmpty) return;
611
+
612
+ final parsed = tokens
613
+ .map((t) => TokenPathInfo.fromJson(t as Map<String, dynamic>))
614
+ .toList();
615
+
616
+ _mergeCache(pathChainId, parsed);
617
+ }
618
+
619
+// Merge by symbol, prefer entries that have a non-empty address/decimals
620
+ void _mergeCache(int chainId, List<TokenPathInfo> incoming) {
621
+ final existing = _tokensCache[chainId] ?? const <TokenPathInfo>[];
622
+ final bySymbol = <String, TokenPathInfo>{
623
+ for (final t in existing) t.symbol: t
624
+ };
625
+
626
+ for (final t in incoming) {
627
+ final cur = bySymbol[t.symbol];
628
+ if (cur == null) {
629
+ bySymbol[t.symbol] = t;
630
+ } else {
631
+ // If incoming has a real address/decimals, prefer it
632
+ final hasBetterAddr = (t.address != null && t.address!.isNotEmpty) &&
633
+ (cur.address == null || cur.address!.isEmpty);
634
+ final hasBetterDec = (t.decimals != null) && (cur.decimals == null);
635
+ if (hasBetterAddr || hasBetterDec) {
636
+ bySymbol[t.symbol] = t;
637
+ }
638
+ }
639
+ }
640
+
641
+ _tokensCache[chainId] = bySymbol.values.toList();
642
+ }
643
+
644
+ String _nativeSymbolForChain(Chain chain) {
645
+ final network = chain.name.toUpperCase();
646
+ switch (network) {
647
+ case 'BTC':
648
+ case 'BITCOIN':
649
+ return 'BTC';
650
+ case 'XMR':
651
+ case 'MONERO':
652
+ return 'XMR';
653
+ case 'TRX':
654
+ case 'TRON':
655
+ return 'TRX';
656
+ case 'ETH':
657
+ case 'ETHEREUM':
658
+ return 'ETH';
659
+ case 'LTC':
660
+ case 'LITECOIN':
661
+ return 'LTC';
662
+ case 'DOGE':
663
+ case 'DOGECOIN':
664
+ return 'DOGE';
665
+ case 'XRP':
666
+ case 'RIPPLE':
667
+ return 'XRP';
668
+ case 'XLM':
669
+ case 'STELLAR':
670
+ return 'XLM';
671
+ case 'SOL':
672
+ case 'SOLANA':
673
+ return 'SOL';
674
+ case 'CARDANO':
675
+ return 'ADA';
676
+ case 'Bitcoin Cash':
677
+ return 'BCH';
678
+ case 'POLYGON':
679
+ return 'POL';
680
+ default:
681
+ return network;
682
+ }
683
+ }
684
+
685
+ String _getTokenAddress({
686
+ required CryptoCurrency currency,
687
+ required Chain chain,
688
+ }) {
689
+ final symbol = currency.title.toUpperCase();
690
+ final list = _tokensCache[chain.chainId];
691
+
692
+ // Try cache hit
693
+ if (list != null && list.isNotEmpty) {
694
+ for (final t in list) {
695
+ if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) {
696
+ return t.address!;
697
+ }
698
+ }
699
+ }
700
+
701
+ // If it's the native coin for this chain, return native placeholder
702
+ final nativeSym = _nativeSymbolForChain(chain);
703
+ if (nativeSym == symbol) {
704
+ return _nativePlaceholderForVm(chain.vmId);
705
+ }
706
+
707
+ // May fail for non-native Alt-VM assets
708
+ return symbol;
709
+ }
710
+
711
+ String _nativePlaceholderForVm(String vmId) {
712
+ switch (vmId.toLowerCase()) {
713
+ case 'evm':
714
+ return '0x0000000000000000000000000000000000000000';
715
+ case 'alt-vm':
716
+ return '0x0000000000000000000000000000000000000000';
717
+ default:
718
+ return '0x0000000000000000000000000000000000000000';
719
+ }
720
+ }
721
+
722
+ Map<String, dynamic>? findTokenBySymbol(
723
+ {required String title, required List<dynamic> tokens}) {
724
+ final reqSymbol = title.toUpperCase();
725
+ for (final token in tokens) {
726
+ final map = token as Map<String, dynamic>;
727
+ final symbol = (map['symbol'] as String?)?.toUpperCase();
728
+ if (symbol == reqSymbol) return map;
729
+ }
730
+ return null;
731
+ }
732
+
733
+ Chain _findChainByCurrency(CryptoCurrency cur, List<Chain> chains) {
734
+ final network = _normalizeCakeNetwork(cur.tag ?? cur.title);
735
+ return chains.firstWhere(
736
+ (c) => c.name.toUpperCase() == network,
737
+ orElse: () => throw Exception('Unsupported chain for ${cur.title}'),
738
+ );
739
+ }
740
+
741
+ String _normalizeCakeNetwork(String network) {
742
+ return switch (network.toUpperCase()) {
743
+ 'ETH' => 'ETHEREUM',
744
+ 'BSC' => 'BNB SMART CHAIN',
745
+ 'POL' => 'POLYGON',
746
+ 'AVAXC' => 'AVALANCHE',
747
+ 'TRX' => 'TRON',
748
+ 'SOL' => 'SOLANA',
749
+ 'CRO' => 'CRONOS',
750
+ 'ADA' => 'CARDANO',
751
+ 'KAS' => 'KASPA',
752
+ 'TON' => 'TONCOIN',
753
+ 'BCH' => 'BITCOIN CASH',
754
+ _ => network.toUpperCase(),
755
+ };
756
+ }
757
+
758
+ int? _parseUnixSeconds(dynamic value) {
759
+ if (value == null) return null;
760
+ if (value is num) return value.toInt();
761
+ if (value is String) {
762
+ final clean = _stripN(value);
763
+ return int.tryParse(clean);
764
+ }
765
+ return null;
766
+ }
767
+
768
+ String _stripN(String? str) {
769
+ final s = str ?? '0';
770
+ return s.endsWith('n') ? s.substring(0, s.length - 1) : s;
771
+ }
772
+}
773
+
774
+class TokenPathInfo {
775
+ final String symbol;
776
+ final String? address;
777
+ final int? decimals;
778
+ final String? minAmount;
779
+ final String? maxAmount;
780
+
781
+ TokenPathInfo({
782
+ required this.symbol,
783
+ required this.address,
784
+ required this.decimals,
785
+ required this.minAmount,
786
+ required this.maxAmount,
787
+ });
788
+
789
+ factory TokenPathInfo.fromJson(Map<String, dynamic> json) => TokenPathInfo(
790
+ symbol: (json['symbol'] as String?)?.toUpperCase() ?? '',
791
+ address: json['address'] as String?,
792
+ decimals: json['decimals'] as int?,
793
+ minAmount: json['minAmount']?.toString(),
794
+ maxAmount: json['maxAmount']?.toString(),
795
+ );
796
+}
797
+
798
+class Chain {
799
+ final int chainId;
800
+ final String name;
801
+ final String vmId;
802
+
803
+ Chain({
804
+ required this.chainId,
805
+ required this.name,
806
+ required this.vmId,
807
+ });
808
+
809
+ factory Chain.fromJson(Map<String, dynamic> json) {
810
+ return Chain(
811
+ chainId: json['chainId'] as int,
812
+ name: json['name'] as String,
813
+ vmId: json['vmId'] as String,
814
+ );
815
+ }
816
+}
817
+
818
+class _PathInfo {
819
+ final bool supportsExactOut;
820
+ final String minToAmountHuman;
821
+
822
+ _PathInfo({required this.supportsExactOut, required this.minToAmountHuman});
823
+}
lib/exchange/provider/swaptrade_exchange_provider.dart
+1
-1
@@ -105,7 +105,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
105
required CryptoCurrency to,
106
required double amount,
107
required bool isFixedRateMode,
108
- required bool isReceiveAmount,
108
+ required bool isReceiveAmount
109
}) async {
110
try {
111
if (amount == 0) return 0.0;
lib/exchange/provider/thorchain_exchange.provider.dart
+7
-6
@@ -66,12 +66,13 @@ class ThorChainExchangeProvider extends ExchangeProvider {
66
Future<bool> checkIsAvailable() async => true;
67
68
@override
69
- Future<double> fetchRate(
70
- {required CryptoCurrency from,
71
- required CryptoCurrency to,
72
- required double amount,
73
- required bool isFixedRateMode,
74
- required bool isReceiveAmount}) async {
69
+ Future<double> fetchRate({
70
+ required CryptoCurrency from,
71
+ required CryptoCurrency to,
72
+ required double amount,
73
+ required bool isFixedRateMode,
74
+ required bool isReceiveAmount
75
+ }) async {
76
try {
77
if (amount == 0) return 0.0;
78
lib/exchange/provider/trocador_exchange_provider.dart
+7
-6
@@ -120,12 +120,13 @@ class TrocadorExchangeProvider extends ExchangeProvider {
120
}
121
122
@override
123
- Future<double> fetchRate(
124
- {required CryptoCurrency from,
125
- required CryptoCurrency to,
126
- required double amount,
127
- required bool isFixedRateMode,
128
- required bool isReceiveAmount}) async {
123
+ Future<double> fetchRate({
124
+ required CryptoCurrency from,
125
+ required CryptoCurrency to,
126
+ required double amount,
127
+ required bool isFixedRateMode,
128
+ required bool isReceiveAmount
129
+ }) async {
130
try {
131
if (amount == 0) return 0.0;
132
lib/exchange/provider/xoswap_exchange_provider.dart
+2
-1
@@ -142,12 +142,13 @@ class XOSwapExchangeProvider extends ExchangeProvider {
142
}
143
}
144
145
+ @override
146
Future<double> fetchRate({
147
required CryptoCurrency from,
148
required CryptoCurrency to,
149
required double amount,
150
required bool isFixedRateMode,
150
- required bool isReceiveAmount,
151
+ required bool isReceiveAmount
152
}) async {
153
try {
154
final rates = await getRatesForPair(from: from, to: to);
lib/exchange/trade.dart
+59
-1
@@ -33,6 +33,15 @@ class Trade extends HiveObject {
33
this.router,
34
this.userCurrencyFromRaw,
35
this.userCurrencyToRaw,
36
+ // The following fields are used for SwapXyz trades only
37
+ this.needToRegisterInSwapXyz,
38
+ this.sourceTokenAddress,
39
+ this.sourceTokenDecimals,
40
+ this.routerData,
41
+ this.routerValue,
42
+ this.routerChainId,
43
+ this.sourceTokenAmountRaw,
44
+ this.requiresTokenApproval
45
}) {
46
if (provider != null) providerRaw = provider.raw;
47
@@ -133,6 +142,31 @@ class Trade extends HiveObject {
142
@HiveField(25, defaultValue: '')
143
String? userCurrencyToRaw;
144
145
+ // The following fields are used for SwapXyz trades only
146
+ @HiveField(26)
147
+ bool? needToRegisterInSwapXyz;
148
+
149
+ @HiveField(27)
150
+ String? sourceTokenAddress;
151
+
152
+ @HiveField(28)
153
+ int? sourceTokenDecimals;
154
+
155
+ @HiveField(29)
156
+ String? routerData;
157
+
158
+ @HiveField(30)
159
+ String? routerValue;
160
+
161
+ @HiveField(31)
162
+ int? routerChainId;
163
+
164
+ @HiveField(32)
165
+ String? sourceTokenAmountRaw;
166
+
167
+ @HiveField(33, defaultValue: false)
168
+ bool? requiresTokenApproval;
169
+
170
CryptoCurrency? get userCurrencyFrom {
171
if (userCurrencyFromRaw == null || userCurrencyFromRaw!.isEmpty) {
172
return null;
@@ -249,6 +283,14 @@ class TradeAdapter extends TypeAdapter<Trade> {
283
router: fields[22] as String?,
284
userCurrencyFromRaw: fields[24] as String?,
285
userCurrencyToRaw: fields[25] as String?,
286
+ needToRegisterInSwapXyz: fields[26] as bool?,
287
+ sourceTokenAddress: fields[27] as String?,
288
+ sourceTokenDecimals: fields[28] as int?,
289
+ routerData: fields[29] as String?,
290
+ routerValue: fields[30] as String?,
291
+ routerChainId: fields[31] as int?,
292
+ sourceTokenAmountRaw: fields[32] as String?,
293
+ requiresTokenApproval: fields[33] as bool?,
294
)
295
..providerRaw = fields[1] == null ? 0 : fields[1] as int
296
..fromRaw = (fields[2] as int?) ?? -1
@@ -311,7 +353,23 @@ class TradeAdapter extends TypeAdapter<Trade> {
353
..writeByte(24)
354
..write(obj.userCurrencyFromRaw)
355
..writeByte(25)
314
- ..write(obj.userCurrencyToRaw);
356
+ ..write(obj.userCurrencyToRaw)
357
+ ..writeByte(26)
358
+ ..write(obj.needToRegisterInSwapXyz)
359
+ ..writeByte(27)
360
+ ..write(obj.sourceTokenAddress)
361
+ ..writeByte(28)
362
+ ..write(obj.sourceTokenDecimals)
363
+ ..writeByte(29)
364
+ ..write(obj.routerData)
365
+ ..writeByte(30)
366
+ ..write(obj.routerValue)
367
+ ..writeByte(31)
368
+ ..write(obj.routerChainId)
369
+ ..writeByte(32)
370
+ ..write(obj.sourceTokenAmountRaw)
371
+ ..writeByte(33)
372
+ ..write(obj.requiresTokenApproval);
373
}
374
375
@override
lib/polygon/cw_polygon.dart
+7
@@ -183,6 +183,13 @@ class CWPolygon extends Polygon {
183
@override
184
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
185
186
+ Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) =>
187
+ (wallet as PolygonWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
188
+
189
+ Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei,
190
+ TransactionPriority priority) =>
191
+ (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority);
192
+
193
@override
194
Future<PendingTransaction> createTokenApproval(
195
WalletBase wallet,
lib/src/screens/exchange_trade/exchange_confirm_page.dart
+10
-9
@@ -65,15 +65,16 @@ class ExchangeConfirmPage extends BasePage {
65
fontWeight: FontWeight.w500,
66
),
67
),
68
- Text(
69
- trade.id,
70
- maxLines: 1,
71
- overflow: TextOverflow.ellipsis,
72
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
73
- fontSize: 20,
74
- fontWeight: FontWeight.w600,
75
- color: Theme.of(context).colorScheme.onSurface,
76
- ),
68
+ Expanded(
69
+ child: Text(
70
+ trade.id,
71
+ overflow: TextOverflow.visible,
72
+ style: Theme.of(context).textTheme.titleLarge?.copyWith(
73
+ fontSize: 20,
74
+ fontWeight: FontWeight.w600,
75
+ color: Theme.of(context).colorScheme.onSurface,
76
+ ),
77
+ ),
78
),
79
],
80
),
lib/src/screens/exchange_trade/exchange_trade_page.dart
+12
-2
@@ -1,3 +1,4 @@
1
+import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2
import 'package:cake_wallet/reactions/wallet_connect.dart';
3
import 'package:cake_wallet/routes.dart';
4
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
@@ -173,6 +174,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
174
textColor: widget.exchangeTradeViewModel.isSendable
175
? Theme.of(context).colorScheme.onSecondaryContainer
176
: Theme.of(context).colorScheme.onPrimary,
177
+ isDisabled: widget.exchangeTradeViewModel.isSwapsXyzSendingEVMTokenSwap,
178
),
179
SizedBox(height: 16),
180
Observer(
@@ -265,6 +267,14 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
267
268
if (state is ExecutedSuccessfullyState) {
269
WidgetsBinding.instance.addPostFrameCallback((_) async {
270
+ final trade = widget.exchangeTradeViewModel.trade;
271
+ final isSwapsXyz = trade.provider == ExchangeProviderDescription.swapsXyz;
272
+ final isEVMWallet = widget.exchangeTradeViewModel.sendViewModel.isEVMWallet;
273
+
274
+ final amountValue = isSwapsXyz && isEVMWallet
275
+ ? trade.amount
276
+ : widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted;
277
+
278
if (context.mounted) {
279
final result = await showModalBottomSheet<bool>(
280
context: context,
@@ -280,8 +290,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
290
widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
291
currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency,
292
amount: S.of(bottomSheetContext).send_amount,
283
- amountValue: widget
284
- .exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted,
293
+ amountValue: amountValue,
294
fiatAmountValue: widget
295
.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
296
fee:
@@ -298,6 +307,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
307
Navigator.of(bottomSheetContext).pop(true);
308
}
309
widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context);
310
+ widget.exchangeTradeViewModel.registerSwapsXyzTransaction();
311
},
312
);
313
},
lib/store/dashboard/trade_filter_store.dart
+15
-3
@@ -21,7 +21,8 @@ abstract class TradeFilterStoreBase with Store {
21
displayLetsExchange = true,
22
displayStealthEx = true,
23
displayXOSwap = true,
24
- displaySwapTrade = true;
24
+ displaySwapTrade = true,
25
+ displaySwapXyz = true;
26
27
@observable
28
bool displayXMRTO;
@@ -62,6 +63,9 @@ abstract class TradeFilterStoreBase with Store {
63
@observable
64
bool displaySwapTrade;
65
66
+ @observable
67
+ bool displaySwapXyz;
68
+
69
@computed
70
bool get displayAllTrades =>
71
displayChangeNow &&
@@ -74,7 +78,8 @@ abstract class TradeFilterStoreBase with Store {
78
displayLetsExchange &&
79
displayStealthEx &&
80
displayXOSwap &&
77
- displaySwapTrade;
81
+ displaySwapTrade &&
82
+ displaySwapXyz;
83
84
@action
85
void toggleDisplayExchange(ExchangeProviderDescription provider) {
@@ -118,6 +123,9 @@ abstract class TradeFilterStoreBase with Store {
123
case ExchangeProviderDescription.swapTrade:
124
displaySwapTrade = !displaySwapTrade;
125
break;
126
+ case ExchangeProviderDescription.swapsXyz:
127
+ displaySwapXyz = !displaySwapXyz;
128
+ break;
129
case ExchangeProviderDescription.all:
130
if (displayAllTrades) {
131
displayChangeNow = false;
@@ -133,6 +141,7 @@ abstract class TradeFilterStoreBase with Store {
141
displayStealthEx = false;
142
displayXOSwap = false;
143
displaySwapTrade = false;
144
+ displaySwapXyz = false;
145
} else {
146
displayChangeNow = true;
147
displaySideShift = true;
@@ -147,6 +156,7 @@ abstract class TradeFilterStoreBase with Store {
156
displayStealthEx = true;
157
displayXOSwap = true;
158
displaySwapTrade = true;
159
+ displaySwapXyz = true;
160
}
161
break;
162
}
@@ -180,7 +190,9 @@ abstract class TradeFilterStoreBase with Store {
190
item.trade.provider == ExchangeProviderDescription.letsExchange) ||
191
(displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) ||
192
(displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) ||
183
- (displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade))
193
+ (displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade) ||
194
+ (displaySwapXyz &&
195
+ item.trade.provider == ExchangeProviderDescription.swapsXyz))
196
.toList()
197
: _trades;
198
}
lib/view_model/dashboard/dashboard_view_model.dart
+5
@@ -181,6 +181,11 @@ abstract class DashboardViewModelBase with Store {
181
caption: ExchangeProviderDescription.swapTrade.title,
182
onChanged: () =>
183
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)),
184
+ FilterItem(
185
+ value: () => tradeFilterStore.displaySwapXyz,
186
+ caption: ExchangeProviderDescription.swapsXyz.title,
187
+ onChanged: () =>
188
+ tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
189
]
190
},
191
subname = '',
lib/view_model/exchange/exchange_trade_view_model.dart
+75
-2
@@ -8,6 +8,7 @@ import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
8
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
9
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
10
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
11
+import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
12
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
13
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
14
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
@@ -80,6 +81,10 @@ abstract class ExchangeTradeViewModelBase with Store {
81
break;
82
case ExchangeProviderDescription.xoSwap:
83
_provider = XOSwapExchangeProvider();
84
+ break;
85
+ case ExchangeProviderDescription.swapsXyz:
86
+ _provider = SwapsXyzExchangeProvider();
87
+ break;
88
}
89
90
_updateItems();
@@ -104,6 +109,15 @@ abstract class ExchangeTradeViewModelBase with Store {
109
@observable
110
bool isSendable;
111
112
+
113
+ bool get isSwapsXyzSendingEVMTokenSwap => (_provider is SwapsXyzExchangeProvider) &&
114
+ (wallet.type == WalletType.ethereum &&
115
+ wallet.currency != trade.from ||
116
+ (wallet.type == WalletType.polygon &&
117
+ wallet.currency != trade.from) ||
118
+ (wallet.type == WalletType.base &&
119
+ wallet.currency != trade.from));
120
+
121
String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty
122
? '\n\n' + S.current.exchange_extra_info
123
: '';
@@ -189,7 +203,19 @@ abstract class ExchangeTradeViewModelBase with Store {
203
204
sendViewModel.selectedCryptoCurrency = selected;
205
192
- final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
206
+ final pendingTransaction = await sendViewModel.createTransaction(provider: _provider, trade: trade);
207
+
208
+ if (_provider is SwapsXyzExchangeProvider) {
209
+ final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? '';
210
+ trade.txId = hash;
211
+
212
+ if (trade.isInBox) {
213
+ await trade.save();
214
+ } else {
215
+ await trades.add(trade);
216
+ }
217
+ }
218
+
219
if (_provider is ThorChainExchangeProvider) {
220
trade.id = pendingTransaction?.id ?? '';
221
trades.add(trade);
@@ -308,7 +334,7 @@ abstract class ExchangeTradeViewModelBase with Store {
334
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
335
336
bool _isBaseToken() =>
311
- wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.title;
337
+ wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag;
338
339
bool _isTronToken() =>
340
wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
@@ -324,6 +350,53 @@ abstract class ExchangeTradeViewModelBase with Store {
350
_isTronToken() ||
351
_isBaseToken();
352
}
353
+
354
+ Future<void> registerSwapsXyzTransaction() async {
355
+ try {
356
+ if (!(_provider is SwapsXyzExchangeProvider)) return;
357
+ final swaps = _provider as SwapsXyzExchangeProvider;
358
+
359
+ // register only for vmId is alt-vm or bridgeId is alt-vm (trade.needToRegisterInSwapXyz)
360
+ final needToRegister = trade.needToRegisterInSwapXyz ?? false;
361
+ if (!needToRegister) return;
362
+
363
+ final vmId = (trade.providerId ?? '').toLowerCase();
364
+ if (vmId.isEmpty) {
365
+ printV('SwapsXyz: transaction register: skipped (vmId empty)');
366
+ return;
367
+ }
368
+
369
+ final txHash = sendViewModel.pendingTransaction?.evmTxHashFromRawHex ?? sendViewModel.pendingTransaction?.id ?? '';
370
+
371
+ if (txHash.isEmpty) {
372
+ printV('SwapsXyz: transaction register: skipped (txHash empty)');
373
+ return;
374
+ }
375
+
376
+ final chainId = int.tryParse(trade.router ?? '') ?? 0;
377
+ if (chainId <= 0) {
378
+ printV('SwapsXyz: transaction register: skipped (invalid chainId)');
379
+ return;
380
+ }
381
+
382
+ printV('SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId');
383
+
384
+ final registered = await swaps.registerAltVmTx(
385
+ txId: trade.id,
386
+ txHash: txHash,
387
+ chainId: chainId,
388
+ vmId: vmId,
389
+ );
390
+
391
+ if (!registered) {
392
+ printV('SwapsXyz: transaction register: failed');
393
+ } else {
394
+ printV('SwapsXyz: transaction register: success');
395
+ }
396
+ } catch (e) {
397
+ printV('registerSwapsXyzTransaction error: $e');
398
+ }
399
+ }
400
401
PaymentURI? get paymentUri {
402
final inputAddress = trade.inputAddress;
lib/view_model/exchange/exchange_view_model.dart
+3
-1
@@ -27,6 +27,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
27
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
28
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
29
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
30
+import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
31
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
32
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
33
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
@@ -214,6 +215,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
215
LetsExchangeExchangeProvider(),
216
StealthExExchangeProvider(),
217
XOSwapExchangeProvider(),
218
+ SwapsXyzExchangeProvider(),
219
TrocadorExchangeProvider(
220
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
221
];
@@ -536,7 +538,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
538
to: receiveCurrency,
539
amount: amount,
540
isFixedRateMode: isFixedRateMode,
539
- isReceiveAmount: isFixedRateMode,
541
+ isReceiveAmount: isFixedRateMode
542
)
543
.timeout(
544
Duration(seconds: 7),
lib/view_model/send/send_view_model.dart
+251
-1
@@ -22,7 +22,9 @@ import 'package:cake_wallet/entities/transaction_description.dart';
22
import 'package:cake_wallet/entities/wallet_contact.dart';
23
import 'package:cake_wallet/ethereum/ethereum.dart';
24
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
25
+import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
26
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
27
+import 'package:cake_wallet/exchange/trade.dart';
28
import 'package:cake_wallet/generated/i18n.dart';
29
import 'package:cake_wallet/monero/monero.dart';
30
import 'package:cake_wallet/nano/nano.dart';
@@ -46,6 +48,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_mod
48
import 'package:cake_wallet/wownero/wownero.dart';
49
import 'package:cake_wallet/zano/zano.dart';
50
import 'package:cw_core/crypto_currency.dart';
51
+import 'package:cw_core/erc20_token.dart';
52
import 'package:cw_core/currency_for_wallet_type.dart';
53
import 'package:cw_core/exceptions.dart';
54
import 'package:cw_core/pending_transaction.dart';
@@ -119,6 +122,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
122
});
123
}
124
125
+ PendingTransaction? _pendingApprovalTx;
126
+ bool _isSwapsXYZCallDataTx = false;
127
+
128
@observable
129
ExecutionState state;
130
@@ -133,6 +139,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
139
140
bool get isMwebEnabled => balanceViewModel.mwebEnabled;
141
142
+ bool get isEVMWallet => walletType == WalletType.ethereum || walletType == WalletType.polygon ||
143
+ walletType == WalletType.base;
144
+
145
@action
146
void setShowAddressBookPopup(bool value) {
147
_settingsStore.showAddressBookPopupEnabled = value;
@@ -490,7 +499,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
499
Timer? _ledgerTxStateTimer;
500
501
@action
493
- Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
502
+ Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
503
try {
504
if (!(state is IsExecutingState)) state = IsExecutingState();
505
@@ -505,6 +514,151 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
514
});
515
}
516
517
+
518
+ // Swaps.xyz (EVM) path
519
+
520
+ if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) {
521
+ final routerTo = trade.inputAddress;
522
+ final routerData = trade.routerData;
523
+ final routerValueWei =
524
+ BigInt.tryParse((trade.routerValue ?? '0').toString()) ?? BigInt.zero;
525
+
526
+ if (routerTo?.isNotEmpty == true && routerData?.isNotEmpty == true) {
527
+
528
+ // detect prepared ERC-20 transfer(...) (alt-vm deposit pattern)
529
+ String _selector(String s) =>
530
+ (s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
531
+ const _transferSig = '0xa9059cbb';
532
+ final _sel = _selector(routerData!);
533
+ final _isPreparedTransfer = _sel == _transferSig &&
534
+ (trade.sourceTokenAddress ?? '').toLowerCase() == (routerTo ?? '').toLowerCase();
535
+
536
+ _pendingApprovalTx = null;
537
+
538
+ // Optionally prebuild approval (SKIP for prepared transfer)
539
+ final tokenContract = trade.sourceTokenAddress ?? '';
540
+ final requiredAmount = BigInt.tryParse(
541
+ (trade.sourceTokenAmountRaw ?? '0').replaceAll('n', ''),
542
+ ) ?? BigInt.zero;
543
+
544
+ // Only do approval when NOT a prepared transfer, and only if the API hinted we might need it
545
+ final requiresTokenApproval = (trade.requiresTokenApproval ?? false) && !_isPreparedTransfer;
546
+
547
+ if (requiresTokenApproval && tokenContract.isNotEmpty && requiredAmount > BigInt.zero) {
548
+ if (walletType == WalletType.ethereum) {
549
+ final priority = _settingsStore.priority[WalletType.ethereum]!;
550
+ _pendingApprovalTx = await buildApprovalIfNeeded(
551
+ spender: routerTo!, // if API provides a specific spender, use that instead
552
+ tokenContract: tokenContract,
553
+ requiredAmount: requiredAmount,
554
+ sourceTokenDecimals: trade.sourceTokenDecimals,
555
+ );
556
+
557
+ // Build the callData tx
558
+ pendingTransaction = await ethereum!.createRawCallDataTransaction(
559
+ wallet,
560
+ routerTo,
561
+ routerData,
562
+ routerValueWei,
563
+ priority,
564
+ );
565
+
566
+ _isSwapsXYZCallDataTx = true;
567
+ state = ExecutedSuccessfullyState();
568
+ return pendingTransaction; // do NOT fall back to regular flow
569
+ }
570
+ if (walletType == WalletType.polygon) {
571
+ final priority = _settingsStore.priority[WalletType.polygon]!;
572
+ _pendingApprovalTx = await buildApprovalIfNeeded(
573
+ spender: routerTo!,
574
+ tokenContract: tokenContract,
575
+ requiredAmount: requiredAmount,
576
+ sourceTokenDecimals: trade.sourceTokenDecimals,
577
+ );
578
+
579
+ // Build the callData tx
580
+ pendingTransaction = await polygon!.createRawCallDataTransaction(
581
+ wallet,
582
+ routerTo,
583
+ routerData,
584
+ routerValueWei,
585
+ priority,
586
+ );
587
+
588
+ _isSwapsXYZCallDataTx = true;
589
+ state = ExecutedSuccessfullyState();
590
+ return pendingTransaction; // do NOT fall back to regular flow
591
+ }
592
+ if (walletType == WalletType.base) {
593
+ final priority = _settingsStore.priority[WalletType.base]!;
594
+ _pendingApprovalTx = await buildApprovalIfNeeded(
595
+ spender: routerTo!,
596
+ tokenContract: tokenContract,
597
+ requiredAmount: requiredAmount,
598
+ sourceTokenDecimals: trade.sourceTokenDecimals,
599
+ );
600
+
601
+ // Build the callData tx
602
+ pendingTransaction = await base!.createRawCallDataTransaction(
603
+ wallet,
604
+ routerTo,
605
+ routerData,
606
+ routerValueWei,
607
+ priority,
608
+ );
609
+
610
+ _isSwapsXYZCallDataTx = true;
611
+ state = ExecutedSuccessfullyState();
612
+ return pendingTransaction; // do NOT fall back to regular flow
613
+ }
614
+ }
615
+
616
+ // No approval needed (or prepared transfer): send exactly what backend prepared
617
+ if (walletType == WalletType.ethereum) {
618
+ final priority = _settingsStore.priority[WalletType.ethereum]!;
619
+ pendingTransaction = await ethereum!.createRawCallDataTransaction(
620
+ wallet,
621
+ routerTo!,
622
+ routerData,
623
+ routerValueWei,
624
+ priority,
625
+ );
626
+ _isSwapsXYZCallDataTx = true;
627
+ state = ExecutedSuccessfullyState();
628
+ return pendingTransaction;
629
+ }
630
+ if (walletType == WalletType.polygon) {
631
+ final priority = _settingsStore.priority[WalletType.polygon]!;
632
+ pendingTransaction = await polygon!.createRawCallDataTransaction(
633
+ wallet,
634
+ routerTo!,
635
+ routerData,
636
+ routerValueWei,
637
+ priority,
638
+ );
639
+ _isSwapsXYZCallDataTx = true;
640
+ state = ExecutedSuccessfullyState();
641
+ return pendingTransaction;
642
+ }
643
+ if (walletType == WalletType.base) {
644
+ final priority = _settingsStore.priority[WalletType.polygon]!;
645
+ pendingTransaction = await base!.createRawCallDataTransaction(
646
+ wallet,
647
+ routerTo!,
648
+ routerData,
649
+ routerValueWei,
650
+ priority,
651
+ );
652
+ _isSwapsXYZCallDataTx = true;
653
+ state = ExecutedSuccessfullyState();
654
+ return pendingTransaction;
655
+ }
656
+ }
657
+ }
658
+
659
+
660
+ // Regular flow
661
+
662
pendingTransaction = await wallet.createTransaction(_credentials(provider));
663
664
if (provider is ThorChainExchangeProvider) {
@@ -602,6 +756,37 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
756
return;
757
}
758
759
+ // Swaps.xyz approval (if any), then commit the prebuilt router tx
760
+ if (_isSwapsXYZCallDataTx) {
761
+ if (_pendingApprovalTx != null) {
762
+ await _pendingApprovalTx!.commit();
763
+ _pendingApprovalTx = null;
764
+ // Small pause to ensure allowance is indexed
765
+ await Future.delayed(const Duration(milliseconds: 300));
766
+ }
767
+
768
+ await pendingTransaction!.commit();
769
+ _isSwapsXYZCallDataTx = false;
770
+
771
+ state = TransactionCommitted();
772
+ return; // skip the regular flow below
773
+ }
774
+
775
+ // Regular flow (non-Swaps)
776
+ if (pendingTransaction!.shouldCommitUR()) {
777
+ final urstr = await pendingTransaction!.commitUR();
778
+ final result = await Navigator.of(context).pushNamed(
779
+ Routes.urqrAnimatedPage,
780
+ arguments: urstr,
781
+ );
782
+ if (result == null) {
783
+ state = FailureState("Canceled by user");
784
+ return;
785
+ }
786
+ } else {
787
+ await pendingTransaction!.commit();
788
+ }
789
+
790
String address = outputs.fold('', (acc, value) {
791
return value.isParsedAddress
792
? '$acc${value.address}\n${value.extractedAddress}\n\n'
@@ -933,6 +1118,71 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1118
return false;
1119
}
1120
1121
+ Future<PendingTransaction?> buildApprovalIfNeeded({
1122
+ required String spender,
1123
+ required String tokenContract,
1124
+ required BigInt requiredAmount,
1125
+ int? sourceTokenDecimals,
1126
+ }) async {
1127
+
1128
+ // Only EVM chains support ERC20 approvals
1129
+ if (!isEVMWallet) return null;
1130
+
1131
+ const zero = '0x0000000000000000000000000000000000000000';
1132
+ const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
1133
+
1134
+ final tokenLc = tokenContract.toLowerCase();
1135
+ if (tokenLc == zero || tokenLc == evmNative.toLowerCase()) return null;
1136
+ if (requiredAmount <= BigInt.zero) return null;
1137
+
1138
+ bool needsApproval = false;
1139
+ if (walletType == WalletType.ethereum) {
1140
+ needsApproval = await ethereum!.isApprovalRequired(
1141
+ wallet, tokenContract, spender, requiredAmount,
1142
+ );
1143
+ } else if (walletType == WalletType.polygon) {
1144
+ needsApproval = await polygon!.isApprovalRequired(
1145
+ wallet, tokenContract, spender, requiredAmount,
1146
+ );
1147
+ } else if (walletType == WalletType.base) {
1148
+ needsApproval = await base!.isApprovalRequired(
1149
+ wallet, tokenContract, spender, requiredAmount,
1150
+ );
1151
+ }
1152
+
1153
+ if (!needsApproval) return null;
1154
+
1155
+ final erc20Token = wallet.balance.keys.whereType<Erc20Token>().firstWhere(
1156
+ (t) => t.contractAddress.toLowerCase() == tokenLc,
1157
+ orElse: () => Erc20Token(
1158
+ name: '',
1159
+ symbol: '',
1160
+ contractAddress: tokenContract,
1161
+ decimal: sourceTokenDecimals ?? 18,
1162
+ enabled: true,
1163
+ ),
1164
+ );
1165
+
1166
+ if (walletType == WalletType.ethereum) {
1167
+ final priority = _settingsStore.priority[WalletType.ethereum]!;
1168
+ return await ethereum!.createTokenApproval(
1169
+ wallet, requiredAmount, spender, erc20Token, priority,
1170
+ );
1171
+ } else if (walletType == WalletType.polygon) {
1172
+ final priority = _settingsStore.priority[WalletType.polygon]!;
1173
+ return await polygon!.createTokenApproval(
1174
+ wallet, requiredAmount, spender, erc20Token, priority,
1175
+ );
1176
+ } else if (walletType == WalletType.base) {
1177
+ final priority = _settingsStore.priority[WalletType.base]!;
1178
+ return await base!.createTokenApproval(
1179
+ wallet, requiredAmount, spender, erc20Token, priority,
1180
+ );
1181
+ }
1182
+
1183
+ return null;
1184
+ }
1185
+
1186
@computed
1187
bool get usePayjoin => _settingsStore.usePayjoin;
1188
lib/view_model/trade_details_view_model.dart
+15
-1
@@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
6
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
7
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
8
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
9
+import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
10
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
11
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
12
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
@@ -77,6 +78,9 @@ abstract class TradeDetailsViewModelBase with Store {
78
case ExchangeProviderDescription.xoSwap:
79
_provider = XOSwapExchangeProvider();
80
break;
81
+ case ExchangeProviderDescription.swapsXyz:
82
+ _provider = SwapsXyzExchangeProvider();
83
+ break;
84
}
85
86
_updateItems();
@@ -134,8 +138,13 @@ abstract class TradeDetailsViewModelBase with Store {
138
try {
139
final updatedTrade = await _provider!.findTradeById(id: trade.id);
140
137
- if (updatedTrade.createdAt == null && trade.createdAt != null)
141
+ if (updatedTrade.createdAt == null && trade.createdAt != null) {
142
updatedTrade.createdAt = trade.createdAt;
143
+ }
144
+
145
+ if (updatedTrade.toRaw == -1 && trade.toRaw != -1) {
146
+ updatedTrade.toRaw = trade.toRaw;
147
+ }
148
149
Trade? foundElement = trades.values.firstWhereOrNull((element) => element.id == trade.id);
150
if (foundElement != null) {
@@ -205,6 +214,11 @@ abstract class TradeDetailsViewModelBase with Store {
214
title: '${trade.providerName} ${S.current.password}', value: trade.password ?? ''));
215
}
216
}
217
+
218
+ if (trade.provider == ExchangeProviderDescription.swapsXyz && trade.txId != null && trade.txId!.isNotEmpty) {
219
+ items.add(StandartListItem(
220
+ title: 'Transaction ID', value: trade.txId!));
221
+ }
222
}
223
224
void _launchUrl(String url) {
tool/configure.dart
+6
@@ -776,7 +776,9 @@ abstract class Ethereum {
776
Web3Client? getWeb3Client(WalletBase wallet);
777
String getTokenAddress(CryptoCurrency asset);
778
779
+ Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount);
780
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority);
781
+ Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, TransactionPriority priority);
782
783
Future<BigInt> getDEuroSavingsBalance(WalletBase wallet);
784
Future<BigInt> getDEuroAccruedInterest(WalletBase wallet);
@@ -899,7 +901,9 @@ abstract class Polygon {
901
Future<void> removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token);
902
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress);
903
904
+ Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount);
905
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority);
906
+ Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, TransactionPriority priority);
907
908
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
909
void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled);
@@ -1595,8 +1599,10 @@ abstract class Base {
1599
Future<void> removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token);
1600
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress);
1601
1602
+ Future<bool> isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount);
1603
Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender,
1604
CryptoCurrency token, TransactionPriority priority);
1605
+ Future<PendingTransaction> createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, TransactionPriority priority);
1606
1607
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
1608
void updateBaseScanUsageState(WalletBase wallet, bool isEnabled);
tool/utils/secret_key.dart
+1
@@ -82,6 +82,7 @@ class SecretKey {
82
SecretKey('chainflipAffiliateFee', () => ''),
83
SecretKey('kryptonimApiKey', () => ''),
84
SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)),
85
+ SecretKey('swapsXyzApiKey', () => ''),
86
];
87
88
static final evmChainsSecrets = [