CW-1175-Implement-swaps.xyz-swap-provider (#2526)
What changed, and why it matters
This commit adds a new cryptocurrency swap provider called Swaps.XYZ to the Cake Wallet app. It also adds the ability to send pre-built EVM (Ethereum, Polygon, Base) transactions with custom data, and to approve ERC-20 token spending before those swaps. The changes are broad but appear to be a normal feature integration. There is no direct evidence in the commit of a security vulnerability, but the new code paths handle real money transactions and rely on an external API, which always carries some risk.
Treat this as a feature commit that expands the trusted-computing base. Reviewers should audit: (1) whether api-v2.swaps.xyz responses are validated before being turned into transactions; (2) whether routerData/to/value are authenticated or could be swapped by a malicious or compromised API; (3) whether the token approval spender matches the intended router and is not attacker-controlled; (4) whether the new BigInt/base-unit conversions handle edge cases and negative values safely; and (5) whether the new Hive fields in Trade are backward-compatible. No immediate patch is indicated by the diff alone, but a focused security review of the new provider and EVM raw-tx path is warranted before release.
Security signals we found
New third-party API integration (api-v2.swaps.xyz) with API key authentication
Raw EVM call-data transaction creation path added to Ethereum, Polygon, and Base wallets
ERC-20 token approval helper added with spender/amount derived from trade object
Trade object extended with new serialized fields for router data, value, chainId, and approval flags
CI workflows updated to inject a new secret (SWAPSXYZ_API_KEY) into generated secrets file
Transaction hash pre-computed from raw hex for Swaps.XYZ alt-vm registration
Evidence from the diff
The commit integrates Swaps.XYZ as a new exchange provider. It introduces: (1) a new provider class that calls api-v2.swaps.xyz endpoints using an API key stored in secrets; (2) generic base-unit conversion helpers in cw_core; (3) EVM wallet methods to create raw call-data transactions and check ERC-20 allowances; (4) UI/UX wiring to display, confirm, and register Swaps.XYZ trades; and (5) a new API key secret injected in CI workflows. The code touches transaction construction, approval logic, and serialization of Trade objects. No explicit security bug is visible in the diff, but the new surface area includes external API trust, raw transaction data execution, and token approval flows.
Changed components
lib/exchange/provider/swapsxyz_exchange_provider.dartlib/view_model/send/send_view_model.dartlib/view_model/exchange/exchange_trade_view_model.dartcw_evm/lib/evm_chain_wallet.dartcw_evm/lib/evm_chain_client.dartcw_evm/lib/pending_evm_chain_transaction.dartlib/exchange/trade.dartlib/ethereum/cw_ethereum.dartlib/polygon/cw_polygon.dartlib/base/cw_base.darttool/configure.dart.github/workflows/automated_integration_test.yml.github/workflows/pr_test_build_android.yml.github/workflows/pr_test_build_linux.ymlInspect captured patch +1557 / −77
diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml
index b64897c..5ccf4aa 100644
--- a/.github/workflows/automated_integration_test.yml
+++ b/.github/workflows/automated_integration_test.yml
@@ -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: |
diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml
index 521cc6e..cd7cd40 100644
--- a/.github/workflows/pr_test_build_android.yml
+++ b/.github/workflows/pr_test_build_android.yml
@@ -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: |
diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml
index 6d39bb8..0365ec2 100644
--- a/.github/workflows/pr_test_build_linux.yml
+++ b/.github/workflows/pr_test_build_linux.yml
@@ -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: |
diff --git a/assets/images/swaps_xyz.svg b/assets/images/swaps_xyz.svg
new file mode 100644
index 0000000..e65d157
--- /dev/null
+++ b/assets/images/swaps_xyz.svg
@@ -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>
\ No newline at end of file
diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart
index 8a095df..3c48cde 100644
--- a/cw_core/lib/amount_converter.dart
+++ b/cw_core/lib/amount_converter.dart
@@ -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;
+ }
}
diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart
index 599047f..78cd4c8 100644
--- a/cw_core/lib/pending_transaction.dart
+++ b/cw_core/lib/pending_transaction.dart
@@ -12,6 +12,7 @@ mixin PendingTransaction {
String get feeFormattedValue;
String? feeRate;
String get hex;
+ String? get evmTxHashFromRawHex => null;
int? get outputCount => null;
PendingChange? change;
diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart
index 814979d..fbc4bad 100644
--- a/cw_evm/lib/evm_chain_client.dart
+++ b/cw_evm/lib/evm_chain_client.dart
@@ -111,6 +111,7 @@ abstract class EVMChainClient {
sender: senderAddress,
to: toAddress,
value: value,
+ data: data,
);
return estimatedGas.toInt();
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index b752aef..c1c3754 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -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,
diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart
index 50213d6..3e9ad39 100644
--- a/cw_evm/lib/pending_evm_chain_transaction.dart
+++ b/cw_evm/lib/pending_evm_chain_transaction.dart
@@ -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();
diff --git a/lib/base/cw_base.dart b/lib/base/cw_base.dart
index 02b9669..a3bbe53 100644
--- a/lib/base/cw_base.dart
+++ b/lib/base/cw_base.dart
@@ -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) {
diff --git a/lib/core/trade_monitor.dart b/lib/core/trade_monitor.dart
index 408f7fa..cacc8ef 100644
--- a/lib/core/trade_monitor.dart
+++ b/lib/core/trade_monitor.dart
@@ -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;
}
diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart
index 840f350..92d8bbe 100644
--- a/lib/ethereum/cw_ethereum.dart
+++ b/lib/ethereum/cw_ethereum.dart
@@ -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) =>
diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart
index b3761ab..d542ff3 100644
--- a/lib/exchange/exchange_provider_description.dart
+++ b/lib/exchange/exchange_provider_description.dart
@@ -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');
}
diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart
index de1642a..cbd9fa8 100644
--- a/lib/exchange/provider/chainflip_exchange_provider.dart
+++ b/lib/exchange/provider/chainflip_exchange_provider.dart
@@ -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 {
diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart
index 5a01f38..8f6e7bc 100644
--- a/lib/exchange/provider/changenow_exchange_provider.dart
+++ b/lib/exchange/provider/changenow_exchange_provider.dart
@@ -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;
diff --git a/lib/exchange/provider/exchange_provider.dart b/lib/exchange/provider/exchange_provider.dart
index a91a7ac..45c61d7 100644
--- a/lib/exchange/provider/exchange_provider.dart
+++ b/lib/exchange/provider/exchange_provider.dart
@@ -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();
}
diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart
index fa0c16f..252222b 100644
--- a/lib/exchange/provider/exolix_exchange_provider.dart
+++ b/lib/exchange/provider/exolix_exchange_provider.dart
@@ -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;
diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart
index 1841dff..d37c6a2 100644
--- a/lib/exchange/provider/letsexchange_exchange_provider.dart
+++ b/lib/exchange/provider/letsexchange_exchange_provider.dart
@@ -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 {
diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart
index 7727f48..763064e 100644
--- a/lib/exchange/provider/sideshift_exchange_provider.dart
+++ b/lib/exchange/provider/sideshift_exchange_provider.dart
@@ -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;
diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart
index e88562b..1118e7b 100644
--- a/lib/exchange/provider/simpleswap_exchange_provider.dart
+++ b/lib/exchange/provider/simpleswap_exchange_provider.dart
@@ -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;
diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart
new file mode 100644
index 0000000..6452b34
--- /dev/null
+++ b/lib/exchange/provider/swapsxyz_exchange_provider.dart
@@ -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});
+}
diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart
index a7e5045..539c026 100644
--- a/lib/exchange/provider/swaptrade_exchange_provider.dart
+++ b/lib/exchange/provider/swaptrade_exchange_provider.dart
@@ -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;
diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart
index d3673eb..023b021 100644
--- a/lib/exchange/provider/thorchain_exchange.provider.dart
+++ b/lib/exchange/provider/thorchain_exchange.provider.dart
@@ -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;
diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart
index b9ac094..65dcfe0 100644
--- a/lib/exchange/provider/trocador_exchange_provider.dart
+++ b/lib/exchange/provider/trocador_exchange_provider.dart
@@ -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;
diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart
index 6d25171..b9d4aed 100644
--- a/lib/exchange/provider/xoswap_exchange_provider.dart
+++ b/lib/exchange/provider/xoswap_exchange_provider.dart
@@ -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);
diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart
index b22a64f..68248ba 100644
--- a/lib/exchange/trade.dart
+++ b/lib/exchange/trade.dart
@@ -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
diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart
index e04e81f..e9bf9a9 100644
--- a/lib/polygon/cw_polygon.dart
+++ b/lib/polygon/cw_polygon.dart
@@ -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,
diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart
index 948dac4..dbb99fc 100644
--- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart
+++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart
@@ -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,
+ ),
+ ),
),
],
),
diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart
index 840fbcf..67f05c7 100644
--- a/lib/src/screens/exchange_trade/exchange_trade_page.dart
+++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart
@@ -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();
},
);
},
diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart
index e3212fd..16be01b 100644
--- a/lib/store/dashboard/trade_filter_store.dart
+++ b/lib/store/dashboard/trade_filter_store.dart
@@ -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;
}
diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart
index 265a483..fc4ef7a 100644
--- a/lib/view_model/dashboard/dashboard_view_model.dart
+++ b/lib/view_model/dashboard/dashboard_view_model.dart
@@ -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 = '',
diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart
index 635bf5c..8fdf029 100644
--- a/lib/view_model/exchange/exchange_trade_view_model.dart
+++ b/lib/view_model/exchange/exchange_trade_view_model.dart
@@ -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;
diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart
index 4ee5117..0642362 100644
--- a/lib/view_model/exchange/exchange_view_model.dart
+++ b/lib/view_model/exchange/exchange_view_model.dart
@@ -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),
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index 55bfabb..4ba87f9 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -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;
diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart
index 64c05c1..3b42b12 100644
--- a/lib/view_model/trade_details_view_model.dart
+++ b/lib/view_model/trade_details_view_model.dart
@@ -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) {
diff --git a/tool/configure.dart b/tool/configure.dart
index 50930c5..09c37e1 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -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);
diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart
index 6a009ff..f6e5018 100644
--- a/tool/utils/secret_key.dart
+++ b/tool/utils/secret_key.dart
@@ -82,6 +82,7 @@ class SecretKey {
SecretKey('chainflipAffiliateFee', () => ''),
SecretKey('kryptonimApiKey', () => ''),
SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)),
+ SecretKey('swapsXyzApiKey', () => ''),
];
static final evmChainsSecrets = [
Why this scored 38/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.