diff --git a/asset_sources/default_themes/stack_duo/dark.zip b/asset_sources/default_themes/stack_duo/dark.zip index c6d417500..1e5f6136e 100644 Binary files a/asset_sources/default_themes/stack_duo/dark.zip and b/asset_sources/default_themes/stack_duo/dark.zip differ diff --git a/asset_sources/default_themes/stack_duo/light.zip b/asset_sources/default_themes/stack_duo/light.zip index d94ce2ac8..bc7a95771 100644 Binary files a/asset_sources/default_themes/stack_duo/light.zip and b/asset_sources/default_themes/stack_duo/light.zip differ diff --git a/asset_sources/default_themes/stack_wallet/dark.zip b/asset_sources/default_themes/stack_wallet/dark.zip index c6d417500..1e5f6136e 100644 Binary files a/asset_sources/default_themes/stack_wallet/dark.zip and b/asset_sources/default_themes/stack_wallet/dark.zip differ diff --git a/asset_sources/default_themes/stack_wallet/light.zip b/asset_sources/default_themes/stack_wallet/light.zip index d94ce2ac8..bc7a95771 100644 Binary files a/asset_sources/default_themes/stack_wallet/light.zip and b/asset_sources/default_themes/stack_wallet/light.zip differ diff --git a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart index 710cbfa1c..c16792d11 100644 --- a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart @@ -13,6 +13,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lottie/lottie.dart'; + import '../../../themes/coin_image_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; @@ -64,6 +65,7 @@ class _RestoringDialogState extends ConsumerState<SendingTransactionDialog> { if (Util.isDesktop) { return DesktopDialog( + maxHeight: assetPath.endsWith(".gif") ? double.infinity : null, child: Padding( padding: const EdgeInsets.all(40), child: Column( @@ -77,8 +79,10 @@ class _RestoringDialogState extends ConsumerState<SendingTransactionDialog> { height: 40, ), assetPath.endsWith(".gif") - ? Image.file( - File(assetPath), + ? Flexible( + child: Image.file( + File(assetPath), + ), ) : ProgressAndSuccess( controller: _progressAndSuccessController!, diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index 18c8a6a12..2f85dc445 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -48,18 +48,17 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { final _searchFocusNode = FocusNode(); void onTap(int index) { + if (currenciesWithoutSelected[index] == current || current.isEmpty) { + // ignore if already selected currency + return; + } + current = currenciesWithoutSelected[index]; + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + if (Util.isDesktop) { - setState(() { - current = currenciesWithoutSelected[index]; - }); + setState(() {}); } else { - if (currenciesWithoutSelected[index] == current || current.isEmpty) { - // ignore if already selected currency - return; - } - current = currenciesWithoutSelected[index]; - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); ref.read(prefsChangeNotifierProvider).currency = current; if (ref.read(prefsChangeNotifierProvider).externalCalls) { @@ -104,13 +103,7 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { void initState() { _searchController = TextEditingController(); if (Util.isDesktop) { - currenciesWithoutSelected = - ref.read(baseCurrenciesProvider).map.keys.toList(); current = ref.read(prefsChangeNotifierProvider).currency; - if (current.isNotEmpty) { - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - } } super.initState(); } @@ -129,16 +122,16 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { if (!isDesktop) { current = ref .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + } - currenciesWithoutSelected = ref - .watch(baseCurrenciesProvider.select((value) => value.map)) - .keys - .toList(); + currenciesWithoutSelected = ref + .watch(baseCurrenciesProvider.select((value) => value.map)) + .keys + .toList(); - if (current.isNotEmpty) { - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - } + if (current.isNotEmpty) { + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); } currenciesWithoutSelected = _filtered(); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart index 26f54fc27..1cacd431e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart @@ -13,8 +13,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'add_edit_node_view.dart'; -import '../../sub_widgets/nodes_list.dart'; +import 'package:tuple/tuple.dart'; + import '../../../../themes/coin_icon_provider.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; @@ -26,7 +26,8 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; -import 'package:tuple/tuple.dart'; +import '../../sub_widgets/nodes_list.dart'; +import 'add_edit_node_view.dart'; class CoinNodesView extends ConsumerStatefulWidget { const CoinNodesView({ @@ -59,7 +60,10 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> { Widget build(BuildContext context) { if (Util.isDesktop) { return DesktopDialog( + maxHeight: null, + maxWidth: 580, child: Column( + mainAxisSize: MainAxisSize.min, children: [ Row( children: [ @@ -129,11 +133,15 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> { const SizedBox( width: 12, ), - Padding( - padding: const EdgeInsets.all(20), - child: NodesList( - coin: widget.coin, - popBackToRoute: CoinNodesView.routeName, + Flexible( + child: Padding( + padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: NodesList( + coin: widget.coin, + popBackToRoute: CoinNodesView.routeName, + ), + ), ), ), ], diff --git a/lib/services/price.dart b/lib/services/price.dart index 83470c125..dc4f3d8d8 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -30,6 +30,7 @@ class PriceAPI { BitcoinFrost: "bitcoin", Litecoin: "litecoin", Bitcoincash: "bitcoin-cash", + Dash: "dash", Dogecoin: "dogecoin", Epiccash: "epic-cash", Ecash: "ecash", diff --git a/lib/wallets/crypto_currency/coins/dash.dart b/lib/wallets/crypto_currency/coins/dash.dart new file mode 100644 index 000000000..366bbbcc8 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/dash.dart @@ -0,0 +1,247 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../crypto_currency.dart'; +import '../interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_currency.dart'; + +class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface { + Dash(super.network) { + _idMain = "dash"; + _uriScheme = "dash"; + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "Dash"; + _ticker = "DASH"; + // case CryptoCurrencyNetwork.test: + // _id = "dashTestNet"; + // _name = "tDash"; + // _ticker = "tDASH"; + 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 + bool get torSupport => true; + + @override + List<DerivePathType> get supportedDerivationPathTypes => [ + DerivePathType.bip44, + ]; + + @override + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { + String coinType; + + switch (networkParams.wifPrefix) { + case 204: // dash mainnet wif + coinType = "5"; // dash mainnet + break; + // case 239: // dash testnet wif + // coinType = "1"; // dash testnet + // break; + default: + throw Exception("Invalid Dash network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip44: + purpose = 44; + break; + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + Amount get dustLimit => Amount( + rawValue: BigInt.from(1000000), + fractionDigits: fractionDigits, + ); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6"; + // case CryptoCurrencyNetwork.test: + // return "00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({ + coinlib.Address address, + AddressType addressType, + }) getAddressForPublicKey({ + required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType, + }) { + switch (derivePathType) { + case DerivePathType.bip44: + final addr = coinlib.P2PKHAddress.fromPublicKey( + publicKey, + version: networkParams.p2pkhPrefix, + ); + + return (address: addr, addressType: AddressType.p2pkh); + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + } + + @override + int get minConfirms => 6; + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return coinlib.Network( + p2pkhPrefix: 76, + p2shPrefix: 16, + wifPrefix: 204, + pubHDPrefix: 0x0488B21E, + privHDPrefix: 0x0488ADE4, + bech32Hrp: "dash", // TODO ????? + messagePrefix: '\x18Dash Signed Message:\n', // TODO ????? + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + // case CryptoCurrencyNetwork.test: + // return coinlib.Network( + // p2pkhPrefix: 140, + // p2shPrefix: 19, + // wifPrefix: 239, + // pubHDPrefix: 0x043587CF, + // privHDPrefix: 0x04358394, + // bech32Hrp: "tdash", // TODO ????? + // messagePrefix: '\x18Dash Signed Message:\n', // TODO ????? + // minFee: BigInt.from(1), // Not used in stack wallet currently + // minOutput: dustLimit.raw, // Not used in stack wallet currently + // feePerKb: BigInt.from(1), // Not used in stack wallet currently + // ); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "dash.stackwallet.com", + port: 60002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + ); + + default: + throw UnimplementedError(); + } + } + + @override + int get defaultSeedPhraseLength => 12; + + @override + int get fractionDigits => 8; + + @override + bool get hasBuySupport => true; + + @override + bool get hasMnemonicPassphraseSupport => true; + + @override + List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24]; + + @override + AddressType get defaultAddressType => defaultDerivePathType.getAddressType(); + + @override + BigInt get satsPerCoin => BigInt.from(100000000); + + @override + int get targetBlockTimeSeconds => 150; + + @override + DerivePathType get defaultDerivePathType => DerivePathType.bip44; + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + case CryptoCurrencyNetwork.main: + return Uri.parse("https://insight.dash.org/insight/tx/$txid"); + // case CryptoCurrencyNetwork.test: + // return Uri.parse( + // "https://insight.testnet.networks.dash.org:3002/insight/tx/$txid", + // ); + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } + + @override + int get transactionVersion => 2; + + @override + BigInt get defaultFeeRate => BigInt.from(1000); // TODO check for dash? +} diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index a498aa53d..788c87b4f 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -6,6 +6,7 @@ export 'coins/banano.dart'; export 'coins/bitcoin.dart'; export 'coins/bitcoin_frost.dart'; export 'coins/bitcoincash.dart'; +export 'coins/dash.dart'; export 'coins/dogecoin.dart'; export 'coins/ecash.dart'; export 'coins/epiccash.dart'; diff --git a/lib/wallets/wallet/impl/dash_wallet.dart b/lib/wallets/wallet/impl/dash_wallet.dart new file mode 100644 index 000000000..8138b684b --- /dev/null +++ b/lib/wallets/wallet/impl/dash_wallet.dart @@ -0,0 +1,314 @@ +import 'package:isar/isar.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 '../../../utilities/amount/amount.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_wallet.dart'; +import '../wallet_mixin_interfaces/coin_control_interface.dart'; +import '../wallet_mixin_interfaces/electrumx_interface.dart'; + +class DashWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T> + with ElectrumXInterface<T>, CoinControlInterface { + DashWallet(CryptoCurrencyNetwork network) : super(Dash(network) as T); + + @override + int get maximumFeerate => 2500; + + @override + int get isarTransactionVersion => 2; + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future<List<Address>> fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Future<void> updateTransactions() async { + // Get all addresses. + final List<Address> allAddressesOld = + await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + final Set<String> receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set<String> changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List<Map<String, dynamic>> allTxHashes = + await fetchHistory(allAddressesSet); + + // Only parse new txs (not in db yet). + final List<Map<String, dynamic>> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); + + // Only tx to list once. + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List<TransactionV2> txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List<InputV2> inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map<String, dynamic>.from(jsonInput as Map); + + final List<String> addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + cryptoCurrency: cryptoCurrency, + ); + + final prevOutJson = Map<String, dynamic>.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + scriptSigAsm: map["scriptSig"]?["asm"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List<OutputV2> outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map<String, dynamic>.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + final TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // This is where we would check for them. + // TODO: [prio=high] Check for special Dash outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["txid"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } + + @override + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( + Map<String, dynamic> jsonUTXO, + String? scriptPubKeyHex, + Map<String, dynamic> jsonTX, + String? utxoOwnerAddress, + ) async { + bool blocked = false; + String? blockedReason; + + // // check for bip47 notification + // final outputs = jsonTX["vout"] as List; + // for (final output in outputs) { + // final List<String>? scriptChunks = + // (output['scriptPubKey']?['asm'] as String?)?.split(" "); + // if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { + // final blindedPaymentCode = scriptChunks![1]; + // final bytes = blindedPaymentCode.toUint8ListFromHex; + // + // // https://en.bitcoin.it/wiki/BIP_0047#Sending + // if (bytes.length == 80 && bytes.first == 1) { + // blocked = true; + // blockedReason = "Paynym notification output. Incautious " + // "handling of outputs from notification transactions " + // "may cause unintended loss of privacy."; + // break; + // } + // } + // } + + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); + } + + @override + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 7b0851ee5..1bca3c858 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -28,6 +28,7 @@ import 'impl/banano_wallet.dart'; import 'impl/bitcoin_frost_wallet.dart'; import 'impl/bitcoin_wallet.dart'; import 'impl/bitcoincash_wallet.dart'; +import 'impl/dash_wallet.dart'; import 'impl/dogecoin_wallet.dart'; import 'impl/ecash_wallet.dart'; import 'impl/epiccash_wallet.dart'; @@ -323,6 +324,9 @@ abstract class Wallet<T extends CryptoCurrency> { case const (Bitcoincash): return BitcoincashWallet(net); + case const (Dash): + return DashWallet(net); + case const (Dogecoin): return DogecoinWallet(net); diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index bb5692b1f..553f60c05 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -13,26 +13,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:solana/solana.dart'; import 'package:tuple/tuple.dart'; -import '../models/node_model.dart'; -import '../notifications/show_flush_bar.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import '../providers/global/active_wallet_provider.dart'; +import '../providers/global/secure_store_provider.dart'; import '../providers/providers.dart'; -import '../services/tor_service.dart'; import '../themes/stack_colors.dart'; import '../utilities/assets.dart'; -import '../utilities/connection_check/electrum_connection_check.dart'; import '../utilities/constants.dart'; import '../utilities/default_nodes.dart'; import '../utilities/enums/sync_type_enum.dart'; -import '../utilities/logger.dart'; -import '../utilities/test_epic_box_connection.dart'; -import '../utilities/test_eth_node_connection.dart'; -import '../utilities/test_monero_node_connection.dart'; +import '../utilities/test_node_connection.dart'; import '../utilities/text_styles.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'rounded_white_container.dart'; @@ -82,150 +75,6 @@ class NodeOptionsSheet extends ConsumerWidget { } } - Future<bool> _testConnection( - NodeModel node, - BuildContext context, - WidgetRef ref, - ) async { - bool testPassed = false; - - switch (coin.runtimeType) { - case const (Epiccash): - try { - testPassed = await testEpicNodeConnection( - NodeFormData() - ..host = node.host - ..useSSL = node.useSSL - ..port = node.port, - ) != - null; - } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); - } - break; - - case const (Monero): - case const (Wownero): - try { - final uri = Uri.parse(node.host); - if (uri.scheme.startsWith("http")) { - final String path = uri.path.isEmpty ? "/json_rpc" : uri.path; - - final String uriString = - "${uri.scheme}://${uri.host}:${node.port}$path"; - - final response = await testMoneroNodeConnection( - Uri.parse(uriString), - false, - proxyInfo: ref.read(prefsChangeNotifierProvider).useTor - ? ref.read(pTorService).getProxyInfo() - : null, - ); - - if (response.cert != null && context.mounted) { - // if (mounted) { - final shouldAllowBadCert = await showBadX509CertificateDialog( - response.cert!, - response.url!, - response.port!, - context, - ); - - if (shouldAllowBadCert) { - final response = await testMoneroNodeConnection( - Uri.parse(uriString), - true, - proxyInfo: ref.read(prefsChangeNotifierProvider).useTor - ? ref.read(pTorService).getProxyInfo() - : null, - ); - testPassed = response.success; - } - // } - } else { - testPassed = response.success; - } - } - } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); - } - - break; - - case const (Bitcoin): - case const (Litecoin): - case const (Dogecoin): - case const (Firo): - case const (Particl): - case const (Bitcoincash): - case const (Namecoin): - case const (Ecash): - case const (BitcoinFrost): - case const (Peercoin): - try { - testPassed = await checkElectrumServer( - host: node.host, - port: node.port, - useSSL: node.useSSL, - overridePrefs: ref.read(prefsChangeNotifierProvider), - overrideTorService: ref.read(pTorService), - ); - } catch (_) { - testPassed = false; - } - - break; - - case const (Ethereum): - try { - testPassed = await testEthNodeConnection(node.host); - } catch (_) { - testPassed = false; - } - break; - - case const (Nano): - case const (Banano): - case const (Tezos): - case const (Stellar): - throw UnimplementedError(); - //TODO: check network/node - - case const (Solana): - try { - RpcClient rpcClient; - if (node.host.startsWith("http") || node.host.startsWith("https")) { - rpcClient = RpcClient("${node.host}:${node.port}"); - } else { - rpcClient = RpcClient("http://${node.host}:${node.port}"); - } - await rpcClient.getEpochInfo().then((value) => testPassed = true); - } catch (_) { - testPassed = false; - } - break; - } - - if (testPassed) { - // showFloatingFlushBar( - // type: FlushBarType.success, - // message: "Server ping success", - // context: context, - // ); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - iconAsset: Assets.svg.circleAlert, - message: "Could not connect to node", - context: context, - ), - ); - } - - return testPassed; - } - @override Widget build(BuildContext context, WidgetRef ref) { final maxHeight = MediaQuery.of(context).size.height * 0.60; @@ -403,21 +252,38 @@ class NodeOptionsSheet extends ConsumerWidget { onPressed: status == "Connected" ? null : () async { - final canConnect = - await _testConnection(node, context, ref); - if (!canConnect) { - return; + final pw = await node.getPassword( + ref.read(secureStoreProvider), + ); + if (context.mounted) { + final canConnect = await testNodeConnection( + context: context, + nodeFormData: NodeFormData() + ..name = node.name + ..host = node.host + ..login = node.loginName + ..password = pw + ..port = node.port + ..useSSL = node.useSSL + ..isFailover = node.isFailover + ..trusted = node.trusted, + cryptoCurrency: coin, + ref: ref, + ); + if (!canConnect) { + return; + } + + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor( + coin: coin, + node: node, + shouldNotifyListeners: true, + ); + + await _notifyWalletsOfUpdatedNode(ref); } - - await ref - .read(nodeServiceChangeNotifierProvider) - .setPrimaryNodeFor( - coin: coin, - node: node, - shouldNotifyListeners: true, - ); - - await _notifyWalletsOfUpdatedNode(ref); }, child: Text( // status == "Connected" ? "Disconnect" : "Connect", diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index cb9bbf46c..2c798e53f 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -55,6 +55,7 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([ Banano(CryptoCurrencyNetwork.main), Bitcoincash(CryptoCurrencyNetwork.main), BitcoinFrost(CryptoCurrencyNetwork.main), + Dash(CryptoCurrencyNetwork.main), Dogecoin(CryptoCurrencyNetwork.main), Ecash(CryptoCurrencyNetwork.main), Epiccash(CryptoCurrencyNetwork.main),