Untested ecash fusion port. Manual port of https://github.com/cypherstack/stack_wallet/pull/705 combined with manual port to v2 transactions for ecash as well as a couple other changes ported from the wallets_refactor branch

This commit is contained in:
julian 2023-12-04 14:50:38 -06:00
parent 747565fa16
commit 9a9c9550ee
10 changed files with 426 additions and 234 deletions

View file

@ -61,10 +61,11 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
FusionOption _option = FusionOption.continuous; FusionOption _option = FusionOption.continuous;
Future<void> _startFusion() async { Future<void> _startFusion() async {
final fusionWallet = ref final wallet = ref
.read(walletsChangeNotifierProvider) .read(walletsChangeNotifierProvider)
.getManager(widget.walletId) .getManager(widget.walletId)
.wallet as FusionWalletInterface; .wallet;
final fusionWallet = wallet as FusionWalletInterface;
try { try {
fusionWallet.uiState = ref.read( fusionWallet.uiState = ref.read(
@ -89,7 +90,9 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
); );
// update user prefs (persistent) // update user prefs (persistent)
ref.read(prefsChangeNotifierProvider).fusionServerInfo = newInfo; ref
.read(prefsChangeNotifierProvider)
.setFusionServerInfo(wallet.coin, newInfo);
unawaited( unawaited(
fusionWallet.fuse( fusionWallet.fuse(
@ -113,7 +116,11 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
portFocusNode = FocusNode(); portFocusNode = FocusNode();
fusionRoundFocusNode = FocusNode(); fusionRoundFocusNode = FocusNode();
final info = ref.read(prefsChangeNotifierProvider).fusionServerInfo; final info = ref.read(prefsChangeNotifierProvider).getFusionServerInfo(ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet
.coin);
serverController.text = info.host; serverController.text = info.host;
portController.text = info.port.toString(); portController.text = info.port.toString();
_enableSSLCheckbox = info.ssl; _enableSSLCheckbox = info.ssl;
@ -150,7 +157,7 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: const AppBarBackButton(), leading: const AppBarBackButton(),
title: Text( title: Text(
"CashFusion", "Fusion",
style: STextStyles.navBarTitle(context), style: STextStyles.navBarTitle(context),
), ),
titleSpacing: 0, titleSpacing: 0,
@ -189,7 +196,7 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
children: [ children: [
RoundedWhiteContainer( RoundedWhiteContainer(
child: Text( child: Text(
"CashFusion allows you to anonymize your BCH coins.", "Fusion helps anonymize your coins by mixing them.",
style: STextStyles.w500_12(context).copyWith( style: STextStyles.w500_12(context).copyWith(
color: Theme.of(context) color: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
@ -214,7 +221,11 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
CustomTextButton( CustomTextButton(
text: "Default", text: "Default",
onTap: () { onTap: () {
const def = FusionInfo.DEFAULTS; final def = kFusionServerInfoDefaults[ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet
.coin]!;
serverController.text = def.host; serverController.text = def.host;
portController.text = def.port.toString(); portController.text = def.port.toString();
fusionRoundController.text = fusionRoundController.text =

View file

@ -18,6 +18,7 @@ import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart'; import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
@ -43,6 +44,8 @@ class FusionProgressView extends ConsumerStatefulWidget {
} }
class _FusionProgressViewState extends ConsumerState<FusionProgressView> { class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
late final Coin coin;
Future<bool> _requestAndProcessCancel() async { Future<bool> _requestAndProcessCancel() async {
final shouldCancel = await showDialog<bool?>( final shouldCancel = await showDialog<bool?>(
context: context, context: context,
@ -88,6 +91,16 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
} }
} }
@override
void initState() {
coin = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet
.coin;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool _succeeded = final bool _succeeded =
@ -230,7 +243,8 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
.getManager(widget.walletId) .getManager(widget.walletId)
.wallet as FusionWalletInterface; .wallet as FusionWalletInterface;
final fusionInfo = ref.read(prefsChangeNotifierProvider).fusionServerInfo; final fusionInfo =
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin);
try { try {
fusionWallet.uiState = ref.read( fusionWallet.uiState = ref.read(

View file

@ -845,7 +845,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
onTap: () { onTap: () {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
coin == Coin.bitcoincash || coin == Coin.bitcoincash ||
coin == Coin.bitcoincashTestnet coin == Coin.bitcoincashTestnet ||
coin == Coin.eCash
? AllTransactionsV2View.routeName ? AllTransactionsV2View.routeName
: AllTransactionsView.routeName, : AllTransactionsView.routeName,
arguments: walletId, arguments: walletId,
@ -902,7 +903,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
children: [ children: [
Expanded( Expanded(
child: coin == Coin.bitcoincash || child: coin == Coin.bitcoincash ||
coin == Coin.bitcoincashTestnet coin ==
Coin.bitcoincashTestnet ||
coin == Coin.eCash
? TransactionsV2List( ? TransactionsV2List(
walletId: widget.walletId, walletId: widget.walletId,
) )

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
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/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';
@ -58,6 +59,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
late final FocusNode portFocusNode; late final FocusNode portFocusNode;
late final TextEditingController fusionRoundController; late final TextEditingController fusionRoundController;
late final FocusNode fusionRoundFocusNode; late final FocusNode fusionRoundFocusNode;
late final Coin coin;
bool _enableStartButton = false; bool _enableStartButton = false;
bool _enableSSLCheckbox = false; bool _enableSSLCheckbox = false;
@ -93,7 +95,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
); );
// update user prefs (persistent) // update user prefs (persistent)
ref.read(prefsChangeNotifierProvider).fusionServerInfo = newInfo; ref.read(prefsChangeNotifierProvider).setFusionServerInfo(coin, newInfo);
unawaited( unawaited(
fusionWallet.fuse( fusionWallet.fuse(
@ -121,8 +123,14 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
serverFocusNode = FocusNode(); serverFocusNode = FocusNode();
portFocusNode = FocusNode(); portFocusNode = FocusNode();
fusionRoundFocusNode = FocusNode(); fusionRoundFocusNode = FocusNode();
coin = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet
.coin;
final info = ref.read(prefsChangeNotifierProvider).fusionServerInfo; final info =
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin);
serverController.text = info.host; serverController.text = info.host;
portController.text = info.port.toString(); portController.text = info.port.toString();
_enableSSLCheckbox = info.ssl; _enableSSLCheckbox = info.ssl;
@ -197,7 +205,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
width: 12, width: 12,
), ),
Text( Text(
"CashFusion", "Fusion",
style: STextStyles.desktopH3(context), style: STextStyles.desktopH3(context),
), ),
], ],
@ -219,7 +227,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
), ),
RichText( RichText(
text: TextSpan( text: TextSpan(
text: "What is CashFusion?", text: "What is Fusion?",
style: STextStyles.richLink(context).copyWith( style: STextStyles.richLink(context).copyWith(
fontSize: 16, fontSize: 16,
), ),
@ -248,7 +256,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
.spaceBetween, .spaceBetween,
children: [ children: [
Text( Text(
"What is CashFusion?", "What is Fusion?",
style: STextStyles.desktopH2( style: STextStyles.desktopH2(
context), context),
), ),
@ -308,7 +316,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
child: Row( child: Row(
children: [ children: [
Text( Text(
"CashFusion allows you to anonymize your BCH coins.", "Fusion helps anonymize your coins by mixing them.",
style: style:
STextStyles.desktopTextExtraExtraSmall(context), STextStyles.desktopTextExtraExtraSmall(context),
), ),
@ -336,7 +344,11 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
CustomTextButton( CustomTextButton(
text: "Default", text: "Default",
onTap: () { onTap: () {
const def = FusionInfo.DEFAULTS; final def = kFusionServerInfoDefaults[ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet
.coin]!;
serverController.text = def.host; serverController.text = def.host;
portController.text = def.port.toString(); portController.text = def.port.toString();
fusionRoundController.text = fusionRoundController.text =

View file

@ -283,12 +283,14 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> {
/// Fuse again. /// Fuse again.
void _fuseAgain() async { void _fuseAgain() async {
final fusionWallet = ref final wallet = ref
.read(walletsChangeNotifierProvider) .read(walletsChangeNotifierProvider)
.getManager(widget.walletId) .getManager(widget.walletId)
.wallet as FusionWalletInterface; .wallet;
final fusionWallet = wallet as FusionWalletInterface;
final fusionInfo = ref.read(prefsChangeNotifierProvider).fusionServerInfo; final fusionInfo =
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(wallet.coin);
try { try {
fusionWallet.uiState = ref.read( fusionWallet.uiState = ref.read(

View file

@ -482,7 +482,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
} else { } else {
await Navigator.of(context).pushNamed( await Navigator.of(context).pushNamed(
coin == Coin.bitcoincash || coin == Coin.bitcoincash ||
coin == Coin.bitcoincashTestnet coin == Coin.bitcoincashTestnet ||
coin == Coin.eCash
? AllTransactionsV2View.routeName ? AllTransactionsV2View.routeName
: AllTransactionsView.routeName, : AllTransactionsView.routeName,
arguments: widget.walletId, arguments: widget.walletId,
@ -520,7 +521,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
walletId: widget.walletId, walletId: widget.walletId,
) )
: coin == Coin.bitcoincash || : coin == Coin.bitcoincash ||
coin == Coin.bitcoincashTestnet coin == Coin.bitcoincashTestnet ||
coin == Coin.eCash
? TransactionsV2List( ? TransactionsV2List(
walletId: widget.walletId, walletId: widget.walletId,
) )

View file

@ -125,8 +125,8 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
), ),
if (manager.hasFusionSupport) if (manager.hasFusionSupport)
_MoreFeaturesItem( _MoreFeaturesItem(
label: "CashFusion", label: "Fusion",
detail: "Decentralized Bitcoin Cash mixing protocol", detail: "Decentralized mixing protocol",
iconAsset: Assets.svg.cashFusion, iconAsset: Assets.svg.cashFusion,
onPressed: () => widget.onFusionPressed?.call(), onPressed: () => widget.onFusionPressed?.call(),
), ),

View file

@ -26,9 +26,13 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/coins/bitcoincash/bch_utils.dart';
import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
@ -37,6 +41,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_
import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/mixins/xpubable.dart'; import 'package:stackwallet/services/mixins/xpubable.dart';
@ -50,6 +55,7 @@ import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
@ -130,7 +136,12 @@ String constructDerivePath({
} }
class ECashWallet extends CoinServiceAPI class ECashWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface with
WalletCache,
WalletDB,
ElectrumXParsing,
CoinControlInterface,
FusionWalletInterface
implements XPubAble { implements XPubAble {
ECashWallet({ ECashWallet({
required String walletId, required String walletId,
@ -162,6 +173,19 @@ class ECashWallet extends CoinServiceAPI
await updateCachedBalance(_balance!); await updateCachedBalance(_balance!);
}, },
); );
initFusionInterface(
walletId: walletId,
coin: coin,
db: db,
getWalletCachedElectrumX: () => cachedElectrumXClient,
getNextUnusedChangeAddress: _getUnusedChangeAddresses,
getChainHeight: () async => chainHeight,
updateWalletUTXOS: _updateUTXOs,
mnemonic: mnemonicString,
mnemonicPassphrase: mnemonicPassphrase,
network: _network,
convertToScriptHash: _convertToScriptHash,
);
} }
static const integrationTestFlag = static const integrationTestFlag =
@ -185,6 +209,81 @@ class ECashWallet extends CoinServiceAPI
} }
} }
Future<List<isar_models.Address>> _getUnusedChangeAddresses({
int numberOfAddresses = 1,
}) async {
if (numberOfAddresses < 1) {
throw ArgumentError.value(
numberOfAddresses,
"numberOfAddresses",
"Must not be less than 1",
);
}
final changeAddresses = await db
.getAddresses(walletId)
.filter()
.typeEqualTo(isar_models.AddressType.p2pkh)
.subTypeEqualTo(isar_models.AddressSubType.change)
.derivationPath((q) => q.not().valueStartsWith("m/44'/0'"))
.sortByDerivationIndex()
.findAll();
final List<isar_models.Address> unused = [];
for (final addr in changeAddresses) {
if (await _isUnused(addr.value)) {
unused.add(addr);
if (unused.length == numberOfAddresses) {
return unused;
}
}
}
// if not returned by now, we need to create more addresses
int countMissing = numberOfAddresses - unused.length;
int nextIndex =
changeAddresses.isEmpty ? 0 : changeAddresses.last.derivationIndex + 1;
while (countMissing > 0) {
// create a new address
final address = await _generateAddressForChain(
1,
nextIndex,
DerivePathTypeExt.primaryFor(coin),
);
nextIndex++;
await db.updateOrPutAddresses([address]);
// check if it has been used before adding
if (await _isUnused(address.value)) {
unused.add(address);
countMissing--;
}
}
return unused;
}
Future<bool> _isUnused(String address) async {
final txCountInDB = await db
.getTransactions(_walletId)
.filter()
.address((q) => q.valueEqualTo(address))
.count();
if (txCountInDB == 0) {
// double check via electrumx
// _getTxCountForAddress can throw!
// final count = await getTxCount(address: address);
// if (count == 0) {
return true;
// }
}
return false;
}
@override @override
set isFavorite(bool markFavorite) { set isFavorite(bool markFavorite) {
_isFavorite = markFavorite; _isFavorite = markFavorite;
@ -1160,6 +1259,8 @@ class ECashWallet extends CoinServiceAPI
} }
}).toSet(); }).toSet();
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
final List<Map<String, dynamic>> allTxHashes = final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory([...receivingAddresses, ...changeAddresses]); await _fetchHistory([...receivingAddresses, ...changeAddresses]);
@ -1194,207 +1295,168 @@ class ECashWallet extends CoinServiceAPI
} }
} }
final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txns = []; final List<TransactionV2> txns = [];
for (final txData in allTransactions) { for (final txData in allTransactions) {
Set<String> inputAddresses = {}; // set to true if any inputs were detected as owned by this wallet
Set<String> outputAddresses = {}; bool wasSentFromThisWallet = false;
Logging.instance.log(txData, level: LogLevel.Fatal); // set to true if any outputs were detected as owned by this wallet
bool wasReceivedInThisWallet = false;
Amount totalInputValue = Amount( BigInt amountReceivedInThisWallet = BigInt.zero;
rawValue: BigInt.from(0), BigInt changeAmountReceivedInThisWallet = BigInt.zero;
fractionDigits: coin.decimals,
);
Amount totalOutputValue = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
Amount amountSentFromWallet = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
Amount amountReceivedInWallet = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
Amount changeAmount = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
// parse inputs // parse inputs
for (final input in txData["vin"] as List) { final List<InputV2> inputs = [];
final prevTxid = input["txid"] as String; for (final jsonInput in txData["vin"] as List) {
final prevOut = input["vout"] as int; final map = Map<String, dynamic>.from(jsonInput as Map);
// fetch input tx to get address final List<String> addresses = [];
final inputTx = await cachedElectrumXClient.getTransaction( String valueStringSats = "0";
txHash: prevTxid, OutpointV2? outpoint;
coin: coin,
);
for (final output in inputTx["vout"] as List) { final coinbase = map["coinbase"] as String?;
// check matching output
if (prevOut == output["n"]) {
// get value
final value = Amount.fromDecimal(
Decimal.parse(output["value"].toString()),
fractionDigits: coin.decimals,
);
// add value to total if (coinbase == null) {
totalInputValue = totalInputValue + value; final txid = map["txid"] as String;
final vout = map["vout"] as int;
// get input(prevOut) address final inputTx = await cachedElectrumXClient.getTransaction(
final address = txHash: txid,
output["scriptPubKey"]?["addresses"]?[0] as String? ?? coin: coin,
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
inputAddresses.add(address);
// if input was from my wallet, add value to amount sent
if (receivingAddresses.contains(address) ||
changeAddresses.contains(address)) {
amountSentFromWallet = amountSentFromWallet + value;
}
}
}
}
}
// parse outputs
for (final output in txData["vout"] as List) {
// get value
final value = Amount.fromDecimal(
Decimal.parse(output["value"].toString()),
fractionDigits: coin.decimals,
);
// add value to total
totalOutputValue += value;
// get output address
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
outputAddresses.add(address);
// if output was to my wallet, add value to amount received
if (receivingAddresses.contains(address)) {
amountReceivedInWallet += value;
} else if (changeAddresses.contains(address)) {
changeAmount += value;
}
}
}
final mySentFromAddresses = [
...receivingAddresses.intersection(inputAddresses),
...changeAddresses.intersection(inputAddresses)
];
final myReceivedOnAddresses =
receivingAddresses.intersection(outputAddresses);
final myChangeReceivedOnAddresses =
changeAddresses.intersection(outputAddresses);
final fee = totalInputValue - totalOutputValue;
// this is the address initially used to fetch the txid
isar_models.Address transactionAddress =
txData["address"] as isar_models.Address;
isar_models.TransactionType type;
Amount amount;
if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) {
// tx is sent to self
type = isar_models.TransactionType.sentToSelf;
amount =
amountSentFromWallet - amountReceivedInWallet - fee - changeAmount;
} else if (mySentFromAddresses.isNotEmpty) {
// outgoing tx
type = isar_models.TransactionType.outgoing;
amount = amountSentFromWallet - changeAmount - fee;
final possible =
outputAddresses.difference(myChangeReceivedOnAddresses).first;
if (transactionAddress.value != possible) {
transactionAddress = isar_models.Address(
walletId: walletId,
value: possible,
publicKey: [],
type: isar_models.AddressType.nonWallet,
derivationIndex: -1,
derivationPath: null,
subType: isar_models.AddressSubType.nonWallet,
); );
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
as Map);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: coin.decimals,
walletOwns: false, // doesn't matter here as this is not saved
);
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: txid,
vout: vout,
);
valueStringSats = prevOut.valueStringSats;
addresses.addAll(prevOut.addresses);
} }
} else {
// incoming tx
type = isar_models.TransactionType.incoming;
amount = amountReceivedInWallet;
}
List<isar_models.Input> inputs = []; InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
List<isar_models.Output> outputs = []; scriptSigHex: map["scriptSig"]?["hex"] as String?,
sequence: map["sequence"] as int?,
for (final json in txData["vin"] as List) { outpoint: outpoint,
bool isCoinBase = json['coinbase'] != null; valueStringSats: valueStringSats,
final input = isar_models.Input( addresses: addresses,
txid: json['txid'] as String, witness: map["witness"] as String?,
vout: json['vout'] as int? ?? -1, coinbase: coinbase,
scriptSig: json['scriptSig']?['hex'] as String?, innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?,
scriptSigAsm: json['scriptSig']?['asm'] as String?, // don't know yet if wallet owns. Need addresses first
isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?, walletOwns: false,
sequence: json['sequence'] as int?,
innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?,
); );
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
wasSentFromThisWallet = true;
input = input.copyWith(walletOwns: true);
}
inputs.add(input); inputs.add(input);
} }
for (final json in txData["vout"] as List) { // parse outputs
final output = isar_models.Output( final List<OutputV2> outputs = [];
scriptPubKey: json['scriptPubKey']?['hex'] as String?, for (final outputJson in txData["vout"] as List) {
scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?, OutputV2 output = OutputV2.fromElectrumXJson(
scriptPubKeyType: json['scriptPubKey']?['type'] as String?, Map<String, dynamic>.from(outputJson as Map),
scriptPubKeyAddress: decimalPlaces: coin.decimals,
json["scriptPubKey"]?["addresses"]?[0] as String? ?? // don't know yet if wallet owns. Need addresses first
json['scriptPubKey']['type'] as String, walletOwns: false,
value: Amount.fromDecimal(
Decimal.parse(json["value"].toString()),
fractionDigits: coin.decimals,
).raw.toInt(),
); );
// if output was to my wallet, add value to amount received
if (receivingAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true;
amountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true);
} else if (changeAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true;
changeAmountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true);
}
outputs.add(output); outputs.add(output);
} }
final tx = isar_models.Transaction( final totalOut = outputs
.map((e) => e.value)
.fold(BigInt.zero, (value, element) => value + element);
isar_models.TransactionType type;
isar_models.TransactionSubType subType =
isar_models.TransactionSubType.none;
// at least one input was owned by this wallet
if (wasSentFromThisWallet) {
type = isar_models.TransactionType.outgoing;
if (wasReceivedInThisWallet) {
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
totalOut) {
// definitely sent all to self
type = isar_models.TransactionType.sentToSelf;
} else if (amountReceivedInThisWallet == BigInt.zero) {
// most likely just a typical send
// do nothing here yet
}
// check vout 0 for special scripts
if (outputs.isNotEmpty) {
final output = outputs.first;
// check for fusion
if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) {
subType = isar_models.TransactionSubType.cashFusion;
} else {
// check other cases here such as SLP or cash tokens etc
}
}
}
} else if (wasReceivedInThisWallet) {
// only found outputs owned by this wallet
type = isar_models.TransactionType.incoming;
} else {
Logging.instance.log(
"Unexpected tx found (ignoring it): $txData",
level: LogLevel.Error,
);
continue;
}
final tx = TransactionV2(
walletId: walletId, walletId: walletId,
blockHash: txData["blockhash"] as String?,
hash: txData["hash"] as String,
txid: txData["txid"] as String, txid: txData["txid"] as String,
timestamp: txData["blocktime"] as int? ??
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
type: type,
subType: isar_models.TransactionSubType.none,
amount: amount.raw.toInt(),
amountString: amount.toJsonString(),
fee: fee.raw.toInt(),
height: txData["height"] as int?, height: txData["height"] as int?,
isCancelled: false, version: txData["version"] as int,
isLelantus: false, timestamp: txData["blocktime"] as int? ??
slateId: null, DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
otherData: null, inputs: List.unmodifiable(inputs),
nonce: null, outputs: List.unmodifiable(outputs),
inputs: inputs, type: type,
outputs: outputs, subType: subType,
numberOfMessages: null,
); );
txns.add(Tuple2(tx, transactionAddress)); txns.add(tx);
} }
await db.addNewTransactionData(txns, walletId); await db.updateOrPutTransactionV2s(txns);
// quick hack to notify manager to call notifyListeners if // quick hack to notify manager to call notifyListeners if
// transactions changed // transactions changed

View file

@ -22,6 +22,33 @@ import 'package:stackwallet/utilities/stack_file_system.dart';
const String kReservedFusionAddress = "reserved_fusion_address"; const String kReservedFusionAddress = "reserved_fusion_address";
final kFusionServerInfoDefaults = Map<Coin, FusionInfo>.unmodifiable(const {
Coin.bitcoincash: FusionInfo(
host: "fusion.servo.cash",
port: 8789,
ssl: true,
// host: "cashfusion.stackwallet.com",
// port: 8787,
// ssl: false,
rounds: 0, // 0 is continuous
),
Coin.bitcoincashTestnet: FusionInfo(
host: "fusion.servo.cash",
port: 8789,
ssl: true,
// host: "cashfusion.stackwallet.com",
// port: 8787,
// ssl: false,
rounds: 0, // 0 is continuous
),
Coin.eCash: FusionInfo(
host: "fusion.tokamak.cash",
port: 8788,
ssl: true,
rounds: 0, // 0 is continuous
),
});
class FusionInfo { class FusionInfo {
final String host; final String host;
final int port; final int port;
@ -37,16 +64,6 @@ class FusionInfo {
required this.rounds, required this.rounds,
}) : assert(rounds >= 0); }) : assert(rounds >= 0);
static const DEFAULTS = FusionInfo(
host: "fusion.servo.cash",
port: 8789,
ssl: true,
// host: "cashfusion.stackwallet.com",
// port: 8787,
// ssl: false,
rounds: 0, // 0 is continuous
);
factory FusionInfo.fromJsonString(String jsonString) { factory FusionInfo.fromJsonString(String jsonString) {
final json = jsonDecode(jsonString); final json = jsonDecode(jsonString);
return FusionInfo( return FusionInfo(
@ -95,7 +112,7 @@ class FusionInfo {
} }
} }
/// A mixin for the BitcoinCashWallet class that adds CashFusion functionality. /// A mixin that adds CashFusion functionality.
mixin FusionWalletInterface { mixin FusionWalletInterface {
// Passed in wallet data. // Passed in wallet data.
late final String _walletId; late final String _walletId;
@ -630,14 +647,25 @@ mixin FusionWalletInterface {
// Loop through UTXOs, checking and adding valid ones. // Loop through UTXOs, checking and adding valid ones.
for (final utxo in walletUtxos) { for (final utxo in walletUtxos) {
final String addressString = utxo.address!; final String addressString = utxo.address!;
final List<String> possibleAddresses = [addressString]; final Set<String> possibleAddresses = {};
if (bitbox.Address.detectFormat(addressString) == if (bitbox.Address.detectFormat(addressString) ==
bitbox.Address.formatCashAddr) { bitbox.Address.formatCashAddr) {
possibleAddresses possibleAddresses.add(addressString);
.add(bitbox.Address.toLegacyAddress(addressString)); possibleAddresses.add(
bitbox.Address.toLegacyAddress(addressString),
);
} else { } else {
possibleAddresses.add(bitbox.Address.toCashAddress(addressString)); possibleAddresses.add(addressString);
if (_coin == Coin.eCash) {
possibleAddresses.add(
bitbox.Address.toECashAddress(addressString),
);
} else {
possibleAddresses.add(
bitbox.Address.toCashAddress(addressString),
);
}
} }
// Fetch address to get pubkey // Fetch address to get pubkey
@ -645,13 +673,13 @@ mixin FusionWalletInterface {
.getAddresses(_walletId) .getAddresses(_walletId)
.filter() .filter()
.anyOf<String, .anyOf<String,
QueryBuilder<Address, Address, QAfterFilterCondition>>( QueryBuilder<Address, Address, QAfterFilterCondition>>(
possibleAddresses, (q, e) => q.valueEqualTo(e)) possibleAddresses, (q, e) => q.valueEqualTo(e))
.and() .and()
.group((q) => q .group((q) => q
.subTypeEqualTo(AddressSubType.change) .subTypeEqualTo(AddressSubType.change)
.or() .or()
.subTypeEqualTo(AddressSubType.receiving)) .subTypeEqualTo(AddressSubType.receiving))
.and() .and()
.typeEqualTo(AddressType.p2pkh) .typeEqualTo(AddressType.p2pkh)
.findFirst(); .findFirst();
@ -681,6 +709,10 @@ mixin FusionWalletInterface {
// Fuse UTXOs. // Fuse UTXOs.
try { try {
if (coinList.isEmpty) {
throw Exception("Started with no coins");
}
await _mainFusionObject!.fuse( await _mainFusionObject!.fuse(
inputsFromWallet: coinList, inputsFromWallet: coinList,
network: _coin.isTestNet network: _coin.isTestNet
@ -710,6 +742,16 @@ mixin FusionWalletInterface {
// Do the same for the UI state. // Do the same for the UI state.
_uiState?.incrementFusionRoundsFailed(); _uiState?.incrementFusionRoundsFailed();
// If we have no coins, stop trying.
if (coinList.isEmpty ||
e.toString().contains("Started with no coins")) {
_updateStatus(
status: fusion.FusionStatus.failed,
info: "Started with no coins, stopping.");
_stopRequested = true;
_uiState?.setFailed(true, shouldNotify: true);
}
// If we fail too many times in a row, stop trying. // If we fail too many times in a row, stop trying.
if (_failedFuseCount >= maxFailedFuseCount) { if (_failedFuseCount >= maxFailedFuseCount) {
_updateStatus( _updateStatus(

View file

@ -8,6 +8,8 @@
* *
*/ */
import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
@ -936,32 +938,74 @@ class Prefs extends ChangeNotifier {
// fusion server info // fusion server info
FusionInfo _fusionServerInfo = FusionInfo.DEFAULTS; Map<Coin, FusionInfo> _fusionServerInfo = {};
FusionInfo get fusionServerInfo => _fusionServerInfo; FusionInfo getFusionServerInfo(Coin coin) {
return _fusionServerInfo[coin] ?? kFusionServerInfoDefaults[coin]!;
}
void setFusionServerInfo(Coin coin, FusionInfo fusionServerInfo) {
if (_fusionServerInfo[coin] != fusionServerInfo) {
_fusionServerInfo[coin] = fusionServerInfo;
set fusionServerInfo(FusionInfo fusionServerInfo) {
if (this.fusionServerInfo != fusionServerInfo) {
DB.instance.put<dynamic>( DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs, boxName: DB.boxNamePrefs,
key: "fusionServerInfo", key: "fusionServerInfoMap",
value: fusionServerInfo.toJsonString(), value: _fusionServerInfo.map(
(key, value) => MapEntry(
key.name,
value.toJsonString(),
),
),
); );
_fusionServerInfo = fusionServerInfo;
notifyListeners(); notifyListeners();
} }
} }
Future<FusionInfo> _getFusionServerInfo() async { Future<Map<Coin, FusionInfo>> _getFusionServerInfo() async {
final saved = await DB.instance.get<dynamic>( final map = await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs, boxName: DB.boxNamePrefs,
key: "fusionServerInfo", key: "fusionServerInfoMap",
) as String?; ) as Map?;
try { if (map == null) {
return FusionInfo.fromJsonString(saved!); return _fusionServerInfo;
} catch (_) {
return FusionInfo.DEFAULTS;
} }
final actualMap = Map<String, String>.from(map).map(
(key, value) => MapEntry(
coinFromPrettyName(key),
FusionInfo.fromJsonString(value),
),
);
// legacy bch check
if (actualMap[Coin.bitcoincash] == null ||
actualMap[Coin.bitcoincashTestnet] == null) {
final saved = await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs,
key: "fusionServerInfo",
) as String?;
if (saved != null) {
final bchInfo = FusionInfo.fromJsonString(saved);
actualMap[Coin.bitcoincash] = bchInfo;
actualMap[Coin.bitcoincashTestnet] = bchInfo;
unawaited(
DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "fusionServerInfoMap",
value: actualMap.map(
(key, value) => MapEntry(
key.name,
value.toJsonString(),
),
),
),
);
}
}
return actualMap;
} }
} }