diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml
index 1ed5baf9f..84c680dda 100644
--- a/.github/workflows/automated_integration_test.yml
+++ b/.github/workflows/automated_integration_test.yml
@@ -12,7 +12,7 @@ on:
jobs:
Automated_integration_test:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml
index b1c356c31..9c3d82bc2 100644
--- a/.github/workflows/no_print_in_dart.yaml
+++ b/.github/workflows/no_print_in_dart.yaml
@@ -4,7 +4,7 @@ on: [pull_request]
jobs:
PR_test_build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/no_restricted_imports.yaml b/.github/workflows/no_restricted_imports.yaml
index 4b17de31a..03c3de018 100644
--- a/.github/workflows/no_restricted_imports.yaml
+++ b/.github/workflows/no_restricted_imports.yaml
@@ -4,7 +4,7 @@ on: [pull_request]
jobs:
check_restricted_imports:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml
index 08a283467..8f6139747 100644
--- a/.github/workflows/pr_test_build_android.yml
+++ b/.github/workflows/pr_test_build_android.yml
@@ -274,7 +274,7 @@ jobs:
- name: Build
run: |
- flutter build apk --release --split-per-abi
+ flutter build apk --dart-define=hasDevOptions=true --release --split-per-abi
- name: Rename apk file
run: |
diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml
index bf55134ad..476a033a0 100644
--- a/.github/workflows/pr_test_build_linux.yml
+++ b/.github/workflows/pr_test_build_linux.yml
@@ -225,7 +225,7 @@ jobs:
- name: Build linux
run: |
- flutter build linux --release
+ flutter build linux --dart-define=hasDevOptions=true --release
- name: Compress release
run: |
@@ -283,6 +283,9 @@ jobs:
xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart
+ - name: Test [cw_monero]
+ timeout-minutes: 2
+ run: cd cw_monero && flutter test
- name: Stop screen recording, encrypt and upload
if: always()
run: |
diff --git a/.gitignore b/.gitignore
index 37db583e5..83e6d03e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,9 +142,28 @@ lib/zano/zano.dart
lib/decred/decred.dart
lib/tari/tari.dart
-ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
-ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
-ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png
+ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png
+
ios/Runner/Info.plist
android/app/src/main/res/mipmap-*
android/app/src/main/res/drawable/ic_launcher.png
diff --git a/README.md b/README.md
index ea8f34624..ea796dbf2 100644
--- a/README.md
+++ b/README.md
@@ -26,10 +26,13 @@ Cake Wallet includes support for several cryptocurrencies, including:
* Ethereum (ETH)
* Litecoin (LTC)
* Bitcoin Cash (BCH)
-* Polygon (Pol)
+* Polygon (POL)
* Solana (SOL)
+* Tron (TRX)
* Nano (XNO)
-* Haven (XHV)
+* Zano (ZANO)
+* Decred (DCR)
+* Wownero (WOW)
## Features
@@ -81,10 +84,6 @@ Cake Wallet includes support for several cryptocurrencies, including:
* Automatically generate new addresses
* Specify multiple recipients for batch sending
-### Haven Specific Features
-
-* Send, receive, and store XHV and all xAssets like xUSD, xEUR, xAG, etc.
-
# Monero.com by Cake Wallet for Android and iOS
## Open Source Monero-Only Wallet
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c45ed9368..6c299c929 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -74,9 +74,6 @@ android {
release {
signingConfig signingConfigs.release
- shrinkResources false
- minifyEnabled false
-
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index d24d7f10a..921ee4d4c 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -5,4 +5,5 @@
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
--dontwarn io.flutter.embedding.**
\ No newline at end of file
+-dontwarn io.flutter.embedding.**
+-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index 9a324edf3..4f15370c3 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -107,6 +107,9 @@
+
_enabled;
@@ -33,14 +35,17 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
bool enabled = true,
this.iconPath,
this.tag,
+ this.isPotentialScam = false,
}) : _enabled = enabled,
super(
- name: symbol.toLowerCase(),
- title: symbol.toUpperCase(),
- fullName: name,
- tag: tag,
- iconPath: iconPath,
- decimals: decimal);
+ name: symbol.toLowerCase(),
+ title: symbol.toUpperCase(),
+ fullName: name,
+ tag: tag,
+ iconPath: iconPath,
+ decimals: decimal,
+ isPotentialScam: isPotentialScam,
+ );
Erc20Token.copyWith(Erc20Token other, String? icon, String? tag)
: this.name = other.name,
@@ -50,6 +55,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
this._enabled = other.enabled,
this.tag = tag,
this.iconPath = icon,
+ this.isPotentialScam = other.isPotentialScam,
super(
name: other.name,
title: other.symbol.toUpperCase(),
@@ -57,6 +63,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
tag: tag,
iconPath: icon,
decimals: other.decimal,
+ isPotentialScam: other.isPotentialScam,
);
static const typeId = ERC20_TOKEN_TYPE_ID;
diff --git a/cw_core/lib/monero_wallet_utils.dart b/cw_core/lib/monero_wallet_utils.dart
index 8a4990f78..9682784f9 100644
--- a/cw_core/lib/monero_wallet_utils.dart
+++ b/cw_core/lib/monero_wallet_utils.dart
@@ -19,15 +19,15 @@ Future backupWalletFiles(String name) async {
final newKeysFilePath = backupFileName(keysFile.path);
final newAddressListFilePath = backupFileName(addressListFile.path);
- if (cacheFile.existsSync()) {
+ if (cacheFile.existsSync() && !File(newCacheFilePath).existsSync()) {
await cacheFile.copy(newCacheFilePath);
}
- if (keysFile.existsSync()) {
+ if (keysFile.existsSync() && !File(newKeysFilePath).existsSync()) {
await keysFile.copy(newKeysFilePath);
}
- if (addressListFile.existsSync()) {
+ if (addressListFile.existsSync() && !File(newAddressListFilePath).existsSync()) {
await addressListFile.copy(newAddressListFilePath);
}
}
@@ -83,10 +83,13 @@ Future backupWalletFilesExists(String name) async {
Future removeCache(String name) async {
final path = await pathForWallet(name: name, type: WalletType.monero);
final cacheFile = File(path);
-
+ final backgroundCacheFile = File(path + ".background");
if (cacheFile.existsSync()) {
cacheFile.deleteSync();
}
+ if (backgroundCacheFile.existsSync()) {
+ backgroundCacheFile.deleteSync();
+ }
}
Future restoreOrResetWalletFiles(String name) async {
@@ -94,7 +97,8 @@ Future restoreOrResetWalletFiles(String name) async {
if (backupsExists) {
await removeCache(name);
-
+ // TODO(mrcyjanek): is this needed?
+ // If we remove cache then wallet should be restored from .keys file.
await restoreWalletFiles(name);
}
}
diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart
index da71f6983..31820e3a1 100644
--- a/cw_core/lib/unspent_transaction_output.dart
+++ b/cw_core/lib/unspent_transaction_output.dart
@@ -21,4 +21,9 @@ class Unspent with UnspentComparable {
bool get isP2wpkh =>
address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc');
+
+ @override
+ String toString() {
+ return 'Unspent(address: $address, hash: $hash, value: $value, vout: $vout, keyImage: $keyImage, isSending: $isSending, isFrozen: $isFrozen, isChange: $isChange, note: $note)';
+ }
}
diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart
index 0686e58c9..46f0b2163 100644
--- a/cw_core/lib/wallet_type.dart
+++ b/cw_core/lib/wallet_type.dart
@@ -202,7 +202,7 @@ String walletTypeToDisplayName(WalletType type) {
case WalletType.banano:
return 'Banano (BAN)';
case WalletType.polygon:
- return 'Polygon (MATIC)';
+ return 'Polygon (POL)';
case WalletType.solana:
return 'Solana (SOL)';
case WalletType.tron:
@@ -260,3 +260,38 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal
'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
}
}
+
+WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) {
+ switch (type) {
+ case CryptoCurrency.xmr:
+ return WalletType.monero;
+ case CryptoCurrency.btc:
+ return WalletType.bitcoin;
+ case CryptoCurrency.ltc:
+ return WalletType.litecoin;
+ case CryptoCurrency.xhv:
+ return WalletType.haven;
+ case CryptoCurrency.eth:
+ return WalletType.ethereum;
+ case CryptoCurrency.bch:
+ return WalletType.bitcoinCash;
+ case CryptoCurrency.nano:
+ return WalletType.nano;
+ case CryptoCurrency.banano:
+ return WalletType.banano;
+ case CryptoCurrency.maticpoly:
+ return WalletType.polygon;
+ case CryptoCurrency.sol:
+ return WalletType.solana;
+ case CryptoCurrency.trx:
+ return WalletType.tron;
+ case CryptoCurrency.wow:
+ return WalletType.wownero;
+ case CryptoCurrency.zano:
+ return WalletType.zano;
+ case CryptoCurrency.dcr:
+ return WalletType.decred;
+ default:
+ return null;
+ }
+}
diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart
index db30ab373..a63a5e2e5 100644
--- a/cw_decred/lib/wallet.dart
+++ b/cw_decred/lib/wallet.dart
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_decred/amount_format.dart';
import 'package:cw_decred/pending_transaction.dart';
import 'package:cw_decred/transaction_credentials.dart';
import 'package:flutter/foundation.dart';
@@ -122,6 +123,9 @@ abstract class DecredWalletBase
return _pubkey;
}
+ @override
+ String formatCryptoAmount(String amount) => decredAmountToString(amount: int.parse(amount));
+
Future init() async {
final getSeed = () async {
if (!watchingOnly) {
@@ -218,7 +222,7 @@ abstract class DecredWalletBase
Future checkSync() async {
final syncStatusJSON = await _libwallet.syncStatus(walletInfo.name);
- final decoded = json.decode(syncStatusJSON);
+ final decoded = json.decode(syncStatusJSON.isEmpty ? "{}" : syncStatusJSON);
final syncStatusCode = decoded["syncstatuscode"] ?? 0;
// final syncStatusStr = decoded["syncstatus"] ?? "";
@@ -706,14 +710,18 @@ abstract class DecredWalletBase
// walletBirthdayBlockHeight checks if the wallet birthday is set and returns
// it. Returns -1 if not.
Future walletBirthdayBlockHeight() async {
- final res = await _libwallet.birthState(walletInfo.name);
- final decoded = json.decode(res);
- // Having these values set indicates that sync has not reached the birthday
- // yet, so no birthday is set.
- if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) {
- return -1;
+ try {
+ final res = await _libwallet.birthState(walletInfo.name);
+ final decoded = json.decode(res);
+ // Having these values set indicates that sync has not reached the birthday
+ // yet, so no birthday is set.
+ if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) {
+ return -1;
+ }
+ return decoded["height"] ?? 0;
+ } on FormatException catch (_) {
+ return 0;
}
- return decoded["height"] ?? 0;
}
Future verifyMessage(String message, String signature, {String? address = null}) async {
diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart
index 765ace052..7cc140c5a 100644
--- a/cw_ethereum/lib/ethereum_wallet.dart
+++ b/cw_ethereum/lib/ethereum_wallet.dart
@@ -76,9 +76,13 @@ class EthereumWallet extends EVMChainWallet {
await erc20TokensBox.deleteFromDisk();
// Add all the previous tokens with configs to the new box
- evmChainErc20TokensBox.addAll(allValues);
+ await evmChainErc20TokensBox.addAll(allValues);
}
+ @override
+ List get getDefaultTokenContractAddresses =>
+ DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList();
+
@override
EVMChainTransactionInfo getTransactionInfo(
EVMChainTransactionModel transactionModel, String address) {
@@ -115,6 +119,7 @@ class EthereumWallet extends EVMChainWallet {
enabled: token.enabled,
tag: token.tag ?? "ETH",
iconPath: iconPath,
+ isPotentialScam: token.isPotentialScam,
);
}
diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart
index b505577e9..7e6caf374 100644
--- a/cw_evm/lib/evm_chain_client.dart
+++ b/cw_evm/lib/evm_chain_client.dart
@@ -190,7 +190,7 @@ abstract class EVMChainClient {
_sendTransaction = () async => await sendTransaction(signedTransaction);
return PendingEVMChainTransaction(
- signedTransaction: signedTransaction,
+ signedTransaction: prepareSignedTransactionForSending(signedTransaction),
amount: amount.toString(),
fee: gasFee,
sendTransaction: _sendTransaction,
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index d3efdaa7c..d640f8c14 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -144,6 +144,8 @@ abstract class EVMChainWalletBase
// required WalletInfo walletInfo,
// });
+ List get getDefaultTokenContractAddresses;
+
Future initErc20TokensBox();
String getTransactionHistoryFileName();
@@ -171,6 +173,9 @@ abstract class EVMChainWalletBase
await walletAddresses.init();
await transactionHistory.init();
+ // check for Already existing scam tokens, cuz users can get scammed twice ¯\_(ツ)_/¯
+ await _checkForExistingScamTokens();
+
if (walletInfo.isHardwareWallet) {
_evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address);
walletAddresses.address = walletInfo.address;
@@ -186,6 +191,31 @@ abstract class EVMChainWalletBase
await save();
}
+ Future _checkForExistingScamTokens() async {
+ final baseCurrencySymbols = CryptoCurrency.all.map((e) => e.title.toUpperCase()).toList();
+
+ for (var token in erc20Currencies) {
+ bool isPotentialScam = false;
+
+ bool isWhitelisted =
+ getDefaultTokenContractAddresses.any((element) => element == token.contractAddress);
+
+ final tokenSymbol = token.title.toUpperCase();
+
+ // check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc):
+ // if it is, then it's probably a scam unless it's in the whitelist
+ if (baseCurrencySymbols.contains(tokenSymbol.trim().toUpperCase()) && !isWhitelisted) {
+ isPotentialScam = true;
+ }
+
+ if (isPotentialScam) {
+ token.isPotentialScam = true;
+ token.iconPath = null;
+ await token.save();
+ }
+ }
+ }
+
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
{
@@ -626,13 +656,13 @@ abstract class EVMChainWalletBase
Future addErc20Token(Erc20Token token) async {
String? iconPath;
- if (token.iconPath == null || token.iconPath!.isEmpty) {
+ if ((token.iconPath == null || token.iconPath!.isEmpty) && !token.isPotentialScam) {
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
- } else {
+ } else if (!token.isPotentialScam) {
iconPath = token.iconPath;
}
diff --git a/cw_monero/.gitignore b/cw_monero/.gitignore
index ebb19df82..2bf094c85 100644
--- a/cw_monero/.gitignore
+++ b/cw_monero/.gitignore
@@ -11,4 +11,5 @@ android/.externalNativeBuild/
android/.cxx/
macos/cw_monero.podspec
-macos/External/
\ No newline at end of file
+macos/External/
+*monero_libwallet2_api_c.*
\ No newline at end of file
diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart
index 0e55ce15c..3ceef5815 100644
--- a/cw_monero/lib/api/account_list.dart
+++ b/cw_monero/lib/api/account_list.dart
@@ -1,4 +1,7 @@
+import 'dart:async';
+
import 'package:cw_monero/api/wallet.dart';
+import 'package:cw_monero/monero_account_list.dart';
import 'package:monero/monero.dart' as monero;
monero.wallet? wptr = null;
@@ -7,14 +10,14 @@ bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;
int _wlptrForW = 0;
monero.WalletListener? _wlptr = null;
-monero.WalletListener getWlptr() {
+monero.WalletListener? getWlptr() {
+ if (wptr == null) return null;
if (wptr!.address == _wlptrForW) return _wlptr!;
_wlptrForW = wptr!.address;
_wlptr = monero.MONERO_cw_getWalletListener(wptr!);
return _wlptr!;
}
-
monero.SubaddressAccount? subaddressAccount;
bool isUpdating = false;
@@ -50,8 +53,9 @@ void addAccountSync({required String label}) {
}
void setLabelForAccountSync({required int accountIndex, required String label}) {
- // TODO(mrcyjanek): this may be wrong function?
- monero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: 0, label: label);
+ monero.SubaddressAccount_setLabel(subaddressAccount!, accountIndex: accountIndex, label: label);
+ MoneroAccountListBase.cachedAccounts[wptr!.address] = [];
+ refreshAccounts();
}
void _addAccount(String label) => addAccountSync(label: label);
@@ -65,10 +69,10 @@ void _setLabelForAccount(Map args) {
Future addAccount({required String label}) async {
_addAccount(label);
- await store();
+ unawaited(store());
}
Future setLabelForAccount({required int accountIndex, required String label}) async {
_setLabelForAccount({'accountIndex': accountIndex, 'label': label});
- await store();
+ unawaited(store());
}
\ No newline at end of file
diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart
index ef7d3cfd6..83382f001 100644
--- a/cw_monero/lib/api/coins_info.dart
+++ b/cw_monero/lib/api/coins_info.dart
@@ -1,21 +1,42 @@
+import 'dart:ffi';
+import 'dart:isolate';
+
import 'package:cw_monero/api/account_list.dart';
import 'package:monero/monero.dart' as monero;
+import 'package:mutex/mutex.dart';
monero.Coins? coins = null;
+final coinsMutex = Mutex();
-void refreshCoins(int accountIndex) {
+Future refreshCoins(int accountIndex) async {
+ if (coinsMutex.isLocked) {
+ return;
+ }
coins = monero.Wallet_coins(wptr!);
- monero.Coins_refresh(coins!);
+ final coinsPtr = coins!.address;
+ await coinsMutex.acquire();
+ await Isolate.run(() => monero.Coins_refresh(Pointer.fromAddress(coinsPtr)));
+ coinsMutex.release();
}
-int countOfCoins() => monero.Coins_count(coins!);
+Future countOfCoins() async {
+ await coinsMutex.acquire();
+ final count = monero.Coins_count(coins!);
+ coinsMutex.release();
+ return count;
+}
-monero.CoinsInfo getCoin(int index) => monero.Coins_coin(coins!, index);
+Future getCoin(int index) async {
+ await coinsMutex.acquire();
+ final coin = monero.Coins_coin(coins!, index);
+ coinsMutex.release();
+ return coin;
+}
-int? getCoinByKeyImage(String keyImage) {
- final count = countOfCoins();
+Future getCoinByKeyImage(String keyImage) async {
+ final count = await countOfCoins();
for (int i = 0; i < count; i++) {
- final coin = getCoin(i);
+ final coin = await getCoin(i);
final coinAddress = monero.CoinsInfo_keyImage(coin);
if (keyImage == coinAddress) {
return i;
@@ -24,6 +45,16 @@ int? getCoinByKeyImage(String keyImage) {
return null;
}
-void freezeCoin(int index) => monero.Coins_setFrozen(coins!, index: index);
+Future freezeCoin(int index) async {
+ await coinsMutex.acquire();
+ final coinsPtr = coins!.address;
+ await Isolate.run(() => monero.Coins_setFrozen(Pointer.fromAddress(coinsPtr), index: index));
+ coinsMutex.release();
+}
-void thawCoin(int index) => monero.Coins_thaw(coins!, index: index);
+Future thawCoin(int index) async {
+ await coinsMutex.acquire();
+ final coinsPtr = coins!.address;
+ await Isolate.run(() => monero.Coins_thaw(Pointer.fromAddress(coinsPtr), index: index));
+ coinsMutex.release();
+}
diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart
index 162b9ac1a..8c7b1e902 100644
--- a/cw_monero/lib/api/transaction_history.dart
+++ b/cw_monero/lib/api/transaction_history.dart
@@ -1,6 +1,7 @@
import 'dart:ffi';
import 'dart:isolate';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_monero/api/monero_output.dart';
@@ -13,15 +14,23 @@ import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
import 'package:mutex/mutex.dart';
+Map> txKeys = {};
String getTxKey(String txId) {
+ txKeys[wptr!.address] ??= {};
+ if (txKeys[wptr!.address]![txId] != null) {
+ return txKeys[wptr!.address]![txId]!;
+ }
final txKey = monero.Wallet_getTxKey(wptr!, txid: txId);
final status = monero.Wallet_status(wptr!);
if (status != 0) {
- final error = monero.Wallet_errorString(wptr!);
+ monero.Wallet_errorString(wptr!);
+ txKeys[wptr!.address]![txId] = "";
return "";
}
+ txKeys[wptr!.address]![txId] = txKey;
return txKey;
}
+
final txHistoryMutex = Mutex();
monero.TransactionHistory? txhistory;
bool isRefreshingTx = false;
@@ -34,6 +43,7 @@ Future refreshTransactions() async {
await Isolate.run(() {
monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
});
+ await Future.delayed(Duration.zero);
txHistoryMutex.release();
isRefreshingTx = false;
}
@@ -45,8 +55,24 @@ Future> getAllTransactions() async {
await txHistoryMutex.acquire();
txhistory ??= monero.Wallet_history(wptr!);
+ final startAddress = txhistory!.address * wptr!.address;
int size = countOfTransactions();
- final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index)));
+ final list = [];
+ for (int index = 0; index < size; index++) {
+ if (index % 25 == 0) {
+ // Give main thread a chance to do other things.
+ await Future.delayed(Duration.zero);
+ }
+ if (txhistory!.address * wptr!.address != startAddress) {
+ printV("Loop broken because txhistory!.address * wptr!.address != startAddress");
+ break;
+ }
+ final txInfo = monero.TransactionHistory_transaction(txhistory!, index: index);
+ final txHash = monero.TransactionInfo_hash(txInfo);
+ txCache[wptr!.address] ??= {};
+ txCache[wptr!.address]![txHash] = Transaction(txInfo: txInfo);
+ list.add(txCache[wptr!.address]![txHash]!);
+ }
txHistoryMutex.release();
final accts = monero.Wallet_numSubaddressAccounts(wptr!);
for (var i = 0; i < accts; i++) {
@@ -79,8 +105,18 @@ Future> getAllTransactions() async {
return list;
}
-Transaction getTransaction(String txId) {
- return Transaction(txInfo: monero.TransactionHistory_transactionById(txhistory!, txid: txId));
+Map> txCache = {};
+Future getTransaction(String txId) async {
+ if (txCache[wptr!.address] != null && txCache[wptr!.address]![txId] != null) {
+ return txCache[wptr!.address]![txId]!;
+ }
+ await txHistoryMutex.acquire();
+ final tx = monero.TransactionHistory_transactionById(txhistory!, txid: txId);
+ final txDart = Transaction(txInfo: tx);
+ txCache[wptr!.address] ??= {};
+ txCache[wptr!.address]![txId] = txDart;
+ txHistoryMutex.release();
+ return txDart;
}
Future createTransactionSync(
@@ -149,13 +185,14 @@ Future createTransactionSync(
final rAmt = monero.PendingTransaction_amount(pendingTx);
final rFee = monero.PendingTransaction_fee(pendingTx);
final rHash = monero.PendingTransaction_txid(pendingTx, '');
+ final rHex = monero.PendingTransaction_hex(pendingTx, '');
final rTxKey = rHash;
return PendingTransactionDescription(
amount: rAmt,
fee: rFee,
hash: rHash,
- hex: '',
+ hex: rHex,
txKey: rTxKey,
pointerAddress: pendingTx.address,
);
@@ -198,7 +235,7 @@ Future createTransactionMultDest(
amount: monero.PendingTransaction_amount(txptr),
fee: monero.PendingTransaction_fee(txptr),
hash: monero.PendingTransaction_txid(txptr, ''),
- hex: monero.PendingTransaction_txid(txptr, ''),
+ hex: monero.PendingTransaction_hex(txptr, ''),
txKey: monero.PendingTransaction_txid(txptr, ''),
pointerAddress: txptr.address,
);
diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart
index 033df53cf..7e64c7f08 100644
--- a/cw_monero/lib/api/wallet.dart
+++ b/cw_monero/lib/api/wallet.dart
@@ -2,10 +2,10 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
-import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
+import 'package:cw_monero/api/wallet_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:monero/monero.dart' as monero;
import 'package:mutex/mutex.dart';
@@ -21,14 +21,18 @@ int getSyncingHeight() {
}
bool isNeededToRefresh() {
- final ret = monero.MONERO_cw_WalletListener_isNeedToRefresh(getWlptr());
- monero.MONERO_cw_WalletListener_resetNeedToRefresh(getWlptr());
+ final wlptr = getWlptr();
+ if (wlptr == null) return false;
+ final ret = monero.MONERO_cw_WalletListener_isNeedToRefresh(wlptr);
+ monero.MONERO_cw_WalletListener_resetNeedToRefresh(wlptr);
return ret;
}
bool isNewTransactionExist() {
- final ret = monero.MONERO_cw_WalletListener_isNewTransactionExist(getWlptr());
- monero.MONERO_cw_WalletListener_resetIsNewTransactionExist(getWlptr());
+ final wlptr = getWlptr();
+ if (wlptr == null) return false;
+ final ret = monero.MONERO_cw_WalletListener_isNewTransactionExist(wlptr);
+ monero.MONERO_cw_WalletListener_resetIsNewTransactionExist(wlptr);
return ret;
}
@@ -58,6 +62,11 @@ String getSeed() {
}
return cakepolyseed;
}
+
+ final bip39 = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed.bip39");
+
+ if(bip39.isNotEmpty) return bip39;
+
final legacy = getSeedLegacy(null);
return legacy;
}
@@ -199,12 +208,15 @@ void startRefreshSync() {
}
-void setRefreshFromBlockHeight({required int height}) =>
- monero.Wallet_setRefreshFromBlockHeight(wptr!,
- refresh_from_block_height: height);
+void setRefreshFromBlockHeight({required int height}) {
+ monero.Wallet_setRefreshFromBlockHeight(wptr!,
+ refresh_from_block_height: height);
+}
-void setRecoveringFromSeed({required bool isRecovery}) =>
- monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
+void setRecoveringFromSeed({required bool isRecovery}) {
+ monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
+ monero.Wallet_store(wptr!);
+}
final storeMutex = Mutex();
@@ -394,4 +406,6 @@ String signMessage(String message, {String address = ""}) {
bool verifyMessage(String message, String address, String signature) {
return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
-}
\ No newline at end of file
+}
+
+Map> debugCallLength() => monero.debugCallLength;
diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart
index 0dcb3c851..b43773447 100644
--- a/cw_monero/lib/api/wallet_manager.dart
+++ b/cw_monero/lib/api/wallet_manager.dart
@@ -137,6 +137,7 @@ void restoreWalletFromSeedSync(
wptr = newWptr;
setRefreshFromBlockHeight(height: restoreHeight);
+ setupBackgroundSync(password, newWptr);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
diff --git a/cw_monero/lib/bip39_seed.dart b/cw_monero/lib/bip39_seed.dart
new file mode 100644
index 000000000..338516e66
--- /dev/null
+++ b/cw_monero/lib/bip39_seed.dart
@@ -0,0 +1,59 @@
+import 'dart:typed_data';
+
+import 'package:bip32/bip32.dart' as bip32;
+import 'package:bip39/bip39.dart' as bip39;
+import 'package:polyseed/polyseed.dart';
+
+bool isBip39Seed(String mnemonic) => bip39.validateMnemonic(mnemonic);
+
+String getBip39Seed() => bip39.generateMnemonic();
+
+String getLegacySeedFromBip39(String mnemonic,
+ {int accountIndex = 0, String passphrase = ""}) {
+ final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase);
+
+ final bip32KeyPair =
+ bip32.BIP32.fromSeed(seed).derivePath("m/44'/128'/$accountIndex'/0/0");
+
+ final spendKey = _reduceECKey(bip32KeyPair.privateKey!);
+
+ return LegacySeedLang.getByEnglishName("English")
+ .encodePhrase(spendKey.toHexString());
+}
+
+const _ed25519CurveOrder =
+ "1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED";
+
+Uint8List _reduceECKey(Uint8List buffer) {
+ final curveOrder = BigInt.parse(_ed25519CurveOrder, radix: 16);
+ final bigNumber = _readBytes(buffer);
+
+ var result = bigNumber % curveOrder;
+
+ final resultBuffer = Uint8List(32);
+ for (var i = 0; i < 32; i++) {
+ resultBuffer[i] = (result & BigInt.from(0xff)).toInt();
+ result = result >> 8;
+ }
+
+ return resultBuffer;
+}
+
+/// Read BigInt from a little-endian Uint8List
+/// From https://github.com/dart-lang/sdk/issues/32803#issuecomment-387405784
+BigInt _readBytes(Uint8List bytes) {
+ BigInt read(int start, int end) {
+ if (end - start <= 4) {
+ var result = 0;
+ for (int i = end - 1; i >= start; i--) {
+ result = result * 256 + bytes[i];
+ }
+ return BigInt.from(result);
+ }
+ final mid = start + ((end - start) >> 1);
+ return read(start, mid) +
+ read(mid, end) * (BigInt.one << ((mid - start) * 8));
+ }
+
+ return read(0, bytes.length);
+}
diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart
index aa23e276f..c9a48a939 100644
--- a/cw_monero/lib/monero_account_list.dart
+++ b/cw_monero/lib/monero_account_list.dart
@@ -1,5 +1,6 @@
import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_monero/api/wallet_manager.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/account.dart';
import 'package:cw_monero/api/account_list.dart' as account_list;
@@ -44,7 +45,18 @@ abstract class MoneroAccountListBase with Store {
}
}
- List getAll() => account_list.getAllAccount().map((accountRow) {
+ static Map> cachedAccounts = {};
+
+ List getAll() {
+ final allAccounts = account_list.getAllAccount();
+ final currentCount = allAccounts.length;
+ cachedAccounts[account_list.wptr!.address] ??= [];
+
+ if (cachedAccounts[account_list.wptr!.address]!.length == currentCount) {
+ return cachedAccounts[account_list.wptr!.address]!;
+ }
+
+ cachedAccounts[account_list.wptr!.address] = allAccounts.map((accountRow) {
final balance = monero.SubaddressAccountRow_getUnlockedBalance(accountRow);
return Account(
@@ -53,6 +65,9 @@ abstract class MoneroAccountListBase with Store {
balance: moneroAmountToString(amount: monero.Wallet_amountFromString(balance)),
);
}).toList();
+
+ return cachedAccounts[account_list.wptr!.address]!;
+ }
Future addAccount({required String label}) async {
await account_list.addAccount(label: label);
diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart
index 8a104edf4..224e33b2a 100644
--- a/cw_monero/lib/monero_unspent.dart
+++ b/cw_monero/lib/monero_unspent.dart
@@ -4,31 +4,47 @@ import 'package:cw_monero/api/coins_info.dart';
import 'package:monero/monero.dart' as monero;
class MoneroUnspent extends Unspent {
- MoneroUnspent(
- String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked)
- : super(address, hash, value, 0, keyImage) {
+ static Future fromUnspent(String address, String hash, String keyImage, int value, bool isFrozen, bool isUnlocked) async {
+ return MoneroUnspent(
+ address: address,
+ hash: hash,
+ keyImage: keyImage,
+ value: value,
+ isFrozen: isFrozen,
+ isUnlocked: isUnlocked);
}
+ MoneroUnspent(
+ {required String address,
+ required String hash,
+ required String keyImage,
+ required int value,
+ required bool isFrozen,
+ required this.isUnlocked})
+ : super(address, hash, value, 0, keyImage) {
+ _frozen = isFrozen;
+ }
+
+ bool _frozen = false;
+
@override
set isFrozen(bool freeze) {
+ _frozen = freeze;
printV("set isFrozen: $freeze ($keyImage): $freeze");
- final coinId = getCoinByKeyImage(keyImage!);
- if (coinId == null) throw Exception("Unable to find a coin for address $address");
- if (freeze) {
- freezeCoin(coinId);
- } else {
- thawCoin(coinId);
- }
+ getCoinByKeyImage(keyImage!).then((coinId) async {
+ if (coinId == null) return;
+ if (freeze) {
+ await freezeCoin(coinId);
+ _frozen = true;
+ } else {
+ await thawCoin(coinId);
+ _frozen = false;
+ }
+ });
}
@override
- bool get isFrozen {
- printV("get isFrozen");
- final coinId = getCoinByKeyImage(keyImage!);
- if (coinId == null) throw Exception("Unable to find a coin for address $address");
- final coin = getCoin(coinId);
- return monero.CoinsInfo_frozen(coin);
- }
+ bool get isFrozen => _frozen;
final bool isUnlocked;
}
diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart
index 00e50f37f..dc346a22c 100644
--- a/cw_monero/lib/monero_wallet.dart
+++ b/cw_monero/lib/monero_wallet.dart
@@ -169,6 +169,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async {
- transaction_history.refreshTransactions();
- return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
+ await transaction_history.refreshTransactions();
+ final resp = (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
.fold