diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml
index b64897c06..5ccf4aacb 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 521cc6ecd..cd7cd400a 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 6d39bb85e..0365ec2d8 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 000000000..e65d157da
--- /dev/null
+++ b/assets/images/swaps_xyz.svg
@@ -0,0 +1,42 @@
+
+
\ No newline at end of file
diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart
index 8a095dffa..3c48cde09 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 599047fbd..78cd4c874 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 814979d6d..fbc4bad95 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 b752aefd4..c1c375482 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 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 createApprovalTransaction(BigInt amount, String spender,
CryptoCurrency token, EVMChainTransactionPriority priority, String feeCurrency) async {
final CryptoCurrency transactionCurrency =
@@ -793,6 +838,37 @@ abstract class EVMChainWalletBase
}
}
+ Future 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 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 50213d6a4..3e9ad39f2 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