diff --git a/.gitignore b/.gitignore index 11959187d..a71bb538f 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,10 @@ secp256k1.dll /lib/app_config.g.dart /android/app/src/main/app_icon-playstore.png +# Dart generated files (Freezed, Riverpod, GoRouter etc..) +lib/**/*.g.dart +lib/**/*.freezed.dart + ## other generated project files pubspec.yaml @@ -104,3 +108,4 @@ scripts/linux/build/libsecret/subprojects/gi-docgen/.meson-subproject-wrap-hash. crypto_plugins/cs_monero/built_outputs crypto_plugins/cs_monero/build +crypto_plugins/*.diff diff --git a/asset_sources/default_themes/stack_duo/dark.zip b/asset_sources/default_themes/stack_duo/dark.zip index 1e5f6136e..8b31f4278 100644 Binary files a/asset_sources/default_themes/stack_duo/dark.zip and b/asset_sources/default_themes/stack_duo/dark.zip differ diff --git a/asset_sources/default_themes/stack_duo/light.zip b/asset_sources/default_themes/stack_duo/light.zip index bc7a95771..120573ccf 100644 Binary files a/asset_sources/default_themes/stack_duo/light.zip and b/asset_sources/default_themes/stack_duo/light.zip differ diff --git a/asset_sources/default_themes/stack_wallet/dark.zip b/asset_sources/default_themes/stack_wallet/dark.zip index df24199c4..8b31f4278 100644 Binary files a/asset_sources/default_themes/stack_wallet/dark.zip and b/asset_sources/default_themes/stack_wallet/dark.zip differ diff --git a/asset_sources/default_themes/stack_wallet/light.zip b/asset_sources/default_themes/stack_wallet/light.zip index 9e0c82d9d..120573ccf 100644 Binary files a/asset_sources/default_themes/stack_wallet/light.zip and b/asset_sources/default_themes/stack_wallet/light.zip differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ffd9d8ca3..add8f4224 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -109,6 +109,8 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter + - xelis_flutter (0.0.1): + - Flutter DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) @@ -138,6 +140,7 @@ DEPENDENCIES: - tor_ffi_plugin (from `.symlinks/plugins/tor_ffi_plugin/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - xelis_flutter (from `.symlinks/plugins/xelis_flutter/ios`) SPEC REPOS: trunk: @@ -205,6 +208,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" + xelis_flutter: + :path: ".symlinks/plugins/xelis_flutter/ios" SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 diff --git a/lib/main.dart b/lib/main.dart index fde2ce42a..ccf2ecf6e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,8 +22,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; +import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:window_size/window_size.dart'; +import 'package:xelis_flutter/src/api/api.dart' as xelis_api; +import 'package:xelis_flutter/src/api/logger.dart' as xelis_logging; +import 'package:xelis_flutter/src/frb_generated.dart' as xelis_rust; import 'app_config.dart'; import 'db/db_version_migration.dart'; @@ -74,13 +78,44 @@ import 'wallets/isar/providers/all_wallets_info_provider.dart'; import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'widgets/crypto_notifications.dart'; -final openedFromSWBFileStringStateProvider = - StateProvider((ref) => null); +final openedFromSWBFileStringStateProvider = StateProvider( + (ref) => null, +); + +void startListeningToRustLogs() { + xelis_api.createLogStream().listen( + (logEntry) { + final Level level; + switch (logEntry.level) { + case xelis_logging.Level.error: + level = Level.error; + case xelis_logging.Level.warn: + level = Level.warning; + case xelis_logging.Level.info: + level = Level.info; + case xelis_logging.Level.debug: + level = Level.debug; + case xelis_logging.Level.trace: + level = Level.trace; + } + + Logging.instance.log( + level, + "[Xelis Rust Log] ${logEntry.tag}: ${logEntry.msg}", + ); + }, + onError: (dynamic e) { + Logging.instance.e("Error receiving Xelis Rust logs: $e"); + }, + ); +} // main() is the entry point to the app. It initializes Hive (local database), // runs the MyApp widget and checks for new users, caching the value in the // miscellaneous box for later use void main(List args) async { + // talker.info('initializing Rust lib ...'); + await xelis_rust.RustLib.init(); WidgetsFlutterBinding.ensureInitialized(); if (Util.isDesktop && args.length == 2 && args.first == "-d") { @@ -108,9 +143,7 @@ void main(List args) async { if (screenHeight != null) { // starting to height be 3/4 screen height or 900, whichever is smaller final height = min(screenHeight * 0.75, 900); - setWindowFrame( - Rect.fromLTWH(0, 0, 1220, height), - ); + setWindowFrame(Rect.fromLTWH(0, 0, 1220, height)); } } @@ -146,8 +179,9 @@ void main(List args) async { // node model adapter DB.instance.hive.registerAdapter(NodeModelAdapter()); - if (!DB.instance.hive - .isAdapterRegistered(lib_monero_compat.WalletInfoAdapter().typeId)) { + if (!DB.instance.hive.isAdapterRegistered( + lib_monero_compat.WalletInfoAdapter().typeId, + )) { DB.instance.hive.registerAdapter(lib_monero_compat.WalletInfoAdapter()); } @@ -168,6 +202,9 @@ void main(List args) async { level: Prefs.instance.logLevel, ); + await xelis_api.setUpRustLogger(); + startListeningToRustLogs(); + // setup lib spark logging initSparkLogging(Prefs.instance.logLevel); @@ -194,10 +231,12 @@ void main(List args) async { // Desktop migrate handled elsewhere (currently desktop_login_view.dart) if (!Util.isDesktop) { - final int dbVersion = DB.instance.get( - boxName: DB.boxNameDBInfo, - key: "hive_data_version", - ) as int? ?? + final int dbVersion = + DB.instance.get( + boxName: DB.boxNameDBInfo, + key: "hive_data_version", + ) + as int? ?? 0; if (dbVersion < Constants.currentDataVersion) { try { @@ -232,22 +271,25 @@ void main(List args) async { // verify current user preference theme and revert to default // if problems are found to prevent app being unusable - if (!(await ThemeService.instance - .verifyInstalled(themeId: Prefs.instance.themeId))) { + if (!(await ThemeService.instance.verifyInstalled( + themeId: Prefs.instance.themeId, + ))) { Prefs.instance.themeId = "light"; } // verify current user preference light brightness theme and revert to default // if problems are found to prevent app being unusable - if (!(await ThemeService.instance - .verifyInstalled(themeId: Prefs.instance.systemBrightnessLightThemeId))) { + if (!(await ThemeService.instance.verifyInstalled( + themeId: Prefs.instance.systemBrightnessLightThemeId, + ))) { Prefs.instance.systemBrightnessLightThemeId = "light"; } // verify current user preference dark brightness theme and revert to default // if problems are found to prevent app being unusable - if (!(await ThemeService.instance - .verifyInstalled(themeId: Prefs.instance.systemBrightnessDarkThemeId))) { + if (!(await ThemeService.instance.verifyInstalled( + themeId: Prefs.instance.systemBrightnessDarkThemeId, + ))) { Prefs.instance.systemBrightnessDarkThemeId = "dark"; } @@ -263,18 +305,14 @@ class MyApp extends StatelessWidget { final localeService = LocaleService(); localeService.loadLocale(); - return const KeyboardDismisser( - child: MaterialAppWithTheme(), - ); + return const KeyboardDismisser(child: MaterialAppWithTheme()); } } // Sidenote: MaterialAppWithTheme and InitView are only separated for clarity. No other reason. class MaterialAppWithTheme extends ConsumerStatefulWidget { - const MaterialAppWithTheme({ - super.key, - }); + const MaterialAppWithTheme({super.key}); @override ConsumerState createState() => @@ -348,7 +386,9 @@ class _MaterialAppWithThemeState extends ConsumerState prefs: ref.read(prefsChangeNotifierProvider), ); ref.read(priceAnd24hChangeNotifierProvider).start(true); - await ref.read(pWallets).load( + await ref + .read(pWallets) + .load( ref.read(prefsChangeNotifierProvider), ref.read(mainDBProvider), ); @@ -378,7 +418,9 @@ class _MaterialAppWithThemeState extends ConsumerState if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { switch (ref.read(prefsChangeNotifierProvider).backupFrequencyType) { case BackupFrequencyType.everyTenMinutes: - ref.read(autoSWBServiceProvider).startPeriodicBackupTimer( + ref + .read(autoSWBServiceProvider) + .startPeriodicBackupTimer( duration: const Duration(minutes: 10), ); break; @@ -411,9 +453,10 @@ class _MaterialAppWithThemeState extends ConsumerState ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId; break; case Brightness.light: - themeId = ref - .read(prefsChangeNotifierProvider) - .systemBrightnessLightThemeId; + themeId = + ref + .read(prefsChangeNotifierProvider) + .systemBrightnessLightThemeId; break; } } else { @@ -432,9 +475,8 @@ class _MaterialAppWithThemeState extends ConsumerState ref.read(applicationThemesDirectoryPathProvider.notifier).state = StackFileSystem.themesDir!.path; - ref.read(themeProvider.state).state = ref.read(pThemeService).getTheme( - themeId: themeId, - )!; + ref.read(themeProvider.state).state = + ref.read(pThemeService).getTheme(themeId: themeId)!; if (Platform.isAndroid) { // fetch open file if it exists @@ -462,18 +504,17 @@ class _MaterialAppWithThemeState extends ConsumerState ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId; break; case Brightness.light: - themeId = ref - .read(prefsChangeNotifierProvider) - .systemBrightnessLightThemeId; + themeId = + ref + .read(prefsChangeNotifierProvider) + .systemBrightnessLightThemeId; break; } WidgetsBinding.instance.addPostFrameCallback((_) { if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { ref.read(themeProvider.state).state = - ref.read(pThemeService).getTheme( - themeId: themeId, - )!; + ref.read(pThemeService).getTheme(themeId: themeId)!; } }); }; @@ -552,8 +593,8 @@ class _MaterialAppWithThemeState extends ConsumerState /// should only be called on android currently Future getOpenFile() async { // update provider with new file content state - ref.read(openedFromSWBFileStringStateProvider.state).state = - await platform.invokeMethod("getOpenFile"); + ref.read(openedFromSWBFileStringStateProvider.state).state = await platform + .invokeMethod("getOpenFile"); // call reset to clear cached value await resetOpenPath(); @@ -570,9 +611,9 @@ class _MaterialAppWithThemeState extends ConsumerState Future goToRestoreSWB(String encrypted) async { if (!ref.read(prefsChangeNotifierProvider).hasPin) { - await Navigator.of(navigatorKey.currentContext!) - .pushNamed(CreatePinView.routeName, arguments: true) - .then((value) { + await Navigator.of( + navigatorKey.currentContext!, + ).pushNamed(CreatePinView.routeName, arguments: true).then((value) { if (value is! bool || value == false) { Navigator.of(navigatorKey.currentContext!).pushNamed( RestoreFromEncryptedStringView.routeName, @@ -586,16 +627,17 @@ class _MaterialAppWithThemeState extends ConsumerState navigatorKey.currentContext!, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - showBackButton: true, - routeOnSuccess: RestoreFromEncryptedStringView.routeName, - routeOnSuccessArguments: encrypted, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to restore ${AppConfig.appName} backup", - biometricsAuthenticationTitle: - "Restore ${AppConfig.prefix} backup", - ), + builder: + (_) => LockscreenView( + showBackButton: true, + routeOnSuccess: RestoreFromEncryptedStringView.routeName, + routeOnSuccessArguments: encrypted, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to restore ${AppConfig.appName} backup", + biometricsAuthenticationTitle: + "Restore ${AppConfig.prefix} backup", + ), settings: const RouteSettings(name: "/swbrestorelockscreen"), ), ), @@ -605,10 +647,7 @@ class _MaterialAppWithThemeState extends ConsumerState InputBorder _buildOutlineInputBorder(Color color) { return OutlineInputBorder( - borderSide: BorderSide( - width: 1, - color: color, - ), + borderSide: BorderSide(width: 1, color: color), borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ); } @@ -646,9 +685,7 @@ class _MaterialAppWithThemeState extends ConsumerState ), // splashFactory: NoSplash.splashFactory, splashColor: Colors.transparent, - buttonTheme: ButtonThemeData( - splashColor: colorScheme.splash, - ), + buttonTheme: ButtonThemeData(splashColor: colorScheme.splash), textButtonTheme: TextButtonThemeData( style: ButtonStyle( // splashFactory: NoSplash.splashFactory, @@ -656,8 +693,9 @@ class _MaterialAppWithThemeState extends ConsumerState minimumSize: MaterialStateProperty.all(const Size(46, 46)), // textStyle: MaterialStateProperty.all( // STextStyles.button(context)), - foregroundColor: - MaterialStateProperty.all(colorScheme.buttonTextSecondary), + foregroundColor: MaterialStateProperty.all( + colorScheme.buttonTextSecondary, + ), backgroundColor: MaterialStateProperty.all( colorScheme.buttonBackSecondary, ), @@ -674,25 +712,22 @@ class _MaterialAppWithThemeState extends ConsumerState checkboxTheme: CheckboxThemeData( splashRadius: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.checkboxBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.checkboxBorderRadius, + ), ), - checkColor: MaterialStateColor.resolveWith( - (state) { - if (state.contains(MaterialState.selected)) { - return colorScheme.checkboxIconChecked; - } + checkColor: MaterialStateColor.resolveWith((state) { + if (state.contains(MaterialState.selected)) { + return colorScheme.checkboxIconChecked; + } + return colorScheme.checkboxBGChecked; + }), + fillColor: MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.selected)) { return colorScheme.checkboxBGChecked; - }, - ), - fillColor: MaterialStateColor.resolveWith( - (states) { - if (states.contains(MaterialState.selected)) { - return colorScheme.checkboxBGChecked; - } - return colorScheme.checkboxBorderEmpty; - }, - ), + } + return colorScheme.checkboxBorderEmpty; + }), ), appBarTheme: AppBarTheme( centerTitle: false, @@ -710,91 +745,101 @@ class _MaterialAppWithThemeState extends ConsumerState ), // labelStyle: STextStyles.fieldLabel(context), // hintStyle: STextStyles.fieldLabel(context), - enabledBorder: - _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), - focusedBorder: - _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), + enabledBorder: _buildOutlineInputBorder( + colorScheme.textFieldDefaultBG, + ), + focusedBorder: _buildOutlineInputBorder( + colorScheme.textFieldDefaultBG, + ), errorBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), - disabledBorder: - _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), - focusedErrorBorder: - _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), + disabledBorder: _buildOutlineInputBorder( + colorScheme.textFieldDefaultBG, + ), + focusedErrorBorder: _buildOutlineInputBorder( + colorScheme.textFieldDefaultBG, + ), ), ), home: CryptoNotifications( - child: Util.isDesktop - ? FutureBuilder( - future: loadShared(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (_desktopHasPassword) { - String? startupWalletId; - if (ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup) { - startupWalletId = ref + child: + Util.isDesktop + ? FutureBuilder( + future: loadShared(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (_desktopHasPassword) { + String? startupWalletId; + if (ref .read(prefsChangeNotifierProvider) - .startupWalletId; + .gotoWalletOnStartup) { + startupWalletId = + ref + .read(prefsChangeNotifierProvider) + .startupWalletId; + } + + return DesktopLoginView( + startupWalletId: startupWalletId, + load: load, + ); + } else { + return const IntroView(); } - - return DesktopLoginView( - startupWalletId: startupWalletId, - load: load, - ); } else { - return const IntroView(); + return const LoadingView(); } - } else { - return const LoadingView(); - } - }, - ) - : FutureBuilder( - future: load(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - // FlutterNativeSplash.remove(); - if (ref.read(pAllWalletsInfo).isNotEmpty || - ref.read(prefsChangeNotifierProvider).hasPin) { - // return HomeView(); - - String? startupWalletId; - if (ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup) { - startupWalletId = ref + }, + ) + : FutureBuilder( + future: load(), + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + if (snapshot.connectionState == ConnectionState.done) { + // FlutterNativeSplash.remove(); + if (ref.read(pAllWalletsInfo).isNotEmpty || + ref.read(prefsChangeNotifierProvider).hasPin) { + // return HomeView(); + + String? startupWalletId; + if (ref .read(prefsChangeNotifierProvider) - .startupWalletId; - } - - return LockscreenView( - isInitialAppLogin: true, - routeOnSuccess: HomeView.routeName, - routeOnSuccessArguments: startupWalletId, - biometricsAuthenticationTitle: - "Unlock ${AppConfig.prefix}", - biometricsLocalizedReason: - "Unlock your ${AppConfig.appName} using biometrics", - biometricsCancelButtonString: "Cancel", - ); - } else { - if (AppConfig.appName == "Campfire" && - !CampfireMigration.didRun && - CampfireMigration.hasOldWallets) { - return const CampfireMigrateView(); + .gotoWalletOnStartup) { + startupWalletId = + ref + .read(prefsChangeNotifierProvider) + .startupWalletId; + } + + return LockscreenView( + isInitialAppLogin: true, + routeOnSuccess: HomeView.routeName, + routeOnSuccessArguments: startupWalletId, + biometricsAuthenticationTitle: + "Unlock ${AppConfig.prefix}", + biometricsLocalizedReason: + "Unlock your ${AppConfig.appName} using biometrics", + biometricsCancelButtonString: "Cancel", + ); } else { - return const IntroView(); + if (AppConfig.appName == "Campfire" && + !CampfireMigration.didRun && + CampfireMigration.hasOldWallets) { + return const CampfireMigrateView(); + } else { + return const IntroView(); + } } + } else { + // CURRENTLY DISABLED as cannot be animated + // technically not needed as FlutterNativeSplash will overlay + // anything returned here until the future completes but + // FutureBuilder requires you to return something + return const LoadingView(); } - } else { - // CURRENTLY DISABLED as cannot be animated - // technically not needed as FlutterNativeSplash will overlay - // anything returned here until the future completes but - // FutureBuilder requires you to return something - return const LoadingView(); - } - }, - ), + }, + ), ), ); } diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 584636050..8947d6c46 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -175,7 +175,8 @@ enum AddressType { frostMS, p2tr, solana, - cardanoShelley; + cardanoShelley, + xelis; String get readableName { switch (this) { @@ -213,6 +214,8 @@ enum AddressType { return "P2TR (taproot)"; case AddressType.cardanoShelley: return "Cardano Shelley"; + case AddressType.xelis: + return "Xelis"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 340ab9f1b..a9289a481 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -279,6 +279,7 @@ const _AddresstypeEnumValueMap = { 'p2tr': 14, 'solana': 15, 'cardanoShelley': 16, + 'xelis': 17, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -298,6 +299,7 @@ const _AddresstypeValueEnumMap = { 14: AddressType.p2tr, 15: AddressType.solana, 16: AddressType.cardanoShelley, + 17: AddressType.xelis, }; Id _addressGetId(Address object) { diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 1ab649f26..7d93d038a 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -191,7 +191,7 @@ class _NewWalletRecoveryPhraseWarningViewState // TODO: Refactor these to generate each coin in their respective classes // This code should not be in a random view page file - if (coin is Monero || coin is Wownero) { + if (coin is Monero || coin is Wownero || coin is Xelis) { // currently a special case due to the // xmr/wow libraries handling their // own mnemonic generation diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart index 3a7131762..9123b2f46 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart @@ -31,6 +31,7 @@ import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -264,6 +265,10 @@ class _RestoreViewOnlyWalletViewState await (wallet as WowneroWallet).init(isRestore: true); break; + case const (XelisWallet): + await (wallet as XelisWallet).init(isRestore: true); + break; + default: await wallet.init(); } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index a5aec4d97..972012c00 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -25,6 +25,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:xelis_flutter/src/api/seed_search_engine.dart' as x_seed; + import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -48,7 +50,8 @@ import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; -import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -103,6 +106,7 @@ class _RestoreWalletViewState extends ConsumerState { late final int _seedWordCount; late final bool isDesktop; + x_seed.SearchEngine? _xelisSeedSearch; final HashSet _wordListHashSet = HashSet.from(bip39wordlist.WORDLIST); final ScrollController controller = ScrollController(); @@ -167,6 +171,10 @@ class _RestoreWalletViewState extends ConsumerState { // _focusNodes.add(FocusNode()); } + if (widget.coin is Xelis) { + _xelisSeedSearch = x_seed.SearchEngine.init(languageIndex: BigInt.from(0)); + } + super.initState(); } @@ -199,6 +207,9 @@ class _RestoreWalletViewState extends ConsumerState { ); return wowneroWordList.contains(word); } + if (widget.coin is Xelis) { + return _xelisSeedSearch!.search(query: word).length > 0; + } return _wordListHashSet.contains(word); } @@ -283,10 +294,9 @@ class _RestoreWalletViewState extends ConsumerState { ); } - // TODO: do actual check to make sure it is a valid mnemonic for monero + // TODO: do actual check to make sure it is a valid mnemonic for monero + xelis if (bip39.validateMnemonic(mnemonic) == false && - !(widget.coin is Monero || widget.coin is Wownero)) { - if (mounted) setState(() => _hideSeedWords = false); + !(widget.coin is Monero || widget.coin is Wownero || widget.coin is Xelis)) { unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -371,13 +381,17 @@ class _RestoreWalletViewState extends ConsumerState { await (wallet as WowneroWallet).init(isRestore: true); break; + case const (XelisWallet): + await (wallet as XelisWallet).init(isRestore: true); + break; + default: await wallet.init(); } await wallet.recover(isRescan: false); - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { await wallet.exit(); } diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index b1f5613bb..04dc94d33 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -40,6 +40,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; @@ -225,6 +226,10 @@ class _VerifyRecoveryPhraseViewState await (voWallet as WowneroWallet).init(isRestore: true); break; + case const (XelisWallet): + await (voWallet as XelisWallet).init(isRestore: true); + break; + default: await voWallet.init(); } diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 59cf20a47..7e97017a3 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -35,7 +35,7 @@ import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; -import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../wallets/wallet/intermediate/external_wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -277,7 +277,7 @@ class _SendFromCardState extends ConsumerState { // access to this screen but this is needed to get past an error that // would occur only to lead to another error which is why xmr/wow wallets // don't have access to this screen currently - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { await wallet.init(); await wallet.open(); } diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 5d630ba4f..c7d6bb254 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -29,7 +29,7 @@ import '../../utilities/show_loading.dart'; import '../../utilities/show_node_tor_settings_mismatch.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; -import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../wallets/wallet/intermediate/external_wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; @@ -119,7 +119,7 @@ class _LockscreenViewState extends ConsumerState { } final Future loadFuture; - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { loadFuture = wallet.init().then((value) async => await (wallet).open()); } else { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 935dad27b..b6e872bd4 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -55,6 +55,7 @@ import '../../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../../wallets/wallet/wallet.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; @@ -506,6 +507,10 @@ abstract class SWB { case const (WowneroWallet): await (wallet as WowneroWallet).init(isRestore: true); break; + + case const (XelisWallet): + await (wallet as XelisWallet).init(isRestore: true); + break; default: await wallet.init(); diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index 2acb6b1a1..8f0852941 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -1366,13 +1366,17 @@ class _TransactionV2DetailsViewState ], ), ), - if (coin is! NanoCurrency) + if (coin is! NanoCurrency && + !(coin is Xelis && _transaction.type == TransactionType.incoming) + ) isDesktop ? const _Divider() : const SizedBox( height: 12, ), - if (coin is! NanoCurrency) + if (coin is! NanoCurrency && + !(coin is Xelis && _transaction.type == TransactionType.incoming) + ) RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(16) diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 26e11624a..d9f730804 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -27,7 +27,7 @@ import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/coins/firo.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../widgets/coin_card.dart'; import '../../../widgets/conditional_parent.dart'; import '../../wallet_view/wallet_view.dart'; @@ -132,7 +132,7 @@ class _FavoriteCardState extends ConsumerState { } final Future loadFuture; - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { loadFuture = wallet.init().then((value) async => await (wallet).open()); } else { diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 767770ba9..938baac71 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -25,7 +25,7 @@ import '../../../utilities/show_node_tor_settings_mismatch.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; -import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../widgets/dialogs/tor_warning_dialog.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../wallet_view/wallet_view.dart'; @@ -99,7 +99,7 @@ class WalletListItem extends ConsumerWidget { } final Future loadFuture; - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { loadFuture = wallet.init().then((value) async => await (wallet).open()); } else { diff --git a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart index 8da907ca3..aaf1d1402 100644 --- a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart @@ -21,7 +21,7 @@ import '../../utilities/show_loading.dart'; import '../../utilities/show_node_tor_settings_mismatch.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; -import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../wallets/wallet/intermediate/external_wallet.dart'; import '../../widgets/rounded_container.dart'; import '../../widgets/wallet_info_row/wallet_info_row.dart'; import 'wallet_view/desktop_wallet_view.dart'; @@ -101,7 +101,7 @@ class CoinWalletsTable extends ConsumerWidget { } final Future loadFuture; - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { loadFuture = wallet .init() .then((value) async => await (wallet).open()); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 5df82cc0f..066ae9073 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -25,7 +25,7 @@ import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/all_wallets_info_provider.dart'; -import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../wallets/wallet/intermediate/external_wallet.dart'; import '../../widgets/breathing.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/desktop/desktop_dialog.dart'; @@ -138,7 +138,7 @@ class _DesktopWalletSummaryRowState } final Future loadFuture; - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { loadFuture = wallet.init().then((value) async => await (wallet).open()); } else { diff --git a/lib/providers/progress_report/xelis_table_progress_provider.dart b/lib/providers/progress_report/xelis_table_progress_provider.dart new file mode 100644 index 000000000..cbebe8224 --- /dev/null +++ b/lib/providers/progress_report/xelis_table_progress_provider.dart @@ -0,0 +1,78 @@ +import 'package:xelis_flutter/src/api/api.dart' as xelis_api; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:flutter/foundation.dart'; +import 'dart:math' as math; + +enum XelisTableGenerationStep { + t1PointsGeneration, + t1CuckooSetup, + t2Table, + unknown; + + factory XelisTableGenerationStep.fromString(String step) { + return switch (step) { + "T1PointsGeneration" => XelisTableGenerationStep.t1PointsGeneration, + "T1CuckooSetup" => XelisTableGenerationStep.t1CuckooSetup, + "T2Table" => XelisTableGenerationStep.t2Table, + _ => XelisTableGenerationStep.unknown, + }; + } + + String get displayName => switch (this) { + t1PointsGeneration => "Generating T1 Points", + t1CuckooSetup => "Setting up T1 Cuckoo", + t2Table => "Generating T2 Table", + unknown => "Processing", + }; +} + +class XelisTableProgressState { + final double? tableProgress; + final XelisTableGenerationStep currentStep; + + const XelisTableProgressState({ + this.tableProgress, + this.currentStep = XelisTableGenerationStep.unknown, + }); + + XelisTableProgressState copyWith({ + double? tableProgress, + XelisTableGenerationStep? currentStep, + }) { + return XelisTableProgressState( + tableProgress: tableProgress ?? this.tableProgress, + currentStep: currentStep ?? this.currentStep, + ); + } +} + +final xelisTableProgressProvider = StreamProvider((ref) { + double lastPrintedProgress = 0.0; + return xelis_api.createProgressReportStream().map((report) { + return report.when( + tableGeneration: (progress, step, _) { + final currentStep = XelisTableGenerationStep.fromString(step); + final stepIndex = switch(currentStep) { + XelisTableGenerationStep.t1PointsGeneration => 0, + XelisTableGenerationStep.t1CuckooSetup => 1, + XelisTableGenerationStep.t2Table => 2, + XelisTableGenerationStep.unknown => 0, + }; + + if ((progress - lastPrintedProgress).abs() >= 0.05 || + currentStep != XelisTableGenerationStep.fromString(step) || + progress >= 0.99) { + debugPrint("Xelis Table Generation: $step - ${progress*100.0}%"); + lastPrintedProgress = progress; + } + + return XelisTableProgressState( + tableProgress: progress, + currentStep: currentStep, + ); + }, + misc: (_) => const XelisTableProgressState(), + ); + }); +}); \ No newline at end of file diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index a96a9869c..e6e9ef20b 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -32,3 +32,4 @@ export './ui/verify_recovery_phrase/correct_word_provider.dart'; export './ui/verify_recovery_phrase/random_index_provider.dart'; export './ui/verify_recovery_phrase/selected_word_provider.dart'; export './wallet/transaction_note_provider.dart'; +export './progress_report/xelis_table_progress_provider.dart'; diff --git a/lib/services/price.dart b/lib/services/price.dart index a3c31ef17..801e720e2 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -48,6 +48,7 @@ class PriceAPI { Namecoin: "namecoin", Nano: "nano", Banano: "banano", + Xelis: "xelis", }; static const refreshInterval = 60; diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index 1915239a5..542c6375e 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -31,9 +31,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - // dumb quick conditional based on name. Should really be done better - static const _currentDefaultThemeVersion = - AppConfig.appName == "Campfire" ? 17 : 16; + static const _currentDefaultThemeVersion = 17; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); @@ -61,9 +59,7 @@ class ThemeService { final jsonString = utf8.decode(themeJsonFiles.first.content as List); final json = jsonDecode(jsonString) as Map; - final theme = StackTheme.fromJson( - json: Map.from(json), - ); + final theme = StackTheme.fromJson(json: Map.from(json)); try { theme.assets; @@ -96,11 +92,12 @@ class ThemeService { Future remove({required String themeId}) async { final themesDir = StackFileSystem.themesDir!; - final isarId = await db.isar.stackThemes - .where() - .themeIdEqualTo(themeId) - .idProperty() - .findFirst(); + final isarId = + await db.isar.stackThemes + .where() + .themeIdEqualTo(themeId) + .idProperty() + .findFirst(); if (isarId != null) { await db.isar.writeTxn(() async { await db.isar.stackThemes.delete(isarId); @@ -184,22 +181,27 @@ class ThemeService { try { final response = await client.get( url: Uri.parse("$baseServerUrl/themes"), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final jsonList = jsonDecode(response.body) as List; - final result = List>.from(jsonList) - .map((e) => StackThemeMetaData.fromMap(e)) - .where((e) => e.id != "light" && e.id != "dark") - .toList(); + final result = + List>.from(jsonList) + .map((e) => StackThemeMetaData.fromMap(e)) + .where((e) => e.id != "light" && e.id != "dark") + .toList(); return result; } catch (e, s) { - Logging.instance - .w("Failed to fetch themes list: ", error: e, stackTrace: s); + Logging.instance.w( + "Failed to fetch themes list: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -210,9 +212,10 @@ class ThemeService { try { final response = await client.get( url: Uri.parse("$baseServerUrl/theme/${themeMetaData.id}"), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final bytes = Uint8List.fromList(response.bodyBytes); @@ -228,8 +231,11 @@ class ThemeService { ); } } catch (e, s) { - Logging.instance - .w("Failed to fetch themes list: ", error: e, stackTrace: s); + Logging.instance.w( + "Failed to fetch themes list: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -270,9 +276,10 @@ class StackThemeMetaData { ); } catch (e, s) { Logging.instance.f( - "Failed to create instance of StackThemeMetaData using $map", - error: e, - stackTrace: s); + "Failed to create instance of StackThemeMetaData using $map", + error: e, + stackTrace: s, + ); rethrow; } } diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 3d64fb45a..e50ed2387 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -19,7 +19,8 @@ enum DerivePathType { eCash44, solana, bip86, - cardanoShelley; + cardanoShelley, + xelis; AddressType getAddressType() { switch (this) { @@ -45,6 +46,9 @@ enum DerivePathType { case DerivePathType.cardanoShelley: return AddressType.cardanoShelley; + + case DerivePathType.xelis: + return AddressType.xelis; } } } diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 67b992bbc..795ea9720 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -39,8 +39,9 @@ abstract class StackFileSystem { // todo: can merge and do same as regular linux home dir? if (Util.isArmLinux) { appDirectory = await getApplicationDocumentsDirectory(); - appDirectory = - Directory("${appDirectory.path}/.${AppConfig.appDefaultDataDirName}"); + appDirectory = Directory( + "${appDirectory.path}/.${AppConfig.appDefaultDataDirName}", + ); } else if (Platform.isLinux) { if (_overrideDesktopDirPath != null) { appDirectory = Directory(_overrideDesktopDirPath!); @@ -148,6 +149,24 @@ abstract class StackFileSystem { } } + static Future applicationXelisDirectory() async { + final root = await applicationRootDirectory(); + final dir = Directory("${root.path}${Platform.pathSeparator}xelis"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } + + static Future applicationXelisTableDirectory() async { + final xelis = await applicationXelisDirectory(); + final dir = Directory("${xelis.path}${Platform.pathSeparator}table"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } + static Future initThemesDir() async { final root = await applicationRootDirectory(); diff --git a/lib/utilities/test_node_connection.dart b/lib/utilities/test_node_connection.dart index b212d3a0a..7e8616ac2 100644 --- a/lib/utilities/test_node_connection.dart +++ b/lib/utilities/test_node_connection.dart @@ -26,6 +26,8 @@ import 'test_monero_node_connection.dart'; import 'test_stellar_node_connection.dart'; import 'tor_plain_net_option_enum.dart'; +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; + Future _xmrHelper( NodeFormData nodeFormData, BuildContext context, @@ -297,6 +299,28 @@ Future testNodeConnection({ testPassed = false; } break; + + case Xelis(): + try { + final daemon = xelis_sdk.DaemonClient( + endPoint: "${formData.host!}:${formData.port!}", + secureWebSocket: formData.useSSL ?? false, + timeout: 5000 + ); + daemon.connect(); + + final xelis_sdk.GetInfoResult networkInfo = await daemon.getInfo(); + testPassed = networkInfo.height != null; + + daemon.disconnect(); + + Logging.instance.i( + "Xelis testNodeConnection result: \"${networkInfo.toString()}\"", + ); + } catch (e, s) { + testPassed = false; + } + break; } return testPassed; diff --git a/lib/wallets/crypto_currency/coins/xelis.dart b/lib/wallets/crypto_currency/coins/xelis.dart new file mode 100644 index 000000000..b05fce216 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/xelis.dart @@ -0,0 +1,142 @@ +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../crypto_currency.dart'; +import '../intermediate/electrum_currency.dart'; + +import 'package:xelis_flutter/src/api/utils.dart' as x_utils; + +class Xelis extends ElectrumCurrency { + Xelis(super.network) { + _idMain = "xelis"; + _uriScheme = "xelis"; + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "Xelis"; + _ticker = "XEL"; + case CryptoCurrencyNetwork.test: + _id = "xelisTestNet"; + _name = "tXelis"; + _ticker = "XET"; + default: + throw Exception("Unsupported network: $network"); + } + } + + late final String _id; + @override + String get identifier => _id; + + late final String _idMain; + @override + String get mainNetId => _idMain; + + late final String _name; + @override + String get prettyName => _name; + + late final String _uriScheme; + @override + String get uriScheme => _uriScheme; + + late final String _ticker; + @override + String get ticker => _ticker; + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "us-node.xelis.io", + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: false, + clearnetEnabled: true, + ); + + case CryptoCurrencyNetwork.test: + return NodeModel( + host: "testnet-node.xelis.io", + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: false, + clearnetEnabled: true, + ); + + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + bool get torSupport => false; + + @override + bool validateAddress(String address) { + try { + return x_utils.isAddressValid(strAddress: address); + } catch (_) { + return false; + } + } + + @override + String get genesisHash => throw UnimplementedError(); + + @override + int get defaultSeedPhraseLength => 25; + + @override + int get fractionDigits => 8; + + @override + bool get hasBuySupport => false; + + @override + bool get hasMnemonicPassphraseSupport => false; + + @override + List get possibleMnemonicLengths => [defaultSeedPhraseLength]; + + @override + AddressType get defaultAddressType => defaultDerivePathType.getAddressType(); + + @override + BigInt get satsPerCoin => BigInt.from(1000000000); + + @override + int get targetBlockTimeSeconds => 15; + + @override + DerivePathType get defaultDerivePathType => DerivePathType.xelis; + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + case CryptoCurrencyNetwork.main: + return Uri.parse("https://explorer.xelis.io/txs/$txid"); + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } +} diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 066675d28..d5553ceca 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -23,6 +23,7 @@ export 'coins/solana.dart'; export 'coins/stellar.dart'; export 'coins/tezos.dart'; export 'coins/wownero.dart'; +export 'coins/xelis.dart'; enum CryptoCurrencyNetwork { main, diff --git a/lib/wallets/crypto_currency/intermediate/electrum_currency.dart b/lib/wallets/crypto_currency/intermediate/electrum_currency.dart new file mode 100644 index 000000000..4d8378f16 --- /dev/null +++ b/lib/wallets/crypto_currency/intermediate/electrum_currency.dart @@ -0,0 +1,5 @@ +import '../crypto_currency.dart'; + +abstract class ElectrumCurrency extends CryptoCurrency { + ElectrumCurrency(super.network); +} \ No newline at end of file diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 51aa69a45..779f1dd78 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -517,6 +517,7 @@ abstract class WalletInfoKeys { static const String epiccashData = "epiccashDataKey"; static const String bananoMonkeyImageBytes = "monkeyImageBytesKey"; static const String tezosDerivationPath = "tezosDerivationPathKey"; + static const String xelisDerivationPath = "xelisDerivationPathKey"; static const String lelantusCoinIsarRescanRequired = "lelantusCoinIsarRescanRequired"; static const String enableLelantusScanning = "enableLelantusScanningKey"; diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 6e02fd6d5..5e93564c0 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -269,6 +269,7 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'p2tr': 14, 'solana': 15, 'cardanoShelley': 16, + 'xelis': 17, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -288,6 +289,7 @@ const _WalletInfomainAddressTypeValueEnumMap = { 14: AddressType.p2tr, 15: AddressType.solana, 16: AddressType.cardanoShelley, + 17: AddressType.xelis, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 21fa206c9..94474e390 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -74,6 +74,9 @@ class TxData { final List? sparkMints; final List? usedSparkCoins; + // xelis specific + final String? otherData; + final TransactionV2? tempTx; final bool ignoreCachedBalanceChecks; @@ -113,6 +116,7 @@ class TxData { this.mintsMapLelantus, this.tezosOperationsList, this.sparkRecipients, + this.otherData, this.sparkMints, this.usedSparkCoins, this.tempTx, @@ -213,6 +217,7 @@ class TxData { String? note, String? noteOnChain, String? memo, + String? otherData, Set? utxos, List? usedUTXOs, List? recipients, @@ -258,6 +263,7 @@ class TxData { note: note ?? this.note, noteOnChain: noteOnChain ?? this.noteOnChain, memo: memo ?? this.memo, + otherData: otherData ?? this.otherData, utxos: utxos ?? this.utxos, usedUTXOs: usedUTXOs ?? this.usedUTXOs, recipients: recipients ?? this.recipients, @@ -321,6 +327,7 @@ class TxData { 'sparkRecipients: $sparkRecipients, ' 'sparkMints: $sparkMints, ' 'usedSparkCoins: $usedSparkCoins, ' + 'otherData: $otherData, ' 'tempTx: $tempTx, ' 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' diff --git a/lib/wallets/wallet/impl/xelis_wallet.dart b/lib/wallets/wallet/impl/xelis_wallet.dart new file mode 100644 index 000000000..87de162f7 --- /dev/null +++ b/lib/wallets/wallet/impl/xelis_wallet.dart @@ -0,0 +1,990 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:stack_wallet_backup/generate_password.dart'; +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; +import 'package:xelis_flutter/src/api/wallet.dart' as x_wallet; + +import '../../../models/balance.dart'; +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../models/paymint/fee_object_model.dart'; +import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../services/event_bus/global_event_bus.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/stack_file_system.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../models/tx_data.dart'; +import '../intermediate/lib_xelis_wallet.dart'; +import '../wallet.dart'; + +class XelisWallet extends LibXelisWallet { + Completer? _initCompleter; + + XelisWallet(CryptoCurrencyNetwork network) : super(Xelis(network)); + // ==================== Overrides ============================================ + + @override + int get isarTransactionVersion => 2; + + Future _restoreWallet() async { + final tablePath = await getPrecomputedTablesPath(); + final tableState = await getTableState(); + final xelisDir = await StackFileSystem.applicationXelisDirectory(); + final String name = walletId; + final String directory = xelisDir.path; + final password = await secureStorageInterface.read( + key: Wallet.mnemonicPassphraseKey(walletId: info.walletId), + ); + + final mnemonic = await getMnemonic(); + final seedLength = mnemonic.trim().split(" ").length; + + invalidSeedLengthCheck(seedLength); + + Logging.instance.i("Xelis: recovering wallet"); + final wallet = await x_wallet.createXelisWallet( + name: name, + directory: directory, + password: password!, + seed: mnemonic.trim(), + network: cryptoCurrency.network.xelisNetwork, + precomputedTablesPath: tablePath, + l1Low: tableState.currentSize.isLow, + ); + + await secureStorageInterface.write( + key: Wallet.mnemonicKey(walletId: walletId), + value: mnemonic.trim(), + ); + + libXelisWallet = wallet; + + await _finishInit(); + } + + Future _createNewWallet() async { + final tablePath = await getPrecomputedTablesPath(); + final tableState = await getTableState(); + final xelisDir = await StackFileSystem.applicationXelisDirectory(); + final String name = walletId; + final String directory = xelisDir.path; + final String password = generatePassword(); + + Logging.instance.d("Xelis: storing password"); + await secureStorageInterface.write( + key: Wallet.mnemonicPassphraseKey(walletId: info.walletId), + value: password, + ); + + final wallet = await x_wallet.createXelisWallet( + name: name, + directory: directory, + password: password, + network: cryptoCurrency.network.xelisNetwork, + precomputedTablesPath: tablePath, + l1Low: tableState.currentSize.isLow, + ); + + final mnemonic = await wallet.getSeed(); + await secureStorageInterface.write( + key: Wallet.mnemonicKey(walletId: walletId), + value: mnemonic.trim(), + ); + + libXelisWallet = wallet; + + await _finishInit(); + } + + Future _existingWallet() async { + Logging.instance.i("Xelis: opening existing wallet"); + final tablePath = await getPrecomputedTablesPath(); + final tableState = await getTableState(); + final xelisDir = await StackFileSystem.applicationXelisDirectory(); + final String name = walletId; + final String directory = xelisDir.path; + final password = await secureStorageInterface.read( + key: Wallet.mnemonicPassphraseKey(walletId: info.walletId), + ); + + libXelisWallet = await x_wallet.openXelisWallet( + name: name, + directory: directory, + password: password!, + network: cryptoCurrency.network.xelisNetwork, + precomputedTablesPath: tablePath, + l1Low: tableState.currentSize.isLow, + ); + + await _finishInit(); + } + + Future _finishInit() async { + if (await isTableUpgradeAvailable()) { + unawaited(updateTablesToDesiredSize()); + } + + final newReceivingAddress = + await getCurrentReceivingAddress() ?? + Address( + walletId: walletId, + derivationIndex: 0, + derivationPath: null, + value: libXelisWallet!.getAddressStr(), + publicKey: [], + type: AddressType.xelis, + subType: AddressSubType.receiving, + ); + + await mainDB.updateOrPutAddresses([newReceivingAddress]); + + if (info.cachedReceivingAddress != newReceivingAddress.value) { + await info.updateReceivingAddress( + newAddress: newReceivingAddress.value, + isar: mainDB.isar, + ); + } + } + + @override + Future init({bool? isRestore}) async { + Logging.instance.d("Xelis: init"); + + if (_initCompleter != null) { + await _initCompleter!.future; + return super.init(); + } + + _initCompleter = Completer(); + + try { + final bool walletExists = await LibXelisWallet.checkWalletExists( + walletId, + ); + + if (libXelisWallet == null) { + if (isRestore == true) { + await _restoreWallet(); + } else { + if (!walletExists) { + await _createNewWallet(); + } else { + await _existingWallet(); + } + } + } + _initCompleter!.complete(); + } catch (e, s) { + _initCompleter!.completeError(e); + Logging.instance.e( + "Xelis init() rethrowing error", + error: e, + stackTrace: s, + ); + rethrow; + } + + return super.init(); + } + + @override + Future recover({required bool isRescan}) async { + if (isRescan) { + await refreshMutex.protect(() async { + await mainDB.deleteWalletBlockchainData(walletId); + await updateTransactions(isRescan: true, topoheight: 0); + }); + return; + } + + // Borrowed from libmonero for now, need to refactor for Xelis view keys + // if (isViewOnly) { + // await recoverViewOnly(); + // return; + // } + + try { + await open(); + } catch (e, s) { + Logging.instance.e( + "Error rethrown from $runtimeType recover(isRescan: $isRescan)", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + @override + Future pingCheck() async { + try { + await libXelisWallet!.getDaemonInfo(); + await handleOnline(); + return true; + } catch (_) { + await handleOffline(); + return false; + } + } + + final _balanceUpdateMutex = Mutex(); + + @override + Future updateBalance({int? newBalance}) async { + await _balanceUpdateMutex.protect(() async { + try { + if (await libXelisWallet!.hasXelisBalance()) { + final BigInt xelBalance = + newBalance != null + ? BigInt.from(newBalance) + : await libXelisWallet! + .getXelisBalanceRaw(); // in the future, use getAssetBalances and handle each + final balance = Balance( + total: Amount( + rawValue: xelBalance, + fractionDigits: cryptoCurrency.fractionDigits, + ), + spendable: Amount( + rawValue: xelBalance, + fractionDigits: cryptoCurrency.fractionDigits, + ), + blockedTotal: Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } + } catch (e, s) { + Logging.instance.e( + "Error in $runtimeType updateBalance()", + error: e, + stackTrace: s, + ); + } + }); + } + + Future _fetchChainHeight() async { + final infoString = await libXelisWallet!.getDaemonInfo(); + final Map nodeInfo = + (json.decode(infoString) as Map).cast(); + + pruningHeight = + int.tryParse(nodeInfo['pruned_topoheight']?.toString() ?? '0') ?? 0; + return int.parse(nodeInfo['topoheight'].toString()); + } + + @override + Future updateChainHeight({int? topoheight}) async { + try { + final height = topoheight ?? await _fetchChainHeight(); + + await info.updateCachedChainHeight( + newHeight: height.toInt(), + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.e( + "Error in $runtimeType updateChainHeight()", + error: e, + stackTrace: s, + ); + } + } + + @override + Future updateNode() async { + try { + final bool online = await libXelisWallet!.isOnline(); + if (online == true) { + await libXelisWallet!.offlineMode(); + } + await super.connect(); + } catch (e, s) { + Logging.instance.e( + "Error rethrown from $runtimeType updateNode()", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + @override + Future> updateTransactions({ + bool isRescan = false, + List? rawTransactions, + int? topoheight, + }) async { + checkInitialized(); + + final newReceivingAddress = + await getCurrentReceivingAddress() ?? + Address( + walletId: walletId, + derivationIndex: 0, + derivationPath: null, + value: libXelisWallet!.getAddressStr(), + publicKey: [], + type: AddressType.xelis, + subType: AddressSubType.receiving, + ); + + final thisAddress = newReceivingAddress.value; + + int firstBlock = 0; + if (!isRescan) { + firstBlock = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .heightProperty() + .max() ?? + 0; + + if (firstBlock > 10) { + // add some buffer + firstBlock -= 10; + } + } else { + await libXelisWallet!.rescan(topoheight: BigInt.from(pruningHeight)); + } + + final txListJson = rawTransactions ?? await libXelisWallet!.allHistory(); + + final List txns = []; + + for (final jsonString in txListJson) { + try { + final transactionEntry = xelis_sdk.TransactionEntry.fromJson( + (json.decode(jsonString) as Map).cast(), + ); + + // Check for duplicates + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(transactionEntry.hash, walletId) + .findFirst(); + + if (storedTx != null && + storedTx.height != null && + storedTx.height! > 0) { + continue; // Skip already processed transactions + } + + final List outputs = []; + final List inputs = []; + TransactionType? txType; + const TransactionSubType txSubType = TransactionSubType.none; + int? nonce; + Amount fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + final Map otherData = {}; + + final entryType = transactionEntry.txEntryType; + + if (entryType is xelis_sdk.CoinbaseEntry) { + final coinbase = entryType; + txType = TransactionType.incoming; + + final int decimals = await libXelisWallet!.getAssetDecimals( + asset: xelis_sdk.xelisAsset, + ); + + fee = Amount(rawValue: BigInt.zero, fractionDigits: decimals); + + outputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: coinbase.reward.toString(), + addresses: [thisAddress], + walletOwns: true, + ), + ); + } else if (entryType is xelis_sdk.BurnEntry) { + final burn = entryType; + txType = TransactionType.outgoing; + + final int decimals = await libXelisWallet!.getAssetDecimals( + asset: burn.asset, + ); + + fee = Amount( + rawValue: BigInt.from(burn.fee), + fractionDigits: decimals, + ); + + inputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigAsm: null, + scriptSigHex: null, + sequence: null, + outpoint: null, + valueStringSats: burn.amount.toString(), + addresses: [thisAddress], + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + + outputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: burn.amount.toString(), + addresses: ['burn'], + walletOwns: false, + ), + ); + + otherData['burnAsset'] = burn.asset; + } else if (entryType is xelis_sdk.IncomingEntry) { + final incoming = entryType; + txType = + incoming.from == thisAddress + ? TransactionType.sentToSelf + : TransactionType.incoming; + + for (final transfer in incoming.transfers) { + final int decimals = await libXelisWallet!.getAssetDecimals( + asset: transfer.asset, + ); + + fee = Amount(rawValue: BigInt.zero, fractionDigits: decimals); + + outputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: transfer.amount.toString(), + addresses: [thisAddress], + walletOwns: true, + ), + ); + + otherData['asset_${transfer.asset}'] = transfer.amount.toString(); + if (transfer.extraData != null) { + otherData['extraData_${transfer.asset}'] = + transfer.extraData!.toJson(); + } + } + } else if (entryType is xelis_sdk.OutgoingEntry) { + final outgoing = entryType; + txType = TransactionType.outgoing; + nonce = outgoing.nonce; + + for (final transfer in outgoing.transfers) { + final int decimals = await libXelisWallet!.getAssetDecimals( + asset: transfer.asset, + ); + + fee = Amount( + rawValue: BigInt.from(outgoing.fee), + fractionDigits: decimals, + ); + + inputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + scriptSigAsm: null, + sequence: null, + outpoint: null, + addresses: [thisAddress], + valueStringSats: (transfer.amount + outgoing.fee).toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + + outputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: transfer.amount.toString(), + addresses: [transfer.destination], + walletOwns: false, + ), + ); + + otherData['asset_${transfer.asset}_amount'] = + transfer.amount.toString(); + otherData['asset_${transfer.asset}_fee'] = fee.raw.toString(); + if (transfer.extraData != null) { + otherData['extraData_${transfer.asset}'] = + transfer.extraData!.toJson(); + } + } + } else { + // Skip unknown entry types + continue; + } + + final txn = TransactionV2( + walletId: walletId, + blockHash: "", // Not provided in Xelis data + hash: transactionEntry.hash, + txid: transactionEntry.hash, + timestamp: + (transactionEntry.timestamp?.millisecondsSinceEpoch ?? 0) ~/ 1000, + height: transactionEntry.topoheight, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + version: -1, // Version not provided + type: txType, + subType: txSubType, + otherData: jsonEncode({ + ...otherData, + if (nonce != null) 'nonce': nonce, + 'overrideFee': fee.toJsonString(), + }), + ); + + // Logging.instance.log( + // "Entry done ${entryType.toString()}", + // level: LogLevel.Debug, + // ); + + txns.add(txn); + } catch (e, s) { + Logging.instance.w( + "Error in $runtimeType handling transaction: $jsonString", + error: e, + stackTrace: s, + ); + } + } + await updateBalance(); + + await mainDB.updateOrPutTransactionV2s(txns); + return txns.map((e) => e.txid).toList(); + } + + @override + Future updateUTXOs() async { + // not used in xel + return false; + } + + @override + Future checkSaveInitialReceivingAddress() async { + // do nothing + } + + @override + FilterOperation? get changeAddressFilterOperation => + throw UnimplementedError("Not used for $runtimeType"); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + @override + Future get fees async { + // TODO: implement _getFees... maybe + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: 1, + medium: 1, + slow: 1, + ); + } + + @override + Future prepareSend({required TxData txData, String? assetId}) async { + try { + checkInitialized(); + + final recipients = + txData.recipients?.isNotEmpty == true + ? txData.recipients! + : throw ArgumentError( + 'Address cannot be empty.', + ); // in the future, support for multiple recipients will work. + + final asset = assetId ?? xelis_sdk.xelisAsset; + + // Calculate total send amount + final totalSendAmount = recipients.fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (sum, recipient) => sum + recipient.amount, + ); + + // Check balance using raw method + final xelBalance = await libXelisWallet!.getXelisBalanceRaw(); + final balance = Amount( + rawValue: xelBalance, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + // Estimate fee using the shared method + final boostedFee = await estimateFeeFor( + totalSendAmount, + 1, + feeMultiplier: 1.0, + recipients: recipients, + assetId: asset, + ); + + // Check if we have enough for both transfers and fee + if (totalSendAmount + boostedFee > balance) { + final requiredAmt = await libXelisWallet!.formatCoin( + atomicAmount: (totalSendAmount + boostedFee).raw, + assetHash: asset, + ); + + final availableAmt = await libXelisWallet!.formatCoin( + atomicAmount: xelBalance, + assetHash: asset, + ); + + throw Exception( + "Insufficient balance to cover transfers and fees. " + "Required: $requiredAmt, Available: $availableAmt", + ); + } + + return txData.copyWith( + fee: boostedFee, + otherData: jsonEncode({'asset': asset}), + ); + } catch (_) { + // Logging.instance.log( + // "Exception rethrown from prepareSend(): $e\n$s", + // level: LogLevel.Error, + // ); + rethrow; + } + } + + @override + Future estimateFeeFor( + Amount amount, + int feeRate, { + double? feeMultiplier, + List recipients = const [], + String? assetId, + }) async { + try { + checkInitialized(); + final asset = assetId ?? xelis_sdk.xelisAsset; + + // Default values for a new wallet or when estimation fails + final defaultDecimals = cryptoCurrency.fractionDigits; + final defaultFee = BigInt.from(0); + + // Use default address if recipients list is empty to ensure basic fee estimates are readily available + final effectiveRecipients = + recipients.isNotEmpty + ? recipients + : [ + ( + address: + 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', + amount: amount, + isChange: false, + ), + ]; + + try { + final transfers = await Future.wait( + effectiveRecipients.map((recipient) async { + try { + final amt = double.parse( + await libXelisWallet!.formatCoin( + atomicAmount: recipient.amount.raw, + assetHash: asset, + ), + ); + return x_wallet.Transfer( + floatAmount: amt, + strAddress: recipient.address, + assetHash: asset, + extraData: null, + ); + } catch (e, s) { + // Handle formatCoin error - use default conversion + Logging.instance.d( + "formatCoin failed, using fallback conversion", + error: e, + stackTrace: s, + ); + final rawAmount = recipient.amount.raw; + final floatAmount = + rawAmount / BigInt.from(10).pow(defaultDecimals); + return x_wallet.Transfer( + floatAmount: floatAmount.toDouble(), + strAddress: recipient.address, + assetHash: asset, + extraData: null, + ); + } + }), + ); + + final decimals = await libXelisWallet!.getAssetDecimals(asset: asset); + final estimatedFee = double.parse( + await libXelisWallet!.estimateFees(transfers: transfers), + ); + final rawFee = (estimatedFee * pow(10, decimals)).round(); + return Amount( + rawValue: BigInt.from(rawFee), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } catch (e, s) { + Logging.instance.d( + "Fee estimation failed. Using fallback fee: $defaultFee", + error: e, + stackTrace: s, + ); + return Amount( + rawValue: defaultFee, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + } catch (_) { + // Logging.instance.log( + // "Exception rethrown from estimateFeeFor(): $e\n$s", + // level: LogLevel.Error, + // ); + rethrow; + } + } + + @override + Future confirmSend({required TxData txData}) async { + try { + checkInitialized(); + + // Validate recipients + if (txData.recipients == null || txData.recipients!.length != 1) { + throw Exception("$runtimeType confirmSend requires 1 recipient"); + } + + final recipient = txData.recipients!.first; + final Amount sendAmount = recipient.amount; + + final asset = + (txData.otherData != null + ? jsonDecode(txData.otherData!) + : null)?['asset'] + as String? ?? + xelis_sdk.xelisAsset; + + final amt = double.parse( + await libXelisWallet!.formatCoin( + atomicAmount: sendAmount.raw, + assetHash: asset, + ), + ); + + // Create a transfer transaction + final txJson = await libXelisWallet!.createTransfersTransaction( + transfers: [ + x_wallet.Transfer( + floatAmount: amt, + strAddress: recipient.address, + assetHash: asset, + extraData: null, // Add extra data if needed + ), + ], + ); + + final txMap = jsonDecode(txJson); + final txHash = txMap['hash'] as String; + + // Broadcast the transaction + await libXelisWallet!.broadcastTransaction(txHash: txHash); + + return await updateSentCachedTxData( + txData: txData.copyWith(txid: txHash), + ); + } catch (_) { + // Logging.instance.log( + // "Exception rethrown from confirmSend(): $e\n$s", + // level: LogLevel.Error, + // ); + rethrow; + } + } + + @override + Future handleEvent(Event event) async { + try { + switch (event) { + case NewTopoheight(:final height): + await handleNewTopoHeight(height); + case NewAsset(:final asset): + await handleNewAsset(asset); + case NewTransaction(:final transaction): + await handleNewTransaction(transaction); + case BalanceChanged(:final event): + await handleBalanceChanged(event); + case Rescan(:final startTopoheight): + await handleRescan(startTopoheight); + case Online(): + await handleOnline(); + case Offline(): + await handleOffline(); + case HistorySynced(:final topoheight): + await handleHistorySynced(topoheight); + } + } catch (e, s) { + Logging.instance.e( + "Error in $runtimeType handleEvent($event)", + error: e, + stackTrace: s, + ); + } + } + + @override + Future handleNewTopoHeight(int height) async { + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); + } + + @override + Future handleNewTransaction(xelis_sdk.TransactionEntry tx) async { + try { + final txListJson = [jsonEncode(tx.toString())]; + final newTxIds = await updateTransactions( + isRescan: false, + rawTransactions: txListJson, + ); + + await updateBalance(); + + // Logging.instance.log( + // "New transaction processed: ${newTxIds.first}", + // level: LogLevel.Info, + // ); + } catch (e, s) { + Logging.instance.e( + "Error in $runtimeType handleNewTransaction($tx)", + error: e, + stackTrace: s, + ); + } + } + + @override + Future handleBalanceChanged(xelis_sdk.BalanceChangedEvent event) async { + try { + final asset = event.assetHash; + if (asset == xelis_sdk.xelisAsset) { + await updateBalance(newBalance: event.balance); + } + + // TODO: Update asset balances if needed + } catch (e, s) { + Logging.instance.e( + "Error in $runtimeType handleBalanceChanged($event)", + error: e, + stackTrace: s, + ); + } + } + + @override + Future handleRescan(int startTopoheight) async { + await refreshMutex.protect(() async { + await mainDB.deleteWalletBlockchainData(walletId); + await updateTransactions(isRescan: true, topoheight: startTopoheight); + await updateBalance(); + }); + } + + @override + Future handleOnline() async { + await updateChainHeight(); + await updateBalance(); + await updateTransactions(); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + info.coin, + ), + ); + unawaited(refresh()); + } + + @override + Future handleOffline() async { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + info.coin, + ), + ); + } + + @override + Future handleHistorySynced(int topoheight) async { + await updateChainHeight(); + await updateBalance(); + await updateTransactions(); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + info.coin, + ), + ); + } + + @override + Future handleNewAsset(xelis_sdk.AssetData asset) async { + // TODO: Store asset information if needed + // TODO: Update UI/state for new asset + Logging.instance.d("New xelis asset detected: $asset"); + } + + @override + Future refresh({int? topoheight}) async { + await refreshMutex.protect(() async { + try { + final bool online = await libXelisWallet!.isOnline(); + if (online == true) { + await updateChainHeight(topoheight: topoheight); + await updateBalance(); + await updateTransactions(); + } else { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + info.coin, + ), + ); + } + } catch (e, s) { + Logging.instance.e( + "Error in $runtimeType refresh()", + error: e, + stackTrace: s, + ); + } + }); + } +} diff --git a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart index 131bf8f04..0d32e07a3 100644 --- a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart +++ b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart @@ -2,8 +2,9 @@ import '../../crypto_currency/intermediate/cryptonote_currency.dart'; import '../wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/mnemonic_interface.dart'; +import 'external_wallet.dart'; -abstract class CryptonoteWallet extends Wallet +abstract class CryptonoteWallet extends ExternalWallet with MnemonicInterface, CoinControlInterface { CryptonoteWallet(super.currency); } diff --git a/lib/wallets/wallet/intermediate/external_wallet.dart b/lib/wallets/wallet/intermediate/external_wallet.dart new file mode 100644 index 000000000..e5ce6b39b --- /dev/null +++ b/lib/wallets/wallet/intermediate/external_wallet.dart @@ -0,0 +1,12 @@ +import '../../crypto_currency/crypto_currency.dart'; +import '../wallet.dart'; + +// anstract class to be fleshed out for the standardization of wallet implementations +// that rely on bridged code libraries outside, or external native wallet functions +abstract class ExternalWallet extends Wallet { + ExternalWallet(super.currency); + + // wallet opening and initialization separated to prevent db lock collision errors + // must be overridden + Future open(); +} diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 5d176ec7d..b6b30de34 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -190,6 +190,7 @@ abstract class LibMoneroWallet } } + @override Future open() async { bool wasNull = false; diff --git a/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart b/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart new file mode 100644 index 000000000..901223469 --- /dev/null +++ b/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart @@ -0,0 +1,438 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; +import 'package:xelis_flutter/src/api/network.dart' as x_network; +import 'package:xelis_flutter/src/api/wallet.dart' as x_wallet; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/stack_file_system.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/intermediate/electrum_currency.dart'; +import '../wallet_mixin_interfaces/mnemonic_interface.dart'; +import 'external_wallet.dart'; + +enum XelisTableSize { + low, + full; + + bool get isLow => this == XelisTableSize.low; + + static XelisTableSize get platformDefault { + if (kIsWeb) { + return XelisTableSize.low; + } + return XelisTableSize.full; + } +} + +class XelisTableState { + final bool isGenerating; + final XelisTableSize currentSize; + final XelisTableSize _desiredSize; + + XelisTableSize get desiredSize { + if (kIsWeb) { + return XelisTableSize.low; + } + return _desiredSize; + } + + const XelisTableState({ + this.isGenerating = false, + this.currentSize = XelisTableSize.low, + XelisTableSize desiredSize = XelisTableSize.full, + }) : _desiredSize = desiredSize; + + XelisTableState copyWith({ + bool? isGenerating, + XelisTableSize? currentSize, + XelisTableSize? desiredSize, + }) { + return XelisTableState( + isGenerating: isGenerating ?? this.isGenerating, + currentSize: currentSize ?? this.currentSize, + desiredSize: kIsWeb ? XelisTableSize.low : (desiredSize ?? _desiredSize), + ); + } + + factory XelisTableState.fromJson(Map json) { + return XelisTableState( + isGenerating: json['isGenerating'] as bool, + currentSize: XelisTableSize.values[json['currentSize'] as int], + desiredSize: XelisTableSize.values[json['desiredSize'] as int], + ); + } + + Map toJson() => { + 'isGenerating': isGenerating, + 'currentSize': currentSize.index, + 'desiredSize': _desiredSize.index, + }; +} + +extension XelisNetworkConversion on CryptoCurrencyNetwork { + x_network.Network get xelisNetwork { + switch (this) { + case CryptoCurrencyNetwork.main: + return x_network.Network.mainnet; + case CryptoCurrencyNetwork.test: + return x_network.Network.testnet; + default: + throw ArgumentError('Unsupported network type for Xelis: $this'); + } + } +} + +extension CryptoCurrencyNetworkConversion on x_network.Network { + CryptoCurrencyNetwork get cryptoCurrencyNetwork { + switch (this) { + case x_network.Network.mainnet: + return CryptoCurrencyNetwork.main; + case x_network.Network.testnet: + return CryptoCurrencyNetwork.test; + default: + throw ArgumentError('Unsupported Xelis network type: $this'); + } + } +} + +sealed class Event { + const Event(); +} + +final class NewTopoheight extends Event { + final int height; + const NewTopoheight(this.height); +} + +final class NewAsset extends Event { + final xelis_sdk.AssetData asset; + const NewAsset(this.asset); +} + +final class NewTransaction extends Event { + final xelis_sdk.TransactionEntry transaction; + const NewTransaction(this.transaction); +} + +final class BalanceChanged extends Event { + final xelis_sdk.BalanceChangedEvent event; + const BalanceChanged(this.event); +} + +final class Rescan extends Event { + final int startTopoheight; + const Rescan(this.startTopoheight); +} + +final class Online extends Event { + const Online(); +} + +final class Offline extends Event { + const Offline(); +} + +final class HistorySynced extends Event { + final int topoheight; + const HistorySynced(this.topoheight); +} + +abstract class LibXelisWallet + extends ExternalWallet + with MnemonicInterface { + LibXelisWallet(super.currency); + + static const String _kHasFullTablesKey = 'xelis_has_full_tables'; + static const String _kGeneratingTablesKey = 'xelis_generating_tables'; + static const String _kWantsFullTablesKey = 'xelis_wants_full_tables'; + static final _tableGenerationMutex = Mutex(); + static Completer? _tableGenerationCompleter; + + x_wallet.XelisWallet? libXelisWallet; + int pruningHeight = 0; + + x_wallet.XelisWallet? get wallet => libXelisWallet; + set wallet(x_wallet.XelisWallet? newWallet) { + if (newWallet == null && libXelisWallet != null) { + throw StateError('Cannot set wallet to null after initialization'); + } + libXelisWallet = newWallet; + } + + void checkInitialized() { + if (libXelisWallet == null) { + throw StateError('libXelisWallet not initialized'); + } + } + + final syncMutex = Mutex(); + Timer? timer; + + StreamSubscription? _eventSubscription; + + Future getPrecomputedTablesPath() async { + if (kIsWeb) { + return ""; + } else { + final appDir = await StackFileSystem.applicationXelisTableDirectory(); + return "${appDir.path}${Platform.pathSeparator}"; + } + } + + Future getTableState() async { + final hasFullTables = + await secureStorageInterface.read(key: _kHasFullTablesKey) == 'true'; + final isGenerating = + await secureStorageInterface.read(key: _kGeneratingTablesKey) == 'true'; + final wantsFull = + await secureStorageInterface.read(key: _kWantsFullTablesKey) != 'false'; + + return XelisTableState( + isGenerating: isGenerating, + currentSize: hasFullTables ? XelisTableSize.full : XelisTableSize.low, + desiredSize: wantsFull ? XelisTableSize.full : XelisTableSize.low, + ); + } + + Future setTableState(XelisTableState state) async { + await secureStorageInterface.write( + key: _kHasFullTablesKey, + value: state.currentSize == XelisTableSize.full ? 'true' : 'false', + ); + await secureStorageInterface.write( + key: _kGeneratingTablesKey, + value: state.isGenerating ? 'true' : 'false', + ); + await secureStorageInterface.write( + key: _kWantsFullTablesKey, + value: state.desiredSize == XelisTableSize.full ? 'true' : 'false', + ); + } + + Stream convertRawEvents() async* { + checkInitialized(); + final rawEventStream = libXelisWallet!.eventsStream(); + + await for (final rawData in rawEventStream) { + final json = jsonDecode(rawData); + try { + final eventType = xelis_sdk.WalletEvent.fromStr( + json['event'] as String, + ); + switch (eventType) { + case xelis_sdk.WalletEvent.newTopoHeight: + yield NewTopoheight(json['data']['topoheight'] as int); + case xelis_sdk.WalletEvent.newAsset: + yield NewAsset( + xelis_sdk.AssetData.fromJson( + json['data'] as Map, + ), + ); + case xelis_sdk.WalletEvent.newTransaction: + yield NewTransaction( + xelis_sdk.TransactionEntry.fromJson( + json['data'] as Map, + ), + ); + case xelis_sdk.WalletEvent.balanceChanged: + yield BalanceChanged( + xelis_sdk.BalanceChangedEvent.fromJson( + json['data'] as Map, + ), + ); + case xelis_sdk.WalletEvent.rescan: + yield Rescan(json['data']['start_topoheight'] as int); + case xelis_sdk.WalletEvent.online: + yield const Online(); + case xelis_sdk.WalletEvent.offline: + yield const Offline(); + case xelis_sdk.WalletEvent.historySynced: + yield HistorySynced(json['data']['topoheight'] as int); + } + } catch (e, s) { + Logging.instance.e( + "Error processing xelis wallet event: $rawData", + error: e, + stackTrace: s, + ); + continue; + } + } + } + + Future handleEvent(Event event) async {} + Future handleNewTopoHeight(int height); + Future handleNewTransaction(xelis_sdk.TransactionEntry tx); + Future handleBalanceChanged(xelis_sdk.BalanceChangedEvent event); + Future handleRescan(int startTopoheight) async {} + Future handleOnline() async {} + Future handleOffline() async {} + Future handleHistorySynced(int topoheight) async {} + Future handleNewAsset(xelis_sdk.AssetData asset) async {} + + @override + Future refresh({int? topoheight}); + + Future connect() async { + final node = getCurrentNode(); + try { + _eventSubscription = convertRawEvents().listen(handleEvent); + + Logging.instance.i("Connecting to node: ${node.host}:${node.port}"); + await libXelisWallet!.onlineMode( + daemonAddress: "${node.host}:${node.port}", + ); + await super.refresh(); + } catch (e, s) { + Logging.instance.e( + "rethrowing error connecting to node: $node", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + List get standardReceivingAddressFilters => [ + FilterCondition.equalTo(property: r"type", value: info.mainAddressType), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.receiving, + ), + ]; + + List get standardChangeAddressFilters => [ + FilterCondition.equalTo(property: r"type", value: info.mainAddressType), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.change, + ), + ]; + + static Future checkWalletExists(String walletId) async { + final xelisDir = await StackFileSystem.applicationXelisDirectory(); + final walletDir = Directory( + "${xelisDir.path}${Platform.pathSeparator}$walletId", + ); + // TODO: should we check for certain files within the dir? + return await walletDir.exists(); + } + + @override + Future open() async { + try { + await connect(); + } catch (e) { + // Logging.instance.log( + // "Failed to start sync: $e", + // level: LogLevel.Error, + // ); + rethrow; + } + unawaited(refresh()); + } + + @override + Future exit() async { + await refreshMutex.protect(() async { + timer?.cancel(); + timer = null; + + await _eventSubscription?.cancel(); + _eventSubscription = null; + + await libXelisWallet?.offlineMode(); + await super.exit(); + }); + } + + void invalidSeedLengthCheck(int length) { + if (!(length == 25)) { + throw Exception("Invalid Xelis mnemonic length found: $length"); + } + } +} + +extension XelisTableManagement on LibXelisWallet { + Future isTableUpgradeAvailable() async { + if (kIsWeb) return false; + + final state = await getTableState(); + return state.currentSize != state.desiredSize; + } + + Future updateTablesToDesiredSize() async { + if (kIsWeb) return; + + await Future.delayed(const Duration(seconds: 1)); + if (LibXelisWallet._tableGenerationCompleter != null) { + try { + await LibXelisWallet._tableGenerationCompleter!.future; + return; + } catch (_) { + // Previous generation failed, we'll try again + } + } + + await LibXelisWallet._tableGenerationMutex.protect(() async { + // Check again after acquiring mutex + if (LibXelisWallet._tableGenerationCompleter != null) { + try { + await LibXelisWallet._tableGenerationCompleter!.future; + return; + } catch (_) { + // Previous generation failed, we'll try again + } + } + + final state = await getTableState(); + if (state.currentSize == state.desiredSize) return; + + LibXelisWallet._tableGenerationCompleter = Completer(); + await setTableState(state.copyWith(isGenerating: true)); + + try { + Logging.instance.i("Xelis: Generating large tables in background"); + + final tablePath = await getPrecomputedTablesPath(); + await x_wallet.updateTables( + precomputedTablesPath: tablePath, + l1Low: state.desiredSize.isLow, + ); + + await setTableState( + XelisTableState( + isGenerating: false, + currentSize: state.desiredSize, + desiredSize: state.desiredSize, + ), + ); + + Logging.instance.i("Xelis: Table upgrade done"); + LibXelisWallet._tableGenerationCompleter!.complete(); + } catch (e) { + // Logging.instance.log( + // "Failed to update tables: $e\n$s", + // level: LogLevel.Error, + // ); + await setTableState(state.copyWith(isGenerating: false)); + + LibXelisWallet._tableGenerationCompleter!.completeError(e); + } finally { + if (!LibXelisWallet._tableGenerationCompleter!.isCompleted) { + LibXelisWallet._tableGenerationCompleter!.completeError( + Exception('Table generation abandoned'), + ); + } + LibXelisWallet._tableGenerationCompleter = null; + } + }); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 161c68f5f..be21c5282 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -47,6 +47,7 @@ import 'impl/stellar_wallet.dart'; import 'impl/sub_wallets/eth_token_wallet.dart'; import 'impl/tezos_wallet.dart'; import 'impl/wownero_wallet.dart'; +import 'impl/xelis_wallet.dart'; import 'intermediate/cryptonote_wallet.dart'; import 'wallet_mixin_interfaces/electrumx_interface.dart'; import 'wallet_mixin_interfaces/lelantus_interface.dart'; @@ -172,8 +173,8 @@ abstract class Wallet { value: viewOnlyData!.toJsonEncodedString(), ); } else if (wallet is MnemonicInterface) { - if (wallet is CryptonoteWallet) { - // currently a special case due to the xmr/wow libraries handling their + if (wallet is CryptonoteWallet || wallet is XelisWallet) { // + // currently a special case due to the xmr/wow/xelis libraries handling their // own mnemonic generation on new wallet creation // if its a restore we must set them if (mnemonic != null) { @@ -406,6 +407,9 @@ abstract class Wallet { case const (Wownero): return WowneroWallet(net); + case const (Xelis): + return XelisWallet(net); + default: // should never hit in reality throw Exception("Unknown crypto currency: ${walletInfo.coin}"); diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 4981dafaf..ed49e5ebb 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -28,7 +28,7 @@ import '../utilities/util.dart'; import '../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../wallets/wallet/impl/ethereum_wallet.dart'; import '../wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; -import '../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../wallets/wallet/intermediate/external_wallet.dart'; import '../wallets/wallet/wallet.dart'; import 'conditional_parent.dart'; import 'desktop/primary_button.dart'; @@ -111,7 +111,7 @@ class SimpleWalletCard extends ConsumerWidget { if (context.mounted) { final Future loadFuture; - if (wallet is LibMoneroWallet) { + if (wallet is ExternalWallet) { loadFuture = wallet.init().then((value) async => await (wallet).open()); } else { loadFuture = wallet.init(); diff --git a/lib/widgets/xelis_table_progress.dart b/lib/widgets/xelis_table_progress.dart new file mode 100644 index 000000000..ffecba861 --- /dev/null +++ b/lib/widgets/xelis_table_progress.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../widgets/progress_bar.dart'; + +import '../providers/providers.dart'; + +class XelisTableProgress extends ConsumerWidget { + const XelisTableProgress({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final progressAsyncValue = ref.watch(xelisTableProgressProvider); + + return DefaultTextStyle( + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge?.color ?? Colors.black, + fontSize: 14, + ), + child: Center( + child: progressAsyncValue.when( + data: (progress) => Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxWidth: 450), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Generating Precomputed Tables...", + style: STextStyles.desktopH3(context).copyWith( + fontSize: 24, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + "These tables are required for the fast decryption of private transactions. This is a one-time process upon the creation of your first Xelis wallet in Stack Wallet.", + style: STextStyles.subtitle600(context).copyWith( + fontSize: 14, + color: Theme.of(context).extension()!.textSubtitle1, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + progress.currentStep.displayName, + style: STextStyles.titleBold12(context), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ProgressBar( + width: 200, + height: 8, + fillColor: const Color.fromARGB(255,2,255,207), + backgroundColor: Theme.of(context).extension()!.textFieldDefaultBG, + percent: progress.tableProgress ?? 0.0, + ), + const SizedBox(height: 4), + Text( + "${((progress.tableProgress ?? 0.0) * 100).toStringAsFixed(1)}%", + style: STextStyles.label(context), + ), + ], + ), + ), + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ), + ), + ); + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 72a81025c..aa67f97ac 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -21,6 +21,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_libsparkmobile frostdart tor_ffi_plugin + xelis_flutter ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index fefce84ae..a51d63f71 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -40,7 +40,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - ReachabilitySwift (5.0.0) + - ReachabilitySwift (5.2.3) - share_plus (0.0.1): - FlutterMacOS - "sqlite3 (3.46.0+1)": @@ -67,6 +67,8 @@ PODS: - FlutterMacOS - window_size (0.0.2): - FlutterMacOS + - xelis_flutter (0.0.1): + - FlutterMacOS DEPENDENCIES: - camera_macos (from `Flutter/ephemeral/.symlinks/plugins/camera_macos/macos`) @@ -95,6 +97,7 @@ DEPENDENCIES: - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) + - xelis_flutter (from `Flutter/ephemeral/.symlinks/plugins/xelis_flutter/macos`) SPEC REPOS: trunk: @@ -154,6 +157,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos window_size: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos + xelis_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/xelis_flutter/macos SPEC CHECKSUMS: camera_macos: c2603f5eed16f05076cf17e12030d2ce55a77839 @@ -173,9 +178,9 @@ SPEC CHECKSUMS: isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a lelantus: 308e42c5a648598936a07a234471dd8cf8e687a0 local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 - package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 @@ -184,6 +189,7 @@ SPEC CHECKSUMS: url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 + xelis_flutter: 34e05f3621e46381fb1b10d7c11f63764d3f7a80 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 diff --git a/pubspec.lock b/pubspec.lock index 0da4f60f4..8bdf56e80 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,10 +50,10 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: "direct main" description: @@ -183,50 +183,58 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -239,10 +247,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.5" calendar_date_picker2: dependency: "direct main" description: @@ -404,10 +412,10 @@ packages: dependency: transitive description: name: coverage - sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" cross_file: dependency: transitive description: @@ -564,10 +572,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" dartx: dependency: transitive description: @@ -742,10 +750,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -823,9 +831,11 @@ packages: flutter_libsparkmobile: dependency: "direct main" description: - path: "../flutter_libsparkmobile" - relative: true - source: path + path: "." + ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b + resolved-ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b + url: "https://github.com/cypherstack/flutter_libsparkmobile.git" + source: git version: "0.0.2" flutter_lints: dependency: "direct dev" @@ -871,10 +881,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted - version: "2.0.23" + version: "2.0.24" flutter_riverpod: dependency: "direct main" description: @@ -883,6 +893,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" + url: "https://pub.dev" + source: hosted + version: "2.9.0" flutter_secure_storage: dependency: "direct main" description: @@ -935,10 +953,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "1b7723a814d84fb65869ea7115cdb3ee7c3be5a27a755c1ec60e049f6b9fcbb2" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -990,10 +1008,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" google_fonts: dependency: "direct main" description: @@ -1070,18 +1088,18 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" ieee754: dependency: transitive description: @@ -1123,10 +1141,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" isar: dependency: "direct main" description: @@ -1179,10 +1197,18 @@ packages: dependency: transitive description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" + jsontool: + dependency: transitive + description: + name: jsontool + sha256: e49bf419e82d90f009426cd7fdec8d54ba8382975b3454ed16a3af3ee1d1b697 + url: "https://pub.dev" + source: hosted + version: "2.1.0" keyboard_dismisser: dependency: "direct main" description: @@ -1428,26 +1454,26 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 + sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.3" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" path: dependency: transitive description: @@ -1460,10 +1486,10 @@ packages: dependency: transitive description: name: path_parsing - sha256: caa17e8f0b386eb190dd5b6a3b71211c76375aa8b6ffb4465b5863d019bdb334 + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1476,18 +1502,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.16" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1636,18 +1662,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" qr: dependency: transitive description: @@ -1732,10 +1758,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -1756,10 +1782,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -1778,10 +1804,10 @@ packages: description: path: "." ref: master - resolved-ref: b1fa8ca505e7e488edb4c2859f0218d48b15dead + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 url: "https://github.com/cypherstack/socks_socket.git" source: git - version: "1.0.0" + version: "1.1.1" solana: dependency: "direct main" description: @@ -1803,10 +1829,10 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -1819,10 +1845,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -1892,10 +1918,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -1981,10 +2007,10 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tint: dependency: transitive description: @@ -2062,26 +2088,26 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -2094,18 +2120,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -2118,26 +2144,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0b9149c6ddb013818075b072b9ddc1b89a5122fff1275d4648d297086b46c4f0" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.12" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.12" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: f3b9b6e4591c11394d4be4806c63e72d3a41778547b2c1e2a8a04fadcfd7d173 + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.12" + version: "1.1.16" vector_math: dependency: transitive description: @@ -2146,6 +2172,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + very_good_analysis: + dependency: transitive + description: + name: very_good_analysis + sha256: "62d2b86d183fb81b2edc22913d9f155d26eb5cf3855173adb1f59fac85035c63" + url: "https://pub.dev" + source: hosted + version: "7.0.0" vm_service: dependency: transitive description: @@ -2207,10 +2241,10 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: "direct overridden" description: @@ -2235,6 +2269,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" + web_socket_client: + dependency: transitive + description: + name: web_socket_client + sha256: "0ec5230852349191188c013112e4d2be03e3fc83dbe80139ead9bf3a136e53b5" + url: "https://pub.dev" + source: hosted + version: "0.1.5" webdriver: dependency: transitive description: @@ -2255,10 +2297,10 @@ packages: dependency: "direct overridden" description: name: win32 - sha256: "10169d3934549017f0ae278ccb07f828f9d6ea21573bab0fb77b0e1ef0fce454" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.7.2" + version: "5.10.1" win32_registry: dependency: transitive description: @@ -2284,6 +2326,23 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xelis_dart_sdk: + dependency: transitive + description: + name: xelis_dart_sdk + sha256: "2a7f8ab4c30fad2fd824ba6ea4e83ac20c726b47c7aa4f1e713ef3971a3ec1f7" + url: "https://pub.dev" + source: hosted + version: "0.24.0" + xelis_flutter: + dependency: "direct main" + description: + path: "." + ref: "v0.1.0" + resolved-ref: c685c5d3550cca414ec30d4b61259761f129dda6 + url: "https://github.com/Tritonn204/xelis_flutter_ffi.git" + source: git + version: "0.1.0" xml: dependency: transitive description: @@ -2304,10 +2363,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" zxcvbn: dependency: "direct main" description: diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index 0fd8e5e8a..e46420fa0 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -73,6 +73,7 @@ final List _supportedCoins = List.unmodifiable([ Stellar(CryptoCurrencyNetwork.main), Tezos(CryptoCurrencyNetwork.main), Wownero(CryptoCurrencyNetwork.main), + Xelis(CryptoCurrencyNetwork.main), Bitcoin(CryptoCurrencyNetwork.test), Bitcoin(CryptoCurrencyNetwork.test4), Bitcoincash(CryptoCurrencyNetwork.test), @@ -83,6 +84,7 @@ final List _supportedCoins = List.unmodifiable([ Litecoin(CryptoCurrencyNetwork.test), Peercoin(CryptoCurrencyNetwork.test), Stellar(CryptoCurrencyNetwork.test), + Xelis(CryptoCurrencyNetwork.test), ]); final ({String from, String to}) _swapDefaults = (from: "BTC", to: "XMR"); diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 5f009019a..62b1947ca 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -30,10 +30,15 @@ dependencies: frostdart: path: ./crypto_plugins/frostdart + xelis_flutter: + git: + url: https://github.com/Tritonn204/xelis_flutter_ffi.git + ref: v0.1.0 + flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: 28d0f6c8db56a7d14d6495e810b8705a5c438929 + ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b # cs_monero compat (unpublished) compat: diff --git a/scripts/windows/build_secp256k1.bat b/scripts/windows/build_secp256k1.bat index f5777b974..bae7c9788 100644 --- a/scripts/windows/build_secp256k1.bat +++ b/scripts/windows/build_secp256k1.bat @@ -7,6 +7,6 @@ git reset --hard cmake -G "Visual Studio 17 2022" -A x64 -S . -B build cd build cmake --build . -if not exist "..\..\..\..\build\" mkdir "..\..\..\..\build\" -xcopy bin\Debug\libsecp256k1-2.dll "..\..\..\..\build\secp256k1.dll" /Y +if not exist "..\..\..\..\..\build\" mkdir "..\..\..\..\..\build\" +xcopy bin\Debug\libsecp256k1-2.dll "..\..\..\..\..\build\secp256k1.dll" /Y cd ..\..\..\ diff --git a/test/price_test.dart b/test/price_test.dart index dc7aaeb9e..2a5b9c038 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -30,7 +30,7 @@ void main() { url: Uri.parse( "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids" "=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,bitcoin-cash" - ",namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos" + ",namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos,xelis" "&order=market_cap_desc&per_page=50" "&page=1&sparkline=false"), headers: { @@ -93,7 +93,10 @@ void main() { 'max_supply":null,"ath":0.00013848,"ath_change_percentage":-79.75864' ',"ath_date":"2021-12-11T08:39:41.129Z","atl":5.74028e-07,"atl_chang' 'e_percentage":4783.08078,"atl_date":"2020-03-13T16:55:01.177Z","roi' - '":null,"last_updated":"2022-08-22T16:38:32.826Z"}]'), + '":null,"last_updated":"2022-08-22T16:38:32.826Z"},{"id":"xelis","sy' + 'mbol":"xel","name":"Xelis","image":"https://assets.coingecko.com/co' + 'ins/images/37615/large/green_background_black_logo.png","current_pr' + 'ice":0.00001234,"price_change_percentage_24h":5.67}]'), 200)); final priceAPI = PriceAPI(client); @@ -125,7 +128,8 @@ void main() { 'Coin.dogecoinTestNet: [0, 0.0], ' 'Coin.firoTestNet: [0, 0.0], ' 'Coin.litecoinTestNet: [0, 0.0], ' - 'Coin.stellarTestnet: [0, 0.0]' + 'Coin.stellarTestnet: [0, 0.0], ' + 'Coin.xelis: [0.00001234, 5.67]' '}', ); verify(client.get( @@ -134,7 +138,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" - ",tezos" + ",tezos,xelis" "&order=market_cap_desc&per_page=50&page=1&sparkline=false", ), headers: {'Content-Type': 'application/json'})).called(1); @@ -151,7 +155,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&" "ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" - ",tezos" + ",tezos,xelis" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -213,7 +217,10 @@ void main() { '21000000.0,"max_supply":null,"ath":0.00013848,"ath_change_percentag' 'e":-79.75864,"ath_date":"2021-12-11T08:39:41.129Z","atl":5.74028e-0' '7,"atl_change_percentage":4783.08078,"atl_date":"2020-03-13T16:55:01' - '.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"}]'), + '.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"},{"id":' + '"xelis","symbol":"xel","name":"Xelis","image":"https://assets.coing' + 'ecko.com/coins/images/37615/large/green_background_black_logo.png",' + '"current_price":0.00001234,"price_change_percentage_24h":5.67}]'), 200)); final priceAPI = PriceAPI(client); @@ -247,7 +254,8 @@ void main() { 'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], ' 'Coin.firoTestNet: [0, 0.0], ' 'Coin.litecoinTestNet: [0, 0.0], ' - 'Coin.stellarTestnet: [0, 0.0]' + 'Coin.stellarTestnet: [0, 0.0], ' + 'Coin.xelis: [0.00001234, 5.67]' '}', ); @@ -258,7 +266,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids" "=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" - ",tezos" + ",tezos,xelis" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); @@ -274,7 +282,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" - ",tezos" + ",tezos,xelis" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -337,7 +345,9 @@ void main() { 'y":21000000.0,"max_supply":null,"ath":0.00013848,"ath_change_perce' 'ntage":-79.75864,"ath_date":"2021-12-11T08:39:41.129Z","atl":5.74' '028e-07,"atl_change_percentage":4783.08078,"atl_date":"2020-03-13T' - '16:55:01.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"}]'), + '16:55:01.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"' + '},{"id":"xelis","symbol":xel,"name":com/coins/images/37615/large/g' + 'reen_background_black_logo.png,"image":"https://assets.coingecko'), 200)); final priceAPI = PriceAPI(client); @@ -368,7 +378,8 @@ void main() { 'Coin.dogecoinTestNet: [0, 0.0], ' 'Coin.firoTestNet: [0, 0.0], ' 'Coin.litecoinTestNet: [0, 0.0], ' - 'Coin.stellarTestnet: [0, 0.0]' + 'Coin.stellarTestnet: [0, 0.0], ' + 'Coin.xelis: [0, 0.0]' '}', ); }); @@ -382,7 +393,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" - ",tezos" + ",tezos,xelis" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -418,7 +429,8 @@ void main() { 'Coin.dogecoinTestNet: [0, 0.0], ' 'Coin.firoTestNet: [0, 0.0], ' 'Coin.litecoinTestNet: [0, 0.0], ' - 'Coin.stellarTestnet: [0, 0.0]' + 'Coin.stellarTestnet: [0, 0.0], ' + 'Coin.xelis: [0, 0.0]' '}', ); }); diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 2f370e685..55c2cc622 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -24,6 +24,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_libsparkmobile frostdart tor_ffi_plugin + xelis_flutter ) set(PLUGIN_BUNDLED_LIBRARIES)