mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-23 19:05:51 +00:00
spark spend from transparent and various clean up
This commit is contained in:
parent
cb46c2fa3a
commit
953acb493c
7 changed files with 673 additions and 401 deletions
|
@ -10,15 +10,18 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart';
|
import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart';
|
||||||
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
|
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
import 'package:stackwallet/providers/providers.dart';
|
import 'package:stackwallet/providers/providers.dart';
|
||||||
import 'package:stackwallet/route_generator.dart';
|
import 'package:stackwallet/route_generator.dart';
|
||||||
import 'package:stackwallet/themes/stack_colors.dart';
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
@ -30,7 +33,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||||
import 'package:stackwallet/widgets/background.dart';
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||||
|
@ -58,6 +63,11 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
late final Coin coin;
|
late final Coin coin;
|
||||||
late final String walletId;
|
late final String walletId;
|
||||||
late final ClipboardInterface clipboard;
|
late final ClipboardInterface clipboard;
|
||||||
|
late final bool supportsSpark;
|
||||||
|
|
||||||
|
String? _sparkAddress;
|
||||||
|
String? _qrcodeContent;
|
||||||
|
bool _showSparkAddress = true;
|
||||||
|
|
||||||
Future<void> generateNewAddress() async {
|
Future<void> generateNewAddress() async {
|
||||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||||
|
@ -96,23 +106,106 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> generateNewSparkAddress() async {
|
||||||
|
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||||
|
if (wallet is SparkInterface) {
|
||||||
|
bool shouldPop = false;
|
||||||
|
unawaited(
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async => shouldPop,
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.overlay
|
||||||
|
.withOpacity(0.5),
|
||||||
|
child: const CustomLoadingOverlay(
|
||||||
|
message: "Generating address",
|
||||||
|
eventBus: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final address = await wallet.generateNextSparkAddress();
|
||||||
|
await ref.read(mainDBProvider).isar.writeTxn(() async {
|
||||||
|
await ref.read(mainDBProvider).isar.addresses.put(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPop = true;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
|
if (_sparkAddress != address.value) {
|
||||||
|
setState(() {
|
||||||
|
_sparkAddress = address.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<Address?>? _streamSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
walletId = widget.walletId;
|
walletId = widget.walletId;
|
||||||
coin = ref.read(pWalletCoin(walletId));
|
coin = ref.read(pWalletCoin(walletId));
|
||||||
clipboard = widget.clipboard;
|
clipboard = widget.clipboard;
|
||||||
|
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
|
||||||
|
|
||||||
|
if (supportsSpark) {
|
||||||
|
_streamSub = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.addresses
|
||||||
|
.where()
|
||||||
|
.walletIdEqualTo(walletId)
|
||||||
|
.filter()
|
||||||
|
.typeEqualTo(AddressType.spark)
|
||||||
|
.sortByDerivationIndexDesc()
|
||||||
|
.findFirst()
|
||||||
|
.asStream()
|
||||||
|
.listen((event) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_sparkAddress = event?.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_streamSub?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
debugPrint("BUILD: $runtimeType");
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
|
||||||
final receivingAddress = ref.watch(pWalletReceivingAddress(walletId));
|
|
||||||
|
|
||||||
final ticker = widget.tokenContract?.symbol ?? coin.ticker;
|
final ticker = widget.tokenContract?.symbol ?? coin.ticker;
|
||||||
|
|
||||||
|
if (supportsSpark) {
|
||||||
|
if (_showSparkAddress) {
|
||||||
|
_qrcodeContent = _sparkAddress;
|
||||||
|
} else {
|
||||||
|
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
|
||||||
|
}
|
||||||
|
|
||||||
return Background(
|
return Background(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
@ -225,86 +318,239 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
ConditionalParent(
|
||||||
onTap: () {
|
condition: supportsSpark,
|
||||||
HapticFeedback.lightImpact();
|
builder: (child) => Column(
|
||||||
clipboard.setData(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
ClipboardData(text: receivingAddress),
|
children: [
|
||||||
);
|
DropdownButtonHideUnderline(
|
||||||
showFloatingFlushBar(
|
child: DropdownButton2<bool>(
|
||||||
type: FlushBarType.info,
|
value: _showSparkAddress,
|
||||||
message: "Copied to clipboard",
|
items: [
|
||||||
iconAsset: Assets.svg.copy,
|
DropdownMenuItem(
|
||||||
context: context,
|
value: true,
|
||||||
);
|
|
||||||
},
|
|
||||||
child: RoundedWhiteContainer(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Your $ticker address",
|
|
||||||
style: STextStyles.itemSubtitle(context),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.svg.copy,
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.infoItemIcons,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Copy",
|
|
||||||
style: STextStyles.link2(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
receivingAddress,
|
"Spark address",
|
||||||
style: STextStyles.itemSubtitle12(context),
|
style: STextStyles.desktopTextMedium(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: false,
|
||||||
|
child: Text(
|
||||||
|
"Transparent address",
|
||||||
|
style: STextStyles.desktopTextMedium(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value is bool && value != _showSparkAddress) {
|
||||||
|
setState(() {
|
||||||
|
_showSparkAddress = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isExpanded: true,
|
||||||
|
iconStyleData: IconStyleData(
|
||||||
|
icon: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.svg.chevronDown,
|
||||||
|
width: 12,
|
||||||
|
height: 6,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldActiveSearchIconRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dropdownStyleData: DropdownStyleData(
|
||||||
|
offset: const Offset(0, -10),
|
||||||
|
elevation: 0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldDefaultBG,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
if (_showSparkAddress)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
clipboard.setData(
|
||||||
|
ClipboardData(text: _sparkAddress ?? "Error"),
|
||||||
|
);
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.info,
|
||||||
|
message: "Copied to clipboard",
|
||||||
|
iconAsset: Assets.svg.copy,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.backgroundAppBar,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: RoundedWhiteContainer(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Your ${coin.ticker} SPARK address",
|
||||||
|
style:
|
||||||
|
STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.copy,
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.infoItemIcons,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Copy",
|
||||||
|
style: STextStyles.link2(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_sparkAddress ?? "Error",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_showSparkAddress) child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text:
|
||||||
|
ref.watch(pWalletReceivingAddress(walletId))),
|
||||||
|
);
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.info,
|
||||||
|
message: "Copied to clipboard",
|
||||||
|
iconAsset: Assets.svg.copy,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: RoundedWhiteContainer(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Your $ticker address",
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.copy,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.infoItemIcons,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Copy",
|
||||||
|
style: STextStyles.link2(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
ref.watch(
|
||||||
|
pWalletReceivingAddress(walletId)),
|
||||||
|
style: STextStyles.itemSubtitle12(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (coin != Coin.epicCash &&
|
if (ref.watch(pWallets
|
||||||
coin != Coin.ethereum &&
|
.select((value) => value.getWallet(walletId)))
|
||||||
coin != Coin.banano &&
|
is MultiAddressInterface ||
|
||||||
coin != Coin.nano &&
|
supportsSpark)
|
||||||
coin != Coin.stellar &&
|
|
||||||
coin != Coin.stellarTestnet &&
|
|
||||||
coin != Coin.tezos)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
if (coin != Coin.epicCash &&
|
if (ref.watch(pWallets
|
||||||
coin != Coin.ethereum &&
|
.select((value) => value.getWallet(walletId)))
|
||||||
coin != Coin.banano &&
|
is MultiAddressInterface ||
|
||||||
coin != Coin.nano &&
|
supportsSpark)
|
||||||
coin != Coin.stellar &&
|
|
||||||
coin != Coin.stellarTestnet &&
|
|
||||||
coin != Coin.tezos)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: generateNewAddress,
|
onPressed: supportsSpark && _showSparkAddress
|
||||||
|
? generateNewSparkAddress
|
||||||
|
: generateNewAddress,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.getSecondaryEnabledButtonStyle(context),
|
.getSecondaryEnabledButtonStyle(context),
|
||||||
|
@ -328,7 +574,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
QrImageView(
|
QrImageView(
|
||||||
data: AddressUtils.buildUriString(
|
data: AddressUtils.buildUriString(
|
||||||
coin,
|
coin,
|
||||||
receivingAddress,
|
_qrcodeContent ?? "",
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
size: MediaQuery.of(context).size.width / 2,
|
size: MediaQuery.of(context).size.width / 2,
|
||||||
|
@ -347,7 +593,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
RouteGenerator.useMaterialPageRoute,
|
RouteGenerator.useMaterialPageRoute,
|
||||||
builder: (_) => GenerateUriQrCodeView(
|
builder: (_) => GenerateUriQrCodeView(
|
||||||
coin: coin,
|
coin: coin,
|
||||||
receivingAddress: receivingAddress,
|
receivingAddress: _qrcodeContent ?? "",
|
||||||
),
|
),
|
||||||
settings: const RouteSettings(
|
settings: const RouteSettings(
|
||||||
name: GenerateUriQrCodeView.routeName,
|
name: GenerateUriQrCodeView.routeName,
|
||||||
|
|
|
@ -120,7 +120,7 @@ class _ConfirmTransactionViewState
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
late String txid;
|
final List<String> txids = [];
|
||||||
Future<TxData> txDataFuture;
|
Future<TxData> txDataFuture;
|
||||||
|
|
||||||
final note = noteController.text;
|
final note = noteController.text;
|
||||||
|
@ -143,7 +143,12 @@ class _ConfirmTransactionViewState
|
||||||
if (wallet is FiroWallet) {
|
if (wallet is FiroWallet) {
|
||||||
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||||
case FiroType.public:
|
case FiroType.public:
|
||||||
txDataFuture = wallet.confirmSend(txData: widget.txData);
|
if (widget.txData.sparkMints == null) {
|
||||||
|
txDataFuture = wallet.confirmSend(txData: widget.txData);
|
||||||
|
} else {
|
||||||
|
txDataFuture =
|
||||||
|
wallet.confirmSparkMintTransactions(txData: widget.txData);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FiroType.lelantus:
|
case FiroType.lelantus:
|
||||||
|
@ -175,17 +180,24 @@ class _ConfirmTransactionViewState
|
||||||
sendProgressController.triggerSuccess?.call();
|
sendProgressController.triggerSuccess?.call();
|
||||||
await Future<void>.delayed(const Duration(seconds: 5));
|
await Future<void>.delayed(const Duration(seconds: 5));
|
||||||
|
|
||||||
txid = (results.first as TxData).txid!;
|
if (wallet is FiroWallet &&
|
||||||
|
(results.first as TxData).sparkMints != null) {
|
||||||
|
txids.addAll((results.first as TxData).sparkMints!.map((e) => e.txid!));
|
||||||
|
} else {
|
||||||
|
txids.add((results.first as TxData).txid!);
|
||||||
|
}
|
||||||
ref.refresh(desktopUseUTXOs);
|
ref.refresh(desktopUseUTXOs);
|
||||||
|
|
||||||
// save note
|
// save note
|
||||||
await ref.read(mainDBProvider).putTransactionNote(
|
for (final txid in txids) {
|
||||||
TransactionNote(
|
await ref.read(mainDBProvider).putTransactionNote(
|
||||||
walletId: walletId,
|
TransactionNote(
|
||||||
txid: txid,
|
walletId: walletId,
|
||||||
value: note,
|
txid: txid,
|
||||||
),
|
value: note,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.isTokenTx) {
|
if (widget.isTokenTx) {
|
||||||
unawaited(ref.read(tokenServiceProvider)!.refresh());
|
unawaited(ref.read(tokenServiceProvider)!.refresh());
|
||||||
|
@ -333,6 +345,48 @@ class _ConfirmTransactionViewState
|
||||||
} else {
|
} else {
|
||||||
unit = coin.ticker;
|
unit = coin.ticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Amount? fee;
|
||||||
|
final Amount amount;
|
||||||
|
|
||||||
|
final wallet = ref.watch(pWallets).getWallet(walletId);
|
||||||
|
|
||||||
|
if (wallet is FiroWallet) {
|
||||||
|
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||||
|
case FiroType.public:
|
||||||
|
if (widget.txData.sparkMints != null) {
|
||||||
|
fee = widget.txData.sparkMints!
|
||||||
|
.map((e) => e.fee!)
|
||||||
|
.reduce((value, element) => value += element);
|
||||||
|
amount = widget.txData.sparkMints!
|
||||||
|
.map((e) => e.amountSpark!)
|
||||||
|
.reduce((value, element) => value += element);
|
||||||
|
} else {
|
||||||
|
fee = widget.txData.fee;
|
||||||
|
amount = widget.txData.amount!;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FiroType.lelantus:
|
||||||
|
fee = widget.txData.fee;
|
||||||
|
amount = widget.txData.amount!;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FiroType.spark:
|
||||||
|
fee = widget.txData.fee;
|
||||||
|
amount = (widget.txData.amount ??
|
||||||
|
Amount.zeroWith(
|
||||||
|
fractionDigits: wallet.cryptoCurrency.fractionDigits)) +
|
||||||
|
(widget.txData.amountSpark ??
|
||||||
|
Amount.zeroWith(
|
||||||
|
fractionDigits: wallet.cryptoCurrency.fractionDigits));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fee = widget.txData.fee;
|
||||||
|
amount = widget.txData.amount!;
|
||||||
|
}
|
||||||
|
|
||||||
return ConditionalParent(
|
return ConditionalParent(
|
||||||
condition: !isDesktop,
|
condition: !isDesktop,
|
||||||
builder: (child) => Background(
|
builder: (child) => Background(
|
||||||
|
@ -438,7 +492,8 @@ class _ConfirmTransactionViewState
|
||||||
Text(
|
Text(
|
||||||
widget.isPaynymTransaction
|
widget.isPaynymTransaction
|
||||||
? widget.txData.paynymAccountLite!.nymName
|
? widget.txData.paynymAccountLite!.nymName
|
||||||
: widget.txData.recipients!.first.address,
|
: widget.txData.recipients?.first.address ??
|
||||||
|
widget.txData.sparkRecipients!.first.address,
|
||||||
style: STextStyles.itemSubtitle12(context),
|
style: STextStyles.itemSubtitle12(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -457,7 +512,7 @@ class _ConfirmTransactionViewState
|
||||||
),
|
),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
ref.watch(pAmountFormatter(coin)).format(
|
ref.watch(pAmountFormatter(coin)).format(
|
||||||
widget.txData.amount!,
|
amount,
|
||||||
ethContract: ref
|
ethContract: ref
|
||||||
.watch(tokenServiceProvider)
|
.watch(tokenServiceProvider)
|
||||||
?.tokenContract,
|
?.tokenContract,
|
||||||
|
@ -482,9 +537,7 @@ class _ConfirmTransactionViewState
|
||||||
style: STextStyles.smallMed12(context),
|
style: STextStyles.smallMed12(context),
|
||||||
),
|
),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
ref
|
ref.watch(pAmountFormatter(coin)).format(fee!),
|
||||||
.watch(pAmountFormatter(coin))
|
|
||||||
.format(widget.txData.fee!),
|
|
||||||
style: STextStyles.itemSubtitle12(context),
|
style: STextStyles.itemSubtitle12(context),
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
),
|
),
|
||||||
|
@ -508,7 +561,7 @@ class _ConfirmTransactionViewState
|
||||||
height: 4,
|
height: 4,
|
||||||
),
|
),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
"~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
"~${fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
||||||
style: STextStyles.itemSubtitle12(context),
|
style: STextStyles.itemSubtitle12(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -639,9 +692,6 @@ class _ConfirmTransactionViewState
|
||||||
),
|
),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
// TODO: [prio=high] spark transaction specifics - better handling
|
|
||||||
final amount = widget.txData.amount ??
|
|
||||||
widget.txData.amountSpark!;
|
|
||||||
final externalCalls = ref.watch(
|
final externalCalls = ref.watch(
|
||||||
prefsChangeNotifierProvider.select(
|
prefsChangeNotifierProvider.select(
|
||||||
(value) => value.externalCalls));
|
(value) => value.externalCalls));
|
||||||
|
@ -778,24 +828,15 @@ class _ConfirmTransactionViewState
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 2,
|
height: 2,
|
||||||
),
|
),
|
||||||
Builder(
|
SelectableText(
|
||||||
builder: (context) {
|
ref.watch(pAmountFormatter(coin)).format(fee!),
|
||||||
final fee = widget.txData.fee!;
|
style: STextStyles.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
return SelectableText(
|
.copyWith(
|
||||||
ref
|
color: Theme.of(context)
|
||||||
.watch(pAmountFormatter(coin))
|
.extension<StackColors>()!
|
||||||
.format(fee),
|
.textDark,
|
||||||
style:
|
),
|
||||||
STextStyles.desktopTextExtraExtraSmall(
|
|
||||||
context)
|
|
||||||
.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.textDark,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1000,15 +1041,9 @@ class _ConfirmTransactionViewState
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textFieldDefaultBG,
|
.textFieldDefaultBG,
|
||||||
child: Builder(
|
child: SelectableText(
|
||||||
builder: (context) {
|
ref.watch(pAmountFormatter(coin)).format(fee!),
|
||||||
final fee = widget.txData.fee!;
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
|
||||||
return SelectableText(
|
|
||||||
ref.watch(pAmountFormatter(coin)).format(fee),
|
|
||||||
style: STextStyles.itemSubtitle(context),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1044,7 +1079,7 @@ class _ConfirmTransactionViewState
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textFieldDefaultBG,
|
.textFieldDefaultBG,
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
"~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
"~${fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
||||||
style: STextStyles.itemSubtitle(context),
|
style: STextStyles.itemSubtitle(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1088,31 +1123,22 @@ class _ConfirmTransactionViewState
|
||||||
.textConfirmTotalAmount,
|
.textConfirmTotalAmount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Builder(builder: (context) {
|
SelectableText(
|
||||||
final fee = widget.txData.fee!;
|
ref.watch(pAmountFormatter(coin)).format(amount + fee!),
|
||||||
|
style: isDesktop
|
||||||
// TODO: [prio=high] spark transaction specifics - better handling
|
? STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
final amount =
|
.copyWith(
|
||||||
widget.txData.amount ?? widget.txData.amountSpark!;
|
color: Theme.of(context)
|
||||||
return SelectableText(
|
.extension<StackColors>()!
|
||||||
ref
|
.textConfirmTotalAmount,
|
||||||
.watch(pAmountFormatter(coin))
|
)
|
||||||
.format(amount + fee),
|
: STextStyles.itemSubtitle12(context).copyWith(
|
||||||
style: isDesktop
|
color: Theme.of(context)
|
||||||
? STextStyles.desktopTextExtraExtraSmall(context)
|
.extension<StackColors>()!
|
||||||
.copyWith(
|
.textConfirmTotalAmount,
|
||||||
color: Theme.of(context)
|
),
|
||||||
.extension<StackColors>()!
|
textAlign: TextAlign.right,
|
||||||
.textConfirmTotalAmount,
|
),
|
||||||
)
|
|
||||||
: STextStyles.itemSubtitle12(context).copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.textConfirmTotalAmount,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -524,29 +524,39 @@ class _SendViewState extends ConsumerState<SendView> {
|
||||||
} else if (wallet is FiroWallet) {
|
} else if (wallet is FiroWallet) {
|
||||||
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||||
case FiroType.public:
|
case FiroType.public:
|
||||||
txDataFuture = wallet.prepareSend(
|
if (_isSparkAddress) {
|
||||||
txData: TxData(
|
txDataFuture = wallet.prepareSparkMintTransaction(
|
||||||
recipients: _isSparkAddress
|
txData: TxData(
|
||||||
? null
|
sparkRecipients: [
|
||||||
: [(address: _address!, amount: amount)],
|
(
|
||||||
sparkRecipients: _isSparkAddress
|
address: _address!,
|
||||||
? [
|
amount: amount,
|
||||||
(
|
memo: memoController.text,
|
||||||
address: _address!,
|
)
|
||||||
amount: amount,
|
],
|
||||||
memo: memoController.text,
|
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||||
)
|
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||||
]
|
utxos: (wallet is CoinControlInterface &&
|
||||||
: null,
|
coinControlEnabled &&
|
||||||
feeRateType: ref.read(feeRateTypeStateProvider),
|
selectedUTXOs.isNotEmpty)
|
||||||
satsPerVByte: isCustomFee ? customFeeRate : null,
|
? selectedUTXOs
|
||||||
utxos: (wallet is CoinControlInterface &&
|
: null,
|
||||||
coinControlEnabled &&
|
),
|
||||||
selectedUTXOs.isNotEmpty)
|
);
|
||||||
? selectedUTXOs
|
} else {
|
||||||
: null,
|
txDataFuture = wallet.prepareSend(
|
||||||
),
|
txData: TxData(
|
||||||
);
|
recipients: [(address: _address!, amount: amount)],
|
||||||
|
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||||
|
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||||
|
utxos: (wallet is CoinControlInterface &&
|
||||||
|
coinControlEnabled &&
|
||||||
|
selectedUTXOs.isNotEmpty)
|
||||||
|
? selectedUTXOs
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FiroType.lelantus:
|
case FiroType.lelantus:
|
||||||
|
|
|
@ -68,6 +68,7 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||||
import 'package:stackwallet/widgets/background.dart';
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
@ -115,6 +116,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
||||||
late final String walletId;
|
late final String walletId;
|
||||||
late final Coin coin;
|
late final Coin coin;
|
||||||
|
|
||||||
|
late final bool isSparkWallet;
|
||||||
|
|
||||||
late final bool _shouldDisableAutoSyncOnLogOut;
|
late final bool _shouldDisableAutoSyncOnLogOut;
|
||||||
|
|
||||||
late WalletSyncStatus _currentSyncStatus;
|
late WalletSyncStatus _currentSyncStatus;
|
||||||
|
@ -171,6 +174,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
||||||
_shouldDisableAutoSyncOnLogOut = false;
|
_shouldDisableAutoSyncOnLogOut = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSparkWallet = wallet is SparkInterface;
|
||||||
|
|
||||||
if (coin == Coin.firo &&
|
if (coin == Coin.firo &&
|
||||||
(wallet as FiroWallet).lelantusCoinIsarRescanRequired) {
|
(wallet as FiroWallet).lelantusCoinIsarRescanRequired) {
|
||||||
_rescanningOnOpen = true;
|
_rescanningOnOpen = true;
|
||||||
|
@ -758,11 +763,11 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (coin == Coin.firo)
|
if (isSparkWallet)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
if (coin == Coin.firo)
|
if (isSparkWallet)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
|
@ -321,29 +321,39 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
} else if (wallet is FiroWallet) {
|
} else if (wallet is FiroWallet) {
|
||||||
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||||
case FiroType.public:
|
case FiroType.public:
|
||||||
txDataFuture = wallet.prepareSend(
|
if (_isSparkAddress) {
|
||||||
txData: TxData(
|
txDataFuture = wallet.prepareSparkMintTransaction(
|
||||||
recipients: _isSparkAddress
|
txData: TxData(
|
||||||
? null
|
sparkRecipients: [
|
||||||
: [(address: _address!, amount: amount)],
|
(
|
||||||
sparkRecipients: _isSparkAddress
|
address: _address!,
|
||||||
? [
|
amount: amount,
|
||||||
(
|
memo: memoController.text,
|
||||||
address: _address!,
|
)
|
||||||
amount: amount,
|
],
|
||||||
memo: memoController.text,
|
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||||
)
|
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||||
]
|
utxos: (wallet is CoinControlInterface &&
|
||||||
: null,
|
coinControlEnabled &&
|
||||||
feeRateType: ref.read(feeRateTypeStateProvider),
|
ref.read(desktopUseUTXOs).isNotEmpty)
|
||||||
satsPerVByte: isCustomFee ? customFeeRate : null,
|
? ref.read(desktopUseUTXOs)
|
||||||
utxos: (wallet is CoinControlInterface &&
|
: null,
|
||||||
coinControlEnabled &&
|
),
|
||||||
ref.read(desktopUseUTXOs).isNotEmpty)
|
);
|
||||||
? ref.read(desktopUseUTXOs)
|
} else {
|
||||||
: null,
|
txDataFuture = wallet.prepareSend(
|
||||||
),
|
txData: TxData(
|
||||||
);
|
recipients: [(address: _address!, amount: amount)],
|
||||||
|
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||||
|
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||||
|
utxos: (wallet is CoinControlInterface &&
|
||||||
|
coinControlEnabled &&
|
||||||
|
ref.read(desktopUseUTXOs).isNotEmpty)
|
||||||
|
? ref.read(desktopUseUTXOs)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FiroType.lelantus:
|
case FiroType.lelantus:
|
||||||
|
@ -579,7 +589,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
ref.read(pWallets).getWallet(walletId).cryptoCurrency;
|
ref.read(pWallets).getWallet(walletId).cryptoCurrency;
|
||||||
final isValidAddress = walletCurrency.validateAddress(address ?? "");
|
final isValidAddress = walletCurrency.validateAddress(address ?? "");
|
||||||
|
|
||||||
_isSparkAddress = isValidAddress
|
_isSparkAddress = isValidAddress &&
|
||||||
|
ref.read(publicPrivateBalanceStateProvider.state).state !=
|
||||||
|
FiroType.lelantus
|
||||||
? SparkInterface.validateSparkAddress(
|
? SparkInterface.validateSparkAddress(
|
||||||
address: address!,
|
address: address!,
|
||||||
isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test,
|
isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test,
|
||||||
|
@ -1409,11 +1421,17 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (isStellar || _isSparkAddress)
|
if (isStellar ||
|
||||||
|
(_isSparkAddress &&
|
||||||
|
ref.watch(publicPrivateBalanceStateProvider) !=
|
||||||
|
FiroType.public))
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
if (isStellar || _isSparkAddress)
|
if (isStellar ||
|
||||||
|
(_isSparkAddress &&
|
||||||
|
ref.watch(publicPrivateBalanceStateProvider) !=
|
||||||
|
FiroType.public))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
Constants.size.circularBorderRadius,
|
Constants.size.circularBorderRadius,
|
||||||
|
|
|
@ -62,6 +62,7 @@ class TxData {
|
||||||
Amount amount,
|
Amount amount,
|
||||||
String memo,
|
String memo,
|
||||||
})>? sparkRecipients;
|
})>? sparkRecipients;
|
||||||
|
final List<TxData>? sparkMints;
|
||||||
|
|
||||||
TxData({
|
TxData({
|
||||||
this.feeRateType,
|
this.feeRateType,
|
||||||
|
@ -94,6 +95,7 @@ class TxData {
|
||||||
this.mintsMapLelantus,
|
this.mintsMapLelantus,
|
||||||
this.tezosOperationsList,
|
this.tezosOperationsList,
|
||||||
this.sparkRecipients,
|
this.sparkRecipients,
|
||||||
|
this.sparkMints,
|
||||||
});
|
});
|
||||||
|
|
||||||
Amount? get amount => recipients != null && recipients!.isNotEmpty
|
Amount? get amount => recipients != null && recipients!.isNotEmpty
|
||||||
|
@ -150,6 +152,7 @@ class TxData {
|
||||||
String memo,
|
String memo,
|
||||||
})>?
|
})>?
|
||||||
sparkRecipients,
|
sparkRecipients,
|
||||||
|
List<TxData>? sparkMints,
|
||||||
}) {
|
}) {
|
||||||
return TxData(
|
return TxData(
|
||||||
feeRateType: feeRateType ?? this.feeRateType,
|
feeRateType: feeRateType ?? this.feeRateType,
|
||||||
|
@ -183,6 +186,7 @@ class TxData {
|
||||||
mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus,
|
mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus,
|
||||||
tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList,
|
tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList,
|
||||||
sparkRecipients: sparkRecipients ?? this.sparkRecipients,
|
sparkRecipients: sparkRecipients ?? this.sparkRecipients,
|
||||||
|
sparkMints: sparkMints ?? this.sparkMints,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,5 +222,6 @@ class TxData {
|
||||||
'mintsMapLelantus: $mintsMapLelantus, '
|
'mintsMapLelantus: $mintsMapLelantus, '
|
||||||
'tezosOperationsList: $tezosOperationsList, '
|
'tezosOperationsList: $tezosOperationsList, '
|
||||||
'sparkRecipients: $sparkRecipients, '
|
'sparkRecipients: $sparkRecipients, '
|
||||||
|
'sparkMints: $sparkMints, '
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:bitcoindart/bitcoindart.dart' as btc;
|
import 'package:bitcoindart/bitcoindart.dart' as btc;
|
||||||
|
import 'package:decimal/decimal.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
|
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
@ -19,8 +20,12 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_int
|
||||||
|
|
||||||
const kDefaultSparkIndex = 1;
|
const kDefaultSparkIndex = 1;
|
||||||
|
|
||||||
|
// TODO dart style constants. Maybe move to spark lib?
|
||||||
const MAX_STANDARD_TX_WEIGHT = 400000;
|
const MAX_STANDARD_TX_WEIGHT = 400000;
|
||||||
|
|
||||||
|
//https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16
|
||||||
|
const SPARK_OUT_LIMIT_PER_TX = 16;
|
||||||
|
|
||||||
const OP_SPARKMINT = 0xd1;
|
const OP_SPARKMINT = 0xd1;
|
||||||
const OP_SPARKSMINT = 0xd2;
|
const OP_SPARKSMINT = 0xd2;
|
||||||
const OP_SPARKSPEND = 0xd3;
|
const OP_SPARKSPEND = 0xd3;
|
||||||
|
@ -125,6 +130,47 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
Future<TxData> prepareSendSpark({
|
Future<TxData> prepareSendSpark({
|
||||||
required TxData txData,
|
required TxData txData,
|
||||||
}) async {
|
}) async {
|
||||||
|
// There should be at least one output.
|
||||||
|
if (!(txData.recipients?.isNotEmpty == true ||
|
||||||
|
txData.sparkRecipients?.isNotEmpty == true)) {
|
||||||
|
throw Exception("No recipients provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txData.sparkRecipients?.isNotEmpty == true &&
|
||||||
|
txData.sparkRecipients!.length >= SPARK_OUT_LIMIT_PER_TX - 1) {
|
||||||
|
throw Exception("Spark shielded output limit exceeded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final transparentSumOut =
|
||||||
|
(txData.recipients ?? []).map((e) => e.amount).fold(
|
||||||
|
Amount(
|
||||||
|
rawValue: BigInt.zero,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
(p, e) => p + e);
|
||||||
|
|
||||||
|
// See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17
|
||||||
|
// and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17
|
||||||
|
// Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31
|
||||||
|
if (transparentSumOut >
|
||||||
|
Amount.fromDecimal(
|
||||||
|
Decimal.parse("10000"),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
)) {
|
||||||
|
throw Exception(
|
||||||
|
"Spend to transparent address limit exceeded (10,000 Firo per transaction).");
|
||||||
|
}
|
||||||
|
|
||||||
|
final sparkSumOut =
|
||||||
|
(txData.sparkRecipients ?? []).map((e) => e.amount).fold(
|
||||||
|
Amount(
|
||||||
|
rawValue: BigInt.zero,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
(p, e) => p + e);
|
||||||
|
|
||||||
|
final txAmount = transparentSumOut + sparkSumOut;
|
||||||
|
|
||||||
// fetch spendable spark coins
|
// fetch spendable spark coins
|
||||||
final coins = await mainDB.isar.sparkCoins
|
final coins = await mainDB.isar.sparkCoins
|
||||||
.where()
|
.where()
|
||||||
|
@ -140,19 +186,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
|
|
||||||
final available = info.cachedBalanceTertiary.spendable;
|
final available = info.cachedBalanceTertiary.spendable;
|
||||||
|
|
||||||
final txAmount = (txData.recipients ?? []).map((e) => e.amount).fold(
|
|
||||||
Amount(
|
|
||||||
rawValue: BigInt.zero,
|
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
|
||||||
),
|
|
||||||
(p, e) => p + e) +
|
|
||||||
(txData.sparkRecipients ?? []).map((e) => e.amount).fold(
|
|
||||||
Amount(
|
|
||||||
rawValue: BigInt.zero,
|
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
|
||||||
),
|
|
||||||
(p, e) => p + e);
|
|
||||||
|
|
||||||
if (txAmount > available) {
|
if (txAmount > available) {
|
||||||
throw Exception("Insufficient Spark balance");
|
throw Exception("Insufficient Spark balance");
|
||||||
}
|
}
|
||||||
|
@ -583,7 +616,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TxData>> createSparkMintTransactions({
|
// modelled on CSparkWallet::CreateSparkMintTransactions https://github.com/firoorg/firo/blob/39c41e5e7ec634ced3700fe3f4f5509dc2e480d0/src/spark/sparkwallet.cpp#L752
|
||||||
|
Future<List<TxData>> _createSparkMintTransactions({
|
||||||
required List<UTXO> availableUtxos,
|
required List<UTXO> availableUtxos,
|
||||||
required List<MutableSparkRecipient> outputs,
|
required List<MutableSparkRecipient> outputs,
|
||||||
required bool subtractFeeFromAmount,
|
required bool subtractFeeFromAmount,
|
||||||
|
@ -593,6 +627,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
if (outputs.isEmpty) {
|
if (outputs.isEmpty) {
|
||||||
throw Exception("Cannot mint without some recipients");
|
throw Exception("Cannot mint without some recipients");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO remove when multiple recipients gui is added. Will need to handle
|
||||||
|
// addresses when confirming the transactions later as well
|
||||||
|
assert(outputs.length == 1);
|
||||||
|
|
||||||
BigInt valueToMint =
|
BigInt valueToMint =
|
||||||
outputs.map((e) => e.value).reduce((value, element) => value + element);
|
outputs.map((e) => e.value).reduce((value, element) => value + element);
|
||||||
|
|
||||||
|
@ -615,7 +654,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
// setup some vars
|
// setup some vars
|
||||||
int nChangePosInOut = -1;
|
int nChangePosInOut = -1;
|
||||||
int nChangePosRequest = nChangePosInOut;
|
int nChangePosRequest = nChangePosInOut;
|
||||||
List<MutableSparkRecipient> outputs_ = outputs.toList();
|
List<MutableSparkRecipient> outputs_ = outputs
|
||||||
|
.map((e) => MutableSparkRecipient(e.address, e.value, e.memo))
|
||||||
|
.toList(); // deep copy
|
||||||
final feesObject = await fees;
|
final feesObject = await fees;
|
||||||
final currentHeight = await chainHeight;
|
final currentHeight = await chainHeight;
|
||||||
final random = Random.secure();
|
final random = Random.secure();
|
||||||
|
@ -671,8 +712,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
vin.clear();
|
vin.clear();
|
||||||
vout.clear();
|
vout.clear();
|
||||||
setCoins.clear();
|
setCoins.clear();
|
||||||
final remainingOutputs = outputs_.toList();
|
|
||||||
|
// deep copy
|
||||||
|
final remainingOutputs = outputs_
|
||||||
|
.map((e) => MutableSparkRecipient(e.address, e.value, e.memo))
|
||||||
|
.toList();
|
||||||
final List<MutableSparkRecipient> singleTxOutputs = [];
|
final List<MutableSparkRecipient> singleTxOutputs = [];
|
||||||
|
|
||||||
if (autoMintAll) {
|
if (autoMintAll) {
|
||||||
singleTxOutputs.add(
|
singleTxOutputs.add(
|
||||||
MutableSparkRecipient(
|
MutableSparkRecipient(
|
||||||
|
@ -682,7 +728,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
BigInt remainingMintValue = mintedValue;
|
BigInt remainingMintValue = BigInt.parse(mintedValue.toString());
|
||||||
|
|
||||||
while (remainingMintValue > BigInt.zero) {
|
while (remainingMintValue > BigInt.zero) {
|
||||||
final singleMintValue =
|
final singleMintValue =
|
||||||
_min(remainingMintValue, remainingOutputs.first.value);
|
_min(remainingMintValue, remainingOutputs.first.value);
|
||||||
|
@ -877,7 +924,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|
||||||
outputs_ = remainingOutputs;
|
// deep copy
|
||||||
|
outputs_ = remainingOutputs
|
||||||
|
.map((e) => MutableSparkRecipient(e.address, e.value, e.memo))
|
||||||
|
.toList();
|
||||||
|
|
||||||
break; // Done, enough fee included.
|
break; // Done, enough fee included.
|
||||||
}
|
}
|
||||||
|
@ -926,12 +976,17 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
final builtTx = txb.build();
|
final builtTx = txb.build();
|
||||||
|
|
||||||
|
// TODO: see todo at top of this function
|
||||||
|
assert(outputs.length == 1);
|
||||||
|
|
||||||
final data = TxData(
|
final data = TxData(
|
||||||
// TODO: add fee output to recipients?
|
|
||||||
sparkRecipients: vout
|
sparkRecipients: vout
|
||||||
|
.where((e) => e.$1 is Uint8List) // ignore change
|
||||||
.map(
|
.map(
|
||||||
(e) => (
|
(e) => (
|
||||||
address: "lol",
|
address: outputs.first
|
||||||
|
.address, // for display purposes on confirm tx screen. See todos above
|
||||||
memo: "",
|
memo: "",
|
||||||
amount: Amount(
|
amount: Amount(
|
||||||
rawValue: BigInt.from(e.$2),
|
rawValue: BigInt.from(e.$2),
|
||||||
|
@ -947,6 +1002,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
rawValue: nFeeRet,
|
rawValue: nFeeRet,
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
),
|
),
|
||||||
|
usedUTXOs: vin.map((e) => e.utxo).toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nFeeRet.toInt() < data.vSize!) {
|
if (nFeeRet.toInt() < data.vSize!) {
|
||||||
|
@ -1030,7 +1086,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
throw Exception("No available UTXOs found to anonymize");
|
throw Exception("No available UTXOs found to anonymize");
|
||||||
}
|
}
|
||||||
|
|
||||||
final results = await createSparkMintTransactions(
|
final mints = await _createSparkMintTransactions(
|
||||||
subtractFeeFromAmount: subtractFeeFromAmount,
|
subtractFeeFromAmount: subtractFeeFromAmount,
|
||||||
autoMintAll: true,
|
autoMintAll: true,
|
||||||
availableUtxos: spendableUtxos,
|
availableUtxos: spendableUtxos,
|
||||||
|
@ -1045,9 +1101,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
for (final data in results) {
|
await confirmSparkMintTransactions(txData: TxData(sparkMints: mints));
|
||||||
await confirmSparkMintTransaction(txData: data);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"Exception caught in anonymizeAllSpark(): $e\n$s",
|
"Exception caught in anonymizeAllSpark(): $e\n$s",
|
||||||
|
@ -1061,196 +1115,98 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
///
|
///
|
||||||
/// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o
|
/// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o
|
||||||
Future<TxData> prepareSparkMintTransaction({required TxData txData}) async {
|
Future<TxData> prepareSparkMintTransaction({required TxData txData}) async {
|
||||||
// "this kind of transaction is generated like a regular transaction, but in
|
|
||||||
// place of [regular] outputs we put spark outputs... we construct the input
|
|
||||||
// part of the transaction first then we generate spark related data [and]
|
|
||||||
// we sign like regular transactions at the end."
|
|
||||||
|
|
||||||
// Validate inputs.
|
|
||||||
|
|
||||||
// There should be at least one input.
|
|
||||||
if (txData.utxos == null || txData.utxos!.isEmpty) {
|
|
||||||
throw Exception("No inputs provided.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate individual inputs.
|
|
||||||
for (final utxo in txData.utxos!) {
|
|
||||||
// Input amount must be greater than zero.
|
|
||||||
if (utxo.value == 0) {
|
|
||||||
throw Exception("Input value cannot be zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input value must be greater than dust limit.
|
|
||||||
if (BigInt.from(utxo.value) < cryptoCurrency.dustLimit.raw) {
|
|
||||||
throw Exception("Input value below dust limit.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate outputs.
|
|
||||||
|
|
||||||
// There should be at least one output.
|
|
||||||
if (txData.recipients == null || txData.recipients!.isEmpty) {
|
|
||||||
throw Exception("No recipients provided.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now let's limit to one output.
|
|
||||||
if (txData.recipients!.length > 1) {
|
|
||||||
throw Exception("Only one recipient supported.");
|
|
||||||
// TODO remove and test with multiple recipients.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit outputs per tx to 16.
|
|
||||||
//
|
|
||||||
// See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16
|
|
||||||
if (txData.recipients!.length > 16) {
|
|
||||||
throw Exception("Too many recipients.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit spend value per tx to 1000000000000 satoshis.
|
|
||||||
//
|
|
||||||
// See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17
|
|
||||||
// and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17
|
|
||||||
// Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31
|
|
||||||
//
|
|
||||||
// This will be added to and checked as we validate outputs.
|
|
||||||
Amount totalAmount = Amount(
|
|
||||||
rawValue: BigInt.zero,
|
|
||||||
fractionDigits: cryptoCurrency.fractionDigits,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Validate individual outputs.
|
|
||||||
for (final recipient in txData.recipients!) {
|
|
||||||
// Output amount must be greater than zero.
|
|
||||||
if (recipient.amount.raw == BigInt.zero) {
|
|
||||||
throw Exception("Output amount cannot be zero.");
|
|
||||||
// Could refactor this for loop to use an index and remove this output.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output amount must be greater than dust limit.
|
|
||||||
if (recipient.amount < cryptoCurrency.dustLimit) {
|
|
||||||
throw Exception("Output below dust limit.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not add outputs that would exceed the spend limit.
|
|
||||||
totalAmount += recipient.amount;
|
|
||||||
if (totalAmount.raw > BigInt.from(1000000000000)) {
|
|
||||||
throw Exception(
|
|
||||||
"Spend limit exceeded (10,000 FIRO per tx).",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a transaction builder and set locktime and version.
|
|
||||||
final txb = btc.TransactionBuilder(
|
|
||||||
network: _bitcoinDartNetwork,
|
|
||||||
);
|
|
||||||
txb.setLockTime(await chainHeight);
|
|
||||||
txb.setVersion(1);
|
|
||||||
|
|
||||||
final signingData = await fetchBuildTxData(txData.utxos!.toList());
|
|
||||||
|
|
||||||
// Create the serial context.
|
|
||||||
//
|
|
||||||
// "...serial_context is a byte array, which should be unique for each
|
|
||||||
// transaction, and for that we serialize and put all inputs into
|
|
||||||
// serial_context vector."
|
|
||||||
final serialContext = LibSpark.serializeMintContext(
|
|
||||||
inputs: signingData
|
|
||||||
.map((e) => (
|
|
||||||
e.utxo.txid,
|
|
||||||
e.utxo.vout,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add inputs.
|
|
||||||
for (final sd in signingData) {
|
|
||||||
txb.addInput(
|
|
||||||
sd.utxo.txid,
|
|
||||||
sd.utxo.vout,
|
|
||||||
0xffffffff -
|
|
||||||
1, // minus 1 is important. 0xffffffff on its own will burn funds
|
|
||||||
sd.output,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create mint recipients.
|
|
||||||
final mintRecipients = LibSpark.createSparkMintRecipients(
|
|
||||||
outputs: txData.recipients!
|
|
||||||
.map((e) => (
|
|
||||||
sparkAddress: e.address,
|
|
||||||
value: e.amount.raw.toInt(),
|
|
||||||
memo: "",
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
serialContext: Uint8List.fromList(serialContext),
|
|
||||||
generate: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add mint output(s).
|
|
||||||
for (final mint in mintRecipients) {
|
|
||||||
txb.addOutput(
|
|
||||||
mint.scriptPubKey,
|
|
||||||
mint.amount,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Sign the transaction accordingly
|
if (txData.sparkRecipients?.isNotEmpty != true) {
|
||||||
for (var i = 0; i < signingData.length; i++) {
|
throw Exception("Missing spark recipients.");
|
||||||
txb.sign(
|
|
||||||
vin: i,
|
|
||||||
keyPair: signingData[i].keyPair!,
|
|
||||||
witnessValue: signingData[i].utxo.value,
|
|
||||||
redeemScript: signingData[i].redeemScript,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
final recipients = txData.sparkRecipients!
|
||||||
|
.map(
|
||||||
|
(e) => MutableSparkRecipient(
|
||||||
|
e.address,
|
||||||
|
e.amount.raw,
|
||||||
|
e.memo,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final total = recipients
|
||||||
|
.map((e) => e.value)
|
||||||
|
.reduce((value, element) => value += element);
|
||||||
|
|
||||||
|
if (total < BigInt.zero) {
|
||||||
|
throw Exception("Attempted send of negative amount");
|
||||||
|
} else if (total == BigInt.zero) {
|
||||||
|
throw Exception("Attempted send of zero amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentHeight = await chainHeight;
|
||||||
|
|
||||||
|
// coin control not enabled for firo currently so we can ignore this
|
||||||
|
// final utxosToUse = txData.utxos?.toList() ?? await mainDB.isar.utxos
|
||||||
|
// .where()
|
||||||
|
// .walletIdEqualTo(walletId)
|
||||||
|
// .filter()
|
||||||
|
// .isBlockedEqualTo(false)
|
||||||
|
// .and()
|
||||||
|
// .group((q) => q.usedEqualTo(false).or().usedIsNull())
|
||||||
|
// .and()
|
||||||
|
// .valueGreaterThan(0)
|
||||||
|
// .findAll();
|
||||||
|
final spendableUtxos = await mainDB.isar.utxos
|
||||||
|
.where()
|
||||||
|
.walletIdEqualTo(walletId)
|
||||||
|
.filter()
|
||||||
|
.isBlockedEqualTo(false)
|
||||||
|
.and()
|
||||||
|
.group((q) => q.usedEqualTo(false).or().usedIsNull())
|
||||||
|
.and()
|
||||||
|
.valueGreaterThan(0)
|
||||||
|
.findAll();
|
||||||
|
|
||||||
|
spendableUtxos.removeWhere(
|
||||||
|
(e) => !e.isConfirmed(
|
||||||
|
currentHeight,
|
||||||
|
cryptoCurrency.minConfirms,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spendableUtxos.isEmpty) {
|
||||||
|
throw Exception("No available UTXOs found to anonymize");
|
||||||
|
}
|
||||||
|
|
||||||
|
final available = spendableUtxos
|
||||||
|
.map((e) => BigInt.from(e.value))
|
||||||
|
.reduce((value, element) => value += element);
|
||||||
|
|
||||||
|
final bool subtractFeeFromAmount;
|
||||||
|
if (available < total) {
|
||||||
|
throw Exception("Insufficient balance");
|
||||||
|
} else if (available == total) {
|
||||||
|
subtractFeeFromAmount = true;
|
||||||
|
} else {
|
||||||
|
subtractFeeFromAmount = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mints = await _createSparkMintTransactions(
|
||||||
|
subtractFeeFromAmount: subtractFeeFromAmount,
|
||||||
|
autoMintAll: false,
|
||||||
|
availableUtxos: spendableUtxos,
|
||||||
|
outputs: recipients,
|
||||||
|
);
|
||||||
|
|
||||||
|
return txData.copyWith(sparkMints: mints);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"Caught exception while signing spark mint transaction: $e\n$s",
|
"Exception caught in prepareSparkMintTransaction(): $e\n$s",
|
||||||
level: LogLevel.Error,
|
level: LogLevel.Warning,
|
||||||
);
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
final builtTx = txb.build();
|
|
||||||
|
|
||||||
// TODO any changes to this txData object required?
|
|
||||||
return txData.copyWith(
|
|
||||||
// recipients: [
|
|
||||||
// (
|
|
||||||
// amount: Amount(
|
|
||||||
// rawValue: BigInt.from(incomplete.outs[0].value!),
|
|
||||||
// fractionDigits: cryptoCurrency.fractionDigits,
|
|
||||||
// ),
|
|
||||||
// address: "no address for lelantus mints",
|
|
||||||
// )
|
|
||||||
// ],
|
|
||||||
vSize: builtTx.virtualSize(),
|
|
||||||
txid: builtTx.getId(),
|
|
||||||
raw: builtTx.toHex(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broadcast a tx and TODO update Spark balance.
|
Future<TxData> confirmSparkMintTransactions({required TxData txData}) async {
|
||||||
Future<TxData> confirmSparkMintTransaction({required TxData txData}) async {
|
final futures = txData.sparkMints!.map((e) => confirmSend(txData: e));
|
||||||
// Broadcast tx.
|
return txData.copyWith(sparkMints: await Future.wait(futures));
|
||||||
final txid = await electrumXClient.broadcastTransaction(
|
|
||||||
rawTx: txData.raw!,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check txid.
|
|
||||||
if (txid == txData.txid!) {
|
|
||||||
print("SPARK TXIDS MATCH!!");
|
|
||||||
} else {
|
|
||||||
print("SUBMITTED SPARK TXID DOES NOT MATCH WHAT WE GENERATED");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO update spark balance.
|
|
||||||
|
|
||||||
return txData.copyWith(
|
|
||||||
txid: txid,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1259,7 +1215,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
||||||
// what ever class this mixin is used on uses LelantusInterface as well)
|
// what ever class this mixin is used on uses LelantusInterface as well)
|
||||||
final normalBalanceFuture = super.updateBalance();
|
final normalBalanceFuture = super.updateBalance();
|
||||||
|
|
||||||
// todo: spark balance aka update info.tertiaryBalance
|
// todo: spark balance aka update info.tertiaryBalance here?
|
||||||
|
// currently happens on spark coins update/refresh
|
||||||
|
|
||||||
// wait for normalBalanceFuture to complete before returning
|
// wait for normalBalanceFuture to complete before returning
|
||||||
await normalBalanceFuture;
|
await normalBalanceFuture;
|
||||||
|
@ -1477,4 +1434,9 @@ class MutableSparkRecipient {
|
||||||
String memo;
|
String memo;
|
||||||
|
|
||||||
MutableSparkRecipient(this.address, this.value, this.memo);
|
MutableSparkRecipient(this.address, this.value, this.memo);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'MutableSparkRecipient{ address: $address, value: $value, memo: $memo }';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue