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 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/models/isar/models/isar_models.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/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/route_generator.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/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/spark_interface.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/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||
|
@ -58,6 +63,11 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
late final Coin coin;
|
||||
late final String walletId;
|
||||
late final ClipboardInterface clipboard;
|
||||
late final bool supportsSpark;
|
||||
|
||||
String? _sparkAddress;
|
||||
String? _qrcodeContent;
|
||||
bool _showSparkAddress = true;
|
||||
|
||||
Future<void> generateNewAddress() async {
|
||||
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
|
||||
void initState() {
|
||||
walletId = widget.walletId;
|
||||
coin = ref.read(pWalletCoin(walletId));
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
final receivingAddress = ref.watch(pWalletReceivingAddress(walletId));
|
||||
|
||||
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(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -225,86 +318,239 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
clipboard.setData(
|
||||
ClipboardData(text: receivingAddress),
|
||||
);
|
||||
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(
|
||||
ConditionalParent(
|
||||
condition: supportsSpark,
|
||||
builder: (child) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<bool>(
|
||||
value: _showSparkAddress,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text(
|
||||
receivingAddress,
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
"Spark address",
|
||||
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 &&
|
||||
coin != Coin.ethereum &&
|
||||
coin != Coin.banano &&
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.stellar &&
|
||||
coin != Coin.stellarTestnet &&
|
||||
coin != Coin.tezos)
|
||||
if (ref.watch(pWallets
|
||||
.select((value) => value.getWallet(walletId)))
|
||||
is MultiAddressInterface ||
|
||||
supportsSpark)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.ethereum &&
|
||||
coin != Coin.banano &&
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.stellar &&
|
||||
coin != Coin.stellarTestnet &&
|
||||
coin != Coin.tezos)
|
||||
if (ref.watch(pWallets
|
||||
.select((value) => value.getWallet(walletId)))
|
||||
is MultiAddressInterface ||
|
||||
supportsSpark)
|
||||
TextButton(
|
||||
onPressed: generateNewAddress,
|
||||
onPressed: supportsSpark && _showSparkAddress
|
||||
? generateNewSparkAddress
|
||||
: generateNewAddress,
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(context),
|
||||
|
@ -328,7 +574,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
QrImageView(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
receivingAddress,
|
||||
_qrcodeContent ?? "",
|
||||
{},
|
||||
),
|
||||
size: MediaQuery.of(context).size.width / 2,
|
||||
|
@ -347,7 +593,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
RouteGenerator.useMaterialPageRoute,
|
||||
builder: (_) => GenerateUriQrCodeView(
|
||||
coin: coin,
|
||||
receivingAddress: receivingAddress,
|
||||
receivingAddress: _qrcodeContent ?? "",
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name: GenerateUriQrCodeView.routeName,
|
||||
|
|
|
@ -120,7 +120,7 @@ class _ConfirmTransactionViewState
|
|||
),
|
||||
);
|
||||
|
||||
late String txid;
|
||||
final List<String> txids = [];
|
||||
Future<TxData> txDataFuture;
|
||||
|
||||
final note = noteController.text;
|
||||
|
@ -143,7 +143,12 @@ class _ConfirmTransactionViewState
|
|||
if (wallet is FiroWallet) {
|
||||
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||
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;
|
||||
|
||||
case FiroType.lelantus:
|
||||
|
@ -175,17 +180,24 @@ class _ConfirmTransactionViewState
|
|||
sendProgressController.triggerSuccess?.call();
|
||||
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);
|
||||
|
||||
// save note
|
||||
await ref.read(mainDBProvider).putTransactionNote(
|
||||
TransactionNote(
|
||||
walletId: walletId,
|
||||
txid: txid,
|
||||
value: note,
|
||||
),
|
||||
);
|
||||
for (final txid in txids) {
|
||||
await ref.read(mainDBProvider).putTransactionNote(
|
||||
TransactionNote(
|
||||
walletId: walletId,
|
||||
txid: txid,
|
||||
value: note,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.isTokenTx) {
|
||||
unawaited(ref.read(tokenServiceProvider)!.refresh());
|
||||
|
@ -333,6 +345,48 @@ class _ConfirmTransactionViewState
|
|||
} else {
|
||||
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(
|
||||
condition: !isDesktop,
|
||||
builder: (child) => Background(
|
||||
|
@ -438,7 +492,8 @@ class _ConfirmTransactionViewState
|
|||
Text(
|
||||
widget.isPaynymTransaction
|
||||
? widget.txData.paynymAccountLite!.nymName
|
||||
: widget.txData.recipients!.first.address,
|
||||
: widget.txData.recipients?.first.address ??
|
||||
widget.txData.sparkRecipients!.first.address,
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
],
|
||||
|
@ -457,7 +512,7 @@ class _ConfirmTransactionViewState
|
|||
),
|
||||
SelectableText(
|
||||
ref.watch(pAmountFormatter(coin)).format(
|
||||
widget.txData.amount!,
|
||||
amount,
|
||||
ethContract: ref
|
||||
.watch(tokenServiceProvider)
|
||||
?.tokenContract,
|
||||
|
@ -482,9 +537,7 @@ class _ConfirmTransactionViewState
|
|||
style: STextStyles.smallMed12(context),
|
||||
),
|
||||
SelectableText(
|
||||
ref
|
||||
.watch(pAmountFormatter(coin))
|
||||
.format(widget.txData.fee!),
|
||||
ref.watch(pAmountFormatter(coin)).format(fee!),
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
|
@ -508,7 +561,7 @@ class _ConfirmTransactionViewState
|
|||
height: 4,
|
||||
),
|
||||
SelectableText(
|
||||
"~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
||||
"~${fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
],
|
||||
|
@ -639,9 +692,6 @@ class _ConfirmTransactionViewState
|
|||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
// TODO: [prio=high] spark transaction specifics - better handling
|
||||
final amount = widget.txData.amount ??
|
||||
widget.txData.amountSpark!;
|
||||
final externalCalls = ref.watch(
|
||||
prefsChangeNotifierProvider.select(
|
||||
(value) => value.externalCalls));
|
||||
|
@ -778,24 +828,15 @@ class _ConfirmTransactionViewState
|
|||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final fee = widget.txData.fee!;
|
||||
|
||||
return SelectableText(
|
||||
ref
|
||||
.watch(pAmountFormatter(coin))
|
||||
.format(fee),
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark,
|
||||
),
|
||||
);
|
||||
},
|
||||
SelectableText(
|
||||
ref.watch(pAmountFormatter(coin)).format(fee!),
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1000,15 +1041,9 @@ class _ConfirmTransactionViewState
|
|||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final fee = widget.txData.fee!;
|
||||
|
||||
return SelectableText(
|
||||
ref.watch(pAmountFormatter(coin)).format(fee),
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
);
|
||||
},
|
||||
child: SelectableText(
|
||||
ref.watch(pAmountFormatter(coin)).format(fee!),
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1044,7 +1079,7 @@ class _ConfirmTransactionViewState
|
|||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
child: SelectableText(
|
||||
"~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
||||
"~${fee!.raw.toInt() ~/ widget.txData.vSize!}",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
),
|
||||
|
@ -1088,31 +1123,22 @@ class _ConfirmTransactionViewState
|
|||
.textConfirmTotalAmount,
|
||||
),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final fee = widget.txData.fee!;
|
||||
|
||||
// TODO: [prio=high] spark transaction specifics - better handling
|
||||
final amount =
|
||||
widget.txData.amount ?? widget.txData.amountSpark!;
|
||||
return SelectableText(
|
||||
ref
|
||||
.watch(pAmountFormatter(coin))
|
||||
.format(amount + fee),
|
||||
style: isDesktop
|
||||
? STextStyles.desktopTextExtraExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textConfirmTotalAmount,
|
||||
)
|
||||
: STextStyles.itemSubtitle12(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textConfirmTotalAmount,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
);
|
||||
}),
|
||||
SelectableText(
|
||||
ref.watch(pAmountFormatter(coin)).format(amount + fee!),
|
||||
style: isDesktop
|
||||
? STextStyles.desktopTextExtraExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.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) {
|
||||
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||
case FiroType.public:
|
||||
txDataFuture = wallet.prepareSend(
|
||||
txData: TxData(
|
||||
recipients: _isSparkAddress
|
||||
? null
|
||||
: [(address: _address!, amount: amount)],
|
||||
sparkRecipients: _isSparkAddress
|
||||
? [
|
||||
(
|
||||
address: _address!,
|
||||
amount: amount,
|
||||
memo: memoController.text,
|
||||
)
|
||||
]
|
||||
: null,
|
||||
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||
utxos: (wallet is CoinControlInterface &&
|
||||
coinControlEnabled &&
|
||||
selectedUTXOs.isNotEmpty)
|
||||
? selectedUTXOs
|
||||
: null,
|
||||
),
|
||||
);
|
||||
if (_isSparkAddress) {
|
||||
txDataFuture = wallet.prepareSparkMintTransaction(
|
||||
txData: TxData(
|
||||
sparkRecipients: [
|
||||
(
|
||||
address: _address!,
|
||||
amount: amount,
|
||||
memo: memoController.text,
|
||||
)
|
||||
],
|
||||
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||
utxos: (wallet is CoinControlInterface &&
|
||||
coinControlEnabled &&
|
||||
selectedUTXOs.isNotEmpty)
|
||||
? selectedUTXOs
|
||||
: null,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
|
||||
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/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/spark_interface.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';
|
||||
|
@ -115,6 +116,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
late final String walletId;
|
||||
late final Coin coin;
|
||||
|
||||
late final bool isSparkWallet;
|
||||
|
||||
late final bool _shouldDisableAutoSyncOnLogOut;
|
||||
|
||||
late WalletSyncStatus _currentSyncStatus;
|
||||
|
@ -171,6 +174,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
_shouldDisableAutoSyncOnLogOut = false;
|
||||
}
|
||||
|
||||
isSparkWallet = wallet is SparkInterface;
|
||||
|
||||
if (coin == Coin.firo &&
|
||||
(wallet as FiroWallet).lelantusCoinIsarRescanRequired) {
|
||||
_rescanningOnOpen = true;
|
||||
|
@ -758,11 +763,11 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (coin == Coin.firo)
|
||||
if (isSparkWallet)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (coin == Coin.firo)
|
||||
if (isSparkWallet)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
|
|
|
@ -321,29 +321,39 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
} else if (wallet is FiroWallet) {
|
||||
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
|
||||
case FiroType.public:
|
||||
txDataFuture = wallet.prepareSend(
|
||||
txData: TxData(
|
||||
recipients: _isSparkAddress
|
||||
? null
|
||||
: [(address: _address!, amount: amount)],
|
||||
sparkRecipients: _isSparkAddress
|
||||
? [
|
||||
(
|
||||
address: _address!,
|
||||
amount: amount,
|
||||
memo: memoController.text,
|
||||
)
|
||||
]
|
||||
: null,
|
||||
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||
utxos: (wallet is CoinControlInterface &&
|
||||
coinControlEnabled &&
|
||||
ref.read(desktopUseUTXOs).isNotEmpty)
|
||||
? ref.read(desktopUseUTXOs)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
if (_isSparkAddress) {
|
||||
txDataFuture = wallet.prepareSparkMintTransaction(
|
||||
txData: TxData(
|
||||
sparkRecipients: [
|
||||
(
|
||||
address: _address!,
|
||||
amount: amount,
|
||||
memo: memoController.text,
|
||||
)
|
||||
],
|
||||
feeRateType: ref.read(feeRateTypeStateProvider),
|
||||
satsPerVByte: isCustomFee ? customFeeRate : null,
|
||||
utxos: (wallet is CoinControlInterface &&
|
||||
coinControlEnabled &&
|
||||
ref.read(desktopUseUTXOs).isNotEmpty)
|
||||
? ref.read(desktopUseUTXOs)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
|
||||
case FiroType.lelantus:
|
||||
|
@ -579,7 +589,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
ref.read(pWallets).getWallet(walletId).cryptoCurrency;
|
||||
final isValidAddress = walletCurrency.validateAddress(address ?? "");
|
||||
|
||||
_isSparkAddress = isValidAddress
|
||||
_isSparkAddress = isValidAddress &&
|
||||
ref.read(publicPrivateBalanceStateProvider.state).state !=
|
||||
FiroType.lelantus
|
||||
? SparkInterface.validateSparkAddress(
|
||||
address: address!,
|
||||
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(
|
||||
height: 10,
|
||||
),
|
||||
if (isStellar || _isSparkAddress)
|
||||
if (isStellar ||
|
||||
(_isSparkAddress &&
|
||||
ref.watch(publicPrivateBalanceStateProvider) !=
|
||||
FiroType.public))
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
|
|
|
@ -62,6 +62,7 @@ class TxData {
|
|||
Amount amount,
|
||||
String memo,
|
||||
})>? sparkRecipients;
|
||||
final List<TxData>? sparkMints;
|
||||
|
||||
TxData({
|
||||
this.feeRateType,
|
||||
|
@ -94,6 +95,7 @@ class TxData {
|
|||
this.mintsMapLelantus,
|
||||
this.tezosOperationsList,
|
||||
this.sparkRecipients,
|
||||
this.sparkMints,
|
||||
});
|
||||
|
||||
Amount? get amount => recipients != null && recipients!.isNotEmpty
|
||||
|
@ -150,6 +152,7 @@ class TxData {
|
|||
String memo,
|
||||
})>?
|
||||
sparkRecipients,
|
||||
List<TxData>? sparkMints,
|
||||
}) {
|
||||
return TxData(
|
||||
feeRateType: feeRateType ?? this.feeRateType,
|
||||
|
@ -183,6 +186,7 @@ class TxData {
|
|||
mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus,
|
||||
tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList,
|
||||
sparkRecipients: sparkRecipients ?? this.sparkRecipients,
|
||||
sparkMints: sparkMints ?? this.sparkMints,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -218,5 +222,6 @@ class TxData {
|
|||
'mintsMapLelantus: $mintsMapLelantus, '
|
||||
'tezosOperationsList: $tezosOperationsList, '
|
||||
'sparkRecipients: $sparkRecipients, '
|
||||
'sparkMints: $sparkMints, '
|
||||
'}';
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoindart/bitcoindart.dart' as btc;
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
@ -19,8 +20,12 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_int
|
|||
|
||||
const kDefaultSparkIndex = 1;
|
||||
|
||||
// TODO dart style constants. Maybe move to spark lib?
|
||||
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_SPARKSMINT = 0xd2;
|
||||
const OP_SPARKSPEND = 0xd3;
|
||||
|
@ -125,6 +130,47 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
Future<TxData> prepareSendSpark({
|
||||
required TxData txData,
|
||||
}) 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
|
||||
final coins = await mainDB.isar.sparkCoins
|
||||
.where()
|
||||
|
@ -140,19 +186,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
|
||||
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) {
|
||||
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<MutableSparkRecipient> outputs,
|
||||
required bool subtractFeeFromAmount,
|
||||
|
@ -593,6 +627,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
if (outputs.isEmpty) {
|
||||
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 =
|
||||
outputs.map((e) => e.value).reduce((value, element) => value + element);
|
||||
|
||||
|
@ -615,7 +654,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
// setup some vars
|
||||
int nChangePosInOut = -1;
|
||||
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 currentHeight = await chainHeight;
|
||||
final random = Random.secure();
|
||||
|
@ -671,8 +712,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
vin.clear();
|
||||
vout.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 = [];
|
||||
|
||||
if (autoMintAll) {
|
||||
singleTxOutputs.add(
|
||||
MutableSparkRecipient(
|
||||
|
@ -682,7 +728,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
BigInt remainingMintValue = mintedValue;
|
||||
BigInt remainingMintValue = BigInt.parse(mintedValue.toString());
|
||||
|
||||
while (remainingMintValue > BigInt.zero) {
|
||||
final singleMintValue =
|
||||
_min(remainingMintValue, remainingOutputs.first.value);
|
||||
|
@ -877,7 +924,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
++i;
|
||||
}
|
||||
|
||||
outputs_ = remainingOutputs;
|
||||
// deep copy
|
||||
outputs_ = remainingOutputs
|
||||
.map((e) => MutableSparkRecipient(e.address, e.value, e.memo))
|
||||
.toList();
|
||||
|
||||
break; // Done, enough fee included.
|
||||
}
|
||||
|
@ -926,12 +976,17 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
rethrow;
|
||||
}
|
||||
final builtTx = txb.build();
|
||||
|
||||
// TODO: see todo at top of this function
|
||||
assert(outputs.length == 1);
|
||||
|
||||
final data = TxData(
|
||||
// TODO: add fee output to recipients?
|
||||
sparkRecipients: vout
|
||||
.where((e) => e.$1 is Uint8List) // ignore change
|
||||
.map(
|
||||
(e) => (
|
||||
address: "lol",
|
||||
address: outputs.first
|
||||
.address, // for display purposes on confirm tx screen. See todos above
|
||||
memo: "",
|
||||
amount: Amount(
|
||||
rawValue: BigInt.from(e.$2),
|
||||
|
@ -947,6 +1002,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
rawValue: nFeeRet,
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
),
|
||||
usedUTXOs: vin.map((e) => e.utxo).toList(),
|
||||
);
|
||||
|
||||
if (nFeeRet.toInt() < data.vSize!) {
|
||||
|
@ -1030,7 +1086,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
throw Exception("No available UTXOs found to anonymize");
|
||||
}
|
||||
|
||||
final results = await createSparkMintTransactions(
|
||||
final mints = await _createSparkMintTransactions(
|
||||
subtractFeeFromAmount: subtractFeeFromAmount,
|
||||
autoMintAll: true,
|
||||
availableUtxos: spendableUtxos,
|
||||
|
@ -1045,9 +1101,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
],
|
||||
);
|
||||
|
||||
for (final data in results) {
|
||||
await confirmSparkMintTransaction(txData: data);
|
||||
}
|
||||
await confirmSparkMintTransactions(txData: TxData(sparkMints: mints));
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"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
|
||||
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 {
|
||||
// Sign the transaction accordingly
|
||||
for (var i = 0; i < signingData.length; i++) {
|
||||
txb.sign(
|
||||
vin: i,
|
||||
keyPair: signingData[i].keyPair!,
|
||||
witnessValue: signingData[i].utxo.value,
|
||||
redeemScript: signingData[i].redeemScript,
|
||||
);
|
||||
if (txData.sparkRecipients?.isNotEmpty != true) {
|
||||
throw Exception("Missing spark recipients.");
|
||||
}
|
||||
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) {
|
||||
Logging.instance.log(
|
||||
"Caught exception while signing spark mint transaction: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
"Exception caught in prepareSparkMintTransaction(): $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
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> confirmSparkMintTransaction({required TxData txData}) async {
|
||||
// Broadcast tx.
|
||||
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,
|
||||
);
|
||||
Future<TxData> confirmSparkMintTransactions({required TxData txData}) async {
|
||||
final futures = txData.sparkMints!.map((e) => confirmSend(txData: e));
|
||||
return txData.copyWith(sparkMints: await Future.wait(futures));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1259,7 +1215,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
|
|||
// what ever class this mixin is used on uses LelantusInterface as well)
|
||||
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
|
||||
await normalBalanceFuture;
|
||||
|
@ -1477,4 +1434,9 @@ class MutableSparkRecipient {
|
|||
String 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