From 1b0d918a6e1f3116990d493ed71cd6876d43caab Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Nov 2023 09:43:40 -0600 Subject: [PATCH 01/11] tx v2 sent amount calc fix --- .../isar/models/blockchain_data/v2/transaction_v2.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index c39c0cf11..eaa548453 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -84,7 +84,14 @@ class TransactionV2 { .where((e) => e.walletOwns) .fold(BigInt.zero, (p, e) => p + e.value); - return Amount(rawValue: inSum, fractionDigits: coin.decimals); + return Amount( + rawValue: inSum, + fractionDigits: coin.decimals, + ) - + getAmountReceivedThisWallet( + coin: coin, + ) - + getFee(coin: coin); } Set associatedAddresses() => { From c51ccd33acacbff8defc233f715f842be5b3e5ec Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 10:46:34 -0600 Subject: [PATCH 02/11] eth token api endpoint update --- lib/services/ethereum/ethereum_api.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 8f0024774..b9a118352 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -611,7 +611,8 @@ abstract class EthereumAPI { try { final response = await client.get( url: Uri.parse( - "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", + // "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", + "$stackBaseServer/names?terms=$contractAddress", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() @@ -621,6 +622,10 @@ abstract class EthereumAPI { if (response.code == 200) { final json = jsonDecode(response.body) as Map; if (json["data"] is List) { + if ((json["data"] as List).isEmpty) { + throw EthApiException("Unknown token"); + } + final map = Map.from(json["data"].first as Map); EthContract? token; if (map["isErc20"] == true) { From 747565fa16610b79b2b667cd0b53bbe49cb001d6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 10:48:09 -0600 Subject: [PATCH 03/11] firo used serials cached electrumx fix --- lib/electrumx_rpc/cached_electrumx.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index a8136dcaf..3bce4a910 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -9,6 +9,7 @@ */ import 'dart:convert'; +import 'dart:math'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -167,8 +168,10 @@ class CachedElectrumX { Set cachedSerials = _list == null ? {} : List.from(_list).toSet(); - final startNumber = - cachedSerials.length - 10; // 10 being some arbitrary buffer + startNumber = max( + max(0, startNumber), + cachedSerials.length - 100, // 100 being some arbitrary buffer + ); final serials = await electrumXClient.getUsedCoinSerials( startNumber: startNumber, From 9a9c9550eefd738d38b8f4263079d1d1fd400a33 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 14:50:38 -0600 Subject: [PATCH 04/11] 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 --- lib/pages/cashfusion/cashfusion_view.dart | 25 +- .../cashfusion/fusion_progress_view.dart | 16 +- lib/pages/wallet_view/wallet_view.dart | 7 +- .../cashfusion/desktop_cashfusion_view.dart | 26 +- .../cashfusion/sub_widgets/fusion_dialog.dart | 8 +- .../wallet_view/desktop_wallet_view.dart | 6 +- .../more_features/more_features_dialog.dart | 4 +- lib/services/coins/ecash/ecash_wallet.dart | 412 ++++++++++-------- .../mixins/fusion_wallet_interface.dart | 82 +++- lib/utilities/prefs.dart | 74 +++- 10 files changed, 426 insertions(+), 234 deletions(-) diff --git a/lib/pages/cashfusion/cashfusion_view.dart b/lib/pages/cashfusion/cashfusion_view.dart index 50407a172..e57fe77f3 100644 --- a/lib/pages/cashfusion/cashfusion_view.dart +++ b/lib/pages/cashfusion/cashfusion_view.dart @@ -61,10 +61,11 @@ class _CashFusionViewState extends ConsumerState { FusionOption _option = FusionOption.continuous; Future _startFusion() async { - final fusionWallet = ref + final wallet = ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) - .wallet as FusionWalletInterface; + .wallet; + final fusionWallet = wallet as FusionWalletInterface; try { fusionWallet.uiState = ref.read( @@ -89,7 +90,9 @@ class _CashFusionViewState extends ConsumerState { ); // update user prefs (persistent) - ref.read(prefsChangeNotifierProvider).fusionServerInfo = newInfo; + ref + .read(prefsChangeNotifierProvider) + .setFusionServerInfo(wallet.coin, newInfo); unawaited( fusionWallet.fuse( @@ -113,7 +116,11 @@ class _CashFusionViewState extends ConsumerState { portFocusNode = 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; portController.text = info.port.toString(); _enableSSLCheckbox = info.ssl; @@ -150,7 +157,7 @@ class _CashFusionViewState extends ConsumerState { automaticallyImplyLeading: false, leading: const AppBarBackButton(), title: Text( - "CashFusion", + "Fusion", style: STextStyles.navBarTitle(context), ), titleSpacing: 0, @@ -189,7 +196,7 @@ class _CashFusionViewState extends ConsumerState { children: [ RoundedWhiteContainer( child: Text( - "CashFusion allows you to anonymize your BCH coins.", + "Fusion helps anonymize your coins by mixing them.", style: STextStyles.w500_12(context).copyWith( color: Theme.of(context) .extension()! @@ -214,7 +221,11 @@ class _CashFusionViewState extends ConsumerState { CustomTextButton( text: "Default", onTap: () { - const def = FusionInfo.DEFAULTS; + final def = kFusionServerInfoDefaults[ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet + .coin]!; serverController.text = def.host; portController.text = def.port.toString(); fusionRoundController.text = diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 96ef5eda9..d8d975805 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/fusion_wallet_interface.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/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -43,6 +44,8 @@ class FusionProgressView extends ConsumerStatefulWidget { } class _FusionProgressViewState extends ConsumerState { + late final Coin coin; + Future _requestAndProcessCancel() async { final shouldCancel = await showDialog( context: context, @@ -88,6 +91,16 @@ class _FusionProgressViewState extends ConsumerState { } } + @override + void initState() { + coin = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet + .coin; + super.initState(); + } + @override Widget build(BuildContext context) { final bool _succeeded = @@ -230,7 +243,8 @@ class _FusionProgressViewState extends ConsumerState { .getManager(widget.walletId) .wallet as FusionWalletInterface; - final fusionInfo = ref.read(prefsChangeNotifierProvider).fusionServerInfo; + final fusionInfo = + ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin); try { fusionWallet.uiState = ref.read( diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 34456cebd..d4201d921 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -845,7 +845,8 @@ class _WalletViewState extends ConsumerState { onTap: () { Navigator.of(context).pushNamed( coin == Coin.bitcoincash || - coin == Coin.bitcoincashTestnet + coin == Coin.bitcoincashTestnet || + coin == Coin.eCash ? AllTransactionsV2View.routeName : AllTransactionsView.routeName, arguments: walletId, @@ -902,7 +903,9 @@ class _WalletViewState extends ConsumerState { children: [ Expanded( child: coin == Coin.bitcoincash || - coin == Coin.bitcoincashTestnet + coin == + Coin.bitcoincashTestnet || + coin == Coin.eCash ? TransactionsV2List( walletId: widget.walletId, ) diff --git a/lib/pages_desktop_specific/cashfusion/desktop_cashfusion_view.dart b/lib/pages_desktop_specific/cashfusion/desktop_cashfusion_view.dart index 7759d0885..9d4f8dec0 100644 --- a/lib/pages_desktop_specific/cashfusion/desktop_cashfusion_view.dart +++ b/lib/pages_desktop_specific/cashfusion/desktop_cashfusion_view.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.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/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -58,6 +59,7 @@ class _DesktopCashFusion extends ConsumerState { late final FocusNode portFocusNode; late final TextEditingController fusionRoundController; late final FocusNode fusionRoundFocusNode; + late final Coin coin; bool _enableStartButton = false; bool _enableSSLCheckbox = false; @@ -93,7 +95,7 @@ class _DesktopCashFusion extends ConsumerState { ); // update user prefs (persistent) - ref.read(prefsChangeNotifierProvider).fusionServerInfo = newInfo; + ref.read(prefsChangeNotifierProvider).setFusionServerInfo(coin, newInfo); unawaited( fusionWallet.fuse( @@ -121,8 +123,14 @@ class _DesktopCashFusion extends ConsumerState { serverFocusNode = FocusNode(); portFocusNode = 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; portController.text = info.port.toString(); _enableSSLCheckbox = info.ssl; @@ -197,7 +205,7 @@ class _DesktopCashFusion extends ConsumerState { width: 12, ), Text( - "CashFusion", + "Fusion", style: STextStyles.desktopH3(context), ), ], @@ -219,7 +227,7 @@ class _DesktopCashFusion extends ConsumerState { ), RichText( text: TextSpan( - text: "What is CashFusion?", + text: "What is Fusion?", style: STextStyles.richLink(context).copyWith( fontSize: 16, ), @@ -248,7 +256,7 @@ class _DesktopCashFusion extends ConsumerState { .spaceBetween, children: [ Text( - "What is CashFusion?", + "What is Fusion?", style: STextStyles.desktopH2( context), ), @@ -308,7 +316,7 @@ class _DesktopCashFusion extends ConsumerState { child: Row( children: [ Text( - "CashFusion allows you to anonymize your BCH coins.", + "Fusion helps anonymize your coins by mixing them.", style: STextStyles.desktopTextExtraExtraSmall(context), ), @@ -336,7 +344,11 @@ class _DesktopCashFusion extends ConsumerState { CustomTextButton( text: "Default", onTap: () { - const def = FusionInfo.DEFAULTS; + final def = kFusionServerInfoDefaults[ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet + .coin]!; serverController.text = def.host; portController.text = def.port.toString(); fusionRoundController.text = diff --git a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart index ca9067129..964584be4 100644 --- a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart +++ b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart @@ -283,12 +283,14 @@ class _FusionDialogViewState extends ConsumerState { /// Fuse again. void _fuseAgain() async { - final fusionWallet = ref + final wallet = ref .read(walletsChangeNotifierProvider) .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 { fusionWallet.uiState = ref.read( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 466a900ef..cd856ebe5 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -482,7 +482,8 @@ class _DesktopWalletViewState extends ConsumerState { } else { await Navigator.of(context).pushNamed( coin == Coin.bitcoincash || - coin == Coin.bitcoincashTestnet + coin == Coin.bitcoincashTestnet || + coin == Coin.eCash ? AllTransactionsV2View.routeName : AllTransactionsView.routeName, arguments: widget.walletId, @@ -520,7 +521,8 @@ class _DesktopWalletViewState extends ConsumerState { walletId: widget.walletId, ) : coin == Coin.bitcoincash || - coin == Coin.bitcoincashTestnet + coin == Coin.bitcoincashTestnet || + coin == Coin.eCash ? TransactionsV2List( walletId: widget.walletId, ) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 2c32d3ebc..de4fa8b28 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -125,8 +125,8 @@ class _MoreFeaturesDialogState extends ConsumerState { ), if (manager.hasFusionSupport) _MoreFeaturesItem( - label: "CashFusion", - detail: "Decentralized Bitcoin Cash mixing protocol", + label: "Fusion", + detail: "Decentralized mixing protocol", iconAsset: Assets.svg.cashFusion, onPressed: () => widget.onFusionPressed?.call(), ), diff --git a/lib/services/coins/ecash/ecash_wallet.dart b/lib/services/coins/ecash/ecash_wallet.dart index c35f7f4c6..ad94035c2 100644 --- a/lib/services/coins/ecash/ecash_wallet.dart +++ b/lib/services/coins/ecash/ecash_wallet.dart @@ -26,9 +26,13 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.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/paymint/fee_object_model.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/event_bus/events/global/node_connection_status_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/mixins/coin_control_interface.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_db.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/derive_path_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/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -130,7 +136,12 @@ String constructDerivePath({ } class ECashWallet extends CoinServiceAPI - with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface + with + WalletCache, + WalletDB, + ElectrumXParsing, + CoinControlInterface, + FusionWalletInterface implements XPubAble { ECashWallet({ required String walletId, @@ -162,6 +173,19 @@ class ECashWallet extends CoinServiceAPI 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 = @@ -185,6 +209,81 @@ class ECashWallet extends CoinServiceAPI } } + Future> _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 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 _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 set isFavorite(bool markFavorite) { _isFavorite = markFavorite; @@ -1160,6 +1259,8 @@ class ECashWallet extends CoinServiceAPI } }).toSet(); + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + final List> allTxHashes = await _fetchHistory([...receivingAddresses, ...changeAddresses]); @@ -1194,207 +1295,168 @@ class ECashWallet extends CoinServiceAPI } } - final List> txns = []; + final List txns = []; for (final txData in allTransactions) { - Set inputAddresses = {}; - Set outputAddresses = {}; + // set to true if any inputs were detected as owned by this wallet + bool wasSentFromThisWallet = false; - Logging.instance.log(txData, level: LogLevel.Fatal); - - Amount totalInputValue = Amount( - rawValue: BigInt.from(0), - 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, - ); + // set to true if any outputs were detected as owned by this wallet + bool wasReceivedInThisWallet = false; + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; // parse inputs - for (final input in txData["vin"] as List) { - final prevTxid = input["txid"] as String; - final prevOut = input["vout"] as int; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); - // fetch input tx to get address - final inputTx = await cachedElectrumXClient.getTransaction( - txHash: prevTxid, - coin: coin, - ); + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; - for (final output in inputTx["vout"] as List) { - // check matching output - if (prevOut == output["n"]) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: coin.decimals, - ); + final coinbase = map["coinbase"] as String?; - // add value to total - totalInputValue = totalInputValue + value; + if (coinbase == null) { + final txid = map["txid"] as String; + final vout = map["vout"] as int; - // get input(prevOut) address - final address = - output["scriptPubKey"]?["addresses"]?[0] as String? ?? - 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 inputTx = await cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, ); + + final prevOutJson = Map.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 inputs = []; - List outputs = []; - - for (final json in txData["vin"] as List) { - bool isCoinBase = json['coinbase'] != null; - final input = isar_models.Input( - txid: json['txid'] as String, - vout: json['vout'] as int? ?? -1, - scriptSig: json['scriptSig']?['hex'] as String?, - scriptSigAsm: json['scriptSig']?['asm'] as String?, - isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?, - sequence: json['sequence'] as int?, - innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?, + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // don't know yet if wallet owns. Need addresses first + walletOwns: false, ); + + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + inputs.add(input); } - for (final json in txData["vout"] as List) { - final output = isar_models.Output( - scriptPubKey: json['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: json['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: - json["scriptPubKey"]?["addresses"]?[0] as String? ?? - json['scriptPubKey']['type'] as String, - value: Amount.fromDecimal( - Decimal.parse(json["value"].toString()), - fractionDigits: coin.decimals, - ).raw.toInt(), + // parse outputs + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: coin.decimals, + // don't know yet if wallet owns. Need addresses first + walletOwns: false, ); + + // if output was to my wallet, add value to amount received + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + outputs.add(output); } - final 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, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] 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?, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: inputs, - outputs: outputs, - numberOfMessages: null, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, ); - 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 // transactions changed diff --git a/lib/services/mixins/fusion_wallet_interface.dart b/lib/services/mixins/fusion_wallet_interface.dart index dc26b4618..259b0b418 100644 --- a/lib/services/mixins/fusion_wallet_interface.dart +++ b/lib/services/mixins/fusion_wallet_interface.dart @@ -22,6 +22,33 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; const String kReservedFusionAddress = "reserved_fusion_address"; +final kFusionServerInfoDefaults = Map.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 { final String host; final int port; @@ -37,16 +64,6 @@ class FusionInfo { required this.rounds, }) : 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) { final json = jsonDecode(jsonString); 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 { // Passed in wallet data. late final String _walletId; @@ -630,14 +647,25 @@ mixin FusionWalletInterface { // Loop through UTXOs, checking and adding valid ones. for (final utxo in walletUtxos) { final String addressString = utxo.address!; - final List possibleAddresses = [addressString]; + final Set possibleAddresses = {}; if (bitbox.Address.detectFormat(addressString) == bitbox.Address.formatCashAddr) { - possibleAddresses - .add(bitbox.Address.toLegacyAddress(addressString)); + possibleAddresses.add(addressString); + possibleAddresses.add( + bitbox.Address.toLegacyAddress(addressString), + ); } 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 @@ -645,13 +673,13 @@ mixin FusionWalletInterface { .getAddresses(_walletId) .filter() .anyOf>( - possibleAddresses, (q, e) => q.valueEqualTo(e)) + QueryBuilder>( + possibleAddresses, (q, e) => q.valueEqualTo(e)) .and() .group((q) => q - .subTypeEqualTo(AddressSubType.change) - .or() - .subTypeEqualTo(AddressSubType.receiving)) + .subTypeEqualTo(AddressSubType.change) + .or() + .subTypeEqualTo(AddressSubType.receiving)) .and() .typeEqualTo(AddressType.p2pkh) .findFirst(); @@ -681,6 +709,10 @@ mixin FusionWalletInterface { // Fuse UTXOs. try { + if (coinList.isEmpty) { + throw Exception("Started with no coins"); + } + await _mainFusionObject!.fuse( inputsFromWallet: coinList, network: _coin.isTestNet @@ -710,6 +742,16 @@ mixin FusionWalletInterface { // Do the same for the UI state. _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 (_failedFuseCount >= maxFailedFuseCount) { _updateStatus( diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 6fe9689ab..eab2d3b73 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:stackwallet/db/hive/db.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 - FusionInfo _fusionServerInfo = FusionInfo.DEFAULTS; + Map _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( boxName: DB.boxNamePrefs, - key: "fusionServerInfo", - value: fusionServerInfo.toJsonString(), + key: "fusionServerInfoMap", + value: _fusionServerInfo.map( + (key, value) => MapEntry( + key.name, + value.toJsonString(), + ), + ), ); - _fusionServerInfo = fusionServerInfo; notifyListeners(); } } - Future _getFusionServerInfo() async { - final saved = await DB.instance.get( + Future> _getFusionServerInfo() async { + final map = await DB.instance.get( boxName: DB.boxNamePrefs, - key: "fusionServerInfo", - ) as String?; + key: "fusionServerInfoMap", + ) as Map?; - try { - return FusionInfo.fromJsonString(saved!); - } catch (_) { - return FusionInfo.DEFAULTS; + if (map == null) { + return _fusionServerInfo; } + + final actualMap = Map.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( + 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( + boxName: DB.boxNamePrefs, + key: "fusionServerInfoMap", + value: actualMap.map( + (key, value) => MapEntry( + key.name, + value.toJsonString(), + ), + ), + ), + ); + } + } + + return actualMap; } } From 01f2cdd1179f4eb7e2efe85f8c7858d692c1531c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 14:54:55 -0600 Subject: [PATCH 05/11] pull over tezos support files from the future --- .../coins/tezos/api/tezos_account.dart | 58 +++++++++ lib/services/coins/tezos/api/tezos_api.dart | 117 ++++++++++++++++++ .../coins/tezos/api/tezos_rpc_api.dart | 71 +++++++++++ .../coins/tezos/api/tezos_transaction.dart | 43 +++++++ 4 files changed, 289 insertions(+) create mode 100644 lib/services/coins/tezos/api/tezos_account.dart create mode 100644 lib/services/coins/tezos/api/tezos_api.dart create mode 100644 lib/services/coins/tezos/api/tezos_rpc_api.dart create mode 100644 lib/services/coins/tezos/api/tezos_transaction.dart diff --git a/lib/services/coins/tezos/api/tezos_account.dart b/lib/services/coins/tezos/api/tezos_account.dart new file mode 100644 index 000000000..8bf9a9f64 --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_account.dart @@ -0,0 +1,58 @@ +class TezosAccount { + final int id; + final String type; + final String address; + final String? publicKey; + final bool revealed; + final int balance; + final int counter; + + TezosAccount({ + required this.id, + required this.type, + required this.address, + required this.publicKey, + required this.revealed, + required this.balance, + required this.counter, + }); + + TezosAccount copyWith({ + int? id, + String? type, + String? address, + String? publicKey, + bool? revealed, + int? balance, + int? counter, + }) { + return TezosAccount( + id: id ?? this.id, + type: type ?? this.type, + address: address ?? this.address, + publicKey: publicKey ?? this.publicKey, + revealed: revealed ?? this.revealed, + balance: balance ?? this.balance, + counter: counter ?? this.counter, + ); + } + + factory TezosAccount.fromMap(Map map) { + return TezosAccount( + id: map['id'] as int, + type: map['type'] as String, + address: map['address'] as String, + publicKey: map['publicKey'] as String?, + revealed: map['revealed'] as bool, + balance: map['balance'] as int, + counter: map['counter'] as int, + ); + } + + @override + String toString() { + return 'UserData{id: $id, type: $type, address: $address, ' + 'publicKey: $publicKey, revealed: $revealed,' + ' balance: $balance, counter: $counter}'; + } +} diff --git a/lib/services/coins/tezos/api/tezos_api.dart b/lib/services/coins/tezos/api/tezos_api.dart new file mode 100644 index 000000000..ac8399327 --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_api.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; + +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_account.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_transaction.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +abstract final class TezosAPI { + static final HTTP _client = HTTP(); + static const String _baseURL = 'https://api.tzkt.io'; + + static Future getCounter(String address) async { + try { + final uriString = "$_baseURL/v1/accounts/$address/counter"; + final response = await _client.get( + url: Uri.parse(uriString), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final result = jsonDecode(response.body); + return result as int; + } catch (e, s) { + Logging.instance.log( + "Error occurred in TezosAPI while getting counter for $address: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + static Future getAccount(String address, + {String type = "user"}) async { + try { + final uriString = "$_baseURL/v1/accounts/$address?legacy=false"; + final response = await _client.get( + url: Uri.parse(uriString), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final result = jsonDecode(response.body) as Map; + + final account = TezosAccount.fromMap(Map.from(result)); + + return account; + } catch (e, s) { + Logging.instance.log( + "Error occurred in TezosAPI while getting account for $address: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + static Future> getTransactions(String address) async { + try { + final transactionsCall = + "$_baseURL/v1/accounts/$address/operations?type=transaction"; + + final response = await _client.get( + url: Uri.parse(transactionsCall), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final result = jsonDecode(response.body) as List; + + List txs = []; + for (var tx in result) { + if (tx["type"] == "transaction") { + final theTx = TezosTransaction( + id: tx["id"] as int, + hash: tx["hash"] as String, + type: tx["type"] as String, + height: tx["level"] as int, + timestamp: DateTime.parse(tx["timestamp"].toString()) + .toUtc() + .millisecondsSinceEpoch ~/ + 1000, + cycle: tx["cycle"] as int?, + counter: tx["counter"] as int, + opN: tx["op_n"] as int?, + opP: tx["op_p"] as int?, + status: tx["status"] as String, + gasLimit: tx["gasLimit"] as int, + gasUsed: tx["gasUsed"] as int, + storageLimit: tx["storageLimit"] as int?, + amountInMicroTez: tx["amount"] as int, + feeInMicroTez: (tx["bakerFee"] as int? ?? 0) + + (tx["storageFee"] as int? ?? 0) + + (tx["allocationFee"] as int? ?? 0), + burnedAmountInMicroTez: tx["burned"] as int?, + senderAddress: tx["sender"]["address"] as String, + receiverAddress: tx["target"]["address"] as String, + ); + txs.add(theTx); + } + } + return txs; + } catch (e, s) { + Logging.instance.log( + "Error occurred in TezosAPI while getting transactions for $address: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } +} diff --git a/lib/services/coins/tezos/api/tezos_rpc_api.dart b/lib/services/coins/tezos/api/tezos_rpc_api.dart new file mode 100644 index 000000000..d5faa060f --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_rpc_api.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +abstract final class TezosRpcAPI { + static final HTTP _client = HTTP(); + + static Future getBalance({ + required ({String host, int port}) nodeInfo, + required String address, + }) async { + try { + String balanceCall = + "${nodeInfo.host}:${nodeInfo.port}/chains/main/blocks/head/context/contracts/$address/balance"; + + final response = await _client.get( + url: Uri.parse(balanceCall), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final balance = + BigInt.parse(response.body.substring(1, response.body.length - 2)); + return balance; + } catch (e) { + Logging.instance.log( + "Error occurred in tezos_rpc_api.dart while getting balance for $address: $e", + level: LogLevel.Error, + ); + } + return null; + } + + static Future getChainHeight({ + required ({String host, int port}) nodeInfo, + }) async { + try { + final api = + "${nodeInfo.host}:${nodeInfo.port}/chains/main/blocks/head/header/shell"; + + final response = await _client.get( + url: Uri.parse(api), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final jsonParsedResponse = jsonDecode(response.body); + return int.parse(jsonParsedResponse["level"].toString()); + } catch (e) { + Logging.instance.log( + "Error occurred in tezos_rpc_api.dart while getting chain height for tezos: $e", + level: LogLevel.Error, + ); + } + return null; + } + + static Future testNetworkConnection({ + required ({String host, int port}) nodeInfo, + }) async { + final result = await getChainHeight(nodeInfo: nodeInfo); + return result != null; + } +} diff --git a/lib/services/coins/tezos/api/tezos_transaction.dart b/lib/services/coins/tezos/api/tezos_transaction.dart new file mode 100644 index 000000000..3ffcd4adf --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_transaction.dart @@ -0,0 +1,43 @@ +class TezosTransaction { + final int? id; + final String hash; + final String? type; + final int height; + final int timestamp; + final int? cycle; + final int? counter; + final int? opN; + final int? opP; + final String? status; + final bool? isSuccess; + final int? gasLimit; + final int? gasUsed; + final int? storageLimit; + final int amountInMicroTez; + final int feeInMicroTez; + final int? burnedAmountInMicroTez; + final String senderAddress; + final String receiverAddress; + + TezosTransaction({ + this.id, + required this.hash, + this.type, + required this.height, + required this.timestamp, + this.cycle, + this.counter, + this.opN, + this.opP, + this.status, + this.isSuccess, + this.gasLimit, + this.gasUsed, + this.storageLimit, + required this.amountInMicroTez, + required this.feeInMicroTez, + this.burnedAmountInMicroTez, + required this.senderAddress, + required this.receiverAddress, + }); +} \ No newline at end of file From d5cb4dd6b4b4baf4ad702ed24257e2887f8d6b58 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 15:45:33 -0600 Subject: [PATCH 06/11] backport of some tezos fixes --- lib/pages/send_view/send_view.dart | 3 + .../wallet_view/sub_widgets/desktop_send.dart | 9 +- lib/services/coins/tezos/tezos_wallet.dart | 307 ++++++++---------- pubspec.lock | 17 +- pubspec.yaml | 5 +- 5 files changed, 167 insertions(+), 174 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 01131b536..500788989 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -2007,6 +2007,7 @@ class _SendViewState extends ConsumerState { ), if (coin != Coin.epicCash && coin != Coin.nano && + coin != Coin.tezos && coin != Coin.banano) Text( "Transaction fee (estimated)", @@ -2015,12 +2016,14 @@ class _SendViewState extends ConsumerState { ), if (coin != Coin.epicCash && coin != Coin.nano && + coin != Coin.tezos && coin != Coin.banano) const SizedBox( height: 8, ), if (coin != Coin.epicCash && coin != Coin.nano && + coin != Coin.tezos && coin != Coin.banano) Stack( children: [ diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index f9f24cb76..6c50fbd84 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -1459,7 +1459,8 @@ class _DesktopSendState extends ConsumerState { const SizedBox( height: 20, ), - if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin))) + if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos] + .contains(coin))) ConditionalParent( condition: coin.isElectrumXCoin && !(((coin == Coin.firo || coin == Coin.firoTestNet) && @@ -1510,11 +1511,13 @@ class _DesktopSendState extends ConsumerState { textAlign: TextAlign.left, ), ), - if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin))) + if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos] + .contains(coin))) const SizedBox( height: 10, ), - if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin))) + if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos] + .contains(coin))) if (!isCustomFee) Padding( padding: const EdgeInsets.all(10), diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index fa5abc30e..cf346840c 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'dart:convert'; -import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; @@ -10,8 +8,9 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/networking/http.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_api.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_rpc_api.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -19,17 +18,15 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; -import 'package:tezart/tezart.dart'; +import 'package:tezart/tezart.dart' as tezart; import 'package:tuple/tuple.dart'; const int MINIMUM_CONFIRMATIONS = 1; @@ -62,8 +59,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { DefaultNodes.getNodeFor(Coin.tezos); } - Future getKeystore() async { - return Keystore.fromMnemonic((await mnemonicString).toString()); + Future getKeystore() async { + return tezart.Keystore.fromMnemonic((await mnemonicString).toString()); } @override @@ -102,8 +99,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override bool get shouldAutoSync => _shouldAutoSync; - HTTP client = HTTP(); - @override set shouldAutoSync(bool shouldAutoSync) { if (_shouldAutoSync != shouldAutoSync) { @@ -164,69 +159,117 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; + Future _buildSendTransaction({ + required Amount amount, + required String address, + required int counter, + }) async { + try { + final sourceKeyStore = await getKeystore(); + final server = (_xtzNode ?? getCurrentNode()).host; + final tezartClient = tezart.TezartClient( + server, + ); + + final opList = await tezartClient.transferOperation( + source: sourceKeyStore, + destination: address, + amount: amount.raw.toInt(), + ); + + for (final op in opList.operations) { + op.counter = counter; + counter++; + } + + return opList; + } catch (e, s) { + Logging.instance.log( + "Error in _buildSendTransaction() in tezos_wallet.dart: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + @override - Future> prepareSend( - {required String address, - required Amount amount, - Map? args}) async { + Future> prepareSend({ + required String address, + required Amount amount, + Map? args, + }) async { try { if (amount.decimals != coin.decimals) { throw Exception("Amount decimals do not match coin decimals!"); } - var fee = int.parse((await estimateFeeFor( - amount, (args!["feeRate"] as FeeRateType).index)) - .raw - .toString()); + + if (amount > balance.spendable) { + throw Exception("Insufficient available balance"); + } + + final myAddress = await currentReceivingAddress; + final account = await TezosAPI.getAccount( + myAddress, + ); + + final opList = await _buildSendTransaction( + amount: amount, + address: address, + counter: account.counter + 1, + ); + + await opList.computeLimits(); + await opList.computeFees(); + await opList.simulate(); + Map txData = { - "fee": fee, + "fee": Amount( + rawValue: opList.operations + .map( + (e) => BigInt.from(e.fee), + ) + .fold( + BigInt.zero, + (p, e) => p + e, + ), + fractionDigits: coin.decimals, + ).raw.toInt(), "address": address, "recipientAmt": amount, + "tezosOperationsList": opList, }; - return Future.value(txData); - } catch (e) { - return Future.error(e); + return txData; + } catch (e, s) { + Logging.instance.log( + "Error in prepareSend() in tezos_wallet.dart: $e\n$s", + level: LogLevel.Error, + ); + + if (e + .toString() + .contains("(_operationResult['errors']): Must not be null")) { + throw Exception("Probably insufficient balance"); + } else if (e.toString().contains( + "The simulation of the operation: \"transaction\" failed with error(s) :" + " contract.balance_too_low, tez.subtraction_underflow.", + )) { + throw Exception("Insufficient balance to pay fees"); + } + + rethrow; } } @override Future confirmSend({required Map txData}) async { try { - final amount = txData["recipientAmt"] as Amount; - final amountInMicroTez = amount.decimal * Decimal.fromInt(1000000); - final microtezToInt = int.parse(amountInMicroTez.toString()); - - final int feeInMicroTez = int.parse(txData["fee"].toString()); - final String destinationAddress = txData["address"] as String; - final secretKey = - Keystore.fromMnemonic((await mnemonicString)!).secretKey; - - Logging.instance.log(secretKey, level: LogLevel.Info); - final sourceKeyStore = Keystore.fromSecretKey(secretKey); - final client = TezartClient(getCurrentNode().host); - - int? sendAmount = microtezToInt; - int gasLimit = _gasLimit; - int thisFee = feeInMicroTez; - - if (balance.spendable == txData["recipientAmt"] as Amount) { - //Fee guides for emptying a tz account - // https://github.com/TezTech/eztz/blob/master/PROTO_004_FEES.md - thisFee = thisFee + 32; - sendAmount = microtezToInt - thisFee; - gasLimit = _gasLimit + 320; - } - - final operation = await client.transferOperation( - source: sourceKeyStore, - destination: destinationAddress, - amount: sendAmount, - customFee: feeInMicroTez, - customGasLimit: gasLimit); - await operation.executeAndMonitor(); - return operation.result.id as String; - } catch (e) { - Logging.instance.log(e.toString(), level: LogLevel.Error); - return Future.error(e); + final opList = txData["tezosOperationsList"] as tezart.OperationsList; + await opList.inject(); + await opList.monitor(); + return opList.result.id!; + } catch (e, s) { + Logging.instance.log("ConfirmSend: $e\n$s", level: LogLevel.Error); + rethrow; } } @@ -236,24 +279,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { if (mneString == null) { throw Exception("No mnemonic found!"); } - return Future.value((Keystore.fromMnemonic(mneString)).address); + return Future.value((tezart.Keystore.fromMnemonic(mneString)).address); } @override Future estimateFeeFor(Amount amount, int feeRate) async { - var api = "https://api.tzstats.com/series/op?start_date=today&collapse=1d"; - var response = jsonDecode((await client.get( - url: Uri.parse(api), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - )) - .body)[0]; - double totalFees = response[4] as double; - int totalTxs = response[8] as int; - int feePerTx = (totalFees / totalTxs * 1000000).floor(); - return Amount( - rawValue: BigInt.from(feePerTx), + rawValue: BigInt.from(0), fractionDigits: coin.decimals, ); } @@ -266,18 +298,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get fees async { - var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; - var response = jsonDecode((await client.get( - url: Uri.parse(api), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - )) - .body); - double totalFees = response[0][4] as double; - int totalTxs = response[0][8] as int; - int feePerTx = (totalFees / totalTxs * 1000000).floor(); - Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); - // TODO: fix numberOfBlocks - Since there is only one fee no need to set blocks + int feePerTx = 0; return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -314,7 +335,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { await _prefs.init(); - var newKeystore = Keystore.random(); + var newKeystore = tezart.Keystore.random(); await _secureStore.write( key: '${_walletId}_mnemonic', value: newKeystore.mnemonic, @@ -380,7 +401,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { required String mnemonicPassphrase, bool isRescan = false, }) async { - final keystore = Keystore.fromMnemonic( + final keystore = tezart.Keystore.fromMnemonic( mnemonic, password: mnemonicPassphrase, ); @@ -504,18 +525,16 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateBalance() async { try { - String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/" - "${await currentReceivingAddress}/balance"; - var response = jsonDecode(await client - .get( - url: Uri.parse(balanceCall), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((value) => value.body)); - Amount balanceInAmount = Amount( - rawValue: BigInt.parse(response.toString()), - fractionDigits: coin.decimals); + final node = getCurrentNode(); + final bal = await TezosRpcAPI.getBalance( + address: await currentReceivingAddress, + nodeInfo: ( + host: node.host, + port: node.port, + ), + ); + Amount balanceInAmount = + Amount(rawValue: bal ?? BigInt.zero, fractionDigits: coin.decimals); _balance = Balance( total: balanceInAmount, spendable: balanceInAmount, @@ -532,22 +551,14 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateTransactions() async { - String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/" - "${await currentReceivingAddress}/operations"; - var response = jsonDecode(await client - .get( - url: Uri.parse(transactionsCall), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((value) => value.body)); + final txns = await TezosAPI.getTransactions(await currentReceivingAddress); List> txs = []; - for (var tx in response as List) { - if (tx["type"] == "transaction") { + for (var tx in txns) { + if (tx.type == "transaction") { TransactionType txType; final String myAddress = await currentReceivingAddress; - final String senderAddress = tx["sender"]["address"] as String; - final String targetAddress = tx["target"]["address"] as String; + final String senderAddress = tx.senderAddress; + final String targetAddress = tx.receiverAddress; if (senderAddress == myAddress && targetAddress == myAddress) { txType = TransactionType.sentToSelf; } else if (senderAddress == myAddress) { @@ -560,21 +571,17 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { var theTx = Transaction( walletId: walletId, - txid: tx["hash"].toString(), - timestamp: DateTime.parse(tx["timestamp"].toString()) - .toUtc() - .millisecondsSinceEpoch ~/ - 1000, + txid: tx.hash, + timestamp: tx.timestamp, type: txType, subType: TransactionSubType.none, - amount: tx["amount"] as int, + amount: tx.amountInMicroTez, amountString: Amount( - rawValue: - BigInt.parse((tx["amount"] as int).toInt().toString()), + rawValue: BigInt.from(tx.amountInMicroTez), fractionDigits: coin.decimals) .toJsonString(), - fee: tx["bakerFee"] as int, - height: int.parse(tx["level"].toString()), + fee: tx.feeInMicroTez, + height: tx.height, isCancelled: false, isLelantus: false, slateId: "", @@ -613,15 +620,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateChainHeight() async { try { - var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; - var jsonParsedResponse = jsonDecode(await client - .get( - url: Uri.parse(api), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((value) => value.body)); - final int intHeight = int.parse(jsonParsedResponse["level"].toString()); + final node = getCurrentNode(); + final int intHeight = (await TezosRpcAPI.getChainHeight( + nodeInfo: ( + host: node.host, + port: node.port, + ), + ))!; Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); await updateCachedChainHeight(intHeight); } catch (e, s) { @@ -700,13 +705,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future testNetworkConnection() async { try { - await client.get( - url: Uri.parse( - "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + final node = getCurrentNode(); + return await TezosRpcAPI.testNetworkConnection( + nodeInfo: ( + host: node.host, + port: node.port, + ), ); - return true; } catch (e) { return false; } @@ -729,37 +734,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future updateSentCachedTxData(Map txData) async { - final transaction = Transaction( - walletId: walletId, - txid: txData["txid"] as String, - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, - type: TransactionType.outgoing, - subType: TransactionSubType.none, - // precision may be lost here hence the following amountString - amount: (txData["recipientAmt"] as Amount).raw.toInt(), - amountString: (txData["recipientAmt"] as Amount).toJsonString(), - fee: txData["fee"] as int, - height: null, - isCancelled: false, - isLelantus: false, - otherData: null, - slateId: null, - nonce: null, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - final address = txData["address"] is String - ? await db.getAddress(walletId, txData["address"] as String) - : null; - - await db.addNewTransactionData( - [ - Tuple2(transaction, address), - ], - walletId, - ); + // do nothing } @override diff --git a/pubspec.lock b/pubspec.lock index d7d431c54..7b1935f23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -142,6 +142,14 @@ packages: url: "https://github.com/cypherstack/bitcoindart.git" source: git version: "3.0.1" + blockchain_signer: + dependency: transitive + description: + name: blockchain_signer + sha256: aa62c62df1fec11dbce7516444715ae492862ebdf3108b8b464a1909827963cd + url: "https://pub.dev" + source: hosted + version: "0.1.0" boolean_selector: dependency: transitive description: @@ -1649,10 +1657,11 @@ packages: tezart: dependency: "direct main" description: - name: tezart - sha256: "35d526f2e6ca250c64461ebfb4fa9f64b6599fab8c4242c8e89ae27d4ac2e15a" - url: "https://pub.dev" - source: hosted + path: "." + ref: main + resolved-ref: "8a7070f533e63dd150edae99476f6853bfb25913" + url: "https://github.com/cypherstack/tezart.git" + source: git version: "2.0.5" time: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index f72c6b9be..4a54602e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -154,7 +154,10 @@ dependencies: url: https://github.com/cypherstack/socks_socket.git ref: master bip340: ^0.2.0 - tezart: ^2.0.5 + tezart: + git: + url: https://github.com/cypherstack/tezart.git + ref: main socks5_proxy: ^1.0.3+dev.3 coinlib_flutter: ^1.0.0 convert: ^3.1.1 From d911ea0e666797300c0bd565ef1085258b0f8aa9 Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 7 Dec 2023 12:17:51 -0600 Subject: [PATCH 07/11] temp fix nano --- lib/services/coins/nano/nano_wallet.dart | 7 ++++--- lib/utilities/default_nodes.dart | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/services/coins/nano/nano_wallet.dart b/lib/services/coins/nano/nano_wallet.dart index bf324a95d..0f647752c 100644 --- a/lib/services/coins/nano/nano_wallet.dart +++ b/lib/services/coins/nano/nano_wallet.dart @@ -850,9 +850,10 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB { int get storedChainHeight => getCachedChainHeight(); NodeModel getCurrentNode() { - return _xnoNode ?? - NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? + return + // _xnoNode ?? + // NodeService(secureStorageInterface: _secureStore) + // .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 210959f65..51696ce5b 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -206,7 +206,8 @@ abstract class DefaultNodes { isDown: false); static NodeModel get nano => NodeModel( - host: "https://rainstorm.city/api", + // host: "https://rainstorm.city/api", + host: "https://app.natrium.io/api", port: 443, name: defaultName, id: _nodeId(Coin.nano), From 1fb62d3e54599a2a6eff92e9570f17fcf8cad8ab Mon Sep 17 00:00:00 2001 From: julian Date: Sun, 10 Dec 2023 12:42:37 -0600 Subject: [PATCH 08/11] send from stack fix --- lib/pages/exchange_view/trade_details_view.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 24208a19c..beab358f9 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -274,8 +274,10 @@ class _TradeDetailsViewState extends ConsumerState { onPressed: () { final coin = coinFromTickerCaseInsensitive(trade.payInCurrency); - final amount = - sendAmount.toAmount(fractionDigits: coin.decimals); + final amount = Amount.fromDecimal( + sendAmount, + fractionDigits: coin.decimals, + ); final address = trade.payInAddress; Navigator.of(context).pushNamed( @@ -1347,11 +1349,13 @@ class _TradeDetailsViewState extends ConsumerState { SecondaryButton( label: "Send from Stack", onPressed: () { - final amount = sendAmount; - final address = trade.payInAddress; - final coin = coinFromTickerCaseInsensitive(trade.payInCurrency); + final amount = Amount.fromDecimal( + sendAmount, + fractionDigits: coin.decimals, + ); + final address = trade.payInAddress; Navigator.of(context).pushNamed( SendFromView.routeName, From 0ce0b1d30ec129816bc07f199186244110563d77 Mon Sep 17 00:00:00 2001 From: julian Date: Sun, 10 Dec 2023 12:58:50 -0600 Subject: [PATCH 09/11] bandaid fix --- .../exchange_currency_selection_view.dart | 2 ++ lib/services/exchange/exchange_data_loading_service.dart | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index 78afc54c0..c2b5acfc0 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -107,6 +107,7 @@ class _ExchangeCurrencySelectionViewState if (widget.pairedTicker == null) { return await _getCurrencies(); } + await ExchangeDataLoadingService.instance.initDB(); List currencies = await ExchangeDataLoadingService .instance.isar.currencies .where() @@ -153,6 +154,7 @@ class _ExchangeCurrencySelectionViewState } Future> _getCurrencies() async { + await ExchangeDataLoadingService.instance.initDB(); final currencies = await ExchangeDataLoadingService.instance.isar.currencies .where() .filter() diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index 6b34fd438..47bbefe79 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -196,6 +196,9 @@ class ExchangeDataLoadingService { } Future _loadChangeNowCurrencies() async { + if (_isar == null) { + await initDB(); + } final exchange = ChangeNowExchange.instance; final responseCurrencies = await exchange.getAllCurrencies(false); if (responseCurrencies.value != null) { @@ -325,6 +328,9 @@ class ExchangeDataLoadingService { // } Future loadMajesticBankCurrencies() async { + if (_isar == null) { + await initDB(); + } final exchange = MajesticBankExchange.instance; final responseCurrencies = await exchange.getAllCurrencies(false); @@ -347,6 +353,9 @@ class ExchangeDataLoadingService { } Future loadTrocadorCurrencies() async { + if (_isar == null) { + await initDB(); + } final exchange = TrocadorExchange.instance; final responseCurrencies = await exchange.getAllCurrencies(false); From 1387a71f4a2560f329022d505560e132e8db6bee Mon Sep 17 00:00:00 2001 From: julian Date: Sun, 10 Dec 2023 13:13:55 -0600 Subject: [PATCH 10/11] take into account tickers and coin names as possible pay in currencies --- .../exchange_view/trade_details_view.dart | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index beab358f9..6f3354b80 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -88,7 +88,10 @@ class _TradeDetailsViewState extends ConsumerState { bool isStackCoin(String ticker) { try { - coinFromTickerCaseInsensitive(ticker); + try { + coinFromTickerCaseInsensitive(ticker); + } catch (_) {} + coinFromPrettyName(ticker); return true; } on ArgumentError catch (_) { return false; @@ -272,8 +275,13 @@ class _TradeDetailsViewState extends ConsumerState { label: "Send from Stack", buttonHeight: ButtonHeight.l, onPressed: () { - final coin = - coinFromTickerCaseInsensitive(trade.payInCurrency); + Coin coin; + try { + coin = coinFromTickerCaseInsensitive( + trade.payInCurrency); + } catch (_) { + coin = coinFromPrettyName(trade.payInCurrency); + } final amount = Amount.fromDecimal( sendAmount, fractionDigits: coin.decimals, @@ -1349,8 +1357,12 @@ class _TradeDetailsViewState extends ConsumerState { SecondaryButton( label: "Send from Stack", onPressed: () { - final coin = - coinFromTickerCaseInsensitive(trade.payInCurrency); + Coin coin; + try { + coin = coinFromTickerCaseInsensitive(trade.payInCurrency); + } catch (_) { + coin = coinFromPrettyName(trade.payInCurrency); + } final amount = Amount.fromDecimal( sendAmount, fractionDigits: coin.decimals, From 8031798892039de6fcc7766a68944817be5c2643 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 11 Dec 2023 13:00:50 -0600 Subject: [PATCH 11/11] exchange fixes --- .../exchange_currency_selection_view.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index 78afc54c0..59a9cd0e7 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -110,7 +111,10 @@ class _ExchangeCurrencySelectionViewState List currencies = await ExchangeDataLoadingService .instance.isar.currencies .where() + .filter() .exchangeNameEqualTo(MajesticBankExchange.exchangeName) + .or() + .exchangeNameStartsWith(TrocadorExchange.exchangeName) .findAll(); final cn = await ChangeNowExchange.instance.getPairedCurrencies( @@ -120,7 +124,7 @@ class _ExchangeCurrencySelectionViewState if (cn.value == null) { if (cn.exception is UnsupportedCurrencyException) { - return currencies; + return _getDistinctCurrenciesFrom(currencies); } if (mounted) { @@ -186,7 +190,8 @@ class _ExchangeCurrencySelectionViewState List _getDistinctCurrenciesFrom(List currencies) { final List distinctCurrencies = []; for (final currency in currencies) { - if (!distinctCurrencies.any((e) => e.ticker == currency.ticker)) { + if (!distinctCurrencies.any( + (e) => e.ticker.toLowerCase() == currency.ticker.toLowerCase())) { distinctCurrencies.add(currency); } }