Merge pull request from cypherstack/xelis

Xelis
This commit is contained in:
julian-CStack 2025-03-20 20:48:55 -06:00 committed by GitHub
commit 3118968230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 2314 additions and 320 deletions
.gitignore
asset_sources/default_themes
ios
lib
linux/flutter
macos
pubspec.lock
scripts
test
windows/flutter

5
.gitignore vendored
View file

@ -68,6 +68,10 @@ secp256k1.dll
/lib/app_config.g.dart
/android/app/src/main/app_icon-playstore.png
# Dart generated files (Freezed, Riverpod, GoRouter etc..)
lib/**/*.g.dart
lib/**/*.freezed.dart
## other generated project files
pubspec.yaml
@ -104,3 +108,4 @@ scripts/linux/build/libsecret/subprojects/gi-docgen/.meson-subproject-wrap-hash.
crypto_plugins/cs_monero/built_outputs
crypto_plugins/cs_monero/build
crypto_plugins/*.diff

View file

@ -109,6 +109,8 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- xelis_flutter (0.0.1):
- Flutter
DEPENDENCIES:
- barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
@ -138,6 +140,7 @@ DEPENDENCIES:
- tor_ffi_plugin (from `.symlinks/plugins/tor_ffi_plugin/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- xelis_flutter (from `.symlinks/plugins/xelis_flutter/ios`)
SPEC REPOS:
trunk:
@ -205,6 +208,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
xelis_flutter:
:path: ".symlinks/plugins/xelis_flutter/ios"
SPEC CHECKSUMS:
barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0

View file

@ -22,8 +22,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:window_size/window_size.dart';
import 'package:xelis_flutter/src/api/api.dart' as xelis_api;
import 'package:xelis_flutter/src/api/logger.dart' as xelis_logging;
import 'package:xelis_flutter/src/frb_generated.dart' as xelis_rust;
import 'app_config.dart';
import 'db/db_version_migration.dart';
@ -74,13 +78,44 @@ import 'wallets/isar/providers/all_wallets_info_provider.dart';
import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'widgets/crypto_notifications.dart';
final openedFromSWBFileStringStateProvider =
StateProvider<String?>((ref) => null);
final openedFromSWBFileStringStateProvider = StateProvider<String?>(
(ref) => null,
);
void startListeningToRustLogs() {
xelis_api.createLogStream().listen(
(logEntry) {
final Level level;
switch (logEntry.level) {
case xelis_logging.Level.error:
level = Level.error;
case xelis_logging.Level.warn:
level = Level.warning;
case xelis_logging.Level.info:
level = Level.info;
case xelis_logging.Level.debug:
level = Level.debug;
case xelis_logging.Level.trace:
level = Level.trace;
}
Logging.instance.log(
level,
"[Xelis Rust Log] ${logEntry.tag}: ${logEntry.msg}",
);
},
onError: (dynamic e) {
Logging.instance.e("Error receiving Xelis Rust logs: $e");
},
);
}
// main() is the entry point to the app. It initializes Hive (local database),
// runs the MyApp widget and checks for new users, caching the value in the
// miscellaneous box for later use
void main(List<String> args) async {
// talker.info('initializing Rust lib ...');
await xelis_rust.RustLib.init();
WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop && args.length == 2 && args.first == "-d") {
@ -108,9 +143,7 @@ void main(List<String> args) async {
if (screenHeight != null) {
// starting to height be 3/4 screen height or 900, whichever is smaller
final height = min<double>(screenHeight * 0.75, 900);
setWindowFrame(
Rect.fromLTWH(0, 0, 1220, height),
);
setWindowFrame(Rect.fromLTWH(0, 0, 1220, height));
}
}
@ -146,8 +179,9 @@ void main(List<String> args) async {
// node model adapter
DB.instance.hive.registerAdapter(NodeModelAdapter());
if (!DB.instance.hive
.isAdapterRegistered(lib_monero_compat.WalletInfoAdapter().typeId)) {
if (!DB.instance.hive.isAdapterRegistered(
lib_monero_compat.WalletInfoAdapter().typeId,
)) {
DB.instance.hive.registerAdapter(lib_monero_compat.WalletInfoAdapter());
}
@ -168,6 +202,9 @@ void main(List<String> args) async {
level: Prefs.instance.logLevel,
);
await xelis_api.setUpRustLogger();
startListeningToRustLogs();
// setup lib spark logging
initSparkLogging(Prefs.instance.logLevel);
@ -194,10 +231,12 @@ void main(List<String> args) async {
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
if (!Util.isDesktop) {
final int dbVersion = DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
) as int? ??
final int dbVersion =
DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
)
as int? ??
0;
if (dbVersion < Constants.currentDataVersion) {
try {
@ -232,22 +271,25 @@ void main(List<String> args) async {
// verify current user preference theme and revert to default
// if problems are found to prevent app being unusable
if (!(await ThemeService.instance
.verifyInstalled(themeId: Prefs.instance.themeId))) {
if (!(await ThemeService.instance.verifyInstalled(
themeId: Prefs.instance.themeId,
))) {
Prefs.instance.themeId = "light";
}
// verify current user preference light brightness theme and revert to default
// if problems are found to prevent app being unusable
if (!(await ThemeService.instance
.verifyInstalled(themeId: Prefs.instance.systemBrightnessLightThemeId))) {
if (!(await ThemeService.instance.verifyInstalled(
themeId: Prefs.instance.systemBrightnessLightThemeId,
))) {
Prefs.instance.systemBrightnessLightThemeId = "light";
}
// verify current user preference dark brightness theme and revert to default
// if problems are found to prevent app being unusable
if (!(await ThemeService.instance
.verifyInstalled(themeId: Prefs.instance.systemBrightnessDarkThemeId))) {
if (!(await ThemeService.instance.verifyInstalled(
themeId: Prefs.instance.systemBrightnessDarkThemeId,
))) {
Prefs.instance.systemBrightnessDarkThemeId = "dark";
}
@ -263,18 +305,14 @@ class MyApp extends StatelessWidget {
final localeService = LocaleService();
localeService.loadLocale();
return const KeyboardDismisser(
child: MaterialAppWithTheme(),
);
return const KeyboardDismisser(child: MaterialAppWithTheme());
}
}
// Sidenote: MaterialAppWithTheme and InitView are only separated for clarity. No other reason.
class MaterialAppWithTheme extends ConsumerStatefulWidget {
const MaterialAppWithTheme({
super.key,
});
const MaterialAppWithTheme({super.key});
@override
ConsumerState<MaterialAppWithTheme> createState() =>
@ -348,7 +386,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
prefs: ref.read(prefsChangeNotifierProvider),
);
ref.read(priceAnd24hChangeNotifierProvider).start(true);
await ref.read(pWallets).load(
await ref
.read(pWallets)
.load(
ref.read(prefsChangeNotifierProvider),
ref.read(mainDBProvider),
);
@ -378,7 +418,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) {
switch (ref.read(prefsChangeNotifierProvider).backupFrequencyType) {
case BackupFrequencyType.everyTenMinutes:
ref.read(autoSWBServiceProvider).startPeriodicBackupTimer(
ref
.read(autoSWBServiceProvider)
.startPeriodicBackupTimer(
duration: const Duration(minutes: 10),
);
break;
@ -411,9 +453,10 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId;
break;
case Brightness.light:
themeId = ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
themeId =
ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
break;
}
} else {
@ -432,9 +475,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
ref.read(applicationThemesDirectoryPathProvider.notifier).state =
StackFileSystem.themesDir!.path;
ref.read(themeProvider.state).state = ref.read(pThemeService).getTheme(
themeId: themeId,
)!;
ref.read(themeProvider.state).state =
ref.read(pThemeService).getTheme(themeId: themeId)!;
if (Platform.isAndroid) {
// fetch open file if it exists
@ -462,18 +504,17 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId;
break;
case Brightness.light:
themeId = ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
themeId =
ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
break;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) {
ref.read(themeProvider.state).state =
ref.read(pThemeService).getTheme(
themeId: themeId,
)!;
ref.read(pThemeService).getTheme(themeId: themeId)!;
}
});
};
@ -552,8 +593,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
/// should only be called on android currently
Future<void> getOpenFile() async {
// update provider with new file content state
ref.read(openedFromSWBFileStringStateProvider.state).state =
await platform.invokeMethod("getOpenFile");
ref.read(openedFromSWBFileStringStateProvider.state).state = await platform
.invokeMethod("getOpenFile");
// call reset to clear cached value
await resetOpenPath();
@ -570,9 +611,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
Future<void> goToRestoreSWB(String encrypted) async {
if (!ref.read(prefsChangeNotifierProvider).hasPin) {
await Navigator.of(navigatorKey.currentContext!)
.pushNamed(CreatePinView.routeName, arguments: true)
.then((value) {
await Navigator.of(
navigatorKey.currentContext!,
).pushNamed(CreatePinView.routeName, arguments: true).then((value) {
if (value is! bool || value == false) {
Navigator.of(navigatorKey.currentContext!).pushNamed(
RestoreFromEncryptedStringView.routeName,
@ -586,16 +627,17 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
navigatorKey.currentContext!,
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView(
showBackButton: true,
routeOnSuccess: RestoreFromEncryptedStringView.routeName,
routeOnSuccessArguments: encrypted,
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to restore ${AppConfig.appName} backup",
biometricsAuthenticationTitle:
"Restore ${AppConfig.prefix} backup",
),
builder:
(_) => LockscreenView(
showBackButton: true,
routeOnSuccess: RestoreFromEncryptedStringView.routeName,
routeOnSuccessArguments: encrypted,
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to restore ${AppConfig.appName} backup",
biometricsAuthenticationTitle:
"Restore ${AppConfig.prefix} backup",
),
settings: const RouteSettings(name: "/swbrestorelockscreen"),
),
),
@ -605,10 +647,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
InputBorder _buildOutlineInputBorder(Color color) {
return OutlineInputBorder(
borderSide: BorderSide(
width: 1,
color: color,
),
borderSide: BorderSide(width: 1, color: color),
borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
);
}
@ -646,9 +685,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
),
// splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
buttonTheme: ButtonThemeData(
splashColor: colorScheme.splash,
),
buttonTheme: ButtonThemeData(splashColor: colorScheme.splash),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
// splashFactory: NoSplash.splashFactory,
@ -656,8 +693,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
minimumSize: MaterialStateProperty.all<Size>(const Size(46, 46)),
// textStyle: MaterialStateProperty.all<TextStyle>(
// STextStyles.button(context)),
foregroundColor:
MaterialStateProperty.all(colorScheme.buttonTextSecondary),
foregroundColor: MaterialStateProperty.all(
colorScheme.buttonTextSecondary,
),
backgroundColor: MaterialStateProperty.all<Color>(
colorScheme.buttonBackSecondary,
),
@ -674,25 +712,22 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
checkboxTheme: CheckboxThemeData(
splashRadius: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(Constants.size.checkboxBorderRadius),
borderRadius: BorderRadius.circular(
Constants.size.checkboxBorderRadius,
),
),
checkColor: MaterialStateColor.resolveWith(
(state) {
if (state.contains(MaterialState.selected)) {
return colorScheme.checkboxIconChecked;
}
checkColor: MaterialStateColor.resolveWith((state) {
if (state.contains(MaterialState.selected)) {
return colorScheme.checkboxIconChecked;
}
return colorScheme.checkboxBGChecked;
}),
fillColor: MaterialStateColor.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.checkboxBGChecked;
},
),
fillColor: MaterialStateColor.resolveWith(
(states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.checkboxBGChecked;
}
return colorScheme.checkboxBorderEmpty;
},
),
}
return colorScheme.checkboxBorderEmpty;
}),
),
appBarTheme: AppBarTheme(
centerTitle: false,
@ -710,91 +745,101 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
),
// labelStyle: STextStyles.fieldLabel(context),
// hintStyle: STextStyles.fieldLabel(context),
enabledBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
focusedBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
enabledBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
focusedBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
errorBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
disabledBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
focusedErrorBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
disabledBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
focusedErrorBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
),
),
home: CryptoNotifications(
child: Util.isDesktop
? FutureBuilder(
future: loadShared(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (_desktopHasPassword) {
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.gotoWalletOnStartup) {
startupWalletId = ref
child:
Util.isDesktop
? FutureBuilder(
future: loadShared(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (_desktopHasPassword) {
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
.gotoWalletOnStartup) {
startupWalletId =
ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
return DesktopLoginView(
startupWalletId: startupWalletId,
load: load,
);
} else {
return const IntroView();
}
} else {
return const LoadingView();
}
},
)
: FutureBuilder(
future: load(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (ref.read(pAllWalletsInfo).isNotEmpty ||
ref.read(prefsChangeNotifierProvider).hasPin) {
// return HomeView();
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.gotoWalletOnStartup) {
startupWalletId = ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
return LockscreenView(
isInitialAppLogin: true,
routeOnSuccess: HomeView.routeName,
routeOnSuccessArguments: startupWalletId,
biometricsAuthenticationTitle:
"Unlock ${AppConfig.prefix}",
biometricsLocalizedReason:
"Unlock your ${AppConfig.appName} using biometrics",
biometricsCancelButtonString: "Cancel",
);
} else {
if (AppConfig.appName == "Campfire" &&
!CampfireMigration.didRun &&
CampfireMigration.hasOldWallets) {
return const CampfireMigrateView();
return DesktopLoginView(
startupWalletId: startupWalletId,
load: load,
);
} else {
return const IntroView();
}
} else {
return const LoadingView();
}
} else {
// CURRENTLY DISABLED as cannot be animated
// technically not needed as FlutterNativeSplash will overlay
// anything returned here until the future completes but
// FutureBuilder requires you to return something
return const LoadingView();
}
},
),
},
)
: FutureBuilder(
future: load(),
builder: (
BuildContext context,
AsyncSnapshot<void> snapshot,
) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (ref.read(pAllWalletsInfo).isNotEmpty ||
ref.read(prefsChangeNotifierProvider).hasPin) {
// return HomeView();
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.gotoWalletOnStartup) {
startupWalletId =
ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
return LockscreenView(
isInitialAppLogin: true,
routeOnSuccess: HomeView.routeName,
routeOnSuccessArguments: startupWalletId,
biometricsAuthenticationTitle:
"Unlock ${AppConfig.prefix}",
biometricsLocalizedReason:
"Unlock your ${AppConfig.appName} using biometrics",
biometricsCancelButtonString: "Cancel",
);
} else {
if (AppConfig.appName == "Campfire" &&
!CampfireMigration.didRun &&
CampfireMigration.hasOldWallets) {
return const CampfireMigrateView();
} else {
return const IntroView();
}
}
} else {
// CURRENTLY DISABLED as cannot be animated
// technically not needed as FlutterNativeSplash will overlay
// anything returned here until the future completes but
// FutureBuilder requires you to return something
return const LoadingView();
}
},
),
),
);
}

View file

@ -175,7 +175,8 @@ enum AddressType {
frostMS,
p2tr,
solana,
cardanoShelley;
cardanoShelley,
xelis;
String get readableName {
switch (this) {
@ -213,6 +214,8 @@ enum AddressType {
return "P2TR (taproot)";
case AddressType.cardanoShelley:
return "Cardano Shelley";
case AddressType.xelis:
return "Xelis";
}
}
}

View file

@ -279,6 +279,7 @@ const _AddresstypeEnumValueMap = {
'p2tr': 14,
'solana': 15,
'cardanoShelley': 16,
'xelis': 17,
};
const _AddresstypeValueEnumMap = {
0: AddressType.p2pkh,
@ -298,6 +299,7 @@ const _AddresstypeValueEnumMap = {
14: AddressType.p2tr,
15: AddressType.solana,
16: AddressType.cardanoShelley,
17: AddressType.xelis,
};
Id _addressGetId(Address object) {

View file

@ -191,7 +191,7 @@ class _NewWalletRecoveryPhraseWarningViewState
// TODO: Refactor these to generate each coin in their respective classes
// This code should not be in a random view page file
if (coin is Monero || coin is Wownero) {
if (coin is Monero || coin is Wownero || coin is Xelis) {
// currently a special case due to the
// xmr/wow libraries handling their
// own mnemonic generation

View file

@ -31,6 +31,7 @@ import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -264,6 +265,10 @@ class _RestoreViewOnlyWalletViewState
await (wallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (wallet as XelisWallet).init(isRestore: true);
break;
default:
await wallet.init();
}

View file

@ -25,6 +25,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xelis_flutter/src/api/seed_search_engine.dart' as x_seed;
import '../../../notifications/show_flush_bar.dart';
import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@ -48,7 +50,8 @@ import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/intermediate/external_wallet.dart';
import '../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -103,6 +106,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
late final int _seedWordCount;
late final bool isDesktop;
x_seed.SearchEngine? _xelisSeedSearch;
final HashSet<String> _wordListHashSet = HashSet.from(bip39wordlist.WORDLIST);
final ScrollController controller = ScrollController();
@ -167,6 +171,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
// _focusNodes.add(FocusNode());
}
if (widget.coin is Xelis) {
_xelisSeedSearch = x_seed.SearchEngine.init(languageIndex: BigInt.from(0));
}
super.initState();
}
@ -199,6 +207,9 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
);
return wowneroWordList.contains(word);
}
if (widget.coin is Xelis) {
return _xelisSeedSearch!.search(query: word).length > 0;
}
return _wordListHashSet.contains(word);
}
@ -283,10 +294,9 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
);
}
// TODO: do actual check to make sure it is a valid mnemonic for monero
// TODO: do actual check to make sure it is a valid mnemonic for monero + xelis
if (bip39.validateMnemonic(mnemonic) == false &&
!(widget.coin is Monero || widget.coin is Wownero)) {
if (mounted) setState(() => _hideSeedWords = false);
!(widget.coin is Monero || widget.coin is Wownero || widget.coin is Xelis)) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
@ -371,13 +381,17 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
await (wallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (wallet as XelisWallet).init(isRestore: true);
break;
default:
await wallet.init();
}
await wallet.recover(isRescan: false);
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
await wallet.exit();
}

View file

@ -40,6 +40,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
@ -225,6 +226,10 @@ class _VerifyRecoveryPhraseViewState
await (voWallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (voWallet as XelisWallet).init(isRestore: true);
break;
default:
await voWallet.init();
}

View file

@ -35,7 +35,7 @@ import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../wallets/wallet/intermediate/external_wallet.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -277,7 +277,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
// access to this screen but this is needed to get past an error that
// would occur only to lead to another error which is why xmr/wow wallets
// don't have access to this screen currently
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
await wallet.init();
await wallet.open();
}

View file

@ -29,7 +29,7 @@ import '../../utilities/show_loading.dart';
import '../../utilities/show_node_tor_settings_mismatch.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../wallets/wallet/intermediate/external_wallet.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/custom_buttons/blue_text_button.dart';
@ -119,7 +119,7 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {

View file

@ -55,6 +55,7 @@ import '../../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../../../wallets/wallet/wallet.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
@ -506,6 +507,10 @@ abstract class SWB {
case const (WowneroWallet):
await (wallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (wallet as XelisWallet).init(isRestore: true);
break;
default:
await wallet.init();

View file

@ -1366,13 +1366,17 @@ class _TransactionV2DetailsViewState
],
),
),
if (coin is! NanoCurrency)
if (coin is! NanoCurrency &&
!(coin is Xelis && _transaction.type == TransactionType.incoming)
)
isDesktop
? const _Divider()
: const SizedBox(
height: 12,
),
if (coin is! NanoCurrency)
if (coin is! NanoCurrency &&
!(coin is Xelis && _transaction.type == TransactionType.incoming)
)
RoundedWhiteContainer(
padding: isDesktop
? const EdgeInsets.all(16)

View file

@ -27,7 +27,7 @@ import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/coins/firo.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/intermediate/external_wallet.dart';
import '../../../widgets/coin_card.dart';
import '../../../widgets/conditional_parent.dart';
import '../../wallet_view/wallet_view.dart';
@ -132,7 +132,7 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {

View file

@ -25,7 +25,7 @@ import '../../../utilities/show_node_tor_settings_mismatch.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/intermediate/external_wallet.dart';
import '../../../widgets/dialogs/tor_warning_dialog.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../wallet_view/wallet_view.dart';
@ -99,7 +99,7 @@ class WalletListItem extends ConsumerWidget {
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {

View file

@ -21,7 +21,7 @@ import '../../utilities/show_loading.dart';
import '../../utilities/show_node_tor_settings_mismatch.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../wallets/wallet/intermediate/external_wallet.dart';
import '../../widgets/rounded_container.dart';
import '../../widgets/wallet_info_row/wallet_info_row.dart';
import 'wallet_view/desktop_wallet_view.dart';
@ -101,7 +101,7 @@ class CoinWalletsTable extends ConsumerWidget {
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
loadFuture = wallet
.init()
.then((value) async => await (wallet).open());

View file

@ -25,7 +25,7 @@ import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/isar/providers/all_wallets_info_provider.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../wallets/wallet/intermediate/external_wallet.dart';
import '../../widgets/breathing.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/desktop/desktop_dialog.dart';
@ -138,7 +138,7 @@ class _DesktopWalletSummaryRowState
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {

View file

@ -0,0 +1,78 @@
import 'package:xelis_flutter/src/api/api.dart' as xelis_api;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart';
import 'dart:math' as math;
enum XelisTableGenerationStep {
t1PointsGeneration,
t1CuckooSetup,
t2Table,
unknown;
factory XelisTableGenerationStep.fromString(String step) {
return switch (step) {
"T1PointsGeneration" => XelisTableGenerationStep.t1PointsGeneration,
"T1CuckooSetup" => XelisTableGenerationStep.t1CuckooSetup,
"T2Table" => XelisTableGenerationStep.t2Table,
_ => XelisTableGenerationStep.unknown,
};
}
String get displayName => switch (this) {
t1PointsGeneration => "Generating T1 Points",
t1CuckooSetup => "Setting up T1 Cuckoo",
t2Table => "Generating T2 Table",
unknown => "Processing",
};
}
class XelisTableProgressState {
final double? tableProgress;
final XelisTableGenerationStep currentStep;
const XelisTableProgressState({
this.tableProgress,
this.currentStep = XelisTableGenerationStep.unknown,
});
XelisTableProgressState copyWith({
double? tableProgress,
XelisTableGenerationStep? currentStep,
}) {
return XelisTableProgressState(
tableProgress: tableProgress ?? this.tableProgress,
currentStep: currentStep ?? this.currentStep,
);
}
}
final xelisTableProgressProvider = StreamProvider<XelisTableProgressState>((ref) {
double lastPrintedProgress = 0.0;
return xelis_api.createProgressReportStream().map((report) {
return report.when(
tableGeneration: (progress, step, _) {
final currentStep = XelisTableGenerationStep.fromString(step);
final stepIndex = switch(currentStep) {
XelisTableGenerationStep.t1PointsGeneration => 0,
XelisTableGenerationStep.t1CuckooSetup => 1,
XelisTableGenerationStep.t2Table => 2,
XelisTableGenerationStep.unknown => 0,
};
if ((progress - lastPrintedProgress).abs() >= 0.05 ||
currentStep != XelisTableGenerationStep.fromString(step) ||
progress >= 0.99) {
debugPrint("Xelis Table Generation: $step - ${progress*100.0}%");
lastPrintedProgress = progress;
}
return XelisTableProgressState(
tableProgress: progress,
currentStep: currentStep,
);
},
misc: (_) => const XelisTableProgressState(),
);
});
});

View file

@ -32,3 +32,4 @@ export './ui/verify_recovery_phrase/correct_word_provider.dart';
export './ui/verify_recovery_phrase/random_index_provider.dart';
export './ui/verify_recovery_phrase/selected_word_provider.dart';
export './wallet/transaction_note_provider.dart';
export './progress_report/xelis_table_progress_provider.dart';

View file

@ -48,6 +48,7 @@ class PriceAPI {
Namecoin: "namecoin",
Nano: "nano",
Banano: "banano",
Xelis: "xelis",
};
static const refreshInterval = 60;

View file

@ -31,9 +31,7 @@ final pThemeService = Provider<ThemeService>((ref) {
});
class ThemeService {
// dumb quick conditional based on name. Should really be done better
static const _currentDefaultThemeVersion =
AppConfig.appName == "Campfire" ? 17 : 16;
static const _currentDefaultThemeVersion = 17;
ThemeService._();
static ThemeService? _instance;
static ThemeService get instance => _instance ??= ThemeService._();
@ -61,9 +59,7 @@ class ThemeService {
final jsonString = utf8.decode(themeJsonFiles.first.content as List<int>);
final json = jsonDecode(jsonString) as Map;
final theme = StackTheme.fromJson(
json: Map<String, dynamic>.from(json),
);
final theme = StackTheme.fromJson(json: Map<String, dynamic>.from(json));
try {
theme.assets;
@ -96,11 +92,12 @@ class ThemeService {
Future<void> remove({required String themeId}) async {
final themesDir = StackFileSystem.themesDir!;
final isarId = await db.isar.stackThemes
.where()
.themeIdEqualTo(themeId)
.idProperty()
.findFirst();
final isarId =
await db.isar.stackThemes
.where()
.themeIdEqualTo(themeId)
.idProperty()
.findFirst();
if (isarId != null) {
await db.isar.writeTxn(() async {
await db.isar.stackThemes.delete(isarId);
@ -184,22 +181,27 @@ class ThemeService {
try {
final response = await client.get(
url: Uri.parse("$baseServerUrl/themes"),
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
proxyInfo:
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
final jsonList = jsonDecode(response.body) as List;
final result = List<Map<String, dynamic>>.from(jsonList)
.map((e) => StackThemeMetaData.fromMap(e))
.where((e) => e.id != "light" && e.id != "dark")
.toList();
final result =
List<Map<String, dynamic>>.from(jsonList)
.map((e) => StackThemeMetaData.fromMap(e))
.where((e) => e.id != "light" && e.id != "dark")
.toList();
return result;
} catch (e, s) {
Logging.instance
.w("Failed to fetch themes list: ", error: e, stackTrace: s);
Logging.instance.w(
"Failed to fetch themes list: ",
error: e,
stackTrace: s,
);
rethrow;
}
}
@ -210,9 +212,10 @@ class ThemeService {
try {
final response = await client.get(
url: Uri.parse("$baseServerUrl/theme/${themeMetaData.id}"),
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
proxyInfo:
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
final bytes = Uint8List.fromList(response.bodyBytes);
@ -228,8 +231,11 @@ class ThemeService {
);
}
} catch (e, s) {
Logging.instance
.w("Failed to fetch themes list: ", error: e, stackTrace: s);
Logging.instance.w(
"Failed to fetch themes list: ",
error: e,
stackTrace: s,
);
rethrow;
}
}
@ -270,9 +276,10 @@ class StackThemeMetaData {
);
} catch (e, s) {
Logging.instance.f(
"Failed to create instance of StackThemeMetaData using $map",
error: e,
stackTrace: s);
"Failed to create instance of StackThemeMetaData using $map",
error: e,
stackTrace: s,
);
rethrow;
}
}

View file

@ -19,7 +19,8 @@ enum DerivePathType {
eCash44,
solana,
bip86,
cardanoShelley;
cardanoShelley,
xelis;
AddressType getAddressType() {
switch (this) {
@ -45,6 +46,9 @@ enum DerivePathType {
case DerivePathType.cardanoShelley:
return AddressType.cardanoShelley;
case DerivePathType.xelis:
return AddressType.xelis;
}
}
}

View file

@ -39,8 +39,9 @@ abstract class StackFileSystem {
// todo: can merge and do same as regular linux home dir?
if (Util.isArmLinux) {
appDirectory = await getApplicationDocumentsDirectory();
appDirectory =
Directory("${appDirectory.path}/.${AppConfig.appDefaultDataDirName}");
appDirectory = Directory(
"${appDirectory.path}/.${AppConfig.appDefaultDataDirName}",
);
} else if (Platform.isLinux) {
if (_overrideDesktopDirPath != null) {
appDirectory = Directory(_overrideDesktopDirPath!);
@ -148,6 +149,24 @@ abstract class StackFileSystem {
}
}
static Future<Directory> applicationXelisDirectory() async {
final root = await applicationRootDirectory();
final dir = Directory("${root.path}${Platform.pathSeparator}xelis");
if (!dir.existsSync()) {
await dir.create();
}
return dir;
}
static Future<Directory> applicationXelisTableDirectory() async {
final xelis = await applicationXelisDirectory();
final dir = Directory("${xelis.path}${Platform.pathSeparator}table");
if (!dir.existsSync()) {
await dir.create();
}
return dir;
}
static Future<void> initThemesDir() async {
final root = await applicationRootDirectory();

View file

@ -26,6 +26,8 @@ import 'test_monero_node_connection.dart';
import 'test_stellar_node_connection.dart';
import 'tor_plain_net_option_enum.dart';
import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
Future<bool> _xmrHelper(
NodeFormData nodeFormData,
BuildContext context,
@ -297,6 +299,28 @@ Future<bool> testNodeConnection({
testPassed = false;
}
break;
case Xelis():
try {
final daemon = xelis_sdk.DaemonClient(
endPoint: "${formData.host!}:${formData.port!}",
secureWebSocket: formData.useSSL ?? false,
timeout: 5000
);
daemon.connect();
final xelis_sdk.GetInfoResult networkInfo = await daemon.getInfo();
testPassed = networkInfo.height != null;
daemon.disconnect();
Logging.instance.i(
"Xelis testNodeConnection result: \"${networkInfo.toString()}\"",
);
} catch (e, s) {
testPassed = false;
}
break;
}
return testPassed;

View file

@ -0,0 +1,142 @@
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/node_model.dart';
import '../../../utilities/default_nodes.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
import '../crypto_currency.dart';
import '../intermediate/electrum_currency.dart';
import 'package:xelis_flutter/src/api/utils.dart' as x_utils;
class Xelis extends ElectrumCurrency {
Xelis(super.network) {
_idMain = "xelis";
_uriScheme = "xelis";
switch (network) {
case CryptoCurrencyNetwork.main:
_id = _idMain;
_name = "Xelis";
_ticker = "XEL";
case CryptoCurrencyNetwork.test:
_id = "xelisTestNet";
_name = "tXelis";
_ticker = "XET";
default:
throw Exception("Unsupported network: $network");
}
}
late final String _id;
@override
String get identifier => _id;
late final String _idMain;
@override
String get mainNetId => _idMain;
late final String _name;
@override
String get prettyName => _name;
late final String _uriScheme;
@override
String get uriScheme => _uriScheme;
late final String _ticker;
@override
String get ticker => _ticker;
@override
NodeModel get defaultNode {
switch (network) {
case CryptoCurrencyNetwork.main:
return NodeModel(
host: "us-node.xelis.io",
port: 443,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: true,
enabled: true,
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: false,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
return NodeModel(
host: "testnet-node.xelis.io",
port: 443,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: true,
enabled: true,
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: false,
clearnetEnabled: true,
);
default:
throw Exception("Unsupported network: $network");
}
}
@override
int get minConfirms => 1;
@override
bool get torSupport => false;
@override
bool validateAddress(String address) {
try {
return x_utils.isAddressValid(strAddress: address);
} catch (_) {
return false;
}
}
@override
String get genesisHash => throw UnimplementedError();
@override
int get defaultSeedPhraseLength => 25;
@override
int get fractionDigits => 8;
@override
bool get hasBuySupport => false;
@override
bool get hasMnemonicPassphraseSupport => false;
@override
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength];
@override
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(1000000000);
@override
int get targetBlockTimeSeconds => 15;
@override
DerivePathType get defaultDerivePathType => DerivePathType.xelis;
@override
Uri defaultBlockExplorer(String txid) {
switch (network) {
case CryptoCurrencyNetwork.main:
return Uri.parse("https://explorer.xelis.io/txs/$txid");
default:
throw Exception(
"Unsupported network for defaultBlockExplorer(): $network",
);
}
}
}

View file

@ -23,6 +23,7 @@ export 'coins/solana.dart';
export 'coins/stellar.dart';
export 'coins/tezos.dart';
export 'coins/wownero.dart';
export 'coins/xelis.dart';
enum CryptoCurrencyNetwork {
main,

View file

@ -0,0 +1,5 @@
import '../crypto_currency.dart';
abstract class ElectrumCurrency extends CryptoCurrency {
ElectrumCurrency(super.network);
}

View file

@ -517,6 +517,7 @@ abstract class WalletInfoKeys {
static const String epiccashData = "epiccashDataKey";
static const String bananoMonkeyImageBytes = "monkeyImageBytesKey";
static const String tezosDerivationPath = "tezosDerivationPathKey";
static const String xelisDerivationPath = "xelisDerivationPathKey";
static const String lelantusCoinIsarRescanRequired =
"lelantusCoinIsarRescanRequired";
static const String enableLelantusScanning = "enableLelantusScanningKey";

View file

@ -269,6 +269,7 @@ const _WalletInfomainAddressTypeEnumValueMap = {
'p2tr': 14,
'solana': 15,
'cardanoShelley': 16,
'xelis': 17,
};
const _WalletInfomainAddressTypeValueEnumMap = {
0: AddressType.p2pkh,
@ -288,6 +289,7 @@ const _WalletInfomainAddressTypeValueEnumMap = {
14: AddressType.p2tr,
15: AddressType.solana,
16: AddressType.cardanoShelley,
17: AddressType.xelis,
};
Id _walletInfoGetId(WalletInfo object) {

View file

@ -74,6 +74,9 @@ class TxData {
final List<TxData>? sparkMints;
final List<SparkCoin>? usedSparkCoins;
// xelis specific
final String? otherData;
final TransactionV2? tempTx;
final bool ignoreCachedBalanceChecks;
@ -113,6 +116,7 @@ class TxData {
this.mintsMapLelantus,
this.tezosOperationsList,
this.sparkRecipients,
this.otherData,
this.sparkMints,
this.usedSparkCoins,
this.tempTx,
@ -213,6 +217,7 @@ class TxData {
String? note,
String? noteOnChain,
String? memo,
String? otherData,
Set<UTXO>? utxos,
List<UTXO>? usedUTXOs,
List<TxRecipient>? recipients,
@ -258,6 +263,7 @@ class TxData {
note: note ?? this.note,
noteOnChain: noteOnChain ?? this.noteOnChain,
memo: memo ?? this.memo,
otherData: otherData ?? this.otherData,
utxos: utxos ?? this.utxos,
usedUTXOs: usedUTXOs ?? this.usedUTXOs,
recipients: recipients ?? this.recipients,
@ -321,6 +327,7 @@ class TxData {
'sparkRecipients: $sparkRecipients, '
'sparkMints: $sparkMints, '
'usedSparkCoins: $usedSparkCoins, '
'otherData: $otherData, '
'tempTx: $tempTx, '
'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, '
'opNameState: $opNameState, '

View file

@ -0,0 +1,990 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stack_wallet_backup/generate_password.dart';
import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
import 'package:xelis_flutter/src/api/wallet.dart' as x_wallet;
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/paymint/fee_object_model.dart';
import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import '../../../services/event_bus/global_event_bus.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/stack_file_system.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import '../intermediate/lib_xelis_wallet.dart';
import '../wallet.dart';
class XelisWallet extends LibXelisWallet {
Completer<void>? _initCompleter;
XelisWallet(CryptoCurrencyNetwork network) : super(Xelis(network));
// ==================== Overrides ============================================
@override
int get isarTransactionVersion => 2;
Future<void> _restoreWallet() async {
final tablePath = await getPrecomputedTablesPath();
final tableState = await getTableState();
final xelisDir = await StackFileSystem.applicationXelisDirectory();
final String name = walletId;
final String directory = xelisDir.path;
final password = await secureStorageInterface.read(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
);
final mnemonic = await getMnemonic();
final seedLength = mnemonic.trim().split(" ").length;
invalidSeedLengthCheck(seedLength);
Logging.instance.i("Xelis: recovering wallet");
final wallet = await x_wallet.createXelisWallet(
name: name,
directory: directory,
password: password!,
seed: mnemonic.trim(),
network: cryptoCurrency.network.xelisNetwork,
precomputedTablesPath: tablePath,
l1Low: tableState.currentSize.isLow,
);
await secureStorageInterface.write(
key: Wallet.mnemonicKey(walletId: walletId),
value: mnemonic.trim(),
);
libXelisWallet = wallet;
await _finishInit();
}
Future<void> _createNewWallet() async {
final tablePath = await getPrecomputedTablesPath();
final tableState = await getTableState();
final xelisDir = await StackFileSystem.applicationXelisDirectory();
final String name = walletId;
final String directory = xelisDir.path;
final String password = generatePassword();
Logging.instance.d("Xelis: storing password");
await secureStorageInterface.write(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
value: password,
);
final wallet = await x_wallet.createXelisWallet(
name: name,
directory: directory,
password: password,
network: cryptoCurrency.network.xelisNetwork,
precomputedTablesPath: tablePath,
l1Low: tableState.currentSize.isLow,
);
final mnemonic = await wallet.getSeed();
await secureStorageInterface.write(
key: Wallet.mnemonicKey(walletId: walletId),
value: mnemonic.trim(),
);
libXelisWallet = wallet;
await _finishInit();
}
Future<void> _existingWallet() async {
Logging.instance.i("Xelis: opening existing wallet");
final tablePath = await getPrecomputedTablesPath();
final tableState = await getTableState();
final xelisDir = await StackFileSystem.applicationXelisDirectory();
final String name = walletId;
final String directory = xelisDir.path;
final password = await secureStorageInterface.read(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
);
libXelisWallet = await x_wallet.openXelisWallet(
name: name,
directory: directory,
password: password!,
network: cryptoCurrency.network.xelisNetwork,
precomputedTablesPath: tablePath,
l1Low: tableState.currentSize.isLow,
);
await _finishInit();
}
Future<void> _finishInit() async {
if (await isTableUpgradeAvailable()) {
unawaited(updateTablesToDesiredSize());
}
final newReceivingAddress =
await getCurrentReceivingAddress() ??
Address(
walletId: walletId,
derivationIndex: 0,
derivationPath: null,
value: libXelisWallet!.getAddressStr(),
publicKey: [],
type: AddressType.xelis,
subType: AddressSubType.receiving,
);
await mainDB.updateOrPutAddresses([newReceivingAddress]);
if (info.cachedReceivingAddress != newReceivingAddress.value) {
await info.updateReceivingAddress(
newAddress: newReceivingAddress.value,
isar: mainDB.isar,
);
}
}
@override
Future<void> init({bool? isRestore}) async {
Logging.instance.d("Xelis: init");
if (_initCompleter != null) {
await _initCompleter!.future;
return super.init();
}
_initCompleter = Completer<void>();
try {
final bool walletExists = await LibXelisWallet.checkWalletExists(
walletId,
);
if (libXelisWallet == null) {
if (isRestore == true) {
await _restoreWallet();
} else {
if (!walletExists) {
await _createNewWallet();
} else {
await _existingWallet();
}
}
}
_initCompleter!.complete();
} catch (e, s) {
_initCompleter!.completeError(e);
Logging.instance.e(
"Xelis init() rethrowing error",
error: e,
stackTrace: s,
);
rethrow;
}
return super.init();
}
@override
Future<void> recover({required bool isRescan}) async {
if (isRescan) {
await refreshMutex.protect(() async {
await mainDB.deleteWalletBlockchainData(walletId);
await updateTransactions(isRescan: true, topoheight: 0);
});
return;
}
// Borrowed from libmonero for now, need to refactor for Xelis view keys
// if (isViewOnly) {
// await recoverViewOnly();
// return;
// }
try {
await open();
} catch (e, s) {
Logging.instance.e(
"Error rethrown from $runtimeType recover(isRescan: $isRescan)",
error: e,
stackTrace: s,
);
rethrow;
}
}
@override
Future<bool> pingCheck() async {
try {
await libXelisWallet!.getDaemonInfo();
await handleOnline();
return true;
} catch (_) {
await handleOffline();
return false;
}
}
final _balanceUpdateMutex = Mutex();
@override
Future<void> updateBalance({int? newBalance}) async {
await _balanceUpdateMutex.protect(() async {
try {
if (await libXelisWallet!.hasXelisBalance()) {
final BigInt xelBalance =
newBalance != null
? BigInt.from(newBalance)
: await libXelisWallet!
.getXelisBalanceRaw(); // in the future, use getAssetBalances and handle each
final balance = Balance(
total: Amount(
rawValue: xelBalance,
fractionDigits: cryptoCurrency.fractionDigits,
),
spendable: Amount(
rawValue: xelBalance,
fractionDigits: cryptoCurrency.fractionDigits,
),
blockedTotal: Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
),
pendingSpendable: Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
),
);
await info.updateBalance(newBalance: balance, isar: mainDB.isar);
}
} catch (e, s) {
Logging.instance.e(
"Error in $runtimeType updateBalance()",
error: e,
stackTrace: s,
);
}
});
}
Future<int> _fetchChainHeight() async {
final infoString = await libXelisWallet!.getDaemonInfo();
final Map<String, dynamic> nodeInfo =
(json.decode(infoString) as Map).cast();
pruningHeight =
int.tryParse(nodeInfo['pruned_topoheight']?.toString() ?? '0') ?? 0;
return int.parse(nodeInfo['topoheight'].toString());
}
@override
Future<void> updateChainHeight({int? topoheight}) async {
try {
final height = topoheight ?? await _fetchChainHeight();
await info.updateCachedChainHeight(
newHeight: height.toInt(),
isar: mainDB.isar,
);
} catch (e, s) {
Logging.instance.e(
"Error in $runtimeType updateChainHeight()",
error: e,
stackTrace: s,
);
}
}
@override
Future<void> updateNode() async {
try {
final bool online = await libXelisWallet!.isOnline();
if (online == true) {
await libXelisWallet!.offlineMode();
}
await super.connect();
} catch (e, s) {
Logging.instance.e(
"Error rethrown from $runtimeType updateNode()",
error: e,
stackTrace: s,
);
rethrow;
}
}
@override
Future<List<String>> updateTransactions({
bool isRescan = false,
List<String>? rawTransactions,
int? topoheight,
}) async {
checkInitialized();
final newReceivingAddress =
await getCurrentReceivingAddress() ??
Address(
walletId: walletId,
derivationIndex: 0,
derivationPath: null,
value: libXelisWallet!.getAddressStr(),
publicKey: [],
type: AddressType.xelis,
subType: AddressSubType.receiving,
);
final thisAddress = newReceivingAddress.value;
int firstBlock = 0;
if (!isRescan) {
firstBlock =
await mainDB.isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.heightProperty()
.max() ??
0;
if (firstBlock > 10) {
// add some buffer
firstBlock -= 10;
}
} else {
await libXelisWallet!.rescan(topoheight: BigInt.from(pruningHeight));
}
final txListJson = rawTransactions ?? await libXelisWallet!.allHistory();
final List<TransactionV2> txns = [];
for (final jsonString in txListJson) {
try {
final transactionEntry = xelis_sdk.TransactionEntry.fromJson(
(json.decode(jsonString) as Map).cast(),
);
// Check for duplicates
final storedTx =
await mainDB.isar.transactionV2s
.where()
.txidWalletIdEqualTo(transactionEntry.hash, walletId)
.findFirst();
if (storedTx != null &&
storedTx.height != null &&
storedTx.height! > 0) {
continue; // Skip already processed transactions
}
final List<OutputV2> outputs = [];
final List<InputV2> inputs = [];
TransactionType? txType;
const TransactionSubType txSubType = TransactionSubType.none;
int? nonce;
Amount fee = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
final Map<String, dynamic> otherData = {};
final entryType = transactionEntry.txEntryType;
if (entryType is xelis_sdk.CoinbaseEntry) {
final coinbase = entryType;
txType = TransactionType.incoming;
final int decimals = await libXelisWallet!.getAssetDecimals(
asset: xelis_sdk.xelisAsset,
);
fee = Amount(rawValue: BigInt.zero, fractionDigits: decimals);
outputs.add(
OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: "",
valueStringSats: coinbase.reward.toString(),
addresses: [thisAddress],
walletOwns: true,
),
);
} else if (entryType is xelis_sdk.BurnEntry) {
final burn = entryType;
txType = TransactionType.outgoing;
final int decimals = await libXelisWallet!.getAssetDecimals(
asset: burn.asset,
);
fee = Amount(
rawValue: BigInt.from(burn.fee),
fractionDigits: decimals,
);
inputs.add(
InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigAsm: null,
scriptSigHex: null,
sequence: null,
outpoint: null,
valueStringSats: burn.amount.toString(),
addresses: [thisAddress],
witness: null,
innerRedeemScriptAsm: null,
coinbase: null,
walletOwns: true,
),
);
outputs.add(
OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: "",
valueStringSats: burn.amount.toString(),
addresses: ['burn'],
walletOwns: false,
),
);
otherData['burnAsset'] = burn.asset;
} else if (entryType is xelis_sdk.IncomingEntry) {
final incoming = entryType;
txType =
incoming.from == thisAddress
? TransactionType.sentToSelf
: TransactionType.incoming;
for (final transfer in incoming.transfers) {
final int decimals = await libXelisWallet!.getAssetDecimals(
asset: transfer.asset,
);
fee = Amount(rawValue: BigInt.zero, fractionDigits: decimals);
outputs.add(
OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: "",
valueStringSats: transfer.amount.toString(),
addresses: [thisAddress],
walletOwns: true,
),
);
otherData['asset_${transfer.asset}'] = transfer.amount.toString();
if (transfer.extraData != null) {
otherData['extraData_${transfer.asset}'] =
transfer.extraData!.toJson();
}
}
} else if (entryType is xelis_sdk.OutgoingEntry) {
final outgoing = entryType;
txType = TransactionType.outgoing;
nonce = outgoing.nonce;
for (final transfer in outgoing.transfers) {
final int decimals = await libXelisWallet!.getAssetDecimals(
asset: transfer.asset,
);
fee = Amount(
rawValue: BigInt.from(outgoing.fee),
fractionDigits: decimals,
);
inputs.add(
InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: null,
scriptSigAsm: null,
sequence: null,
outpoint: null,
addresses: [thisAddress],
valueStringSats: (transfer.amount + outgoing.fee).toString(),
witness: null,
innerRedeemScriptAsm: null,
coinbase: null,
walletOwns: true,
),
);
outputs.add(
OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: "",
valueStringSats: transfer.amount.toString(),
addresses: [transfer.destination],
walletOwns: false,
),
);
otherData['asset_${transfer.asset}_amount'] =
transfer.amount.toString();
otherData['asset_${transfer.asset}_fee'] = fee.raw.toString();
if (transfer.extraData != null) {
otherData['extraData_${transfer.asset}'] =
transfer.extraData!.toJson();
}
}
} else {
// Skip unknown entry types
continue;
}
final txn = TransactionV2(
walletId: walletId,
blockHash: "", // Not provided in Xelis data
hash: transactionEntry.hash,
txid: transactionEntry.hash,
timestamp:
(transactionEntry.timestamp?.millisecondsSinceEpoch ?? 0) ~/ 1000,
height: transactionEntry.topoheight,
inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs),
version: -1, // Version not provided
type: txType,
subType: txSubType,
otherData: jsonEncode({
...otherData,
if (nonce != null) 'nonce': nonce,
'overrideFee': fee.toJsonString(),
}),
);
// Logging.instance.log(
// "Entry done ${entryType.toString()}",
// level: LogLevel.Debug,
// );
txns.add(txn);
} catch (e, s) {
Logging.instance.w(
"Error in $runtimeType handling transaction: $jsonString",
error: e,
stackTrace: s,
);
}
}
await updateBalance();
await mainDB.updateOrPutTransactionV2s(txns);
return txns.map((e) => e.txid).toList();
}
@override
Future<bool> updateUTXOs() async {
// not used in xel
return false;
}
@override
Future<void> checkSaveInitialReceivingAddress() async {
// do nothing
}
@override
FilterOperation? get changeAddressFilterOperation =>
throw UnimplementedError("Not used for $runtimeType");
@override
FilterOperation? get receivingAddressFilterOperation =>
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<FeeObject> get fees async {
// TODO: implement _getFees... maybe
return FeeObject(
numberOfBlocksFast: 10,
numberOfBlocksAverage: 10,
numberOfBlocksSlow: 10,
fast: 1,
medium: 1,
slow: 1,
);
}
@override
Future<TxData> prepareSend({required TxData txData, String? assetId}) async {
try {
checkInitialized();
final recipients =
txData.recipients?.isNotEmpty == true
? txData.recipients!
: throw ArgumentError(
'Address cannot be empty.',
); // in the future, support for multiple recipients will work.
final asset = assetId ?? xelis_sdk.xelisAsset;
// Calculate total send amount
final totalSendAmount = recipients.fold<Amount>(
Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
),
(sum, recipient) => sum + recipient.amount,
);
// Check balance using raw method
final xelBalance = await libXelisWallet!.getXelisBalanceRaw();
final balance = Amount(
rawValue: xelBalance,
fractionDigits: cryptoCurrency.fractionDigits,
);
// Estimate fee using the shared method
final boostedFee = await estimateFeeFor(
totalSendAmount,
1,
feeMultiplier: 1.0,
recipients: recipients,
assetId: asset,
);
// Check if we have enough for both transfers and fee
if (totalSendAmount + boostedFee > balance) {
final requiredAmt = await libXelisWallet!.formatCoin(
atomicAmount: (totalSendAmount + boostedFee).raw,
assetHash: asset,
);
final availableAmt = await libXelisWallet!.formatCoin(
atomicAmount: xelBalance,
assetHash: asset,
);
throw Exception(
"Insufficient balance to cover transfers and fees. "
"Required: $requiredAmt, Available: $availableAmt",
);
}
return txData.copyWith(
fee: boostedFee,
otherData: jsonEncode({'asset': asset}),
);
} catch (_) {
// Logging.instance.log(
// "Exception rethrown from prepareSend(): $e\n$s",
// level: LogLevel.Error,
// );
rethrow;
}
}
@override
Future<Amount> estimateFeeFor(
Amount amount,
int feeRate, {
double? feeMultiplier,
List<TxRecipient> recipients = const [],
String? assetId,
}) async {
try {
checkInitialized();
final asset = assetId ?? xelis_sdk.xelisAsset;
// Default values for a new wallet or when estimation fails
final defaultDecimals = cryptoCurrency.fractionDigits;
final defaultFee = BigInt.from(0);
// Use default address if recipients list is empty to ensure basic fee estimates are readily available
final effectiveRecipients =
recipients.isNotEmpty
? recipients
: [
(
address:
'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m',
amount: amount,
isChange: false,
),
];
try {
final transfers = await Future.wait(
effectiveRecipients.map((recipient) async {
try {
final amt = double.parse(
await libXelisWallet!.formatCoin(
atomicAmount: recipient.amount.raw,
assetHash: asset,
),
);
return x_wallet.Transfer(
floatAmount: amt,
strAddress: recipient.address,
assetHash: asset,
extraData: null,
);
} catch (e, s) {
// Handle formatCoin error - use default conversion
Logging.instance.d(
"formatCoin failed, using fallback conversion",
error: e,
stackTrace: s,
);
final rawAmount = recipient.amount.raw;
final floatAmount =
rawAmount / BigInt.from(10).pow(defaultDecimals);
return x_wallet.Transfer(
floatAmount: floatAmount.toDouble(),
strAddress: recipient.address,
assetHash: asset,
extraData: null,
);
}
}),
);
final decimals = await libXelisWallet!.getAssetDecimals(asset: asset);
final estimatedFee = double.parse(
await libXelisWallet!.estimateFees(transfers: transfers),
);
final rawFee = (estimatedFee * pow(10, decimals)).round();
return Amount(
rawValue: BigInt.from(rawFee),
fractionDigits: cryptoCurrency.fractionDigits,
);
} catch (e, s) {
Logging.instance.d(
"Fee estimation failed. Using fallback fee: $defaultFee",
error: e,
stackTrace: s,
);
return Amount(
rawValue: defaultFee,
fractionDigits: cryptoCurrency.fractionDigits,
);
}
} catch (_) {
// Logging.instance.log(
// "Exception rethrown from estimateFeeFor(): $e\n$s",
// level: LogLevel.Error,
// );
rethrow;
}
}
@override
Future<TxData> confirmSend({required TxData txData}) async {
try {
checkInitialized();
// Validate recipients
if (txData.recipients == null || txData.recipients!.length != 1) {
throw Exception("$runtimeType confirmSend requires 1 recipient");
}
final recipient = txData.recipients!.first;
final Amount sendAmount = recipient.amount;
final asset =
(txData.otherData != null
? jsonDecode(txData.otherData!)
: null)?['asset']
as String? ??
xelis_sdk.xelisAsset;
final amt = double.parse(
await libXelisWallet!.formatCoin(
atomicAmount: sendAmount.raw,
assetHash: asset,
),
);
// Create a transfer transaction
final txJson = await libXelisWallet!.createTransfersTransaction(
transfers: [
x_wallet.Transfer(
floatAmount: amt,
strAddress: recipient.address,
assetHash: asset,
extraData: null, // Add extra data if needed
),
],
);
final txMap = jsonDecode(txJson);
final txHash = txMap['hash'] as String;
// Broadcast the transaction
await libXelisWallet!.broadcastTransaction(txHash: txHash);
return await updateSentCachedTxData(
txData: txData.copyWith(txid: txHash),
);
} catch (_) {
// Logging.instance.log(
// "Exception rethrown from confirmSend(): $e\n$s",
// level: LogLevel.Error,
// );
rethrow;
}
}
@override
Future<void> handleEvent(Event event) async {
try {
switch (event) {
case NewTopoheight(:final height):
await handleNewTopoHeight(height);
case NewAsset(:final asset):
await handleNewAsset(asset);
case NewTransaction(:final transaction):
await handleNewTransaction(transaction);
case BalanceChanged(:final event):
await handleBalanceChanged(event);
case Rescan(:final startTopoheight):
await handleRescan(startTopoheight);
case Online():
await handleOnline();
case Offline():
await handleOffline();
case HistorySynced(:final topoheight):
await handleHistorySynced(topoheight);
}
} catch (e, s) {
Logging.instance.e(
"Error in $runtimeType handleEvent($event)",
error: e,
stackTrace: s,
);
}
}
@override
Future<void> handleNewTopoHeight(int height) async {
await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar);
}
@override
Future<void> handleNewTransaction(xelis_sdk.TransactionEntry tx) async {
try {
final txListJson = [jsonEncode(tx.toString())];
final newTxIds = await updateTransactions(
isRescan: false,
rawTransactions: txListJson,
);
await updateBalance();
// Logging.instance.log(
// "New transaction processed: ${newTxIds.first}",
// level: LogLevel.Info,
// );
} catch (e, s) {
Logging.instance.e(
"Error in $runtimeType handleNewTransaction($tx)",
error: e,
stackTrace: s,
);
}
}
@override
Future<void> handleBalanceChanged(xelis_sdk.BalanceChangedEvent event) async {
try {
final asset = event.assetHash;
if (asset == xelis_sdk.xelisAsset) {
await updateBalance(newBalance: event.balance);
}
// TODO: Update asset balances if needed
} catch (e, s) {
Logging.instance.e(
"Error in $runtimeType handleBalanceChanged($event)",
error: e,
stackTrace: s,
);
}
}
@override
Future<void> handleRescan(int startTopoheight) async {
await refreshMutex.protect(() async {
await mainDB.deleteWalletBlockchainData(walletId);
await updateTransactions(isRescan: true, topoheight: startTopoheight);
await updateBalance();
});
}
@override
Future<void> handleOnline() async {
await updateChainHeight();
await updateBalance();
await updateTransactions();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
info.coin,
),
);
unawaited(refresh());
}
@override
Future<void> handleOffline() async {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
info.coin,
),
);
}
@override
Future<void> handleHistorySynced(int topoheight) async {
await updateChainHeight();
await updateBalance();
await updateTransactions();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
info.coin,
),
);
}
@override
Future<void> handleNewAsset(xelis_sdk.AssetData asset) async {
// TODO: Store asset information if needed
// TODO: Update UI/state for new asset
Logging.instance.d("New xelis asset detected: $asset");
}
@override
Future<void> refresh({int? topoheight}) async {
await refreshMutex.protect(() async {
try {
final bool online = await libXelisWallet!.isOnline();
if (online == true) {
await updateChainHeight(topoheight: topoheight);
await updateBalance();
await updateTransactions();
} else {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
info.coin,
),
);
}
} catch (e, s) {
Logging.instance.e(
"Error in $runtimeType refresh()",
error: e,
stackTrace: s,
);
}
});
}
}

View file

@ -2,8 +2,9 @@ import '../../crypto_currency/intermediate/cryptonote_currency.dart';
import '../wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/mnemonic_interface.dart';
import 'external_wallet.dart';
abstract class CryptonoteWallet<T extends CryptonoteCurrency> extends Wallet<T>
abstract class CryptonoteWallet<T extends CryptonoteCurrency> extends ExternalWallet<T>
with MnemonicInterface<T>, CoinControlInterface<T> {
CryptonoteWallet(super.currency);
}

View file

@ -0,0 +1,12 @@
import '../../crypto_currency/crypto_currency.dart';
import '../wallet.dart';
// anstract class to be fleshed out for the standardization of wallet implementations
// that rely on bridged code libraries outside, or external native wallet functions
abstract class ExternalWallet<T extends CryptoCurrency> extends Wallet<T> {
ExternalWallet(super.currency);
// wallet opening and initialization separated to prevent db lock collision errors
// must be overridden
Future<void> open();
}

View file

@ -190,6 +190,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
}
}
@override
Future<void> open() async {
bool wasNull = false;

View file

@ -0,0 +1,438 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
import 'package:xelis_flutter/src/api/network.dart' as x_network;
import 'package:xelis_flutter/src/api/wallet.dart' as x_wallet;
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/stack_file_system.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/intermediate/electrum_currency.dart';
import '../wallet_mixin_interfaces/mnemonic_interface.dart';
import 'external_wallet.dart';
enum XelisTableSize {
low,
full;
bool get isLow => this == XelisTableSize.low;
static XelisTableSize get platformDefault {
if (kIsWeb) {
return XelisTableSize.low;
}
return XelisTableSize.full;
}
}
class XelisTableState {
final bool isGenerating;
final XelisTableSize currentSize;
final XelisTableSize _desiredSize;
XelisTableSize get desiredSize {
if (kIsWeb) {
return XelisTableSize.low;
}
return _desiredSize;
}
const XelisTableState({
this.isGenerating = false,
this.currentSize = XelisTableSize.low,
XelisTableSize desiredSize = XelisTableSize.full,
}) : _desiredSize = desiredSize;
XelisTableState copyWith({
bool? isGenerating,
XelisTableSize? currentSize,
XelisTableSize? desiredSize,
}) {
return XelisTableState(
isGenerating: isGenerating ?? this.isGenerating,
currentSize: currentSize ?? this.currentSize,
desiredSize: kIsWeb ? XelisTableSize.low : (desiredSize ?? _desiredSize),
);
}
factory XelisTableState.fromJson(Map<String, dynamic> json) {
return XelisTableState(
isGenerating: json['isGenerating'] as bool,
currentSize: XelisTableSize.values[json['currentSize'] as int],
desiredSize: XelisTableSize.values[json['desiredSize'] as int],
);
}
Map<String, dynamic> toJson() => {
'isGenerating': isGenerating,
'currentSize': currentSize.index,
'desiredSize': _desiredSize.index,
};
}
extension XelisNetworkConversion on CryptoCurrencyNetwork {
x_network.Network get xelisNetwork {
switch (this) {
case CryptoCurrencyNetwork.main:
return x_network.Network.mainnet;
case CryptoCurrencyNetwork.test:
return x_network.Network.testnet;
default:
throw ArgumentError('Unsupported network type for Xelis: $this');
}
}
}
extension CryptoCurrencyNetworkConversion on x_network.Network {
CryptoCurrencyNetwork get cryptoCurrencyNetwork {
switch (this) {
case x_network.Network.mainnet:
return CryptoCurrencyNetwork.main;
case x_network.Network.testnet:
return CryptoCurrencyNetwork.test;
default:
throw ArgumentError('Unsupported Xelis network type: $this');
}
}
}
sealed class Event {
const Event();
}
final class NewTopoheight extends Event {
final int height;
const NewTopoheight(this.height);
}
final class NewAsset extends Event {
final xelis_sdk.AssetData asset;
const NewAsset(this.asset);
}
final class NewTransaction extends Event {
final xelis_sdk.TransactionEntry transaction;
const NewTransaction(this.transaction);
}
final class BalanceChanged extends Event {
final xelis_sdk.BalanceChangedEvent event;
const BalanceChanged(this.event);
}
final class Rescan extends Event {
final int startTopoheight;
const Rescan(this.startTopoheight);
}
final class Online extends Event {
const Online();
}
final class Offline extends Event {
const Offline();
}
final class HistorySynced extends Event {
final int topoheight;
const HistorySynced(this.topoheight);
}
abstract class LibXelisWallet<T extends ElectrumCurrency>
extends ExternalWallet<T>
with MnemonicInterface {
LibXelisWallet(super.currency);
static const String _kHasFullTablesKey = 'xelis_has_full_tables';
static const String _kGeneratingTablesKey = 'xelis_generating_tables';
static const String _kWantsFullTablesKey = 'xelis_wants_full_tables';
static final _tableGenerationMutex = Mutex();
static Completer<void>? _tableGenerationCompleter;
x_wallet.XelisWallet? libXelisWallet;
int pruningHeight = 0;
x_wallet.XelisWallet? get wallet => libXelisWallet;
set wallet(x_wallet.XelisWallet? newWallet) {
if (newWallet == null && libXelisWallet != null) {
throw StateError('Cannot set wallet to null after initialization');
}
libXelisWallet = newWallet;
}
void checkInitialized() {
if (libXelisWallet == null) {
throw StateError('libXelisWallet not initialized');
}
}
final syncMutex = Mutex();
Timer? timer;
StreamSubscription<void>? _eventSubscription;
Future<String> getPrecomputedTablesPath() async {
if (kIsWeb) {
return "";
} else {
final appDir = await StackFileSystem.applicationXelisTableDirectory();
return "${appDir.path}${Platform.pathSeparator}";
}
}
Future<XelisTableState> getTableState() async {
final hasFullTables =
await secureStorageInterface.read(key: _kHasFullTablesKey) == 'true';
final isGenerating =
await secureStorageInterface.read(key: _kGeneratingTablesKey) == 'true';
final wantsFull =
await secureStorageInterface.read(key: _kWantsFullTablesKey) != 'false';
return XelisTableState(
isGenerating: isGenerating,
currentSize: hasFullTables ? XelisTableSize.full : XelisTableSize.low,
desiredSize: wantsFull ? XelisTableSize.full : XelisTableSize.low,
);
}
Future<void> setTableState(XelisTableState state) async {
await secureStorageInterface.write(
key: _kHasFullTablesKey,
value: state.currentSize == XelisTableSize.full ? 'true' : 'false',
);
await secureStorageInterface.write(
key: _kGeneratingTablesKey,
value: state.isGenerating ? 'true' : 'false',
);
await secureStorageInterface.write(
key: _kWantsFullTablesKey,
value: state.desiredSize == XelisTableSize.full ? 'true' : 'false',
);
}
Stream<Event> convertRawEvents() async* {
checkInitialized();
final rawEventStream = libXelisWallet!.eventsStream();
await for (final rawData in rawEventStream) {
final json = jsonDecode(rawData);
try {
final eventType = xelis_sdk.WalletEvent.fromStr(
json['event'] as String,
);
switch (eventType) {
case xelis_sdk.WalletEvent.newTopoHeight:
yield NewTopoheight(json['data']['topoheight'] as int);
case xelis_sdk.WalletEvent.newAsset:
yield NewAsset(
xelis_sdk.AssetData.fromJson(
json['data'] as Map<String, dynamic>,
),
);
case xelis_sdk.WalletEvent.newTransaction:
yield NewTransaction(
xelis_sdk.TransactionEntry.fromJson(
json['data'] as Map<String, dynamic>,
),
);
case xelis_sdk.WalletEvent.balanceChanged:
yield BalanceChanged(
xelis_sdk.BalanceChangedEvent.fromJson(
json['data'] as Map<String, dynamic>,
),
);
case xelis_sdk.WalletEvent.rescan:
yield Rescan(json['data']['start_topoheight'] as int);
case xelis_sdk.WalletEvent.online:
yield const Online();
case xelis_sdk.WalletEvent.offline:
yield const Offline();
case xelis_sdk.WalletEvent.historySynced:
yield HistorySynced(json['data']['topoheight'] as int);
}
} catch (e, s) {
Logging.instance.e(
"Error processing xelis wallet event: $rawData",
error: e,
stackTrace: s,
);
continue;
}
}
}
Future<void> handleEvent(Event event) async {}
Future<void> handleNewTopoHeight(int height);
Future<void> handleNewTransaction(xelis_sdk.TransactionEntry tx);
Future<void> handleBalanceChanged(xelis_sdk.BalanceChangedEvent event);
Future<void> handleRescan(int startTopoheight) async {}
Future<void> handleOnline() async {}
Future<void> handleOffline() async {}
Future<void> handleHistorySynced(int topoheight) async {}
Future<void> handleNewAsset(xelis_sdk.AssetData asset) async {}
@override
Future<void> refresh({int? topoheight});
Future<void> connect() async {
final node = getCurrentNode();
try {
_eventSubscription = convertRawEvents().listen(handleEvent);
Logging.instance.i("Connecting to node: ${node.host}:${node.port}");
await libXelisWallet!.onlineMode(
daemonAddress: "${node.host}:${node.port}",
);
await super.refresh();
} catch (e, s) {
Logging.instance.e(
"rethrowing error connecting to node: $node",
error: e,
stackTrace: s,
);
rethrow;
}
}
List<FilterOperation> get standardReceivingAddressFilters => [
FilterCondition.equalTo(property: r"type", value: info.mainAddressType),
const FilterCondition.equalTo(
property: r"subType",
value: AddressSubType.receiving,
),
];
List<FilterOperation> get standardChangeAddressFilters => [
FilterCondition.equalTo(property: r"type", value: info.mainAddressType),
const FilterCondition.equalTo(
property: r"subType",
value: AddressSubType.change,
),
];
static Future<bool> checkWalletExists(String walletId) async {
final xelisDir = await StackFileSystem.applicationXelisDirectory();
final walletDir = Directory(
"${xelisDir.path}${Platform.pathSeparator}$walletId",
);
// TODO: should we check for certain files within the dir?
return await walletDir.exists();
}
@override
Future<void> open() async {
try {
await connect();
} catch (e) {
// Logging.instance.log(
// "Failed to start sync: $e",
// level: LogLevel.Error,
// );
rethrow;
}
unawaited(refresh());
}
@override
Future<void> exit() async {
await refreshMutex.protect(() async {
timer?.cancel();
timer = null;
await _eventSubscription?.cancel();
_eventSubscription = null;
await libXelisWallet?.offlineMode();
await super.exit();
});
}
void invalidSeedLengthCheck(int length) {
if (!(length == 25)) {
throw Exception("Invalid Xelis mnemonic length found: $length");
}
}
}
extension XelisTableManagement on LibXelisWallet {
Future<bool> isTableUpgradeAvailable() async {
if (kIsWeb) return false;
final state = await getTableState();
return state.currentSize != state.desiredSize;
}
Future<void> updateTablesToDesiredSize() async {
if (kIsWeb) return;
await Future<void>.delayed(const Duration(seconds: 1));
if (LibXelisWallet._tableGenerationCompleter != null) {
try {
await LibXelisWallet._tableGenerationCompleter!.future;
return;
} catch (_) {
// Previous generation failed, we'll try again
}
}
await LibXelisWallet._tableGenerationMutex.protect(() async {
// Check again after acquiring mutex
if (LibXelisWallet._tableGenerationCompleter != null) {
try {
await LibXelisWallet._tableGenerationCompleter!.future;
return;
} catch (_) {
// Previous generation failed, we'll try again
}
}
final state = await getTableState();
if (state.currentSize == state.desiredSize) return;
LibXelisWallet._tableGenerationCompleter = Completer<void>();
await setTableState(state.copyWith(isGenerating: true));
try {
Logging.instance.i("Xelis: Generating large tables in background");
final tablePath = await getPrecomputedTablesPath();
await x_wallet.updateTables(
precomputedTablesPath: tablePath,
l1Low: state.desiredSize.isLow,
);
await setTableState(
XelisTableState(
isGenerating: false,
currentSize: state.desiredSize,
desiredSize: state.desiredSize,
),
);
Logging.instance.i("Xelis: Table upgrade done");
LibXelisWallet._tableGenerationCompleter!.complete();
} catch (e) {
// Logging.instance.log(
// "Failed to update tables: $e\n$s",
// level: LogLevel.Error,
// );
await setTableState(state.copyWith(isGenerating: false));
LibXelisWallet._tableGenerationCompleter!.completeError(e);
} finally {
if (!LibXelisWallet._tableGenerationCompleter!.isCompleted) {
LibXelisWallet._tableGenerationCompleter!.completeError(
Exception('Table generation abandoned'),
);
}
LibXelisWallet._tableGenerationCompleter = null;
}
});
}
}

View file

@ -47,6 +47,7 @@ import 'impl/stellar_wallet.dart';
import 'impl/sub_wallets/eth_token_wallet.dart';
import 'impl/tezos_wallet.dart';
import 'impl/wownero_wallet.dart';
import 'impl/xelis_wallet.dart';
import 'intermediate/cryptonote_wallet.dart';
import 'wallet_mixin_interfaces/electrumx_interface.dart';
import 'wallet_mixin_interfaces/lelantus_interface.dart';
@ -172,8 +173,8 @@ abstract class Wallet<T extends CryptoCurrency> {
value: viewOnlyData!.toJsonEncodedString(),
);
} else if (wallet is MnemonicInterface) {
if (wallet is CryptonoteWallet) {
// currently a special case due to the xmr/wow libraries handling their
if (wallet is CryptonoteWallet || wallet is XelisWallet) { //
// currently a special case due to the xmr/wow/xelis libraries handling their
// own mnemonic generation on new wallet creation
// if its a restore we must set them
if (mnemonic != null) {
@ -406,6 +407,9 @@ abstract class Wallet<T extends CryptoCurrency> {
case const (Wownero):
return WowneroWallet(net);
case const (Xelis):
return XelisWallet(net);
default:
// should never hit in reality
throw Exception("Unknown crypto currency: ${walletInfo.coin}");

View file

@ -28,7 +28,7 @@ import '../utilities/util.dart';
import '../wallets/isar/providers/eth/current_token_wallet_provider.dart';
import '../wallets/wallet/impl/ethereum_wallet.dart';
import '../wallets/wallet/impl/sub_wallets/eth_token_wallet.dart';
import '../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../wallets/wallet/intermediate/external_wallet.dart';
import '../wallets/wallet/wallet.dart';
import 'conditional_parent.dart';
import 'desktop/primary_button.dart';
@ -111,7 +111,7 @@ class SimpleWalletCard extends ConsumerWidget {
if (context.mounted) {
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
loadFuture = wallet.init().then((value) async => await (wallet).open());
} else {
loadFuture = wallet.init();

View file

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/text_styles.dart';
import '../../../widgets/progress_bar.dart';
import '../providers/providers.dart';
class XelisTableProgress extends ConsumerWidget {
const XelisTableProgress({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final progressAsyncValue = ref.watch(xelisTableProgressProvider);
return DefaultTextStyle(
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge?.color ?? Colors.black,
fontSize: 14,
),
child: Center(
child: progressAsyncValue.when(
data: (progress) => Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(12),
),
constraints: const BoxConstraints(maxWidth: 450),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Generating Precomputed Tables...",
style: STextStyles.desktopH3(context).copyWith(
fontSize: 24,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
"These tables are required for the fast decryption of private transactions. This is a one-time process upon the creation of your first Xelis wallet in Stack Wallet.",
style: STextStyles.subtitle600(context).copyWith(
fontSize: 14,
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
progress.currentStep.displayName,
style: STextStyles.titleBold12(context),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
ProgressBar(
width: 200,
height: 8,
fillColor: const Color.fromARGB(255,2,255,207),
backgroundColor: Theme.of(context).extension<StackColors>()!.textFieldDefaultBG,
percent: progress.tableProgress ?? 0.0,
),
const SizedBox(height: 4),
Text(
"${((progress.tableProgress ?? 0.0) * 100).toStringAsFixed(1)}%",
style: STextStyles.label(context),
),
],
),
),
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
),
);
}
}

View file

@ -21,6 +21,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_libsparkmobile
frostdart
tor_ffi_plugin
xelis_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)

View file

@ -40,7 +40,7 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- ReachabilitySwift (5.2.3)
- share_plus (0.0.1):
- FlutterMacOS
- "sqlite3 (3.46.0+1)":
@ -67,6 +67,8 @@ PODS:
- FlutterMacOS
- window_size (0.0.2):
- FlutterMacOS
- xelis_flutter (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- camera_macos (from `Flutter/ephemeral/.symlinks/plugins/camera_macos/macos`)
@ -95,6 +97,7 @@ DEPENDENCIES:
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
- xelis_flutter (from `Flutter/ephemeral/.symlinks/plugins/xelis_flutter/macos`)
SPEC REPOS:
trunk:
@ -154,6 +157,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
window_size:
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
xelis_flutter:
:path: Flutter/ephemeral/.symlinks/plugins/xelis_flutter/macos
SPEC CHECKSUMS:
camera_macos: c2603f5eed16f05076cf17e12030d2ce55a77839
@ -173,9 +178,9 @@ SPEC CHECKSUMS:
isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a
lelantus: 308e42c5a648598936a07a234471dd8cf8e687a0
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83
@ -184,6 +189,7 @@ SPEC CHECKSUMS:
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
xelis_flutter: 34e05f3621e46381fb1b10d7c11f63764d3f7a80
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367

View file

@ -50,10 +50,10 @@ packages:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.7.0"
async:
dependency: "direct main"
description:
@ -183,50 +183,58 @@ packages:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
url: "https://pub.dev"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.0.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev"
source: hosted
version: "2.4.13"
version: "2.4.15"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
source: hosted
version: "7.3.2"
version: "8.0.0"
built_collection:
dependency: transitive
description:
@ -239,10 +247,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
url: "https://pub.dev"
source: hosted
version: "8.9.2"
version: "8.9.5"
calendar_date_picker2:
dependency: "direct main"
description:
@ -404,10 +412,10 @@ packages:
dependency: transitive
description:
name: coverage
sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268"
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.11.1"
cross_file:
dependency: transitive
description:
@ -564,10 +572,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
url: "https://pub.dev"
source: hosted
version: "2.3.7"
version: "2.3.8"
dartx:
dependency: transitive
description:
@ -742,10 +750,10 @@ packages:
dependency: "direct main"
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
file:
dependency: transitive
description:
@ -823,9 +831,11 @@ packages:
flutter_libsparkmobile:
dependency: "direct main"
description:
path: "../flutter_libsparkmobile"
relative: true
source: path
path: "."
ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b
resolved-ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b
url: "https://github.com/cypherstack/flutter_libsparkmobile.git"
source: git
version: "0.0.2"
flutter_lints:
dependency: "direct dev"
@ -871,10 +881,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398"
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
url: "https://pub.dev"
source: hosted
version: "2.0.23"
version: "2.0.24"
flutter_riverpod:
dependency: "direct main"
description:
@ -883,6 +893,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
flutter_secure_storage:
dependency: "direct main"
description:
@ -935,10 +953,10 @@ packages:
dependency: "direct main"
description:
name: flutter_svg
sha256: "1b7723a814d84fb65869ea7115cdb3ee7c3be5a27a755c1ec60e049f6b9fcbb2"
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
url: "https://pub.dev"
source: hosted
version: "2.0.11"
version: "2.0.17"
flutter_test:
dependency: "direct dev"
description: flutter
@ -990,10 +1008,10 @@ packages:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
@ -1070,18 +1088,18 @@ packages:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.2"
ieee754:
dependency: transitive
description:
@ -1123,10 +1141,10 @@ packages:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.0.5"
isar:
dependency: "direct main"
description:
@ -1179,10 +1197,18 @@ packages:
dependency: transitive
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c
url: "https://pub.dev"
source: hosted
version: "6.8.0"
version: "6.9.0"
jsontool:
dependency: transitive
description:
name: jsontool
sha256: e49bf419e82d90f009426cd7fdec8d54ba8382975b3454ed16a3af3ee1d1b697
url: "https://pub.dev"
source: hosted
version: "2.1.0"
keyboard_dismisser:
dependency: "direct main"
description:
@ -1428,26 +1454,26 @@ packages:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
url: "https://pub.dev"
source: hosted
version: "8.1.0"
version: "8.1.3"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
path:
dependency: transitive
description:
@ -1460,10 +1486,10 @@ packages:
dependency: transitive
description:
name: path_parsing
sha256: caa17e8f0b386eb190dd5b6a3b71211c76375aa8b6ffb4465b5863d019bdb334
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
@ -1476,18 +1502,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
url: "https://pub.dev"
source: hosted
version: "2.2.12"
version: "2.2.16"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
@ -1636,18 +1662,18 @@ packages:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.5.0"
qr:
dependency: transitive
description:
@ -1732,10 +1758,10 @@ packages:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
@ -1756,10 +1782,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
@ -1778,10 +1804,10 @@ packages:
description:
path: "."
ref: master
resolved-ref: b1fa8ca505e7e488edb4c2859f0218d48b15dead
resolved-ref: e6232c53c1595469931ababa878759a067c02e94
url: "https://github.com/cypherstack/socks_socket.git"
source: git
version: "1.0.0"
version: "1.1.1"
solana:
dependency: "direct main"
description:
@ -1803,10 +1829,10 @@ packages:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
version: "1.3.5"
source_map_stack_trace:
dependency: transitive
description:
@ -1819,10 +1845,10 @@ packages:
dependency: transitive
description:
name: source_maps
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.12"
version: "0.10.13"
source_span:
dependency: transitive
description:
@ -1892,10 +1918,10 @@ packages:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -1981,10 +2007,10 @@ packages:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
tint:
dependency: transitive
description:
@ -2062,26 +2088,26 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
version: "6.3.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
@ -2094,18 +2120,18 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.4.0"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.1.4"
uuid:
dependency: "direct main"
description:
@ -2118,26 +2144,26 @@ packages:
dependency: transitive
description:
name: vector_graphics
sha256: "0b9149c6ddb013818075b072b9ddc1b89a5122fff1275d4648d297086b46c4f0"
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
url: "https://pub.dev"
source: hosted
version: "1.1.12"
version: "1.1.15"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.12"
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: f3b9b6e4591c11394d4be4806c63e72d3a41778547b2c1e2a8a04fadcfd7d173
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
url: "https://pub.dev"
source: hosted
version: "1.1.12"
version: "1.1.16"
vector_math:
dependency: transitive
description:
@ -2146,6 +2172,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
very_good_analysis:
dependency: transitive
description:
name: very_good_analysis
sha256: "62d2b86d183fb81b2edc22913d9f155d26eb5cf3855173adb1f59fac85035c63"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
vm_service:
dependency: transitive
description:
@ -2207,10 +2241,10 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
web:
dependency: "direct overridden"
description:
@ -2235,6 +2269,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.5"
web_socket_client:
dependency: transitive
description:
name: web_socket_client
sha256: "0ec5230852349191188c013112e4d2be03e3fc83dbe80139ead9bf3a136e53b5"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
webdriver:
dependency: transitive
description:
@ -2255,10 +2297,10 @@ packages:
dependency: "direct overridden"
description:
name: win32
sha256: "10169d3934549017f0ae278ccb07f828f9d6ea21573bab0fb77b0e1ef0fce454"
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev"
source: hosted
version: "5.7.2"
version: "5.10.1"
win32_registry:
dependency: transitive
description:
@ -2284,6 +2326,23 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xelis_dart_sdk:
dependency: transitive
description:
name: xelis_dart_sdk
sha256: "2a7f8ab4c30fad2fd824ba6ea4e83ac20c726b47c7aa4f1e713ef3971a3ec1f7"
url: "https://pub.dev"
source: hosted
version: "0.24.0"
xelis_flutter:
dependency: "direct main"
description:
path: "."
ref: "v0.1.0"
resolved-ref: c685c5d3550cca414ec30d4b61259761f129dda6
url: "https://github.com/Tritonn204/xelis_flutter_ffi.git"
source: git
version: "0.1.0"
xml:
dependency: transitive
description:
@ -2304,10 +2363,10 @@ packages:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
zxcvbn:
dependency: "direct main"
description:

View file

@ -73,6 +73,7 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Stellar(CryptoCurrencyNetwork.main),
Tezos(CryptoCurrencyNetwork.main),
Wownero(CryptoCurrencyNetwork.main),
Xelis(CryptoCurrencyNetwork.main),
Bitcoin(CryptoCurrencyNetwork.test),
Bitcoin(CryptoCurrencyNetwork.test4),
Bitcoincash(CryptoCurrencyNetwork.test),
@ -83,6 +84,7 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Litecoin(CryptoCurrencyNetwork.test),
Peercoin(CryptoCurrencyNetwork.test),
Stellar(CryptoCurrencyNetwork.test),
Xelis(CryptoCurrencyNetwork.test),
]);
final ({String from, String to}) _swapDefaults = (from: "BTC", to: "XMR");

View file

@ -30,10 +30,15 @@ dependencies:
frostdart:
path: ./crypto_plugins/frostdart
xelis_flutter:
git:
url: https://github.com/Tritonn204/xelis_flutter_ffi.git
ref: v0.1.0
flutter_libsparkmobile:
git:
url: https://github.com/cypherstack/flutter_libsparkmobile.git
ref: 28d0f6c8db56a7d14d6495e810b8705a5c438929
ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b
# cs_monero compat (unpublished)
compat:

View file

@ -7,6 +7,6 @@ git reset --hard
cmake -G "Visual Studio 17 2022" -A x64 -S . -B build
cd build
cmake --build .
if not exist "..\..\..\..\build\" mkdir "..\..\..\..\build\"
xcopy bin\Debug\libsecp256k1-2.dll "..\..\..\..\build\secp256k1.dll" /Y
if not exist "..\..\..\..\..\build\" mkdir "..\..\..\..\..\build\"
xcopy bin\Debug\libsecp256k1-2.dll "..\..\..\..\..\build\secp256k1.dll" /Y
cd ..\..\..\

View file

@ -30,7 +30,7 @@ void main() {
url: Uri.parse(
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids"
"=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,bitcoin-cash"
",namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos"
",namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos,xelis"
"&order=market_cap_desc&per_page=50"
"&page=1&sparkline=false"),
headers: {
@ -93,7 +93,10 @@ void main() {
'max_supply":null,"ath":0.00013848,"ath_change_percentage":-79.75864'
',"ath_date":"2021-12-11T08:39:41.129Z","atl":5.74028e-07,"atl_chang'
'e_percentage":4783.08078,"atl_date":"2020-03-13T16:55:01.177Z","roi'
'":null,"last_updated":"2022-08-22T16:38:32.826Z"}]'),
'":null,"last_updated":"2022-08-22T16:38:32.826Z"},{"id":"xelis","sy'
'mbol":"xel","name":"Xelis","image":"https://assets.coingecko.com/co'
'ins/images/37615/large/green_background_black_logo.png","current_pr'
'ice":0.00001234,"price_change_percentage_24h":5.67}]'),
200));
final priceAPI = PriceAPI(client);
@ -125,7 +128,8 @@ void main() {
'Coin.dogecoinTestNet: [0, 0.0], '
'Coin.firoTestNet: [0, 0.0], '
'Coin.litecoinTestNet: [0, 0.0], '
'Coin.stellarTestnet: [0, 0.0]'
'Coin.stellarTestnet: [0, 0.0], '
'Coin.xelis: [0.00001234, 5.67]'
'}',
);
verify(client.get(
@ -134,7 +138,7 @@ void main() {
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
",tezos"
",tezos,xelis"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false",
),
headers: {'Content-Type': 'application/json'})).called(1);
@ -151,7 +155,7 @@ void main() {
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&"
"ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
",tezos"
",tezos,xelis"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false"),
headers: {
'Content-Type': 'application/json'
@ -213,7 +217,10 @@ void main() {
'21000000.0,"max_supply":null,"ath":0.00013848,"ath_change_percentag'
'e":-79.75864,"ath_date":"2021-12-11T08:39:41.129Z","atl":5.74028e-0'
'7,"atl_change_percentage":4783.08078,"atl_date":"2020-03-13T16:55:01'
'.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"}]'),
'.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"},{"id":'
'"xelis","symbol":"xel","name":"Xelis","image":"https://assets.coing'
'ecko.com/coins/images/37615/large/green_background_black_logo.png",'
'"current_price":0.00001234,"price_change_percentage_24h":5.67}]'),
200));
final priceAPI = PriceAPI(client);
@ -247,7 +254,8 @@ void main() {
'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], '
'Coin.firoTestNet: [0, 0.0], '
'Coin.litecoinTestNet: [0, 0.0], '
'Coin.stellarTestnet: [0, 0.0]'
'Coin.stellarTestnet: [0, 0.0], '
'Coin.xelis: [0.00001234, 5.67]'
'}',
);
@ -258,7 +266,7 @@ void main() {
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids"
"=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
",tezos"
",tezos,xelis"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false"),
headers: {'Content-Type': 'application/json'})).called(1);
@ -274,7 +282,7 @@ void main() {
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
",tezos"
",tezos,xelis"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false"),
headers: {
'Content-Type': 'application/json'
@ -337,7 +345,9 @@ void main() {
'y":21000000.0,"max_supply":null,"ath":0.00013848,"ath_change_perce'
'ntage":-79.75864,"ath_date":"2021-12-11T08:39:41.129Z","atl":5.74'
'028e-07,"atl_change_percentage":4783.08078,"atl_date":"2020-03-13T'
'16:55:01.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"}]'),
'16:55:01.177Z","roi":null,"last_updated":"2022-08-22T16:38:32.826Z"'
'},{"id":"xelis","symbol":xel,"name":com/coins/images/37615/large/g'
'reen_background_black_logo.png,"image":"https://assets.coingecko'),
200));
final priceAPI = PriceAPI(client);
@ -368,7 +378,8 @@ void main() {
'Coin.dogecoinTestNet: [0, 0.0], '
'Coin.firoTestNet: [0, 0.0], '
'Coin.litecoinTestNet: [0, 0.0], '
'Coin.stellarTestnet: [0, 0.0]'
'Coin.stellarTestnet: [0, 0.0], '
'Coin.xelis: [0, 0.0]'
'}',
);
});
@ -382,7 +393,7 @@ void main() {
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
",tezos"
",tezos,xelis"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false"),
headers: {
'Content-Type': 'application/json'
@ -418,7 +429,8 @@ void main() {
'Coin.dogecoinTestNet: [0, 0.0], '
'Coin.firoTestNet: [0, 0.0], '
'Coin.litecoinTestNet: [0, 0.0], '
'Coin.stellarTestnet: [0, 0.0]'
'Coin.stellarTestnet: [0, 0.0], '
'Coin.xelis: [0, 0.0]'
'}',
);
});

View file

@ -24,6 +24,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_libsparkmobile
frostdart
tor_ffi_plugin
xelis_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)