From 62c1628fa7e80e7f72c113c7840238789b76a9a2 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 20 Nov 2023 13:55:22 -0600 Subject: [PATCH] WIP tezos --- .../new_wallet_recovery_phrase_view.dart | 2 +- .../restore_wallet_view.dart | 2 +- .../sub_widgets/restore_failed_dialog.dart | 3 +- .../verify_recovery_phrase_view.dart | 2 +- .../delete_wallet_recovery_phrase_view.dart | 2 +- .../sub_widgets/delete_wallet_keys_popup.dart | 3 +- lib/services/coins/coin_service.dart | 9 +- lib/services/coins/tezos/tezos_wallet.dart | 1465 ++++++++--------- lib/services/wallets.dart | 13 +- lib/wallets/api/tezos/tezos_api.dart | 2 +- lib/wallets/crypto_currency/coins/tezos.dart | 31 +- lib/wallets/models/tx_data.dart | 8 + lib/wallets/wallet/impl/tezos_wallet.dart | 365 +++- 13 files changed, 1111 insertions(+), 796 deletions(-) diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart index 69420e04d..8a22bc8f2 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart @@ -83,7 +83,7 @@ class _NewWalletRecoveryPhraseViewState await _wallet.exit(); await ref .read(pWallets) - .deleteWallet(_wallet.walletId, ref.read(secureStoreProvider)); + .deleteWallet(_wallet.info, ref.read(secureStoreProvider)); } Future _copy() async { diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 0c122659b..74b1e517d 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -258,7 +258,7 @@ class _RestoreWalletViewState extends ConsumerState { isRestoring = false; await ref.read(pWallets).deleteWallet( - info.walletId, + info, ref.read(secureStoreProvider), ); }, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart index 528aeb42e..ea77f9d33 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class RestoreFailedDialog extends ConsumerStatefulWidget { @@ -65,7 +66,7 @@ class _RestoreFailedDialogState extends ConsumerState { ), onPressed: () async { await ref.read(pWallets).deleteWallet( - walletId, + ref.read(pWalletInfo(walletId)), ref.read(secureStoreProvider), ); diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index f18879297..90163db3f 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -262,7 +262,7 @@ class _VerifyRecoveryPhraseViewState Future delete() async { await ref.read(pWallets).deleteWallet( - _wallet.walletId, + _wallet.info, ref.read(secureStoreProvider), ); } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart index 504a4b501..420002c83 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart @@ -197,7 +197,7 @@ class _DeleteWalletRecoveryPhraseViewState .getPrimaryEnabledButtonStyle(context), onPressed: () async { await ref.read(pWallets).deleteWallet( - widget.walletId, + ref.read(pWalletInfo(widget.walletId)), ref.read(secureStoreProvider), ); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index f5d0e7395..6085e90e2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -238,7 +239,7 @@ class _ConfirmDeleteState extends ConsumerState { label: "Continue", onPressed: () async { await ref.read(pWallets).deleteWallet( - widget.walletId, + ref.read(pWalletInfo(widget.walletId)), ref.read(secureStoreProvider), ); diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 778ba5d2a..175110369 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -21,7 +21,6 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart'; -import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -175,13 +174,7 @@ abstract class CoinServiceAPI { ); case Coin.tezos: - return TezosWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStore: secureStorageInterface, - tracker: tracker, - ); + throw UnimplementedError("moved"); case Coin.wownero: throw UnimplementedError("moved"); diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 743a349fb..1c8345e7f 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,733 +1,732 @@ -import 'dart:async'; - -import 'package:decimal/decimal.dart'; -import 'package:isar/isar.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; -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/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/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -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/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:stackwallet/wallets/api/tezos/tezos_api.dart'; -import 'package:stackwallet/wallets/api/tezos/tezos_rpc_api.dart'; -import 'package:stackwallet/wallets/api/tezos/tezos_transaction.dart'; -import 'package:tezart/tezart.dart'; -import 'package:tuple/tuple.dart'; - -const int MINIMUM_CONFIRMATIONS = 1; -const int _gasLimit = 10200; - -class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { - TezosWallet({ - required String walletId, - required String walletName, - required Coin coin, - required SecureStorageInterface secureStore, - required TransactionNotificationTracker tracker, - MainDB? mockableOverride, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - _coin = coin; - _secureStore = secureStore; - initCache(walletId, coin); - initWalletDB(mockableOverride: mockableOverride); - } - - NodeModel? _xtzNode; - - NodeModel getCurrentNode() { - return _xtzNode ?? - NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: Coin.tezos) ?? - DefaultNodes.getNodeFor(Coin.tezos); - } - - Future getKeystore() async { - return Keystore.fromMnemonic((await mnemonicString).toString()); - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - @override - set walletName(String name) => _walletName = name; - - @override - set isFavorite(bool markFavorite) { - _isFavorite = markFavorite; - updateCachedIsFavorite(markFavorite); - } - - @override - bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); - bool? _isFavorite; - - @override - Coin get coin => _coin; - late Coin _coin; - - late SecureStorageInterface _secureStore; - late final TransactionNotificationTracker txTracker; - final _prefs = Prefs.instance; - - Timer? timer; - bool _shouldAutoSync = false; - Timer? _networkAliveTimer; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - coin, - ), - ); - - _isConnected = hasNetwork; - if (hasNetwork) { - unawaited(refresh()); - } - } - } - - @override - Balance get balance => _balance ??= getCachedBalance(); - Balance? _balance; - - @override - 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()); - Map txData = { - "fee": fee, - "address": address, - "recipientAmt": amount, - }; - return Future.value(txData); - } catch (e) { - return Future.error(e); - } - } - - @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); - } - } - - @override - Future get currentReceivingAddress async { - var mneString = await mnemonicString; - if (mneString == null) { - throw Exception("No mnemonic found!"); - } - return Future.value((Keystore.fromMnemonic(mneString)).address); - } - - @override - Future estimateFeeFor(Amount amount, int feeRate) async { - int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1); - feePerTx ??= 0; - return Amount( - rawValue: BigInt.from(feePerTx), - fractionDigits: coin.decimals, - ); - } - - @override - Future exit() { - _hasCalledExit = true; - return Future.value(); - } - - @override - Future get fees async { - int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1); - feePerTx ??= 0; - Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); - return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: feePerTx, - medium: feePerTx, - slow: feePerTx, - ); - } - - @override - Future generateNewAddress() { - // TODO: implement generateNewAddress - throw UnimplementedError(); - } - - @override - bool get hasCalledExit => _hasCalledExit; - bool _hasCalledExit = false; - - @override - Future initializeExisting() async { - await _prefs.init(); - } - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - - await _prefs.init(); - - var newKeystore = Keystore.random(); - await _secureStore.write( - key: '${_walletId}_mnemonic', - value: newKeystore.mnemonic, - ); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: "", - ); - - final address = Address( - walletId: walletId, - value: newKeystore.address, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.receiving, - ); - - await db.putAddress(address); - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } - - @override - bool get isConnected => _isConnected; - bool _isConnected = false; - - @override - bool get isRefreshing => refreshMutex; - bool refreshMutex = false; - - @override - // TODO: implement maxFee - Future get maxFee => throw UnimplementedError(); - - @override - Future> get mnemonic async { - final mnemonic = await mnemonicString; - final mnemonicPassphrase = await this.mnemonicPassphrase; - if (mnemonic == null) { - throw Exception("No mnemonic found!"); - } - if (mnemonicPassphrase == null) { - throw Exception("No mnemonic passphrase found!"); - } - return mnemonic.split(" "); - } - - @override - Future get mnemonicPassphrase => - _secureStore.read(key: '${_walletId}_mnemonicPassphrase'); - - @override - Future get mnemonicString => - _secureStore.read(key: '${_walletId}_mnemonic'); - - Future _recoverWalletFromSeedPhrase({ - required String mnemonic, - required String mnemonicPassphrase, - bool isRescan = false, - }) async { - final keystore = Keystore.fromMnemonic( - mnemonic, - password: mnemonicPassphrase, - ); - - final address = Address( - walletId: walletId, - value: keystore.address, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.receiving, - ); - - if (isRescan) { - await db.updateOrPutAddresses([address]); - } else { - await db.putAddress(address); - } - } - - bool longMutex = false; - @override - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - try { - Logging.instance.log("Starting full rescan!", level: LogLevel.Info); - longMutex = true; - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - final _mnemonic = await mnemonicString; - final _mnemonicPassphrase = await mnemonicPassphrase; - - await db.deleteWalletBlockchainData(walletId); - - await _recoverWalletFromSeedPhrase( - mnemonic: _mnemonic!, - mnemonicPassphrase: _mnemonicPassphrase!, - isRescan: true, - ); - - await refresh(); - Logging.instance.log("Full rescan complete!", level: LogLevel.Info); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - } catch (e, s) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - - Logging.instance.log( - "Exception rethrown from fullRescan(): $e\n$s", - level: LogLevel.Error, - ); - rethrow; - } finally { - longMutex = false; - } - } - - @override - Future recoverFromMnemonic({ - required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height, - }) async { - longMutex = true; - try { - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - - await _recoverWalletFromSeedPhrase( - mnemonic: mnemonic, - mnemonicPassphrase: mnemonicPassphrase ?? "", - isRescan: false, - ); - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - - await refresh(); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - - rethrow; - } finally { - longMutex = false; - } - } - - Future updateBalance() async { - try { - NodeModel currentNode = getCurrentNode(); - BigInt? balance = await TezosRpcAPI.getBalance( - nodeInfo: (host: currentNode.host, port: currentNode.port), - address: await currentReceivingAddress); - if (balance == null) { - return; - } - Logging.instance.log( - "Balance for ${await currentReceivingAddress}: $balance", - level: LogLevel.Info); - Amount balanceInAmount = - Amount(rawValue: balance, fractionDigits: coin.decimals); - _balance = Balance( - total: balanceInAmount, - spendable: balanceInAmount, - blockedTotal: - Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals), - pendingSpendable: - Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals), - ); - await updateCachedBalance(_balance!); - } catch (e, s) { - Logging.instance.log( - "Error getting balance in tezos_wallet.dart: ${e.toString()}", - level: LogLevel.Error); - } - } - - Future updateTransactions() async { - List? txs = - await TezosAPI.getTransactions(await currentReceivingAddress); - Logging.instance.log("Transactions: $txs", level: LogLevel.Info); - if (txs == null) { - return; - } else if (txs.isEmpty) { - return; - } - List> transactions = []; - for (var theTx in txs) { - var txType = TransactionType.unknown; - var selfAddress = await currentReceivingAddress; - if (selfAddress == theTx.senderAddress) { - txType = TransactionType.outgoing; - } else if (selfAddress == theTx.receiverAddress) { - txType = TransactionType.incoming; - } else if (selfAddress == theTx.receiverAddress && - selfAddress == theTx.senderAddress) { - txType = TransactionType.sentToSelf; - } - var transaction = Transaction( - walletId: walletId, - txid: theTx.hash, - timestamp: theTx.timestamp, - type: txType, - subType: TransactionSubType.none, - amount: theTx.amountInMicroTez, - amountString: Amount( - rawValue: BigInt.parse(theTx.amountInMicroTez.toString()), - fractionDigits: coin.decimals, - ).toJsonString(), - fee: theTx.feeInMicroTez, - height: theTx.height, - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - final AddressSubType subType; - switch (txType) { - case TransactionType.incoming: - case TransactionType.sentToSelf: - subType = AddressSubType.receiving; - break; - case TransactionType.outgoing: - case TransactionType.unknown: - subType = AddressSubType.unknown; - break; - } - final theAddress = Address( - walletId: walletId, - value: theTx.receiverAddress, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: subType, - ); - transactions.add(Tuple2(transaction, theAddress)); - } - await db.addNewTransactionData(transactions, walletId); - } - - Future updateChainHeight() async { - try { - NodeModel currentNode = getCurrentNode(); - int? intHeight = await TezosRpcAPI.getChainHeight( - nodeInfo: (host: currentNode.host, port: currentNode.port)); - if (intHeight == null) { - return; - } - Logging.instance - .log("Chain height for tezos: $intHeight", level: LogLevel.Info); - await updateCachedChainHeight(intHeight); - } catch (e, s) { - Logging.instance.log( - "Error occured in tezos_wallet.dart while getting chain height for tezos: ${e.toString()}", - level: LogLevel.Error); - } - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log( - "$walletId $walletName refreshMutex denied", - level: LogLevel.Info, - ); - return; - } else { - refreshMutex = true; - } - - try { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - await updateChainHeight(); - await updateBalance(); - await updateTransactions(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - - await refresh(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId, - ), - ); - }); - } - } catch (e, s) { - Logging.instance.log( - "Failed to refresh tezos wallet $walletId: '$walletName': $e\n$s", - level: LogLevel.Warning, - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - } - - refreshMutex = false; - } - - @override - int get storedChainHeight => getCachedChainHeight(); - - @override - Future testNetworkConnection() async { - NodeModel currentNode = getCurrentNode(); - return await TezosRpcAPI.testNetworkConnection( - nodeInfo: (host: currentNode.host, port: currentNode.port)); - } - - @override - Future> get transactions => - db.getTransactions(walletId).findAll(); - - @override - Future updateNode(bool shouldRefresh) async { - _xtzNode = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - - if (shouldRefresh) { - await refresh(); - } - } - - @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, - ); - } - - @override - // TODO: implement utxos - Future> get utxos => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); - } -} +// import 'dart:async'; +// +// import 'package:decimal/decimal.dart'; +// import 'package:isar/isar.dart'; +// import 'package:stackwallet/db/isar/main_db.dart'; +// import 'package:stackwallet/models/balance.dart'; +// import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +// import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +// import 'package:stackwallet/models/node_model.dart'; +// import 'package:stackwallet/models/paymint/fee_object_model.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/updated_in_background_event.dart'; +// import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +// 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/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:stackwallet/wallets/api/tezos/tezos_api.dart'; +// import 'package:stackwallet/wallets/api/tezos/tezos_rpc_api.dart'; +// import 'package:stackwallet/wallets/api/tezos/tezos_transaction.dart'; +// import 'package:tezart/tezart.dart'; +// import 'package:tuple/tuple.dart'; +// +// const int MINIMUM_CONFIRMATIONS = 1; +// const int _gasLimit = 10200; +// +// class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { +// TezosWallet({ +// required String walletId, +// required String walletName, +// required Coin coin, +// required SecureStorageInterface secureStore, +// required TransactionNotificationTracker tracker, +// MainDB? mockableOverride, +// }) { +// txTracker = tracker; +// _walletId = walletId; +// _walletName = walletName; +// _coin = coin; +// _secureStore = secureStore; +// initCache(walletId, coin); +// initWalletDB(mockableOverride: mockableOverride); +// } +// +// // NodeModel? _xtzNode; +// // +// // NodeModel getCurrentNode() { +// // return _xtzNode ?? +// // NodeService(secureStorageInterface: _secureStore) +// // .getPrimaryNodeFor(coin: Coin.tezos) ?? +// // DefaultNodes.getNodeFor(Coin.tezos); +// // } +// // +// // Future getKeystore() async { +// // return Keystore.fromMnemonic((await mnemonicString).toString()); +// // } +// // +// // @override +// // String get walletId => _walletId; +// // late String _walletId; +// // +// // @override +// // String get walletName => _walletName; +// // late String _walletName; +// // +// // @override +// // set walletName(String name) => _walletName = name; +// // +// // @override +// // set isFavorite(bool markFavorite) { +// // _isFavorite = markFavorite; +// // updateCachedIsFavorite(markFavorite); +// // } +// // +// // @override +// // bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); +// // bool? _isFavorite; +// // +// // @override +// // Coin get coin => _coin; +// // late Coin _coin; +// // +// // late SecureStorageInterface _secureStore; +// // late final TransactionNotificationTracker txTracker; +// // final _prefs = Prefs.instance; +// // +// // Timer? timer; +// // bool _shouldAutoSync = false; +// // Timer? _networkAliveTimer; +// // +// // @override +// // bool get shouldAutoSync => _shouldAutoSync; +// // +// // @override +// // set shouldAutoSync(bool shouldAutoSync) { +// // if (_shouldAutoSync != shouldAutoSync) { +// // _shouldAutoSync = shouldAutoSync; +// // if (!shouldAutoSync) { +// // timer?.cancel(); +// // timer = null; +// // stopNetworkAlivePinging(); +// // } else { +// // startNetworkAlivePinging(); +// // refresh(); +// // } +// // } +// // } +// // +// // void startNetworkAlivePinging() { +// // // call once on start right away +// // _periodicPingCheck(); +// // +// // // then periodically check +// // _networkAliveTimer = Timer.periodic( +// // Constants.networkAliveTimerDuration, +// // (_) async { +// // _periodicPingCheck(); +// // }, +// // ); +// // } +// // +// // void stopNetworkAlivePinging() { +// // _networkAliveTimer?.cancel(); +// // _networkAliveTimer = null; +// // } +// // +// // void _periodicPingCheck() async { +// // bool hasNetwork = await testNetworkConnection(); +// // +// // if (_isConnected != hasNetwork) { +// // NodeConnectionStatus status = hasNetwork +// // ? NodeConnectionStatus.connected +// // : NodeConnectionStatus.disconnected; +// // +// // GlobalEventBus.instance.fire( +// // NodeConnectionStatusChangedEvent( +// // status, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // _isConnected = hasNetwork; +// // if (hasNetwork) { +// // unawaited(refresh()); +// // } +// // } +// // } +// // +// // @override +// // Balance get balance => _balance ??= getCachedBalance(); +// // Balance? _balance; +// +// @override +// 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()); +// Map txData = { +// "fee": fee, +// "address": address, +// "recipientAmt": amount, +// }; +// return Future.value(txData); +// } catch (e) { +// return Future.error(e); +// } +// } +// +// @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); +// } +// } +// +// @override +// Future get currentReceivingAddress async { +// var mneString = await mnemonicString; +// if (mneString == null) { +// throw Exception("No mnemonic found!"); +// } +// return Future.value((Keystore.fromMnemonic(mneString)).address); +// } +// +// @override +// Future estimateFeeFor(Amount amount, int feeRate) async { +// int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1); +// feePerTx ??= 0; +// return Amount( +// rawValue: BigInt.from(feePerTx), +// fractionDigits: coin.decimals, +// ); +// } +// +// // @override +// // Future exit() { +// // _hasCalledExit = true; +// // return Future.value(); +// // } +// +// @override +// Future get fees async { +// int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1); +// feePerTx ??= 0; +// Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); +// return FeeObject( +// numberOfBlocksFast: 10, +// numberOfBlocksAverage: 10, +// numberOfBlocksSlow: 10, +// fast: feePerTx, +// medium: feePerTx, +// slow: feePerTx, +// ); +// } +// +// // @override +// // Future generateNewAddress() { +// // // TODO: implement generateNewAddress +// // throw UnimplementedError(); +// // } +// // +// // @override +// // bool get hasCalledExit => _hasCalledExit; +// // bool _hasCalledExit = false; +// // +// // @override +// // Future initializeExisting() async { +// // await _prefs.init(); +// // } +// // +// // @override +// // Future initializeNew( +// // ({String mnemonicPassphrase, int wordCount})? data, +// // ) async { +// // if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { +// // throw Exception( +// // "Attempted to overwrite mnemonic on generate new wallet!"); +// // } +// // +// // await _prefs.init(); +// // +// // var newKeystore = Keystore.random(); +// // await _secureStore.write( +// // key: '${_walletId}_mnemonic', +// // value: newKeystore.mnemonic, +// // ); +// // await _secureStore.write( +// // key: '${_walletId}_mnemonicPassphrase', +// // value: "", +// // ); +// // +// // final address = Address( +// // walletId: walletId, +// // value: newKeystore.address, +// // publicKey: [], +// // derivationIndex: 0, +// // derivationPath: null, +// // type: AddressType.unknown, +// // subType: AddressSubType.receiving, +// // ); +// // +// // await db.putAddress(address); +// // +// // await Future.wait([ +// // updateCachedId(walletId), +// // updateCachedIsFavorite(false), +// // ]); +// // } +// // +// // @override +// // bool get isConnected => _isConnected; +// // bool _isConnected = false; +// // +// // @override +// // bool get isRefreshing => refreshMutex; +// // bool refreshMutex = false; +// // +// // @override +// // // TODO: implement maxFee +// // Future get maxFee => throw UnimplementedError(); +// // +// // @override +// // Future> get mnemonic async { +// // final mnemonic = await mnemonicString; +// // final mnemonicPassphrase = await this.mnemonicPassphrase; +// // if (mnemonic == null) { +// // throw Exception("No mnemonic found!"); +// // } +// // if (mnemonicPassphrase == null) { +// // throw Exception("No mnemonic passphrase found!"); +// // } +// // return mnemonic.split(" "); +// // } +// // +// // @override +// // Future get mnemonicPassphrase => +// // _secureStore.read(key: '${_walletId}_mnemonicPassphrase'); +// // +// // @override +// // Future get mnemonicString => +// // _secureStore.read(key: '${_walletId}_mnemonic'); +// // +// // Future _recoverWalletFromSeedPhrase({ +// // required String mnemonic, +// // required String mnemonicPassphrase, +// // bool isRescan = false, +// // }) async { +// // final keystore = Keystore.fromMnemonic( +// // mnemonic, +// // password: mnemonicPassphrase, +// // ); +// // +// // final address = Address( +// // walletId: walletId, +// // value: keystore.address, +// // publicKey: [], +// // derivationIndex: 0, +// // derivationPath: null, +// // type: AddressType.unknown, +// // subType: AddressSubType.receiving, +// // ); +// // +// // if (isRescan) { +// // await db.updateOrPutAddresses([address]); +// // } else { +// // await db.putAddress(address); +// // } +// // } +// // +// // bool longMutex = false; +// // @override +// // Future fullRescan( +// // int maxUnusedAddressGap, +// // int maxNumberOfIndexesToCheck, +// // ) async { +// // try { +// // Logging.instance.log("Starting full rescan!", level: LogLevel.Info); +// // longMutex = true; +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.syncing, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // final _mnemonic = await mnemonicString; +// // final _mnemonicPassphrase = await mnemonicPassphrase; +// // +// // await db.deleteWalletBlockchainData(walletId); +// // +// // await _recoverWalletFromSeedPhrase( +// // mnemonic: _mnemonic!, +// // mnemonicPassphrase: _mnemonicPassphrase!, +// // isRescan: true, +// // ); +// // +// // await refresh(); +// // Logging.instance.log("Full rescan complete!", level: LogLevel.Info); +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.synced, +// // walletId, +// // coin, +// // ), +// // ); +// // } catch (e, s) { +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.unableToSync, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // Logging.instance.log( +// // "Exception rethrown from fullRescan(): $e\n$s", +// // level: LogLevel.Error, +// // ); +// // rethrow; +// // } finally { +// // longMutex = false; +// // } +// // } +// // +// // @override +// // Future recoverFromMnemonic({ +// // required String mnemonic, +// // String? mnemonicPassphrase, +// // required int maxUnusedAddressGap, +// // required int maxNumberOfIndexesToCheck, +// // required int height, +// // }) async { +// // longMutex = true; +// // try { +// // if ((await mnemonicString) != null || +// // (await this.mnemonicPassphrase) != null) { +// // throw Exception("Attempted to overwrite mnemonic on restore!"); +// // } +// // await _secureStore.write( +// // key: '${_walletId}_mnemonic', value: mnemonic.trim()); +// // await _secureStore.write( +// // key: '${_walletId}_mnemonicPassphrase', +// // value: mnemonicPassphrase ?? "", +// // ); +// // +// // await _recoverWalletFromSeedPhrase( +// // mnemonic: mnemonic, +// // mnemonicPassphrase: mnemonicPassphrase ?? "", +// // isRescan: false, +// // ); +// // +// // await Future.wait([ +// // updateCachedId(walletId), +// // updateCachedIsFavorite(false), +// // ]); +// // +// // await refresh(); +// // } catch (e, s) { +// // Logging.instance.log( +// // "Exception rethrown from recoverFromMnemonic(): $e\n$s", +// // level: LogLevel.Error); +// // +// // rethrow; +// // } finally { +// // longMutex = false; +// // } +// // } +// // +// // Future updateBalance() async { +// // try { +// // NodeModel currentNode = getCurrentNode(); +// // BigInt? balance = await TezosRpcAPI.getBalance( +// // nodeInfo: (host: currentNode.host, port: currentNode.port), +// // address: await currentReceivingAddress); +// // if (balance == null) { +// // return; +// // } +// // Logging.instance.log( +// // "Balance for ${await currentReceivingAddress}: $balance", +// // level: LogLevel.Info); +// // Amount balanceInAmount = +// // Amount(rawValue: balance, fractionDigits: coin.decimals); +// // _balance = Balance( +// // total: balanceInAmount, +// // spendable: balanceInAmount, +// // blockedTotal: +// // Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals), +// // pendingSpendable: +// // Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals), +// // ); +// // await updateCachedBalance(_balance!); +// // } catch (e, s) { +// // Logging.instance.log( +// // "Error getting balance in tezos_wallet.dart: ${e.toString()}", +// // level: LogLevel.Error); +// // } +// // } +// // +// // Future updateTransactions() async { +// // List? txs = +// // await TezosAPI.getTransactions(await currentReceivingAddress); +// // Logging.instance.log("Transactions: $txs", level: LogLevel.Info); +// // if (txs == null) { +// // return; +// // } else if (txs.isEmpty) { +// // return; +// // } +// // List> transactions = []; +// // for (var theTx in txs) { +// // var txType = TransactionType.unknown; +// // var selfAddress = await currentReceivingAddress; +// // if (selfAddress == theTx.senderAddress) { +// // txType = TransactionType.outgoing; +// // } else if (selfAddress == theTx.receiverAddress) { +// // txType = TransactionType.incoming; +// // } else if (selfAddress == theTx.receiverAddress && +// // selfAddress == theTx.senderAddress) { +// // txType = TransactionType.sentToSelf; +// // } +// // var transaction = Transaction( +// // walletId: walletId, +// // txid: theTx.hash, +// // timestamp: theTx.timestamp, +// // type: txType, +// // subType: TransactionSubType.none, +// // amount: theTx.amountInMicroTez, +// // amountString: Amount( +// // rawValue: BigInt.parse(theTx.amountInMicroTez.toString()), +// // fractionDigits: coin.decimals, +// // ).toJsonString(), +// // fee: theTx.feeInMicroTez, +// // height: theTx.height, +// // isCancelled: false, +// // isLelantus: false, +// // slateId: "", +// // otherData: "", +// // inputs: [], +// // outputs: [], +// // nonce: 0, +// // numberOfMessages: null, +// // ); +// // final AddressSubType subType; +// // switch (txType) { +// // case TransactionType.incoming: +// // case TransactionType.sentToSelf: +// // subType = AddressSubType.receiving; +// // break; +// // case TransactionType.outgoing: +// // case TransactionType.unknown: +// // subType = AddressSubType.unknown; +// // break; +// // } +// // final theAddress = Address( +// // walletId: walletId, +// // value: theTx.receiverAddress, +// // publicKey: [], +// // derivationIndex: 0, +// // derivationPath: null, +// // type: AddressType.unknown, +// // subType: subType, +// // ); +// // transactions.add(Tuple2(transaction, theAddress)); +// // } +// // await db.addNewTransactionData(transactions, walletId); +// // } +// // +// // Future updateChainHeight() async { +// // try { +// // NodeModel currentNode = getCurrentNode(); +// // int? intHeight = await TezosRpcAPI.getChainHeight( +// // nodeInfo: (host: currentNode.host, port: currentNode.port)); +// // if (intHeight == null) { +// // return; +// // } +// // Logging.instance +// // .log("Chain height for tezos: $intHeight", level: LogLevel.Info); +// // await updateCachedChainHeight(intHeight); +// // } catch (e, s) { +// // Logging.instance.log( +// // "Error occured in tezos_wallet.dart while getting chain height for tezos: ${e.toString()}", +// // level: LogLevel.Error); +// // } +// // } +// // +// // @override +// // Future refresh() async { +// // if (refreshMutex) { +// // Logging.instance.log( +// // "$walletId $walletName refreshMutex denied", +// // level: LogLevel.Info, +// // ); +// // return; +// // } else { +// // refreshMutex = true; +// // } +// // +// // try { +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.syncing, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // await updateChainHeight(); +// // await updateBalance(); +// // await updateTransactions(); +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.synced, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // if (shouldAutoSync) { +// // timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { +// // Logging.instance.log( +// // "Periodic refresh check for $walletId $walletName in object instance: $hashCode", +// // level: LogLevel.Info); +// // +// // await refresh(); +// // GlobalEventBus.instance.fire( +// // UpdatedInBackgroundEvent( +// // "New data found in $walletId $walletName in background!", +// // walletId, +// // ), +// // ); +// // }); +// // } +// // } catch (e, s) { +// // Logging.instance.log( +// // "Failed to refresh tezos wallet $walletId: '$walletName': $e\n$s", +// // level: LogLevel.Warning, +// // ); +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.unableToSync, +// // walletId, +// // coin, +// // ), +// // ); +// // } +// // +// // refreshMutex = false; +// // } +// // +// // @override +// // int get storedChainHeight => getCachedChainHeight(); +// +// // @override +// // Future testNetworkConnection() async { +// // NodeModel currentNode = getCurrentNode(); +// // return await TezosRpcAPI.testNetworkConnection( +// // nodeInfo: (host: currentNode.host, port: currentNode.port)); +// // } +// +// // @override +// // Future> get transactions => +// // db.getTransactions(walletId).findAll(); +// +// // @override +// // Future updateNode(bool shouldRefresh) async { +// // _xtzNode = NodeService(secureStorageInterface: _secureStore) +// // .getPrimaryNodeFor(coin: coin) ?? +// // DefaultNodes.getNodeFor(coin); +// // +// // if (shouldRefresh) { +// // await refresh(); +// // } +// // } +// +// @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, +// ); +// } +// +// // @override +// // // TODO: implement utxos +// // Future> get utxos => throw UnimplementedError(); +// // +// // @override +// // bool validateAddress(String address) { +// // return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); +// // } +// } diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index e15451469..3e8ebc46f 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -55,36 +55,37 @@ class Wallets { } Future deleteWallet( - String walletId, + WalletInfo info, SecureStorageInterface secureStorage, ) async { + final walletId = info.walletId; Logging.instance.log( "deleteWallet called with walletId=$walletId", level: LogLevel.Warning, ); - final wallet = getWallet(walletId); + final wallet = _wallets[walletId]; _wallets.remove(walletId); - await wallet.exit(); + await wallet?.exit(); await secureStorage.delete(key: Wallet.mnemonicKey(walletId: walletId)); await secureStorage.delete( key: Wallet.mnemonicPassphraseKey(walletId: walletId)); await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId)); - if (wallet.info.coin == Coin.wownero) { + if (info.coin == Coin.wownero) { final wowService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); await wowService.remove(walletId); Logging.instance .log("monero wallet: $walletId deleted", level: LogLevel.Info); - } else if (wallet.info.coin == Coin.monero) { + } else if (info.coin == Coin.monero) { final xmrService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); await xmrService.remove(walletId); Logging.instance .log("monero wallet: $walletId deleted", level: LogLevel.Info); - } else if (wallet.info.coin == Coin.epicCash) { + } else if (info.coin == Coin.epicCash) { final deleteResult = await deleteEpicWallet( walletId: walletId, secureStore: secureStorage); Logging.instance.log( diff --git a/lib/wallets/api/tezos/tezos_api.dart b/lib/wallets/api/tezos/tezos_api.dart index a55bd45be..7e774c44b 100644 --- a/lib/wallets/api/tezos/tezos_api.dart +++ b/lib/wallets/api/tezos/tezos_api.dart @@ -10,7 +10,7 @@ import 'package:stackwallet/wallets/api/tezos/tezos_transaction.dart'; abstract final class TezosAPI { static final HTTP _client = HTTP(); - static const String _baseURL = 'https://api.tzstats.com'; + static const String _baseURL = 'https://api.mainnet.tzkt.io'; static Future?> getTransactions(String address) async { try { diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 0af387a10..548422f5c 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -1,4 +1,5 @@ import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; @@ -12,21 +13,37 @@ class Tezos extends Bip39Currency { throw Exception("Unsupported network: $network"); } } - @override - // TODO: implement defaultNode - NodeModel get defaultNode => throw UnimplementedError(); @override // TODO: implement genesisHash String get genesisHash => throw UnimplementedError(); @override - // TODO: implement minConfirms - int get minConfirms => throw UnimplementedError(); + int get minConfirms => 1; @override bool validateAddress(String address) { - // TODO: implement validateAddress - throw UnimplementedError(); + return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "https://mainnet.api.tez.ie", + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.tezos), + useSSL: true, + enabled: true, + coinName: Coin.tezos.name, + isFailover: true, + isDown: false, + ); + + default: + throw UnimplementedError(); + } } } diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 73e4e5205..455790f67 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -3,6 +3,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:tezart/tezart.dart' as tezart; import 'package:web3dart/web3dart.dart' as web3dart; class TxData { @@ -51,6 +52,9 @@ class TxData { final TransactionSubType? txSubType; final List>? mintsMapLelantus; + // tezos specific + final tezart.OperationsList? tezosOperationsList; + TxData({ this.feeRateType, this.feeRateAmount, @@ -80,6 +84,7 @@ class TxData { this.txType, this.txSubType, this.mintsMapLelantus, + this.tezosOperationsList, }); Amount? get amount => recipients != null && recipients!.isNotEmpty @@ -121,6 +126,7 @@ class TxData { TransactionType? txType, TransactionSubType? txSubType, List>? mintsMapLelantus, + tezart.OperationsList? tezosOperationsList, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -152,6 +158,7 @@ class TxData { txType: txType ?? this.txType, txSubType: txSubType ?? this.txSubType, mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus, + tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList, ); } @@ -185,5 +192,6 @@ class TxData { 'txType: $txType, ' 'txSubType: $txSubType, ' 'mintsMapLelantus: $mintsMapLelantus, ' + 'tezosOperationsList: $tezosOperationsList, ' '}'; } diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index dfec4f7be..a9c503353 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -1,85 +1,380 @@ import 'package:isar/isar.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/extensions/impl/string.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/api/tezos/tezos_api.dart'; +import 'package:stackwallet/wallets/api/tezos/tezos_rpc_api.dart'; +import 'package:stackwallet/wallets/api/tezos/tezos_transaction.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:tezart/tezart.dart' as tezart; +import 'package:tuple/tuple.dart'; + +const int GAS_LIMIT = 10200; class TezosWallet extends Bip39Wallet { TezosWallet(CryptoCurrencyNetwork network) : super(Tezos(network)); + NodeModel? _xtzNode; + + Future _getKeyStore() async { + final mnemonic = await getMnemonic(); + final passphrase = await getMnemonicPassphrase(); + return tezart.Keystore.fromMnemonic(mnemonic, password: passphrase); + } + + Future
_getAddressFromMnemonic() async { + final keyStore = await _getKeyStore(); + return Address( + walletId: walletId, + value: keyStore.address, + publicKey: keyStore.publicKey.toUint8ListFromBase58CheckEncoded, + derivationIndex: 0, + derivationPath: null, + type: info.coin.primaryAddressType, + subType: AddressSubType.receiving, + ); + } + + // =========================================================================== + + @override + Future init() async { + final _address = await getCurrentReceivingAddress(); + if (_address == null) { + final address = await _getAddressFromMnemonic(); + + await mainDB.updateOrPutAddresses([address]); + } + + await super.init(); + } + @override - // TODO: implement changeAddressFilterOperation FilterOperation? get changeAddressFilterOperation => - throw UnimplementedError(); + throw UnimplementedError("Not used for $runtimeType"); @override - // TODO: implement receivingAddressFilterOperation FilterOperation? get receivingAddressFilterOperation => - throw UnimplementedError(); + FilterGroup.and(standardReceivingAddressFilters); @override - Future confirmSend({required TxData txData}) { + Future prepareSend({required TxData txData}) async { + if (txData.recipients == null || txData.recipients!.length != 1) { + throw Exception("$runtimeType prepareSend requires 1 recipient"); + } + + Amount sendAmount = txData.amount!; + + if (sendAmount > info.cachedBalance.spendable) { + throw Exception("Insufficient available balance"); + } + + final bool isSendAll = sendAmount == info.cachedBalance.spendable; + + final sourceKeyStore = await _getKeyStore(); + final tezartClient = tezart.TezartClient( + (_xtzNode ?? getCurrentNode()).host, + ); + + final opList = await tezartClient.transferOperation( + source: sourceKeyStore, + destination: txData.recipients!.first.address, + amount: sendAmount.raw.toInt(), + ); + + await opList.computeFees(); + + final fee = Amount( + rawValue: opList.operations + .map( + (e) => BigInt.from(e.fee), + ) + .fold( + BigInt.zero, + (p, e) => p + e, + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + if (isSendAll) { + sendAmount = sendAmount - fee; + } + + return txData.copyWith( + recipients: [ + ( + amount: sendAmount, + address: txData.recipients!.first.address, + ) + ], + fee: fee, + tezosOperationsList: opList, + ); + } + + @override + Future confirmSend({required TxData txData}) async { // TODO: implement confirmSend throw UnimplementedError(); } @override - Future estimateFeeFor(Amount amount, int feeRate) { - // TODO: implement estimateFeeFor + Future estimateFeeFor(Amount amount, int feeRate) async { throw UnimplementedError(); + // final ADDRESS_REPLACEME = (await getCurrentReceivingAddress())!.value; + // + // try { + // final sourceKeyStore = await _getKeyStore(); + // final tezartClient = tezart.TezartClient( + // (_xtzNode ?? getCurrentNode()).host, + // ); + // + // final opList = await tezartClient.transferOperation( + // source: sourceKeyStore, + // destination: ADDRESS_REPLACEME, + // amount: amount.raw.toInt(), + // ); + // + // await opList.run(); + // await opList.estimate(); + // + // final fee = Amount( + // rawValue: opList.operations + // .map( + // (e) => BigInt.from(e.fee), + // ) + // .fold( + // BigInt.zero, + // (p, e) => p + e, + // ), + // fractionDigits: cryptoCurrency.fractionDigits, + // ); + // + // return fee; + // } catch (e, s) { + // Logging.instance.log( + // "Error in estimateFeeFor() in tezos_wallet.dart: $e\n$s}", + // level: LogLevel.Error, + // ); + // rethrow; + // } } @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); - - @override - Future pingCheck() { - // TODO: implement pingCheck - throw UnimplementedError(); + Future get fees async { + final feePerTx = (await estimateFeeFor( + Amount( + rawValue: BigInt.one, + fractionDigits: cryptoCurrency.fractionDigits, + ), + 42)) + .raw + .toInt(); + Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: feePerTx, + medium: feePerTx, + slow: feePerTx, + ); } @override - Future prepareSend({required TxData txData}) { - // TODO: implement prepareSend - throw UnimplementedError(); + Future pingCheck() async { + final currentNode = getCurrentNode(); + return await TezosRpcAPI.testNetworkConnection( + nodeInfo: ( + host: currentNode.host, + port: currentNode.port, + ), + ); } @override - Future recover({required bool isRescan}) { - // TODO: implement recover - throw UnimplementedError(); + Future recover({required bool isRescan}) async { + await refreshMutex.protect(() async { + if (isRescan) { + await mainDB.deleteWalletBlockchainData(walletId); + } + + final address = await _getAddressFromMnemonic(); + + await mainDB.updateOrPutAddresses([address]); + + await Future.wait([ + updateBalance(), + updateTransactions(), + updateChainHeight(), + ]); + }); } @override - Future updateBalance() { - // TODO: implement updateBalance - throw UnimplementedError(); + Future updateBalance() async { + try { + final currentNode = _xtzNode ?? getCurrentNode(); + final balance = await TezosRpcAPI.getBalance( + nodeInfo: (host: currentNode.host, port: currentNode.port), + address: (await getCurrentReceivingAddress())!.value, + ); + + final balanceInAmount = Amount( + rawValue: balance!, + fractionDigits: cryptoCurrency.fractionDigits, + ); + final newBalance = Balance( + total: balanceInAmount, + spendable: balanceInAmount, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + + await info.updateBalance(newBalance: newBalance, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.log( + "Error getting balance in tezos_wallet.dart: $e\n$s}", + level: LogLevel.Error, + ); + } } @override - Future updateChainHeight() { - // TODO: implement updateChainHeight - throw UnimplementedError(); + Future updateChainHeight() async { + try { + final currentNode = _xtzNode ?? getCurrentNode(); + final height = await TezosRpcAPI.getChainHeight( + nodeInfo: ( + host: currentNode.host, + port: currentNode.port, + ), + ); + + await info.updateCachedChainHeight( + newHeight: height!, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.log( + "Error occurred in tezos_wallet.dart while getting" + " chain height for tezos: $e\n$s}", + level: LogLevel.Error, + ); + } } @override - Future updateNode() { - // TODO: implement updateNode - throw UnimplementedError(); + Future updateNode() async { + _xtzNode = NodeService(secureStorageInterface: secureStorageInterface) + .getPrimaryNodeFor(coin: info.coin) ?? + DefaultNodes.getNodeFor(info.coin); + + await refresh(); } @override - Future updateTransactions() { - // TODO: implement updateTransactions - throw UnimplementedError(); + NodeModel getCurrentNode() { + return _xtzNode ?? + NodeService(secureStorageInterface: secureStorageInterface) + .getPrimaryNodeFor(coin: info.coin) ?? + DefaultNodes.getNodeFor(info.coin); } @override - Future updateUTXOs() { - // TODO: implement updateUTXOs - throw UnimplementedError(); + Future updateTransactions() async { + // TODO: optimize updateTransactions + + final myAddress = (await getCurrentReceivingAddress())!; + List? txs = + await TezosAPI.getTransactions(myAddress.value); + Logging.instance.log("Transactions: $txs", level: LogLevel.Info); + if (txs == null || txs.isEmpty) { + return; + } + + List> transactions = []; + for (final theTx in txs) { + final TransactionType txType; + + if (myAddress.value == theTx.senderAddress) { + txType = TransactionType.outgoing; + } else if (myAddress.value == theTx.receiverAddress) { + if (myAddress.value == theTx.senderAddress) { + txType = TransactionType.sentToSelf; + } else { + txType = TransactionType.incoming; + } + } else { + txType = TransactionType.unknown; + } + + var transaction = Transaction( + walletId: walletId, + txid: theTx.hash, + timestamp: theTx.timestamp, + type: txType, + subType: TransactionSubType.none, + amount: theTx.amountInMicroTez, + amountString: Amount( + rawValue: BigInt.parse(theTx.amountInMicroTez.toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + fee: theTx.feeInMicroTez, + height: theTx.height, + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, + ); + + final Address theAddress; + switch (txType) { + case TransactionType.incoming: + case TransactionType.sentToSelf: + theAddress = myAddress; + break; + case TransactionType.outgoing: + case TransactionType.unknown: + theAddress = Address( + walletId: walletId, + value: theTx.receiverAddress, + publicKey: [], + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + break; + } + transactions.add(Tuple2(transaction, theAddress)); + } + await mainDB.addNewTransactionData(transactions, walletId); + } + + @override + Future updateUTXOs() async { + // do nothing. Not used in tezos } }