mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-04-08 07:17:35 +00:00
Merge pull request #893 from cypherstack/dash
Dash and various bug fixes
This commit is contained in:
commit
19fbb33629
14 changed files with 640 additions and 201 deletions
asset_sources/default_themes
lib
pages
send_view/sub_widgets
settings_views/global_settings_view
services
wallets
widgets
scripts/app_config
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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!,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -30,6 +30,7 @@ class PriceAPI {
|
|||
BitcoinFrost: "bitcoin",
|
||||
Litecoin: "litecoin",
|
||||
Bitcoincash: "bitcoin-cash",
|
||||
Dash: "dash",
|
||||
Dogecoin: "dogecoin",
|
||||
Epiccash: "epic-cash",
|
||||
Ecash: "ecash",
|
||||
|
|
247
lib/wallets/crypto_currency/coins/dash.dart
Normal file
247
lib/wallets/crypto_currency/coins/dash.dart
Normal file
|
@ -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?
|
||||
}
|
|
@ -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';
|
||||
|
|
314
lib/wallets/wallet/impl/dash_wallet.dart
Normal file
314
lib/wallets/wallet/impl/dash_wallet.dart
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue