diff --git a/assets/images/litecoin.png b/assets/images/litecoin.png
new file mode 100644
index 000000000..17994bd47
Binary files /dev/null and b/assets/images/litecoin.png differ
diff --git a/assets/svg/coin_icons/Litecoin.svg b/assets/svg/coin_icons/Litecoin.svg
new file mode 100644
index 000000000..2b89ca50b
--- /dev/null
+++ b/assets/svg/coin_icons/Litecoin.svg
@@ -0,0 +1,11 @@
+
diff --git a/lib/main.dart b/lib/main.dart
index aa2155478..58a287b31 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -68,7 +68,7 @@ final openedFromSWBFileStringStateProvider =
void main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
GoogleFonts.config.allowRuntimeFetching = false;
- if(Platform.isIOS){
+ if (Platform.isIOS) {
Util.libraryPath = await getLibraryDirectory();
}
@@ -209,56 +209,59 @@ class _MaterialAppWithThemeState extends ConsumerState
bool didLoad = false;
Future load() async {
- if (didLoad) {
- return;
- }
- didLoad = true;
-
- await DB.instance.init();
- await _prefs.init();
-
- _notificationsService = ref.read(notificationsProvider);
- _nodeService = ref.read(nodeServiceChangeNotifierProvider);
- _tradesService = ref.read(tradesServiceProvider);
-
- NotificationApi.prefs = _prefs;
- NotificationApi.notificationsService = _notificationsService;
-
- unawaited(ref.read(baseCurrenciesProvider).update());
-
- await _nodeService.updateDefaults();
- await _notificationsService.init(
- nodeService: _nodeService,
- tradesService: _tradesService,
- prefs: _prefs,
- );
- ref.read(priceAnd24hChangeNotifierProvider).start(true);
- await _wallets.load(_prefs);
- loadingCompleter.complete();
- // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
- // unawaited(_nodeService.updateCommunityNodes());
-
- // run without awaiting
- if (Constants.enableExchange &&
- _prefs.externalCalls &&
- await _prefs.isExternalCallsSet()) {
- unawaited(ExchangeDataLoadingService().loadAll(ref));
- }
-
- if (_prefs.isAutoBackupEnabled) {
- switch (_prefs.backupFrequencyType) {
- case BackupFrequencyType.everyTenMinutes:
- ref
- .read(autoSWBServiceProvider)
- .startPeriodicBackupTimer(duration: const Duration(minutes: 10));
- break;
- case BackupFrequencyType.everyAppStart:
- unawaited(ref.read(autoSWBServiceProvider).doBackup());
- break;
- case BackupFrequencyType.afterClosingAWallet:
- // ignore this case here
- break;
+ try {
+ if (didLoad) {
+ return;
}
+ didLoad = true;
+
+ await DB.instance.init();
+ await _prefs.init();
+
+ _notificationsService = ref.read(notificationsProvider);
+ _nodeService = ref.read(nodeServiceChangeNotifierProvider);
+ _tradesService = ref.read(tradesServiceProvider);
+
+ NotificationApi.prefs = _prefs;
+ NotificationApi.notificationsService = _notificationsService;
+
+ unawaited(ref.read(baseCurrenciesProvider).update());
+
+ await _nodeService.updateDefaults();
+ await _notificationsService.init(
+ nodeService: _nodeService,
+ tradesService: _tradesService,
+ prefs: _prefs,
+ );
+ ref.read(priceAnd24hChangeNotifierProvider).start(true);
+ await _wallets.load(_prefs);
+ loadingCompleter.complete();
+ // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
+ // unawaited(_nodeService.updateCommunityNodes());
+
+ // run without awaiting
+ if (Constants.enableExchange &&
+ _prefs.externalCalls &&
+ await _prefs.isExternalCallsSet()) {
+ unawaited(ExchangeDataLoadingService().loadAll(ref));
+ }
+
+ if (_prefs.isAutoBackupEnabled) {
+ switch (_prefs.backupFrequencyType) {
+ case BackupFrequencyType.everyTenMinutes:
+ ref.read(autoSWBServiceProvider).startPeriodicBackupTimer(
+ duration: const Duration(minutes: 10));
+ break;
+ case BackupFrequencyType.everyAppStart:
+ unawaited(ref.read(autoSWBServiceProvider).doBackup());
+ break;
+ case BackupFrequencyType.afterClosingAWallet:
+ // ignore this case here
+ break;
+ }
+ }
+ } catch (e, s) {
+ Logger.print("$e $s", normalLength: false);
}
}
diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart
index 51c420c82..20fc81903 100644
--- a/lib/pages/exchange_view/send_from_view.dart
+++ b/lib/pages/exchange_view/send_from_view.dart
@@ -52,11 +52,13 @@ class _SendFromViewState extends ConsumerState {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoincash:
+ case Coin.litecoin:
case Coin.dogecoin:
case Coin.epicCash:
case Coin.firo:
case Coin.namecoin:
case Coin.bitcoinTestNet:
+ case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.dogecoinTestNet:
case Coin.firoTestNet:
diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
index 100c03e1b..77349f399 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
@@ -117,10 +117,12 @@ class _AddEditNodeViewState extends ConsumerState {
case Coin.bitcoin:
case Coin.bitcoincash:
+ case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.namecoin:
case Coin.bitcoinTestNet:
+ case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
@@ -531,11 +533,13 @@ class _NodeFormState extends ConsumerState {
// TODO: which coin servers can have username and password?
switch (coin) {
case Coin.bitcoin:
+ case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.namecoin:
case Coin.bitcoincash:
case Coin.bitcoinTestNet:
+ case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
index 8baddb700..c5f797e37 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
@@ -98,6 +98,7 @@ class _NodeDetailsViewState extends ConsumerState {
break;
case Coin.bitcoin:
+ case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.bitcoinTestNet:
@@ -105,6 +106,7 @@ class _NodeDetailsViewState extends ConsumerState {
case Coin.dogecoinTestNet:
case Coin.bitcoincash:
case Coin.namecoin:
+ case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
final client = ElectrumX(
host: node!.host,
diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart
index 089b20a33..c36fa9eee 100644
--- a/lib/services/coins/coin_service.dart
+++ b/lib/services/coins/coin_service.dart
@@ -15,6 +15,8 @@ import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/prefs.dart';
+import 'litecoin/litecoin_wallet.dart';
+
abstract class CoinServiceAPI {
CoinServiceAPI();
@@ -90,6 +92,26 @@ abstract class CoinServiceAPI {
tracker: tracker,
);
+ case Coin.litecoin:
+ return LitecoinWallet(
+ walletId: walletId,
+ walletName: walletName,
+ coin: coin,
+ client: client,
+ cachedClient: cachedClient,
+ tracker: tracker,
+ );
+
+ case Coin.litecoinTestNet:
+ return LitecoinWallet(
+ walletId: walletId,
+ walletName: walletName,
+ coin: coin,
+ client: client,
+ cachedClient: cachedClient,
+ tracker: tracker,
+ );
+
case Coin.bitcoinTestNet:
return BitcoinWallet(
walletId: walletId,
diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart
new file mode 100644
index 000000000..0ab3a92a8
--- /dev/null
+++ b/lib/services/coins/litecoin/litecoin_wallet.dart
@@ -0,0 +1,3828 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:bech32/bech32.dart';
+import 'package:bip32/bip32.dart' as bip32;
+import 'package:bip39/bip39.dart' as bip39;
+import 'package:bitcoindart/bitcoindart.dart';
+import 'package:bs58check/bs58check.dart' as bs58check;
+import 'package:crypto/crypto.dart';
+import 'package:decimal/decimal.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:http/http.dart';
+import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
+import 'package:stackwallet/electrumx_rpc/electrumx.dart';
+import 'package:stackwallet/hive/db.dart';
+import 'package:stackwallet/models/models.dart' as models;
+import 'package:stackwallet/models/paymint/fee_object_model.dart';
+import 'package:stackwallet/models/paymint/transactions_model.dart';
+import 'package:stackwallet/models/paymint/utxo_model.dart';
+import 'package:stackwallet/services/coins/coin_service.dart';
+import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
+import 'package:stackwallet/services/event_bus/global_event_bus.dart';
+import 'package:stackwallet/services/node_service.dart';
+import 'package:stackwallet/services/notifications_api.dart';
+import 'package:stackwallet/services/price.dart';
+import 'package:stackwallet/services/transaction_notification_tracker.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/default_nodes.dart';
+import 'package:stackwallet/utilities/enums/coin_enum.dart';
+import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
+import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
+import 'package:stackwallet/utilities/format.dart';
+import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/prefs.dart';
+import 'package:tuple/tuple.dart';
+import 'package:uuid/uuid.dart';
+
+const int MINIMUM_CONFIRMATIONS = 1;
+const int DUST_LIMIT = 294;
+
+const String GENESIS_HASH_MAINNET =
+ "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2";
+const String GENESIS_HASH_TESTNET =
+ "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
+
+enum DerivePathType { bip44, bip49, bip84 }
+
+bip32.BIP32 getBip32Node(
+ int chain,
+ int index,
+ String mnemonic,
+ NetworkType network,
+ DerivePathType derivePathType,
+) {
+ final root = getBip32Root(mnemonic, network);
+
+ final node = getBip32NodeFromRoot(chain, index, root, derivePathType);
+ return node;
+}
+
+/// wrapper for compute()
+bip32.BIP32 getBip32NodeWrapper(
+ Tuple5 args,
+) {
+ return getBip32Node(
+ args.item1,
+ args.item2,
+ args.item3,
+ args.item4,
+ args.item5,
+ );
+}
+
+bip32.BIP32 getBip32NodeFromRoot(
+ int chain,
+ int index,
+ bip32.BIP32 root,
+ DerivePathType derivePathType,
+) {
+ String coinType;
+ switch (root.network.wif) {
+ case 0xb0: // ltc mainnet wif
+ coinType = "2"; // ltc mainnet
+ break;
+ case 0xef: // ltc testnet wif
+ coinType = "1"; // ltc testnet
+ break;
+ default:
+ throw Exception("Invalid Litecoin network type used!");
+ }
+ switch (derivePathType) {
+ case DerivePathType.bip44:
+ return root.derivePath("m/44'/$coinType'/0'/$chain/$index");
+ case DerivePathType.bip49:
+ return root.derivePath("m/49'/$coinType'/0'/$chain/$index");
+ case DerivePathType.bip84:
+ return root.derivePath("m/84'/$coinType'/0'/$chain/$index");
+ default:
+ throw Exception("DerivePathType must not be null.");
+ }
+}
+
+/// wrapper for compute()
+bip32.BIP32 getBip32NodeFromRootWrapper(
+ Tuple4 args,
+) {
+ return getBip32NodeFromRoot(
+ args.item1,
+ args.item2,
+ args.item3,
+ args.item4,
+ );
+}
+
+bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) {
+ final seed = bip39.mnemonicToSeed(mnemonic);
+ final networkType = bip32.NetworkType(
+ wif: network.wif,
+ bip32: bip32.Bip32Type(
+ public: network.bip32.public,
+ private: network.bip32.private,
+ ),
+ );
+
+ final root = bip32.BIP32.fromSeed(seed, networkType);
+ return root;
+}
+
+/// wrapper for compute()
+bip32.BIP32 getBip32RootWrapper(Tuple2 args) {
+ return getBip32Root(args.item1, args.item2);
+}
+
+class LitecoinWallet extends CoinServiceAPI {
+ static const integrationTestFlag =
+ bool.fromEnvironment("IS_INTEGRATION_TEST");
+
+ final _prefs = Prefs.instance;
+
+ Timer? timer;
+ late Coin _coin;
+
+ late final TransactionNotificationTracker txTracker;
+
+ NetworkType get _network {
+ switch (coin) {
+ case Coin.litecoin:
+ return litecoin;
+ case Coin.litecoinTestNet:
+ return litecointestnet;
+ default:
+ throw Exception("Invalid network type!");
+ }
+ }
+
+ List outputsList = [];
+
+ @override
+ set isFavorite(bool markFavorite) {
+ DB.instance.put(
+ boxName: walletId, key: "isFavorite", value: markFavorite);
+ }
+
+ @override
+ bool get isFavorite {
+ try {
+ return DB.instance.get(boxName: walletId, key: "isFavorite")
+ as bool;
+ } catch (e, s) {
+ Logging.instance
+ .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
+ rethrow;
+ }
+ }
+
+ @override
+ Coin get coin => _coin;
+
+ @override
+ Future> get allOwnAddresses =>
+ _allOwnAddresses ??= _fetchAllOwnAddresses();
+ Future>? _allOwnAddresses;
+
+ Future? _utxoData;
+ Future get utxoData => _utxoData ??= _fetchUtxoData();
+
+ @override
+ Future> get unspentOutputs async =>
+ (await utxoData).unspentOutputArray;
+
+ @override
+ Future get availableBalance async {
+ final data = await utxoData;
+ return Format.satoshisToAmount(
+ data.satoshiBalance - data.satoshiBalanceUnconfirmed);
+ }
+
+ @override
+ Future get pendingBalance async {
+ final data = await utxoData;
+ return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed);
+ }
+
+ @override
+ Future get balanceMinusMaxFee async =>
+ (await availableBalance) -
+ (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin))
+ .toDecimal();
+
+ @override
+ Future get totalBalance async {
+ if (!isActive) {
+ final totalBalance = DB.instance
+ .get(boxName: walletId, key: 'totalBalance') as int?;
+ if (totalBalance == null) {
+ final data = await utxoData;
+ return Format.satoshisToAmount(data.satoshiBalance);
+ } else {
+ return Format.satoshisToAmount(totalBalance);
+ }
+ }
+ final data = await utxoData;
+ return Format.satoshisToAmount(data.satoshiBalance);
+ }
+
+ @override
+ Future get currentReceivingAddress => _currentReceivingAddress ??=
+ _getCurrentAddressForChain(0, DerivePathType.bip84);
+ Future? _currentReceivingAddress;
+
+ Future get currentLegacyReceivingAddress =>
+ _currentReceivingAddressP2PKH ??=
+ _getCurrentAddressForChain(0, DerivePathType.bip44);
+ Future? _currentReceivingAddressP2PKH;
+
+ Future get currentReceivingAddressP2SH =>
+ _currentReceivingAddressP2SH ??=
+ _getCurrentAddressForChain(0, DerivePathType.bip49);
+ Future? _currentReceivingAddressP2SH;
+
+ @override
+ Future exit() async {
+ _hasCalledExit = true;
+ timer?.cancel();
+ timer = null;
+ stopNetworkAlivePinging();
+ }
+
+ bool _hasCalledExit = false;
+
+ @override
+ bool get hasCalledExit => _hasCalledExit;
+
+ @override
+ Future get fees => _feeObject ??= _getFees();
+ Future? _feeObject;
+
+ @override
+ Future get maxFee async {
+ final fee = (await fees).fast as String;
+ final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin);
+ return satsFee.floor().toBigInt().toInt();
+ }
+
+ @override
+ Future> get mnemonic => _getMnemonicList();
+
+ Future get chainHeight async {
+ try {
+ final result = await _electrumXClient.getBlockHeadTip();
+ return result["height"] as int;
+ } catch (e, s) {
+ Logging.instance.log("Exception caught in chainHeight: $e\n$s",
+ level: LogLevel.Error);
+ return -1;
+ }
+ }
+
+ int get storedChainHeight {
+ final storedHeight = DB.instance
+ .get(boxName: walletId, key: "storedChainHeight") as int?;
+ return storedHeight ?? 0;
+ }
+
+ Future updateStoredChainHeight({required int newHeight}) async {
+ await DB.instance.put(
+ boxName: walletId, key: "storedChainHeight", value: newHeight);
+ }
+
+ DerivePathType addressType({required String address}) {
+ Uint8List? decodeBase58;
+ Segwit? decodeBech32;
+ try {
+ decodeBase58 = bs58check.decode(address);
+ } catch (err) {
+ // Base58check decode fail
+ }
+ if (decodeBase58 != null) {
+ if (decodeBase58[0] == _network.pubKeyHash) {
+ // P2PKH
+ return DerivePathType.bip44;
+ }
+ if (decodeBase58[0] == _network.scriptHash) {
+ // P2SH
+ return DerivePathType.bip49;
+ }
+ throw ArgumentError('Invalid version or Network mismatch');
+ } else {
+ try {
+ decodeBech32 = segwit.decode(address, _network.bech32!);
+ } catch (err) {
+ // Bech32 decode fail
+ }
+ if (_network.bech32 != decodeBech32!.hrp) {
+ throw ArgumentError('Invalid prefix or Network mismatch');
+ }
+ if (decodeBech32.version != 0) {
+ throw ArgumentError('Invalid address version');
+ }
+ // P2WPKH
+ return DerivePathType.bip84;
+ }
+ }
+
+ bool longMutex = false;
+
+ @override
+ Future recoverFromMnemonic({
+ required String mnemonic,
+ required int maxUnusedAddressGap,
+ required int maxNumberOfIndexesToCheck,
+ required int height,
+ }) async {
+ longMutex = true;
+ final start = DateTime.now();
+ try {
+ Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag",
+ level: LogLevel.Info);
+ if (!integrationTestFlag) {
+ final features = await electrumXClient.getServerFeatures();
+ Logging.instance.log("features: $features", level: LogLevel.Info);
+ switch (coin) {
+ case Coin.litecoin:
+ if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
+ throw Exception("genesis hash does not match main net!");
+ }
+ break;
+ case Coin.litecoinTestNet:
+ if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
+ throw Exception("genesis hash does not match test net!");
+ }
+ break;
+ default:
+ throw Exception(
+ "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}");
+ }
+ // if (_networkType == BasicNetworkType.main) {
+ // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
+ // throw Exception("genesis hash does not match main net!");
+ // }
+ // } else if (_networkType == BasicNetworkType.test) {
+ // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
+ // throw Exception("genesis hash does not match test net!");
+ // }
+ // }
+ }
+ // check to make sure we aren't overwriting a mnemonic
+ // this should never fail
+ if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) {
+ longMutex = false;
+ throw Exception("Attempted to overwrite mnemonic on restore!");
+ }
+ await _secureStore.write(
+ key: '${_walletId}_mnemonic', value: mnemonic.trim());
+ await _recoverWalletFromBIP32SeedPhrase(
+ mnemonic: mnemonic.trim(),
+ maxUnusedAddressGap: maxUnusedAddressGap,
+ maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
+ );
+ } catch (e, s) {
+ Logging.instance.log(
+ "Exception rethrown from recoverFromMnemonic(): $e\n$s",
+ level: LogLevel.Error);
+ longMutex = false;
+ rethrow;
+ }
+ longMutex = false;
+
+ final end = DateTime.now();
+ Logging.instance.log(
+ "$walletName recovery time: ${end.difference(start).inMilliseconds} millis",
+ level: LogLevel.Info);
+ }
+
+ Future