mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
Merge remote-tracking branch 'origin/staging' into arti
This commit is contained in:
commit
0745ee6617
82 changed files with 2264 additions and 479 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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!),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
import '../../../themes/coin_image_provider.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/assets.dart';
|
||||
|
@ -64,6 +65,7 @@ class _RestoringDialogState extends ConsumerState<SendingTransactionDialog> {
|
|||
|
||||
if (Util.isDesktop) {
|
||||
return DesktopDialog(
|
||||
maxHeight: assetPath.endsWith(".gif") ? double.infinity : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Column(
|
||||
|
@ -77,8 +79,10 @@ class _RestoringDialogState extends ConsumerState<SendingTransactionDialog> {
|
|||
height: 40,
|
||||
),
|
||||
assetPath.endsWith(".gif")
|
||||
? Image.file(
|
||||
File(assetPath),
|
||||
? Flexible(
|
||||
child: Image.file(
|
||||
File(assetPath),
|
||||
),
|
||||
)
|
||||
: ProgressAndSuccess(
|
||||
controller: _progressAndSuccessController!,
|
||||
|
|
|
@ -48,18 +48,17 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> {
|
|||
final _searchFocusNode = FocusNode();
|
||||
|
||||
void onTap(int index) {
|
||||
if (currenciesWithoutSelected[index] == current || current.isEmpty) {
|
||||
// ignore if already selected currency
|
||||
return;
|
||||
}
|
||||
current = currenciesWithoutSelected[index];
|
||||
currenciesWithoutSelected.remove(current);
|
||||
currenciesWithoutSelected.insert(0, current);
|
||||
|
||||
if (Util.isDesktop) {
|
||||
setState(() {
|
||||
current = currenciesWithoutSelected[index];
|
||||
});
|
||||
setState(() {});
|
||||
} else {
|
||||
if (currenciesWithoutSelected[index] == current || current.isEmpty) {
|
||||
// ignore if already selected currency
|
||||
return;
|
||||
}
|
||||
current = currenciesWithoutSelected[index];
|
||||
currenciesWithoutSelected.remove(current);
|
||||
currenciesWithoutSelected.insert(0, current);
|
||||
ref.read(prefsChangeNotifierProvider).currency = current;
|
||||
|
||||
if (ref.read(prefsChangeNotifierProvider).externalCalls) {
|
||||
|
@ -104,13 +103,7 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> {
|
|||
void initState() {
|
||||
_searchController = TextEditingController();
|
||||
if (Util.isDesktop) {
|
||||
currenciesWithoutSelected =
|
||||
ref.read(baseCurrenciesProvider).map.keys.toList();
|
||||
current = ref.read(prefsChangeNotifierProvider).currency;
|
||||
if (current.isNotEmpty) {
|
||||
currenciesWithoutSelected.remove(current);
|
||||
currenciesWithoutSelected.insert(0, current);
|
||||
}
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
@ -129,16 +122,16 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> {
|
|||
if (!isDesktop) {
|
||||
current = ref
|
||||
.watch(prefsChangeNotifierProvider.select((value) => value.currency));
|
||||
}
|
||||
|
||||
currenciesWithoutSelected = ref
|
||||
.watch(baseCurrenciesProvider.select((value) => value.map))
|
||||
.keys
|
||||
.toList();
|
||||
currenciesWithoutSelected = ref
|
||||
.watch(baseCurrenciesProvider.select((value) => value.map))
|
||||
.keys
|
||||
.toList();
|
||||
|
||||
if (current.isNotEmpty) {
|
||||
currenciesWithoutSelected.remove(current);
|
||||
currenciesWithoutSelected.insert(0, current);
|
||||
}
|
||||
if (current.isNotEmpty) {
|
||||
currenciesWithoutSelected.remove(current);
|
||||
currenciesWithoutSelected.insert(0, current);
|
||||
}
|
||||
|
||||
currenciesWithoutSelected = _filtered();
|
||||
|
|
|
@ -13,8 +13,8 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'add_edit_node_view.dart';
|
||||
import '../../sub_widgets/nodes_list.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../../themes/coin_icon_provider.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
|
@ -26,7 +26,8 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
|||
import '../../../../widgets/custom_buttons/blue_text_button.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import '../../sub_widgets/nodes_list.dart';
|
||||
import 'add_edit_node_view.dart';
|
||||
|
||||
class CoinNodesView extends ConsumerStatefulWidget {
|
||||
const CoinNodesView({
|
||||
|
@ -59,7 +60,10 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
|
|||
Widget build(BuildContext context) {
|
||||
if (Util.isDesktop) {
|
||||
return DesktopDialog(
|
||||
maxHeight: null,
|
||||
maxWidth: 580,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
|
@ -129,11 +133,15 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
|
|||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: NodesList(
|
||||
coin: widget.coin,
|
||||
popBackToRoute: CoinNodesView.routeName,
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: NodesList(
|
||||
coin: widget.coin,
|
||||
popBackToRoute: CoinNodesView.routeName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -30,6 +30,7 @@ class PriceAPI {
|
|||
BitcoinFrost: "bitcoin",
|
||||
Litecoin: "litecoin",
|
||||
Bitcoincash: "bitcoin-cash",
|
||||
Dash: "dash",
|
||||
Dogecoin: "dogecoin",
|
||||
Epiccash: "epic-cash",
|
||||
Ecash: "ecash",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
247
lib/wallets/crypto_currency/coins/dash.dart
Normal file
247
lib/wallets/crypto_currency/coins/dash.dart
Normal file
|
@ -0,0 +1,247 @@
|
|||
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
|
||||
|
||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../../models/node_model.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
import '../../../utilities/default_nodes.dart';
|
||||
import '../../../utilities/enums/derive_path_type_enum.dart';
|
||||
import '../crypto_currency.dart';
|
||||
import '../interfaces/electrumx_currency_interface.dart';
|
||||
import '../intermediate/bip39_hd_currency.dart';
|
||||
|
||||
class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
|
||||
Dash(super.network) {
|
||||
_idMain = "dash";
|
||||
_uriScheme = "dash";
|
||||
switch (network) {
|
||||
case CryptoCurrencyNetwork.main:
|
||||
_id = _idMain;
|
||||
_name = "Dash";
|
||||
_ticker = "DASH";
|
||||
// case CryptoCurrencyNetwork.test:
|
||||
// _id = "dashTestNet";
|
||||
// _name = "tDash";
|
||||
// _ticker = "tDASH";
|
||||
default:
|
||||
throw Exception("Unsupported network: $network");
|
||||
}
|
||||
}
|
||||
|
||||
late final String _id;
|
||||
@override
|
||||
String get identifier => _id;
|
||||
|
||||
late final String _idMain;
|
||||
@override
|
||||
String get mainNetId => _idMain;
|
||||
|
||||
late final String _name;
|
||||
@override
|
||||
String get prettyName => _name;
|
||||
|
||||
late final String _uriScheme;
|
||||
@override
|
||||
String get uriScheme => _uriScheme;
|
||||
|
||||
late final String _ticker;
|
||||
@override
|
||||
String get ticker => _ticker;
|
||||
|
||||
@override
|
||||
bool get torSupport => true;
|
||||
|
||||
@override
|
||||
List<DerivePathType> get supportedDerivationPathTypes => [
|
||||
DerivePathType.bip44,
|
||||
];
|
||||
|
||||
@override
|
||||
String constructDerivePath({
|
||||
required DerivePathType derivePathType,
|
||||
int account = 0,
|
||||
required int chain,
|
||||
required int index,
|
||||
}) {
|
||||
String coinType;
|
||||
|
||||
switch (networkParams.wifPrefix) {
|
||||
case 204: // dash mainnet wif
|
||||
coinType = "5"; // dash mainnet
|
||||
break;
|
||||
// case 239: // dash testnet wif
|
||||
// coinType = "1"; // dash testnet
|
||||
// break;
|
||||
default:
|
||||
throw Exception("Invalid Dash network wif used!");
|
||||
}
|
||||
|
||||
int purpose;
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
purpose = 44;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception("DerivePathType $derivePathType not supported");
|
||||
}
|
||||
|
||||
return "m/$purpose'/$coinType'/$account'/$chain/$index";
|
||||
}
|
||||
|
||||
@override
|
||||
Amount get dustLimit => Amount(
|
||||
rawValue: BigInt.from(1000000),
|
||||
fractionDigits: fractionDigits,
|
||||
);
|
||||
|
||||
@override
|
||||
String get genesisHash {
|
||||
switch (network) {
|
||||
case CryptoCurrencyNetwork.main:
|
||||
return "00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6";
|
||||
// case CryptoCurrencyNetwork.test:
|
||||
// return "00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c";
|
||||
default:
|
||||
throw Exception("Unsupported network: $network");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
({
|
||||
coinlib.Address address,
|
||||
AddressType addressType,
|
||||
}) getAddressForPublicKey({
|
||||
required coinlib.ECPublicKey publicKey,
|
||||
required DerivePathType derivePathType,
|
||||
}) {
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
final addr = coinlib.P2PKHAddress.fromPublicKey(
|
||||
publicKey,
|
||||
version: networkParams.p2pkhPrefix,
|
||||
);
|
||||
|
||||
return (address: addr, addressType: AddressType.p2pkh);
|
||||
|
||||
default:
|
||||
throw Exception("DerivePathType $derivePathType not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get minConfirms => 6;
|
||||
|
||||
@override
|
||||
coinlib.Network get networkParams {
|
||||
switch (network) {
|
||||
case CryptoCurrencyNetwork.main:
|
||||
return coinlib.Network(
|
||||
p2pkhPrefix: 76,
|
||||
p2shPrefix: 16,
|
||||
wifPrefix: 204,
|
||||
pubHDPrefix: 0x0488B21E,
|
||||
privHDPrefix: 0x0488ADE4,
|
||||
bech32Hrp: "dash", // TODO ?????
|
||||
messagePrefix: '\x18Dash Signed Message:\n', // TODO ?????
|
||||
minFee: BigInt.from(1), // Not used in stack wallet currently
|
||||
minOutput: dustLimit.raw, // Not used in stack wallet currently
|
||||
feePerKb: BigInt.from(1), // Not used in stack wallet currently
|
||||
);
|
||||
// case CryptoCurrencyNetwork.test:
|
||||
// return coinlib.Network(
|
||||
// p2pkhPrefix: 140,
|
||||
// p2shPrefix: 19,
|
||||
// wifPrefix: 239,
|
||||
// pubHDPrefix: 0x043587CF,
|
||||
// privHDPrefix: 0x04358394,
|
||||
// bech32Hrp: "tdash", // TODO ?????
|
||||
// messagePrefix: '\x18Dash Signed Message:\n', // TODO ?????
|
||||
// minFee: BigInt.from(1), // Not used in stack wallet currently
|
||||
// minOutput: dustLimit.raw, // Not used in stack wallet currently
|
||||
// feePerKb: BigInt.from(1), // Not used in stack wallet currently
|
||||
// );
|
||||
default:
|
||||
throw Exception("Unsupported network: $network");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool validateAddress(String address) {
|
||||
try {
|
||||
coinlib.Address.fromString(address, networkParams);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
NodeModel get defaultNode {
|
||||
switch (network) {
|
||||
case CryptoCurrencyNetwork.main:
|
||||
return NodeModel(
|
||||
host: "dash.stackwallet.com",
|
||||
port: 60002,
|
||||
name: DefaultNodes.defaultName,
|
||||
id: DefaultNodes.buildId(this),
|
||||
useSSL: true,
|
||||
enabled: true,
|
||||
coinName: identifier,
|
||||
isFailover: true,
|
||||
isDown: false,
|
||||
);
|
||||
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get defaultSeedPhraseLength => 12;
|
||||
|
||||
@override
|
||||
int get fractionDigits => 8;
|
||||
|
||||
@override
|
||||
bool get hasBuySupport => true;
|
||||
|
||||
@override
|
||||
bool get hasMnemonicPassphraseSupport => true;
|
||||
|
||||
@override
|
||||
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 24];
|
||||
|
||||
@override
|
||||
AddressType get defaultAddressType => defaultDerivePathType.getAddressType();
|
||||
|
||||
@override
|
||||
BigInt get satsPerCoin => BigInt.from(100000000);
|
||||
|
||||
@override
|
||||
int get targetBlockTimeSeconds => 150;
|
||||
|
||||
@override
|
||||
DerivePathType get defaultDerivePathType => DerivePathType.bip44;
|
||||
|
||||
@override
|
||||
Uri defaultBlockExplorer(String txid) {
|
||||
switch (network) {
|
||||
case CryptoCurrencyNetwork.main:
|
||||
return Uri.parse("https://insight.dash.org/insight/tx/$txid");
|
||||
// case CryptoCurrencyNetwork.test:
|
||||
// return Uri.parse(
|
||||
// "https://insight.testnet.networks.dash.org:3002/insight/tx/$txid",
|
||||
// );
|
||||
default:
|
||||
throw Exception(
|
||||
"Unsupported network for defaultBlockExplorer(): $network",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get transactionVersion => 2;
|
||||
|
||||
@override
|
||||
BigInt get defaultFeeRate => BigInt.from(1000); // TODO check for dash?
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -10,5 +10,5 @@ abstract class CryptonoteCurrency extends CryptoCurrency {
|
|||
}
|
||||
|
||||
@override
|
||||
AddressType get primaryAddressType => AddressType.cryptonote;
|
||||
AddressType get defaultAddressType => AddressType.cryptonote;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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, '
|
||||
'}';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
314
lib/wallets/wallet/impl/dash_wallet.dart
Normal file
314
lib/wallets/wallet/impl/dash_wallet.dart
Normal file
|
@ -0,0 +1,314 @@
|
|||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../../models/isar/models/blockchain_data/transaction.dart';
|
||||
import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
|
||||
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
|
||||
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
import '../../../utilities/logger.dart';
|
||||
import '../../crypto_currency/crypto_currency.dart';
|
||||
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||
import '../intermediate/bip39_hd_wallet.dart';
|
||||
import '../wallet_mixin_interfaces/coin_control_interface.dart';
|
||||
import '../wallet_mixin_interfaces/electrumx_interface.dart';
|
||||
|
||||
class DashWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
|
||||
with ElectrumXInterface<T>, CoinControlInterface {
|
||||
DashWallet(CryptoCurrencyNetwork network) : super(Dash(network) as T);
|
||||
|
||||
@override
|
||||
int get maximumFeerate => 2500;
|
||||
|
||||
@override
|
||||
int get isarTransactionVersion => 2;
|
||||
|
||||
@override
|
||||
FilterOperation? get changeAddressFilterOperation =>
|
||||
FilterGroup.and(standardChangeAddressFilters);
|
||||
|
||||
@override
|
||||
FilterOperation? get receivingAddressFilterOperation =>
|
||||
FilterGroup.and(standardReceivingAddressFilters);
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@override
|
||||
Future<List<Address>> fetchAddressesForElectrumXScan() async {
|
||||
final allAddresses = await mainDB
|
||||
.getAddresses(walletId)
|
||||
.filter()
|
||||
.not()
|
||||
.group(
|
||||
(q) => q
|
||||
.typeEqualTo(AddressType.nonWallet)
|
||||
.or()
|
||||
.subTypeEqualTo(AddressSubType.nonWallet),
|
||||
)
|
||||
.findAll();
|
||||
return allAddresses;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@override
|
||||
Future<void> updateTransactions() async {
|
||||
// Get all addresses.
|
||||
final List<Address> allAddressesOld =
|
||||
await fetchAddressesForElectrumXScan();
|
||||
|
||||
// Separate receiving and change addresses.
|
||||
final Set<String> receivingAddresses = allAddressesOld
|
||||
.where((e) => e.subType == AddressSubType.receiving)
|
||||
.map((e) => e.value)
|
||||
.toSet();
|
||||
final Set<String> changeAddresses = allAddressesOld
|
||||
.where((e) => e.subType == AddressSubType.change)
|
||||
.map((e) => e.value)
|
||||
.toSet();
|
||||
|
||||
// Remove duplicates.
|
||||
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
||||
|
||||
// Fetch history from ElectrumX.
|
||||
final List<Map<String, dynamic>> allTxHashes =
|
||||
await fetchHistory(allAddressesSet);
|
||||
|
||||
// Only parse new txs (not in db yet).
|
||||
final List<Map<String, dynamic>> allTransactions = [];
|
||||
for (final txHash in allTxHashes) {
|
||||
// Check for duplicates by searching for tx by tx_hash in db.
|
||||
final storedTx = await mainDB.isar.transactionV2s
|
||||
.where()
|
||||
.txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
|
||||
.findFirst();
|
||||
|
||||
if (storedTx == null ||
|
||||
storedTx.height == null ||
|
||||
(storedTx.height != null && storedTx.height! <= 0)) {
|
||||
// Tx not in db yet.
|
||||
final tx = await electrumXCachedClient.getTransaction(
|
||||
txHash: txHash["tx_hash"] as String,
|
||||
verbose: true,
|
||||
cryptoCurrency: cryptoCurrency,
|
||||
);
|
||||
|
||||
// Only tx to list once.
|
||||
if (allTransactions
|
||||
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
|
||||
-1) {
|
||||
tx["height"] = txHash["height"];
|
||||
allTransactions.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse all new txs.
|
||||
final List<TransactionV2> txns = [];
|
||||
for (final txData in allTransactions) {
|
||||
bool wasSentFromThisWallet = false;
|
||||
// Set to true if any inputs were detected as owned by this wallet.
|
||||
|
||||
bool wasReceivedInThisWallet = false;
|
||||
// Set to true if any outputs were detected as owned by this wallet.
|
||||
|
||||
// Parse inputs.
|
||||
BigInt amountReceivedInThisWallet = BigInt.zero;
|
||||
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
|
||||
final List<InputV2> inputs = [];
|
||||
for (final jsonInput in txData["vin"] as List) {
|
||||
final map = Map<String, dynamic>.from(jsonInput as Map);
|
||||
|
||||
final List<String> addresses = [];
|
||||
String valueStringSats = "0";
|
||||
OutpointV2? outpoint;
|
||||
|
||||
final coinbase = map["coinbase"] as String?;
|
||||
|
||||
if (coinbase == null) {
|
||||
// Not a coinbase (ie a typical input).
|
||||
final txid = map["txid"] as String;
|
||||
final vout = map["vout"] as int;
|
||||
|
||||
final inputTx = await electrumXCachedClient.getTransaction(
|
||||
txHash: txid,
|
||||
cryptoCurrency: cryptoCurrency,
|
||||
);
|
||||
|
||||
final prevOutJson = Map<String, dynamic>.from(
|
||||
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map,
|
||||
);
|
||||
|
||||
final prevOut = OutputV2.fromElectrumXJson(
|
||||
prevOutJson,
|
||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
isFullAmountNotSats: true,
|
||||
walletOwns: false, // Doesn't matter here as this is not saved.
|
||||
);
|
||||
|
||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
);
|
||||
valueStringSats = prevOut.valueStringSats;
|
||||
addresses.addAll(prevOut.addresses);
|
||||
}
|
||||
|
||||
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptSigHex: map["scriptSig"]?["hex"] as String?,
|
||||
scriptSigAsm: map["scriptSig"]?["asm"] as String?,
|
||||
sequence: map["sequence"] as int?,
|
||||
outpoint: outpoint,
|
||||
valueStringSats: valueStringSats,
|
||||
addresses: addresses,
|
||||
witness: map["witness"] as String?,
|
||||
coinbase: coinbase,
|
||||
innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?,
|
||||
// Need addresses before we can know if the wallet owns this input.
|
||||
walletOwns: false,
|
||||
);
|
||||
|
||||
// Check if input was from this wallet.
|
||||
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
|
||||
wasSentFromThisWallet = true;
|
||||
input = input.copyWith(walletOwns: true);
|
||||
}
|
||||
|
||||
inputs.add(input);
|
||||
}
|
||||
|
||||
// Parse outputs.
|
||||
final List<OutputV2> outputs = [];
|
||||
for (final outputJson in txData["vout"] as List) {
|
||||
OutputV2 output = OutputV2.fromElectrumXJson(
|
||||
Map<String, dynamic>.from(outputJson as Map),
|
||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
isFullAmountNotSats: true,
|
||||
// Need addresses before we can know if the wallet owns this input.
|
||||
walletOwns: false,
|
||||
);
|
||||
|
||||
// If output was to my wallet, add value to amount received.
|
||||
if (receivingAddresses
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {
|
||||
wasReceivedInThisWallet = true;
|
||||
amountReceivedInThisWallet += output.value;
|
||||
output = output.copyWith(walletOwns: true);
|
||||
} else if (changeAddresses
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {
|
||||
wasReceivedInThisWallet = true;
|
||||
changeAmountReceivedInThisWallet += output.value;
|
||||
output = output.copyWith(walletOwns: true);
|
||||
}
|
||||
|
||||
outputs.add(output);
|
||||
}
|
||||
|
||||
final totalOut = outputs
|
||||
.map((e) => e.value)
|
||||
.fold(BigInt.zero, (value, element) => value + element);
|
||||
|
||||
TransactionType type;
|
||||
final TransactionSubType subType = TransactionSubType.none;
|
||||
|
||||
// At least one input was owned by this wallet.
|
||||
if (wasSentFromThisWallet) {
|
||||
type = TransactionType.outgoing;
|
||||
|
||||
if (wasReceivedInThisWallet) {
|
||||
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
|
||||
totalOut) {
|
||||
// Definitely sent all to self.
|
||||
type = TransactionType.sentToSelf;
|
||||
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
||||
// Most likely just a typical send, do nothing here yet.
|
||||
}
|
||||
|
||||
// This is where we would check for them.
|
||||
// TODO: [prio=high] Check for special Dash outputs.
|
||||
}
|
||||
} else if (wasReceivedInThisWallet) {
|
||||
// Only found outputs owned by this wallet.
|
||||
type = TransactionType.incoming;
|
||||
} else {
|
||||
Logging.instance.log(
|
||||
"Unexpected tx found (ignoring it): $txData",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final tx = TransactionV2(
|
||||
walletId: walletId,
|
||||
blockHash: txData["blockhash"] as String?,
|
||||
hash: txData["txid"] as String,
|
||||
txid: txData["txid"] as String,
|
||||
height: txData["height"] as int?,
|
||||
version: txData["version"] as int,
|
||||
timestamp: txData["blocktime"] as int? ??
|
||||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
||||
inputs: List.unmodifiable(inputs),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
type: type,
|
||||
subType: subType,
|
||||
otherData: null,
|
||||
);
|
||||
|
||||
txns.add(tx);
|
||||
}
|
||||
|
||||
await mainDB.updateOrPutTransactionV2s(txns);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<({String? blockedReason, bool blocked, String? utxoLabel})>
|
||||
checkBlockUTXO(
|
||||
Map<String, dynamic> jsonUTXO,
|
||||
String? scriptPubKeyHex,
|
||||
Map<String, dynamic> jsonTX,
|
||||
String? utxoOwnerAddress,
|
||||
) async {
|
||||
bool blocked = false;
|
||||
String? blockedReason;
|
||||
|
||||
// // check for bip47 notification
|
||||
// final outputs = jsonTX["vout"] as List;
|
||||
// for (final output in outputs) {
|
||||
// final List<String>? scriptChunks =
|
||||
// (output['scriptPubKey']?['asm'] as String?)?.split(" ");
|
||||
// if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
|
||||
// final blindedPaymentCode = scriptChunks![1];
|
||||
// final bytes = blindedPaymentCode.toUint8ListFromHex;
|
||||
//
|
||||
// // https://en.bitcoin.it/wiki/BIP_0047#Sending
|
||||
// if (bytes.length == 80 && bytes.first == 1) {
|
||||
// blocked = true;
|
||||
// blockedReason = "Paynym notification output. Incautious "
|
||||
// "handling of outputs from notification transactions "
|
||||
// "may cause unintended loss of privacy.";
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null);
|
||||
}
|
||||
|
||||
@override
|
||||
Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
|
||||
return Amount(
|
||||
rawValue: BigInt.from(
|
||||
((181 * inputCount) + (34 * outputCount) + 10) *
|
||||
(feeRatePerKB / 1000).ceil(),
|
||||
),
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int estimateTxFee({required int vSize, required int feeRatePerKB}) {
|
||||
return vSize * (feeRatePerKB / 1000).ceil();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||
import 'electrumx_interface.dart';
|
||||
|
||||
mixin CpfpInterface<T extends ElectrumXCurrencyInterface>
|
||||
on ElectrumXInterface<T> {
|
||||
//
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
283
lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart
Normal file
283
lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart
Normal 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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
145
lib/widgets/boost_fee_slider.dart
Normal file
145
lib/widgets/boost_fee_slider.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue