diff --git a/.gitignore b/.gitignore index 84a7ecdcd..98ac9ab3a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.log *.pyc *.swp +*.zip .DS_Store .atom/ .buildlog/ @@ -140,6 +141,7 @@ lib/tron/tron.dart lib/wownero/wownero.dart lib/zano/zano.dart lib/decred/decred.dart +lib/xelis/xelis.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png diff --git a/README.md b/README.md index ea796dbf2..1790c4e68 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,12 @@ Cake Wallet includes support for several cryptocurrencies, including: * Automatically generate new addresses * Specify multiple recipients for batch sending +### Xelis Specific Features + +* Store XEL and all native assets/tokens +* Add custom tokens by asset ID +* Specify multiple recipients for batch sending + # Monero.com by Cake Wallet for Android and iOS ## Open Source Monero-Only Wallet diff --git a/analysis_options.yaml b/analysis_options.yaml index bd35233ba..2ee64b902 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -23,6 +23,7 @@ analyzer: lib/tron/cw_tron.dart, lib/wownero/cw_wownero.dart, lib/zano/cw_zano.dart, + lib/xelis/cw_xelis.dart, ] language: strict-casts: true diff --git a/android/.project b/android/.project index 17c95d4b1..9b27d1b7f 100644 --- a/android/.project +++ b/android/.project @@ -14,4 +14,15 @@ org.eclipse.buildship.core.gradleprojectnature + + + 1744859372158 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index 9d2efc8e7..e47955840 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -1,2 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= -eclipse.preferences.version=1 \ No newline at end of file +eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=false +show.console.view=false +show.executions.view=false diff --git a/assets/images/xelis_icon.png b/assets/images/xelis_icon.png new file mode 100644 index 000000000..9239ab811 Binary files /dev/null and b/assets/images/xelis_icon.png differ diff --git a/assets/images/xelis_testnet_icon.png b/assets/images/xelis_testnet_icon.png new file mode 100644 index 000000000..3fb68b4be Binary files /dev/null and b/assets/images/xelis_testnet_icon.png differ diff --git a/assets/xelis_node_list.yml b/assets/xelis_node_list.yml new file mode 100644 index 000000000..db92ebcf5 --- /dev/null +++ b/assets/xelis_node_list.yml @@ -0,0 +1,25 @@ +- + uri: us-node.xelis.io + useSSL: true + is_default: true +- + uri: pl-node.xelis.io + useSSL: true +- + uri: de-node.xelis.io + useSSL: true +- + uri: fr-node.xelis.io + useSSL: true +- + uri: sg-node.xelis.io + useSSL: true +- + uri: uk-node.xelis.io + useSSL: true +- + uri: ca-node.xelis.io + useSSL: true +- + uri: testnet-node.xelis.io + useSSL: true \ No newline at end of file diff --git a/cakewallet.bat b/cakewallet.bat index 1904c5710..86c7abe21 100644 --- a/cakewallet.bat +++ b/cakewallet.bat @@ -1,5 +1,5 @@ @echo off -set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron +set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --xelis set cw_root=%cd% set cw_archive_name=Cake Wallet.zip set cw_archive_path=%cw_root%\%cw_archive_name% @@ -24,7 +24,7 @@ IF NOT EXIST "%secrets_file_path%" ( ) ELSE (echo === Using previously/already generated secrets file: %secrets_file_path% ===) echo === Generating mobx models === -for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron .) do ( +for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron cw_xelis.) do ( cd %%i call flutter pub get > nul call dart run build_runner build --delete-conflicting-outputs > nul diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 4b0f9521e..366e19e50 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -111,7 +111,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.zano, CryptoCurrency.ton, CryptoCurrency.flip, - CryptoCurrency.deuro + CryptoCurrency.deuro, + CryptoCurrency.xel, + CryptoCurrency.xet ]; static const havenCurrencies = [ @@ -234,6 +236,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); + static const xel = CryptoCurrency(title: 'XEL', fullName: 'Xelis', raw: 99, name: 'xel', iconPath: 'assets/images/xelis_icon.png', decimals: 8); + static const xet = CryptoCurrency(title: 'XET', fullName: 'Testnet Xelis', raw: 100, name: 'xet', iconPath: 'assets/images/xelis_testnet_icon.png', decimals: 8); + static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { acc.addAll({item.raw: item}); diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 0f913cb79..0ae26d4b7 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -34,6 +34,11 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.zano; case WalletType.decred: return CryptoCurrency.dcr; + case WalletType.xelis: + if (isTestnet == true) { + return CryptoCurrency.xet; + } + return CryptoCurrency.xel; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); @@ -70,6 +75,8 @@ WalletType? walletTypeForCurrency(CryptoCurrency currency) { return WalletType.zano; case CryptoCurrency.dcr: return WalletType.decred; + case CryptoCurrency.xel: + return WalletType.xelis; default: return null; } diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index e7c20a444..60ef5291a 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -22,3 +22,4 @@ const MWEB_UTXO_TYPE_ID = 20; const HAVEN_SEED_STORE_TYPE_ID = 21; const ZANO_ASSET_TYPE_ID = 22; const PAYJOIN_SESSION_TYPE_ID = 23; +const XELIS_ASSET_TYPE_ID = 24; \ No newline at end of file diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 38fcde9e1..a5cebfaae 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -104,6 +104,7 @@ class Node extends HiveObject with Keyable { case WalletType.tron: case WalletType.zano: case WalletType.decred: + case WalletType.xelis: return Uri.parse( "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}"); case WalletType.none: @@ -165,6 +166,7 @@ class Node extends HiveObject with Keyable { case WalletType.polygon: case WalletType.solana: case WalletType.tron: + case WalletType.xelis: return requestElectrumServer(); case WalletType.zano: return requestZanoNode(); @@ -360,13 +362,13 @@ class Node extends HiveObject with Keyable { } Future requestDecredNode() async { - if (uri.host == "default-spv-nodes") { - // Just show default port as ok. The wallet will connect to a list of known - // nodes automatically. - return true; - } - try { - final socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + if (uri.host == "default-spv-nodes") { + // Just show default port as ok. The wallet will connect to a list of known + // nodes automatically. + return true; + } + try { + final socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); socket.destroy(); return true; } catch (_) { diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 40a2b31d5..141159165 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -18,6 +18,7 @@ const walletTypes = [ WalletType.tron, WalletType.zano, WalletType.decred, + WalletType.xelis, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -65,7 +66,10 @@ enum WalletType { zano, @HiveField(14) - decred + decred, + + @HiveField(15) + xelis } int serializeToInt(WalletType type) { @@ -98,6 +102,8 @@ int serializeToInt(WalletType type) { return 12; case WalletType.decred: return 13; + case WalletType.xelis: + return 14; case WalletType.none: return -1; } @@ -133,6 +139,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.zano; case 13: return WalletType.decred; + case 14: + return WalletType.xelis; default: throw Exception( 'Unexpected token: $raw for WalletType deserializeFromInt'); @@ -169,6 +177,8 @@ String walletTypeToString(WalletType type) { return 'Zano'; case WalletType.decred: return 'Decred'; + case WalletType.xelis: + return 'Xelis'; case WalletType.none: return ''; } @@ -204,6 +214,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Zano (ZANO)'; case WalletType.decred: return 'Decred (DCR)'; + case WalletType.xelis: + return 'Xelis (XEL)'; case WalletType.none: return ''; } @@ -242,6 +254,11 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.zano; case WalletType.decred: return CryptoCurrency.dcr; + case WalletType.xelis: + if (isTestnet) { + return CryptoCurrency.xet; + } + return CryptoCurrency.xel; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_xelis/.gitignore b/cw_xelis/.gitignore new file mode 100644 index 000000000..71e9e0b83 --- /dev/null +++ b/cw_xelis/.gitignore @@ -0,0 +1,38 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +flutter_rust_bridge_local.yaml +build/ +rust_temp/ +rust_src/ +lib/src/** +xelis-flutter-ffi/ \ No newline at end of file diff --git a/cw_xelis/.metadata b/cw_xelis/.metadata new file mode 100644 index 000000000..b53fa71d5 --- /dev/null +++ b/cw_xelis/.metadata @@ -0,0 +1,27 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_xelis/CHANGELOG.md b/cw_xelis/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_xelis/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_xelis/LICENSE b/cw_xelis/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_xelis/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_xelis/README.md b/cw_xelis/README.md new file mode 100644 index 000000000..8c8f297a2 --- /dev/null +++ b/cw_xelis/README.md @@ -0,0 +1,18 @@ +# cw_xelis + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + +The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. +To add platforms, run `flutter create -t plugin --platforms .` in this directory. +You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/to/pubspec-plugin-platforms. diff --git a/cw_xelis/analysis_options.yaml b/cw_xelis/analysis_options.yaml new file mode 100644 index 000000000..0f6b02050 --- /dev/null +++ b/cw_xelis/analysis_options.yaml @@ -0,0 +1,13 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - "lib/src/api/generated/**" + - "**/*.g.dart" + - "**/*frb_generated*.dart" + errors: + # Ignore specific errors that might come from generated code + invalid_annotation_target: ignore + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_xelis/example/.gitignore b/cw_xelis/example/.gitignore new file mode 100644 index 000000000..79c113f9b --- /dev/null +++ b/cw_xelis/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/cw_xelis/example/README.md b/cw_xelis/example/README.md new file mode 100644 index 000000000..6f803ebe2 --- /dev/null +++ b/cw_xelis/example/README.md @@ -0,0 +1,16 @@ +# cw_xelis_example + +Demonstrates how to use the cw_xelis plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/cw_xelis/example/analysis_options.yaml b/cw_xelis/example/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/cw_xelis/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_xelis/example/integration_test/plugin_integration_test.dart b/cw_xelis/example/integration_test/plugin_integration_test.dart new file mode 100644 index 000000000..fa7d7c92f --- /dev/null +++ b/cw_xelis/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,25 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://flutter.dev/to/integration-testing + + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:cw_xelis/cw_xelis.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getPlatformVersion test', (WidgetTester tester) async { + final CwXelis plugin = CwXelis(); + final String? version = await plugin.getPlatformVersion(); + // The version string depends on the host platform running the test, so + // just assert that some non-empty string is returned. + expect(version?.isNotEmpty, true); + }); +} diff --git a/cw_xelis/example/lib/main.dart b/cw_xelis/example/lib/main.dart new file mode 100644 index 000000000..e90b389fc --- /dev/null +++ b/cw_xelis/example/lib/main.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:cw_xelis/cw_xelis.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _cwXelisPlugin = CwXelis(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await _cwXelisPlugin.getPlatformVersion() ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/cw_xelis/example/pubspec.lock b/cw_xelis/example/pubspec.lock new file mode 100644 index 000000000..4797ba9d9 --- /dev/null +++ b/cw_xelis/example/pubspec.lock @@ -0,0 +1,755 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" + url: "https://pub.dev" + source: hosted + version: "1.5.9" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + url: "https://pub.dev" + source: hosted + version: "1.19.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + cw_core: + dependency: transitive + description: + path: "../../cw_core" + relative: true + source: path + version: "0.0.1" + cw_xelis: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" + url: "https://pub.dev" + source: hosted + version: "2.9.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + url: "https://pub.dev" + source: hosted + version: "3.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + globbing: + dependency: transitive + description: + name: globbing + sha256: "4f89cfaf6fa74c9c1740a96259da06bd45411ede56744e28017cc534a12b6e2d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "3c46c2633aec07810c3d6a2eb08d575b5b4072980db08f1344e66aeb53d6e4a7" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + jsontool: + dependency: transitive + description: + name: jsontool + sha256: e49bf419e82d90f009426cd7fdec8d54ba8382975b3454ed16a3af3ee1d1b697 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.dev" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mobx: + dependency: transitive + description: + name: mobx + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + provider: + dependency: transitive + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + system_info2: + dependency: transitive + description: + name: system_info2 + sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.dev" + source: hosted + version: "0.7.3" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "8e3870a1caa60bde8352f9597dd3535d8068613269444f8e35ea8925ec84c1f5" + url: "https://pub.dev" + source: hosted + version: "0.3.1+1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + 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: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.dev" + source: hosted + version: "14.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + web_socket_client: + dependency: transitive + description: + name: web_socket_client + sha256: "394789177aa3bc1b7b071622a1dbf52a4631d7ce23c555c39bb2523e92316b07" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xelis_dart_sdk: + dependency: transitive + description: + name: xelis_dart_sdk + sha256: "990c6be6be5f0410764fd4034f940d2870bc70974feb3ed465123102a3bd9dfd" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + xelis_flutter: + dependency: transitive + description: + path: "." + ref: "83bda92f1b833fe5d8584aa429d5143a3698b33f" + resolved-ref: "83bda92f1b833fe5d8584aa429d5143a3698b33f" + url: "https://github.com/xelis-project/xelis-flutter-ffi.git" + source: git + version: "0.2.0" +sdks: + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_xelis/example/pubspec.yaml b/cw_xelis/example/pubspec.yaml new file mode 100644 index 000000000..3f3280a3d --- /dev/null +++ b/cw_xelis/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: cw_xelis_example +description: "Demonstrates how to use the cw_xelis plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=3.0.6 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + cw_xelis: + # When depending on this package from a real application you should use: + # cw_xelis: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/cw_xelis/example/test/widget_test.dart b/cw_xelis/example/test/widget_test.dart new file mode 100644 index 000000000..1b398727b --- /dev/null +++ b/cw_xelis/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_xelis_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/cw_xelis/lib/cw_xelis.dart b/cw_xelis/lib/cw_xelis.dart new file mode 100644 index 000000000..f5112fadd --- /dev/null +++ b/cw_xelis/lib/cw_xelis.dart @@ -0,0 +1,14 @@ +// You have generated a new plugin project without specifying the `--platforms` +// flag. A plugin project with no platform support was generated. To add a +// platform, run `flutter create -t plugin --platforms .` under the +// same directory. You can also find a detailed instruction on how to add +// platforms in the `pubspec.yaml` at +// https://flutter.dev/to/pubspec-plugin-platforms. + +import 'cw_xelis_platform_interface.dart'; + +class CwXelis { + Future getPlatformVersion() { + return CwXelisPlatform.instance.getPlatformVersion(); + } +} diff --git a/cw_xelis/lib/cw_xelis_method_channel.dart b/cw_xelis/lib/cw_xelis_method_channel.dart new file mode 100644 index 000000000..08af5dadc --- /dev/null +++ b/cw_xelis/lib/cw_xelis_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_xelis_platform_interface.dart'; + +/// An implementation of [CwXelisPlatform] that uses method channels. +class MethodChannelCwXelis extends CwXelisPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_xelis'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/cw_xelis/lib/cw_xelis_platform_interface.dart b/cw_xelis/lib/cw_xelis_platform_interface.dart new file mode 100644 index 000000000..aeb6ee51d --- /dev/null +++ b/cw_xelis/lib/cw_xelis_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_xelis_method_channel.dart'; + +abstract class CwXelisPlatform extends PlatformInterface { + /// Constructs a CwXelisPlatform. + CwXelisPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwXelisPlatform _instance = MethodChannelCwXelis(); + + /// The default instance of [CwXelisPlatform] to use. + /// + /// Defaults to [MethodChannelCwXelis]. + static CwXelisPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwXelisPlatform] when + /// they register themselves. + static set instance(CwXelisPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/cw_xelis/lib/src/api/api.dart b/cw_xelis/lib/src/api/api.dart new file mode 100644 index 000000000..a63867ddd --- /dev/null +++ b/cw_xelis/lib/src/api/api.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/api.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/api/logger.dart b/cw_xelis/lib/src/api/logger.dart new file mode 100644 index 000000000..81f6c5212 --- /dev/null +++ b/cw_xelis/lib/src/api/logger.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/logger.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/api/network.dart b/cw_xelis/lib/src/api/network.dart new file mode 100644 index 000000000..3dd7583a1 --- /dev/null +++ b/cw_xelis/lib/src/api/network.dart @@ -0,0 +1,24 @@ +export 'package:xelis_flutter/src/api/network.dart'; +import 'package:xelis_flutter/src/api/network.dart'; + +extension NetworkName on Network { + String get name { + switch (this) { + case Network.mainnet: + return 'mainnet'; + case Network.testnet: + return 'testnet'; + } + } + + static Network fromName(String name) { + switch (name) { + case 'mainnet': + return Network.mainnet; + case 'testnet': + return Network.testnet; + default: + throw ArgumentError('Unknown network name: $name'); + } + } +} \ No newline at end of file diff --git a/cw_xelis/lib/src/api/progress_report.dart b/cw_xelis/lib/src/api/progress_report.dart new file mode 100644 index 000000000..231c26c13 --- /dev/null +++ b/cw_xelis/lib/src/api/progress_report.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/progress_report.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/api/seed_search_engine.dart b/cw_xelis/lib/src/api/seed_search_engine.dart new file mode 100644 index 000000000..28c48a6b0 --- /dev/null +++ b/cw_xelis/lib/src/api/seed_search_engine.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/seed_search_engine.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/api/table_generation.dart b/cw_xelis/lib/src/api/table_generation.dart new file mode 100644 index 000000000..d076ff978 --- /dev/null +++ b/cw_xelis/lib/src/api/table_generation.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/table_generation.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/api/utils.dart b/cw_xelis/lib/src/api/utils.dart new file mode 100644 index 000000000..b8d95b8e4 --- /dev/null +++ b/cw_xelis/lib/src/api/utils.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/utils.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/api/wallet.dart b/cw_xelis/lib/src/api/wallet.dart new file mode 100644 index 000000000..fe3ab4dfa --- /dev/null +++ b/cw_xelis/lib/src/api/wallet.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/api/wallet.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/src/frb_generated.dart b/cw_xelis/lib/src/frb_generated.dart new file mode 100644 index 000000000..26684b944 --- /dev/null +++ b/cw_xelis/lib/src/frb_generated.dart @@ -0,0 +1 @@ +export 'package:xelis_flutter/src/frb_generated.dart'; \ No newline at end of file diff --git a/cw_xelis/lib/xelis_asset.dart b/cw_xelis/lib/xelis_asset.dart new file mode 100644 index 000000000..af1dcfa1c --- /dev/null +++ b/cw_xelis/lib/xelis_asset.dart @@ -0,0 +1,115 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'xelis_asset.g.dart'; + +@HiveType(typeId: XelisAsset.typeId) +class XelisAsset extends CryptoCurrency with HiveObjectMixin { + @override + @HiveField(0) + final String name; + + @HiveField(1) + final String symbol; + + @HiveField(2) + final String id; + + @HiveField(3) + final int decimals; + + @HiveField(4, defaultValue: true) + bool _enabled; + + @override + @HiveField(5) + final String? iconPath; + + @override + @HiveField(6) + final String? tag; + + @override + @HiveField(7, defaultValue: false) + final bool isPotentialScam; + + XelisAsset({ + required this.name, + required this.symbol, + required this.id, + required this.decimals, + this.iconPath, + this.tag = 'XEL', + bool enabled = true, + this.isPotentialScam = false, + }) : _enabled = enabled, + super( + name: id.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: tag, + iconPath: iconPath, + decimals: decimals, + isPotentialScam: isPotentialScam, + ); + + factory XelisAsset.fromMetadata({ + required String name, + required String id, + required String symbol, + required int decimals, + String? iconPath, + bool isPotentialScam = false, + }) { + return XelisAsset( + name: name, + symbol: symbol, + decimals: decimals, + id: id, + iconPath: iconPath, + isPotentialScam: isPotentialScam, + ); + } + + @override + bool get enabled => _enabled; + + @override + set enabled(bool value) => _enabled = value; + + XelisAsset.copyWith(XelisAsset other, String? icon, String? tag) + : name = other.name, + symbol = other.symbol, + decimals = other.decimals, + _enabled = other.enabled, + id = other.id, + tag = other.tag, + iconPath = icon, + isPotentialScam = other.isPotentialScam, + super( + title: other.symbol.toUpperCase(), + name: other.symbol.toLowerCase(), + decimals: other.decimals, + fullName: other.name, + tag: other.tag, + iconPath: icon, + isPotentialScam: other.isPotentialScam, + ); + + static const typeId = XELIS_ASSET_TYPE_ID; + static const boxName = 'XelisAssets'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + if (other is XelisAsset) { + return other.id == id; + } + return false; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/cw_xelis/lib/xelis_asset_balance.dart b/cw_xelis/lib/xelis_asset_balance.dart new file mode 100644 index 000000000..53aea00d3 --- /dev/null +++ b/cw_xelis/lib/xelis_asset_balance.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'package:intl/intl.dart'; +import 'package:cw_core/balance.dart'; + +import 'package:cw_xelis/xelis_formatting.dart'; +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; + +class XelisAssetBalance extends Balance { + XelisAssetBalance({ + required this.balance, + required this.decimals, + this.asset = xelis_sdk.xelisAsset, + this.symbol = "XEL" + }): super(balance, 0); + + final int balance; + final int decimals; + final String asset; + final String symbol; + + String get formatted { + final formatter = NumberFormat('0.00##########', 'en_US'); + final value = (BigInt.from(balance) / BigInt.from(10).pow(decimals)).toDouble(); + return formatter.format(value); + } + + String toJSON() => json.encode({ + 'balance': balance.toString(), + 'decimals': decimals, + 'asset': asset, + 'symbol': symbol + }); + + static XelisAssetBalance fromJSON(String jsonSource) { + final decoded = json.decode(jsonSource) as Map; + return XelisAssetBalance( + balance: decoded['balance'], + decimals: decoded['decimals'], + asset: decoded['asset'], + symbol: decoded['symbol'], + ); + } + + static XelisAssetBalance zero({int? decimals, String? asset, String? symbol}) { + return XelisAssetBalance( + balance: 0, + decimals: decimals ?? 8, + ); + } + + @override + String get formattedAvailableBalance => XelisFormatter.formatAmount(balance, decimals: decimals); + + @override + String get formattedAdditionalBalance => '0'; +} diff --git a/cw_xelis/lib/xelis_events.dart b/cw_xelis/lib/xelis_events.dart new file mode 100644 index 000000000..3d8f4622d --- /dev/null +++ b/cw_xelis/lib/xelis_events.dart @@ -0,0 +1,64 @@ +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; + +abstract class Event { + const Event(); +} + +class NewTopoheight extends Event { + final int topoheight; + const NewTopoheight(this.topoheight); +} + +class NewTransaction extends Event { + final xelis_sdk.TransactionEntry tx; + const NewTransaction(this.tx); +} + +class BalanceChanged extends Event { + final String asset; + final int balance; + const BalanceChanged(this.asset, this.balance); +} + +class Online extends Event { + const Online(); +} + +class Offline extends Event { + const Offline(); +} + +class Rescan extends Event { + final int startTopoheight; + const Rescan(this.startTopoheight); +} + +class HistorySynced extends Event { + final int topoheight; + const HistorySynced(this.topoheight); +} + +class NewAsset extends Event { + final String asset; + final int decimals; + final int? max_supply; + final String name; + final String? owner; + final String ticker; + final int topoheight; + const NewAsset( + this.asset, + this.decimals, + this.max_supply, + this.name, + this.owner, + this.ticker, + this.topoheight + ); +} + +class SyncError extends Event { + final String message; + + const SyncError(this.message); +} \ No newline at end of file diff --git a/cw_xelis/lib/xelis_exception.dart b/cw_xelis/lib/xelis_exception.dart new file mode 100644 index 000000000..aaf7dc537 --- /dev/null +++ b/cw_xelis/lib/xelis_exception.dart @@ -0,0 +1,25 @@ +import 'package:cw_core/crypto_currency.dart'; + +class XelisMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Xelis mnemonic has incorrect format. Mnemonic should contain 25 words separated by space.'; +} +class XelisTransactionCreationException implements Exception { + final String exceptionMessage; + + XelisTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} +class XelisTooManyOutputsException implements Exception { + final int count; + + XelisTooManyOutputsException(this.count); + + @override + String toString() => + 'Cannot include more than 255 transfers in a single TX. Attempted to use $count.'; +} diff --git a/cw_xelis/lib/xelis_formatting.dart b/cw_xelis/lib/xelis_formatting.dart new file mode 100644 index 000000000..6b1e4afa4 --- /dev/null +++ b/cw_xelis/lib/xelis_formatting.dart @@ -0,0 +1,54 @@ +import 'dart:math'; + +class XelisFormatter { + static int parseXelisAmount(String amount) { + try { + return (double.parse(amount) * pow(10, 8)).round(); + } catch (_) { + return 0; + } + } + + static double parseXelisAmountToDouble(int amount) { + try { + return amount / pow(10, 8); + } catch (_) { + return 0; + } + } + + static int parseAmount(String amount, int decimals) { + try { + return (double.parse(amount) * pow(10, decimals)).round(); + } catch (_) { + return 0; + } + } + + static double parseAmountToDouble(int amount, int decimals) { + try { + return amount / pow(10, decimals); + } catch (_) { + return 0; + } + } + + static String formatAmountWithSymbol( + int rawAmount, { + required int decimals, + String? symbol, + }) { + final formatted = rawAmount / pow(10, decimals); + // final symbol = assetId == null || assetId == xelisAsset ? 'XEL' : assetId; + final sym = symbol ?? 'XEL'; + return '$formatted $sym'; + } + + static String formatAmount( + int rawAmount, { + required int decimals, + }) { + final formatted = rawAmount / pow(10, decimals); + return '$formatted'; + } +} diff --git a/cw_xelis/lib/xelis_pending_transaction.dart b/cw_xelis/lib/xelis_pending_transaction.dart new file mode 100644 index 000000000..e32791e3b --- /dev/null +++ b/cw_xelis/lib/xelis_pending_transaction.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_xelis/xelis_formatting.dart'; + +class XelisPendingTransaction with PendingTransaction { + XelisPendingTransaction( + { + required this.txid, + required this.amount, + required this.fee, + required this.decimals, + required this.send, + }); + + final String amount; + final int fee; + final String txid; + final int decimals; + final Future Function() send; + + @override + String get id => txid; + + @override + String get amountFormatted => amount.toString(); + + @override + String get feeFormatted => XelisFormatter.formatAmount(fee, decimals: 8); + + @override + String get hex => ""; + + @override + Future commit() async { + return send(); + } + + @override + Future commitUR() { + throw UnimplementedError(); + } +} diff --git a/cw_xelis/lib/xelis_store_utils.dart b/cw_xelis/lib/xelis_store_utils.dart new file mode 100644 index 000000000..614a780eb --- /dev/null +++ b/cw_xelis/lib/xelis_store_utils.dart @@ -0,0 +1,27 @@ +import 'dart:io'; +import 'package:path/path.dart' as p; +import 'package:cw_xelis/src/api/network.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_type.dart'; + +Future pathForXelisNetworkFile(String name) async { + final walletDir = await pathForWalletDir(name: name, type: WalletType.xelis); + return p.join(walletDir, 'network.txt'); +} + +Future saveXelisNetwork(String name, Network network) async { + final path = await pathForXelisNetworkFile(name); + await File(path).writeAsString(network.name); +} + +Future loadXelisNetwork(String name) async { + final path = await pathForXelisNetworkFile(name); + final file = File(path); + + if (!await file.exists()) { + throw FileSystemException('Missing Xelis network file', path); + } + + final contents = await file.readAsString(); + return NetworkName.fromName(contents.trim()); +} diff --git a/cw_xelis/lib/xelis_transaction_credentials.dart b/cw_xelis/lib/xelis_transaction_credentials.dart new file mode 100644 index 000000000..89d6a254c --- /dev/null +++ b/cw_xelis/lib/xelis_transaction_credentials.dart @@ -0,0 +1,12 @@ +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/crypto_currency.dart'; + +import 'package:cw_xelis/xelis_transaction_priority.dart'; + +class XelisTransactionCredentials { + XelisTransactionCredentials(this.outputs, {required this.priority, required this.currency}); + + final List outputs; + final XelisTransactionPriority? priority; + final CryptoCurrency currency; +} diff --git a/cw_xelis/lib/xelis_transaction_history.dart b/cw_xelis/lib/xelis_transaction_history.dart new file mode 100644 index 000000000..40a7ad82b --- /dev/null +++ b/cw_xelis/lib/xelis_transaction_history.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_xelis/xelis_transaction_info.dart'; + +part 'xelis_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'xelis_transactions.json'; + +class XelisTransactionHistory = XelisTransactionHistoryBase with _$XelisTransactionHistory; + +abstract class XelisTransactionHistoryBase + extends TransactionHistoryBase with Store { + XelisTransactionHistoryBase( + {required this.walletInfo, required String password, required this.encryptionFileUtils}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + final EncryptionFileUtils encryptionFileUtils; + String _password; + + Future init() async { + clear(); + await _load(); + } + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); + final data = json.encode({'transactions': transactionMaps}); + await encryptionFileUtils.write(path: path, password: _password, data: data); + } catch (e, s) { + printV('Error while saving xelis transaction history: ${e.toString()}'); + printV(s); + } + } + + @override + void addOne(XelisTransactionInfo transaction) => + transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await encryptionFileUtils.read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = XelisTransactionInfo.fromJson(val, isAssetEnabled: (id) => true); // asset filtering needs to happen elsewhere before serializing + addOne(tx); + } + }); + } catch (e) { + printV(e); + } + } + + bool update(Map txs) { + var foundOldTx = false; + txs.forEach((_, tx) { + if (!transactions.containsKey(tx.id)) { + transactions[tx.id] = tx; + } else { + foundOldTx = true; + } + }); + return foundOldTx; + } +} diff --git a/cw_xelis/lib/xelis_transaction_info.dart b/cw_xelis/lib/xelis_transaction_info.dart new file mode 100644 index 000000000..2ad4d67f8 --- /dev/null +++ b/cw_xelis/lib/xelis_transaction_info.dart @@ -0,0 +1,319 @@ +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_xelis/xelis_formatting.dart'; +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; +import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +import 'dart:math'; + +class XelisTxRecipient { + final String address; + final String amount; + final bool isChange; + + const XelisTxRecipient({ + required this.address, + required this.amount, + required this.isChange, + }); +} + +class XelisTransfer { + final x_wallet.XelisAssetMetadata meta; + final int amount; + + XelisTransfer({ + required this.meta, + required this.amount + }); + + String format() { + final amountDouble = (BigInt.from(amount) / BigInt.from(10).pow(meta.decimals)).toString(); + return '${formatAmount(amountDouble)} ${meta.ticker}'; + } +} + +class XelisTransactionInfo extends TransactionInfo { + XelisTransactionInfo({ + required this.id, + required this.height, + required this.direction, + required this.date, + required this.xelAmount, + required this.xelFee, + required this.decimals, + required this.assetSymbols, + required this.assetIds, + required this.assetAmounts, + required this.to, + required this.from, + this.isTestnet = false, + }) : + amount = xelAmount.toInt(), + fee = xelFee.toInt() + ; + + final String id; + final int amount; + final int fee; + final int height; + final BigInt xelAmount; + final BigInt xelFee; + final DateTime date; + final TransactionDirection direction; + final List assetAmounts; + final List decimals; + final List assetSymbols; + final List assetIds; + final String? to; + final String? from; + final bool isTestnet; + final int confirmations = 3; // static/unused atm, purely for compatibility + + String? _fiatAmount; + + @override + String amountFormatted() { + final List formattedAssets = []; + + if (formattedAssets.length > 1) return ":MULTI:" + multiFormatted(); + + final amount = (assetAmounts[0] / BigInt.from(10).pow(decimals[0])).toString(); + return '$amount ${assetSymbols[0]}'; + } + + String multiFormatted() { + final List formattedAssets = []; + + for (int i = 0; i < assetSymbols.length; i++) { + final amount = (assetAmounts[i] / BigInt.from(10).pow(decimals[i])).toString(); + formattedAssets.add('$amount ${assetSymbols[i]}'); + } + + return formattedAssets.join('\n\n'); + } + + @override + String feeFormatted() => + XelisFormatter.formatAmountWithSymbol(fee, decimals: 8, symbol: isTestnet ? 'XET' : 'XEL'); + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) { + _fiatAmount = formatAmount(amount); + } + + static Future fromTransactionEntry( + xelis_sdk.TransactionEntry entry, + {required x_wallet.XelisWallet wallet, required bool Function(String assetId) isAssetEnabled, bool isTestnet = false} + ) async { + final txType = entry.txEntryType; + + late TransactionDirection direction; + BigInt amount = BigInt.zero; + BigInt fee = BigInt.zero; + String? to; + String? from; + + String asset = xelis_sdk.xelisAsset; + + final Map assetAmounts = {}; + final Map assetDecimals = {}; + final Map assetSymbolsMap = {}; + + switch (txType) { + case xelis_sdk.IncomingEntry(): + direction = TransactionDirection.incoming; + + for (final transfer in txType.transfers) { + final asset = transfer.asset; + if (!isAssetEnabled(asset)) continue; + + assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount); + + final meta = await wallet.getAssetMetadata(asset: asset); + assetDecimals[asset] = meta.decimals; + assetSymbolsMap[asset] = meta.ticker; + } + + from = txType.from; + break; + + case xelis_sdk.OutgoingEntry(): + direction = TransactionDirection.outgoing; + + final formattedTransfers = []; + + for (final transfer in txType.transfers) { + final asset = transfer.asset; + if (!isAssetEnabled(asset)) continue; + + final meta = await wallet.getAssetMetadata(asset: asset); + final formatted = XelisTransfer(meta: meta, amount: transfer.amount).format(); + + assetDecimals[asset] = meta.decimals; + assetSymbolsMap[asset] = meta.ticker; + assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount); + + if (txType.transfers.length > 1) { + formattedTransfers.add("${transfer.destination} [ $formatted ]"); + } else { + formattedTransfers.add("${transfer.destination}"); + } + } + + to = formattedTransfers.join('\n\n'); + fee = BigInt.from(txType.fee); + break; + + case xelis_sdk.BurnEntry(): + direction = TransactionDirection.outgoing; + final asset = txType.asset; + final meta = await wallet.getAssetMetadata(asset: asset); + + if (!isAssetEnabled(asset)) { + break; + } + + assetAmounts[asset] = BigInt.from(txType.amount); + assetDecimals[asset] = meta.decimals; + assetSymbolsMap[asset] = meta.ticker; + + to = "Burned"; + + fee = BigInt.from(txType.fee); + break; + + case xelis_sdk.CoinbaseEntry(): + direction = TransactionDirection.incoming; + final meta = await wallet.getAssetMetadata(asset: xelis_sdk.xelisAsset); + + assetAmounts[xelis_sdk.xelisAsset] = BigInt.from(txType.reward); + assetDecimals[xelis_sdk.xelisAsset] = meta.decimals; + assetSymbolsMap[xelis_sdk.xelisAsset] = meta.ticker; + break; + + case xelis_sdk.InvokeContractEntry(): + direction = TransactionDirection.outgoing; + + for (final entry in txType.deposits.entries) { + final asset = entry.key; + final amount = entry.value; + + if (!isAssetEnabled(asset)) { + continue; + } + + assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(amount); + + final meta = await wallet.getAssetMetadata(asset: asset); + assetDecimals[asset] = meta.decimals; + assetSymbolsMap[asset] = meta.ticker; + } + + fee = BigInt.from(txType.fee); + to = "SCID:\n${txType.contract}\n\nChunk ID:\n${txType.chunkId}"; + break; + + case xelis_sdk.DeployContractEntry(): + direction = TransactionDirection.outgoing; + + final meta = await wallet.getAssetMetadata(asset: xelis_sdk.xelisAsset); + + assetAmounts[xelis_sdk.xelisAsset] = BigInt.zero; + assetDecimals[xelis_sdk.xelisAsset] = meta.decimals; + assetSymbolsMap[xelis_sdk.xelisAsset] = meta.ticker; + fee = BigInt.from(txType.fee); + + default: + direction = TransactionDirection.outgoing; + break; + } + + final filteredAssetIds = assetAmounts.keys.where(isAssetEnabled).toList(); + final assetIds = filteredAssetIds; + final assetSymbols = assetIds.map((id) => assetSymbolsMap[id] ?? '???').toList(); + final decimals = assetIds.map((id) => assetDecimals[id] ?? 8).toList(); + final amounts = assetIds.map((id) => assetAmounts[id]!).toList(); + + final xelAmount = assetAmounts[xelis_sdk.xelisAsset] ?? BigInt.zero; + + return XelisTransactionInfo( + id: entry.hash, + height: entry.topoheight, + direction: direction, + date: entry.timestamp ?? DateTime.now(), + xelAmount: xelAmount, + xelFee: fee, + to: to, + from: from, + decimals: decimals, + assetSymbols: assetSymbols, + assetIds: assetIds, + assetAmounts: amounts, + isTestnet: isTestnet + ); + } + + factory XelisTransactionInfo.fromJson( + Map data, { + required bool Function(String assetId) isAssetEnabled, + }) { + final allAssetIds = List.from(data['assetIds']); + final allAssetSymbols = List.from(data['assetSymbols']); + final allAssetAmounts = (data['assetAmounts'] as List) + .map((val) => BigInt.parse(val.toString())) + .toList(); + final allDecimals = List.from(data['decimals']); + + final filteredIndices = []; + for (int i = 0; i < allAssetIds.length; i++) { + if (isAssetEnabled(allAssetIds[i])) { + filteredIndices.add(i); + } + } + + final assetIds = filteredIndices.map((i) => allAssetIds[i]).toList(); + final assetSymbols = filteredIndices.map((i) => allAssetSymbols[i]).toList(); + final assetAmounts = filteredIndices.map((i) => allAssetAmounts[i]).toList(); + final decimals = filteredIndices.map((i) => allDecimals[i]).toList(); + + final xelAmount = assetAmounts.isNotEmpty ? assetAmounts[0] : BigInt.zero; + + return XelisTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + decimals: decimals, + assetAmounts: assetAmounts, + xelAmount: xelAmount, + xelFee: BigInt.parse(data['xelFee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + assetSymbols: assetSymbols, + assetIds: assetIds, + to: data['to'], + from: data['from'], + isTestnet: data['isTestnet'], + ); + } + + Map toJson() => { + 'id': id, + 'height': height, + 'decimals': decimals, + 'assetSymbols': assetSymbols, + 'assetIds': assetIds, + 'assetAmounts': assetAmounts.map((e) => e.toString()).toList(), + 'xelAmount': xelAmount.toString(), + 'xelFee': xelFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'to': to, + 'from': from, + 'isTestnet': isTestnet, + }; +} \ No newline at end of file diff --git a/cw_xelis/lib/xelis_transaction_priority.dart b/cw_xelis/lib/xelis_transaction_priority.dart new file mode 100644 index 000000000..ce2b41808 --- /dev/null +++ b/cw_xelis/lib/xelis_transaction_priority.dart @@ -0,0 +1,48 @@ +import 'package:cw_core/transaction_priority.dart'; + +class XelisTransactionPriority extends TransactionPriority { + const XelisTransactionPriority({required String title, required int raw}) + : super(title: title, raw: raw); + + static const List all = [medium]; + static const XelisTransactionPriority slow = XelisTransactionPriority(title: 'Slow', raw: 1); + static const XelisTransactionPriority medium = + XelisTransactionPriority(title: 'Medium', raw: 2); + static const XelisTransactionPriority fast = XelisTransactionPriority(title: 'Fast', raw: 4); + + static XelisTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 1: + return slow; + case 2: + return medium; + case 4: + return fast; + default: + throw Exception('Unexpected token: $raw for XelisTransactionPriority deserialize'); + } + } + + String get units => 'atom'; + + @override + String toString() { + var label = ''; + + switch (this) { + case XelisTransactionPriority.slow: + label = 'Slow'; + break; + case XelisTransactionPriority.medium: + label = 'Standard'; + break; + case XelisTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} \ No newline at end of file diff --git a/cw_xelis/lib/xelis_wallet.dart b/cw_xelis/lib/xelis_wallet.dart new file mode 100644 index 000000000..562b1b6eb --- /dev/null +++ b/cw_xelis/lib/xelis_wallet.dart @@ -0,0 +1,1051 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:convert'; + +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/pathForWallet.dart'; + +import 'package:cw_xelis/xelis_formatting.dart'; +import 'package:cw_xelis/xelis_exception.dart'; +import 'package:cw_xelis/xelis_asset_balance.dart'; +import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; +import 'package:cw_xelis/src/api/utils.dart'; +import 'package:cw_xelis/src/api/network.dart'; +import 'package:cw_xelis/xelis_asset.dart'; +import 'package:cw_xelis/xelis_transaction_info.dart'; +import 'package:cw_xelis/xelis_transaction_history.dart'; +import 'package:cw_xelis/xelis_transaction_credentials.dart'; +import 'package:cw_xelis/xelis_wallet_addresses.dart'; +import 'package:cw_xelis/xelis_pending_transaction.dart'; +import 'package:cw_xelis/xelis_events.dart'; +import 'package:cw_xelis/xelis_store_utils.dart'; +import 'package:cw_core/wallet_keys_file.dart'; + +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; +import 'package:mutex/mutex.dart'; + +part 'xelis_wallet.g.dart'; + +class XelisWallet = XelisWalletBase with _$XelisWallet; + +class Recipient { + final String address; + final BigInt amount; + final bool isChange; + + Recipient({ + required this.address, + required this.amount, + required this.isChange, + }); +} + +abstract class XelisWalletBase + extends WalletBase with Store, WalletKeysFile { + final x_wallet.XelisWallet _libWallet; + + XelisWalletBase({ + required WalletInfo walletInfo, + required x_wallet.XelisWallet libWallet, + required String password, + required this.network, + required this.encryptionFileUtils, + }) + : + _password = password, + _libWallet = libWallet, + _isTransactionUpdating = false, + this.syncStatus = NotConnectedSyncStatus(), + balance = ObservableMap(), + super(walletInfo) + { + this.walletInfo = walletInfo; + isTestnet = network == Network.testnet; + final curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + balance = ObservableMap.of({curr: XelisAssetBalance.zero(symbol: isTestnet ? "XET" : "XEL")}); + walletAddresses = XelisWalletAddresses(walletInfo, _libWallet); + transactionHistory = XelisTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils + ); + + if (!CakeHive.isAdapterRegistered(XelisAsset.typeId)) { + CakeHive.registerAdapter(XelisAssetAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + Future _retryWithBackoff( + Future Function() operation, + String operationName, { + int maxRetries = 5, + int baseDelayMs = 1000, + T? defaultValue, + }) async { + for (int attempt = 0; attempt < maxRetries; attempt++) { + if (requestedClose) { + printV("$operationName cancelled - wallet is closing"); + return defaultValue ?? (throw StateError('Wallet is closing')); + } + + try { + final result = await operation(); + if (attempt > 0) { + printV("$operationName succeeded on attempt ${attempt + 1}"); + } + return result; + } catch (e) { + final isLastAttempt = attempt == maxRetries - 1; + + if (isLastAttempt) { + if (defaultValue != null) { + printV("$operationName failed after $maxRetries attempts. Using default value. Final error: $e"); + return defaultValue; + } else { + printV("$operationName failed after $maxRetries attempts. Final error: $e"); + rethrow; + } + } + + final delayMs = baseDelayMs * (1 << attempt); + printV("$operationName failed (attempt ${attempt + 1}/$maxRetries): $e. Retrying in ${delayMs}ms..."); + + if (requestedClose) { + printV("$operationName cancelled during delay - wallet is closing"); + return defaultValue ?? (throw StateError('Wallet is closing')); + } + + await Future.delayed(Duration(milliseconds: delayMs)); + } + } + + throw StateError('This should never be reached'); + } + + String _password; + final EncryptionFileUtils encryptionFileUtils; + + Node? currentNode; + + bool connecting = false; + bool requestedClose = false; + String persistantPeer = ""; + Timer? syncTimer; + int pruneHeight = 0; + String _seed = ""; + Network network; + int topoheight = 0; + + late final Box xelAssetsBox; + + @observable + double? estimatedFee = 0.00025; + + @override + @observable + late ObservableMap balance; + + final Completer _sharedPrefs = Completer(); + + @override + @observable + SyncStatus syncStatus; + + @override + late XelisWalletAddresses walletAddresses; + + @override + bool get hasRescan => true; + + @override + String get password => _password; + + @override + String get seed => _seed; + void updateSeed(String? language) async { + _seed = await _libWallet.getSeed(languageIndex: getLanguageIndexFromStr(input: language ?? "english")); + } + + Future langSeed(String? language) async => await _libWallet.getSeed(languageIndex: getLanguageIndexFromStr(input: language ?? "english")); + + @override + Object get keys => {}; + + bool _isTransactionUpdating; + StreamSubscription? _eventSub; + + Future _subscribeToWalletEvents() async { + await _unsubscribeFromWalletEvents(); + _eventSub = _convertRawEvents().listen(_handleEvent); + } + + Future _unsubscribeFromWalletEvents() async { + await _eventSub?.cancel(); + _eventSub = null; + } + + Stream _convertRawEvents() async* { + final stream = _libWallet.eventsStream(); + + await for (final raw in stream) { + try { + final data = jsonDecode(raw); + final event = xelis_sdk.WalletEvent.fromStr(data['event'] as String); + + switch (event) { + case xelis_sdk.WalletEvent.newTransaction: + yield NewTransaction(xelis_sdk.TransactionEntry.fromJson(data['data']) as xelis_sdk.TransactionEntry); + case xelis_sdk.WalletEvent.balanceChanged: + yield BalanceChanged(data['data']['asset'] as String, (data['data']['balance'] as num).toInt()); + case xelis_sdk.WalletEvent.newTopoHeight: + yield NewTopoheight((data['data']['topoheight'] as num).toInt()); + case xelis_sdk.WalletEvent.rescan: + yield Rescan((data['data']['start_topoheight'] as num).toInt()); + case xelis_sdk.WalletEvent.online: + yield const Online(); + case xelis_sdk.WalletEvent.offline: + yield const Offline(); + case xelis_sdk.WalletEvent.historySynced: + yield HistorySynced((data['data']['topoheight'] as num).toInt()); + case xelis_sdk.WalletEvent.newAsset: + final owner = data['data']['owner']; + yield NewAsset( + data['data']['asset'] as String, + (data['data']['decimals'] as num).toInt(), + (data['data']['max_supply'] as num?)?.toInt(), + data['data']['name'] as String, + owner is Map ? jsonEncode(owner) : owner as String?, + data['data']['ticker'] as String, + (data['data']['topoheight'] as num).toInt(), + ); + case xelis_sdk.WalletEvent.syncError: + yield SyncError(data['data']['message'] as String); + default: + continue; + } + } catch (e) { + printV('Failed to parse wallet event $raw: $e'); + continue; + } + } + } + + @override + Future save() async { + await saveXelisNetwork(name, network); + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password, encryptionFileUtils); + saveKeysFile(_password, encryptionFileUtils, true); + } + + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + WalletKeysData get walletKeysData => WalletKeysData( + mnemonic: _seed, + privateKey: "", + passphrase: "", + ); + + String toJSON() => json.encode({ + 'mnemonic': _seed, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, + }); + + @action + @override + Future startSync() async { + try { + if (!(await _libWallet.isOnline())) { return; } + syncStatus = AttemptingSyncStatus(); + await scanAssets(); + await updateBalance(); + await _updateTransactions(); + syncStatus = SyncedSyncStatus(); + } catch (e, s) { + syncStatus = FailedSyncStatus(); + printV("Failed to synchronize. error: $e, message: $s"); + rethrow; + } + } + + @action + @override + Future connectToNode({required Node node}) async { + if (connecting) { + return; + } + connecting = true; + try { + await _subscribeToWalletEvents(); + String addr = isTestnet ? "testnet-node.xelis.io" : "us-node.xelis.io"; + if (node.uri.host != addr) { + addr = node.uri.host; + if (node.uri.port != null && node.uri.port > 0) { + addr += ":${node.uri.port}"; + } else { + addr += ":443"; + } + } + if (addr != persistantPeer) { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + persistantPeer = addr; + } + if (await _libWallet.isOnline()) { + await goOffline(); + } + await goOnline(addr); + currentNode = node; + unawaited(_fetchPruneHeight()); + await this.startSync(); + } catch(e, s) { + printV("Error when connecting to xelis node ${node.uri}: error: $e, message: $s"); + syncStatus = FailedSyncStatus(); + } + connecting = false; + } + + bool isSupportedEntryType(xelis_sdk.TransactionEntry entry) { + switch (entry.txEntryType) { + case xelis_sdk.IncomingEntry(): + case xelis_sdk.OutgoingEntry(): + case xelis_sdk.BurnEntry(): + case xelis_sdk.CoinbaseEntry(): + case xelis_sdk.InvokeContractEntry(): + case xelis_sdk.DeployContractEntry(): + return true; + default: + return false; + } + } + + @override + @action + Future rescan({required int height}) async { + walletInfo.restoreHeight = height; + walletInfo.isRecovery = true; + balance.clear(); + + final curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + balance[curr] = XelisAssetBalance.zero(symbol: isTestnet ? "XET" : "XEL"); + + await _libWallet.rescan(topoheight: BigInt.from(pruneHeight > height ? pruneHeight : height)); + syncStatus = NotConnectedSyncStatus(); + await connectToNode(node: currentNode!); + await walletInfo.save(); + } + + List _txBuffer = []; + Timer? _txBatchTimer; + Timer? _txSaveDebounceTimer; + final _txBufferLock = Mutex(); + + void _bufferTransaction(XelisTransactionInfo tx) { + _txBufferLock.protect(() async { + _txBuffer.add(tx); + + if (_txBuffer.length > 1000) { + _txBatchTimer?.cancel(); + _txBatchTimer = Timer(Duration.zero, _processTransactionBuffer); + } else if (_txBatchTimer == null || !_txBatchTimer!.isActive) { + _txBatchTimer = Timer(Duration(seconds: 1), _processTransactionBuffer); + } + }); + } + + Future _processTransactionBuffer() async { + await _txBufferLock.protect(() async { + if (_txBuffer.isEmpty) return; + + final buffered = List.from(_txBuffer); + + final txMap = {for (var tx in buffered) tx.id: tx}; + runInAction(() => transactionHistory.addMany(txMap)); + _txBuffer.clear(); + + + _txSaveDebounceTimer?.cancel(); + _txSaveDebounceTimer = Timer(Duration(seconds: 2), () async { + await transactionHistory.save(); + }); + }); + } + + @action + Future _handleEvent(Event event) async { + try { + switch (event) { + case NewTransaction(): + if (!isSupportedEntryType(event.tx)) break; + + final transactionInfo = await XelisTransactionInfo.fromTransactionEntry( + event.tx, + wallet: _libWallet, + isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset, + isTestnet: isTestnet + ); + _bufferTransaction(transactionInfo); + break; + + case BalanceChanged(): + if (event.asset == xelis_sdk.xelisAsset) { + final curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + + balance[curr] = XelisAssetBalance( + balance: event.balance, + decimals: 8, + ); + } else { + final curr = findTrackedAssetById(event.asset); + + if (curr != null) { + if (curr.enabled) { + balance[curr] = XelisAssetBalance( + balance: event.balance, + decimals: curr.decimals, + symbol: curr.symbol, + ); + } else { + balance.remove(curr); + } + } else { + try { + final metadata = await _libWallet.getAssetMetadata(asset: event.asset); + final asset = XelisAsset( + name: metadata.name, + symbol: metadata.ticker, + id: event.asset, + decimals: metadata.decimals, + enabled: false, + ); + await xelAssetsBox.put(asset.id, asset); + } catch (e) { + printV("Failed to fetch metadata for asset ${event.asset}: $e"); + } + } + } + break; + + case NewTopoheight(): + topoheight = event.topoheight; + break; + + case Online(): + syncStatus = SyncedSyncStatus(); + break; + + case Offline(): + syncStatus = NotConnectedSyncStatus(); + break; + + case HistorySynced(): + syncStatus = SyncedSyncStatus(); + break; + + case Rescan(): + // optional + break; + + case NewAsset(): + if (event.asset == xelis_sdk.xelisAsset) { + break; + } + + final existing = xelAssetsBox.values + .cast() + .firstWhere((e) => e?.id == event.asset, orElse: () => null); + + final newAsset = XelisAsset( + name: event.name, + symbol: event.ticker, + id: event.asset, + decimals: event.decimals, + enabled: existing?.enabled ?? false, + ); + + await updateAssetState(newAsset); + break; + + case SyncError(): + printV("Sync error occurred: ${event.message}"); + syncStatus = FailedSyncStatus(); + // TODO: display the error message + // _lastSyncError = event.message; + break; + } + } catch (e, s) { + printV("Error handling event $event: $e\n$s"); + } + await Future.delayed(Duration.zero); + } + + Future _fetchPruneHeight() async { + pruneHeight = await _retryWithBackoff( + () async { + final infoString = await _libWallet.getDaemonInfo(); + final Map nodeInfo = + (json.decode(infoString) as Map).cast(); + return int.tryParse(nodeInfo['pruned_topoheight']?.toString() ?? '0') ?? 0; + }, + 'Fetch prune height', + defaultValue: 0, + maxRetries: 30 + ); + } + + @action + Future init() async { + try { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${XelisAsset.boxName}"; + xelAssetsBox = await CakeHive.openBox(boxName); + + walletAddresses.init(); + await transactionHistory.init(); + _seed = await _libWallet.getSeed(); + await save(); + } catch (e) { + printV("Failed to init wallet: $e"); + } + } + + @override + XelisAsset? findTrackedAssetById(String assetId) { + try { + return xelAssetsBox.values.firstWhere((asset) => asset.id == assetId); + } catch (_) { + return null; + } + } + + @override + Future changePassword(String password) async { + return () async { + await _libWallet.changePassword(oldPassword: _password, newPassword: password); + _password = password; + }(); + } + + Future _fetchAssetBalances() async { + for (final asset in xelAssetsBox.values) { + try { + if (asset.id == xelis_sdk.xelisAsset) continue; + + final isTracked = await _libWallet.isAssetTracked(asset: asset.id); + + if (asset.enabled && !isTracked) { + await _libWallet.trackAsset(asset: asset.id); + } else if (!asset.enabled && isTracked) { + await _libWallet.untrackAsset(asset: asset.id); + } + } catch (e) { + printV("Failed to sync tracking for asset ${asset.id}: $e"); + } + } + + final bal = await _libWallet.getTrackedAssetBalancesRaw(); + + for (final entry in bal.entries) { + final assetId = entry.key; + if (assetId == xelis_sdk.xelisAsset) continue; + + final asset = findTrackedAssetById(assetId); + + if (asset != null && asset.enabled) { + balance[asset] = XelisAssetBalance( + balance: entry.value.toInt(), + asset: asset.id, + symbol: asset.symbol, + decimals: asset.decimals, + ); + } + } + + balance.removeWhere((currency, _) { + if (currency is XelisAsset) { + return !currency.enabled; + } + return false; + }); + } + + List get xelAssets => xelAssetsBox.values.toList(); + + @action + Future filterAssets() async { + for (final asset in xelAssetsBox.values) { + if (asset.id == xelis_sdk.xelisAsset) continue; + + final isTracked = await _libWallet.isAssetTracked(asset: asset.id); + + if (asset.enabled != isTracked) { + if (asset.enabled) { + await _libWallet.trackAsset(asset: asset.id); + } else { + await _libWallet.untrackAsset(asset: asset.id); + } + } + + if (!asset.enabled && balance.containsKey(asset)) { + balance.remove(asset); + } + } + } + + @action + Future scanAssets() async { + try { + // Get all assets present in the wallet (not just tracked ones) + final (allAssets) = await _libWallet.getAllAssets(); + + for (final (assetId, assetData) in allAssets) { + if (assetId == xelis_sdk.xelisAsset) continue; + + // Check if we already know about this asset + var existingAsset = findTrackedAssetById(assetId); + + if (existingAsset == null) { + // New asset discovered - fetch metadata and add to box + try { + final asset = XelisAsset( + name: assetData.name, + symbol: assetData.ticker, + id: assetId, + decimals: assetData.decimals, + enabled: false, + ); + + await xelAssetsBox.put(asset.id, asset); + } catch (e) { + printV("Failed to fetch metadata for asset $assetId: $e"); + } + } + } + + // Now sync tracking status with wallet + for (final asset in xelAssetsBox.values) { + if (asset.id == xelis_sdk.xelisAsset) continue; + + final isTracked = await _libWallet.isAssetTracked(asset: asset.id); + + if (asset.enabled != isTracked) { + if (asset.enabled && !isTracked) { + await _libWallet.trackAsset(asset: asset.id); + } else if (!asset.enabled && isTracked) { + await _libWallet.untrackAsset(asset: asset.id); + } + } + } + } catch (e, s) { + printV("Error scanning assets: $e, $s"); + } + } + + @override + Future updateAssetState(XelisAsset asset) async { + await xelAssetsBox.put(asset.id, asset); + + if (asset.enabled) { + balance[asset] = XelisAssetBalance( + balance: 0, + asset: asset.id, + symbol: asset.symbol, + decimals: asset.decimals + ); + try { + await _libWallet.trackAsset(asset: asset.id); + + final assetBalance = (await _libWallet.getAssetBalanceByIdRaw(asset: asset.id)).toInt(); + balance[asset] = XelisAssetBalance( + balance: assetBalance, + asset: asset.id, + symbol: asset.symbol, + decimals: asset.decimals + ); + } catch (e) { + printV("Failed to track asset ${asset.id}: $e"); + } + } else { + balance.remove(asset); + await _libWallet.untrackAsset(asset: asset.id); + } + } + + // Update deleteAsset to handle untracking: + @override + Future deleteAsset(XelisAsset asset) async { + try { + _libWallet.untrackAsset(asset: asset.id); + } catch (e) { + printV("Failed to untrack asset ${asset.id}: $e"); + } + + await asset.delete(); + balance.remove(asset); + await removeAssetTransactionsInHistory(asset); + await updateBalance(); + } + + @override + Future removeAssetTransactionsInHistory(XelisAsset asset) async { + transactionHistory.transactions.removeWhere((key, value) => value.assetIds[0] == asset.id && value.assetIds.length == 1); + await transactionHistory.save(); + } + + Future getAsset(String id) async { + try { + final metadata = await _libWallet.getAssetMetadata(asset: id); + + return XelisAsset( + name: metadata.name, + symbol: metadata.ticker, + id: id, + decimals: metadata.decimals + ); + } catch (e, s) { + printV('Error fetching asset: ${e.toString()}, ${s.toString()}'); + return null; + } + } + + + @override + Future updateBalance() async { + if (!(await _libWallet.isOnline())) { + return; + } + + var curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + balance[curr] = XelisAssetBalance( + balance: (await _libWallet.getXelisBalanceRaw()).toInt(), + decimals: 8 + ); + await _fetchAssetBalances(); + await save(); + } + + Future _liveFeeEstimate(Object credentials, {String? assetId}) async { + final xelisCredentials = credentials as XelisTransactionCredentials; + + final outputs = xelisCredentials.outputs; + final asset = assetId ?? xelis_sdk.xelisAsset; + + final defaultFee = 0.00025; + + // Use default address if recipients list is empty to ensure basic fee estimates are readily available + final effectiveRecipients = xelisCredentials.outputs.isNotEmpty + ? xelisCredentials.outputs.map((output) { + final address = output.isParsedAddress + ? output.extractedAddress! + : output.address; + + return XelisTxRecipient ( + address: address, + amount: output.cryptoAmount ?? '0.0', + isChange: false, + ); + }).toList() + : [ + XelisTxRecipient ( + address: 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', + amount: '1.0', + isChange: false, + ), + ]; + + try { + final transfers = await Future.wait( + effectiveRecipients.map((recipient) async { + return x_wallet.Transfer( + floatAmount: double.parse(recipient.amount), + strAddress: recipient.address, + assetHash: asset, + extraData: null, + ); + }), + ); + + final liveFee = double.parse( + await _libWallet.estimateFees(transfers: transfers), + ); + final rawFee = (liveFee * pow(10, 8)).round(); + estimatedFee = liveFee; + return rawFee.toInt(); + } catch (e, s) { + printV("Fee estimation failed. Using fallback fee: $defaultFee. error: $e, stackTrace: $s"); + estimatedFee = 0.00025; + return (defaultFee * pow(10, 8)).round().toInt(); + } + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + final asset = xelis_sdk.xelisAsset; + + // Default values + final defaultDecimals = 8; + final defaultFee = 25000; + + final effectiveRecipients = [ + Recipient( + address: 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', + amount: BigInt.from(amount ?? 0), + isChange: false, + ), + ]; + + // FIXME: hardcoded + return defaultFee; + } + + @override + Future createTransaction(Object credentials) async { + final xelisCredentials = credentials as XelisTransactionCredentials; + + final outputs = xelisCredentials.outputs; + + final hasMultiDestination = outputs.length > 1; + if (outputs.length > 255) { + throw XelisTooManyOutputsException(outputs.length); + } + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element == xelisCredentials.currency); + + final asset = balance[transactionCurrency]!.asset; + final walletBalanceForCurrency = balance[transactionCurrency]!.balance; + var totalAmountFromCredentials = 0; + + final fee = await _liveFeeEstimate(credentials, assetId: asset); + + double totalAmount = 0.0; + bool shouldSendAll = false; + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) < 0)) { + throw XelisTransactionCreationException(transactionCurrency); + } + + totalAmountFromCredentials = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + + totalAmount = double.parse(await _libWallet.formatCoin( + atomicAmount: BigInt.from(totalAmountFromCredentials), + assetHash: asset, + )); + + if (walletBalanceForCurrency < totalAmount) { + throw XelisTransactionCreationException(transactionCurrency); + } + } else { + final output = outputs.first; + shouldSendAll = output.sendAll; + + if (!shouldSendAll) { + totalAmountFromCredentials = output.formattedCryptoAmount ?? 0; + totalAmount = double.parse(output.cryptoAmount ?? '0.0'); + } else { + totalAmountFromCredentials = balance[transactionCurrency]!.balance; + totalAmount = double.parse(await _libWallet.formatCoin( + atomicAmount: BigInt.from(totalAmountFromCredentials), + assetHash: asset, + )); + } + + if (walletBalanceForCurrency < totalAmount || totalAmount < 0) { + throw XelisTransactionCreationException(transactionCurrency); + } + } + + var feeCurrency = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + bool isSendingXelis = true; + if (transactionCurrency != feeCurrency) { + isSendingXelis = false; + } + + if (isSendingXelis) { + if (balance[feeCurrency]!.balance < (totalAmount + fee)) { + throw XelisTransactionCreationException(transactionCurrency); + } + } else { + if (balance[feeCurrency]!.balance < fee) { + throw XelisTransactionCreationException(feeCurrency); + } + } + + late final String txJson; + if (shouldSendAll) { + txJson = await _libWallet.createTransferAllTransaction( + strAddress: xelisCredentials.outputs.first.isParsedAddress + ? xelisCredentials.outputs.first.extractedAddress! + : xelisCredentials.outputs.first.address, + assetHash: asset, + extraData: null, + ); + } else { + txJson = await _libWallet.createTransfersTransaction( + transfers: xelisCredentials.outputs.map((output) { + final amount = double.parse(output.cryptoAmount ?? '0.0'); + + return x_wallet.Transfer( + floatAmount: amount, + strAddress: output.isParsedAddress + ? output.extractedAddress! + : output.address, + assetHash: asset, + extraData: null, + ); + }).toList(), + ); + } + + + final txMap = jsonDecode(txJson); + final txHash = txMap['hash'] as String; + + // Broadcast the transaction + final send = () async { + await _libWallet.broadcastTransaction(txHash: txHash); + await updateBalance(); + }; + + return XelisPendingTransaction( + txid: txHash, + amount: XelisFormatter.formatAmount(totalAmountFromCredentials, decimals: balance[transactionCurrency]!.decimals), + fee: txMap['fee'], + decimals: balance[transactionCurrency]!.decimals, + send: send + ); + } + + // TODO + @override + Future signMessage(String message, {String? address}) async { + throw UnimplementedError(); + } + + // TODO + @override + Future verifyMessage(String message, String signature, {String? address}) async { + throw UnimplementedError(); + } + + @override + Future> fetchTransactions() async { + if (!(await _libWallet.isOnline())) { + return {}; + } + + final txList = (await _libWallet.allHistory()) + .map((jsonStr) => xelis_sdk.TransactionEntry.fromJson(json.decode(jsonStr)) as xelis_sdk.TransactionEntry) + .toList(); + + final Map result = {}; + + for (var entry in txList) { + if (!isSupportedEntryType(entry)) { continue; } + final info = await XelisTransactionInfo.fromTransactionEntry( + entry, + wallet: _libWallet, + isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset, + isTestnet: isTestnet + ); + if (entry.txEntryType is! xelis_sdk.InvokeContractEntry && info.assetAmounts.isEmpty) { continue; } + result[entry.hash] = info; + } + + return result; + } + + Future _updateTransactions({bool? isRescan}) async { + try { + if (!(await _libWallet.isOnline())) { + return; + } + + if (_isTransactionUpdating) { + return; + } + + try { + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + + if (isRescan == true) { + transactionHistory.clear(); + transactionHistory.addMany(transactions); + } else { + transactionHistory.update(transactions); + } + } finally { + _isTransactionUpdating = false; + await transactionHistory.save(); + } + } catch (e, s) { + printV("Xelis TX history update failed. error: $e, stackTrace: $s"); + _isTransactionUpdating = false; + } + } + + @override + Future renameWalletFiles(String newWalletName) async { + throw UnimplementedError(); + } + + @action + Future goOnline(String daemon) async { + await _libWallet.onlineMode(daemonAddress: daemon); + } + + @action + Future goOffline() async { + _isTransactionUpdating = false; + await _libWallet.offlineMode(); + } + + @override + Future close({bool shouldCleanup = false}) async { + if (requestedClose) { + return; + } + requestedClose = true; + _isTransactionUpdating = false; + _txSaveDebounceTimer?.cancel(); + _txBatchTimer?.cancel(); + + try { + await _processTransactionBuffer(); + await transactionHistory.save(); + } catch (e) { + printV("Error during wallet close cleanup: $e"); + } + + await _unsubscribeFromWalletEvents(); + await _libWallet.close(); + x_wallet.dropWallet(wallet: _libWallet); + } +} \ No newline at end of file diff --git a/cw_xelis/lib/xelis_wallet_addresses.dart b/cw_xelis/lib/xelis_wallet_addresses.dart new file mode 100644 index 000000000..986b00454 --- /dev/null +++ b/cw_xelis/lib/xelis_wallet_addresses.dart @@ -0,0 +1,61 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; + +part 'xelis_wallet_addresses.g.dart'; + +class XelisWalletAddresses = XelisWalletAddressesBase with _$XelisWalletAddresses; + +abstract class XelisWalletAddressesBase extends WalletAddresses with Store { + XelisWalletAddressesBase(this.walletInfo, this.wallet) + : super(walletInfo); + + final WalletInfo walletInfo; + final x_wallet.XelisWallet wallet; + @observable + String selectedAddr = ''; + + @override + @computed + String get address { + return selectedAddr; + } + + @override + set address(String addr) { + selectedAddr = addr; + } + + @override + Future init() async { + address = wallet.getAddressStr(); + addressesMap[address] = ''; + addressInfos[0] = [ + AddressInfo( + address: address, + label: '', + accountIndex: 0, + ) + ]; + usedAddresses.add(address); + await saveAddressesInBox(); + } + + @override + Future updateAddressesInBox() async {} + + List getAddressInfos() => addressInfos[0] ?? []; + + Future updateAddress(String addr, String label) async { + final infos = addressInfos[0]; + if (infos == null) return; + for (var info in infos) { + if (info.address == addr) { + info.label = label; + } + } + await saveAddressesInBox(); + } +} diff --git a/cw_xelis/lib/xelis_wallet_creation_credentials.dart b/cw_xelis/lib/xelis_wallet_creation_credentials.dart new file mode 100644 index 000000000..6d968f6a8 --- /dev/null +++ b/cw_xelis/lib/xelis_wallet_creation_credentials.dart @@ -0,0 +1,15 @@ +import 'package:cw_core/wallet_credentials.dart'; + +class XelisNewWalletCredentials extends WalletCredentials { + XelisNewWalletCredentials( + {required String name, String? password}) + : super(name: name, password: password); +} + +class XelisRestoreWalletFromSeedCredentials extends WalletCredentials { + XelisRestoreWalletFromSeedCredentials( + {required String name, required this.mnemonic, int height = 0, String? password}) + : super(name: name, password: password, height: height); + + final String mnemonic; +} \ No newline at end of file diff --git a/cw_xelis/lib/xelis_wallet_service.dart b/cw_xelis/lib/xelis_wallet_service.dart new file mode 100644 index 000000000..1a6b570c8 --- /dev/null +++ b/cw_xelis/lib/xelis_wallet_service.dart @@ -0,0 +1,497 @@ +import 'dart:io'; +import 'dart:async'; + +import 'package:system_info2/system_info2.dart'; + +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +import 'package:cw_xelis/xelis_wallet.dart'; +import 'package:cw_xelis/src/api/network.dart'; +import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; +import 'package:cw_xelis/xelis_wallet_creation_credentials.dart'; +import 'package:cw_xelis/xelis_store_utils.dart'; +import 'package:cw_xelis/src/api/logger.dart' as x_logger; +import 'package:cw_xelis/src/api/api.dart' as x_api; + +import 'package:collection/collection.dart'; +import 'package:hive/hive.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:mutex/mutex.dart'; + +class MemoryTierCalculator { + Future getDeviceRAMInGB() async { + if (kIsWeb) return 2; // Default for web + + try { + final totalRAM = SysInfo.getTotalPhysicalMemory(); + // Convert bytes to GB (1 GB = 1024³ bytes) + final ramGB = totalRAM / (1024 * 1024 * 1024); + return ramGB.round(); + } catch (e) { + print('Error getting RAM info: $e'); + return 4; // Default fallback + } + } +} + +class XelisLoggerFactory { + static bool _isSetup = false; + static const LOG_LEVEL = 3; + /* + Log level for FFI Rust outputs in xelis_flutter + + 0: None + 1: Error + 2: Warn + 3: Info + 4: Debug + 5: Trace + + */ + + static Future setupIfNeeded() async { + if (_isSetup) return; + await x_api.setUpRustLogger(); + _setupLogStream(); + _isSetup = true; + } + + static void _setupLogStream() { + x_api.createLogStream().listen((entry) { + final logLine = 'XELIS LOG | [${entry.level.name}] ${entry.tag}: ${entry.msg}'; + + switch (entry.level) { + case x_logger.Level.error: + if (LOG_LEVEL > 0) { + printV('❌ $logLine'); + } + break; + case x_logger.Level.warn: + if (LOG_LEVEL > 1) { + printV('⚠️ $logLine'); + } + break; + case x_logger.Level.info: + if (LOG_LEVEL > 2) { + printV('ℹ️ $logLine'); + } + break; + case x_logger.Level.debug: + if (LOG_LEVEL > 3) { + printV('🐛 $logLine'); + } + break; + case x_logger.Level.trace: + if (LOG_LEVEL > 4) { + printV('🔍 $logLine'); + } + break; + } + }, + onError: (dynamic e) { + printV("Error receiving Xelis Rust logs: $e"); + }); + } +} + + +enum XelisTableSize { + initial, + web, + low, + medium, + high; + + BigInt get l1Size { + switch (this) { + case XelisTableSize.initial: + case XelisTableSize.web: + return BigInt.from(23); + case XelisTableSize.low: + return BigInt.from(24); + case XelisTableSize.medium: + return BigInt.from(25); + case XelisTableSize.high: + return BigInt.from(26); + } + } + + static Future getPlatformDefault() async { + if (kIsWeb) { + return XelisTableSize.web; + } + + final calculator = MemoryTierCalculator(); + final ramInGB = await calculator.getDeviceRAMInGB(); + + if (ramInGB <= 2) { + return XelisTableSize.web; + } else if (ramInGB <= 4) { + return XelisTableSize.low; + } else if (ramInGB <= 8) { + return XelisTableSize.medium; + } else { + return XelisTableSize.high; + } + } +} + + +bool get kIsMobile { + if (kIsWeb) return false; + return Platform.isAndroid || Platform.isIOS; +} + + +Future getTableSize() async { + final tableSize = await XelisTableSize.getPlatformDefault(); + return tableSize.l1Size; +} + + +class XelisTableState { + final XelisTableSize currentSize; + final XelisTableSize _desiredSize; + + XelisTableSize get desiredSize { + if (kIsWeb) { + return XelisTableSize.low; + } + return _desiredSize; + } + + const XelisTableState({ + this.currentSize = XelisTableSize.low, + XelisTableSize desiredSize = XelisTableSize.high, + }) : _desiredSize = desiredSize; + + XelisTableState copyWith({ + XelisTableSize? currentSize, + XelisTableSize? desiredSize, + }) { + return XelisTableState( + currentSize: currentSize ?? this.currentSize, + desiredSize: kIsWeb ? XelisTableSize.low : (desiredSize ?? this._desiredSize), + ); + } + + factory XelisTableState.fromJson(Map json) { + return XelisTableState( + currentSize: XelisTableSize.values[json['currentSize'] as int], + desiredSize: XelisTableSize.values[json['desiredSize'] as int], + ); + } + + Map toJson() => { + 'currentSize': currentSize.index, + 'desiredSize': _desiredSize.index, + }; +} + +class XelisWalletService extends WalletService< + XelisNewWalletCredentials, + XelisRestoreWalletFromSeedCredentials, // TODO: add view key credentials when supported by Xelis + XelisNewWalletCredentials, + XelisNewWalletCredentials +> { + XelisWalletService(this.walletInfoSource, {required this.isDirect}) { + XelisLoggerFactory.setupIfNeeded(); + } + + final Box walletInfoSource; + final bool isDirect; + + static bool isGenerating = false; + static final _tableUpgradeMutex = Mutex(); + static Completer? _tableUpgradeCompleter; + static XelisWallet? _activeWallet; + + @override + WalletType getType() => WalletType.xelis; + + @override + Future isWalletExit(String name) async => + await File(await pathForWalletDir(name: name, type: getType())).exists(); + + Future _closeActiveWalletIfNeeded() async { + if (_activeWallet != null) { + try { + await _activeWallet!.close(); + } catch (e) { + printV("Error closing active Xelis wallet: $e"); + } + _activeWallet = null; + } + } + + Future _getTableState() async { + final tablesPath = await _getTablePath(); + final tablesDir = Directory(tablesPath); + final desiredSize = await XelisTableSize.getPlatformDefault(); + + final files = await tablesDir.list().toList(); + + // Check for the device-appropriate full table + final expectedFullTableName = 'tables_${desiredSize.l1Size}.bin'; + + final hasFullTables = files.any((file) => + file is File && file.path.contains(expectedFullTableName) + ); + + final hasLowTables = files.isNotEmpty; + + final currentSize = hasFullTables + ? desiredSize + : hasLowTables + ? XelisTableSize.initial + : XelisTableSize.initial; + + return XelisTableState( + currentSize: currentSize, + desiredSize: desiredSize, + ); + } + + Future _getTablePath() async { + final root = await getAppDir(); + final prefix = walletTypeToString(getType()).toLowerCase(); + final tablesDir = Directory('${root.path}/wallets/$prefix/tables/'); + + if (!await tablesDir.exists()) { + await tablesDir.create(recursive: true); + } + + return tablesDir.path; + } + + @override + Future create(XelisNewWalletCredentials credentials, {bool? isTestnet}) async { + final fullPath = await pathForWalletDir(name: credentials.name, type: getType()); + final tableState = await _getTableState(); + final tablesRoot = await _getTablePath(); + + final network = isTestnet == true ? Network.testnet : Network.mainnet; + + await _closeActiveWalletIfNeeded(); + final frbWallet = await x_wallet.createXelisWallet( + name: fullPath, + directory: "", + password: credentials.password ?? "x", + network: network, + precomputedTablesPath: tablesRoot, + l1Size: (await _getTableState()).currentSize.l1Size, + ); + + credentials.walletInfo!.address = frbWallet.getAddressStr(); + credentials.walletInfo!.network = network.name; + + final wallet = XelisWallet( + walletInfo:credentials.walletInfo!, + libWallet: frbWallet, + password: credentials.password ?? "x", + network: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + await wallet.init(); + await wallet.save(); + unawaited(_upgradeTablesIfNeeded()); + _activeWallet = wallet; + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; + + final fullPath = await pathForWalletDir(name: name, type: getType()); + final tableState = await _getTableState(); + final tablesRoot = await _getTablePath(); + + late final Network network; + + if (walletInfo?.network != null) { + network = NetworkName.fromName(walletInfo!.network!); + } else { + network = await loadXelisNetwork(name); + } + + await _closeActiveWalletIfNeeded(); + + late final x_wallet.XelisWallet frbWallet; + try { + frbWallet = await x_wallet.openXelisWallet( + name: fullPath, + directory: "", + password: password, + network: network, + precomputedTablesPath: tablesRoot, + l1Size: (await _getTableState()).currentSize.l1Size, + ); + } catch (_) { + try { + await restoreWalletFilesFromBackup(name); + frbWallet = await x_wallet.openXelisWallet( + name: fullPath, + directory: "", + password: password, + network: network, + precomputedTablesPath: tablesRoot, + l1Size: (await _getTableState()).currentSize.l1Size, + ); + } catch(_) { + rethrow; + } + } + final wallet = XelisWallet( + walletInfo: walletInfo, + libWallet: frbWallet, + password: password, + network: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + saveBackup(name); + await wallet.init(); + await wallet.save(); + unawaited(_upgradeTablesIfNeeded()); + _activeWallet = wallet; + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + + final fullPath = await pathForWalletDir(name: currentName, type: getType()); + + final newPath = await pathForWalletDir(name: newName, type: getType()); + final newDir = Directory(newPath); + final exists = await newDir.exists(); + if (exists) { + throw 'A wallet with this name already exists.'; + } + + await Directory(fullPath).rename(newPath); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + newWalletInfo.dirPath = await pathForWalletDir(name: newName, type: getType()); + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).deleteSync(recursive: true); + + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future restoreFromSeed(XelisRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + final fullPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); + final tableState = await _getTableState(); + final tablesRoot = await _getTablePath(); + + final network = isTestnet == true ? Network.testnet : Network.mainnet; + + await _closeActiveWalletIfNeeded(); + final frbWallet = await x_wallet.createXelisWallet( + name: fullPath, + directory: "", + password: credentials.password ?? "x", + seed: credentials.mnemonic, + network: network, + precomputedTablesPath: tablesRoot, + l1Size: (await _getTableState()).currentSize.l1Size, + ); + + credentials.walletInfo!.address = frbWallet.getAddressStr(); + credentials.walletInfo!.network = network.name; + + final wallet = XelisWallet( + walletInfo: credentials.walletInfo!, + libWallet: frbWallet, + password: credentials.password ?? "x", + network: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + await wallet.init(); + await wallet.save(); + unawaited(_upgradeTablesIfNeeded()); + _activeWallet = wallet; + return wallet; + } + + Future _upgradeTablesIfNeeded() async { + if (isGenerating || kIsWeb) return; + + if (_tableUpgradeCompleter != null) { + try { + await _tableUpgradeCompleter!.future; + return; + } catch (_) { + // Previous upgrade failed, try again + } + } + + await _tableUpgradeMutex.protect(() async { + if (_tableUpgradeCompleter != null) { + try { + await _tableUpgradeCompleter!.future; + return; + } catch (_) {} + } + + final state = await _getTableState(); + if (state.currentSize == state.desiredSize) return; + + _tableUpgradeCompleter = Completer(); + isGenerating = true; + + try { + printV("Xelis: Starting background table generation..."); + + final tablesPath = await _getTablePath(); + + await x_wallet.updateTables( + precomputedTablesPath: tablesPath, + l1Size: await getTableSize(), + ); + + printV("Xelis: Table upgrade to ${state.desiredSize.name} complete"); + _tableUpgradeCompleter?.complete(); + } catch (e, s) { + printV("Xelis: Failed to generate tables, $e, $s"); + _tableUpgradeCompleter?.completeError(e); + } finally { + isGenerating = false; + _tableUpgradeCompleter = null; + } + }); + } + + @override + Future restoreFromHardwareWallet( + XelisNewWalletCredentials credentials) async => + throw UnimplementedError(); + + @override + Future restoreFromKeys( + XelisNewWalletCredentials credentials, {bool? isTestnet}) async => + throw UnimplementedError(); +} diff --git a/cw_xelis/pubspec.yaml b/cw_xelis/pubspec.yaml new file mode 100644 index 000000000..79bc35ce0 --- /dev/null +++ b/cw_xelis/pubspec.yaml @@ -0,0 +1,91 @@ +name: cw_xelis +description: "A new Flutter plugin project." +version: 0.0.1 +homepage: +publish_to: none + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + http: ^1.1.0 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + shared_preferences: ^2.0.15 + flutter_mobx: ^2.0.6+1 + intl: ^0.19.0 + cw_core: + path: ../cw_core + xelis_flutter: + git: + url: https://github.com/xelis-project/xelis-flutter-ffi.git + ref: 83bda92f1b833fe5d8584aa429d5143a3698b33f + # path: ./xelis-flutter-ffi + xelis_dart_sdk: ^0.28.0 + mutex: ^3.1.0 + hive: ^2.2.3 + system_info2: ^4.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + build_runner: ^2.4.7 + mobx_codegen: ^2.3.0 + hive_generator: ^2.0.1 + build: ^2.4.0 + path: ^1.8.3 + analyzer: ^6.2.0 + +dependency_overrides: + watcher: ^1.1.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart index 7f68b71e6..90d17a1cc 100644 --- a/cw_zano/lib/zano_wallet.dart +++ b/cw_zano/lib/zano_wallet.dart @@ -121,7 +121,7 @@ abstract class ZanoWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, [int? amount = null]) => + int (TransactionPriority priority, [int? amount = null]) => getCurrentTxFee(priority); @override diff --git a/integration_test/robots/wallet_keys_robot.dart b/integration_test/robots/wallet_keys_robot.dart index 189929737..cb02e0012 100644 --- a/integration_test/robots/wallet_keys_robot.dart +++ b/integration_test/robots/wallet_keys_robot.dart @@ -100,6 +100,15 @@ class WalletKeysAndSeedPageRobot { commonTestCases.hasText(appStore.wallet!.privateKey!); tester.printToConsole('$walletName wallet has private key properly displayed'); } + + if (walletType == WalletType.xelis) { + if (hasSeed) { + commonTestCases.hasText(appStore.wallet!.seed); + tester.printToConsole('$walletName wallet has seeds properly displayed'); + commonTestCases.hasText(appStore.wallet!.langSeed(lang.nameEnglish)); + tester.printToConsole('$walletName wallet has language seed variant properly displayed'); + } + } } await commonTestCases.defaultSleepTime(seconds: 5); diff --git a/key.jks b/key.jks new file mode 100644 index 000000000..9343a4e6f Binary files /dev/null and b/key.jks differ diff --git a/lib/core/execution_state.dart b/lib/core/execution_state.dart index 860dfee10..db060406f 100644 --- a/lib/core/execution_state.dart +++ b/lib/core/execution_state.dart @@ -5,6 +5,7 @@ class InitialExecutionState extends ExecutionState {} class LoadingTemplateExecutingState extends ExecutionState {} class IsExecutingState extends ExecutionState {} +class IsLoadingState extends ExecutionState {} class ExecutedSuccessfullyState extends ExecutionState { ExecutedSuccessfullyState({this.payload}); diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 344b5391d..6d0bbb54c 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cake_wallet/utils/language_list.dart'; import 'package:cw_core/wallet_type.dart'; @@ -50,6 +51,8 @@ class SeedValidator extends Validator { return zano!.getWordList(language); case WalletType.decred: return decred!.getDecredWordList(); + case WalletType.xelis: + return xelis!.getXelisWordList(language); case WalletType.none: case WalletType.haven: return []; diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index b44e56a98..7f4410b25 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -91,6 +91,7 @@ class WalletCreationService { case WalletType.banano: case WalletType.zano: case WalletType.decred: + case WalletType.xelis: return false; } } diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index f16bf7e14..595eb2cc1 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -64,6 +64,7 @@ class WalletLoadingService { } final walletService = walletServiceFactory.call(type); final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name)); + final wallet = await walletService.openWallet(name, walletPassword); if (type == WalletType.monero) { @@ -93,6 +94,7 @@ class WalletLoadingService { try { final walletService = walletServiceFactory.call(walletInfo.type); final walletPassword = await keyService.getWalletPassword(walletName: walletInfo.name); + wallet = await walletService.openWallet(walletInfo.name, walletPassword); if (walletInfo.type == WalletType.monero) { diff --git a/lib/di.dart b/lib/di.dart index 46c453d61..2a9035a2c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -80,6 +80,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -1153,6 +1154,8 @@ Future setup({ return zano!.createZanoWalletService(_walletInfoSource); case WalletType.decred: return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource); + case WalletType.xelis: + return xelis!.createXelisWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.haven: return HavenWalletService(_walletInfoSource); case WalletType.none: diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 45234d5ec..86382173b 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -46,6 +46,8 @@ const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const zanoDefaultNodeUri = 'zano.cakewallet.com:11211'; const moneroWorldNodeUri = '.moneroworld.com'; const decredDefaultUri = "default-spv-nodes"; +const xelisDefaultUri = "us-node.xelis.io"; +const xelisTestnetUri = "testnet-node.xelis.io"; Future defaultSettingsMigration( {required int version, @@ -511,6 +513,17 @@ Future defaultSettingsMigration( enabled: true, ); break; + case 50: + await _fixNodesUseSSLFlag(nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.xelis); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.xelis, + currentNodePreferenceKey: PreferencesKey.currentXelisNodeIdKey, + useSSL: true, + ); + break; default: break; } @@ -617,6 +630,8 @@ String _getDefaultNodeUri(WalletType type) { return zanoDefaultNodeUri; case WalletType.decred: return decredDefaultUri; + case WalletType.xelis: + return xelisDefaultUri; case WalletType.banano: case WalletType.none: return ''; @@ -807,6 +822,12 @@ Node? getBitcoinTestnetDefaultElectrumServer({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); } +Node? getXelisTestnetDefault({required Box nodes}) { + return nodes.values + .firstWhereOrNull((Node node) => node.uriRaw == xelisTestnetUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.xelis); +} + Node? getDefaultNode({required Box nodes, required WalletType type}) { final defaultUri = _getDefaultNodeUri(type); return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == defaultUri) ?? @@ -1045,6 +1066,7 @@ Future checkCurrentNodes( final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); + final currentXelisNodeId = sharedPreferences.getInt(PreferencesKey.currentXelisNodeIdKey); final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1079,6 +1101,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); final currentZanoNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); + final currentXelisNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentXelisNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); @@ -1175,6 +1199,15 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); } + + if (currentXelisNodeServer == null) { + final node = Node(uri: xelisDefaultUri, type: WalletType.xelis); + await nodeSource.add(node); + final xelisTestnet = + Node(uri: xelisTestnetUri, type: WalletType.xelis, useSSL: true); + await nodeSource.add(xelisTestnet); + await sharedPreferences.setInt(PreferencesKey.currentXelisNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index bb489e715..a66796b29 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -46,6 +46,9 @@ Future> loadDefaultNodes(WalletType type) async { case WalletType.decred: path = 'assets/decred_node_list.yml'; break; + case WalletType.xelis: + path = 'assets/xelis_node_list.yml'; + break; case WalletType.banano: case WalletType.none: path = ''; @@ -96,6 +99,7 @@ Future resetToDefault(Box nodeSource) async { final tronNodes = await loadDefaultNodes(WalletType.tron); final decredNodes = await loadDefaultNodes(WalletType.decred); final zanoNodes = await loadDefaultNodes(WalletType.zano); + final xelisNodes = await loadDefaultNodes(WalletType.xelis); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -108,7 +112,8 @@ Future resetToDefault(Box nodeSource) async { solanaNodes + tronNodes + zanoNodes + - decredNodes; + decredNodes + + xelisNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 9e384f462..04a1c3612 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -19,6 +19,7 @@ class PreferencesKey { static const currentSolanaNodeIdKey = 'current_node_id_sol'; static const currentTronNodeIdKey = 'current_node_id_trx'; static const currentWowneroNodeIdKey = 'current_node_id_wow'; + static const currentXelisNodeIdKey = 'current_node_id_xelis'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; @@ -52,6 +53,7 @@ class PreferencesKey { static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; static const decredTransactionPriority = 'current_fee_priority_decred'; + static const xelisTransactionPriority = 'current_fee_priority_xelis'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 5307250d5..bdce80a3a 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -35,6 +36,8 @@ List priorityForWalletType(WalletType type) { return zano!.getTransactionPriorities(); case WalletType.decred: return decred!.getTransactionPriorities(); + case WalletType.xelis: + return xelis!.getTransactionPriorities(); case WalletType.none: case WalletType.haven: return []; diff --git a/lib/main.dart b/lib/main.dart index 6795b7ff9..a7902b0b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,6 +52,8 @@ import 'package:cw_core/window_size.dart'; import 'package:logging/logging.dart'; import 'package:cake_wallet/core/trade_monitor.dart'; +import 'frb_init.g.dart'; + final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); @@ -63,6 +65,7 @@ Future main({Key? topLevelKey}) async { Future runAppWithZone({Key? topLevelKey}) async { bool isAppRunning = false; + await frb_init(); await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = ExceptionHandler.onError; @@ -228,7 +231,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 49, + initialMigrationVersion: 50, ); } diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart index a46adb6b1..a833c7536 100644 --- a/lib/reactions/bip39_wallet_utils.dart +++ b/lib/reactions/bip39_wallet_utils.dart @@ -17,6 +17,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.haven: case WalletType.zano: case WalletType.decred: + case WalletType.xelis: case WalletType.none: return false; } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index dbc5f9ae4..1893ddd0d 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; @@ -54,6 +55,11 @@ Future startFiatRateUpdate( tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } + if (appStore.wallet!.type == WalletType.xelis) { + currencies = + xelis!.getXelisAssets(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { // skip potential scams: diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 08f1a25ee..10aa9f262 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; @@ -132,6 +133,10 @@ void startCurrentWalletChangeReaction( currencies = tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } + if (wallet.type == WalletType.xelis) { + currencies = + xelis!.getXelisAssets(appStore.wallet!).where((element) => element.enabled); + } if (currencies != null) { for (final currency in currencies) { diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index b4b6d0b8b..087e314c4 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -45,6 +45,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -181,6 +182,8 @@ class _DesktopWalletSelectionDropDownState extends State { this.tronIcon = Image.asset('assets/images/trx_icon.png'), this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'), this.zanoIcon = Image.asset('assets/images/zano_icon.png'), - this.decredIcon = Image.asset('assets/images/decred_menu.png'); + this.decredIcon = Image.asset('assets/images/decred_menu.png'), + this.xelisIcon = Image.asset('assets/images/xelis_icon.png'); final largeScreen = 731; @@ -64,6 +65,7 @@ class MenuWidgetState extends State { Image wowneroIcon; Image zanoIcon; Image decredIcon; + Image xelisIcon; @override void initState() { @@ -255,6 +257,8 @@ class MenuWidgetState extends State { return zanoIcon; case WalletType.decred: return decredIcon; + case WalletType.xelis: + return xelisIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index bd03c17bd..da29260d7 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -275,7 +275,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); }), if (widget.privacySettingsViewModel.type == WalletType.bitcoin || - widget.privacySettingsViewModel.type == WalletType.decred) + widget.privacySettingsViewModel.type == WalletType.decred || + widget.privacySettingsViewModel.type == WalletType.xelis) Builder(builder: (_) { final val = testnetValue ?? false; return SettingsSwitcherCell( diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 5c60a7d23..082d5b48e 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -24,7 +24,7 @@ class RescanPage extends BasePage { @override Widget body(BuildContext context) { Widget child; - if (_rescanViewModel.wallet.type != WalletType.decred) { + if (_rescanViewModel.wallet.type != WalletType.decred && _rescanViewModel.wallet.type != WalletType.xelis) { child = Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), child: Column( diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index dd4a284be..645bfd250 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -584,6 +584,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true; if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true; + if (seedWords.length == 25 && walletRestoreViewModel.type == WalletType.xelis) return true; if (seedWords.length == 12 && walletRestoreViewModel.type == WalletType.monero) { return walletRestoreFromSeedFormKey.currentState?.blockchainHeightKey.currentState diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index b2f3bc605..4e4f97f34 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -129,6 +129,7 @@ class WalletListBodyState extends State { final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24); final wowneroIcon = Image.asset('assets/images/wownero_icon.png', height: 24, width: 24); final zanoIcon = Image.asset('assets/images/zano_icon.png', height: 24, width: 24); + final xelisIcon = Image.asset('assets/images/xelis_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; diff --git a/lib/src/screens/wallet_unlock/wallet_unlock_page.dart b/lib/src/screens/wallet_unlock/wallet_unlock_page.dart index d0c6d0844..1f0ce8a72 100644 --- a/lib/src/screens/wallet_unlock/wallet_unlock_page.dart +++ b/lib/src/screens/wallet_unlock/wallet_unlock_page.dart @@ -56,6 +56,13 @@ class WalletUnlockPageState extends AuthPageState { }); } + if (state is IsLoadingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // null duration to make it indefinite until its disposed + _authBar = createBar(S.of(context).loading_wallet, context, duration: null)..show(context); + }); + } + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) async { dismissFlushBar(_authBar); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index c88590475..bee8cd61a 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; @@ -137,6 +138,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialBitcoinCashTransactionPriority, TransactionPriority? initialZanoTransactionPriority, TransactionPriority? initialDecredTransactionPriority, + TransactionPriority? initialXelisTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -225,6 +227,9 @@ abstract class SettingsStoreBase with Store { if (initialDecredTransactionPriority != null) { priority[WalletType.decred] = initialDecredTransactionPriority; } + if (initialXelisTransactionPriority != null) { + priority[WalletType.xelis] = initialXelisTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; @@ -283,6 +288,9 @@ abstract class SettingsStoreBase with Store { case WalletType.decred: key = PreferencesKey.decredTransactionPriority; break; + case WalletType.xelis: + key = PreferencesKey.xelisTransactionPriority; + break; default: key = null; } @@ -912,6 +920,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? wowneroTransactionPriority; TransactionPriority? zanoTransactionPriority; TransactionPriority? decredTransactionPriority; + TransactionPriority? xelisTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -945,6 +954,10 @@ abstract class SettingsStoreBase with Store { decredTransactionPriority = decred?.deserializeDecredTransactionPriority( sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.xelisTransactionPriority) != null) { + xelisTransactionPriority = xelis?.deserializeXelisTransactionPriority( + sharedPreferences.getInt(PreferencesKey.xelisTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -954,6 +967,7 @@ abstract class SettingsStoreBase with Store { bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); decredTransactionPriority ??= decred?.getDecredTransactionPriorityMedium(); + xelisTransactionPriority ??= xelis?.getXelisTransactionPriorityMedium(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); @@ -1050,6 +1064,7 @@ abstract class SettingsStoreBase with Store { final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); + final xelisNodeId = sharedPreferences.getInt(PreferencesKey.currentXelisNodeIdKey); /// get the selected node, if null, then use the default final moneroNode = nodeSource.get(nodeId) ?? @@ -1068,6 +1083,8 @@ abstract class SettingsStoreBase with Store { nodeSource.values.firstWhereOrNull((e) => e.uriRaw == nanoDefaultNodeUri); final decredNode = nodeSource.get(decredNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == decredDefaultUri); + final xelisNode = nodeSource.get(xelisNodeId) ?? + nodeSource.values.firstWhereOrNull((e) => e.uriRaw == xelisDefaultUri); final nanoPowNode = powNodeSource.get(nanoPowNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == nanoDefaultPowNodeUri); final solanaNode = nodeSource.get(solanaNodeId) ?? @@ -1164,6 +1181,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.decred] = decredNode; } + if (xelisNode != null) { + nodes[WalletType.xelis] = xelisNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 2); // default to 2 - daily sync }); @@ -1334,6 +1355,7 @@ abstract class SettingsStoreBase with Store { initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, initialDecredTransactionPriority: decredTransactionPriority, + initialXelisTransactionPriority: xelisTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, @@ -1412,6 +1434,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.decred] = decred!.deserializeDecredTransactionPriority( sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); } + if (xelis != null && + sharedPreferences.getInt(PreferencesKey.xelisTransactionPriority) != null) { + priority[WalletType.xelis] = xelis!.deserializeXelisTransactionPriority( + sharedPreferences.getInt(PreferencesKey.xelisTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -1522,6 +1549,7 @@ abstract class SettingsStoreBase with Store { final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); + final xelisNodeId = sharedPreferences.getInt(PreferencesKey.currentXelisNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1535,6 +1563,7 @@ abstract class SettingsStoreBase with Store { final wowneroNode = nodeSource.get(wowneroNodeId); final zanoNode = nodeSource.get(zanoNodeId); final decredNode = nodeSource.get(decredNodeId); + final xelisNode = nodeSource.get(xelisNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; @@ -1589,6 +1618,9 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.decred] = decredNode; } + if (xelisNode != null) { + nodes[WalletType.xelis] = xelisNode; + } // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1728,6 +1760,9 @@ abstract class SettingsStoreBase with Store { case WalletType.decred: await _sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); break; + case WalletType.xelis: + await _sharedPreferences.setInt(PreferencesKey.currentXelisNodeIdKey, node.key as int); + break; case WalletType.zano: await _sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); default: diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart index f2083c772..1b8e5f839 100644 --- a/lib/utils/address_formatter.dart +++ b/lib/utils/address_formatter.dart @@ -11,7 +11,103 @@ class AddressFormatter { TextAlign? textAlign, bool shouldTruncate = false, }) { + // Check for parentheses in the address + final bracketIndex = address.indexOf('['); + + if (bracketIndex != -1) { + // Split address and amount parts + final addressPart = address.substring(0, bracketIndex).trim(); + final amountPart = address.substring(bracketIndex); + + // For truncated addresses, handle differently + if (shouldTruncate) { + final addressWidget = _buildAddressWidget( + address: addressPart, + walletType: walletType, + evenTextStyle: evenTextStyle, + oddTextStyle: oddTextStyle, + textAlign: textAlign, + shouldTruncate: shouldTruncate, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + addressWidget, + Text(amountPart, style: evenTextStyle), + ], + ); + } + + // For full addresses, integrate amount with last line + final cleanAddress = addressPart.replaceAll('bitcoincash:', ''); + final isMWEB = addressPart.startsWith('ltcmweb'); + final chunkSize = walletType != null ? _getChunkSize(walletType) : 4; + + // Build chunks + final chunks = []; + if (isMWEB) { + const mwebDisplayPrefix = 'ltcmweb'; + chunks.add(mwebDisplayPrefix); + final startIndex = mwebDisplayPrefix.length; + for (int i = startIndex; i < cleanAddress.length; i += chunkSize) { + final chunk = cleanAddress.substring( + i, + math.min(i + chunkSize, cleanAddress.length), + ); + chunks.add(chunk); + } + } else { + for (int i = 0; i < cleanAddress.length; i += chunkSize) { + final chunk = cleanAddress.substring( + i, + math.min(i + chunkSize, cleanAddress.length), + ); + chunks.add(chunk); + } + } + + // Build text spans with amount appended to last chunk + final spans = []; + for (int i = 0; i < chunks.length; i++) { + final style = (i % 2 == 0) ? evenTextStyle : oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128)); + + if (i == chunks.length - 1) { + // Last chunk - append amount + spans.add(TextSpan(text: '${chunks[i]} ', style: style)); + spans.add(TextSpan(text: amountPart, style: evenTextStyle)); + } else { + spans.add(TextSpan(text: '${chunks[i]} ', style: style)); + } + } + + return RichText( + text: TextSpan(children: spans), + textAlign: textAlign ?? TextAlign.start, + overflow: TextOverflow.visible, + ); + } + + // No parentheses - use original logic + return _buildAddressWidget( + address: address, + walletType: walletType, + evenTextStyle: evenTextStyle, + oddTextStyle: oddTextStyle, + textAlign: textAlign, + shouldTruncate: shouldTruncate, + ); + } + static Widget _buildAddressWidget({ + required String address, + WalletType? walletType, + required TextStyle evenTextStyle, + TextStyle? oddTextStyle, + TextAlign? textAlign, + bool shouldTruncate = false, + }) { final cleanAddress = address.replaceAll('bitcoincash:', ''); final isMWEB = address.startsWith('ltcmweb'); final chunkSize = walletType != null ? _getChunkSize(walletType) : 4; diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 803744590..56fe480c9 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -56,6 +56,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.haven: case WalletType.zano: case WalletType.decred: + case WalletType.xelis: return false; } } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index ea9272a5c..ccdb5d8e8 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -112,7 +112,8 @@ abstract class BalanceViewModelBase with Store { isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || wallet.type == WalletType.tron || - wallet.type == WalletType.zano; + wallet.type == WalletType.zano || + wallet.type == WalletType.xelis; @computed bool get hasAccounts => wallet.type == WalletType.monero || wallet.type == WalletType.wownero; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 30bd1c8b3..f163994ea 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; @@ -468,7 +469,8 @@ abstract class DashboardViewModelBase with Store { WalletBase, TransactionInfo> wallet; @computed - bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); + bool get isTestnet => (wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet)) || + (wallet.type == WalletType.xelis && xelis!.isTestnet(wallet)); @computed bool get hasRescan => wallet.hasRescan; @@ -859,6 +861,7 @@ abstract class DashboardViewModelBase with Store { return true; case WalletType.zano: case WalletType.haven: + case WalletType.xelis: // TODO: finalize whether to change this case WalletType.none: return false; } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 4794746c4..b2a84511e 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -109,6 +110,14 @@ abstract class HomeSettingsViewModelBase with Store { ); } + if (_balanceViewModel.wallet.type == WalletType.xelis) { + await xelis!.updateAssetState( + _balanceViewModel.wallet, + token, + contractAddress, + ); + } + if (_balanceViewModel.wallet.type == WalletType.tron) { await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); } @@ -148,6 +157,9 @@ abstract class HomeSettingsViewModelBase with Store { if (_balanceViewModel.wallet.type == WalletType.zano) { await zano!.deleteZanoAsset(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.xelis) { + await xelis!.deleteAsset(_balanceViewModel.wallet, token); + } _updateTokensList(); } finally { isDeletingToken = false; @@ -206,6 +218,11 @@ abstract class HomeSettingsViewModelBase with Store { case WalletType.tron: defaultTokenAddresses = tron!.getDefaultTokenContractAddresses(); break; + case WalletType.xelis: + // TODO + // defaultTokenAddresses = xelis!.getDefaultAssetIDs(); + // break; + return false; case WalletType.zano: case WalletType.banano: case WalletType.monero: @@ -388,6 +405,10 @@ abstract class HomeSettingsViewModelBase with Store { return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.xelis) { + return await xelis!.getAsset(_balanceViewModel.wallet, contractAddress); + } + if (_balanceViewModel.wallet.type == WalletType.tron) { return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress); } @@ -430,6 +451,12 @@ abstract class HomeSettingsViewModelBase with Store { solana!.addSPLToken(_balanceViewModel.wallet, token, address); } + if (_balanceViewModel.wallet.type == WalletType.xelis) { + final id = xelis!.getAssetId(token); + xelis!.updateAssetState(_balanceViewModel.wallet, token, id); + if (!value) await xelis!.removeAssetTransactionsInHistory(_balanceViewModel.wallet, token); + } + if (_balanceViewModel.wallet.type == WalletType.tron) { final address = tron!.getTokenAddress(token); tron!.addTronToken(_balanceViewModel.wallet, token, address); @@ -501,6 +528,15 @@ abstract class HomeSettingsViewModelBase with Store { .toList() ..sort(_sortFunc)); } + + + if (_balanceViewModel.wallet.type == WalletType.xelis) { + tokens.addAll(xelis! + .getXelisAssets(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } } @action @@ -549,7 +585,12 @@ abstract class HomeSettingsViewModelBase with Store { return zano!.getZanoAssetAddress(asset); } - // We return null if it's neither Tron, Polygon, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets). + if (_balanceViewModel.wallet.type == WalletType.xelis) { + return xelis!.getAssetId(asset); + } + + + // We return null if it's neither Tron, Polygon, Ethereum, Xelis or Solana wallet (which is actually impossible because we only display home settings for either of these five wallets). return null; } } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index f15d7dad6..6f4ddb666 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -36,6 +36,7 @@ abstract class ReceiveOptionViewModelBase with Store { _options = [ReceivePageOption.mainnet]; break; case WalletType.decred: + case WalletType.xelis: if (_wallet.isTestnet) { _options = [ ReceivePageOption.testnet, diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 808ccabea..1e0e5394e 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -46,10 +47,17 @@ class TransactionListItem extends ActionListItem with Keyable { bool get hasTokens => isEVMCompatibleChain(balanceViewModel.wallet.type) || balanceViewModel.wallet.type == WalletType.solana || - balanceViewModel.wallet.type == WalletType.tron; + balanceViewModel.wallet.type == WalletType.tron || + balanceViewModel.wallet.type == WalletType.xelis; String get formattedCryptoAmount { - return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted(); + late final String amtText; + if (transaction.amountFormatted() == "MULTI") { + amtText = S.current.multi_transfer; + } else { + amtText = transaction.amountFormatted(); + } + return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : amtText; } String get formattedTitle { @@ -108,6 +116,7 @@ class TransactionListItem extends ActionListItem with Keyable { WalletType.wownero, WalletType.litecoin, WalletType.zano, + WalletType.xelis ].contains(balanceViewModel.wallet.type)) { return formattedPendingStatus; } @@ -143,6 +152,11 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); return asset; } + + if (balanceViewModel.wallet.type == WalletType.xelis) { + final asset = xelis!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } } catch (e) { return null; } @@ -224,6 +238,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), price: price); break; + case WalletType.xelis: + final asset = xelis!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: xelis!.formatterXelisAmountToDouble(amount: xelis!.getTransactionAmountRaw(transaction)), + price: price); + break; case WalletType.none: case WalletType.banano: case WalletType.haven: diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 76a49e1d5..a48647a43 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -253,11 +253,16 @@ abstract class ExchangeTradeViewModelBase with Store { wallet.currency == CryptoCurrency.sol && tradesStore.trade!.from.tag == CryptoCurrency.sol.title; + bool _isXelisAsset() => + wallet.currency == CryptoCurrency.xel && + tradesStore.trade!.from.tag == CryptoCurrency.xel.title; + return tradesStore.trade!.from == wallet.currency || tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || _isEthToken() || _isPolygonToken() || _isSplToken() || - _isTronToken(); + _isTronToken() || + _isXelisAsset(); } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 58b5e5756..3f6daba1d 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -779,6 +779,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.dcr; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.xelis: + depositCurrency = CryptoCurrency.xel; + receiveCurrency = CryptoCurrency.btc; + break; case WalletType.none: break; } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 7e4e73915..6a8360ad5 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -87,6 +87,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.bitcoin: case WalletType.zano: case WalletType.decred: + case WalletType.xelis: return false; } } diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 9df5f2980..89772cd2a 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -51,6 +51,8 @@ abstract class NodeListViewModelBase with Store { Node node; if (_appStore.wallet!.type == WalletType.bitcoin && _appStore.wallet!.isTestnet) { node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; + } else if (_appStore.wallet!.type == WalletType.xelis && _appStore.wallet!.isTestnet) { + node = getXelisTestnetDefault(nodes: _nodeSource)!; } else { node = getDefaultNode(nodes: _nodeSource, type: _appStore.wallet!.type)!; } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 530ba1725..232b12af8 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -47,6 +47,9 @@ class WalletRestoreFromQRCode { 'decred': WalletType.decred, 'decred-wallet': WalletType.decred, 'decred_wallet': WalletType.decred, + 'xelis': WalletType.xelis, + 'xelis-wallet': WalletType.xelis, + 'xelis_wallet': WalletType.xelis, }; static WalletType? _extractWalletType(String code) { diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index f6dd0f201..10a6ccfb2 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -91,6 +92,7 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); case WalletType.decred: return transactionPriority == decred!.getDecredTransactionPrioritySlow(); + case WalletType.xelis: case WalletType.none: case WalletType.nano: case WalletType.banano: diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 6c3588404..7255924f9 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; @@ -116,6 +117,9 @@ abstract class OutputBase with Store { case WalletType.zano: _amount = zano!.formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); break; + case WalletType.xelis: + _amount = xelis!.formatterStringDoubleToAmount(_cryptoAmount, currency: cryptoCurrencyHandler()); + break; case WalletType.none: case WalletType.haven: case WalletType.nano: @@ -153,6 +157,10 @@ abstract class OutputBase with Store { return solana!.getEstimateFees(_wallet) ?? 0.0; } + if (_wallet.type == WalletType.xelis) { + return xelis!.getEstimateFees(_wallet) ?? 0.0; + } + int? fee = _wallet.calculateEstimatedFee( _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); @@ -193,6 +201,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.decred) { return decred!.formatterDecredAmountToDouble(amount: fee); } + + if (_wallet.type == WalletType.xelis) { + return xelis!.formatterXelisAmountToDouble(amount: BigInt.from(fee)); + } } catch (e) { printV(e.toString()); } @@ -205,7 +217,8 @@ abstract class OutputBase with Store { try { final currency = (isEVMCompatibleChain(_wallet.type) || _wallet.type == WalletType.solana || - _wallet.type == WalletType.tron) + _wallet.type == WalletType.tron || + _wallet.type == WalletType.xelis) ? _wallet.currency : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( @@ -302,6 +315,7 @@ abstract class OutputBase with Store { case WalletType.zano: case WalletType.nano: case WalletType.decred: + case WalletType.xelis: maximumFractionDigits = 12; break; case WalletType.bitcoin: diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 3c78f3000..2011b096a 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -101,5 +101,6 @@ abstract class SendTemplateViewModelBase with Store { bool get hasMultipleTokens => isEVMCompatibleChain(_wallet.type) || _wallet.type == WalletType.solana || - _wallet.type == WalletType.tron; + _wallet.type == WalletType.tron || + _wallet.type == WalletType.xelis; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index edca5636c..1dcfd9f6d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -25,6 +25,11 @@ import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; +import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; +import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; @@ -69,7 +74,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || wallet.type == WalletType.tron || - wallet.type == WalletType.zano; + wallet.type == WalletType.zano || + wallet.type == WalletType.xelis; } UnspentCoinsListViewModel unspentCoinsListViewModel; @@ -91,7 +97,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || appStore.wallet!.type == WalletType.solana || appStore.wallet!.type == WalletType.tron || - appStore.wallet!.type == WalletType.zano, + appStore.wallet!.type == WalletType.zano || + appStore.wallet!.type == WalletType.xelis, outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -199,6 +206,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.polygon: case WalletType.tron: case WalletType.solana: + case WalletType.xelis: return wallet.currency; default: return selectedCryptoCurrency; @@ -482,6 +490,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } + if (wallet.type == WalletType.xelis) { + final outputCount = pendingTransaction?.outputCount ?? 0; + if (outputCount > 255) { + throw Exception("Xelis does not support more than 255 outputs"); + } + } + state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { @@ -534,6 +549,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } + String normalizeAmount(String amount) { + if (amount.contains('.')) return amount; + return '$amount.0'; + } + @action Future commitTransaction(BuildContext context) async { if (pendingTransaction == null) { @@ -558,11 +578,25 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return; } - String address = outputs.fold('', (acc, value) { - return value.isParsedAddress - ? '$acc${value.address}\n${value.extractedAddress}\n\n' - : '$acc${value.address}\n\n'; - }); + late String address; + + if (walletType == WalletType.xelis) { + address = outputs.fold('', (acc, value) { + final nameLine = value.isParsedAddress ? '${value.address}\n' : ''; + final realAddress = value.isParsedAddress ? value.extractedAddress : value.address; + final amount = normalizeAmount(value.cryptoAmount ?? '0.0'); + final symbol = value.cryptoCurrencyHandler().title ?? ''; + final amountPart = outputs.length > 1 ? ' ($amount $symbol)' : ''; + + return '$acc$nameLine$realAddress$amountPart\n\n'; + }); + } else { + address = outputs.fold('', (acc, value) { + return value.isParsedAddress + ? '$acc${value.address}\n${value.extractedAddress}\n\n' + : '$acc${value.address}\n\n'; + }); + } address = address.trim(); @@ -677,6 +711,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.decred: this.coinTypeToSpendFrom = UnspentCoinType.any; return decred!.createDecredTransactionCredentials(outputs, priority!); + case WalletType.xelis: + return xelis!.createXelisTransactionCredentials(outputs, priority: priority!, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 691c94f56..a7266120b 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -48,6 +48,11 @@ abstract class TransactionDetailsViewModelBase with Store { final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); + // TODO: can be cleaned further switch (wallet.type) { case WalletType.monero: @@ -88,16 +93,14 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.decred: _addDecredListItems(tx, dateFormat); break; + case WalletType.xelis: + _addXelisListItems(tx, dateFormat, description.recipientAddress?.isEmpty ?? true); + break; case WalletType.none: case WalletType.banano: break; } - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; - final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: descriptionKey)); - if (showRecipientAddress && !isRecipientAddressShown) { final recipientAddress = description.recipientAddress; @@ -193,6 +196,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://explorer.zano.org/transaction/${txId}'; case WalletType.decred: return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}'; + case WalletType.xelis: + return 'https://${wallet.isTestnet ? "testnet-" : ""}explorer.xelis.io/txs/${txId}'; case WalletType.none: return ''; } @@ -227,6 +232,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'explorer.zano.org'; case WalletType.decred: return S.current.view_transaction_on + 'dcrdata.decred.org'; + case WalletType.xelis: + return S.current.view_transaction_on + 'explorer.xelis.io'; case WalletType.none: return ''; } @@ -562,6 +569,58 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addXelisListItems(TransactionInfo tx, DateFormat dateFormat, bool showTo) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.id, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + if (!tx.amountFormatted().startsWith(":MULTI:")) + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.amountFormatted().startsWith(":MULTI:")) + StandartListItem( + title: S.current.transaction_details_multi_breakdown, + value: tx.amountFormatted().split(":MULTI:")[1]!, + key: ValueKey('standard_list_item_transaction_details_multi_breakdown_key'), + ), + if (tx.feeFormatted()?.isNotEmpty ?? false && tx.direction == TransactionDirection.outgoing) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (showRecipientAddress && tx.to != null && showTo) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + ]; + + items.addAll(_items); + } + void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) { transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium(); final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true) @@ -629,7 +688,7 @@ abstract class TransactionDetailsViewModelBase with Store { ); } - if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { + if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty && wallet.type != WalletType.xelis) { final outputAddresses = transactionInfo.outputAddresses!.map((element) { if (element.contains('OP_RETURN:') && element.length > 40) { return element.substring(0, 40) + '...'; @@ -670,7 +729,7 @@ abstract class TransactionDetailsViewModelBase with Store { value: tx.feeFormatted()!, key: ValueKey('standard_list_item_transaction_details_fee_key'), ), - if (showRecipientAddress && tx.to != null) + if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing) StandartListItem( title: S.current.transaction_details_recipient_address, value: tron!.getTronBase58Address(tx.to!, wallet), diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 910f081ee..98baa5c24 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -242,6 +242,22 @@ class DecredURI extends PaymentURI { } } +class XelisURI extends PaymentURI { + XelisURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'xelis:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -350,6 +366,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return ZanoURI(amount: amount, address: address.address); case WalletType.decred: return DecredURI(amount: amount, address: address.address); + case WalletType.xelis: + return XelisURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index da5d04f59..7f2ce75bc 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -164,6 +164,7 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.none: case WalletType.haven: + case WalletType.xelis: // final keys = bitcoin!.getWalletKeys(_appStore.wallet!); // // items.addAll([ @@ -252,6 +253,8 @@ abstract class WalletKeysViewModelBase with Store { return 'zano-wallet'; case WalletType.decred: return 'decred-wallet'; + case WalletType.xelis: + return 'xelis-wallet'; default: throw Exception('Unexpected wallet type: ${_wallet.type.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 86d1be65f..8568570c3 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -16,6 +16,7 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -47,7 +48,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { String selectedMnemonicLanguage; bool get hasLanguageSelector => - [WalletType.monero, WalletType.haven, WalletType.wownero].contains(type); + [WalletType.monero, WalletType.haven, WalletType.wownero, WalletType.xelis].contains(type); bool get showLanguageSelector => newWalletArguments?.mnemonic == null && hasLanguageSelector; @@ -141,6 +142,11 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { ); case WalletType.decred: return decred!.createDecredNewWalletCredentials(name: name); + case WalletType.xelis: + return xelis!.createXelisNewWalletCredentials( + name: name, + password: walletPassword, + ); case WalletType.none: case WalletType.haven: throw Exception('Unexpected type: ${type.toString()}'); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 6e00ba4cc..841f52c33 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -17,6 +17,7 @@ import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/xelis/xelis.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -33,9 +34,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { Box walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, {required WalletType type, this.restoredWallet}) : hasSeedLanguageSelector = - type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero || + type == WalletType.xelis, hasBlockchainHeightLanguageSelector = - type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero || + type == WalletType.xelis, hasRestoreFromPrivateKey = type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.nano || @@ -66,6 +69,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.zano: + case WalletType.xelis: case WalletType.none: availableModes = [WalletRestoreMode.seed]; break; @@ -182,9 +186,15 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { ); case WalletType.decred: return decred!.createDecredRestoreWalletFromSeedCredentials( - name: name, - mnemonic: seed, - password: password, + name: name, + mnemonic: seed, + password: password, + ); + case WalletType.xelis: + return xelis!.createXelisRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, ); case WalletType.none: case WalletType.haven: diff --git a/lib/view_model/wallet_unlock_loadable_view_model.dart b/lib/view_model/wallet_unlock_loadable_view_model.dart index f5e7bb917..4e4135ff1 100644 --- a/lib/view_model/wallet_unlock_loadable_view_model.dart +++ b/lib/view_model/wallet_unlock_loadable_view_model.dart @@ -40,7 +40,7 @@ abstract class WalletUnlockLoadableViewModelBase extends WalletUnlockViewModel w @action Future unlock() async { try { - state = InitialExecutionState(); + state = IsLoadingState(); final wallet = await _walletLoadingService.load(walletType, walletName, password: password); _appStore.changeCurrentWallet(wallet); success(); diff --git a/lib/xelis/cw_xelis.dart b/lib/xelis/cw_xelis.dart new file mode 100644 index 000000000..799cc8e69 --- /dev/null +++ b/lib/xelis/cw_xelis.dart @@ -0,0 +1,193 @@ +part of 'xelis.dart'; + +class CWXelis extends Xelis { + @override + List getXelisWordList(String language) { + final lang_idx = getLanguageIndexFromStr(input: language); + return getMnemonicWords(languageIndex: lang_idx); + } + + WalletService createXelisWalletService(Box walletInfoSource, bool isDirect) => + XelisWalletService(walletInfoSource, isDirect: isDirect); + + @override + WalletCredentials createXelisNewWalletCredentials({ + required String name, + String? mnemonic, + WalletInfo? walletInfo, + String? password, + }) => + XelisNewWalletCredentials( + name: name, + password: password, + ); + + @override + WalletCredentials createXelisRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + String? passphrase, + }) => + XelisRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + ); + + @override + String getAddress(WalletBase wallet) => (wallet as XelisWallet).walletAddresses.address; + + @override + bool validateAddress(String address) => isAddressValid(strAddress: address); + + @override + bool isTestnet(Object wallet) { + final xelisWallet = wallet as XelisWallet; + return xelisWallet.isTestnet; + } + + @override + List getTransactionPriorities() => XelisTransactionPriority.all; + + @override + TransactionPriority getDefaultTransactionPriority() => XelisTransactionPriority.medium; + + @override + TransactionPriority getXelisTransactionPriorityFast() => XelisTransactionPriority.fast; + + @override + TransactionPriority getXelisTransactionPriorityMedium() => XelisTransactionPriority.medium; + + @override + BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) { + return (transactionInfo as XelisTransactionInfo).xelAmount; + } + + @override + List getXelisAssets(WalletBase wallet) { + final xelisWallet = wallet as XelisWallet; + return xelisWallet.xelAssets; + } + + @override + TransactionPriority deserializeXelisTransactionPriority(int raw) => + XelisTransactionPriority.deserialize(raw: raw); + + @override + Object createXelisTransactionCredentials( + List outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + }) => + XelisTransactionCredentials( + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + memo: out.memo)) + .toList(), + priority: priority as XelisTransactionPriority, + currency: currency, + ); + + @override + Object createXelisTransactionCredentialsRaw( + List outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + }) => + XelisTransactionCredentials( + outputs, + priority: priority as XelisTransactionPriority?, + currency: currency, + ); + + @override + int formatterStringDoubleToAmount(String amount, {required CryptoCurrency currency}) { + if (currency is XelisAsset) return XelisFormatter.parseAmount(amount, currency.decimals); + return XelisFormatter.parseXelisAmount(amount); + } + + @override + double formatterXelisAmountToDouble( + {TransactionInfo? transaction, BigInt? amount, int decimals = 8}) { + assert(transaction != null || amount != null); + + if (transaction != null) { + transaction as XelisTransactionInfo; + return transaction.xelAmount / BigInt.from(10).pow(decimals); + } else { + return amount! / BigInt.from(10).pow(decimals); + } + } + + @override + Future updateAssetState( + WalletBase wallet, + CryptoCurrency asset, + String id, + ) async { + final xelAsset = XelisAsset( + name: asset.name, + symbol: asset.title, + id: id, + decimals: asset.decimals, + enabled: asset.enabled, + iconPath: asset.iconPath, + isPotentialScam: asset.isPotentialScam, + ); + + await (wallet as XelisWallet).updateAssetState(xelAsset); + } + + @override + Future removeAssetTransactionsInHistory(WalletBase wallet, CryptoCurrency token) async => + await (wallet as XelisWallet).removeAssetTransactionsInHistory(token as XelisAsset); + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as XelisTransactionInfo; + if (transaction.assetIds[0] == xelis_sdk.xelisAsset) { + if (wallet.isTestnet) { + return CryptoCurrency.xet; + } + return CryptoCurrency.xel; + } + + wallet as XelisWallet; + + return wallet.xelAssets.firstWhere( + (element) => transaction.assetIds[0] == element.id, + ); + } + + @override + Future deleteAsset(WalletBase wallet, CryptoCurrency asset) async => + await (wallet as XelisWallet).deleteAsset(asset as XelisAsset); + + @override + Future getAsset(WalletBase wallet, String id) async { + final xelisWallet = wallet as XelisWallet; + return await xelisWallet.getAsset(id); + } + + @override + double? getEstimateFees(WalletBase wallet) { + return (wallet as XelisWallet).estimatedFee; + } + + @override + String getAssetId(CryptoCurrency asset) => (asset as XelisAsset).id; + + @override + List getDefaultAssetIDs() { + return []; + } +} diff --git a/model_generator.sh b/model_generator.sh index 7a5e3bfd7..c0545eea5 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,7 +1,7 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,nano,bitcoin_cash,solana,tron,wownero,zano,decred} +for cwcoin in cw_{core,evm,monero,bitcoin,nano,bitcoin_cash,solana,tron,wownero,zano,decred,xelis} do if [[ "x$1" == "xasync" ]]; then diff --git a/pubspec_base.yaml b/pubspec_base.yaml index e11703607..fd3659d67 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -165,9 +165,10 @@ dependency_overrides: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: 60817d4b20144f9da9029f5034790272795b9d38 - web_socket_channel: ^3.0.2 - freezed_annotation: 2.4.4 + ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 + web_socket_channel: ">= 2.4.5" + json_rpc_2: ^4.0.0 + freezed_annotation: ">= 2.0.13" flutter_icons: image_path: "assets/images/app_logo.png" @@ -198,6 +199,7 @@ flutter: - assets/wownero_node_list.yml - assets/zano_node_list.yml - assets/decred_node_list.yml + - assets/xelis_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index f12171f31..aabe19f56 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "ما هو MWEB؟", "live_fee_rates": "أسعار الرسوم المباشرة عبر API", "load_more": "تحميل المزيد", + "loading_wallet": "محفظة تحميل ...", "loading_your_wallet": "يتم تحميل محفظتك", "login": "تسجيل الدخول", "logout": "تسجيل خروج", @@ -496,6 +497,7 @@ "moonpay_alert_text": "يجب أن تكون قيمة المبلغ أكبر من أو تساوي ${minAmount} ${fiatCurrency}", "moralis_nft_error": "حدث خطأ أثناء جلب NFTs. يرجى التحقق من اتصال الإنترنت الخاص بك وحاول مرة أخرى.", "more_options": "المزيد من الخيارات", + "multi_transfer": "رمز متعدد", "multiple_addresses_detected": "عناوين متعددة تم اكتشافها", "mweb_confirmed": "أكد MWEB", "mweb_unconfirmed": "غير مؤكد MWEB", @@ -564,6 +566,7 @@ "overwrite_amount": "تغير المبلغ", "pairingInvalidEvent": "ﺢﻟﺎﺻ ﺮﻴﻏ ﺙﺪﺣ ﻥﺍﺮﻗﺇ", "passphrase": "عبارة الممر (اختياري)", + "passphrase_view_keys": "عبارة المرور", "passphrases_doesnt_match": "لا تتطابق عبارات المرور ، يرجى المحاولة مرة أخرى", "password": "كلمة المرور", "paste": "لصق", @@ -962,6 +965,7 @@ "transaction_details_date": "تاريخ", "transaction_details_fee": "رسوم", "transaction_details_height": "ارتفاع", + "transaction_details_multi_breakdown": "مبالغ نظرة عامة", "transaction_details_recipient_address": "عناوين المستلم", "transaction_details_source_address": "عنوان المصدر", "transaction_details_title": "تفاصيل المعاملة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e1d364b95..e8000dc36 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Какво е MWEB?", "live_fee_rates": "Цени на таксите на живо чрез API", "load_more": "Зареди още", + "loading_wallet": "Зареждане на портфейл ...", "loading_your_wallet": "Зареждане на портфейл", "login": "Влизане", "logout": "Logout", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Сумата трябва да бъде най-малко ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Възникна грешка при извличането на NFT. Моля, проверете вашата интернет връзка и опитайте отново.", "more_options": "Още настройки", + "multi_transfer": "Мулти маркер", "multiple_addresses_detected": "Открити множество адреси", "mweb_confirmed": "Потвърден MWeb", "mweb_unconfirmed": "Непотвърден mweb", @@ -564,6 +566,7 @@ "overwrite_amount": "Промени сума", "pairingInvalidEvent": "Невалидно събитие при сдвояване", "passphrase": "Passphrase (по избор)", + "passphrase_view_keys": "Парола", "passphrases_doesnt_match": "Пасифрази не съвпадат, моля, опитайте отново", "password": "Парола", "paste": "Поставяне", @@ -962,6 +965,7 @@ "transaction_details_date": "Дата", "transaction_details_fee": "Такса", "transaction_details_height": "Height", + "transaction_details_multi_breakdown": "Преглед на сумите", "transaction_details_recipient_address": "Адрес на получател", "transaction_details_source_address": "Адрес на източника", "transaction_details_title": "Подробности на транзакцията", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 0a6693316..49025d9ee 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Co je Mweb?", "live_fee_rates": "Živé sazby poplatků prostřednictvím API", "load_more": "Načíst další", + "loading_wallet": "Načítání peněženky ...", "loading_your_wallet": "Načítám peněženku", "login": "Login", "logout": "Odhlásit", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Částka musí být větší nebo rovna ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Při načítání NFT došlo k chybě. Laskavě zkontrolujte připojení k internetu a zkuste to znovu.", "more_options": "Více možností", + "multi_transfer": "Multi token", "multiple_addresses_detected": "Detekované více adres", "mweb_confirmed": "Potvrzený mweb", "mweb_unconfirmed": "Nepotvrzené mWeb", @@ -564,6 +566,7 @@ "overwrite_amount": "Přepsat částku", "pairingInvalidEvent": "Neplatná událost párování", "passphrase": "Passphrase (volitelné)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passfrázy se neshodují, zkuste to znovu", "password": "Heslo", "paste": "Vložit", @@ -962,6 +965,7 @@ "transaction_details_date": "Datum", "transaction_details_fee": "Poplatek", "transaction_details_height": "Výška", + "transaction_details_multi_breakdown": "Přehled částk", "transaction_details_recipient_address": "Adresa příjemce", "transaction_details_source_address": "Zdrojová adresa", "transaction_details_title": "Podrobnosti o transakci", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 5e9c2209c..51cdf145d 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Was ist MWeb?", "live_fee_rates": "Live-Gebührenpreise über API", "load_more": "Mehr laden", + "loading_wallet": "Brieftasche laden ...", "loading_your_wallet": "Wallet wird geladen", "login": "Einloggen", "logout": "Abmelden", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", "moralis_nft_error": "Ein Fehler trat beim Abrufen von NFTs auf. Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.", "more_options": "Weitere Optionen", + "multi_transfer": "Multi -Token", "multiple_addresses_detected": "Mehrere Adressen erkannt", "mweb_confirmed": "Bestätigt MWeb", "mweb_unconfirmed": "Unbestätigter MWeb", @@ -564,6 +566,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Paarung ungültiges Ereignis", "passphrase": "Passphrase (optional)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrasen stimmen nicht überein, bitte versuchen Sie es erneut", "password": "Passwort", "paste": "Einfügen", @@ -963,6 +966,7 @@ "transaction_details_date": "Datum", "transaction_details_fee": "Gebühr", "transaction_details_height": "Höhe", + "transaction_details_multi_breakdown": "Beträgeübersicht", "transaction_details_recipient_address": "Empfängeradressen", "transaction_details_source_address": "Quelladresse", "transaction_details_title": "Transaktionsdetails", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ad4894238..ca0858379 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -66,6 +66,7 @@ "auth_store_incorrect_password": "Wrong PIN", "authenticated": "Authenticated", "authentication": "Authentication", + "loading_wallet": "Loading Wallet...", "auto_generate_addresses": "Auto generate addresses", "auto_generate_subaddresses": "Auto generate subaddresses", "automatic": "Automatic", @@ -134,7 +135,7 @@ "change": "Change", "change_backup_password_alert": "Your previous backup files will be not available to import with new backup password. New backup password will be used only for new backup files. Are you sure that you want to change backup password?", "change_currency": "Change Currency", - "change_current_node": "Are you sure to change current node to ${node}?", + "change_current_node": "Are you sure you want to change the current node to ${node}?", "change_current_node_title": "Change current node", "change_exchange_provider": "Change Swap Provider", "change_language": "Change language", @@ -957,11 +958,13 @@ "trade_state_underpaid": "Underpaid", "trade_state_unpaid": "Unpaid", "trades": "Trades", + "multi_transfer": "Multi Token", "transaction_cost": "Transaction Cost", "transaction_details_amount": "Amount", "transaction_details_copied": "${title} copied to Clipboard", "transaction_details_date": "Date", "transaction_details_fee": "Fee", + "transaction_details_multi_breakdown": "Amounts Overview", "transaction_details_height": "Height", "transaction_details_recipient_address": "Recipient addresses", "transaction_details_source_address": "Source address", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3f8657ec4..7caa88d97 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "¿Qué es mweb?", "live_fee_rates": "Tasas de tarifas en vivo a través de API", "load_more": "Carga más", + "loading_wallet": "Carga de billetera ...", "loading_your_wallet": "Cargando tu billetera", "login": "Iniciar sesión", "logout": "Cerrar sesión", @@ -496,6 +497,7 @@ "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Se produjo un error al alcanzar las NFT. Por favor, consulte su conexión a Internet y vuelva a intentarlo.", "more_options": "Más Opciones", + "multi_transfer": "Token múltiple", "multiple_addresses_detected": "Múltiples direcciones detectadas", "mweb_confirmed": "Confirmado mweb", "mweb_unconfirmed": "Mweb no confirmado", @@ -564,6 +566,7 @@ "overwrite_amount": "Sobreescribir monto", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Passfrase (opcional)", + "passphrase_view_keys": "Frase", "passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente", "password": "Contraseña", "paste": "Pegar", @@ -963,6 +966,7 @@ "transaction_details_date": "Fecha", "transaction_details_fee": "Cuota", "transaction_details_height": "Altura", + "transaction_details_multi_breakdown": "Visión general de las cantidades", "transaction_details_recipient_address": "Direcciones de destinatarios", "transaction_details_source_address": "Dirección de la fuente", "transaction_details_title": "Detalles de la transacción", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ba3ae4859..e2930e405 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Qu'est-ce que MWEB?", "live_fee_rates": "Taux de frais en direct via l'API", "load_more": "Charger plus", + "loading_wallet": "Portefeuille de chargement ...", "loading_your_wallet": "Chargement de votre portefeuille (wallet)", "login": "Connexion", "logout": "Déconnexion", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Une erreur s'est produite tout en récupérant les NFT. Veuillez vérifier votre connexion Internet et réessayer.", "more_options": "Plus d'options", + "multi_transfer": "Jeton multiple", "multiple_addresses_detected": "Plusieurs adresses détectées", "mweb_confirmed": "Confirmé MWEB", "mweb_unconfirmed": "Mweb non confirmé", @@ -564,6 +566,7 @@ "overwrite_amount": "Remplacer le montant", "pairingInvalidEvent": "Événement de couplage non valide", "passphrase": "Phrase de passe (facultative)", + "passphrase_view_keys": "Phrase secrète", "passphrases_doesnt_match": "Les phrases de passe ne correspondent pas, veuillez réessayer", "password": "Mot de passe", "paste": "Coller", @@ -962,6 +965,7 @@ "transaction_details_date": "Date", "transaction_details_fee": "Frais", "transaction_details_height": "Hauteur", + "transaction_details_multi_breakdown": "Présentation des montants", "transaction_details_recipient_address": "Adresse du bénéficiaire", "transaction_details_source_address": "Adresse source", "transaction_details_title": "Détails de transaction", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index ebd236332..0af8975cc 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Menene Mweb?", "live_fee_rates": "Kudin Kiɗa ta API", "load_more": "Like more", + "loading_wallet": "Loading Wallet ...", "loading_your_wallet": "Ana loda walat ɗin ku", "login": "Shiga", "logout": "Fita", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Darajar adadin dole ne ya zama fiye ko daidai da ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Kuskuren ya faru yayin da ake kawo NFFTs. Da fatan za a duba haɗin intanet ɗinka kuma sake gwadawa.", "more_options": "Ƙarin Zaɓuɓɓuka", + "multi_transfer": "Multi Bashen", "multiple_addresses_detected": "An gano adiresoshin da aka gano", "mweb_confirmed": "Tabbatar da Mweb", "mweb_unconfirmed": "Myconfired", @@ -566,6 +568,7 @@ "overwrite_amount": "Rubuta adadin", "pairingInvalidEvent": "Haɗa Lamarin mara inganci", "passphrase": "Passphrase (Zabi)", + "passphrase_view_keys": "Mashiganya", "passphrases_doesnt_match": "Passphrases bai dace ba, don Allah sake gwadawa", "password": "Kalmar wucewa", "paste": "Manna", @@ -964,6 +967,7 @@ "transaction_details_date": "Kwanan wata", "transaction_details_fee": "Kudin", "transaction_details_height": "tsawo", + "transaction_details_multi_breakdown": "Adadin maimaitawa", "transaction_details_recipient_address": "Adireshin masu amfani", "transaction_details_source_address": "Adireshin Incord", "transaction_details_title": "Bayanai game da aikace-aikacen", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 2e71b6685..bb16b7e5e 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "MWEB क्या है?", "live_fee_rates": "एपीआई के माध्यम से लाइव शुल्क दरें", "load_more": "और लोड करें", + "loading_wallet": "लोडिंग वॉलेट ...", "loading_your_wallet": "अपना बटुआ लोड कर रहा है", "login": "लॉग इन करें", "logout": "लॉगआउट", @@ -496,6 +497,7 @@ "moonpay_alert_text": "राशि का मूल्य अधिक है या करने के लिए बराबर होना चाहिए ${minAmount} ${fiatCurrency}", "moralis_nft_error": "एनएफटी लाने के दौरान एक त्रुटि हुई। कृपया अपने इंटरनेट कनेक्शन की जाँच करें और फिर से प्रयास करें।", "more_options": "और विकल्प", + "multi_transfer": "बहु टोकन", "multiple_addresses_detected": "कई पते का पता चला", "mweb_confirmed": "MWEB की पुष्टि की", "mweb_unconfirmed": "अपुष्ट MWEB", @@ -564,6 +566,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "अमान्य ईवेंट युग्मित करना", "passphrase": "पासफ्रेज़ (वैकल्पिक)", + "passphrase_view_keys": "पदबंध", "passphrases_doesnt_match": "PassPhrases मेल नहीं खाता, कृपया पुनः प्रयास करें", "password": "पारण शब्द", "paste": "पेस्ट करें", @@ -964,6 +967,7 @@ "transaction_details_date": "तारीख", "transaction_details_fee": "शुल्क", "transaction_details_height": "ऊंचाई", + "transaction_details_multi_breakdown": "मात्रा अवलोकन", "transaction_details_recipient_address": "प्राप्तकर्ता के पते", "transaction_details_source_address": "स्रोत पता", "transaction_details_title": "लेनदेन का विवरण", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index b0aa12937..7a445ea7f 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Što je MWEB?", "live_fee_rates": "Stope naknada uživo putem API -ja", "load_more": "Učitaj više", + "loading_wallet": "Utovarivanje novčanika ...", "loading_your_wallet": "Novčanik se učitava", "login": "Prijava", "logout": "Odjava", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Vrijednost iznosa mora biti veća ili jednaka ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Došlo je do pogreške tijekom dohvaćanja NFT -a. Ljubazno provjerite internetsku vezu i pokušajte ponovo.", "more_options": "Više opcija", + "multi_transfer": "Multi token", "multiple_addresses_detected": "Otkrivene više adresa", "mweb_confirmed": "Potvrđen MWeb", "mweb_unconfirmed": "Nepotvrđeni mWeb", @@ -564,6 +566,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Nevažeći događaj uparivanja", "passphrase": "Prolaznica (neobavezno)", + "passphrase_view_keys": "Prolazna fraza", "passphrases_doesnt_match": "Prolazne fraze se ne podudaraju, pokušajte ponovo", "password": "Lozinka", "paste": "Zalijepi", @@ -962,6 +965,7 @@ "transaction_details_date": "Datum", "transaction_details_fee": "Naknada", "transaction_details_height": "Visina", + "transaction_details_multi_breakdown": "Pregled iznosa", "transaction_details_recipient_address": "Adrese primatelja", "transaction_details_source_address": "Adresa izvora", "transaction_details_title": "Detalji transakcije", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index cafabe5a6..39e73f5f9 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Ինչ է Mweb- ը:", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", "load_more": "Բեռնել ավելին", + "loading_wallet": "Դրամապանակ բեռնման ...", "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", "login": "Մուտք", "logout": "Ելք", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}", "moralis_nft_error": "NFT- ները հանելիս սխալ է տեղի ունեցել: Սիրով ստուգեք ձեր ինտերնետային կապը եւ կրկին փորձեք:", "more_options": "Այլ տարբերակներ", + "multi_transfer": "Multi Token", "multiple_addresses_detected": "Հայտնաբերվել են բազմաթիվ հասցեներ", "mweb_confirmed": "Հաստատված MWEB", "mweb_unconfirmed": "Չկարգավորված Mweb", @@ -518,6 +520,7 @@ "no_extra_detail": "Լրացուցիչ մանրամասներ մատչելի չեն", "no_id_needed": "Ոչ մի փաստաթուղթ չի պահանջվում!", "no_id_required": "Ոչ մի փաստաթուղթ չի պահանջվում։ Լրացրեք և ծախսեք ամենուր", + "no_providers_available": "Ոչ մատակարարներ մատչելի չեն", "no_relay_on_domain": "Տիրույթի համար ընդունող չկա կամ անհասանելի է։ Խնդրում ենք ընտրել ընդունող", "no_relays": "Ընդունողներ չկան", "no_relays_message": "Մենք գտել ենք Nostr NIP-05 գրառում այս օգտատիրոջ համար, բայց այն չի պարունակում ոչ մի ընդունող։ Խնդրում ենք հրահանգել ստացողին ավելացնել ընդունողներ իր Nostr գրառման մեջ", @@ -563,6 +566,7 @@ "overwrite_amount": "Գրեք գումարը", "pairingInvalidEvent": "Սխալ միացում", "passphrase": "Պարող արտահայտություն (Ոչ պարտադիր)", + "passphrase_view_keys": "Փոշի", "passphrases_doesnt_match": "Անհատները չեն համընկնում, խնդրում ենք կրկին փորձել", "password": "Գաղտնաբառ", "paste": "Տեղադրել", @@ -753,6 +757,7 @@ "select_hw_account_below": "Խնդրում ենք ընտրել, թե որ հաշիվն է վերականգնել հետեւյալը.", "select_sell_provider_notice": "Ընտրեք վաճառքի մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն վաճառքի մատակարարը հավելվածի կարգավորումներում սահմանելով", "select_your_country": "Խնդրում ենք ընտրել ձեր երկիրը", + "selected_trocador_provider": "Ընտրված Trocador մատակարարը", "sell": "Ծախել", "sell_alert_content": "Մենք ներկայումս պաշտպանում ենք միայն Bitcoin, Ethereum և Litecoin վաճառքը։ Խնդրում ենք ստեղծել կամ միացնել ձեր Bitcoin, Ethereum կամ Litecoin դրամապանակը", "sell_monero_com_alert_content": "Monero-ի վաճառքը դեռ չի պաշտպանվում", @@ -960,6 +965,7 @@ "transaction_details_date": "Ամսաթիվ", "transaction_details_fee": "Տուրք", "transaction_details_height": "Բլոկի համար", + "transaction_details_multi_breakdown": "Գումարների ակնարկ", "transaction_details_recipient_address": "Ստացողի հասցե", "transaction_details_source_address": "Ուղարկողի հասցե", "transaction_details_title": "Գործարքի մանրամասներ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e726ffb3c..d3a2f50f5 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Apa itu MWEB?", "live_fee_rates": "Tarif biaya langsung melalui API", "load_more": "Muat lebih banyak", + "loading_wallet": "Memuat dompet ...", "loading_your_wallet": "Memuat dompet Anda", "login": "Masuk", "logout": "Keluar", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Nilai jumlah harus lebih atau sama dengan ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Terjadi kesalahan saat mengambil NFT. Mohon periksa koneksi internet Anda dan coba lagi.", "more_options": "Opsi Lainnya", + "multi_transfer": "Multi -token", "multiple_addresses_detected": "Banyak alamat terdeteksi", "mweb_confirmed": "Mengkonfirmasi mWeb", "mweb_unconfirmed": "MWEB yang belum dikonfirmasi", @@ -566,6 +568,7 @@ "overwrite_amount": "Timpa jumlah", "pairingInvalidEvent": "Menyandingkan Acara Tidak Valid", "passphrase": "Frasa sandi (opsional)", + "passphrase_view_keys": "Frasa sandi", "passphrases_doesnt_match": "Sandi tidak cocok, coba lagi", "password": "Kata Sandi", "paste": "Tempel", @@ -965,6 +968,7 @@ "transaction_details_date": "Tanggal", "transaction_details_fee": "Biaya", "transaction_details_height": "Tinggi", + "transaction_details_multi_breakdown": "Jumlah Ikhtisar", "transaction_details_recipient_address": "Alamat Penerima", "transaction_details_source_address": "Alamat sumber", "transaction_details_title": "Rincian Transaksi", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4e94c349f..3e01c8000 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Cos'è MWeb?", "live_fee_rates": "Tariffe delle commissioni in tempo reale tramite API", "load_more": "Carica di più", + "loading_wallet": "Portafoglio di caricamento ...", "loading_your_wallet": "Caricamento portafoglio", "login": "Accedi", "logout": "Logout", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Si è verificato un errore durante il recupero di NFT. Si prega di controllare la tua connessione Internet e riprovare.", "more_options": "Altre opzioni", + "multi_transfer": "Multi token", "multiple_addresses_detected": "Più indirizzi rilevati", "mweb_confirmed": "MWeb confermato", "mweb_unconfirmed": "MWeb non confermato", @@ -565,6 +567,7 @@ "overwrite_amount": "Sovrascrivi quantità", "pairingInvalidEvent": "Associazione evento non valido", "passphrase": "Passphrase (opzionale)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Le passphrase non corrispondono, riprova", "password": "Password", "paste": "Incolla", @@ -963,6 +966,7 @@ "transaction_details_date": "Data", "transaction_details_fee": "Commissione", "transaction_details_height": "Altezza", + "transaction_details_multi_breakdown": "Panoramica degli importi", "transaction_details_recipient_address": "Indirizzi dei destinatari", "transaction_details_source_address": "Indirizzo mittente", "transaction_details_title": "Dettagli Transazione", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 960634e27..9e9af3c8f 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -464,6 +464,7 @@ "litecoin_what_is_mweb": "MWEBとは何ですか?", "live_fee_rates": "API経由のライブ料金", "load_more": "もっと読み込む", + "loading_wallet": "ウォレットのロード...", "loading_your_wallet": "ウォレットをロードしています", "login": "ログイン", "logout": "ログアウト", @@ -497,6 +498,7 @@ "moonpay_alert_text": "金額の値は以上でなければなりません ${minAmount} ${fiatCurrency}", "moralis_nft_error": "NFTの取得中にエラーが発生しました。インターネット接続を確認して、もう一度やり直してください。", "more_options": "その他のオプション", + "multi_transfer": "マルチトークン", "multiple_addresses_detected": "複数のアドレスが検出されました", "mweb_confirmed": "確認されたMWEB", "mweb_unconfirmed": "未確認のMWEB", @@ -565,6 +567,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "ペアリング無効イベント", "passphrase": "パスフレーズ(オプション)", + "passphrase_view_keys": "パスフレーズ", "passphrases_doesnt_match": "パスフレーズは一致しません。もう一度やり直してください", "password": "パスワード", "paste": "ペースト", @@ -963,6 +966,7 @@ "transaction_details_date": "日付", "transaction_details_fee": "費用", "transaction_details_height": "高さ", + "transaction_details_multi_breakdown": "金額の概要", "transaction_details_recipient_address": "受信者のアドレス", "transaction_details_source_address": "ソースアドレス", "transaction_details_title": "取引の詳細", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index e107e1a29..56e51db30 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -496,6 +496,7 @@ "moonpay_alert_text": "금액 값은 ${minAmount} ${fiatCurrency} 이상이어야 합니다", "moralis_nft_error": "NFT를 가져오는 중 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도하십시오.", "more_options": "추가 옵션", + "multi_transfer": "멀티 토큰", "multiple_addresses_detected": "여러 주소 감지됨", "mweb_confirmed": "확정된 MWEB", "mweb_unconfirmed": "미확정 MWEB", @@ -963,6 +964,7 @@ "transaction_details_date": "날짜", "transaction_details_fee": "수수료", "transaction_details_height": "높이", + "transaction_details_multi_breakdown": "금액 개요", "transaction_details_recipient_address": "수신자 주소", "transaction_details_source_address": "송신자 주소", "transaction_details_title": "트랜잭션 세부 정보", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d9f3c46da..56ddf9c91 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "MweB ဆိုတာဘာလဲ။", "live_fee_rates": "API မှတစ်ဆင့် Live အခကြေးငွေနှုန်းထားများ", "load_more": "ပိုပြီး load", + "loading_wallet": "ပိုက်ဆံအိတ်တင်နေသည် ...", "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", "login": "လော့ဂ်အင်", "logout": "ထွက်လိုက်ပါ။", @@ -496,6 +497,7 @@ "moonpay_alert_text": "ပမာဏ၏တန်ဖိုးသည် ${minAmount} ${fiatCurrency} နှင့် ပိုနေရမည်", "moralis_nft_error": "nfts ရယူနေစဉ်အမှားတစ်ခုဖြစ်ပွားခဲ့သည်။ သင်၏အင်တာနက်ဆက်သွယ်မှုကိုကြင်နာစွာစစ်ဆေးပြီးထပ်ကြိုးစားပါ။", "more_options": "နောက်ထပ် ရွေးချယ်စရာများ", + "multi_transfer": "Multi တိုကင်", "multiple_addresses_detected": "အများအပြားလိပ်စာများရှာဖွေတွေ့ရှိ", "mweb_confirmed": "အတည်ပြုလိုက် mweb", "mweb_unconfirmed": "အတည်မပြုနိုင်သော mweb", @@ -564,6 +566,7 @@ "overwrite_amount": "ပမာဏကို ထပ်ရေးပါ။", "pairingInvalidEvent": "မမှန်ကန်သောဖြစ်ရပ်ကို တွဲချိတ်ခြင်း။", "passphrase": "passphrase (optional)", + "passphrase_view_keys": "စကားဝှက်PPRase", "passphrases_doesnt_match": "passphrases မကိုက်ညီဘူး, ကျေးဇူးပြုပြီးထပ်ကြိုးစားပါ", "password": "စကားဝှက်", "paste": "ငါးပိ", @@ -962,6 +965,7 @@ "transaction_details_date": "ရက်စွဲ", "transaction_details_fee": "ကြေး", "transaction_details_height": "အရပ်အမြင့်", + "transaction_details_multi_breakdown": "ပမာဏခြုံငုံသုံးသပ်ချက်ပမာဏ", "transaction_details_recipient_address": "လက်ခံသူလိပ်စာများ", "transaction_details_source_address": "အရင်းအမြစ်လိပ်စာ", "transaction_details_title": "ငွေပေးငွေယူအသေးစိတ်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 756f0b311..db3e6b8fe 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Wat is Mweb?", "live_fee_rates": "Live -tarieven via API", "load_more": "Meer laden", + "loading_wallet": "Wallet laden ...", "loading_your_wallet": "Uw portemonnee laden", "login": "Log in", "logout": "Uitloggen", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Waarde van het bedrag moet meer of gelijk zijn aan ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Er is een fout opgetreden bij het ophalen van NFT's. Controleer uw internetverbinding vriendelijk en probeer het opnieuw.", "more_options": "Meer opties", + "multi_transfer": "Multi -token", "multiple_addresses_detected": "Meerdere adressen gedetecteerd", "mweb_confirmed": "Bevestigde MWEB", "mweb_unconfirmed": "Onbevestigde MWEB", @@ -564,6 +566,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Koppelen Ongeldige gebeurtenis", "passphrase": "PassaspHRASE (optioneel)", + "passphrase_view_keys": "Wachtwoordzin", "passphrases_doesnt_match": "Passaspelfiaal komt niet overeen, probeer het opnieuw", "password": "Wachtwoord", "paste": "Plakken", @@ -962,6 +965,7 @@ "transaction_details_date": "Datum", "transaction_details_fee": "Vergoeding", "transaction_details_height": "Hoogte", + "transaction_details_multi_breakdown": "Bedragen overzicht", "transaction_details_recipient_address": "Adressen van ontvangers", "transaction_details_source_address": "Bron adres", "transaction_details_title": "Transactie details", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 78a1af410..c89a1fcd9 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Co to jest MWEB?", "live_fee_rates": "Stawki opłaty na żywo za pośrednictwem API", "load_more": "Załaduj więcej", + "loading_wallet": "Portfel ładujący ...", "loading_your_wallet": "Ładowanie portfela", "login": "Login", "logout": "Wyloguj", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Wartość kwoty musi być większa lub równa ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Wystąpił błąd podczas pobierania NFT. Uprzejmie sprawdź swoje połączenie internetowe i spróbuj ponownie.", "more_options": "Więcej opcji", + "multi_transfer": "Multi Token", "multiple_addresses_detected": "Wykryto wiele adresów", "mweb_confirmed": "Potwierdzone MWEB", "mweb_unconfirmed": "Niepotwierdzone MWEB", @@ -564,6 +566,7 @@ "overwrite_amount": "Nadpisz ilość", "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", "passphrase": "Hasło frazy seed (opcjonalnie)", + "passphrase_view_keys": "Fraza", "passphrases_doesnt_match": "Hasła frazy seed nie pasują, spróbuj ponownie", "password": "Hasło", "paste": "Wklej", @@ -962,6 +965,7 @@ "transaction_details_date": "Data", "transaction_details_fee": "Opłata", "transaction_details_height": "Wysokość Bloku", + "transaction_details_multi_breakdown": "Przegląd kwot", "transaction_details_recipient_address": "Adres odbiorcy", "transaction_details_source_address": "Adres źródłowy", "transaction_details_title": "Szczegóły transakcji", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 64c54916a..35e2041b7 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "O que é MWeb?", "live_fee_rates": "Taxas de taxas ao vivo via API", "load_more": "Carregue mais", + "loading_wallet": "Carteira de carregamento ...", "loading_your_wallet": "Abrindo sua carteira", "login": "Login", "logout": "Logout", @@ -497,6 +498,7 @@ "moonpay_alert_text": "O valor do montante deve ser maior ou igual a ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Ocorreu um erro ao buscar NFTs. Por favor, verifique sua conexão com a Internet e tente novamente.", "more_options": "Mais opções", + "multi_transfer": "Multi token", "multiple_addresses_detected": "Vários endereços detectados", "mweb_confirmed": "MWEB confirmado", "mweb_unconfirmed": "MWEB não confirmado", @@ -566,6 +568,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Emparelhamento de evento inválido", "passphrase": "Senha (opcional)", + "passphrase_view_keys": "Senha", "passphrases_doesnt_match": "Passagases não correspondem, por favor tente novamente", "password": "Senha", "paste": "Colar", @@ -964,6 +967,7 @@ "transaction_details_date": "Data", "transaction_details_fee": "Taxa", "transaction_details_height": "Altura", + "transaction_details_multi_breakdown": "Visão geral dos valores", "transaction_details_recipient_address": "Endereços de destinatários", "transaction_details_source_address": "Endereço de Origem", "transaction_details_title": "Detalhes da transação", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 7f0ce362a..8ddb3b5b7 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Что такое MWEB?", "live_fee_rates": "Ставки по сбору вживую через API", "load_more": "Загрузи больше", + "loading_wallet": "Погрузочный кошелек ...", "loading_your_wallet": "Загрузка кошелька", "login": "Логин", "logout": "Выйти", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Сумма должна быть больше или равна ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Произошла ошибка при получении NFT. Пожалуйста, проверьте подключение к Интернету и попробуйте еще раз.", "more_options": "Дополнительные параметры", + "multi_transfer": "Multi Token", "multiple_addresses_detected": "Обнаружено несколько адресов", "mweb_confirmed": "Подтверждено MWEB", "mweb_unconfirmed": "Неподтвержденная MWEB", @@ -565,6 +567,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недействительное событие сопряжения", "passphrase": "Passfrase (необязательно)", + "passphrase_view_keys": "Пасфраза", "passphrases_doesnt_match": "Пасфразы не совпадают, попробуйте еще раз", "password": "Пароль", "paste": "Вставить", @@ -963,6 +966,7 @@ "transaction_details_date": "Дата", "transaction_details_fee": "Комиссия", "transaction_details_height": "Высота", + "transaction_details_multi_breakdown": "Обзор сумм", "transaction_details_recipient_address": "Адреса получателей", "transaction_details_source_address": "Адрес источника", "transaction_details_title": "Детали транзакции", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 9908f93a0..fb840f1a9 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "MWEB คืออะไร?", "live_fee_rates": "อัตราค่าธรรมเนียมสดผ่าน API", "load_more": "โหลดมากขึ้น", + "loading_wallet": "โหลดกระเป๋าเงิน ...", "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ", "login": "เข้าสู่ระบบ", "logout": "ออกจากระบบ", @@ -496,6 +497,7 @@ "moonpay_alert_text": "มูลค่าของจำนวนต้องมากกว่าหรือเท่ากับ ${minAmount} ${fiatCurrency}", "moralis_nft_error": "เกิดข้อผิดพลาดในขณะที่ดึง NFTs กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณและลองอีกครั้ง", "more_options": "ตัวเลือกเพิ่มเติม", + "multi_transfer": "โทเค็น", "multiple_addresses_detected": "ตรวจพบหลายที่อยู่", "mweb_confirmed": "MWEB ยืนยันแล้ว", "mweb_unconfirmed": "mweb ที่ไม่ได้รับการยืนยัน", @@ -564,6 +566,7 @@ "overwrite_amount": "เขียนทับจำนวน", "pairingInvalidEvent": "การจับคู่เหตุการณ์ที่ไม่ถูกต้อง", "passphrase": "ข้อความรหัสผ่าน (ไม่บังคับ)", + "passphrase_view_keys": "วรรณะ", "passphrases_doesnt_match": "Passphrases ไม่ตรงกันโปรดลองอีกครั้ง", "password": "รหัสผ่าน", "paste": "วาง", @@ -962,6 +965,7 @@ "transaction_details_date": "วันที่", "transaction_details_fee": "ค่าธรรมเนียม", "transaction_details_height": "ความสูง", + "transaction_details_multi_breakdown": "ภาพรวมจำนวนเงิน", "transaction_details_recipient_address": "ที่อยู่ผู้รับ", "transaction_details_source_address": "ที่อยู่แหล่งกำเนิด", "transaction_details_title": "รายละเอียดการทำรายการ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 9ad025aa7..5833c01d2 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Ano ang MWEB?", "live_fee_rates": "Mga rate ng live na bayad sa pamamagitan ng API", "load_more": "Mag-load pa", + "loading_wallet": "Naglo -load ng Wallet ...", "loading_your_wallet": "Naglo-load ng iyong wallet", "login": "Mag-login", "logout": "Mag-logout", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", "moralis_nft_error": "May naganap na error habang kinukuha ang mga NFT. Mabuting suriin ang iyong koneksyon sa internet at subukang muli.", "more_options": "Higit pang mga Pagpipilian", + "multi_transfer": "Maraming token", "multiple_addresses_detected": "Maramihang mga address na napansin", "mweb_confirmed": "Nakumpirma na MWeb", "mweb_unconfirmed": "Hindi nakumpirma si Mweb", @@ -564,6 +566,7 @@ "overwrite_amount": "I-overwrite ang halaga", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (opsyonal)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Ang mga passphrases ay hindi tumutugma, mangyaring subukang muli", "password": "Password", "paste": "I-paste", @@ -962,6 +965,7 @@ "transaction_details_date": "Petsa", "transaction_details_fee": "Fee", "transaction_details_height": "Height", + "transaction_details_multi_breakdown": "Pangkalahatang -ideya ng Halaga", "transaction_details_recipient_address": "Mga address ng tatanggap", "transaction_details_source_address": "Address ng pinagmulan", "transaction_details_title": "Mga detalye ng transaksyon", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index add8ee22f..f690a4046 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "MWEB nedir?", "live_fee_rates": "API üzerinden canlı ücret oranları", "load_more": "Daha fazla yükle", + "loading_wallet": "Yükleme cüzdanı ...", "loading_your_wallet": "Cüzdanın yükleniyor", "login": "Login", "logout": "Çıkış yap", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Tutar ${minAmount} ${fiatCurrency} miktarına eşit veya daha fazla olmalıdır", "moralis_nft_error": "NFT'ler getirilirken bir hata oluştu. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin.", "more_options": "Daha Fazla Seçenek", + "multi_transfer": "Çoklu jeton", "multiple_addresses_detected": "Birden çok adres tespit edildi", "mweb_confirmed": "Onaylanmış mweb", "mweb_unconfirmed": "Doğrulanmamış mweb", @@ -564,6 +566,7 @@ "overwrite_amount": "Miktarın üzerine yaz", "pairingInvalidEvent": "Geçersiz Etkinliği Eşleştirme", "passphrase": "Passfrase (isteğe bağlı)", + "passphrase_view_keys": "Parola", "passphrases_doesnt_match": "Passfrases eşleşmiyor, lütfen tekrar deneyin", "password": "Parola", "paste": "Yapıştır", @@ -962,6 +965,7 @@ "transaction_details_date": "Tarih", "transaction_details_fee": "Fee", "transaction_details_height": "Yükseklik", + "transaction_details_multi_breakdown": "Miktarlara Genel Bakış", "transaction_details_recipient_address": "Alıcı adres", "transaction_details_source_address": "Kaynak adresi", "transaction_details_title": "İşlem Detayları", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 07ed5d532..370eeb91a 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "Що таке mweb?", "live_fee_rates": "Ставки плати за живий через API", "load_more": "Завантажити ще", + "loading_wallet": "Завантаження гаманця ...", "loading_your_wallet": "Завантаження гаманця", "login": "Логін", "logout": "Вийти", @@ -496,6 +497,7 @@ "moonpay_alert_text": "Значення суми має бути більшим або дорівнювати ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Під час отримання NFTS сталася помилка. Будь ласка, перевірте підключення до Інтернету та повторіть спробу.", "more_options": "Більше параметрів", + "multi_transfer": "Мультикен", "multiple_addresses_detected": "Виявлено кілька адрес", "mweb_confirmed": "Підтвердив Mweb", "mweb_unconfirmed": "Неперевірений MWEB", @@ -564,6 +566,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недійсна подія сполучення", "passphrase": "Пасофрази (необов’язково)", + "passphrase_view_keys": "Пропуск", "passphrases_doesnt_match": "Пасофрази не відповідають, спробуйте ще раз", "password": "Пароль", "paste": "Вставити", @@ -963,6 +966,7 @@ "transaction_details_date": "Дата", "transaction_details_fee": "Комісія", "transaction_details_height": "Висота", + "transaction_details_multi_breakdown": "Суми Огляд", "transaction_details_recipient_address": "Адреси одержувачів", "transaction_details_source_address": "Адреса джерела", "transaction_details_title": "Деталі транзакції", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 7240db6b0..c5a32a24a 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "MWEB کیا ہے؟", "live_fee_rates": "API کے ذریعے براہ راست فیس کی شرح", "load_more": "مزید لوڈ کریں", + "loading_wallet": "بٹوے لوڈ ہو رہا ہے ...", "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔", "login": "لاگ ان کریں", "logout": "لاگ آوٹ", @@ -496,6 +497,7 @@ "moonpay_alert_text": "رقم کی قدر ${minAmount} ${fiatCurrency} کے برابر یا زیادہ ہونی چاہیے۔", "moralis_nft_error": "این ایف ٹی ایس لانے کے دوران ایک خرابی پیش آگئی۔ برائے مہربانی اپنا انٹرنیٹ کنیکشن چیک کریں اور دوبارہ کوشش کریں۔", "more_options": "مزید زرائے", + "multi_transfer": "ملٹی ٹوکن", "multiple_addresses_detected": "متعدد پتے کا پتہ چلا", "mweb_confirmed": "تصدیق شدہ MWEB", "mweb_unconfirmed": "غیر مصدقہ MWEB", @@ -566,6 +568,7 @@ "overwrite_amount": "رقم کو اوور رائٹ کریں۔", "pairingInvalidEvent": "ﭧﻧﻮﯾﺍ ﻂﻠﻏ ﺎﻧﺎﻨﺑ ﺍﮌﻮﺟ", "passphrase": "پاسفریز (اختیاری)", + "passphrase_view_keys": "پاسفریز", "passphrases_doesnt_match": "پاسفریز مماثل نہیں ہیں ، براہ کرم دوبارہ کوشش کریں", "password": "پاس ورڈ", "paste": "چسپاں کریں۔", @@ -964,6 +967,7 @@ "transaction_details_date": "تاریخ", "transaction_details_fee": "فیس", "transaction_details_height": "اونچائی", + "transaction_details_multi_breakdown": "جائزہ جائزہ", "transaction_details_recipient_address": "وصول کنندگان کے پتے", "transaction_details_source_address": "ماخذ ایڈریس", "transaction_details_title": "لین دین کی تفصیلات", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 3dd184ddd..aa4619340 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -147,6 +147,8 @@ "change_selected_pair": "Thay đổi cặp đã chọn", "change_wallet_alert_content": "Bạn có muốn thay đổi ví hiện tại thành ${wallet_name} không?", "change_wallet_alert_title": "Thay đổi ví hiện tại", + "choose_a_payment_method": "Chọn một phương thức thanh toán", + "choose_a_provider": "Chọn một nhà cung cấp", "choose_account": "Chọn tài khoản", "choose_address": "\n\nVui lòng chọn địa chỉ:", "choose_card_value": "Chọn giá trị thẻ", @@ -462,6 +464,7 @@ "litecoin_what_is_mweb": "MWEB là gì?", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", "load_more": "Tải thêm", + "loading_wallet": "Tải ví ...", "loading_your_wallet": "Đang tải ví của bạn", "login": "Đăng nhập", "logout": "Đăng xuất", @@ -495,6 +498,7 @@ "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Một lỗi đã xảy ra trong khi tìm nạp NFT. Vui lòng kiểm tra kết nối Internet của bạn và thử lại.", "more_options": "Thêm tùy chọn", + "multi_transfer": "Nhiều mã thông báo", "multiple_addresses_detected": "Nhiều địa chỉ được phát hiện", "mweb_confirmed": "Xác nhận MWEB", "mweb_unconfirmed": "MWEB chưa được xác nhận", @@ -517,6 +521,7 @@ "no_extra_detail": "Không có thêm chi tiết có sẵn", "no_id_needed": "Không cần ID!", "no_id_required": "Không yêu cầu ID. Nạp tiền và chi tiêu ở bất kỳ đâu", + "no_providers_available": "Không có nhà cung cấp có sẵn", "no_relay_on_domain": "Không có relay cho miền của người dùng hoặc relay không khả dụng. Vui lòng chọn một relay để sử dụng.", "no_relays": "Không có relay", "no_relays_message": "Chúng tôi đã tìm thấy một bản ghi Nostr NIP-05 cho người dùng này, nhưng nó không chứa bất kỳ relay nào. Vui lòng hướng dẫn người nhận thêm relay vào bản ghi Nostr của họ.", @@ -562,6 +567,7 @@ "overwrite_amount": "Ghi đè số tiền", "pairingInvalidEvent": "Sự kiện ghép nối không hợp lệ", "passphrase": "Cụm từ bảo mật (Tùy chọn)", + "passphrase_view_keys": "Cụm cụm", "passphrases_doesnt_match": "Vòng thông không khớp, vui lòng thử lại", "password": "Mật khẩu", "paste": "Dán", @@ -752,6 +758,7 @@ "select_hw_account_below": "Vui lòng chọn tài khoản nào để khôi phục bên dưới:", "select_sell_provider_notice": "Chọn nhà cung cấp bán ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp bán mặc định trong cài đặt ứng dụng.", "select_your_country": "Vui lòng chọn quốc gia của bạn", + "selected_trocador_provider": "Nhà cung cấp Trocador được chọn", "sell": "Bán", "sell_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ bán Bitcoin, Ethereum và Litecoin. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum hoặc Litecoin của bạn.", "sell_monero_com_alert_content": "Bán Monero chưa được hỗ trợ", @@ -959,6 +966,7 @@ "transaction_details_date": "Ngày", "transaction_details_fee": "Phí", "transaction_details_height": "Chiều cao", + "transaction_details_multi_breakdown": "Tổng quan về số tiền", "transaction_details_recipient_address": "Địa chỉ người nhận", "transaction_details_source_address": "Địa chỉ nguồn", "transaction_details_title": "Chi tiết giao dịch", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 0a3e382e7..b895656ea 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -464,6 +464,7 @@ "litecoin_what_is_mweb": "Kini mweb?", "live_fee_rates": "Awọn oṣuwọn Owo laaye laaye nipasẹ API", "load_more": "Ẹru diẹ sii", + "loading_wallet": "Loading apamọwọ ...", "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín", "login": "Orúkọ", "logout": "Jáde", @@ -497,6 +498,7 @@ "moonpay_alert_text": "Iye owó kò gbọ́dọ̀ kéré ju ${minAmount} ${fiatCurrency}", "moralis_nft_error": "Aṣiṣe kan waye lakoko ti nfts. Fi inurere ṣayẹwo asopọ intanẹẹti rẹ ki o tun gbiyanju lẹẹkan si.", "more_options": "Ìyàn àfikún", + "multi_transfer": "Ologo nla", "multiple_addresses_detected": "Awọn adirẹsi ọpọ rii", "mweb_confirmed": "Jẹrisi Mweb", "mweb_unconfirmed": "Ajopo Mweb", @@ -565,6 +567,7 @@ "overwrite_amount": "Pààrọ̀ iye owó", "pairingInvalidEvent": "Pipọpọ Iṣẹlẹ Ti ko tọ", "passphrase": "Ọrọ kukuru (iyan)", + "passphrase_view_keys": "Kukurukọni", "passphrases_doesnt_match": "Awọn ọrọ kukuru ko baamu, jọwọ gbiyanju lẹẹkansi", "password": "Ọ̀rọ̀ aṣínà", "paste": "Fikún ẹ̀dà yín", @@ -963,6 +966,7 @@ "transaction_details_date": "Ìgbà", "transaction_details_fee": "Iye àfikún", "transaction_details_height": "Gíga", + "transaction_details_multi_breakdown": "Iye imọwe", "transaction_details_recipient_address": "Àwọn àdírẹ́sì olùgbà", "transaction_details_source_address": "Adirẹsi orisun", "transaction_details_title": "Àránṣẹ́ ìsọfúnni", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 2f02673b7..b05bd1193 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -463,6 +463,7 @@ "litecoin_what_is_mweb": "什么是MWEB?", "live_fee_rates": "通过API的实时费率", "load_more": "装载更多", + "loading_wallet": "加载钱包...", "loading_your_wallet": "加载您的钱包", "login": "登录", "logout": "注销", @@ -496,6 +497,7 @@ "moonpay_alert_text": "金额的价值必须大于或等于 ${minAmount} ${fiatCurrency}", "moralis_nft_error": "获取NFT时发生错误。请检查您的互联网连接,然后重试。", "more_options": "更多选项", + "multi_transfer": "多代币", "multiple_addresses_detected": "检测到的多个地址", "mweb_confirmed": "确认的MWEB", "mweb_unconfirmed": "未经证实的MWEB", @@ -564,6 +566,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "配对无效事件", "passphrase": "密码(可选)", + "passphrase_view_keys": "密码", "passphrases_doesnt_match": "密码不匹配,请重试", "password": "密码", "paste": "粘贴", @@ -962,6 +965,7 @@ "transaction_details_date": "日期", "transaction_details_fee": "手续费", "transaction_details_height": "区块高度", + "transaction_details_multi_breakdown": "金额概述", "transaction_details_recipient_address": "收件人地址", "transaction_details_source_address": "源地址", "transaction_details_title": "交易明细", diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index b980f877d..a0d043ff3 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred --xelis" ;; esac diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index b642d67e4..f9a6b7417 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -31,7 +31,7 @@ case $APP_IOS_TYPE in ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --xelis" ;; esac diff --git a/scripts/linux/app_config.sh b/scripts/linux/app_config.sh index 5d9d8597b..1f1db9fe0 100755 --- a/scripts/linux/app_config.sh +++ b/scripts/linux/app_config.sh @@ -13,7 +13,7 @@ CONFIG_ARGS="" case $APP_LINUX_TYPE in $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --excludeFlutterSecureStorage";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --xelis --excludeFlutterSecureStorage";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 641a7b46b..399101934 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --xelis";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/tool/configure.dart b/tool/configure.dart index 95c8a4d5a..98a2bcbf3 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -11,6 +11,8 @@ const tronOutputPath = 'lib/tron/tron.dart'; const wowneroOutputPath = 'lib/wownero/wownero.dart'; const zanoOutputPath = 'lib/zano/zano.dart'; const decredOutputPath = 'lib/decred/decred.dart'; +const xelisOutputPath = 'lib/xelis/xelis.dart'; +const frbInitPath = 'lib/frb_init.g.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -30,6 +32,7 @@ Future main(List args) async { final hasWownero = args.contains('${prefix}wownero'); final hasZano = args.contains('${prefix}zano'); final hasDecred = args.contains('${prefix}decred'); + final hasXelis = args.contains('${prefix}xelis'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -44,6 +47,7 @@ Future main(List args) async { await generateZano(hasZano); // await generateBanano(hasEthereum); await generateDecred(hasDecred); + await generateXelis(hasXelis); await generatePubspec( hasMonero: hasMonero, @@ -59,6 +63,7 @@ Future main(List args) async { hasWownero: hasWownero, hasZano: hasZano, hasDecred: hasDecred, + hasXelis: hasXelis, ); await generateWalletTypes( hasMonero: hasMonero, @@ -73,6 +78,10 @@ Future main(List args) async { hasWownero: hasWownero, hasZano: hasZano, hasDecred: hasDecred, + hasXelis: hasXelis, + ); + await generateFRBInit( + hasXelis: hasXelis, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -1406,6 +1415,103 @@ abstract class Decred { await outputFile.writeAsString(output); } +Future generateXelis(bool hasImplementation) async { + final outputFile = File(xelisOutputPath); + const xelisCommonHeaders = """ +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:hive/hive.dart'; +"""; + const xelisCWHeaders = """ +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; + +import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; +import 'package:cw_xelis/src/api/utils.dart'; +import 'package:cw_xelis/xelis_asset.dart'; +import 'package:cw_xelis/xelis_wallet_service.dart'; +import 'package:cw_xelis/xelis_wallet.dart'; +import 'package:cw_xelis/xelis_wallet_creation_credentials.dart'; +import 'package:cw_xelis/xelis_transaction_info.dart'; +import 'package:cw_xelis/xelis_transaction_history.dart'; +import 'package:cw_xelis/xelis_formatting.dart'; +import 'package:cw_xelis/xelis_transaction_credentials.dart'; +import 'package:cw_xelis/xelis_transaction_priority.dart'; +"""; + const xelisCwPart = "part 'cw_xelis.dart';"; + const xelisContent = """ + +abstract class Xelis { + List getXelisWordList(String language); + WalletCredentials createXelisNewWalletCredentials( + {required String name, WalletInfo? walletInfo, String? password}); + WalletCredentials createXelisRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}); + + WalletService createXelisWalletService(Box walletInfoSource, bool isDirect); + + String getAddress(WalletBase wallet); + bool validateAddress(String address); + bool isTestnet(Object wallet); + + BigInt getTransactionAmountRaw(TransactionInfo transactionInfo); + + List getTransactionPriorities(); + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority getXelisTransactionPriorityFast(); + TransactionPriority getXelisTransactionPriorityMedium(); + TransactionPriority deserializeXelisTransactionPriority(int raw); + + Object createXelisTransactionCredentials(List outputs, {required TransactionPriority priority, required CryptoCurrency currency}); + Future removeAssetTransactionsInHistory(WalletBase wallet, CryptoCurrency token); + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + + double formatterXelisAmountToDouble({TransactionInfo? transaction, BigInt? amount, int decimals = 8}); + int formatterStringDoubleToAmount(String amount, {required CryptoCurrency currency}); + + // CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + double? getEstimateFees(WalletBase wallet); + + List getXelisAssets(WalletBase wallet); + Future updateAssetState( + WalletBase wallet, + CryptoCurrency asset, + String id, + ); + Future deleteAsset(WalletBase wallet, CryptoCurrency asset); + Future getAsset(WalletBase wallet, String id); + String getAssetId(CryptoCurrency asset); + List getDefaultAssetIDs(); +} +"""; + + const xelisEmptyDefinition = 'Xelis? xelis;\n'; + const xelisCWDefinition = 'Xelis? xelis = CWXelis();\n'; + + final output = '$xelisCommonHeaders\n' + + (hasImplementation ? '$xelisCWHeaders\n' : '\n') + + (hasImplementation ? '$xelisCwPart\n\n' : '\n') + + (hasImplementation ? xelisCWDefinition : xelisEmptyDefinition) + + '\n' + + xelisContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1420,6 +1526,7 @@ Future generatePubspec({ required bool hasWownero, required bool hasZano, required bool hasDecred, + required bool hasXelis, }) async { const cwCore = """ cw_core: @@ -1484,6 +1591,10 @@ Future generatePubspec({ cw_decred: path: ./cw_decred """; + const cwXelis = """ + cw_xelis: + path: ./cw_xelis + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1533,6 +1644,10 @@ Future generatePubspec({ output += '\n$cwDecred'; } + if (hasXelis) { + output += '\n$cwXelis'; + } + if (hasFlutterSecureStorage) { output += '\n$flutterSecureStorage\n'; } @@ -1561,6 +1676,32 @@ Future generatePubspec({ await outputFile.writeAsString(outputContent); } +Future generateFRBInit({ + required bool hasXelis, +}) async { + final frbInitFile = File(frbInitPath); + + if (frbInitFile.existsSync()) { + await frbInitFile.delete(); + } + + const outputDefinition = 'Future frb_init() async {'; + var outputHeader = ""; + var inits = ""; + var outputContent = outputHeader + '\n\n' + outputDefinition + '\n'; + + if (hasXelis) { + outputHeader += "import 'package:cw_xelis/src/frb_generated.dart' as xelis_rust;\n"; + inits += '\tawait xelis_rust.RustLib.init();\n'; + } + + outputContent = outputHeader + '\n\n' + + outputDefinition + '\n' + + inits + + '}'; + await frbInitFile.writeAsString(outputContent); +} + Future generateWalletTypes({ required bool hasMonero, required bool hasBitcoin, @@ -1574,6 +1715,7 @@ Future generateWalletTypes({ required bool hasWownero, required bool hasZano, required bool hasDecred, + required bool hasXelis, }) async { final walletTypesFile = File(walletTypesPath); @@ -1637,6 +1779,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.wownero,\n'; } + if (hasXelis) { + outputContent += '\tWalletType.xelis,\n'; + } + outputContent += '];\n'; await walletTypesFile.writeAsString(outputContent); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index a8ebcc8cc..527c325a9 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -58,6 +58,7 @@ class SecretKey { SecretKey('tronTestWalletSeeds', () => ''), SecretKey('nanoTestWalletSeeds', () => ''), SecretKey('wowneroTestWalletSeeds', () => ''), + SecretKey('xelisTestWalletSeeds', () => ''), SecretKey('moneroTestWalletReceiveAddress', () => ''), SecretKey('bitcoinTestWalletReceiveAddress', () => ''), SecretKey('ethereumTestWalletReceiveAddress', () => ''),