Merge pull request from Tritonn204/main

Initial Xelis integration
This commit is contained in:
julian-CStack 2025-03-17 08:08:40 -06:00 committed by GitHub
commit 6b33aee103
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 2088 additions and 125 deletions

5
.gitignore vendored
View file

@ -70,6 +70,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
@ -106,3 +110,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
@ -215,34 +220,35 @@ SPEC CHECKSUMS:
devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_libepiccash: 36241aa7d3126f6521529985ccb3dc5eaf7bb317
flutter_libsparkmobile: 6373955cc3327a926d17059e7405dde2fb12f99f
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_libepiccash: 7d480d11241faea8aefdcb96991319c5e26d4735
flutter_libsparkmobile: e2fcf99dafae4f5d28bf15468bad6d2ba9403c64
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: 9e672d3818957718ee006a491730c09deeecace9
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
frostdart: 4c72b69ccac2f13ede744107db046a125acce597
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
lelantus: 417f0221260013dfc052cae9cf4b741b6479edba
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
lelantus: a5bdee9de3d78dedd1fffda1d92ea3c67106881f
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
share_plus: de6030e33b4e106470e09322d87cf2a4258d2d1d
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31
stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03
SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
tor_ffi_plugin: d80e291b649379c8176e1be739e49be007d4ef93
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
tor_ffi_plugin: 2a4dd758515e5124dce8323f950f6bb135e611b5
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: fd58c82b1388f4afe3fe8aa2c856503a262a5b03
xelis_flutter: 63a1007da6fd27faa9f451bb9c2f7aa165c01cbe
PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View file

@ -48,6 +48,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View file

@ -74,15 +74,31 @@ import 'wallets/isar/providers/all_wallets_info_provider.dart';
import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'widgets/crypto_notifications.dart';
import 'package:xelis_flutter/src/frb_generated.dart' as xelis_rust;
import 'package:xelis_flutter/src/api/api.dart' as xelis_api;
final openedFromSWBFileStringStateProvider =
StateProvider<String?>((ref) => null);
void startListeningToRustLogs() {
xelis_api.createLogStream().listen((logEntry) {
Logging.instance.i("[Rust Log] [${logEntry.level}] ${logEntry.tag}: ${logEntry.msg}");
}, onError: (e) {
Logging.instance.e("Error receiving 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();
await xelis_api.setUpRustLogger();
startListeningToRustLogs();
if (Util.isDesktop && args.length == 2 && args.first == "-d") {
StackFileSystem.setDesktopOverrideDir(args.last);
}

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';
@ -49,6 +51,7 @@ 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/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,6 +381,10 @@ 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();
}

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

@ -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

@ -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

@ -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

@ -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,25 @@ Future<bool> testNodeConnection({
testPassed = false;
}
break;
case Xelis():
try {
final daemon = xelis_sdk.DaemonClient(
endPoint: "${formData.host!}:${formData.port!}",
secureWebSocket: formData.useSSL ?? false,
);
daemon.connect();
final xelis_sdk.GetInfoResult networkInfo = await daemon.getInfo();
testPassed = networkInfo.height != null;
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,880 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:xelis_flutter/src/api/network.dart' as x_network;
import 'package:xelis_flutter/src/api/wallet.dart' as x_wallet;
import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import '../intermediate/lib_xelis_wallet.dart';
import '../../../utilities/stack_file_system.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 '../../../services/event_bus/events/global/blocks_remaining_event.dart';
import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart';
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../../../services/event_bus/events/global/tor_status_changed_event.dart';
import '../../../services/event_bus/events/global/updated_in_background_event.dart';
import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import '../../../services/event_bus/global_event_bus.dart';
import '../../../models/node_model.dart';
import '../../../models/paymint/fee_object_model.dart';
import '../../../models/balance.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import '../wallet.dart';
import '../../../providers/providers.dart';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stack_wallet_backup/generate_password.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../intermediate/lib_xelis_wallet.dart';
import 'dart:math';
class XelisWallet extends LibXelisWallet {
XelisWallet(CryptoCurrencyNetwork network) : super(Xelis(network));
// ==================== Overrides ============================================
@override
int get isarTransactionVersion => 2;
@override
Future<void> init({bool? isRestore}) async {
debugPrint("Xelis: init");
if (isRestore == true) {
await _restoreWallet();
return await super.init();
}
String? walletExists =
await secureStorageInterface.read(key: "${walletId}_wallet");
if (walletExists == null) {
await _createNewWallet();
}
await open();
return await super.init();
}
Future<void> _createNewWallet() async {
final String password = generatePassword();
debugPrint("Xelis: storing password");
await secureStorageInterface.write(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
value: password,
);
await secureStorageInterface.write(
key: '${walletId}_wallet',
value: 'true',
);
await secureStorageInterface.write(
key: '_${walletId}_needs_creation',
value: 'true',
);
}
Future<void> _restoreWallet() async {
final String password = generatePassword();
await secureStorageInterface.write(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
value: password,
);
await secureStorageInterface.write(
key: '${walletId}_wallet',
value: 'true',
);
await secureStorageInterface.write(
key: '_${walletId}_needs_restoration',
value: 'true',
);
if (libXelisWallet != null) {
await super.exit();
}
}
@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.log(
// "Exception rethrown from recoverFromMnemonic(): $e\n$s",
// level: LogLevel.Error,
// );
rethrow;
}
}
@override
Future<bool> pingCheck() async {
checkInitialized();
try {
final nodeInfo = await libXelisWallet!.getDaemonInfo();
await handleOnline();
return true;
} catch (_) {
return false;
await handleOffline();
}
}
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.log(
// "Error in updateBalance(): $e\n$s",
// level: LogLevel.Warning,
// );
}
});
}
Future<int> _fetchChainHeight() async {
final infoString = await libXelisWallet!.getDaemonInfo();
final Map<String, dynamic> nodeInfo = json.decode(infoString);
pruningHeight = int.parse(nodeInfo['pruned_topoheight'].toString());
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.log(
// "Error in updateChainHeight(): $e\n$s",
// level: LogLevel.Warning,
// );
}
}
@override
Future<void> updateNode() async {
try {
final node = getCurrentNode();
final bool online = await libXelisWallet!.isOnline();
if (online == true) {
await libXelisWallet!.offlineMode();
}
await super.connect();
} catch (e, s) {
// Logging.instance.log(
// "Error updating node: $e\n$s",
// level: LogLevel.Error,
// );
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));
// 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;
TransactionSubType txSubType = TransactionSubType.none;
int? nonce;
Amount fee = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits
);
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.toString();
if (transfer.extraData != null) {
otherData['extraData_${transfer.asset}'] = transfer.extraData!.toJson();
}
}
} else {
// Skip unknown entry types
}
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,
if (fee != null) 'overrideFee': fee.toJsonString(),
}),
);
// Logging.instance.log(
// "Entry done ${entryType.toString()}",
// level: LogLevel.Debug,
// );
txns.add(txn);
} catch (e, s) {
// Logging.instance.log(
// "Error handling tx $jsonString: $e\n$s",
// level: LogLevel.Warning,
// );
}
}
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();
// Use default address if recipients list is empty
final recipients = txData.recipients?.isNotEmpty == true
? txData.recipients!
: [(
address: 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m',
amount: Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: false
)];
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 (e, s) {
// 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 = 8;
final defaultFee = BigInt.from(0);
// Use default address if recipients list is empty
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) {
// Handle formatCoin error - use default conversion
debugPrint("formatCoin failed: $e, using fallback conversion");
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) {
debugPrint("Fee estimation failed: $e\n$s");
debugPrint("Using fallback fee: $defaultFee");
return Amount(
rawValue: defaultFee,
fractionDigits: cryptoCurrency.fractionDigits,
);
}
} catch (e, s) {
// 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'] ?? 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 (e, s) {
// 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.log(
// "Error handling wallet event: $e\n$s",
// level: LogLevel.Error,
// );
}
}
@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.log(
// "Error handling new transaction: $e\n$s",
// level: LogLevel.Warning,
// );
}
}
@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.log(
// "Error handling balance change: $e\n$s",
// level: LogLevel.Warning,
// );
}
}
@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 {
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.log(
// "New asset detected: ${asset}",
// level: LogLevel.Info,
// );
}
@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.log(
// "Error in refresh(): $e\n$s",
// level: LogLevel.Warning,
// );
}
});
}
}

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,576 @@
import 'package:isar/isar.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../crypto_currency/intermediate/electrum_currency.dart';
import '../wallet.dart';
import '../wallet_mixin_interfaces/mnemonic_interface.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:xelis_flutter/src/api/network.dart' as x_network;
import 'package:xelis_flutter/src/api/wallet.dart' as x_wallet;
import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import '../../../utilities/stack_file_system.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/node_model.dart';
import '../../../models/paymint/fee_object_model.dart';
import '../../../models/balance.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stack_wallet_backup/generate_password.dart';
import 'package:web_socket_channel/web_socket_channel.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 ?? this._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 bool _isAnyWalletGeneratingTables = false;
static final _initMutex = Mutex();
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();
NodeModel? _xelisNode;
Timer? timer;
String? tablePath;
StreamSubscription<void>? _eventSubscription;
Future<String> getPrecomputedTablesPath() async {
if (kIsWeb) {
return "";
} else {
final appDir = await getApplicationSupportDirectory();
return "${appDir.path}/";
}
}
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.log(
// "Error processing wallet event: $e\n$s",
// level: LogLevel.Error,
// );
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 {}
Future<void> refresh({int? topoheight});
Future<void> connect() async {
try {
_eventSubscription =
convertRawEvents().listen(handleEvent);
final node = getCurrentNode();
// Logging.instance.log(
// "Connecting to node: ${node.host}:${node.port}",
// level: LogLevel.Info,
// );
await libXelisWallet!.onlineMode(
daemonAddress: "${node.host}:${node.port}"
);
await super.refresh();
} catch (e, s) {
// Logging.instance.log(
// "Error connecting to node: $e\n$s",
// level: LogLevel.Error,
// );
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,
),
];
@override
Future<void> open() async {
bool wasNull = false;
if (libXelisWallet == null) {
wasNull = true;
final tablePath = await getPrecomputedTablesPath();
final tableState = await getTableState();
final appDir = await getApplicationDocumentsDirectory();
final String name = walletId;
final String directory = appDir.path;
final password = await secureStorageInterface.read(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
);
await LibXelisWallet._initMutex.protect(() async {
try {
final needsCreation = await secureStorageInterface.read(
key: '_${walletId}_needs_creation',
);
final needsRestoration = await secureStorageInterface.read(
key: '_${walletId}_needs_restoration',
);
libXelisWallet = await syncMutex.protect(() async {
if (needsCreation == 'true') {
debugPrint("Xelis: creating new wallet");
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(),
);
await secureStorageInterface.delete(
key: '_${walletId}_needs_creation',
);
return wallet;
} else if (needsRestoration == 'true') {
final mnemonic = await getMnemonic();
final seedLength = mnemonic.trim().split(" ").length;
invalidSeedLengthCheck(seedLength);
debugPrint("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(),
);
await secureStorageInterface.delete(
key: '_${walletId}_needs_restoration',
);
return wallet;
} else {
debugPrint("Xelis: opening existing wallet");
return await x_wallet.openXelisWallet(
name: name,
directory: directory,
password: password!,
network: cryptoCurrency.network.xelisNetwork,
precomputedTablesPath: tablePath,
l1Low: tableState.currentSize.isLow,
);
}
});
} catch (e, s) {
// Logging.instance.log(
// "Failed to open/create wallet: $e\n$s",
// level: LogLevel.Error,
// );
rethrow;
}
});
debugPrint("Checking for upgradability");
if (await isTableUpgradeAvailable()) {
debugPrint("Generating large tables in background");
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,
);
}
if (wasNull) {
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 libXelisWallet?.close();
libXelisWallet?.dispose();
libXelisWallet = null;
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.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 {
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,
));
debugPrint("Table upgrade done");
LibXelisWallet._tableGenerationCompleter!.complete();
} catch (e, s) {
// 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

@ -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

@ -65,6 +65,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`)
@ -92,6 +94,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:
@ -149,6 +152,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
@ -170,7 +175,7 @@ SPEC CHECKSUMS:
package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
share_plus: 3c787998077d6b31e839225a282e9e27edf99274
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83
stack_wallet_backup: 6ebc60b1bdcf11cf1f1cbad9aa78332e1e15778c
@ -181,4 +186,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View file

@ -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: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
url: "https://pub.dev"
source: hosted
version: "2.4.13"
version: "2.4.14"
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: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
url: "https://pub.dev"
source: hosted
version: "8.9.2"
version: "8.9.3"
calendar_date_picker2:
dependency: "direct main"
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:
@ -873,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:
@ -885,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: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
flutter_secure_storage:
dependency: "direct main"
description:
@ -937,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
@ -992,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:
@ -1072,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:
@ -1125,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:
@ -1181,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:
@ -1430,26 +1454,26 @@ packages:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
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:
@ -1462,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:
@ -1478,18 +1502,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.2.12"
version: "2.2.15"
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:
@ -1638,18 +1662,18 @@ packages:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
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:
@ -1734,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:
@ -1758,10 +1782,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.0.1"
sky_engine:
dependency: transitive
description: flutter
@ -1780,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:
@ -1805,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:
@ -1894,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:
@ -1983,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:
@ -2064,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:
@ -2096,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:
@ -2120,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:
@ -2148,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:
@ -2209,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:
@ -2237,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:
@ -2257,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:
@ -2286,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: "966469f2660226c33a1de77d1c5efee5459a2d4e"
url: "https://github.com/Tritonn204/xelis_flutter_ffi.git"
source: git
version: "0.1.0"
xml:
dependency: transitive
description:
@ -2306,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,6 +30,11 @@ 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

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)