Merge remote-tracking branch 'origin/staging' into arti

This commit is contained in:
sneurlax 2024-06-22 11:42:15 -05:00
commit 0745ee6617
82 changed files with 2264 additions and 479 deletions

View file

@ -16,6 +16,7 @@ abstract class AppConfig {
static const suffix = _suffix;
static String get appDefaultDataDirName => _appDataDirName;
static String get shortDescriptionText => _shortDescriptionText;
static String get commitHash => _commitHash;
static bool hasFeature(AppFeature feature) => _features.contains(feature);

View file

@ -171,7 +171,7 @@ Future<void> migrateWalletsToIsar({
walletId: old.walletId,
name: old.name,
mainAddressType: AppConfig.getCryptoCurrencyFor(old.coinIdentifier)!
.primaryAddressType,
.defaultAddressType,
favouriteOrderIndex: favourites.indexOf(old.walletId),
cachedChainHeight: walletBox.get(
DBKeys.storedChainHeight,

View file

@ -2,12 +2,13 @@ import 'dart:convert';
import 'dart:math';
import 'package:isar/isar.dart';
import '../transaction.dart';
import 'input_v2.dart';
import 'output_v2.dart';
import '../../../../../utilities/amount/amount.dart';
import '../../../../../utilities/extensions/extensions.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../transaction.dart';
import 'input_v2.dart';
import 'output_v2.dart';
part 'transaction_v2.g.dart';
@ -56,17 +57,52 @@ class TransactionV2 {
required this.otherData,
});
TransactionV2 copyWith({
String? walletId,
String? txid,
String? hash,
int? timestamp,
int? height,
String? blockHash,
int? version,
List<InputV2>? inputs,
List<OutputV2>? outputs,
TransactionType? type,
TransactionSubType? subType,
String? otherData,
}) {
return TransactionV2(
walletId: walletId ?? this.walletId,
txid: txid ?? this.txid,
hash: hash ?? this.hash,
timestamp: timestamp ?? this.timestamp,
height: height ?? this.height,
blockHash: blockHash ?? this.blockHash,
version: version ?? this.version,
inputs: inputs ?? this.inputs,
outputs: outputs ?? this.outputs,
type: type ?? this.type,
subType: subType ?? this.subType,
otherData: otherData ?? this.otherData,
);
}
int? get size => _getFromOtherData(key: TxV2OdKeys.size) as int?;
int? get vSize => _getFromOtherData(key: TxV2OdKeys.vSize) as int?;
bool get isEpiccashTransaction =>
_getFromOtherData(key: "isEpiccashTransaction") == true;
_getFromOtherData(key: TxV2OdKeys.isEpiccashTransaction) == true;
int? get numberOfMessages =>
_getFromOtherData(key: "numberOfMessages") as int?;
String? get slateId => _getFromOtherData(key: "slateId") as String?;
String? get onChainNote => _getFromOtherData(key: "onChainNote") as String?;
bool get isCancelled => _getFromOtherData(key: "isCancelled") == true;
_getFromOtherData(key: TxV2OdKeys.numberOfMessages) as int?;
String? get slateId => _getFromOtherData(key: TxV2OdKeys.slateId) as String?;
String? get onChainNote =>
_getFromOtherData(key: TxV2OdKeys.onChainNote) as String?;
bool get isCancelled =>
_getFromOtherData(key: TxV2OdKeys.isCancelled) == true;
String? get contractAddress =>
_getFromOtherData(key: "contractAddress") as String?;
int? get nonce => _getFromOtherData(key: "nonce") as int?;
_getFromOtherData(key: TxV2OdKeys.contractAddress) as String?;
int? get nonce => _getFromOtherData(key: TxV2OdKeys.nonce) as int?;
int getConfirmations(int currentChainHeight) {
if (height == null || height! <= 0) return 0;
@ -145,7 +181,7 @@ class TransactionV2 {
Amount? _getOverrideFee() {
try {
return Amount.fromSerializedJsonString(
_getFromOtherData(key: "overrideFee") as String,
_getFromOtherData(key: TxV2OdKeys.overrideFee) as String,
);
} catch (_) {
return null;
@ -249,3 +285,16 @@ class TransactionV2 {
')';
}
}
abstract final class TxV2OdKeys {
static const size = "size";
static const vSize = "vSize";
static const isEpiccashTransaction = "isEpiccashTransaction";
static const numberOfMessages = "numberOfMessages";
static const slateId = "slateId";
static const onChainNote = "onChainNote";
static const isCancelled = "isCancelled";
static const contractAddress = "contractAddress";
static const nonce = "nonce";
static const overrideFee = "overrideFee";
}

View file

@ -63,7 +63,7 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
String _searchTerm = "";
final _coinsTestnet = [
...AppConfig.coins.where((e) => e.network == CryptoCurrencyNetwork.test),
...AppConfig.coins.where((e) => e.network.isTestNet),
];
final _coins = [
...AppConfig.coins.where((e) => e.network == CryptoCurrencyNetwork.main),

View file

@ -11,11 +11,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../app_config.dart';
import '../../models/isar/models/blockchain_data/address.dart';
import '../../models/isar/models/contact_entry.dart';
import 'subviews/add_address_book_entry_view.dart';
import 'subviews/address_book_filter_view.dart';
import '../../providers/db/main_db_provider.dart';
import '../../providers/global/address_book_service_provider.dart';
import '../../providers/providers.dart';
@ -35,6 +34,8 @@ import '../../widgets/icon_widgets/x_icon.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import 'subviews/add_address_book_entry_view.dart';
import 'subviews/address_book_filter_view.dart';
class AddressBookView extends ConsumerStatefulWidget {
const AddressBookView({
@ -67,7 +68,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> {
if (widget.coin == null) {
final coins = [...AppConfig.coins];
coins.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
final bool showTestNet =

View file

@ -43,7 +43,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> {
void initState() {
final coins = [...AppConfig.coins];
coins.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
final showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins;

View file

@ -13,8 +13,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../providers/global/prefs_provider.dart';
import '../../../app_config.dart';
import '../../../providers/global/prefs_provider.dart';
import '../../../themes/coin_image_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/constants.dart';
@ -29,7 +30,7 @@ class CoinSelectSheet extends StatelessWidget {
final maxHeight = MediaQuery.of(context).size.height * 0.60;
final coins_ = [...AppConfig.coins];
coins_.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
return Container(
decoration: BoxDecoration(

View file

@ -15,11 +15,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'coin_select_sheet.dart';
import '../../../app_config.dart';
import '../../../providers/providers.dart';
// import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart';
import '../../../providers/ui/address_book_providers/address_entry_data_provider.dart';
import '../../../app_config.dart';
import '../../../themes/coin_icon_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/address_utils.dart';
@ -36,6 +36,7 @@ import '../../../widgets/icon_widgets/qrcode_icon.dart';
import '../../../widgets/icon_widgets/x_icon.dart';
import '../../../widgets/stack_text_field.dart';
import '../../../widgets/textfield_icon_button.dart';
import 'coin_select_sheet.dart';
class NewContactAddressEntryForm extends ConsumerStatefulWidget {
const NewContactAddressEntryForm({
@ -92,7 +93,7 @@ class _NewContactAddressEntryFormState
if (isDesktop) {
coins = [...AppConfig.coins];
coins.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
final showTestNet =

View file

@ -694,7 +694,7 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
selectedSumInt.toAmountAsRaw(
fractionDigits: coin.fractionDigits,
);
return Text(
return SelectableText(
ref
.watch(pAmountFormatter(coin))
.format(selectedSum),
@ -739,7 +739,7 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
"Amount to send",
style: STextStyles.w600_14(context),
),
Text(
SelectableText(
ref
.watch(pAmountFormatter(coin))
.format(widget.requestedTotal!),

View file

@ -213,7 +213,7 @@ class IntroAboutText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
"An open-source, multicoin wallet for everyone",
AppConfig.shortDescriptionText,
textAlign: TextAlign.center,
style: !isDesktop
? STextStyles.subtitle(context)

View file

@ -188,7 +188,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
wallet is Bip39HDWallet &&
wallet.supportedAddressTypes.length > 1);
_walletAddressTypes.add(coin.primaryAddressType);
_walletAddressTypes.add(wallet.info.mainAddressType);
if (_showMultiType) {
if (_supportsSpark) {
@ -197,7 +197,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
_walletAddressTypes.addAll(
(wallet as Bip39HDWallet)
.supportedAddressTypes
.where((e) => e != coin.primaryAddressType),
.where((e) => e != wallet.info.mainAddressType),
);
}
}

View file

@ -392,8 +392,7 @@ class _SendViewState extends ConsumerState<SendView> {
ref.read(pValidSparkSendToAddress.notifier).state =
SparkInterface.validateSparkAddress(
address: address ?? "",
isTestNet:
wallet.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: wallet.cryptoCurrency.network.isTestNet,
);
}

View file

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

View file

@ -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();

View file

@ -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,
),
),
),
),
],

View file

@ -44,7 +44,7 @@ class _ManageNodesViewState extends ConsumerState<ManageNodesView> {
void initState() {
_coins = _coins.toList();
_coins.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
super.initState();
}

View file

@ -814,7 +814,7 @@ abstract class SWB {
coinName: coin.identifier,
walletId: walletId,
name: walletName,
mainAddressType: coin.primaryAddressType,
mainAddressType: coin.defaultAddressType,
restoreHeight: walletbackup['restoreHeight'] as int? ?? 0,
otherDataJsonString: otherData == null ? null : jsonEncode(otherData),
cachedChainHeight: walletbackup['storedChainHeight'] as int? ?? 0,

View file

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../providers/db/main_db_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/isar/models/wallet_info.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
class RbfSettingsView extends ConsumerStatefulWidget {
const RbfSettingsView({
super.key,
required this.walletId,
});
static const String routeName = "/rbfSettings";
final String walletId;
@override
ConsumerState<RbfSettingsView> createState() => _RbfSettingsViewState();
}
class _RbfSettingsViewState extends ConsumerState<RbfSettingsView> {
bool _switchRbfToggledLock = false; // Mutex.
Future<void> _switchRbfToggled(bool newValue) async {
if (_switchRbfToggledLock) {
return;
}
_switchRbfToggledLock = true; // Lock mutex.
try {
// Toggle enableOptInRbf in wallet info.
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.enableOptInRbf: newValue,
},
isar: ref.read(mainDBProvider).isar,
);
} finally {
// ensure _switchRbfToggledLock is set to false no matter what
_switchRbfToggledLock = false;
}
}
@override
Widget build(BuildContext context) {
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"RBF settings",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const SizedBox(width: 3),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
pWalletInfo(widget.walletId)
.select((value) => value.otherData),
)[WalletInfoKeys.enableOptInRbf] as bool? ??
false,
onValueChanged: _switchRbfToggled,
),
),
const SizedBox(
width: 16,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Enable opt-in RBF",
style: STextStyles.w600_20(context),
),
],
),
],
),
],
),
),
),
);
}
}

View file

@ -11,11 +11,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../providers/providers.dart';
import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/rounded_white_container.dart';
@ -23,6 +27,7 @@ import '../../../../widgets/stack_dialog.dart';
import '../../../pinpad_views/lock_screen_view.dart';
import 'delete_wallet_warning_view.dart';
import 'lelantus_settings_view.dart';
import 'rbf_settings_view.dart';
import 'rename_wallet_view.dart';
import 'spark_info.dart';
@ -183,73 +188,115 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
),
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
if (ref.watch(pWallets).getWallet(walletId)
is LelantusInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(walletId)
is LelantusInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
LelantusSettingsView.routeName,
arguments: walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Lelantus settings",
style: STextStyles.titleBold12(context),
),
],
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
LelantusSettingsView.routeName,
arguments: walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Lelantus settings",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
if (ref.watch(pWallets).getWallet(walletId) is SparkInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(walletId) is SparkInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
SparkInfoView.routeName,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Spark info",
style: STextStyles.titleBold12(context),
),
],
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
SparkInfoView.routeName,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Spark info",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
if (ref.watch(pWallets).getWallet(walletId) is RbfInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(walletId) is RbfInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
RbfSettingsView.routeName,
arguments: walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"RBF settings",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
),
],
),
),

View file

@ -0,0 +1,313 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../../providers/providers.dart';
import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/amount/amount.dart';
import '../../../../utilities/amount/amount_formatter.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/fee_slider.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart';
import '../../../send_view/confirm_transaction_view.dart';
class BoostTransactionView extends ConsumerStatefulWidget {
const BoostTransactionView({
super.key,
required this.transaction,
});
static const String routeName = "/boostTransaction";
final TransactionV2 transaction;
@override
ConsumerState<BoostTransactionView> createState() =>
_BoostTransactionViewState();
}
class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
late final bool isDesktop;
late final String walletId;
late final TransactionV2 _transaction;
late final Amount fee;
late final Amount amount;
late final int rate;
BigInt? customFee;
int _newRate = 0;
bool _previewTxnLock = false;
Future<void> _previewTxn() async {
if (_previewTxnLock) {
return;
}
_previewTxnLock = true;
try {
if (_newRate <= rate) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error",
message: "New fee rate must be greater than the current rate.",
),
);
return;
}
final wallet = (ref.read(pWallets).getWallet(walletId) as RbfInterface);
Exception? ex;
// build new tx and show loading/tx generation
final txData = await showLoading(
whileFuture: wallet.prepareRbfSend(
oldTransaction: _transaction,
newRate: _newRate,
),
context: context,
message: "Preparing RBF Transaction...",
onException: (e) => ex = e,
);
// on failure show error message
if (txData == null && mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "RBF send error",
message: ex?.toString() ?? "Unknown error found",
maxWidth: 600,
),
);
return;
} else {
// on success show confirm tx screen
if (isDesktop && mounted) {
unawaited(
showDialog(
context: context,
builder: (context) => DesktopDialog(
maxHeight: MediaQuery.of(context).size.height - 64,
maxWidth: 580,
child: ConfirmTransactionView(
txData: txData!,
walletId: walletId,
onSuccess: () {},
// isPaynymTransaction: isPaynymSend, TODO ?
routeOnSuccessName: DesktopHomeView.routeName,
),
),
),
);
} else if (mounted) {
unawaited(
Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmTransactionView(
txData: txData!,
walletId: walletId,
// isPaynymTransaction: isPaynymSend, TODO ?
onSuccess: () {},
),
settings: const RouteSettings(
name: ConfirmTransactionView.routeName,
),
),
),
);
}
}
} finally {
_previewTxnLock = false;
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
_transaction = widget.transaction;
walletId = _transaction.walletId;
fee = _transaction.getFee(
fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits,
);
amount = _transaction.getAmountSentFromThisWallet(
fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits,
);
rate = (fee.raw ~/ BigInt.from(_transaction.vSize!)).toInt();
_newRate = rate + 1;
super.initState();
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(pWalletCoin(walletId));
final String feeString = ref.watch(pAmountFormatter(coin)).format(
fee,
);
final String amountString = ref.watch(pAmountFormatter(coin)).format(
amount,
);
final String feeRateString = "$rate sats/vByte";
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
"Boost transaction",
style: STextStyles.navBarTitle(context),
),
),
body: child,
),
),
child: Padding(
padding: isDesktop
? const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
)
: const EdgeInsets.all(12),
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Column(
children: [
RoundedWhiteContainer(
borderColor: isDesktop
? Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar
: null,
padding: const EdgeInsets.all(0),
child: child,
),
const SizedBox(
height: 32,
),
PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Preview send",
onPressed: _previewTxn,
),
],
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ConditionalParent(
condition: isDesktop,
builder: (child) => RoundedWhiteContainer(
padding: EdgeInsets.zero,
child: child,
),
child: Column(
children: [
DetailItem(
title: "Send amount",
detail: amountString,
horizontal: true,
),
const _Divider(),
DetailItem(
title: "Current fee",
detail: feeString,
horizontal: true,
),
const _Divider(),
DetailItem(
title: "Current rate",
detail: feeRateString,
horizontal: true,
),
const _Divider(),
Padding(
padding: const EdgeInsets.all(16),
child: FeeSlider(
overrideLabel: "Select a higher rate",
onSatVByteChanged: (value) => _newRate = value,
coin: coin,
min: rate.toDouble() + 1,
max: rate * 5.0,
pow: 1,
),
),
],
),
),
if (!isDesktop) const Spacer(),
if (!isDesktop)
const SizedBox(
height: 16,
),
if (!isDesktop)
PrimaryButton(
label: "Preview send",
onPressed: _previewTxn,
),
],
),
),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider({super.key});
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return Container(
height: 1,
color: Theme.of(context).extension<StackColors>()!.backgroundAppBar,
);
} else {
return const SizedBox(
height: 12,
);
}
}
}

View file

@ -16,14 +16,13 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:isar/isar.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../sub_widgets/tx_icon.dart';
import '../dialogs/cancelling_transaction_progress_dialog.dart';
import '../edit_note_view.dart';
import '../../wallet_view.dart';
import '../../../../providers/db/main_db_provider.dart';
import '../../../../providers/global/address_book_service_provider.dart';
import '../../../../providers/providers.dart';
@ -35,6 +34,7 @@ import '../../../../utilities/block_explorers.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/format.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
@ -42,6 +42,7 @@ import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../../../wallets/isar/models/spark_coin.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/conditional_parent.dart';
@ -55,8 +56,11 @@ import '../../../../widgets/icon_widgets/copy_icon.dart';
import '../../../../widgets/icon_widgets/pencil_icon.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../sub_widgets/tx_icon.dart';
import '../../wallet_view.dart';
import '../dialogs/cancelling_transaction_progress_dialog.dart';
import '../edit_note_view.dart';
import 'boost_transaction_view.dart';
class TransactionV2DetailsView extends ConsumerStatefulWidget {
const TransactionV2DetailsView({
@ -90,6 +94,7 @@ class _TransactionV2DetailsViewState
late final String unit;
late final int minConfirms;
late final EthContract? ethContract;
late final bool supportsRbf;
bool get isTokenTx => ethContract != null;
@ -99,12 +104,89 @@ class _TransactionV2DetailsViewState
String? _sparkMemo;
bool _boostButtonLock = false;
Future<void> _boostPressed() async {
final wallet = ref.read(pWallets).getWallet(walletId);
if (_boostButtonLock || wallet is! RbfInterface) {
return;
}
_boostButtonLock = true;
try {
if (Util.isDesktop) {
if (_transaction.vSize == null) {
final updatedTx = await showLoading(
whileFuture: wallet.updateVSize(_transaction),
context: context,
message: "Fetching transaction vSize...",
);
// TODO handle errors if null
_transaction = updatedTx!;
}
// TODO pass the size in to the rbf screen
if (mounted) {
await showDialog<void>(
context: context,
builder: (context) => DesktopDialog(
maxHeight: null,
maxWidth: 580,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Boost transaction",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Flexible(
child: SingleChildScrollView(
child: BoostTransactionView(
transaction: _transaction,
),
),
),
],
),
),
);
}
} else {
unawaited(
Navigator.of(context).pushNamed(
BoostTransactionView.routeName,
arguments: _transaction,
),
);
}
} finally {
_boostButtonLock = false;
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
_transaction = widget.transaction;
walletId = widget.walletId;
if (_transaction.type
case TransactionType.sentToSelf || TransactionType.outgoing) {
supportsRbf = _transaction.subType == TransactionSubType.none &&
ref.read(pWallets).getWallet(walletId) is RbfInterface;
} else {
supportsRbf = false;
}
coin = widget.coin;
if (_transaction.subType == TransactionSubType.ethToken) {
@ -482,6 +564,11 @@ class _TransactionV2DetailsViewState
outputLabel = "Sent to";
}
final confirmedTxn = _transaction.isConfirmed(
currentHeight,
coin.minConfirms,
);
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
@ -1330,6 +1417,15 @@ class _TransactionV2DetailsViewState
context,
),
),
if (supportsRbf && !confirmedTxn)
const SizedBox(
height: 8,
),
if (supportsRbf && !confirmedTxn)
CustomTextButton(
text: "Boost transaction",
onTap: _boostPressed,
),
],
),
if (!isDesktop)
@ -1779,6 +1875,16 @@ class _TransactionV2DetailsViewState
const SizedBox(
height: 12,
),
// if (whatIsIt(
// _transaction,
// currentHeight,
// ) !=
// "Sending")
// isDesktop
// ? const _Divider()
// : const SizedBox(
// height: 12,
// ),
],
),
),

View file

@ -377,7 +377,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
Future<void> _onExchangePressed(BuildContext context) async {
final CryptoCurrency coin = ref.read(pWalletCoin(walletId));
if (coin.network == CryptoCurrencyNetwork.test) {
if (coin.network.isTestNet) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
@ -423,7 +423,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
Future<void> _onBuyPressed(BuildContext context) async {
final CryptoCurrency coin = ref.read(pWalletCoin(walletId));
if (coin.network == CryptoCurrencyNetwork.test) {
if (coin.network.isTestNet) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(

View file

@ -11,13 +11,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../app_config.dart';
import '../../models/isar/models/blockchain_data/address.dart';
import '../../models/isar/models/contact_entry.dart';
import '../../pages/address_book_views/subviews/add_address_book_entry_view.dart';
import '../../pages/address_book_views/subviews/address_book_filter_view.dart';
import 'subwidgets/desktop_address_book_scaffold.dart';
import 'subwidgets/desktop_contact_details.dart';
import '../../providers/db/main_db_provider.dart';
import '../../providers/global/address_book_service_provider.dart';
import '../../providers/providers.dart';
@ -40,6 +39,8 @@ import '../../widgets/rounded_container.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import 'subwidgets/desktop_address_book_scaffold.dart';
import 'subwidgets/desktop_contact_details.dart';
class DesktopAddressBook extends ConsumerStatefulWidget {
const DesktopAddressBook({super.key});
@ -99,7 +100,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> {
// if (widget.coin == null) {
final coins = AppConfig.coins.toList();
coins.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
final bool showTestNet =

View file

@ -488,7 +488,7 @@ class _DesktopCoinControlUseDialogState
.textDark,
),
),
Text(
SelectableText(
"${widget.amountToSend!.decimal.toStringAsFixed(
coin.fractionDigits,
)}"
@ -523,7 +523,7 @@ class _DesktopCoinControlUseDialogState
.textDark,
),
),
Text(
SelectableText(
"${selectedSum.decimal.toStringAsFixed(
coin.fractionDigits,
)} ${coin.ticker}",

View file

@ -704,8 +704,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
ref.read(pValidSparkSendToAddress.notifier).state =
SparkInterface.validateSparkAddress(
address: address ?? "",
isTestNet:
wallet.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: wallet.cryptoCurrency.network.isTestNet,
);
}
@ -883,7 +882,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_cryptoFocus.addListener(() {
if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
if (ref.read(pSendAmount) == null) {
ref.refresh(sendAmountProvider);
if (ref.read(sendAmountProvider) != Amount.zero && mounted) {
ref.read(sendAmountProvider.state).state = Amount.zero;
}
} else {
ref.read(sendAmountProvider.state).state = ref.read(pSendAmount)!;
}
@ -1468,7 +1469,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
if (_data != null && _data!.contactLabel == _address) {
error = SparkInterface.validateSparkAddress(
address: _data!.address,
isTestNet: coin.network == CryptoCurrencyNetwork.test,
isTestNet: coin.network.isTestNet,
)
? "Lelantus to Spark not supported"
: null;

View file

@ -28,6 +28,7 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_inter
import '../../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../../widgets/custom_buttons/draggable_switch_button.dart';
import '../../../../../widgets/desktop/desktop_dialog.dart';
@ -81,6 +82,27 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
}
}
bool _switchRbfToggledLock = false; // Mutex.
Future<void> _switchRbfToggled(bool newValue) async {
if (_switchRbfToggledLock) {
return;
}
_switchRbfToggledLock = true; // Lock mutex.
try {
// Toggle enableOptInRbf in wallet info.
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.enableOptInRbf: newValue,
},
isar: ref.read(mainDBProvider).isar,
);
} finally {
// ensure _switchRbfToggledLock is set to false no matter what
_switchRbfToggledLock = false;
}
}
@override
Widget build(BuildContext context) {
final wallet = ref.watch(
@ -198,6 +220,38 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
],
),
),
if (wallet is RbfInterface)
_MoreFeaturesItemBase(
child: Row(
children: [
const SizedBox(width: 3),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
pWalletInfo(widget.walletId)
.select((value) => value.otherData),
)[WalletInfoKeys.enableOptInRbf] as bool? ??
false,
onValueChanged: _switchRbfToggled,
),
),
const SizedBox(
width: 16,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Flag outgoing transactions with opt-in RBF",
style: STextStyles.w600_20(context),
),
],
),
],
),
),
const SizedBox(
height: 28,
),

View file

@ -208,7 +208,7 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
SizedBox(
width: 350,
child: Text(
"Open source multicoin wallet for everyone",
AppConfig.shortDescriptionText,
textAlign: TextAlign.center,
style: STextStyles.desktopSubtitleH1(context),
),

View file

@ -69,7 +69,7 @@ class _NodesSettings extends ConsumerState<NodesSettings> {
void initState() {
_coins = _coins.toList();
_coins.removeWhere(
(e) => e is Firo && e.network == CryptoCurrencyNetwork.test,
(e) => e is Firo && e.network.isTestNet,
);
searchNodeController = TextEditingController();

View file

@ -130,6 +130,7 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
@ -144,6 +145,7 @@ import 'pages/wallet_view/transaction_views/edit_note_view.dart';
import 'pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'pages/wallet_view/transaction_views/transaction_search_filter_view.dart';
import 'pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart';
import 'pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart';
import 'pages/wallet_view/transaction_views/tx_v2/fusion_group_details_view.dart';
import 'pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'pages/wallet_view/wallet_view.dart';
@ -1967,6 +1969,18 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case RbfSettingsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => RbfSettingsView(walletId: args),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case SparkInfoView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -2173,6 +2187,20 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case BoostTransactionView.routeName:
if (args is TransactionV2) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => BoostTransactionView(
transaction: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case BackupRestoreSettings.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,

View file

@ -6,6 +6,7 @@ import 'package:frostdart/frostdart.dart';
import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:frostdart/output.dart';
import 'package:frostdart/util.dart';
import '../models/isar/models/blockchain_data/utxo.dart';
import '../utilities/amount/amount.dart';
import '../utilities/extensions/extensions.dart';
@ -83,9 +84,8 @@ abstract class Frost {
required CryptoCurrency coin,
}) {
try {
final network = coin.network == CryptoCurrencyNetwork.test
? Network.Testnet
: Network.Mainnet;
final network =
coin.network.isTestNet ? Network.Testnet : Network.Mainnet;
final signConfigPointer = decodedSignConfig(
encodedConfig: signConfig,
network: network,

View file

@ -30,6 +30,7 @@ class PriceAPI {
BitcoinFrost: "bitcoin",
Litecoin: "litecoin",
Bitcoincash: "bitcoin-cash",
Dash: "dash",
Dogecoin: "dogecoin",
Epiccash: "epic-cash",
Ecash: "ecash",

View file

@ -11,7 +11,6 @@ import '../../utilities/extensions/impl/string.dart';
import '../../utilities/extensions/impl/uint8_list.dart';
import '../../utilities/format.dart';
import '../../utilities/logger.dart';
import '../crypto_currency/crypto_currency.dart';
import '../crypto_currency/intermediate/bip39_hd_currency.dart';
import '../models/tx_data.dart';
@ -92,7 +91,7 @@ abstract final class LelantusFfiWrapper {
mintKeyPair.privateKey!.toHex,
currentIndex,
mintKeyPair.identifier.toHex,
isTestnet: args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: args.cryptoCurrency.network.isTestNet,
);
for (int setId = 1; setId <= args.latestSetId; setId++) {
@ -117,8 +116,7 @@ abstract final class LelantusFfiWrapper {
amount,
mintKeyPair.privateKey!.toHex,
currentIndex,
isTestnet:
args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: args.cryptoCurrency.network.isTestNet,
);
final bool isUsed = args.usedSerialNumbers.contains(serialNumber);
@ -162,8 +160,7 @@ abstract final class LelantusFfiWrapper {
amount,
aesPrivateKey,
currentIndex,
isTestnet:
args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: args.cryptoCurrency.network.isTestNet,
);
final bool isUsed = args.usedSerialNumbers.contains(serialNumber);
@ -314,7 +311,7 @@ abstract final class LelantusFfiWrapper {
spendAmount: spendAmount,
subtractFeeFromAmount: arg.subtractFeeFromAmount,
lelantusEntries: arg.lelantusEntries,
isTestNet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: arg.cryptoCurrency.network.isTestNet,
),
);
final changeToMint = estimateJoinSplitFee.changeToMint;
@ -364,7 +361,7 @@ abstract final class LelantusFfiWrapper {
changeToMint,
jmintprivatekey,
arg.index,
isTestnet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: arg.cryptoCurrency.network.isTestNet,
);
final _derivePath = "${arg.partialDerivationPath}$JMINT_INDEX/$keyPath";
@ -378,7 +375,7 @@ abstract final class LelantusFfiWrapper {
arg.index,
Format.uint8listToString(jmintKeyPair.identifier),
aesPrivateKey,
isTestnet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: arg.cryptoCurrency.network.isTestNet,
);
tx.addOutput(
@ -434,7 +431,7 @@ abstract final class LelantusFfiWrapper {
anonymitySets,
anonymitySetHashes,
groupBlockHashes,
isTestnet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: arg.cryptoCurrency.network.isTestNet,
);
final finalTx = bitcoindart.TransactionBuilder(network: _network);

View file

@ -53,7 +53,7 @@ class Banano extends NanoCurrency {
int get minConfirms => 1;
@override
AddressType get primaryAddressType => AddressType.banano;
AddressType get defaultAddressType => AddressType.banano;
@override
String get defaultRepresentative =>
@ -97,7 +97,7 @@ class Banano extends NanoCurrency {
}
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);
}

View file

@ -24,6 +24,10 @@ class Bitcoin extends Bip39HDCurrency
_id = "bitcoinTestNet";
_name = "tBitcoin";
_ticker = "tBTC";
case CryptoCurrencyNetwork.test4:
_id = "bitcoinTestNet4";
_name = "t4Bitcoin";
_ticker = "t4BTC";
default:
throw Exception("Unsupported network: $network");
}
@ -71,6 +75,8 @@ class Bitcoin extends Bip39HDCurrency
return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
case CryptoCurrencyNetwork.test:
return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
case CryptoCurrencyNetwork.test4:
return "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043";
default:
throw Exception("Unsupported network: $network");
}
@ -99,6 +105,7 @@ class Bitcoin extends Bip39HDCurrency
feePerKb: BigInt.from(1), // Not used in stack wallet currently
);
case CryptoCurrencyNetwork.test:
case CryptoCurrencyNetwork.test4:
return coinlib.Network(
wifPrefix: 0xef,
p2pkhPrefix: 0x6f,
@ -247,6 +254,19 @@ class Bitcoin extends Bip39HDCurrency
isDown: false,
);
case CryptoCurrencyNetwork.test4:
return NodeModel(
host: "bitcoin-testnet4.stackwallet.com",
port: 50002,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: true,
enabled: true,
coinName: identifier,
isFailover: true,
isDown: false,
);
default:
throw UnimplementedError();
}
@ -268,7 +288,7 @@ class Bitcoin extends Bip39HDCurrency
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2wpkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -277,7 +297,7 @@ class Bitcoin extends Bip39HDCurrency
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip84;
DerivePathType get defaultDerivePathType => DerivePathType.bip86;
@override
Uri defaultBlockExplorer(String txid) {
@ -286,6 +306,8 @@ class Bitcoin extends Bip39HDCurrency
return Uri.parse("https://mempool.space/tx/$txid");
case CryptoCurrencyNetwork.test:
return Uri.parse("https://mempool.space/testnet/tx/$txid");
case CryptoCurrencyNetwork.test4:
return Uri.parse("https://mempool.space/testnet4/tx/$txid");
default:
throw Exception(
"Unsupported network for defaultBlockExplorer(): $network",

View file

@ -24,6 +24,10 @@ class BitcoinFrost extends FrostCurrency {
_id = "bitcoinFrostTestNet";
_name = "tBitcoin Frost";
_ticker = "tBTC";
case CryptoCurrencyNetwork.test4:
_id = "bitcoinFrostTestNet4";
_name = "t4Bitcoin Frost";
_ticker = "t4BTC";
default:
throw Exception("Unsupported network: $network");
}
@ -84,6 +88,19 @@ class BitcoinFrost extends FrostCurrency {
isDown: false,
);
case CryptoCurrencyNetwork.test4:
return NodeModel(
host: "bitcoin-testnet4.stackwallet.com",
port: 50002,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: true,
enabled: true,
coinName: identifier,
isFailover: true,
isDown: false,
);
default:
throw UnimplementedError();
}
@ -96,6 +113,8 @@ class BitcoinFrost extends FrostCurrency {
return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
case CryptoCurrencyNetwork.test:
return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
case CryptoCurrencyNetwork.test4:
return "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043";
default:
throw Exception("Unsupported network: $network");
}
@ -132,6 +151,7 @@ class BitcoinFrost extends FrostCurrency {
feePerKb: BigInt.from(1), // Not used in stack wallet currently
);
case CryptoCurrencyNetwork.test:
case CryptoCurrencyNetwork.test4:
return coinlib.Network(
wifPrefix: 0xef,
p2pkhPrefix: 0x6f,
@ -175,7 +195,7 @@ class BitcoinFrost extends FrostCurrency {
List<int> get possibleMnemonicLengths => [];
@override
AddressType get primaryAddressType => AddressType.frostMS;
AddressType get defaultAddressType => AddressType.frostMS;
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -184,7 +204,7 @@ class BitcoinFrost extends FrostCurrency {
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);
@ -195,6 +215,8 @@ class BitcoinFrost extends FrostCurrency {
return Uri.parse("https://mempool.space/tx/$txid");
case CryptoCurrencyNetwork.test:
return Uri.parse("https://mempool.space/testnet/tx/$txid");
case CryptoCurrencyNetwork.test4:
return Uri.parse("https://mempool.space/testnet4/tx/$txid");
default:
throw Exception(
"Unsupported network for defaultBlockExplorer(): $network",

View file

@ -211,7 +211,7 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
// 0 for bitcoincash: address scheme, 1 for legacy address
final format = bitbox.Address.detectFormat(address);
if (network == CryptoCurrencyNetwork.test) {
if (network.isTestNet) {
return true;
}
@ -336,7 +336,7 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2pkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -345,7 +345,7 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip44;
DerivePathType get defaultDerivePathType => DerivePathType.bip44;
@override
Uri defaultBlockExplorer(String txid) {

View 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?
}

View file

@ -225,7 +225,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2pkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -234,7 +234,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 60;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip44;
DerivePathType get defaultDerivePathType => DerivePathType.bip44;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -314,7 +314,7 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2pkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100);
@ -323,7 +323,7 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => DerivePathType.eCash44;
DerivePathType get defaultDerivePathType => DerivePathType.eCash44;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -1,4 +1,5 @@
import 'package:flutter_libepiccash/lib.dart' as epic;
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/node_model.dart';
import '../../../utilities/default_nodes.dart';
@ -102,7 +103,7 @@ class Epiccash extends Bip39Currency {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 12];
@override
AddressType get primaryAddressType => AddressType.mimbleWimble;
AddressType get defaultAddressType => AddressType.mimbleWimble;
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -111,7 +112,7 @@ class Epiccash extends Bip39Currency {
int get targetBlockTimeSeconds => 60;
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);

View file

@ -1,4 +1,5 @@
import 'package:ethereum_addresses/ethereum_addresses.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/node_model.dart';
import '../../../utilities/default_nodes.dart';
@ -86,7 +87,7 @@ class Ethereum extends Bip39Currency {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.ethereum;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(1000000000000000000);
@ -95,7 +96,7 @@ class Ethereum extends Bip39Currency {
int get targetBlockTimeSeconds => 15;
@override
DerivePathType get primaryDerivePathType => DerivePathType.eth;
DerivePathType get defaultDerivePathType => DerivePathType.eth;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -176,7 +176,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
bool validateSparkAddress(String address) {
return SparkInterface.validateSparkAddress(
address: address,
isTestNet: network == CryptoCurrencyNetwork.test,
isTestNet: network.isTestNet,
);
}
@ -243,7 +243,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2pkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -252,7 +252,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 150;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip44;
DerivePathType get defaultDerivePathType => DerivePathType.bip44;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -256,7 +256,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2wpkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -265,7 +265,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 150;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip84;
DerivePathType get defaultDerivePathType => DerivePathType.bip84;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -100,7 +100,7 @@ class Monero extends CryptonoteCurrency {
int get targetBlockTimeSeconds => 120;
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);

View file

@ -230,7 +230,7 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 12];
@override
AddressType get primaryAddressType => AddressType.p2wpkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -239,7 +239,7 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip84;
DerivePathType get defaultDerivePathType => DerivePathType.bip84;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -53,7 +53,7 @@ class Nano extends NanoCurrency {
int get minConfirms => 1;
@override
AddressType get primaryAddressType => AddressType.nano;
AddressType get defaultAddressType => AddressType.nano;
@override
String get defaultRepresentative =>
@ -85,7 +85,7 @@ class Nano extends NanoCurrency {
}
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);

View file

@ -208,7 +208,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2wpkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(100000000);
@ -217,7 +217,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip84;
DerivePathType get defaultDerivePathType => DerivePathType.bip84;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -228,7 +228,7 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.p2wpkh;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(1000000); // 1*10^6.
@ -237,7 +237,7 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
int get targetBlockTimeSeconds => 600;
@override
DerivePathType get primaryDerivePathType => DerivePathType.bip84;
DerivePathType get defaultDerivePathType => DerivePathType.bip84;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -94,7 +94,7 @@ class Solana extends Bip39Currency {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
@override
AddressType get primaryAddressType => AddressType.solana;
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
@override
BigInt get satsPerCoin => BigInt.from(1000000000);
@ -103,7 +103,7 @@ class Solana extends Bip39Currency {
int get targetBlockTimeSeconds => 1;
@override
DerivePathType get primaryDerivePathType => DerivePathType.solana;
DerivePathType get defaultDerivePathType => DerivePathType.solana;
@override
Uri defaultBlockExplorer(String txid) {

View file

@ -108,7 +108,7 @@ class Stellar extends Bip39Currency {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 12];
@override
AddressType get primaryAddressType => AddressType.stellar;
AddressType get defaultAddressType => AddressType.stellar;
@override
BigInt get satsPerCoin => BigInt.from(
@ -119,7 +119,7 @@ class Stellar extends Bip39Currency {
int get targetBlockTimeSeconds => 5;
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);

View file

@ -195,7 +195,7 @@ class Tezos extends Bip39Currency {
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 12];
@override
AddressType get primaryAddressType => AddressType.tezos;
AddressType get defaultAddressType => AddressType.tezos;
@override
BigInt get satsPerCoin => BigInt.from(1000000);
@ -204,7 +204,7 @@ class Tezos extends Bip39Currency {
int get targetBlockTimeSeconds => 60;
@override
DerivePathType get primaryDerivePathType =>
DerivePathType get defaultDerivePathType =>
throw UnsupportedError("Is this even used?");
@override

View file

@ -95,7 +95,7 @@ class Wownero extends CryptonoteCurrency {
int get targetBlockTimeSeconds => 120;
@override
DerivePathType get primaryDerivePathType => throw UnsupportedError(
DerivePathType get defaultDerivePathType => throw UnsupportedError(
"$runtimeType does not use bitcoin style derivation paths",
);

View file

@ -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';
@ -25,7 +26,11 @@ export 'coins/wownero.dart';
enum CryptoCurrencyNetwork {
main,
test,
stage;
stage,
test4;
bool get isTestNet =>
this == CryptoCurrencyNetwork.test || this == CryptoCurrencyNetwork.test4;
}
abstract class CryptoCurrency {
@ -67,10 +72,10 @@ abstract class CryptoCurrency {
bool get hasBuySupport;
bool get hasMnemonicPassphraseSupport;
List<int> get possibleMnemonicLengths;
AddressType get primaryAddressType;
AddressType get defaultAddressType;
BigInt get satsPerCoin;
int get targetBlockTimeSeconds;
DerivePathType get primaryDerivePathType;
DerivePathType get defaultDerivePathType;
Uri defaultBlockExplorer(String txid);

View file

@ -10,5 +10,5 @@ abstract class CryptonoteCurrency extends CryptoCurrency {
}
@override
AddressType get primaryAddressType => AddressType.cryptonote;
AddressType get defaultAddressType => AddressType.cryptonote;
}

View file

@ -458,7 +458,7 @@ class WalletInfo implements IsarId {
coinName: coin.identifier,
walletId: walletIdOverride ?? const Uuid().v1(),
name: name,
mainAddressType: coin.primaryAddressType,
mainAddressType: coin.defaultAddressType,
restoreHeight: restoreHeight,
otherDataJsonString: otherDataJsonString,
);
@ -510,4 +510,5 @@ abstract class WalletInfoKeys {
static const String enableLelantusScanning = "enableLelantusScanningKey";
static const String firoSparkCacheSetTimestampCache =
"firoSparkCacheSetTimestampCacheKey";
static const String enableOptInRbf = "enableOptInRbfKey";
}

View file

@ -1,13 +1,16 @@
import 'package:cw_monero/pending_monero_transaction.dart';
import 'package:cw_wownero/pending_wownero_transaction.dart';
import 'package:tezart/tezart.dart' as tezart;
import 'package:web3dart/web3dart.dart' as web3dart;
import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../models/isar/models/isar_models.dart';
import '../../models/paynym/paynym_account_lite.dart';
import '../../utilities/amount/amount.dart';
import '../../utilities/enums/fee_rate_type_enum.dart';
import '../isar/models/spark_coin.dart';
import 'package:tezart/tezart.dart' as tezart;
import 'package:web3dart/web3dart.dart' as web3dart;
typedef TxRecipient = ({String address, Amount amount, bool isChange});
class TxData {
final FeeRateType? feeRateType;
@ -27,7 +30,7 @@ class TxData {
final String? memo;
final List<({String address, Amount amount, bool isChange})>? recipients;
final List<TxRecipient>? recipients;
final Set<UTXO>? utxos;
final List<UTXO>? usedUTXOs;
@ -76,6 +79,8 @@ class TxData {
final TransactionV2? tempTx;
final bool ignoreCachedBalanceChecks;
TxData({
this.feeRateType,
this.feeRateAmount,
@ -112,6 +117,7 @@ class TxData {
this.sparkMints,
this.usedSparkCoins,
this.tempTx,
this.ignoreCachedBalanceChecks = false,
});
Amount? get amount => recipients != null && recipients!.isNotEmpty
@ -161,13 +167,7 @@ class TxData {
String? memo,
Set<UTXO>? utxos,
List<UTXO>? usedUTXOs,
List<
({
String address,
Amount amount,
bool isChange,
})>?
recipients,
List<TxRecipient>? recipients,
String? frostMSConfig,
List<String>? frostSigners,
String? changeAddress,
@ -196,6 +196,7 @@ class TxData {
List<TxData>? sparkMints,
List<SparkCoin>? usedSparkCoins,
TransactionV2? tempTx,
bool? ignoreCachedBalanceChecks,
}) {
return TxData(
feeRateType: feeRateType ?? this.feeRateType,
@ -235,6 +236,8 @@ class TxData {
sparkMints: sparkMints ?? this.sparkMints,
usedSparkCoins: usedSparkCoins ?? this.usedSparkCoins,
tempTx: tempTx ?? this.tempTx,
ignoreCachedBalanceChecks:
ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks,
);
}
@ -274,5 +277,6 @@ class TxData {
'sparkMints: $sparkMints, '
'usedSparkCoins: $usedSparkCoins, '
'tempTx: $tempTx, '
'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, '
'}';
}

View file

@ -6,11 +6,18 @@ import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/interfaces/paynym_currency_interface.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/cpfp_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/paynym_interface.dart';
import '../wallet_mixin_interfaces/rbf_interface.dart';
class BitcoinWallet<T extends PaynymCurrencyInterface> extends Bip39HDWallet<T>
with ElectrumXInterface<T>, CoinControlInterface, PaynymInterface<T> {
with
ElectrumXInterface<T>,
CoinControlInterface,
PaynymInterface<T>,
RbfInterface<T>,
CpfpInterface<T> {
@override
int get isarTransactionVersion => 2;

View 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();
}
}

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:isar/isar.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
@ -14,10 +16,15 @@ import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/ordinals_interface.dart';
import '../wallet_mixin_interfaces/rbf_interface.dart';
class LitecoinWallet<T extends ElectrumXCurrencyInterface>
extends Bip39HDWallet<T>
with ElectrumXInterface<T>, CoinControlInterface<T>, OrdinalsInterface<T> {
with
ElectrumXInterface<T>,
CoinControlInterface<T>,
RbfInterface<T>,
OrdinalsInterface<T> {
@override
int get isarTransactionVersion => 2;
@ -285,6 +292,14 @@ class LitecoinWallet<T extends ElectrumXCurrencyInterface>
continue;
}
String? otherData;
if (txData["size"] is int || txData["vsize"] is int) {
otherData = jsonEncode({
TxV2OdKeys.size: txData["size"] as int?,
TxV2OdKeys.vSize: txData["vsize"] as int?,
});
}
final tx = TransactionV2(
walletId: walletId,
blockHash: txData["blockhash"] as String?,
@ -298,7 +313,7 @@ class LitecoinWallet<T extends ElectrumXCurrencyInterface>
outputs: List.unmodifiable(outputs),
type: type,
subType: subType,
otherData: null,
otherData: otherData,
);
txns.add(tx);

View file

@ -46,7 +46,7 @@ class SolanaWallet extends Bip39Wallet<Solana> {
publicKey: List<int>.empty(),
derivationIndex: 0,
derivationPath: DerivationPath()..value = _addressDerivationPath,
type: cryptoCurrency.primaryAddressType,
type: info.mainAddressType,
subType: AddressSubType.receiving,
);
return addressStruct;

View file

@ -1,6 +1,9 @@
import 'dart:io';
import 'package:isar/isar.dart';
import 'package:tezart/tezart.dart' as tezart;
import 'package:tuple/tuple.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
@ -18,8 +21,6 @@ import '../../crypto_currency/crypto_currency.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_wallet.dart';
import 'package:tezart/tezart.dart' as tezart;
import 'package:tuple/tuple.dart';
// const kDefaultTransactionStorageLimit = 496;
// const kDefaultTransactionGasLimit = 10600;
@ -83,7 +84,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
publicKey: keyStore.publicKey.toUint8ListFromBase58CheckEncoded,
derivationIndex: 0,
derivationPath: DerivationPath()..value = derivationPath,
type: info.coin.primaryAddressType,
type: info.mainAddressType,
subType: AddressSubType.receiving,
);
}

View file

@ -1,13 +1,14 @@
import 'package:bip39/bip39.dart' as bip39;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:isar/isar.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
import 'bip39_wallet.dart';
import '../wallet_mixin_interfaces/multi_address_interface.dart';
import 'bip39_wallet.dart';
abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
with MultiAddressInterface<T> {
@ -66,7 +67,7 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
final address = await _generateAddress(
chain: chain,
index: index,
derivePathType: info.coin.primaryDerivePathType,
derivePathType: _fromAddressType(info.mainAddressType),
);
await mainDB.updateOrPutAddresses([address]);
@ -88,7 +89,7 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
final address = await _generateAddress(
chain: chain,
index: index,
derivePathType: info.coin.primaryDerivePathType,
derivePathType: _fromAddressType(info.mainAddressType),
);
await mainDB.updateOrPutAddresses([address]);
@ -101,7 +102,7 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
final address = await _generateAddress(
chain: 0, // receiving
index: 0, // initial index
derivePathType: info.coin.primaryDerivePathType,
derivePathType: _fromAddressType(info.mainAddressType),
);
await mainDB.updateOrPutAddresses([address]);
@ -118,6 +119,37 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
// ========== Private ========================================================
DerivePathType _fromAddressType(AddressType addressType) {
switch (addressType) {
case AddressType.p2pkh:
// DerivePathType.bip44:
// DerivePathType.bch44:
// DerivePathType.eCash44:
// Should be one of the above due to silly case due to bch and ecash
return info.coin.defaultDerivePathType;
case AddressType.p2sh:
return DerivePathType.bip49;
case AddressType.p2wpkh:
return DerivePathType.bip84;
case AddressType.p2tr:
return DerivePathType.bip86;
case AddressType.solana:
return DerivePathType.solana;
case AddressType.ethereum:
return DerivePathType.eth;
default:
throw ArgumentError(
"Incompatible AddressType \"$addressType\" passed to DerivePathType.fromAddressType()",
);
}
}
Future<Address> _generateAddress({
required int chain,
required int index,

View file

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

View file

@ -8,7 +8,6 @@ import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../models/signing_data.dart';
import '../../../utilities/logger.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
@ -27,7 +26,7 @@ mixin BCashInterface<T extends ElectrumXCurrencyInterface>
// TODO: use coinlib
final builder = bitbox.Bitbox.transactionBuilder(
testnet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
testnet: cryptoCurrency.network.isTestNet,
);
builder.setVersion(cryptoCurrency.transactionVersion);
@ -100,7 +99,7 @@ mixin BCashInterface<T extends ElectrumXCurrencyInterface>
network: bitbox_utils.Network(
cryptoCurrency.networkParams.privHDPrefix,
cryptoCurrency.networkParams.pubHDPrefix,
cryptoCurrency.network == CryptoCurrencyNetwork.test,
cryptoCurrency.network.isTestNet,
cryptoCurrency.networkParams.p2pkhPrefix,
cryptoCurrency.networkParams.wifPrefix,
cryptoCurrency.networkParams.p2pkhPrefix,

View file

@ -0,0 +1,7 @@
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import 'electrumx_interface.dart';
mixin CpfpInterface<T extends ElectrumXCurrencyInterface>
on ElectrumXInterface<T> {
//
}

View file

@ -26,7 +26,9 @@ import '../../models/tx_data.dart';
import '../impl/bitcoin_wallet.dart';
import '../impl/peercoin_wallet.dart';
import '../intermediate/bip39_hd_wallet.dart';
import 'cpfp_interface.dart';
import 'paynym_interface.dart';
import 'rbf_interface.dart';
mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
on Bip39HDWallet<T> {
@ -122,12 +124,16 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
utxos ?? await mainDB.getUTXOs(walletId).findAll();
final currentChainHeight = await chainHeight;
final canCPFP = this is CpfpInterface && coinControl;
final spendableOutputs = availableOutputs
.where(
(e) =>
!e.isBlocked &&
(e.used != true) &&
e.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms),
(canCPFP ||
e.isConfirmed(
currentChainHeight, cryptoCurrency.minConfirms)),
)
.toList();
final spendableSatoshiValue =
@ -628,6 +634,11 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
outputs: [],
);
// TODO: [prio=high]: check this opt in rbf
final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF
? 0xffffffff - 10
: 0xffffffff - 1;
// Add transaction inputs
for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxoSigningData[i].utxo.txid;
@ -659,7 +670,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
input = coinlib.P2PKHInput(
prevOut: prevOutpoint,
publicKey: utxoSigningData[i].keyPair!.publicKey,
sequence: 0xffffffff - 1,
sequence: sequence,
);
// TODO: fix this as it is (probably) wrong!
@ -670,14 +681,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
// program: coinlib.MultisigProgram.decompile(
// utxoSigningData[i].redeemScript!,
// ),
// sequence: 0xffffffff - 1,
// sequence: sequence,
// );
case DerivePathType.bip84:
input = coinlib.P2WPKHInput(
prevOut: prevOutpoint,
publicKey: utxoSigningData[i].keyPair!.publicKey,
sequence: 0xffffffff - 1,
sequence: sequence,
);
case DerivePathType.bip86:
@ -695,7 +706,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: input.scriptSig.toHex,
scriptSigAsm: null,
sequence: 0xffffffff - 1,
sequence: sequence,
outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: utxoSigningData[i].utxo.txid,
vout: utxoSigningData[i].utxo.vout,
@ -1648,12 +1659,21 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
if (customSatsPerVByte != null) {
// check for send all
bool isSendAll = false;
if (txData.amount == info.cachedBalance.spendable) {
if (txData.ignoreCachedBalanceChecks ||
txData.amount == info.cachedBalance.spendable) {
isSendAll = true;
}
final bool coinControl = utxos != null;
if (coinControl &&
this is CpfpInterface &&
txData.amount ==
(info.cachedBalance.spendable +
info.cachedBalance.pendingSpendable)) {
isSendAll = true;
}
final result = await coinSelection(
txData: txData.copyWith(feeRateAmount: -1),
isSendAll: isSendAll,

View file

@ -6,6 +6,8 @@ import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
import 'package:decimal/decimal.dart';
import 'package:isar/isar.dart';
import 'package:lelantus/lelantus.dart' as lelantus;
import 'package:tuple/tuple.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../models/lelantus_fee_data.dart';
@ -15,12 +17,10 @@ import '../../../utilities/extensions/impl/uint8_list.dart';
import '../../../utilities/format.dart';
import '../../../utilities/logger.dart';
import '../../api/lelantus_ffi_wrapper.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
import 'electrumx_interface.dart';
import 'package:tuple/tuple.dart';
mixin LelantusInterface<T extends ElectrumXCurrencyInterface>
on Bip39HDWallet<T>, ElectrumXInterface<T> {
@ -38,7 +38,7 @@ mixin LelantusInterface<T extends ElectrumXCurrencyInterface>
spendAmount: amount,
subtractFeeFromAmount: true,
lelantusEntries: lelantusEntries,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: cryptoCurrency.network.isTestNet,
);
return Amount(
@ -526,7 +526,7 @@ mixin LelantusInterface<T extends ElectrumXCurrencyInterface>
int.parse(coin.value),
mintKeyPair.privateKey.data.toHex,
coin.mintIndex,
isTestnet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestnet: cryptoCurrency.network.isTestNet,
);
final bool isUsed = usedSerialNumbersSet.contains(serialNumber);
@ -1033,7 +1033,7 @@ mixin LelantusInterface<T extends ElectrumXCurrencyInterface>
await mainDB.getHighestUsedMintIndex(walletId: walletId);
final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1;
final isTestnet = cryptoCurrency.network == CryptoCurrencyNetwork.test;
final isTestnet = cryptoCurrency.network.isTestNet;
final root = await getRootHDNode();

View file

@ -93,7 +93,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
publicKey: publicKey.toUint8ListFromHex,
derivationIndex: 0,
derivationPath: null,
type: cryptoCurrency.primaryAddressType,
type: info.mainAddressType,
subType: AddressSubType.receiving,
);
}
@ -599,7 +599,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
value: tx["account"].toString(),
derivationIndex: 0,
derivationPath: null,
type: info.coin.primaryAddressType,
type: info.mainAddressType,
subType: AddressSubType.nonWallet,
);
final Tuple2<Transaction, Address> tuple = Tuple2(transaction, address);

View file

@ -68,7 +68,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
final root = await _getRootNode();
final node = root.derivePath(
_basePaynymDerivePath(
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
);
return node;
@ -159,7 +159,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
final root = await _getRootNode();
final node = root.derivePath(
_basePaynymDerivePath(
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
);
@ -182,7 +182,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
derivationPath: DerivationPath()
..value = _receivingPaynymAddressDerivationPath(
index,
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
type: generateSegwitAddress ? AddressType.p2wpkh : AddressType.p2pkh,
subType: AddressSubType.paynymReceive,
@ -219,7 +219,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
derivationPath: DerivationPath()
..value = _sendPaynymAddressDerivationPath(
index,
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
type: AddressType.nonWallet,
subType: AddressSubType.paynymSend,
@ -314,7 +314,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
final node = root
.derivePath(
_basePaynymDerivePath(
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
)
.derive(0);
@ -330,7 +330,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
final paymentCode = PaymentCode.fromBip32Node(
node.derivePath(
_basePaynymDerivePath(
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
),
networkType: networkType,
@ -1469,7 +1469,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
final root = await _getRootNode();
final node = root.derivePath(
_basePaynymDerivePath(
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
);
final paymentCode = PaymentCode.fromBip32Node(
@ -1497,7 +1497,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
derivationIndex: 0,
derivationPath: DerivationPath()
..value = _notificationDerivationPath(
testnet: info.coin.network == CryptoCurrencyNetwork.test,
testnet: info.coin.network.isTestNet,
),
type: AddressType.p2pkh,
subType: AddressSubType.paynymNotification,
@ -1617,6 +1617,24 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
final List<Map<String, dynamic>> allTxHashes =
await fetchHistory(allAddressesSet);
final unconfirmedTxs = await mainDB.isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.filter()
.heightIsNull()
.or()
.heightEqualTo(0)
.txidProperty()
.findAll();
allTxHashes.addAll(
unconfirmedTxs.map(
(e) => {
"tx_hash": e,
},
),
);
// Only parse new txs (not in db yet).
final List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) {
@ -1630,16 +1648,36 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
// 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,
);
final txid = txHash["tx_hash"] as String;
final Map<String, dynamic> tx;
try {
tx = await electrumXCachedClient.getTransaction(
txHash: txid,
verbose: true,
cryptoCurrency: cryptoCurrency,
);
} catch (e) {
// tx no longer exists then delete from local db
if (e.toString().contains(
"JSON-RPC error 2: daemon error: DaemonError({'code': -5, "
"'message': 'No such mempool or blockchain transaction",
)) {
await mainDB.isar.writeTxn(
() async => await mainDB.isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.filter()
.txidEqualTo(txid)
.deleteFirst(),
);
continue;
} else {
rethrow;
}
}
// Only tx to list once.
if (allTransactions
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
-1) {
if (allTransactions.indexWhere((e) => e["txid"] == txid) == -1) {
tx["height"] = txHash["height"];
allTransactions.add(tx);
}
@ -1794,6 +1832,14 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
continue;
}
String? otherData;
if (txData["size"] is int || txData["vsize"] is int) {
otherData = jsonEncode({
TxV2OdKeys.size: txData["size"] as int?,
TxV2OdKeys.vSize: txData["vsize"] as int?,
});
}
final tx = TransactionV2(
walletId: walletId,
blockHash: txData["blockhash"] as String?,
@ -1807,7 +1853,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
outputs: List.unmodifiable(outputs),
type: type,
subType: subType,
otherData: null,
otherData: otherData,
);
txns.add(tx);

View file

@ -0,0 +1,283 @@
import 'dart:convert';
import 'package:isar/isar.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/fee_rate_type_enum.dart';
import '../../../utilities/logger.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart';
import 'electrumx_interface.dart';
typedef TxSize = ({int real, int virtual});
mixin RbfInterface<T extends ElectrumXCurrencyInterface>
on ElectrumXInterface<T> {
bool get flagOptInRBF =>
info.otherData[WalletInfoKeys.enableOptInRbf] as bool? ?? false;
Future<TxSize?> getVSize(String txid) async {
final tx = await electrumXCachedClient.getTransaction(
txHash: txid,
cryptoCurrency: cryptoCurrency,
);
try {
return (real: tx["size"] as int, virtual: tx["vsize"] as int);
} catch (_) {
return null;
}
}
Future<TransactionV2> updateVSize(TransactionV2 transactionV2) async {
final size = await getVSize(transactionV2.txid);
final otherData = jsonDecode(transactionV2.otherData ?? "{}");
otherData[TxV2OdKeys.vSize] = size!.virtual;
otherData[TxV2OdKeys.size] = size.real;
final updatedTx = transactionV2.copyWith(otherData: jsonEncode(otherData));
await mainDB.updateOrPutTransactionV2s([updatedTx]);
return updatedTx;
}
Future<TxData> prepareRbfSend({
required TransactionV2 oldTransaction,
required int newRate,
}) async {
final note = await mainDB.isar.transactionNotes
.where()
.walletIdEqualTo(walletId)
.filter()
.txidEqualTo(oldTransaction.txid)
.findFirst();
final Set<UTXO> utxos = {};
for (final input in oldTransaction.inputs) {
final utxo = UTXO(
walletId: walletId,
txid: input.outpoint!.txid,
vout: input.outpoint!.vout,
value: input.value.toInt(),
name: "rbf",
isBlocked: false,
blockedReason: null,
isCoinbase: false,
blockHash: "rbf",
blockHeight: 1,
blockTime: 1,
used: false,
address: input.addresses.first,
);
utxos.add(utxo);
}
final List<TxRecipient> recipients = [];
for (final output in oldTransaction.outputs) {
if (output.addresses.length != 1) {
throw UnsupportedError(
"Unexpected output.addresses.length: ${output.addresses.length}",
);
}
final address = output.addresses.first;
final addressModel = await mainDB.getAddress(walletId, address);
final isChange = addressModel?.subType == AddressSubType.change;
recipients.add(
(
address: address,
amount: Amount(
rawValue: output.value,
fractionDigits: cryptoCurrency.fractionDigits),
isChange: isChange,
),
);
}
final oldFee = oldTransaction
.getFee(fractionDigits: cryptoCurrency.fractionDigits)
.raw;
final inSum = utxos
.map((e) => BigInt.from(e.value))
.fold(BigInt.zero, (p, e) => p + e);
final noChange =
recipients.map((e) => e.isChange).fold(false, (p, e) => p || e) ==
false;
final otherAvailableUtxos = await mainDB
.getUTXOs(walletId)
.filter()
.isBlockedEqualTo(false)
.and()
.group(
(q) => q.usedIsNull().or().usedEqualTo(false),
)
.findAll();
final height = await chainHeight;
otherAvailableUtxos.removeWhere(
(e) => !e.isConfirmed(
height,
cryptoCurrency.minConfirms,
),
);
TxData txData = TxData(
recipients: recipients,
feeRateType: FeeRateType.custom,
satsPerVByte: newRate,
utxos: utxos,
ignoreCachedBalanceChecks: true,
note: note?.value ?? "",
);
if (otherAvailableUtxos.isEmpty && noChange && recipients.length == 1) {
// safe to assume send all?
txData = txData.copyWith(
recipients: [
(
address: recipients.first.address,
amount: Amount(
rawValue: inSum,
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: false,
),
],
);
Logging.instance.log(
"RBF on assumed send all",
level: LogLevel.Debug,
);
return await prepareSend(txData: txData);
} else if (txData.recipients!.where((e) => e.isChange).length == 1) {
final newFee = BigInt.from(oldTransaction.vSize! * newRate);
final feeDifferenceRequired = newFee - oldFee;
if (feeDifferenceRequired < BigInt.zero) {
throw Exception("Negative new fee in RBF found");
} else if (feeDifferenceRequired == BigInt.zero) {
throw Exception("New fee in RBF has not changed at all");
}
final indexOfChangeOutput =
txData.recipients!.indexWhere((e) => e.isChange);
final removed = txData.recipients!.removeAt(indexOfChangeOutput);
BigInt newChangeAmount = removed.amount.raw - feeDifferenceRequired;
if (newChangeAmount >= BigInt.zero) {
if (newChangeAmount >= cryptoCurrency.dustLimit.raw) {
// yay we have enough
// update recipients
txData.recipients!.insert(
indexOfChangeOutput,
(
address: removed.address,
amount: Amount(
rawValue: newChangeAmount,
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: removed.isChange,
),
);
Logging.instance.log(
"RBF with same utxo set with increased fee and reduced change",
level: LogLevel.Debug,
);
} else {
// new change amount is less than dust limit.
// TODO: check if worth adding another utxo?
// depending on several factors, it may be cheaper to just add]
// the dust to the fee...
// we'll do that for now... aka remove the change output entirely
// which now that I think about it, will reduce the size of the tx...
// oh well...
// do nothing here as we already removed the change output above
Logging.instance.log(
"RBF with same utxo set with increased fee and no change",
level: LogLevel.Debug,
);
}
return await buildTransaction(
txData: txData.copyWith(
usedUTXOs: txData.utxos!.toList(),
fee: Amount(
rawValue: newFee,
fractionDigits: cryptoCurrency.fractionDigits,
),
),
utxoSigningData: await fetchBuildTxData(txData.utxos!.toList()),
);
// if change amount is negative
} else {
// we need more utxos
if (otherAvailableUtxos.isEmpty) {
throw Exception("Insufficient funds to pay for increased fee");
}
final List<UTXO> extraUtxos = [];
for (int i = 0; i < otherAvailableUtxos.length; i++) {
final utxoToAdd = otherAvailableUtxos[i];
newChangeAmount += BigInt.from(utxoToAdd.value);
extraUtxos.add(utxoToAdd);
if (newChangeAmount >= cryptoCurrency.dustLimit.raw) {
break;
}
}
if (newChangeAmount < cryptoCurrency.dustLimit.raw) {
throw Exception("Insufficient funds to pay for increased fee");
}
txData.recipients!.insert(
indexOfChangeOutput,
(
address: removed.address,
amount: Amount(
rawValue: newChangeAmount,
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: removed.isChange,
),
);
final newUtxoSet = {
...txData.utxos!,
...extraUtxos,
};
// TODO: remove assert
assert(newUtxoSet.length == txData.utxos!.length + extraUtxos.length);
Logging.instance.log(
"RBF with ${extraUtxos.length} extra utxo(s)"
" added to pay for the new fee",
level: LogLevel.Debug,
);
return await buildTransaction(
txData: txData.copyWith(
utxos: newUtxoSet,
usedUTXOs: newUtxoSet.toList(),
fee: Amount(
rawValue: newFee,
fractionDigits: cryptoCurrency.fractionDigits,
),
),
utxoSigningData: await fetchBuildTxData(newUtxoSet.toList()),
);
}
} else {
// TODO handle building a tx here in this case
throw Exception(
"Unexpected number of change outputs found:"
" ${txData.recipients!.where((e) => e.isChange).length}",
);
}
}
}

View file

@ -18,7 +18,6 @@ import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
import '../../../utilities/extensions/extensions.dart';
import '../../../utilities/logger.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../../isar/models/spark_coin.dart';
import '../../isar/models/wallet_info.dart';
@ -86,7 +85,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
if (_sparkChangeAddressCached == null) {
final root = await getRootHDNode();
final String derivationPath;
if (cryptoCurrency.network == CryptoCurrencyNetwork.test) {
if (cryptoCurrency.network.isTestNet) {
derivationPath =
"$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex";
} else {
@ -98,7 +97,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
privateKey: keys.privateKey.data,
index: kDefaultSparkIndex,
diversifier: kSparkChange,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: cryptoCurrency.network.isTestNet,
);
}
} catch (e, s) {
@ -158,7 +157,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
final root = await getRootHDNode();
final String derivationPath;
if (cryptoCurrency.network == CryptoCurrencyNetwork.test) {
if (cryptoCurrency.network.isTestNet) {
derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex";
} else {
derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex";
@ -169,7 +168,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
privateKey: keys.privateKey.data,
index: kDefaultSparkIndex,
diversifier: diversifier,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: cryptoCurrency.network.isTestNet,
);
return Address(
@ -335,7 +334,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
final root = await getRootHDNode();
final String derivationPath;
if (cryptoCurrency.network == CryptoCurrencyNetwork.test) {
if (cryptoCurrency.network.isTestNet) {
derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex";
} else {
derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex";
@ -704,7 +703,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
groupId: groupId,
privateKeyHexSet: privateKeyHexSet,
walletId: walletId,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: cryptoCurrency.network.isTestNet,
),
);
@ -836,7 +835,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
groupId: groupId,
privateKeyHexSet: privateKeyHexSet,
walletId: walletId,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
isTestNet: cryptoCurrency.network.isTestNet,
),
);
newlyIdCoins.addAll(myCoins);

View file

@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../utilities/amount/amount.dart';
import '../utilities/amount/amount_formatter.dart';
import '../wallets/crypto_currency/crypto_currency.dart'; // Update with your actual path
class BoostFeeSlider extends ConsumerStatefulWidget {
final CryptoCurrency coin;
final void Function(BigInt) onFeeChanged;
final BigInt min;
final BigInt max;
const BoostFeeSlider({
super.key,
required this.coin,
required this.onFeeChanged,
required this.min,
required this.max,
});
@override
ConsumerState<BoostFeeSlider> createState() => _BoostFeeSliderState();
}
class _BoostFeeSliderState extends ConsumerState<BoostFeeSlider> {
double _currentSliderValue = 0;
late TextEditingController _textEditingController;
@override
void initState() {
super.initState();
_currentSliderValue = widget.min.toDouble();
_textEditingController = TextEditingController(
text: ref.read(pAmountFormatter(widget.coin)).format(
Amount(
rawValue: BigInt.from(_currentSliderValue),
fractionDigits: widget.coin.fractionDigits,
),
withUnitName: false,
),
);
_textEditingController.addListener(() {
// TODO: value.replaceAll(',', '') doesn't work for certain locales
final double? value =
double.tryParse(_textEditingController.text.replaceAll(',', ''));
if (value != null) {
final BigInt bigIntValue = BigInt.from(
value * BigInt.from(10).pow(widget.coin.fractionDigits).toInt(),
);
if (bigIntValue >= widget.min && bigIntValue <= widget.max) {
setState(() {
_currentSliderValue = value;
widget.onFeeChanged(bigIntValue);
});
}
}
});
}
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
Row(
children: [
Expanded(
child: Slider(
value: _currentSliderValue,
min: widget.min.toDouble(),
max: widget.max.toDouble(),
divisions: (widget.max - widget.min).toInt(),
label: ref.read(pAmountFormatter(widget.coin)).format(
Amount(
rawValue: BigInt.from(_currentSliderValue),
fractionDigits: widget.coin.fractionDigits,
),
),
onChanged: (value) {
setState(() {
_currentSliderValue = value;
_textEditingController.text =
ref.read(pAmountFormatter(widget.coin)).format(
Amount(
rawValue: BigInt.from(_currentSliderValue),
fractionDigits: widget.coin.fractionDigits,
),
);
widget.onFeeChanged(BigInt.from(_currentSliderValue));
});
},
),
),
SizedBox(
width: 16 + // Left and right padding.
122 / 8 * widget.coin.fractionDigits + // Variable width.
8 * widget.coin.ticker.length, // End padding for ticker.
child: TextField(
controller: _textEditingController,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
],
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
// TODO: value.replaceAll(',', '') doesn't work for certain locales
final double? newValue =
double.tryParse(value.replaceAll(',', ''));
if (newValue != null) {
final BigInt bigIntValue = BigInt.from(
newValue *
BigInt.from(10)
.pow(widget.coin.fractionDigits)
.toInt(),
);
if (bigIntValue >= widget.min &&
bigIntValue <= widget.max) {
setState(() {
_currentSliderValue = newValue;
widget.onFeeChanged(bigIntValue);
});
}
}
},
),
),
],
),
],
),
);
}
}

View file

@ -1,32 +1,45 @@
import 'dart:math';
import 'package:flutter/material.dart';
import '../utilities/text_styles.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
/// This has limitations. At least one of [pow] or [min] must be set to 1
class FeeSlider extends StatefulWidget {
const FeeSlider({
super.key,
required this.onSatVByteChanged,
required this.coin,
this.min = 1,
this.max = 5,
this.pow = 4,
this.showWU = false,
this.overrideLabel,
});
final CryptoCurrency coin;
final double min;
final double max;
final double pow;
final bool showWU;
final void Function(int) onSatVByteChanged;
final String? overrideLabel;
@override
State<FeeSlider> createState() => _FeeSliderState();
}
class _FeeSliderState extends State<FeeSlider> {
static const double min = 1;
static const double max = 4;
double sliderValue = 0;
int rate = min.toInt();
late int rate;
@override
void initState() {
rate = widget.min.toInt();
super.initState();
}
@override
Widget build(BuildContext context) {
@ -36,7 +49,7 @@ class _FeeSliderState extends State<FeeSlider> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.showWU ? "sat/WU" : "sat/vByte",
widget.overrideLabel ?? (widget.showWU ? "sat/WU" : "sat/vByte"),
style: STextStyles.smallMed12(context),
),
Text(
@ -50,7 +63,10 @@ class _FeeSliderState extends State<FeeSlider> {
onChanged: (value) {
setState(() {
sliderValue = value;
final number = pow(sliderValue * (max - min) + min, 4).toDouble();
final number = pow(
sliderValue * (widget.max - widget.min) + widget.min,
widget.pow,
).toDouble();
if (widget.coin is Dogecoin) {
rate = (number * 1000).toInt();
} else {

View file

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

View file

@ -9,6 +9,7 @@
*/
import 'package:flutter/material.dart';
import '../themes/stack_colors.dart';
import '../utilities/text_styles.dart';
import '../utilities/util.dart';
@ -148,6 +149,7 @@ class StackOkDialog extends StatelessWidget {
required this.title,
this.message,
this.desktopPopRootNavigator = false,
this.maxWidth,
});
final bool desktopPopRootNavigator;
@ -158,6 +160,7 @@ class StackOkDialog extends StatelessWidget {
final String title;
final String? message;
final double? maxWidth;
@override
Widget build(BuildContext context) {
@ -165,17 +168,20 @@ class StackOkDialog extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
title,
style: STextStyles.pageTitleH2(context),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
title,
style: STextStyles.pageTitleH2(context),
),
),
),
icon != null ? icon! : Container(),
],
icon != null ? icon! : Container(),
],
),
),
if (message != null)
const SizedBox(
@ -194,40 +200,44 @@ class StackOkDialog extends StatelessWidget {
const SizedBox(
height: 20,
),
Row(
children: [
leftButton == null
? const Spacer()
: Expanded(child: leftButton!),
const SizedBox(
width: 8,
),
Expanded(
child: TextButton(
onPressed: !Util.isDesktop
? () {
Navigator.of(context).pop();
onOkPressed?.call("OK");
}
: () {
if (desktopPopRootNavigator) {
Navigator.of(context, rootNavigator: true).pop();
} else {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Row(
children: [
leftButton == null
? const Spacer()
: Expanded(child: leftButton!),
const SizedBox(
width: 8,
),
Expanded(
child: TextButton(
onPressed: !Util.isDesktop
? () {
Navigator.of(context).pop();
onOkPressed?.call("OK");
}
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context),
: () {
if (desktopPopRootNavigator) {
Navigator.of(context, rootNavigator: true).pop();
} else {
int count = 0;
Navigator.of(context)
.popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
}
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context),
),
),
),
),
],
],
),
),
],
),

View file

@ -44,6 +44,7 @@ const _prefix = "Campfire";
const _separator = "";
const _suffix = "";
const _appDataDirName = "campfire";
const _shortDescriptionText = "Your privacy. Your wallet. Your Firo.";
const _commitHash = "$BUILT_COMMIT_HASH";
const Set<AppFeature> _features = {};

View file

@ -38,6 +38,7 @@ const _prefix = "Stack";
const _separator = " ";
const _suffix = "Duo";
const _appDataDirName = "stackduo";
const _shortDescriptionText = "An open-source, multicoin wallet for everyone";
const _commitHash = "$BUILT_COMMIT_HASH";
const Set<AppFeature> _features = {
@ -56,7 +57,9 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Monero(CryptoCurrencyNetwork.main),
BitcoinFrost(CryptoCurrencyNetwork.main),
Bitcoin(CryptoCurrencyNetwork.test),
Bitcoin(CryptoCurrencyNetwork.test4),
BitcoinFrost(CryptoCurrencyNetwork.test),
BitcoinFrost(CryptoCurrencyNetwork.test4),
]);
EOF

View file

@ -38,6 +38,7 @@ const _prefix = "Stack";
const _separator = " ";
const _suffix = "Wallet";
const _appDataDirName = "stackwallet";
const _shortDescriptionText = "An open-source, multicoin wallet for everyone";
const _commitHash = "$BUILT_COMMIT_HASH";
const Set<AppFeature> _features = {
@ -54,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),
@ -69,13 +71,15 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Tezos(CryptoCurrencyNetwork.main),
Wownero(CryptoCurrencyNetwork.main),
Bitcoin(CryptoCurrencyNetwork.test),
BitcoinFrost(CryptoCurrencyNetwork.test),
Litecoin(CryptoCurrencyNetwork.test),
Bitcoin(CryptoCurrencyNetwork.test4),
Bitcoincash(CryptoCurrencyNetwork.test),
Firo(CryptoCurrencyNetwork.test),
BitcoinFrost(CryptoCurrencyNetwork.test),
BitcoinFrost(CryptoCurrencyNetwork.test4),
Dogecoin(CryptoCurrencyNetwork.test),
Stellar(CryptoCurrencyNetwork.test),
Firo(CryptoCurrencyNetwork.test),
Litecoin(CryptoCurrencyNetwork.test),
Peercoin(CryptoCurrencyNetwork.test),
Stellar(CryptoCurrencyNetwork.test),
]);
EOF