From f43aeadc3f8ed5296e320ed97d572c5558d29646 Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:03:32 +0300 Subject: [PATCH 01/35] feat: add xtz --- lib/models/isar/stack_theme.dart | 9 + .../add_edit_node_view.dart | 1 + lib/services/coins/coin_service.dart | 10 + lib/services/coins/tezos/tezos_wallet.dart | 314 ++++++++++++++++++ lib/themes/coin_icon_provider.dart | 2 + lib/themes/coin_image_provider.dart | 4 + lib/themes/color_theme.dart | 3 + lib/themes/stack_colors.dart | 2 + lib/utilities/address_utils.dart | 2 + lib/utilities/block_explorers.dart | 2 + lib/utilities/constants.dart | 11 + lib/utilities/default_nodes.dart | 18 +- lib/utilities/enums/coin_enum.dart | 21 ++ .../enums/derive_path_type_enum.dart | 1 + pubspec.lock | 80 ++++- pubspec.yaml | 2 + 16 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 lib/services/coins/tezos/tezos_wallet.dart diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index 5809e4443..67fbb0ab7 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -1902,6 +1902,7 @@ class ThemeAssets implements IThemeAssets { late final String wownero; late final String namecoin; late final String particl; + late final String tezos; late final String bitcoinImage; late final String bitcoincashImage; late final String dogecoinImage; @@ -1913,6 +1914,7 @@ class ThemeAssets implements IThemeAssets { late final String wowneroImage; late final String namecoinImage; late final String particlImage; + late final String tezosImage; late final String bitcoinImageSecondary; late final String bitcoincashImageSecondary; late final String dogecoinImageSecondary; @@ -1924,6 +1926,7 @@ class ThemeAssets implements IThemeAssets { late final String wowneroImageSecondary; late final String namecoinImageSecondary; late final String particlImageSecondary; + late final String tezosImageSecondary; @override late final String? loadingGif; @override @@ -1997,6 +2000,8 @@ class ThemeAssets implements IThemeAssets { "$applicationThemesDirectoryPath/$themeId/assets/${json["namecoin"] as String}" ..particl = "$applicationThemesDirectoryPath/$themeId/assets/${json["particl"] as String}" + ..tezos = + "$applicationThemesDirectoryPath/$themeId/assets/${json["bitcoin"] as String}" // TODO: change to tezos ..bitcoinImage = "$applicationThemesDirectoryPath/$themeId/assets/${json["bitcoin_image"] as String}" ..bitcoincashImage = @@ -2019,6 +2024,8 @@ class ThemeAssets implements IThemeAssets { "$applicationThemesDirectoryPath/$themeId/assets/${json["namecoin_image"] as String}" ..particlImage = "$applicationThemesDirectoryPath/$themeId/assets/${json["particl_image"] as String}" + ..tezosImage = + "$applicationThemesDirectoryPath/$themeId/assets/${json["bitcoin_image"] as String}" // TODO: change to tezos ..bitcoinImageSecondary = "$applicationThemesDirectoryPath/$themeId/assets/${json["bitcoin_image_secondary"] as String}" ..bitcoincashImageSecondary = @@ -2041,6 +2048,8 @@ class ThemeAssets implements IThemeAssets { "$applicationThemesDirectoryPath/$themeId/assets/${json["namecoin_image_secondary"] as String}" ..particlImageSecondary = "$applicationThemesDirectoryPath/$themeId/assets/${json["particl_image_secondary"] as String}" + ..tezosImageSecondary = + "$applicationThemesDirectoryPath/$themeId/assets/${json["bitcoin_image_secondary"] as String}" // TODO: change to tezos ..loadingGif = json["loading_gif"] is String ? "$applicationThemesDirectoryPath/$themeId/assets/${json["loading_gif"] as String}" : null diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 1ff1a1359..b29f4718c 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -711,6 +711,7 @@ class _NodeFormState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincash: case Coin.particl: + case Coin.tezos: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 48fa59630..a6d1b023b 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; 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/tezos/tezos_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -204,6 +205,15 @@ abstract class CoinServiceAPI { cachedClient: cachedClient, tracker: tracker); + case Coin.tezos: + return TezosWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + tracker: tracker, + ); + case Coin.wownero: return WowneroWallet( walletId: walletId, diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart new file mode 100644 index 000000000..fca4eabc2 --- /dev/null +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -0,0 +1,314 @@ +import 'dart:async'; + +import 'package:http/http.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/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; + +import 'package:tezart/tezart.dart'; + +import '../../../db/isar/main_db.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; +import '../../mixins/wallet_cache.dart'; +import '../../mixins/wallet_db.dart'; +import '../../transaction_notification_tracker.dart'; + +const int MINIMUM_CONFIRMATIONS = 1; + +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); + } + + @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; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + } else { + refresh(); + } + } + } + + @override + Balance get balance => _balance ??= getCachedBalance(); + Balance? _balance; + + @override + Future confirmSend({required Map txData}) { + // TODO: implement confirmSend, + // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with + throw UnimplementedError(); + } + + @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) { + // TODO: implement estimateFeeFor + throw UnimplementedError(); + } + + @override + Future exit() { + _hasCalledExit = true; + return Future.value(); + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + @override + Future fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { + // TODO: implement fullRescan + throw UnimplementedError(); + } + + @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() async { + 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: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + + 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'); + + @override + Future> prepareSend({required String address, required Amount amount, Map? args}) { + // TODO: implement prepareSend + // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with + throw UnimplementedError(); + } + + @override + Future recoverFromMnemonic({required String mnemonic, String? mnemonicPassphrase, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { + 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 ?? "", + ); + + final address = Address( + walletId: walletId, + value: Keystore.fromMnemonic(mnemonic).address, + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + + await db.putAddress(address); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } + + Future updateBalance() async { + var api = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance"; // TODO: Can we use current node instead of this? + var theBalance = await get(Uri.parse(api)).then((value) => value.body); + Logging.instance.log("Balance for ${await currentReceivingAddress}: $theBalance", level: LogLevel.Info); + var balanceInAmount = Amount(rawValue: BigInt.parse(theBalance.toString()), fractionDigits: 6); + _balance = Balance( + total: balanceInAmount, + spendable: balanceInAmount, + blockedTotal: Amount(rawValue: BigInt.parse("0"), fractionDigits: 6), + pendingSpendable: Amount(rawValue: BigInt.parse("0"), fractionDigits: 6), + ); + await updateCachedBalance(_balance!); + } + + @override + Future refresh() { + updateBalance(); + return Future.value(); + } + + @override + int get storedChainHeight => getCachedChainHeight(); + + @override + Future testNetworkConnection() { + // TODO: implement testNetworkConnection + throw UnimplementedError(); + } + + @override + // TODO: implement transactions + Future> get transactions async { + // TODO: Maybe we can use this -> https://api.tzkt.io/#operation/Accounts_GetBalanceHistory + } + + @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) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } + + @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/themes/coin_icon_provider.dart b/lib/themes/coin_icon_provider.dart index 6c17969e6..3792560e6 100644 --- a/lib/themes/coin_icon_provider.dart +++ b/lib/themes/coin_icon_provider.dart @@ -35,6 +35,8 @@ final coinIconProvider = Provider.family((ref, coin) { return assets.namecoin; case Coin.particl: return assets.particl; + case Coin.tezos: + return assets.tezos; case Coin.ethereum: return assets.ethereum; } diff --git a/lib/themes/coin_image_provider.dart b/lib/themes/coin_image_provider.dart index 239e1d1cb..3de9ab40a 100644 --- a/lib/themes/coin_image_provider.dart +++ b/lib/themes/coin_image_provider.dart @@ -31,6 +31,8 @@ final coinImageProvider = Provider.family((ref, coin) { return assets.namecoinImage; case Coin.particl: return assets.particlImage; + case Coin.tezos: + return assets.tezosImage; case Coin.bitcoinTestNet: return assets.bitcoinImage; case Coin.bitcoincashTestnet: @@ -75,6 +77,8 @@ final coinImageSecondaryProvider = Provider.family((ref, coin) { return assets.namecoinImageSecondary; case Coin.particl: return assets.particlImageSecondary; + case Coin.tezos: + return assets.tezosImageSecondary; case Coin.bitcoinTestNet: return assets.bitcoinImageSecondary; case Coin.bitcoincashTestnet: diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index b4f00adcf..7946139c3 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -18,6 +18,7 @@ class CoinThemeColorDefault { Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); Color get particl => const Color(0xFF8175BD); + Color get tezos => const Color(0xFF0F61FF); Color forCoin(Coin coin) { switch (coin) { @@ -50,6 +51,8 @@ class CoinThemeColorDefault { return wownero; case Coin.particl: return particl; + case Coin.tezos: + return tezos; } } } diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index 6717615b1..8f5dbbe15 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1697,6 +1697,8 @@ class StackColors extends ThemeExtension { return _coin.wownero; case Coin.particl: return _coin.particl; + case Coin.tezos: + return _coin.tezos; } } diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 44850bc65..a151abbdf 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -74,6 +74,8 @@ class AddressUtils { return Address.validateAddress(address, namecoin, namecoin.bech32!); case Coin.particl: return Address.validateAddress(address, particl); + case Coin.tezos: + return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); case Coin.litecoinTestNet: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index cf0628fd2..4f4228e27 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -43,6 +43,8 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); case Coin.particl: return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); + case Coin.tezos: + return Uri.parse("https://tzstats.com/$txid"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index b3295b3c8..2fdacc64b 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -29,12 +29,14 @@ abstract class Constants { static const int _satsPerCoinECash = 100; static const int _satsPerCoinMonero = 1000000000000; static const int _satsPerCoinWownero = 100000000000; + static const int _satsPerCoinTezos = 1000000; static const int _satsPerCoin = 100000000; static const int _decimalPlaces = 8; static const int _decimalPlacesWownero = 11; static const int _decimalPlacesMonero = 12; static const int _decimalPlacesEthereum = 18; static const int _decimalPlacesECash = 2; + static const int _decimalPlacesTezos = 6; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -76,6 +78,9 @@ abstract class Constants { case Coin.eCash: return _satsPerCoinECash; + + case Coin.tezos: + return _satsPerCoinTezos; } } @@ -107,6 +112,9 @@ abstract class Constants { case Coin.eCash: return _decimalPlacesECash; + + case Coin.tezos: + return _decimalPlacesTezos; } } @@ -180,6 +188,9 @@ abstract class Constants { case Coin.particl: return 600; + + case Coin.tezos: + return 60; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 0f8425c88..8c48f5340 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -169,7 +169,20 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.particl.name, isFailover: true, - isDown: false); + isDown: false + ); + + static NodeModel get tezos => NodeModel( // TODO: Change this to original one + host: "https://mainnet.api.tez.ie", + port: 443, + name: defaultName, + id: _nodeId(Coin.tezos), + useSSL: true, + enabled: true, + coinName: Coin.tezos.name, + isFailover: true, + isDown: false + ); static NodeModel get bitcoinTestnet => NodeModel( host: "bitcoin-testnet.stackwallet.com", @@ -269,6 +282,9 @@ abstract class DefaultNodes { case Coin.particl: return particl; + case Coin.tezos: + return tezos; + case Coin.bitcoinTestNet: return bitcoinTestnet; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index f1109a626..096e26db5 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; +import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart' as tezos; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/utilities/constants.dart'; @@ -31,6 +32,7 @@ enum Coin { monero, namecoin, particl, + tezos, wownero, /// @@ -71,6 +73,8 @@ extension CoinExt on Coin { return "Monero"; case Coin.particl: return "Particl"; + case Coin.tezos: + return "Tezos"; case Coin.wownero: return "Wownero"; case Coin.namecoin: @@ -110,6 +114,8 @@ extension CoinExt on Coin { return "XMR"; case Coin.particl: return "PART"; + case Coin.tezos: + return "XTZ"; case Coin.wownero: return "WOW"; case Coin.namecoin: @@ -150,6 +156,8 @@ extension CoinExt on Coin { return "monero"; case Coin.particl: return "particl"; + case Coin.tezos: + return "tezos"; case Coin.wownero: return "wownero"; case Coin.namecoin: @@ -187,6 +195,7 @@ extension CoinExt on Coin { case Coin.epicCash: case Coin.ethereum: case Coin.monero: + case Coin.tezos: case Coin.wownero: return false; } @@ -207,6 +216,7 @@ extension CoinExt on Coin { case Coin.eCash: case Coin.epicCash: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.dogecoinTestNet: case Coin.bitcoinTestNet: @@ -229,6 +239,7 @@ extension CoinExt on Coin { case Coin.epicCash: case Coin.ethereum: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.eCash: return false; @@ -254,6 +265,7 @@ extension CoinExt on Coin { case Coin.epicCash: case Coin.ethereum: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.eCash: return this; @@ -312,6 +324,9 @@ extension CoinExt on Coin { case Coin.particl: return particl.MINIMUM_CONFIRMATIONS; + case Coin.tezos: + return tezos.MINIMUM_CONFIRMATIONS; + case Coin.wownero: return wow.MINIMUM_CONFIRMATIONS; @@ -367,6 +382,10 @@ Coin coinFromPrettyName(String name) { case "particl": return Coin.particl; + case "Tezos": + case "tezos": + return Coin.tezos; + case "Namecoin": case "namecoin": return Coin.namecoin; @@ -436,6 +455,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "part": return Coin.particl; + case "xtz": + return Coin.tezos; case "tltc": return Coin.litecoinTestNet; case "tbtc": diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 72899f5bd..315a355db 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -37,6 +37,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.epicCash: case Coin.monero: case Coin.wownero: + case Coin.tezos: // TODO: Is this true? throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); } diff --git a/pubspec.lock b/pubspec.lock index 600a92d67..14c0aca59 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "61.0.0" analyzer: dependency: "direct dev" description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.13.0" animations: dependency: "direct main" description: @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.29" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" app_settings: dependency: "direct main" description: @@ -490,6 +498,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.5" + dio: + dependency: transitive + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" dropdown_button2: dependency: "direct main" description: @@ -966,6 +982,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + json_serializable: + dependency: transitive + description: + name: json_serializable + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" + source: hosted + version: "6.6.1" jsonrpc2: dependency: "direct main" description: @@ -1037,6 +1061,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + memoize: + dependency: transitive + description: + name: memoize + sha256: "51481d328c86cbdc59711369179bac88551ca0556569249be5317e66fc796cac" + url: "https://pub.dev" + source: hosted + version: "3.0.0" meta: dependency: transitive description: @@ -1285,6 +1317,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: e5fb0bce1717b7f136f35ee98b5c02b3e6383211f8a77ca882fa7812232a07b9 + url: "https://pub.dev" + source: hosted + version: "0.3.4" platform: dependency: transitive description: @@ -1317,6 +1357,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + pretty_dio_logger: + dependency: transitive + description: + name: pretty_dio_logger + sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" + url: "https://pub.dev" + source: hosted + version: "1.2.0-beta-1" process: dependency: transitive description: @@ -1365,6 +1413,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" rational: dependency: "direct main" description: @@ -1373,6 +1429,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + retry: + dependency: transitive + description: + name: retry + sha256: a8a1e475a100a0bdc73f529ca8ea1e9c9c76bec8ad86a1f47780139a34ce7aea + url: "https://pub.dev" + source: hosted + version: "3.1.1" riverpod: dependency: transitive description: @@ -1659,6 +1723,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.20" + tezart: + dependency: "direct main" + description: + name: tezart + sha256: "35d526f2e6ca250c64461ebfb4fa9f64b6599fab8c4242c8e89ae27d4ac2e15a" + url: "https://pub.dev" + source: hosted + version: "2.0.5" time: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3533282bf..25cd1e354 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -153,6 +153,7 @@ dependencies: rational: ^2.2.2 archive: ^3.3.2 desktop_drop: ^0.4.1 + tezart: ^2.0.5 dev_dependencies: flutter_test: @@ -198,6 +199,7 @@ dependency_overrides: url: https://github.com/cypherstack/stack-bip39.git ref: 3bef5acc21340f3cc78df0ad1dce5868a3ed68a5 crypto: 3.0.2 + analyzer: ^5.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From d04df00691dd71acae9261e18d831a5f2b08b0df Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:29:28 +0300 Subject: [PATCH 02/35] feat: update xtz --- lib/services/coins/tezos/tezos_wallet.dart | 146 +++++++++++++++++---- lib/utilities/constants.dart | 1 + lib/utilities/default_nodes.dart | 2 +- 3 files changed, 123 insertions(+), 26 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index fca4eabc2..9048c1d04 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; import 'package:http/http.dart'; +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'; @@ -11,8 +14,10 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:tezart/tezart.dart'; +import 'package:tuple/tuple.dart'; import '../../../db/isar/main_db.dart'; import '../../../models/node_model.dart'; @@ -49,6 +54,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { 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; @@ -102,10 +111,36 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Balance? _balance; @override - Future confirmSend({required Map txData}) { - // TODO: implement confirmSend, - // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with - throw UnimplementedError(); + 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 node = getCurrentNode().host + getCurrentNode().port.toString(); + final int amountInMicroTez = ((int.parse((txData["recipientAmt"] as Amount).raw.toString()) * 1000000)).round(); + final int feeInMicroTez = int.parse(txData["fee"].toString()); + final String destinationAddress = txData["address"] as String; + final String sourceAddress = await currentReceivingAddress; + return Future.value(""); // TODO: return tx hash + } catch (e) { + Logging.instance.log(e.toString(), level: LogLevel.Error); + return Future.error(e); + } } @override @@ -114,13 +149,14 @@ 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((Keystore.fromMnemonic(mneString)).address); } @override Future estimateFeeFor(Amount amount, int feeRate) { - // TODO: implement estimateFeeFor - throw UnimplementedError(); + return Future.value( + Amount(rawValue: BigInt.parse(100000.toString()), fractionDigits: coin.decimals), + ); } @override @@ -130,13 +166,22 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); + Future get fees async { + // TODO: Change this to get fees from node and fix numberOfBlocks + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: 1000000, + medium: 100000, + slow: 10000, + ); + } @override Future fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { - // TODO: implement fullRescan - throw UnimplementedError(); + refresh(); + return Future.value(); } @override @@ -215,13 +260,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get mnemonicString => _secureStore.read(key: '${_walletId}_mnemonic'); - @override - Future> prepareSend({required String address, required Amount amount, Map? args}) { - // TODO: implement prepareSend - // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with - throw UnimplementedError(); - } - @override Future recoverFromMnemonic({required String mnemonic, String? mnemonicPassphrase, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { if ((await mnemonicString) != null || @@ -267,9 +305,66 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { await updateCachedBalance(_balance!); } + Future updateTransactions() async { + var api = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance_history"; // TODO: Can we use current node instead of this? + var returnedTxs = await get(Uri.parse(api)).then((value) => value.body); + Logging.instance.log( + "Transactions for ${await currentReceivingAddress}: $returnedTxs", + level: LogLevel.Info); + List> txs = []; + Object? jsonTxs = jsonDecode(returnedTxs); + if (jsonTxs == null) { + await db.addNewTransactionData(txs, walletId); + } else { + for (var tx in jsonTxs as List) { + var theTx = Transaction( + walletId: walletId, + txid: "", + timestamp: DateTime.parse(tx["timestamp"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.unknown, + subType: TransactionSubType.none, + amount: int.parse(tx["balance"].toString()), + amountString: Amount( + rawValue: BigInt.parse(tx["balance"].toString()), + fractionDigits: 6 + ).toJsonString(), + fee: 0, + height: int.parse(tx["level"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0 + ); + var theAddress = Address( + walletId: walletId, + value: await currentReceivingAddress, + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + txs.add(Tuple2(theTx, theAddress)); + } + await db.addNewTransactionData(txs, walletId); + } + } + + Future updateChainHeight() async { + var api = "https://api.tzkt.io/v1/blocks/count"; // TODO: Can we use current node instead of this? + var returnedHeight = await get(Uri.parse(api)).then((value) => value.body); + final int intHeight = int.parse(returnedHeight.toString()); + await updateCachedChainHeight(intHeight); + } + @override Future refresh() { + updateChainHeight(); updateBalance(); + updateTransactions(); return Future.value(); } @@ -277,16 +372,17 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { int get storedChainHeight => getCachedChainHeight(); @override - Future testNetworkConnection() { - // TODO: implement testNetworkConnection - throw UnimplementedError(); + Future testNetworkConnection() async{ + try { + await get(Uri.parse("https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance")); + return true; + } catch (e) { + return false; + } } @override - // TODO: implement transactions - Future> get transactions async { - // TODO: Maybe we can use this -> https://api.tzkt.io/#operation/Accounts_GetBalanceHistory - } + Future> get transactions => db.getTransactions(walletId).findAll(); @override Future updateNode(bool shouldRefresh) async { diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 2fdacc64b..b74de5b98 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -136,6 +136,7 @@ abstract class Constants { case Coin.ethereum: case Coin.namecoin: case Coin.particl: + case Coin.tezos: values.addAll([24, 21, 18, 15, 12]); break; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 8c48f5340..ad9310c3d 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -173,7 +173,7 @@ abstract class DefaultNodes { ); static NodeModel get tezos => NodeModel( // TODO: Change this to original one - host: "https://mainnet.api.tez.ie", + host: "mainnet.api.tez.ie", port: 443, name: defaultName, id: _nodeId(Coin.tezos), From 0ae747aebc48ade08b02ad2e9b22a1a809099b1a Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:03:32 +0300 Subject: [PATCH 03/35] feat: add xtz --- lib/models/isar/stack_theme.dart | 9 + .../add_edit_node_view.dart | 1 + lib/services/coins/coin_service.dart | 10 + lib/services/coins/tezos/tezos_wallet.dart | 314 ++++++++++++++++++ lib/themes/coin_icon_provider.dart | 2 + lib/themes/coin_image_provider.dart | 4 + lib/themes/color_theme.dart | 3 + lib/themes/stack_colors.dart | 2 + lib/utilities/address_utils.dart | 2 + lib/utilities/block_explorers.dart | 2 + lib/utilities/constants.dart | 14 + lib/utilities/default_nodes.dart | 18 +- lib/utilities/enums/coin_enum.dart | 21 ++ .../enums/derive_path_type_enum.dart | 1 + pubspec.lock | 260 +++++++++------ pubspec.yaml | 6 +- 16 files changed, 571 insertions(+), 98 deletions(-) create mode 100644 lib/services/coins/tezos/tezos_wallet.dart diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index 557090584..f6f7c351d 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -1921,6 +1921,7 @@ class ThemeAssets implements IThemeAssets { late final String wownero; late final String namecoin; late final String particl; + late final String tezos; late final String bitcoinImage; late final String bitcoincashImage; late final String dogecoinImage; @@ -1932,6 +1933,7 @@ class ThemeAssets implements IThemeAssets { late final String wowneroImage; late final String namecoinImage; late final String particlImage; + late final String tezosImage; late final String bitcoinImageSecondary; late final String bitcoincashImageSecondary; late final String dogecoinImageSecondary; @@ -1943,6 +1945,7 @@ class ThemeAssets implements IThemeAssets { late final String wowneroImageSecondary; late final String namecoinImageSecondary; late final String particlImageSecondary; + late final String tezosImageSecondary; @override late final String? loadingGif; @override @@ -1988,6 +1991,8 @@ class ThemeAssets implements IThemeAssets { ..wownero = "$themeId/assets/${json["wownero"] as String}" ..namecoin = "$themeId/assets/${json["namecoin"] as String}" ..particl = "$themeId/assets/${json["particl"] as String}" + ..tezos = + "$themeId/assets/${json["bitcoin"] as String}" ..bitcoinImage = "$themeId/assets/${json["bitcoin_image"] as String}" ..bitcoincashImage = "$themeId/assets/${json["bitcoincash_image"] as String}" @@ -2000,6 +2005,8 @@ class ThemeAssets implements IThemeAssets { ..wowneroImage = "$themeId/assets/${json["wownero_image"] as String}" ..namecoinImage = "$themeId/assets/${json["namecoin_image"] as String}" ..particlImage = "$themeId/assets/${json["particl_image"] as String}" + ..tezosImage = + "$themeId/assets/${json["bitcoin_image"] as String}" ..bitcoinImageSecondary = "$themeId/assets/${json["bitcoin_image_secondary"] as String}" ..bitcoincashImageSecondary = @@ -2022,6 +2029,8 @@ class ThemeAssets implements IThemeAssets { "$themeId/assets/${json["namecoin_image_secondary"] as String}" ..particlImageSecondary = "$themeId/assets/${json["particl_image_secondary"] as String}" + ..tezosImageSecondary = + "$themeId/assets/${json["bitcoin_image_secondary"] as String}" // TODO: change to tezos ..loadingGif = json["loading_gif"] is String ? "$themeId/assets/${json["loading_gif"] as String}" : null diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 3e5d3e3e4..c2543088d 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -727,6 +727,7 @@ class _NodeFormState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincash: case Coin.particl: + case Coin.tezos: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 00a52eba5..3c1542805 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -27,6 +27,7 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/nano/nano_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; +import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -218,6 +219,15 @@ abstract class CoinServiceAPI { cachedClient: cachedClient, tracker: tracker); + case Coin.tezos: + return TezosWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + tracker: tracker, + ); + case Coin.wownero: return WowneroWallet( walletId: walletId, diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart new file mode 100644 index 000000000..fca4eabc2 --- /dev/null +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -0,0 +1,314 @@ +import 'dart:async'; + +import 'package:http/http.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/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; + +import 'package:tezart/tezart.dart'; + +import '../../../db/isar/main_db.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; +import '../../mixins/wallet_cache.dart'; +import '../../mixins/wallet_db.dart'; +import '../../transaction_notification_tracker.dart'; + +const int MINIMUM_CONFIRMATIONS = 1; + +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); + } + + @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; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + } else { + refresh(); + } + } + } + + @override + Balance get balance => _balance ??= getCachedBalance(); + Balance? _balance; + + @override + Future confirmSend({required Map txData}) { + // TODO: implement confirmSend, + // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with + throw UnimplementedError(); + } + + @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) { + // TODO: implement estimateFeeFor + throw UnimplementedError(); + } + + @override + Future exit() { + _hasCalledExit = true; + return Future.value(); + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + @override + Future fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { + // TODO: implement fullRescan + throw UnimplementedError(); + } + + @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() async { + 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: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + + 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'); + + @override + Future> prepareSend({required String address, required Amount amount, Map? args}) { + // TODO: implement prepareSend + // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with + throw UnimplementedError(); + } + + @override + Future recoverFromMnemonic({required String mnemonic, String? mnemonicPassphrase, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { + 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 ?? "", + ); + + final address = Address( + walletId: walletId, + value: Keystore.fromMnemonic(mnemonic).address, + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + + await db.putAddress(address); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } + + Future updateBalance() async { + var api = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance"; // TODO: Can we use current node instead of this? + var theBalance = await get(Uri.parse(api)).then((value) => value.body); + Logging.instance.log("Balance for ${await currentReceivingAddress}: $theBalance", level: LogLevel.Info); + var balanceInAmount = Amount(rawValue: BigInt.parse(theBalance.toString()), fractionDigits: 6); + _balance = Balance( + total: balanceInAmount, + spendable: balanceInAmount, + blockedTotal: Amount(rawValue: BigInt.parse("0"), fractionDigits: 6), + pendingSpendable: Amount(rawValue: BigInt.parse("0"), fractionDigits: 6), + ); + await updateCachedBalance(_balance!); + } + + @override + Future refresh() { + updateBalance(); + return Future.value(); + } + + @override + int get storedChainHeight => getCachedChainHeight(); + + @override + Future testNetworkConnection() { + // TODO: implement testNetworkConnection + throw UnimplementedError(); + } + + @override + // TODO: implement transactions + Future> get transactions async { + // TODO: Maybe we can use this -> https://api.tzkt.io/#operation/Accounts_GetBalanceHistory + } + + @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) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } + + @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/themes/coin_icon_provider.dart b/lib/themes/coin_icon_provider.dart index 9bd3990bb..acbfaed3f 100644 --- a/lib/themes/coin_icon_provider.dart +++ b/lib/themes/coin_icon_provider.dart @@ -43,6 +43,8 @@ final coinIconProvider = Provider.family((ref, coin) { return assets.namecoin; case Coin.particl: return assets.particl; + case Coin.tezos: + return assets.tezos; case Coin.ethereum: return assets.ethereum; default: diff --git a/lib/themes/coin_image_provider.dart b/lib/themes/coin_image_provider.dart index 6ca839fb9..d72e7b778 100644 --- a/lib/themes/coin_image_provider.dart +++ b/lib/themes/coin_image_provider.dart @@ -41,6 +41,8 @@ final coinImageProvider = Provider.family((ref, coin) { return assets.namecoinImage; case Coin.particl: return assets.particlImage; + case Coin.tezos: + return assets.tezosImage; case Coin.bitcoinTestNet: return assets.bitcoinImage; case Coin.bitcoincashTestnet: @@ -89,6 +91,8 @@ final coinImageSecondaryProvider = Provider.family((ref, coin) { return assets.namecoinImageSecondary; case Coin.particl: return assets.particlImageSecondary; + case Coin.tezos: + return assets.tezosImageSecondary; case Coin.bitcoinTestNet: return assets.bitcoinImageSecondary; case Coin.bitcoincashTestnet: diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index bcfa45ac9..1ea9253a4 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -30,6 +30,7 @@ class CoinThemeColorDefault { Color get particl => const Color(0xFF8175BD); Color get nano => const Color(0xFF209CE9); Color get banano => const Color(0xFFFBDD11); + Color get tezos => const Color(0xFF0F61FF); Color forCoin(Coin coin) { switch (coin) { @@ -66,6 +67,8 @@ class CoinThemeColorDefault { return nano; case Coin.banano: return banano; + case Coin.tezos: + return tezos; } } } diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index b9e58a5ca..4c3362b95 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1711,6 +1711,8 @@ class StackColors extends ThemeExtension { return _coin.nano; case Coin.banano: return _coin.banano; + case Coin.tezos: + return _coin.tezos; } } diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 0093e1d00..77cff70ab 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -109,6 +109,8 @@ class AddressUtils { return NanoAccounts.isValid(NanoAccountType.NANO, address); case Coin.banano: return NanoAccounts.isValid(NanoAccountType.BANANO, address); + case Coin.tezos: + return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); case Coin.litecoinTestNet: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 7ff0cf349..bcca709e6 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -58,6 +58,8 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://www.nanolooker.com/block/$txid"); case Coin.banano: return Uri.parse("https://www.bananolooker.com/block/$txid"); + case Coin.tezos: + return Uri.parse("https://tzstats.com/$txid"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 2dd7be287..f23c1827a 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -44,6 +44,7 @@ abstract class Constants { static final BigInt _satsPerCoinBanano = BigInt.parse("100000000000000000000000000000"); // 1*10^29 static final BigInt _satsPerCoin = BigInt.from(100000000); + static final BigInt _satsPerCoinTezos = BigInt.from(1000000); static const int _decimalPlaces = 8; static const int _decimalPlacesNano = 30; static const int _decimalPlacesBanano = 29; @@ -51,6 +52,7 @@ abstract class Constants { static const int _decimalPlacesMonero = 12; static const int _decimalPlacesEthereum = 18; static const int _decimalPlacesECash = 2; + static const int _decimalPlacesTezos = 6; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -96,6 +98,9 @@ abstract class Constants { case Coin.eCash: return _satsPerCoinECash; + + case Coin.tezos: + return _satsPerCoinTezos; } } @@ -133,6 +138,9 @@ abstract class Constants { case Coin.eCash: return _decimalPlacesECash; + + case Coin.tezos: + return _decimalPlacesTezos; } } @@ -160,6 +168,8 @@ abstract class Constants { case Coin.banano: values.addAll([24, 12]); break; + case Coin.tezos: + values.addAll([24, 12]); case Coin.monero: values.addAll([25]); @@ -214,6 +224,9 @@ abstract class Constants { case Coin.nano: // TODO: Verify this case Coin.banano: // TODO: Verify this return 1; + + case Coin.tezos: + return 60; } } @@ -241,6 +254,7 @@ abstract class Constants { case Coin.nano: case Coin.banano: + case Coin.tezos: return 24; case Coin.monero: diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index c8ff94120..05e192f77 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -179,7 +179,20 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.particl.name, isFailover: true, - isDown: false); + isDown: false + ); + + static NodeModel get tezos => NodeModel( // TODO: Change this to original one + host: "https://mainnet.api.tez.ie", + port: 443, + name: defaultName, + id: _nodeId(Coin.tezos), + useSSL: true, + enabled: true, + coinName: Coin.tezos.name, + isFailover: true, + isDown: false + ); static NodeModel get nano => NodeModel( host: "https://rainstorm.city/api", @@ -300,12 +313,13 @@ abstract class DefaultNodes { case Coin.particl: return particl; - case Coin.nano: return nano; case Coin.banano: return banano; + case Coin.tezos: + return tezos; case Coin.bitcoinTestNet: return bitcoinTestnet; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index c656b2c62..f4a1eebbb 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -27,6 +27,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' import 'package:stackwallet/services/coins/nano/nano_wallet.dart' as nano; import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; +import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart' as tezos; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/utilities/constants.dart'; @@ -44,6 +45,7 @@ enum Coin { namecoin, nano, particl, + tezos, wownero, /// @@ -84,6 +86,8 @@ extension CoinExt on Coin { return "Monero"; case Coin.particl: return "Particl"; + case Coin.tezos: + return "Tezos"; case Coin.wownero: return "Wownero"; case Coin.namecoin: @@ -127,6 +131,8 @@ extension CoinExt on Coin { return "XMR"; case Coin.particl: return "PART"; + case Coin.tezos: + return "XTZ"; case Coin.wownero: return "WOW"; case Coin.namecoin: @@ -171,6 +177,8 @@ extension CoinExt on Coin { return "monero"; case Coin.particl: return "particl"; + case Coin.tezos: + return "tezos"; case Coin.wownero: return "wownero"; case Coin.namecoin: @@ -212,6 +220,7 @@ extension CoinExt on Coin { case Coin.epicCash: case Coin.ethereum: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.nano: case Coin.banano: @@ -234,6 +243,7 @@ extension CoinExt on Coin { case Coin.eCash: case Coin.epicCash: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.dogecoinTestNet: case Coin.bitcoinTestNet: @@ -258,6 +268,7 @@ extension CoinExt on Coin { case Coin.epicCash: case Coin.ethereum: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.nano: case Coin.banano: @@ -285,6 +296,7 @@ extension CoinExt on Coin { case Coin.epicCash: case Coin.ethereum: case Coin.monero: + case Coin.tezos: case Coin.wownero: case Coin.nano: case Coin.banano: @@ -345,6 +357,9 @@ extension CoinExt on Coin { case Coin.particl: return particl.MINIMUM_CONFIRMATIONS; + case Coin.tezos: + return tezos.MINIMUM_CONFIRMATIONS; + case Coin.wownero: return wow.MINIMUM_CONFIRMATIONS; @@ -404,6 +419,10 @@ Coin coinFromPrettyName(String name) { case "particl": return Coin.particl; + case "Tezos": + case "tezos": + return Coin.tezos; + case "Namecoin": case "namecoin": return Coin.namecoin; @@ -481,6 +500,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "part": return Coin.particl; + case "xtz": + return Coin.tezos; case "tltc": return Coin.litecoinTestNet; case "tbtc": diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index f3e7f86df..99d26e24c 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -49,6 +49,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.wownero: case Coin.nano: case Coin.banano: + case Coin.tezos: // TODO: Is this true? throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); } diff --git a/pubspec.lock b/pubspec.lock index 2dcd3c598..4bcb94925 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.30" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" archive: dependency: "direct main" description: @@ -37,18 +45,18 @@ packages: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: b74e3842a52c61f8819a1ec8444b4de5419b41a7465e69d4aa681445377398b0 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" async: dependency: "direct main" description: @@ -146,10 +154,10 @@ packages: dependency: transitive description: name: build - sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" build_config: dependency: transitive description: @@ -170,26 +178,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "220ae4553e50d7c21a17c051afc7b183d28a24a420502e842f303f8e4e6edced" + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.6" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "88a57f2ac99849362e73878334caa9f06ee25f31d2adced882b8337838c84e1e" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.9" + version: "7.2.10" built_collection: dependency: transitive description: @@ -202,10 +210,10 @@ packages: dependency: transitive description: name: built_value - sha256: "7dd62d9faf105c434f3d829bbe9c4be02ec67f5ed94832222116122df67c5452" + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" url: "https://pub.dev" source: hosted - version: "8.6.0" + version: "8.6.1" characters: dependency: transitive description: @@ -242,10 +250,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" collection: dependency: transitive description: @@ -298,10 +306,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cryptography: dependency: transitive description: @@ -314,10 +322,10 @@ packages: dependency: transitive description: name: csslib - sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" url: "https://pub.dev" source: hosted - version: "0.17.3" + version: "1.0.0" cw_core: dependency: "direct main" description: @@ -382,18 +390,18 @@ packages: dependency: transitive description: name: dart_style - sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" dartx: dependency: transitive description: name: dartx - sha256: "45d7176701f16c5a5e00a4798791c1964bc231491b879369c818dd9a9c764871" + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" dbus: dependency: transitive description: @@ -406,10 +414,10 @@ packages: dependency: "direct main" description: name: decimal - sha256: eece91944f523657c75a3a008a90ec7f7eb3986191153a78570c7d0ac8ef3d01 + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" dependency_validator: dependency: "direct dev" description: @@ -450,14 +458,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + dio: + dependency: transitive + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" dropdown_button2: dependency: "direct main" description: name: dropdown_button2 - sha256: "374f2390161bf782b4896f0b1b24cbb2b5daaa1cfb11047c3307461dcdf44e07" + sha256: "83c54a5022f898d63e3abe21240b64b937e676103207287e6705d3f9bb04d654" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.6" eip1559: dependency: transitive description: @@ -542,10 +558,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" + sha256: b1729fc96627dd44012d0a901558177418818d6bd428df59dcfeb594e5f66432 url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.3.2" fixnum: dependency: transitive description: @@ -606,10 +622,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_local_notifications: dependency: "direct main" description: @@ -670,10 +686,10 @@ packages: dependency: "direct main" description: name: flutter_rounded_date_picker - sha256: e7143cc5cbf3aec1536286653e38b0809abc99fb76c91bd910dbd98ae003d890 + sha256: e6aa2dc5d3b44e8bbe85ef901be69eac59ba4136427f11f4c8b2a303e1e774e7 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" flutter_secure_storage: dependency: "direct main" description: @@ -821,10 +837,10 @@ packages: dependency: transitive description: name: html - sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" url: "https://pub.dev" source: hosted - version: "0.15.3" + version: "0.15.4" http: dependency: "direct main" description: @@ -934,6 +950,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + json_serializable: + dependency: transitive + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" keyboard_dismisser: dependency: "direct main" description: @@ -953,10 +977,10 @@ packages: dependency: transitive description: name: lints - sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" local_auth: dependency: "direct main" description: @@ -977,10 +1001,10 @@ packages: dependency: "direct main" description: name: lottie - sha256: "23522951540d20a57a60202ed7022e6376bed206a4eee1c347a91f58bd57eb9f" + sha256: "0793a5866062e5cc8a8b24892fa94c3095953ea914a7fdf790f550dd7537fe60" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.0" matcher: dependency: transitive description: @@ -997,6 +1021,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + memoize: + dependency: transitive + description: + name: memoize + sha256: "51481d328c86cbdc59711369179bac88551ca0556569249be5317e66fc796cac" + url: "https://pub.dev" + source: hosted + version: "3.0.0" meta: dependency: transitive description: @@ -1033,10 +1065,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "8b46d7eb40abdda92d62edd01546051f0c27365e65608c284de336dccfef88cc" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "5.4.2" mocktail: dependency: transitive description: @@ -1145,10 +1177,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" path_provider_linux: dependency: transitive description: @@ -1169,50 +1201,50 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.4.3" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d8cc6a62ded6d0f49c6eac337e080b066ee3bce4d405bd9439a61e1f1927bfe8 + sha256: c0c9754479a4c4b1c1f3862ddc11930c9b3f03bef2816bb4ea6eed1e13551d6f url: "https://pub.dev" source: hosted - version: "10.2.1" + version: "10.3.2" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" url: "https://pub.dev" source: hosted - version: "9.0.8" + version: "9.1.4" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.11.3" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" petitparser: dependency: transitive description: @@ -1221,6 +1253,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.0" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: e5fb0bce1717b7f136f35ee98b5c02b3e6383211f8a77ca882fa7812232a07b9 + url: "https://pub.dev" + source: hosted + version: "0.3.4" platform: dependency: transitive description: @@ -1253,6 +1293,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + pretty_dio_logger: + dependency: transitive + description: + name: pretty_dio_logger + sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" + url: "https://pub.dev" + source: hosted + version: "1.2.0-beta-1" process: dependency: transitive description: @@ -1301,6 +1349,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" rational: dependency: transitive description: @@ -1309,6 +1365,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" riverpod: dependency: transitive description: @@ -1353,58 +1417,58 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb + sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shelf: dependency: transitive description: @@ -1446,18 +1510,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_map_stack_trace: dependency: transitive description: @@ -1579,6 +1643,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + tezart: + dependency: "direct main" + description: + name: tezart + sha256: "35d526f2e6ca250c64461ebfb4fa9f64b6599fab8c4242c8e89ae27d4ac2e15a" + url: "https://pub.dev" + source: hosted + version: "2.0.5" time: dependency: transitive description: @@ -1615,10 +1687,10 @@ packages: dependency: "direct main" description: name: tuple - sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" typed_data: dependency: transitive description: @@ -1631,26 +1703,26 @@ packages: dependency: transitive description: name: universal_io - sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.1.12" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 + sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" url: "https://pub.dev" source: hosted - version: "6.0.35" + version: "6.0.36" url_launcher_ios: dependency: transitive description: @@ -1671,34 +1743,34 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" uuid: dependency: "direct main" description: @@ -1824,18 +1896,18 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.5" win32_registry: dependency: transitive description: name: win32_registry - sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" window_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fd5eac1dc..000ecd8a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,6 +138,7 @@ dependencies: desktop_drop: ^0.4.1 nanodart: ^2.0.0 basic_utils: ^5.5.4 + tezart: ^2.0.5 dev_dependencies: flutter_test: @@ -196,9 +197,10 @@ dependency_overrides: bip39: git: url: https://github.com/cypherstack/stack-bip39.git + ref: 0cd6d54e2860bea68fc50c801cb9db2a760192fb - - + crypto: ^3.0.2 + analyzer: ^5.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From d387fcc7b9a916e3c3ef74cdefa3ab7357f6e334 Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:29:28 +0300 Subject: [PATCH 04/35] feat: update xtz --- lib/services/coins/tezos/tezos_wallet.dart | 146 +++++++++++++++++---- lib/utilities/constants.dart | 1 - lib/utilities/default_nodes.dart | 2 +- 3 files changed, 122 insertions(+), 27 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index fca4eabc2..9048c1d04 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; import 'package:http/http.dart'; +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'; @@ -11,8 +14,10 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:tezart/tezart.dart'; +import 'package:tuple/tuple.dart'; import '../../../db/isar/main_db.dart'; import '../../../models/node_model.dart'; @@ -49,6 +54,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { 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; @@ -102,10 +111,36 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Balance? _balance; @override - Future confirmSend({required Map txData}) { - // TODO: implement confirmSend, - // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with - throw UnimplementedError(); + 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 node = getCurrentNode().host + getCurrentNode().port.toString(); + final int amountInMicroTez = ((int.parse((txData["recipientAmt"] as Amount).raw.toString()) * 1000000)).round(); + final int feeInMicroTez = int.parse(txData["fee"].toString()); + final String destinationAddress = txData["address"] as String; + final String sourceAddress = await currentReceivingAddress; + return Future.value(""); // TODO: return tx hash + } catch (e) { + Logging.instance.log(e.toString(), level: LogLevel.Error); + return Future.error(e); + } } @override @@ -114,13 +149,14 @@ 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((Keystore.fromMnemonic(mneString)).address); } @override Future estimateFeeFor(Amount amount, int feeRate) { - // TODO: implement estimateFeeFor - throw UnimplementedError(); + return Future.value( + Amount(rawValue: BigInt.parse(100000.toString()), fractionDigits: coin.decimals), + ); } @override @@ -130,13 +166,22 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); + Future get fees async { + // TODO: Change this to get fees from node and fix numberOfBlocks + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: 1000000, + medium: 100000, + slow: 10000, + ); + } @override Future fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { - // TODO: implement fullRescan - throw UnimplementedError(); + refresh(); + return Future.value(); } @override @@ -215,13 +260,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get mnemonicString => _secureStore.read(key: '${_walletId}_mnemonic'); - @override - Future> prepareSend({required String address, required Amount amount, Map? args}) { - // TODO: implement prepareSend - // NOTE FROM DETHERMINAL: I couldnt write this function because I dont have any tezos to test with - throw UnimplementedError(); - } - @override Future recoverFromMnemonic({required String mnemonic, String? mnemonicPassphrase, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { if ((await mnemonicString) != null || @@ -267,9 +305,66 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { await updateCachedBalance(_balance!); } + Future updateTransactions() async { + var api = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance_history"; // TODO: Can we use current node instead of this? + var returnedTxs = await get(Uri.parse(api)).then((value) => value.body); + Logging.instance.log( + "Transactions for ${await currentReceivingAddress}: $returnedTxs", + level: LogLevel.Info); + List> txs = []; + Object? jsonTxs = jsonDecode(returnedTxs); + if (jsonTxs == null) { + await db.addNewTransactionData(txs, walletId); + } else { + for (var tx in jsonTxs as List) { + var theTx = Transaction( + walletId: walletId, + txid: "", + timestamp: DateTime.parse(tx["timestamp"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.unknown, + subType: TransactionSubType.none, + amount: int.parse(tx["balance"].toString()), + amountString: Amount( + rawValue: BigInt.parse(tx["balance"].toString()), + fractionDigits: 6 + ).toJsonString(), + fee: 0, + height: int.parse(tx["level"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0 + ); + var theAddress = Address( + walletId: walletId, + value: await currentReceivingAddress, + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + txs.add(Tuple2(theTx, theAddress)); + } + await db.addNewTransactionData(txs, walletId); + } + } + + Future updateChainHeight() async { + var api = "https://api.tzkt.io/v1/blocks/count"; // TODO: Can we use current node instead of this? + var returnedHeight = await get(Uri.parse(api)).then((value) => value.body); + final int intHeight = int.parse(returnedHeight.toString()); + await updateCachedChainHeight(intHeight); + } + @override Future refresh() { + updateChainHeight(); updateBalance(); + updateTransactions(); return Future.value(); } @@ -277,16 +372,17 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { int get storedChainHeight => getCachedChainHeight(); @override - Future testNetworkConnection() { - // TODO: implement testNetworkConnection - throw UnimplementedError(); + Future testNetworkConnection() async{ + try { + await get(Uri.parse("https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance")); + return true; + } catch (e) { + return false; + } } @override - // TODO: implement transactions - Future> get transactions async { - // TODO: Maybe we can use this -> https://api.tzkt.io/#operation/Accounts_GetBalanceHistory - } + Future> get transactions => db.getTransactions(walletId).findAll(); @override Future updateNode(bool shouldRefresh) async { diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index f23c1827a..1a285cc41 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -167,7 +167,6 @@ abstract class Constants { break; case Coin.banano: values.addAll([24, 12]); - break; case Coin.tezos: values.addAll([24, 12]); diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 05e192f77..ac2edb482 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -183,7 +183,7 @@ abstract class DefaultNodes { ); static NodeModel get tezos => NodeModel( // TODO: Change this to original one - host: "https://mainnet.api.tez.ie", + host: "mainnet.api.tez.ie", port: 443, name: defaultName, id: _nodeId(Coin.tezos), From 45af21dc270a2814a4ca953d76dac1adcbdaed53 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 19 Jul 2023 16:37:54 +0200 Subject: [PATCH 05/35] Fix merge conflicts with staging --- .../manage_nodes_views/add_edit_node_view.dart | 1 + .../manage_nodes_views/node_details_view.dart | 1 + lib/services/coins/tezos/tezos_wallet.dart | 3 ++- lib/utilities/amount/amount_unit.dart | 2 ++ lib/widgets/node_card.dart | 1 + lib/widgets/node_options_sheet.dart | 1 + 6 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index c2543088d..5702c9740 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -196,6 +196,7 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.nano: case Coin.banano: + case Coin.tezos: //TODO: check network/node } diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index fa69871cb..3e10b9240 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -172,6 +172,7 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.nano: case Coin.banano: + case Coin.tezos: //TODO: check network/node } diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 9048c1d04..e5576741c 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -336,7 +336,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { otherData: "", inputs: [], outputs: [], - nonce: 0 + nonce: 0, + numberOfMessages: null, ); var theAddress = Address( walletId: walletId, diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 0c0600d7a..fd2366dd4 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -50,6 +50,7 @@ enum AmountUnit { case Coin.dogecoin: case Coin.eCash: case Coin.epicCash: + case Coin.tezos: return AmountUnit.values.sublist(0, 4); case Coin.monero: @@ -62,6 +63,7 @@ enum AmountUnit { case Coin.nano: case Coin.banano: return AmountUnit.values; + } } } diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 9214e3e0a..3f4384ffc 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -194,6 +194,7 @@ class _NodeCardState extends ConsumerState { case Coin.nano: case Coin.banano: + case Coin.tezos: //TODO: check network/node } diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 16e87581c..faebd95e3 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -177,6 +177,7 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.nano: case Coin.banano: + case Coin.tezos: //TODO: check network/node } From c8d0ecf28dc10ee2f63dcc47c84e205acf0daf5b Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Thu, 27 Jul 2023 11:11:11 -0600 Subject: [PATCH 06/35] remove changes to ThemeAssets v1 --- lib/models/isar/stack_theme.dart | 9 --------- lib/themes/coin_image_provider.dart | 4 ---- 2 files changed, 13 deletions(-) diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index ea449a497..32878366a 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -1921,7 +1921,6 @@ class ThemeAssets implements IThemeAssets { late final String wownero; late final String namecoin; late final String particl; - late final String tezos; late final String bitcoinImage; late final String bitcoincashImage; late final String dogecoinImage; @@ -1933,7 +1932,6 @@ class ThemeAssets implements IThemeAssets { late final String wowneroImage; late final String namecoinImage; late final String particlImage; - late final String tezosImage; late final String bitcoinImageSecondary; late final String bitcoincashImageSecondary; late final String dogecoinImageSecondary; @@ -1945,7 +1943,6 @@ class ThemeAssets implements IThemeAssets { late final String wowneroImageSecondary; late final String namecoinImageSecondary; late final String particlImageSecondary; - late final String tezosImageSecondary; @override late final String? loadingGif; @override @@ -1991,8 +1988,6 @@ class ThemeAssets implements IThemeAssets { ..wownero = "$themeId/assets/${json["wownero"] as String}" ..namecoin = "$themeId/assets/${json["namecoin"] as String}" ..particl = "$themeId/assets/${json["particl"] as String}" - ..tezos = - "$themeId/assets/${json["bitcoin"] as String}" ..bitcoinImage = "$themeId/assets/${json["bitcoin_image"] as String}" ..bitcoincashImage = "$themeId/assets/${json["bitcoincash_image"] as String}" @@ -2005,8 +2000,6 @@ class ThemeAssets implements IThemeAssets { ..wowneroImage = "$themeId/assets/${json["wownero_image"] as String}" ..namecoinImage = "$themeId/assets/${json["namecoin_image"] as String}" ..particlImage = "$themeId/assets/${json["particl_image"] as String}" - ..tezosImage = - "$themeId/assets/${json["bitcoin_image"] as String}" ..bitcoinImageSecondary = "$themeId/assets/${json["bitcoin_image_secondary"] as String}" ..bitcoincashImageSecondary = @@ -2029,8 +2022,6 @@ class ThemeAssets implements IThemeAssets { "$themeId/assets/${json["namecoin_image_secondary"] as String}" ..particlImageSecondary = "$themeId/assets/${json["particl_image_secondary"] as String}" - ..tezosImageSecondary = - "$themeId/assets/${json["bitcoin_image_secondary"] as String}" // TODO: change to tezos ..loadingGif = json["loading_gif"] is String ? "$themeId/assets/${json["loading_gif"] as String}" : null diff --git a/lib/themes/coin_image_provider.dart b/lib/themes/coin_image_provider.dart index d72e7b778..6ca839fb9 100644 --- a/lib/themes/coin_image_provider.dart +++ b/lib/themes/coin_image_provider.dart @@ -41,8 +41,6 @@ final coinImageProvider = Provider.family((ref, coin) { return assets.namecoinImage; case Coin.particl: return assets.particlImage; - case Coin.tezos: - return assets.tezosImage; case Coin.bitcoinTestNet: return assets.bitcoinImage; case Coin.bitcoincashTestnet: @@ -91,8 +89,6 @@ final coinImageSecondaryProvider = Provider.family((ref, coin) { return assets.namecoinImageSecondary; case Coin.particl: return assets.particlImageSecondary; - case Coin.tezos: - return assets.tezosImageSecondary; case Coin.bitcoinTestNet: return assets.bitcoinImageSecondary; case Coin.bitcoincashTestnet: From 2ee65b584ea21d59f44d01884afb2cf9fe296068 Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Thu, 27 Jul 2023 23:15:49 +0300 Subject: [PATCH 07/35] fix: fetch from nodes instead of tzkt --- lib/services/coins/tezos/tezos_wallet.dart | 13 +++++++------ lib/utilities/default_nodes.dart | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index e5576741c..38631a071 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -292,8 +292,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateBalance() async { - var api = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance"; // TODO: Can we use current node instead of this? - var theBalance = await get(Uri.parse(api)).then((value) => value.body); + var api = "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; + var theBalance = (await get(Uri.parse(api)).then((value) => value.body)).substring(1, (await get(Uri.parse(api)).then((value) => value.body)).length - 2); Logging.instance.log("Balance for ${await currentReceivingAddress}: $theBalance", level: LogLevel.Info); var balanceInAmount = Amount(rawValue: BigInt.parse(theBalance.toString()), fractionDigits: 6); _balance = Balance( @@ -355,9 +355,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateChainHeight() async { - var api = "https://api.tzkt.io/v1/blocks/count"; // TODO: Can we use current node instead of this? - var returnedHeight = await get(Uri.parse(api)).then((value) => value.body); - final int intHeight = int.parse(returnedHeight.toString()); + var api = "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"; + var jsonParsedResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + final int intHeight = int.parse(jsonParsedResponse["level"].toString()); + Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); await updateCachedChainHeight(intHeight); } @@ -375,7 +376,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future testNetworkConnection() async{ try { - await get(Uri.parse("https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance")); + await get(Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell")); return true; } catch (e) { return false; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index ac2edb482..22f274d62 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -182,8 +182,8 @@ abstract class DefaultNodes { isDown: false ); - static NodeModel get tezos => NodeModel( // TODO: Change this to original one - host: "mainnet.api.tez.ie", + static NodeModel get tezos => NodeModel( // TODO: Change this to stack wallet one + host: "https://mainnet.api.tez.ie", port: 443, name: defaultName, id: _nodeId(Coin.tezos), From 3077465e81e017c524b61b1ea9afcda3c1bb01fc Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:39:07 +0300 Subject: [PATCH 08/35] xtz: update api and add price --- lib/services/coins/tezos/tezos_wallet.dart | 87 +++++++++++----------- lib/services/price.dart | 2 +- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 38631a071..8faa15427 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -306,52 +306,53 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateTransactions() async { - var api = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/balance_history"; // TODO: Can we use current node instead of this? - var returnedTxs = await get(Uri.parse(api)).then((value) => value.body); - Logging.instance.log( - "Transactions for ${await currentReceivingAddress}: $returnedTxs", - level: LogLevel.Info); + // TODO: Use node RPC instead of tzstats API + var api = "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; + var jsonResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); List> txs = []; - Object? jsonTxs = jsonDecode(returnedTxs); - if (jsonTxs == null) { - await db.addNewTransactionData(txs, walletId); - } else { - for (var tx in jsonTxs as List) { - var theTx = Transaction( - walletId: walletId, - txid: "", - timestamp: DateTime.parse(tx["timestamp"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, - type: TransactionType.unknown, - subType: TransactionSubType.none, - amount: int.parse(tx["balance"].toString()), - amountString: Amount( - rawValue: BigInt.parse(tx["balance"].toString()), - fractionDigits: 6 - ).toJsonString(), - fee: 0, - height: int.parse(tx["level"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - var theAddress = Address( - walletId: walletId, - value: await currentReceivingAddress, - publicKey: [], // TODO: Add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.unknown, - ); - txs.add(Tuple2(theTx, theAddress)); + for (var tx in jsonResponse as List) { + var txApi = "https://api.tzstats.com/explorer/op/${tx["hash"]}"; + var txJsonResponse = jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body))[0]; + TransactionType txType; + if (txJsonResponse["sender"] == (await currentReceivingAddress)) { + txType = TransactionType.outgoing; + } else { + txType = TransactionType.incoming; } - await db.addNewTransactionData(txs, walletId); + var theTx = Transaction( + walletId: walletId, + txid: txJsonResponse["hash"].toString(), + timestamp: DateTime.parse(txJsonResponse["time"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, + type: txType, + subType: TransactionSubType.none, + amount: (float.parse(txJsonResponse["volume"].toString()) * 1000000).toInt(), + amountString: Amount( + rawValue: BigInt.parse((float.parse(txJsonResponse["volume"].toString()) * 1000000).toString()), + fractionDigits: 6 + ).toJsonString(), + fee: (float.parse(txJsonResponse["fee"].toString()) * 1000000).toInt(), + height: int.parse(txJsonResponse["height"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, + ); + var theAddress = Address( + walletId: walletId, + value: txJsonResponse["receiver"].toString(), + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + txs.add(Tuple2(theTx, theAddress)); } + await db.addNewTransactionData(txs, walletId); } Future updateChainHeight() async { diff --git a/lib/services/price.dart b/lib/services/price.dart index 24d929062..e9cbacc96 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -100,7 +100,7 @@ class PriceAPI { Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency" "=${baseCurrency.toLowerCase()}" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," - "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano" + "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,tezos" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( From 5c06d9dc596288a8166fca43e2aac25d25cbcd88 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Fri, 28 Jul 2023 10:32:50 -0600 Subject: [PATCH 09/35] format adjustments + remove tezos from themeAssetsv1 --- lib/services/coins/tezos/tezos_wallet.dart | 202 ++++++++++++--------- lib/themes/coin_icon_provider.dart | 2 - lib/utilities/default_nodes.dart | 10 +- 3 files changed, 126 insertions(+), 88 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 8faa15427..4f66df22e 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,33 +1,30 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'package:http/http.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/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/enums/coin_enum.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:tuple/tuple.dart'; -import '../../../db/isar/main_db.dart'; -import '../../../models/node_model.dart'; -import '../../../utilities/flutter_secure_storage_interface.dart'; -import '../../../utilities/logger.dart'; -import '../../../utilities/prefs.dart'; -import '../../mixins/wallet_cache.dart'; -import '../../mixins/wallet_db.dart'; -import '../../transaction_notification_tracker.dart'; - const int MINIMUM_CONFIRMATIONS = 1; class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @@ -51,7 +48,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { NodeModel? _xtzNode; NodeModel getCurrentNode() { - return _xtzNode ?? NodeService(secureStorageInterface: _secureStore).getPrimaryNodeFor(coin: Coin.tezos) ?? DefaultNodes.getNodeFor(Coin.tezos); + return _xtzNode ?? + NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: Coin.tezos) ?? + DefaultNodes.getNodeFor(Coin.tezos); } Future getKeystore() async { @@ -111,12 +111,18 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Balance? _balance; @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()); + var fee = int.parse((await estimateFeeFor( + amount, (args!["feeRate"] as FeeRateType).index)) + .raw + .toString()); Map txData = { "fee": fee, "address": address, @@ -132,7 +138,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future confirmSend({required Map txData}) async { try { final node = getCurrentNode().host + getCurrentNode().port.toString(); - final int amountInMicroTez = ((int.parse((txData["recipientAmt"] as Amount).raw.toString()) * 1000000)).round(); + final int amountInMicroTez = + ((int.parse((txData["recipientAmt"] as Amount).raw.toString()) * + 1000000)) + .round(); final int feeInMicroTez = int.parse(txData["fee"].toString()); final String destinationAddress = txData["address"] as String; final String sourceAddress = await currentReceivingAddress; @@ -155,7 +164,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future estimateFeeFor(Amount amount, int feeRate) { return Future.value( - Amount(rawValue: BigInt.parse(100000.toString()), fractionDigits: coin.decimals), + Amount( + rawValue: BigInt.parse(100000.toString()), + fractionDigits: coin.decimals), ); } @@ -169,17 +180,18 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future get fees async { // TODO: Change this to get fees from node and fix numberOfBlocks return FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 1, - numberOfBlocksSlow: 1, - fast: 1000000, - medium: 100000, - slow: 10000, + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: 1000000, + medium: 100000, + slow: 10000, ); } @override - Future fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { + Future fullRescan( + int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { refresh(); return Future.value(); } @@ -212,13 +224,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { ); final address = Address( - walletId: walletId, - value: newKeystore.address, - publicKey: [], // TODO: Add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.unknown, + walletId: walletId, + value: newKeystore.address, + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, ); await db.putAddress(address); @@ -255,13 +267,20 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future get mnemonicPassphrase => _secureStore.read(key: '${_walletId}_mnemonicPassphrase'); + Future get mnemonicPassphrase => + _secureStore.read(key: '${_walletId}_mnemonicPassphrase'); @override - Future get mnemonicString => _secureStore.read(key: '${_walletId}_mnemonic'); + Future get mnemonicString => + _secureStore.read(key: '${_walletId}_mnemonic'); @override - Future recoverFromMnemonic({required String mnemonic, String? mnemonicPassphrase, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { + Future recoverFromMnemonic( + {required String mnemonic, + String? mnemonicPassphrase, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height}) async { if ((await mnemonicString) != null || (await this.mnemonicPassphrase) != null) { throw Exception("Attempted to overwrite mnemonic on restore!"); @@ -274,13 +293,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { ); final address = Address( - walletId: walletId, - value: Keystore.fromMnemonic(mnemonic).address, - publicKey: [], // TODO: Add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.unknown, + walletId: walletId, + value: Keystore.fromMnemonic(mnemonic).address, + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, ); await db.putAddress(address); @@ -292,10 +311,16 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateBalance() async { - var api = "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; - var theBalance = (await get(Uri.parse(api)).then((value) => value.body)).substring(1, (await get(Uri.parse(api)).then((value) => value.body)).length - 2); - Logging.instance.log("Balance for ${await currentReceivingAddress}: $theBalance", level: LogLevel.Info); - var balanceInAmount = Amount(rawValue: BigInt.parse(theBalance.toString()), fractionDigits: 6); + var api = + "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; + var theBalance = (await get(Uri.parse(api)).then((value) => value.body)) + .substring(1, + (await get(Uri.parse(api)).then((value) => value.body)).length - 2); + Logging.instance.log( + "Balance for ${await currentReceivingAddress}: $theBalance", + level: LogLevel.Info); + var balanceInAmount = Amount( + rawValue: BigInt.parse(theBalance.toString()), fractionDigits: 6); _balance = Balance( total: balanceInAmount, spendable: balanceInAmount, @@ -307,12 +332,15 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateTransactions() async { // TODO: Use node RPC instead of tzstats API - var api = "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; - var jsonResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + var api = + "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; + var jsonResponse = + jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); List> txs = []; for (var tx in jsonResponse as List) { var txApi = "https://api.tzstats.com/explorer/op/${tx["hash"]}"; - var txJsonResponse = jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body))[0]; + var txJsonResponse = jsonDecode( + await get(Uri.parse(txApi)).then((value) => value.body))[0]; TransactionType txType; if (txJsonResponse["sender"] == (await currentReceivingAddress)) { txType = TransactionType.outgoing; @@ -320,35 +348,41 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { txType = TransactionType.incoming; } var theTx = Transaction( - walletId: walletId, - txid: txJsonResponse["hash"].toString(), - timestamp: DateTime.parse(txJsonResponse["time"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, - type: txType, - subType: TransactionSubType.none, - amount: (float.parse(txJsonResponse["volume"].toString()) * 1000000).toInt(), - amountString: Amount( - rawValue: BigInt.parse((float.parse(txJsonResponse["volume"].toString()) * 1000000).toString()), - fractionDigits: 6 - ).toJsonString(), - fee: (float.parse(txJsonResponse["fee"].toString()) * 1000000).toInt(), - height: int.parse(txJsonResponse["height"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, + walletId: walletId, + txid: txJsonResponse["hash"].toString(), + timestamp: DateTime.parse(txJsonResponse["time"].toString()) + .toUtc() + .millisecondsSinceEpoch ~/ + 1000, + type: txType, + subType: TransactionSubType.none, + amount: (float.parse(txJsonResponse["volume"].toString()) * 1000000) + .toInt(), + amountString: Amount( + rawValue: BigInt.parse( + (float.parse(txJsonResponse["volume"].toString()) * 1000000) + .toString()), + fractionDigits: 6) + .toJsonString(), + fee: (float.parse(txJsonResponse["fee"].toString()) * 1000000).toInt(), + height: int.parse(txJsonResponse["height"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, ); var theAddress = Address( - walletId: walletId, - value: txJsonResponse["receiver"].toString(), - publicKey: [], // TODO: Add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.unknown, + walletId: walletId, + value: txJsonResponse["receiver"].toString(), + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, ); txs.add(Tuple2(theTx, theAddress)); } @@ -356,8 +390,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateChainHeight() async { - var api = "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"; - var jsonParsedResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + var api = + "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"; + var jsonParsedResponse = + jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); final int intHeight = int.parse(jsonParsedResponse["level"].toString()); Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); await updateCachedChainHeight(intHeight); @@ -375,9 +411,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { int get storedChainHeight => getCachedChainHeight(); @override - Future testNetworkConnection() async{ + Future testNetworkConnection() async { try { - await get(Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell")); + await get(Uri.parse( + "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell")); return true; } catch (e) { return false; @@ -385,11 +422,14 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future> get transactions => db.getTransactions(walletId).findAll(); + Future> get transactions => + db.getTransactions(walletId).findAll(); @override Future updateNode(bool shouldRefresh) async { - _xtzNode = NodeService(secureStorageInterface: _secureStore).getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); + _xtzNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); if (shouldRefresh) { await refresh(); diff --git a/lib/themes/coin_icon_provider.dart b/lib/themes/coin_icon_provider.dart index acbfaed3f..9bd3990bb 100644 --- a/lib/themes/coin_icon_provider.dart +++ b/lib/themes/coin_icon_provider.dart @@ -43,8 +43,6 @@ final coinIconProvider = Provider.family((ref, coin) { return assets.namecoin; case Coin.particl: return assets.particl; - case Coin.tezos: - return assets.tezos; case Coin.ethereum: return assets.ethereum; default: diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 22f274d62..87b7849f4 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -179,10 +179,10 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.particl.name, isFailover: true, - isDown: false - ); + isDown: false); - static NodeModel get tezos => NodeModel( // TODO: Change this to stack wallet one + static NodeModel get tezos => NodeModel( + // TODO: Change this to stack wallet one host: "https://mainnet.api.tez.ie", port: 443, name: defaultName, @@ -191,8 +191,7 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.tezos.name, isFailover: true, - isDown: false - ); + isDown: false); static NodeModel get nano => NodeModel( host: "https://rainstorm.city/api", @@ -318,6 +317,7 @@ abstract class DefaultNodes { case Coin.banano: return banano; + case Coin.tezos: return tezos; From 724f6193d7f95e4f19eea89166af0f414d9a21b8 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Fri, 28 Jul 2023 10:50:05 -0600 Subject: [PATCH 10/35] merge conflicts --- .../manage_nodes_views/add_edit_node_view.dart | 2 +- .../manage_nodes_views/node_details_view.dart | 2 +- lib/services/price.dart | 12 ++++++------ lib/utilities/constants.dart | 3 ++- lib/widgets/node_options_sheet.dart | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index eb100b8cd..781a566ef 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -199,7 +199,7 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.stellar: case Coin.stellarTestnet: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node case Coin.tezos: //TODO: check network/node } diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index ee9cf02e5..31a9c6717 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -176,7 +176,7 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.stellar: case Coin.stellarTestnet: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node } if (testPassed) { diff --git a/lib/services/price.dart b/lib/services/price.dart index 38f560244..c6f28673f 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -96,12 +96,12 @@ class PriceAPI { } Map> result = {}; try { - final uri = - Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency" - "=${baseCurrency.toLowerCase()}" - "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," - "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos" - "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); + final uri = Uri.parse( + "https://api.coingecko.com/api/v3/coins/markets?vs_currency" + "=${baseCurrency.toLowerCase()}" + "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( uri, diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index b1e2e6b31..1253e08f1 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -42,7 +42,8 @@ abstract class Constants { BigInt.parse("1000000000000000000000000000000"); // 1*10^30 static final BigInt _satsPerCoinBanano = BigInt.parse("100000000000000000000000000000"); // 1*10^29 - static final BigInt _satsPerCoinStellar = BigInt.from(10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision + static final BigInt _satsPerCoinStellar = BigInt.from( + 10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision static final BigInt _satsPerCoin = BigInt.from(100000000); static final BigInt _satsPerCoinTezos = BigInt.from(1000000); static const int _decimalPlaces = 8; diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 9f7802536..953ac78a1 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -181,7 +181,7 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.stellar: case Coin.stellarTestnet: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node } if (testPassed) { From 68f3bc8731e215d906ce2ad57dfca5814f86275e Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Fri, 28 Jul 2023 22:15:34 +0300 Subject: [PATCH 11/35] fix dependency conflict --- pubspec.lock | 72 ++++++++++++++++++++++++++++++++++++++++++++++------ pubspec.yaml | 2 ++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index cfdf7a710..111b58d75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.30" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" archive: dependency: "direct main" description: @@ -298,10 +306,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.2" cryptography: dependency: transitive description: @@ -451,13 +459,13 @@ packages: source: hosted version: "0.6.0" dio: - dependency: transitive + dependency: "direct overridden" description: name: dio - sha256: "3866d67f93523161b643187af65f5ac08bc991a5bcdaf41a2d587fe4ccb49993" + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "4.0.6" dropdown_button2: dependency: "direct main" description: @@ -942,6 +950,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + json_serializable: + dependency: transitive + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" keyboard_dismisser: dependency: "direct main" description: @@ -1005,6 +1021,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + memoize: + dependency: transitive + description: + name: memoize + sha256: "51481d328c86cbdc59711369179bac88551ca0556569249be5317e66fc796cac" + url: "https://pub.dev" + source: hosted + version: "3.0.0" meta: dependency: transitive description: @@ -1230,13 +1254,13 @@ packages: source: hosted version: "5.4.0" pinenacl: - dependency: transitive + dependency: "direct overridden" description: name: pinenacl - sha256: "3a5503637587d635647c93ea9a8fecf48a420cc7deebe6f1fc85c2a5637ab327" + sha256: e5fb0bce1717b7f136f35ee98b5c02b3e6383211f8a77ca882fa7812232a07b9 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.3.4" platform: dependency: transitive description: @@ -1269,6 +1293,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + pretty_dio_logger: + dependency: transitive + description: + name: pretty_dio_logger + sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2" + url: "https://pub.dev" + source: hosted + version: "1.3.1" process: dependency: transitive description: @@ -1317,6 +1349,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" rational: dependency: transitive description: @@ -1325,6 +1365,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" riverpod: dependency: transitive description: @@ -1547,6 +1595,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + tezart: + dependency: "direct main" + description: + name: tezart + sha256: "35d526f2e6ca250c64461ebfb4fa9f64b6599fab8c4242c8e89ae27d4ac2e15a" + url: "https://pub.dev" + source: hosted + version: "2.0.5" time: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 506d6d431..4e4a6755e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -202,6 +202,8 @@ dependency_overrides: crypto: 3.0.2 analyzer: ^5.2.0 + pinenacl: ^0.3.3 + dio : ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From f4688bd041974d25219734df285ba2bd679d6ed7 Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Fri, 28 Jul 2023 22:16:04 +0300 Subject: [PATCH 12/35] xtz: filter other operations --- lib/services/coins/tezos/tezos_wallet.dart | 101 +++++++++++---------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 4f66df22e..9fa8e499f 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -332,60 +332,61 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateTransactions() async { // TODO: Use node RPC instead of tzstats API - var api = - "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; - var jsonResponse = - jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + var api = "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; + var jsonResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); List> txs = []; for (var tx in jsonResponse as List) { - var txApi = "https://api.tzstats.com/explorer/op/${tx["hash"]}"; - var txJsonResponse = jsonDecode( - await get(Uri.parse(txApi)).then((value) => value.body))[0]; - TransactionType txType; - if (txJsonResponse["sender"] == (await currentReceivingAddress)) { - txType = TransactionType.outgoing; - } else { - txType = TransactionType.incoming; + if (tx[1] == "transaction") { + var txApi = "https://api.tzstats.com/explorer/op/${tx[2]}"; + var txJsonResponse = jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body)); + // Check if list is larger than 1 (if it is, it's a batch transaction) + if (!((txJsonResponse as List).length > 1)) { + for (var (opJson as Map) in txJsonResponse) { + if (opJson.containsKey("volume")) { // This is to check if transaction is a token transfer + TransactionType txType; + if (opJson["sender"] == (await currentReceivingAddress)) { + txType = TransactionType.outgoing; + } else { + txType = TransactionType.incoming; + } + var theTx = Transaction( + walletId: walletId, + txid: opJson["hash"].toString(), + timestamp: DateTime.parse(opJson["time"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, + type: txType, + subType: TransactionSubType.none, + amount: (float.parse(opJson["volume"].toString()) * 1000000).toInt(), + amountString: Amount( + rawValue: BigInt.parse((float.parse(opJson["volume"].toString()) * 1000000).toInt().toString()), + fractionDigits: 6 + ).toJsonString(), + fee: (float.parse(opJson["fee"].toString()) * 1000000).toInt(), + height: int.parse(opJson["height"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, + ); + var theAddress = Address( + walletId: walletId, + value: opJson["receiver"].toString(), + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + txs.add(Tuple2(theTx, theAddress)); + } + } + } } - var theTx = Transaction( - walletId: walletId, - txid: txJsonResponse["hash"].toString(), - timestamp: DateTime.parse(txJsonResponse["time"].toString()) - .toUtc() - .millisecondsSinceEpoch ~/ - 1000, - type: txType, - subType: TransactionSubType.none, - amount: (float.parse(txJsonResponse["volume"].toString()) * 1000000) - .toInt(), - amountString: Amount( - rawValue: BigInt.parse( - (float.parse(txJsonResponse["volume"].toString()) * 1000000) - .toString()), - fractionDigits: 6) - .toJsonString(), - fee: (float.parse(txJsonResponse["fee"].toString()) * 1000000).toInt(), - height: int.parse(txJsonResponse["height"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - var theAddress = Address( - walletId: walletId, - value: txJsonResponse["receiver"].toString(), - publicKey: [], // TODO: Add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.unknown, - ); - txs.add(Tuple2(theTx, theAddress)); } + Logging.instance.log("Transactions: $txs", level: LogLevel.Info); await db.addNewTransactionData(txs, walletId); } From a1ef84fbe64f5e815bdfd24e4854a189720f1589 Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Fri, 28 Jul 2023 23:00:14 +0300 Subject: [PATCH 13/35] fix stellarTestNet and dependency problems --- .../add_edit_node_view.dart | 5 +++- .../manage_nodes_views/node_details_view.dart | 2 +- lib/services/coins/coin_service.dart | 9 ++++++++ lib/themes/color_theme.dart | 2 +- lib/themes/stack_colors.dart | 2 +- lib/utilities/address_utils.dart | 2 +- lib/utilities/amount/amount_unit.dart | 2 +- lib/utilities/block_explorers.dart | 2 +- lib/utilities/constants.dart | 10 ++++---- lib/utilities/default_nodes.dart | 6 ++--- lib/utilities/enums/coin_enum.dart | 23 +++++++++++-------- .../enums/derive_path_type_enum.dart | 2 +- lib/widgets/node_card.dart | 2 +- lib/widgets/node_options_sheet.dart | 2 +- pubspec.lock | 10 ++++---- pubspec.yaml | 3 +-- 16 files changed, 49 insertions(+), 35 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 781a566ef..4bd42f70b 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -197,7 +197,7 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: throw UnimplementedError(); //TODO: check network/node case Coin.tezos: @@ -732,11 +732,14 @@ class _NodeFormState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincash: case Coin.particl: + case Coin.stellar: + case Coin.tezos: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.stellarTestNet: case Coin.epicCash: case Coin.nano: case Coin.banano: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 31a9c6717..d22af6e25 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -174,7 +174,7 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.banano: case Coin.tezos: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: throw UnimplementedError(); //TODO: check network/node } diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 394ff5fc1..9ee397e0d 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -229,6 +229,15 @@ abstract class CoinServiceAPI { tracker: tracker, ); + case Coin.stellarTestNet: + return StellarWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + tracker: tracker, + ); + case Coin.tezos: return TezosWallet( walletId: walletId, diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index abec28d4e..0e6816dd8 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -65,7 +65,7 @@ class CoinThemeColorDefault { case Coin.particl: return particl; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return stellar; case Coin.nano: return nano; diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index cbec0077a..fde5219cf 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1708,7 +1708,7 @@ class StackColors extends ThemeExtension { case Coin.particl: return _coin.particl; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return _coin.stellar; case Coin.nano: return _coin.nano; diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 3fa895282..b4974d8c6 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -143,7 +143,7 @@ class AddressUtils { return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: return Address.validateAddress(address, dogecointestnet); - case Coin.stellarTestnet: + case Coin.stellarTestNet: return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); } } diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 6a646fd11..00055a0fe 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -51,7 +51,7 @@ enum AmountUnit { case Coin.eCash: case Coin.epicCash: case Coin.stellar: // TODO: check if this is correct - case Coin.stellarTestnet: + case Coin.stellarTestNet: case Coin.tezos: return AmountUnit.values.sublist(0, 4); diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index bb4ac06fb..ddc055fb1 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -60,7 +60,7 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://www.nanolooker.com/block/$txid"); case Coin.banano: return Uri.parse("https://www.bananolooker.com/block/$txid"); - case Coin.stellarTestnet: + case Coin.stellarTestNet: return Uri.parse("https://testnet.stellarchain.io/transactions/$txid"); case Coin.tezos: return Uri.parse("https://tzstats.com/$txid"); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 1253e08f1..1bd3a5a50 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -102,7 +102,7 @@ abstract class Constants { return _satsPerCoinECash; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return _satsPerCoinStellar; case Coin.tezos: @@ -146,7 +146,7 @@ abstract class Constants { return _decimalPlacesECash; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return _decimalPlacesStellar; case Coin.tezos: @@ -174,7 +174,7 @@ abstract class Constants { case Coin.particl: case Coin.nano: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: values.addAll([24, 12]); break; case Coin.banano: @@ -238,7 +238,7 @@ abstract class Constants { return 1; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return 5; case Coin.tezos: @@ -271,7 +271,7 @@ abstract class Constants { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: case Coin.tezos: return 24; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 7a4daf2f2..7fed3d7fd 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -292,10 +292,10 @@ abstract class DefaultNodes { host: "https://horizon-testnet.stellar.org/", port: 50022, name: defaultName, - id: _nodeId(Coin.stellarTestnet), + id: _nodeId(Coin.stellarTestNet), useSSL: true, enabled: true, - coinName: Coin.stellarTestnet.name, + coinName: Coin.stellarTestNet.name, isFailover: true, isDown: false, ); @@ -365,7 +365,7 @@ abstract class DefaultNodes { case Coin.dogecoinTestNet: return dogecoinTestnet; - case Coin.stellarTestnet: + case Coin.stellarTestNet: return stellarTestnet; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index b62e4ea67..2e5062b6d 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -60,9 +60,10 @@ enum Coin { dogecoinTestNet, firoTestNet, litecoinTestNet, + stellarTestNet, } -final int kTestNetCoinCount = 4; // Util.isDesktop ? 5 : 4; +final int kTestNetCoinCount = 5; // Util.isDesktop ? 5 : 4; // remove firotestnet for now extension CoinExt on Coin { @@ -110,6 +111,8 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; + case Coin.stellarTestNet: + return "tStellar"; } } @@ -157,7 +160,7 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; - case Coin.stellarTestnet: + case Coin.stellarTestNet: return "tXLM"; } } @@ -207,7 +210,7 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; - case Coin.stellarTestnet: + case Coin.stellarTestNet: return "stellar"; } } @@ -237,7 +240,7 @@ extension CoinExt on Coin { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return false; } } @@ -267,7 +270,7 @@ extension CoinExt on Coin { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return false; } } @@ -297,7 +300,7 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return true; } } @@ -337,7 +340,7 @@ extension CoinExt on Coin { case Coin.firoTestNet: return Coin.firo; - case Coin.stellarTestnet: + case Coin.stellarTestNet: return Coin.stellar; } } @@ -380,7 +383,7 @@ extension CoinExt on Coin { return particl.MINIMUM_CONFIRMATIONS; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return xlm.MINIMUM_CONFIRMATIONS; case Coin.tezos: @@ -500,7 +503,7 @@ Coin coinFromPrettyName(String name) { case "Stellar Testnet": case "stellarTestnet": case "tStellar": - return Coin.stellarTestnet; + return Coin.stellarTestNet; default: throw ArgumentError.value( @@ -556,7 +559,7 @@ Coin coinFromTickerCaseInsensitive(String ticker) { case "ban": return Coin.banano; case "txlm": - return Coin.stellarTestnet; + return Coin.stellarTestNet; default: throw ArgumentError.value( ticker, "name", "No Coin enum value with that ticker"); diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 5b94f41f6..f3c456f67 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -50,7 +50,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: case Coin.tezos: // TODO: Is this true? throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 661af4190..8d4c29ac2 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -196,7 +196,7 @@ class _NodeCardState extends ConsumerState { case Coin.banano: case Coin.tezos: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: throw UnimplementedError(); //TODO: check network/node } diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 953ac78a1..63fd2b13e 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -179,7 +179,7 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.banano: case Coin.tezos: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: throw UnimplementedError(); //TODO: check network/node } diff --git a/pubspec.lock b/pubspec.lock index 111b58d75..c01aca249 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -459,7 +459,7 @@ packages: source: hosted version: "0.6.0" dio: - dependency: "direct overridden" + dependency: transitive description: name: dio sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" @@ -1297,10 +1297,10 @@ packages: dependency: transitive description: name: pretty_dio_logger - sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2" + sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.2.0-beta-1" process: dependency: transitive description: @@ -1519,10 +1519,10 @@ packages: dependency: "direct main" description: name: stellar_flutter_sdk - sha256: "7a9b7dc76018bbd0b9c828045cf0e26e07ec44208fb1a1733273de2390205475" + sha256: "4c55b1b6dfbde7f89bba59a422754280715fa3b5726cff5e7eeaed454d2c4b89" url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "1.5.3" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4e4a6755e..e3fa11745 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,7 +138,7 @@ dependencies: desktop_drop: ^0.4.1 nanodart: ^2.0.0 basic_utils: ^5.5.4 - stellar_flutter_sdk: ^1.6.0 + stellar_flutter_sdk: ^1.5.3 tezart: ^2.0.5 dev_dependencies: @@ -203,7 +203,6 @@ dependency_overrides: crypto: 3.0.2 analyzer: ^5.2.0 pinenacl: ^0.3.3 - dio : ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 182a8cc73252fc9799c2d131ea7a7168599f5026 Mon Sep 17 00:00:00 2001 From: detherminal <76167420+detherminal@users.noreply.github.com> Date: Sun, 6 Aug 2023 00:16:19 +0300 Subject: [PATCH 14/35] implement fees and sending? --- lib/services/coins/tezos/tezos_wallet.dart | 66 +++++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 9fa8e499f..e62a5afb0 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -144,8 +144,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { .round(); final int feeInMicroTez = int.parse(txData["fee"].toString()); final String destinationAddress = txData["address"] as String; - final String sourceAddress = await currentReceivingAddress; - return Future.value(""); // TODO: return tx hash + final secretKey = Keystore.fromMnemonic((await mnemonicString)!).secretKey; + Logging.instance.log(secretKey, level: LogLevel.Info); + final sourceKeyStore = Keystore.fromSecretKey(secretKey); + final client = TezartClient("${getCurrentNode().host}:${getCurrentNode().port}"); + final operation = await client.transferOperation(source: sourceKeyStore, destination: destinationAddress, amount: amountInMicroTez, customFee: feeInMicroTez); + await operation.executeAndMonitor(); // This line gives an error + return Future.value(""); } catch (e) { Logging.instance.log(e.toString(), level: LogLevel.Error); return Future.error(e); @@ -162,12 +167,29 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future estimateFeeFor(Amount amount, int feeRate) { - return Future.value( - Amount( - rawValue: BigInt.parse(100000.toString()), - fractionDigits: coin.decimals), - ); + Future estimateFeeFor(Amount amount, int feeRate) async { + // TODO: Check if this is correct + var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; + var response = jsonDecode((await get(Uri.parse(api))).body)[0]; + double totalFees = response[4] as double; + int totalTxs = response[8] as int; + int feePerTx = (totalFees / totalTxs * 1000000).floor(); + int estimatedFee = 0; + Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); + Logging.instance.log("feeRate:$feeRate", level: LogLevel.Info); + switch (feeRate) { + case 0: + estimatedFee = feePerTx * 2; + case 1: + estimatedFee = feePerTx; + case 2: + case 3: + estimatedFee = (feePerTx / 2).floor(); + default: + estimatedFee = feeRate; + } + Logging.instance.log("estimatedFee:$estimatedFee", level: LogLevel.Info); + return Amount(rawValue: BigInt.from(estimatedFee), fractionDigits: 6); } @override @@ -178,14 +200,21 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get fees async { - // TODO: Change this to get fees from node and fix numberOfBlocks + // TODO: Check if this is correct + var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; + var response = jsonDecode((await get(Uri.parse(api))).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 return FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 1, - numberOfBlocksSlow: 1, - fast: 1000000, - medium: 100000, - slow: 10000, + numberOfBlocksFast: 3, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 30, + fast: (feePerTx * 2), + medium: feePerTx, + slow: (feePerTx / 2).floor(), ); } @@ -213,6 +242,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future initializeNew() 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', From d785a2ef839bde2019faee65f407fc653ac1b5b1 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 14 Aug 2023 10:14:47 +0200 Subject: [PATCH 15/35] Fix error with Stellar pretty name not found --- lib/utilities/enums/coin_enum.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 2e5062b6d..8db5d4843 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -502,6 +502,7 @@ Coin coinFromPrettyName(String name) { case "Stellar Testnet": case "stellarTestnet": + case "stellarTestNet": case "tStellar": return Coin.stellarTestNet; From a6d39418584cb884d4ba43a353d57af4d1cb1f2b Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 14 Aug 2023 16:36:27 +0200 Subject: [PATCH 16/35] Fix send and refactoring --- lib/services/coins/tezos/tezos_wallet.dart | 92 ++++++++++++++-------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index e62a5afb0..3ecc94eb8 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; @@ -137,18 +138,26 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future confirmSend({required Map txData}) async { try { - final node = getCurrentNode().host + getCurrentNode().port.toString(); - final int amountInMicroTez = - ((int.parse((txData["recipientAmt"] as Amount).raw.toString()) * - 1000000)) - .round(); + + 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}:${getCurrentNode().port}"); - final operation = await client.transferOperation(source: sourceKeyStore, destination: destinationAddress, amount: amountInMicroTez, customFee: feeInMicroTez); + final client = TezartClient(getCurrentNode().host); + //TODO - Update gas Limit + final operation = await client.transferOperation( + source: sourceKeyStore, + destination: destinationAddress, + amount: microtezToInt, + customFee: feeInMicroTez, + customGasLimit: 400 + ); await operation.executeAndMonitor(); // This line gives an error return Future.value(""); } catch (e) { @@ -189,7 +198,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { estimatedFee = feeRate; } Logging.instance.log("estimatedFee:$estimatedFee", level: LogLevel.Info); - return Amount(rawValue: BigInt.from(estimatedFee), fractionDigits: 6); + return Amount(rawValue: BigInt.from(estimatedFee), fractionDigits: coin.decimals); } @override @@ -347,33 +356,43 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateBalance() async { - var api = - "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; - var theBalance = (await get(Uri.parse(api)).then((value) => value.body)) - .substring(1, - (await get(Uri.parse(api)).then((value) => value.body)).length - 2); - Logging.instance.log( - "Balance for ${await currentReceivingAddress}: $theBalance", - level: LogLevel.Info); - var balanceInAmount = Amount( - rawValue: BigInt.parse(theBalance.toString()), fractionDigits: 6); - _balance = Balance( - total: balanceInAmount, - spendable: balanceInAmount, - blockedTotal: Amount(rawValue: BigInt.parse("0"), fractionDigits: 6), - pendingSpendable: Amount(rawValue: BigInt.parse("0"), fractionDigits: 6), - ); - await updateCachedBalance(_balance!); + + try { + var api = + "${getCurrentNode().host}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; + var theBalance = (await get(Uri.parse(api)).then((value) => value.body)) + .substring(1, + (await get(Uri.parse(api)).then((value) => value.body)).length - 2); + Logging.instance.log( + "Balance for ${await currentReceivingAddress}: $theBalance", + level: LogLevel.Info); + var balanceInAmount = Amount( + rawValue: BigInt.parse(theBalance.toString()), 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 ${e.toString()}", level: LogLevel.Error); + } + } Future updateTransactions() async { + // TODO: Use node RPC instead of tzstats API var api = "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; var jsonResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + List> txs = []; + for (var tx in jsonResponse as List) { + if (tx[1] == "transaction") { - var txApi = "https://api.tzstats.com/explorer/op/${tx[2]}"; + var txApi = "https://api.tzstats.com/explorer/op/${tx[0]}"; //Get transactions by Unique Id, this way we will only get txs var txJsonResponse = jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body)); // Check if list is larger than 1 (if it is, it's a batch transaction) if (!((txJsonResponse as List).length > 1)) { @@ -394,7 +413,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { amount: (float.parse(opJson["volume"].toString()) * 1000000).toInt(), amountString: Amount( rawValue: BigInt.parse((float.parse(opJson["volume"].toString()) * 1000000).toInt().toString()), - fractionDigits: 6 + fractionDigits: coin.decimals ).toJsonString(), fee: (float.parse(opJson["fee"].toString()) * 1000000).toInt(), height: int.parse(opJson["height"].toString()), @@ -427,13 +446,18 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateChainHeight() async { - var api = - "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"; - var jsonParsedResponse = - jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); - final int intHeight = int.parse(jsonParsedResponse["level"].toString()); - Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); - await updateCachedChainHeight(intHeight); + try { + var api = + "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; + var jsonParsedResponse = + jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + final int intHeight = int.parse(jsonParsedResponse["level"].toString()); + Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); + await updateCachedChainHeight(intHeight); + } catch (e, s) { + Logging.instance.log("GET CHAIN HEIGHT ERROR ${e.toString()}", level: LogLevel.Error); + } + } @override From 373637701c980eb165b671d9746de64eac599fc1 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Mon, 14 Aug 2023 16:53:44 -0600 Subject: [PATCH 17/35] Fix tezos network status --- lib/services/coins/tezos/tezos_wallet.dart | 123 +++++++++++++++------ 1 file changed, 91 insertions(+), 32 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 3ecc94eb8..e60d353dc 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -12,11 +12,14 @@ 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/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'; @@ -90,6 +93,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Timer? timer; bool _shouldAutoSync = false; + Timer? _networkAliveTimer; @override bool get shouldAutoSync => _shouldAutoSync; @@ -101,12 +105,55 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { 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; @@ -138,15 +185,14 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future confirmSend({required Map txData}) async { try { - final amount = txData["recipientAmt"] as Amount; - final amountInMicroTez = - amount.decimal * Decimal.fromInt(1000000); + 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; + final secretKey = + Keystore.fromMnemonic((await mnemonicString)!).secretKey; Logging.instance.log(secretKey, level: LogLevel.Info); final sourceKeyStore = Keystore.fromSecretKey(secretKey); final client = TezartClient(getCurrentNode().host); @@ -156,8 +202,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { destination: destinationAddress, amount: microtezToInt, customFee: feeInMicroTez, - customGasLimit: 400 - ); + customGasLimit: 400); await operation.executeAndMonitor(); // This line gives an error return Future.value(""); } catch (e) { @@ -198,7 +243,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { estimatedFee = feeRate; } Logging.instance.log("estimatedFee:$estimatedFee", level: LogLevel.Info); - return Amount(rawValue: BigInt.from(estimatedFee), fractionDigits: coin.decimals); + return Amount( + rawValue: BigInt.from(estimatedFee), fractionDigits: coin.decimals); } @override @@ -356,48 +402,55 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateBalance() async { - try { var api = "${getCurrentNode().host}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; var theBalance = (await get(Uri.parse(api)).then((value) => value.body)) - .substring(1, - (await get(Uri.parse(api)).then((value) => value.body)).length - 2); + .substring( + 1, + (await get(Uri.parse(api)).then((value) => value.body)).length - + 2); Logging.instance.log( "Balance for ${await currentReceivingAddress}: $theBalance", level: LogLevel.Info); var balanceInAmount = Amount( - rawValue: BigInt.parse(theBalance.toString()), fractionDigits: coin.decimals); + rawValue: BigInt.parse(theBalance.toString()), + 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), + 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 ${e.toString()}", level: LogLevel.Error); + Logging.instance + .log("ERROR GETTING BALANCE ${e.toString()}", level: LogLevel.Error); } - } Future updateTransactions() async { - // TODO: Use node RPC instead of tzstats API - var api = "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; - var jsonResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + var api = + "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; + var jsonResponse = + jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); List> txs = []; for (var tx in jsonResponse as List) { - if (tx[1] == "transaction") { - var txApi = "https://api.tzstats.com/explorer/op/${tx[0]}"; //Get transactions by Unique Id, this way we will only get txs - var txJsonResponse = jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body)); + var txApi = + "https://api.tzstats.com/explorer/op/${tx[0]}"; //Get transactions by Unique Id, this way we will only get txs + var txJsonResponse = + jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body)); // Check if list is larger than 1 (if it is, it's a batch transaction) if (!((txJsonResponse as List).length > 1)) { for (var (opJson as Map) in txJsonResponse) { - if (opJson.containsKey("volume")) { // This is to check if transaction is a token transfer + if (opJson.containsKey("volume")) { + // This is to check if transaction is a token transfer TransactionType txType; if (opJson["sender"] == (await currentReceivingAddress)) { txType = TransactionType.outgoing; @@ -407,14 +460,21 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { var theTx = Transaction( walletId: walletId, txid: opJson["hash"].toString(), - timestamp: DateTime.parse(opJson["time"].toString()).toUtc().millisecondsSinceEpoch ~/ 1000, + timestamp: DateTime.parse(opJson["time"].toString()) + .toUtc() + .millisecondsSinceEpoch ~/ + 1000, type: txType, subType: TransactionSubType.none, - amount: (float.parse(opJson["volume"].toString()) * 1000000).toInt(), + amount: (float.parse(opJson["volume"].toString()) * 1000000) + .toInt(), amountString: Amount( - rawValue: BigInt.parse((float.parse(opJson["volume"].toString()) * 1000000).toInt().toString()), - fractionDigits: coin.decimals - ).toJsonString(), + rawValue: BigInt.parse( + (float.parse(opJson["volume"].toString()) * 1000000) + .toInt() + .toString()), + fractionDigits: coin.decimals) + .toJsonString(), fee: (float.parse(opJson["fee"].toString()) * 1000000).toInt(), height: int.parse(opJson["height"].toString()), isCancelled: false, @@ -447,17 +507,16 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateChainHeight() async { try { - var api = - "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; + var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; var jsonParsedResponse = - jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); + jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); final int intHeight = int.parse(jsonParsedResponse["level"].toString()); Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); await updateCachedChainHeight(intHeight); } catch (e, s) { - Logging.instance.log("GET CHAIN HEIGHT ERROR ${e.toString()}", level: LogLevel.Error); + Logging.instance + .log("GET CHAIN HEIGHT ERROR ${e.toString()}", level: LogLevel.Error); } - } @override From 520ceabf7993e41ef50023c3fb58c4c7f4450284 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 15 Aug 2023 15:28:53 +0200 Subject: [PATCH 18/35] Clean up and refactor --- lib/services/coins/tezos/tezos_wallet.dart | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index e60d353dc..f33b619d0 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -185,14 +185,16 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future confirmSend({required Map txData}) async { try { + final amount = txData["recipientAmt"] as Amount; - final amountInMicroTez = amount.decimal * Decimal.fromInt(1000000); + 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; + final secretKey = Keystore.fromMnemonic((await mnemonicString)!) + .secretKey; Logging.instance.log(secretKey, level: LogLevel.Info); final sourceKeyStore = Keystore.fromSecretKey(secretKey); final client = TezartClient(getCurrentNode().host); @@ -202,7 +204,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { destination: destinationAddress, amount: microtezToInt, customFee: feeInMicroTez, - customGasLimit: 400); + customGasLimit: feeInMicroTez + ); + await operation.executeAndMonitor(); // This line gives an error return Future.value(""); } catch (e) { @@ -403,18 +407,12 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateBalance() async { try { - var api = - "${getCurrentNode().host}/chains/main/blocks/head/context/contracts/${await currentReceivingAddress}/balance"; - var theBalance = (await get(Uri.parse(api)).then((value) => value.body)) - .substring( - 1, - (await get(Uri.parse(api)).then((value) => value.body)).length - - 2); - Logging.instance.log( - "Balance for ${await currentReceivingAddress}: $theBalance", - level: LogLevel.Info); - var balanceInAmount = Amount( - rawValue: BigInt.parse(theBalance.toString()), + final client = TezartClient(getCurrentNode().host); + final thisBalance = await client.getBalance( + address: await currentReceivingAddress + ); + Amount balanceInAmount = Amount( + rawValue: BigInt.parse(thisBalance.toString()), fractionDigits: coin.decimals); _balance = Balance( total: balanceInAmount, @@ -507,6 +505,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateChainHeight() async { try { + final client = TezartClient(getCurrentNode().host); var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; var jsonParsedResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); From 6047c433afd59d5da917a6f5466872bb16dd6bf0 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Tue, 15 Aug 2023 08:18:39 -0600 Subject: [PATCH 19/35] add tezos coin images --- assets/default_themes/dark.zip | Bin 659201 -> 660115 bytes assets/default_themes/light.zip | Bin 607092 -> 608006 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index fe11a5463f8ae50dad10e51a2f664cbdcc55a25c..4ec1a415b80e284ff31ae47e778ea38513befae8 100644 GIT binary patch delta 6136 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 N9t7f_MR<=w{157-N-F>W delta 5217 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 diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip index 1453d6ba0606fba827105cb4a76af4df5b6b78dc..f9910e94b9ae11dd260a8b7e5707fe37702e1b63 100644 GIT binary patch delta 6186 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 SvofFz6mS8678ZU71OEd^yF+XM delta 5463 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 From 0a77dad7ec84295eed1b6b31ca3424f88e26c8f3 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 15 Aug 2023 16:49:28 +0200 Subject: [PATCH 20/35] Refactor Update transactions call --- lib/services/coins/tezos/tezos_wallet.dart | 123 ++++++++++----------- 1 file changed, 57 insertions(+), 66 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index f33b619d0..c628253d8 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -198,7 +198,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance.log(secretKey, level: LogLevel.Info); final sourceKeyStore = Keystore.fromSecretKey(secretKey); final client = TezartClient(getCurrentNode().host); - //TODO - Update gas Limit + final operation = await client.transferOperation( source: sourceKeyStore, destination: destinationAddress, @@ -431,72 +431,64 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateTransactions() async { // TODO: Use node RPC instead of tzstats API - var api = - "https://api.tzstats.com/tables/op?address=${await currentReceivingAddress}"; - var jsonResponse = - jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); - + String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/operations"; + var response = jsonDecode(await get(Uri.parse(transactionsCall)).then((value) => value.body)); List> txs = []; - - for (var tx in jsonResponse as List) { - if (tx[1] == "transaction") { - var txApi = - "https://api.tzstats.com/explorer/op/${tx[0]}"; //Get transactions by Unique Id, this way we will only get txs - var txJsonResponse = - jsonDecode(await get(Uri.parse(txApi)).then((value) => value.body)); - // Check if list is larger than 1 (if it is, it's a batch transaction) - if (!((txJsonResponse as List).length > 1)) { - for (var (opJson as Map) in txJsonResponse) { - if (opJson.containsKey("volume")) { - // This is to check if transaction is a token transfer - TransactionType txType; - if (opJson["sender"] == (await currentReceivingAddress)) { - txType = TransactionType.outgoing; - } else { - txType = TransactionType.incoming; - } - var theTx = Transaction( - walletId: walletId, - txid: opJson["hash"].toString(), - timestamp: DateTime.parse(opJson["time"].toString()) - .toUtc() - .millisecondsSinceEpoch ~/ - 1000, - type: txType, - subType: TransactionSubType.none, - amount: (float.parse(opJson["volume"].toString()) * 1000000) - .toInt(), - amountString: Amount( - rawValue: BigInt.parse( - (float.parse(opJson["volume"].toString()) * 1000000) - .toInt() - .toString()), - fractionDigits: coin.decimals) - .toJsonString(), - fee: (float.parse(opJson["fee"].toString()) * 1000000).toInt(), - height: int.parse(opJson["height"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - var theAddress = Address( - walletId: walletId, - value: opJson["receiver"].toString(), - publicKey: [], // TODO: Add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: AddressSubType.unknown, - ); - txs.add(Tuple2(theTx, theAddress)); - } - } + for (var tx in response as List) { + if (tx["type"] == "transaction") { + TransactionType txType; + if (tx["sender"]["address"] == (await currentReceivingAddress)) { + txType = TransactionType.outgoing; + } else { + txType = TransactionType.incoming; } + + final amount = tx["amount"] as int; + final fee = tx["bakerFee"] as int; + final amountInMicroTez = + amount / 1000000; + + final feeInMicroTez = + fee / 1000000; + + var theTx = Transaction( + walletId: walletId, + txid: tx["hash"].toString(), + timestamp: DateTime.parse(tx["timestamp"].toString()) + .toUtc() + .millisecondsSinceEpoch ~/ + 1000, + type: txType, + subType: TransactionSubType.none, + amount: tx["amount"] as int, + amountString: Amount( + rawValue: BigInt.parse( + (tx["amount"] as int) + .toInt() + .toString()), + fractionDigits: coin.decimals) + .toJsonString(), + fee: tx["bakerFee"] as int, + height: int.parse(tx["level"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, + ); + var theAddress = Address( + walletId: walletId, + value: tx["target"]["address"].toString(), + publicKey: [], // TODO: Add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.unknown, + ); + txs.add(Tuple2(theTx, theAddress)); } } Logging.instance.log("Transactions: $txs", level: LogLevel.Info); @@ -505,7 +497,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateChainHeight() async { try { - final client = TezartClient(getCurrentNode().host); var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; var jsonParsedResponse = jsonDecode(await get(Uri.parse(api)).then((value) => value.body)); From e3fb83a61fdc8af4a49479fcd54e17881259c670 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 15 Aug 2023 18:01:51 +0200 Subject: [PATCH 21/35] Send all --- lib/services/coins/tezos/tezos_wallet.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index c628253d8..857f62f12 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -195,14 +195,20 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { 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; + if (balance.spendable == txData["recipientAmt"] as Amount) { + sendAmount = microtezToInt - feeInMicroTez; + } + final operation = await client.transferOperation( source: sourceKeyStore, destination: destinationAddress, - amount: microtezToInt, + amount: sendAmount, customFee: feeInMicroTez, customGasLimit: feeInMicroTez ); @@ -430,7 +436,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateTransactions() async { - // TODO: Use node RPC instead of tzstats API String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/operations"; var response = jsonDecode(await get(Uri.parse(transactionsCall)).then((value) => value.body)); List> txs = []; @@ -443,14 +448,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { txType = TransactionType.incoming; } - final amount = tx["amount"] as int; - final fee = tx["bakerFee"] as int; - final amountInMicroTez = - amount / 1000000; - - final feeInMicroTez = - fee / 1000000; - var theTx = Transaction( walletId: walletId, txid: tx["hash"].toString(), From e8d5c3ad8b865fbe7e21dcd4c58951b98e2e715b Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 16 Aug 2023 14:46:21 +0200 Subject: [PATCH 22/35] Return tx id when sending, implement updateSentTx --- lib/services/coins/tezos/tezos_wallet.dart | 64 +++++++++++++++++----- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 857f62f12..56e08ba8b 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -30,6 +30,7 @@ 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({ @@ -201,8 +202,15 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { final client = TezartClient(getCurrentNode().host); int? sendAmount = microtezToInt; + int gasLimit = _gasLimit; + int thisFee = feeInMicroTez; + if (balance.spendable == txData["recipientAmt"] as Amount) { - sendAmount = microtezToInt - feeInMicroTez; + //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( @@ -210,11 +218,10 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { destination: destinationAddress, amount: sendAmount, customFee: feeInMicroTez, - customGasLimit: feeInMicroTez + customGasLimit: gasLimit ); - - await operation.executeAndMonitor(); // This line gives an error - return Future.value(""); + await operation.executeAndMonitor(); + return operation.result.id as String; } catch (e) { Logging.instance.log(e.toString(), level: LogLevel.Error); return Future.error(e); @@ -413,12 +420,11 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateBalance() async { try { - final client = TezartClient(getCurrentNode().host); - final thisBalance = await client.getBalance( - address: await currentReceivingAddress - ); + String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/" + "${await currentReceivingAddress}/balance"; + var response = jsonDecode(await get(Uri.parse(balanceCall)).then((value) => value.body)); Amount balanceInAmount = Amount( - rawValue: BigInt.parse(thisBalance.toString()), + rawValue: BigInt.parse(response.toString()), fractionDigits: coin.decimals); _balance = Balance( total: balanceInAmount, @@ -436,7 +442,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } Future updateTransactions() async { - String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/${await currentReceivingAddress}/operations"; + String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/" + "${await currentReceivingAddress}/operations"; var response = jsonDecode(await get(Uri.parse(transactionsCall)).then((value) => value.body)); List> txs = []; for (var tx in response as List) { @@ -544,9 +551,38 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future updateSentCachedTxData(Map txData) { - // TODO: implement updateSentCachedTxData - throw UnimplementedError(); + 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 From 444860da53ad13a87137e79c8985eefa120d0343 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 16 Aug 2023 15:17:58 +0200 Subject: [PATCH 23/35] Return just one value for fees since endpoint returns only one value for fees --- lib/services/coins/tezos/tezos_wallet.dart | 35 +++++++--------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 56e08ba8b..1fb192190 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -239,29 +239,17 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future estimateFeeFor(Amount amount, int feeRate) async { - // TODO: Check if this is correct - var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; + + var api = "https://api.tzstats.com/series/op?start_date=today&collapse=1d"; var response = jsonDecode((await get(Uri.parse(api))).body)[0]; double totalFees = response[4] as double; int totalTxs = response[8] as int; int feePerTx = (totalFees / totalTxs * 1000000).floor(); - int estimatedFee = 0; - Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); - Logging.instance.log("feeRate:$feeRate", level: LogLevel.Info); - switch (feeRate) { - case 0: - estimatedFee = feePerTx * 2; - case 1: - estimatedFee = feePerTx; - case 2: - case 3: - estimatedFee = (feePerTx / 2).floor(); - default: - estimatedFee = feeRate; - } - Logging.instance.log("estimatedFee:$estimatedFee", level: LogLevel.Info); + return Amount( - rawValue: BigInt.from(estimatedFee), fractionDigits: coin.decimals); + rawValue: BigInt.from(feePerTx), + fractionDigits: coin.decimals, + ); } @override @@ -272,21 +260,20 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get fees async { - // TODO: Check if this is correct var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; var response = jsonDecode((await get(Uri.parse(api))).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 + // TODO: fix numberOfBlocks - Since there is only one fee no need to set blocks return FeeObject( - numberOfBlocksFast: 3, + numberOfBlocksFast: 10, numberOfBlocksAverage: 10, - numberOfBlocksSlow: 30, - fast: (feePerTx * 2), + numberOfBlocksSlow: 10, + fast: feePerTx, medium: feePerTx, - slow: (feePerTx / 2).floor(), + slow: feePerTx, ); } From 89aa4e2a327dec3a8b70e747545b076f216af725 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 23 Aug 2023 10:48:28 -0600 Subject: [PATCH 24/35] tezos images --- assets/default_themes/dark.zip | Bin 660115 -> 676128 bytes assets/default_themes/light.zip | Bin 608006 -> 624020 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index 4ec1a415b80e284ff31ae47e778ea38513befae8..eb20d7e6e6279c6c6ca50a76aef686d968861b47 100644 GIT binary patch delta 21204 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 oK7D@I?W~P95Qrg){s4pg57?EME&u=k From 467f57c74e1f3902acf221d1903b24dc7be33c82 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 23 Aug 2023 10:59:13 -0600 Subject: [PATCH 25/35] resolve merge conflicts --- .../manage_nodes_views/add_edit_node_view.dart | 9 +++++---- .../manage_nodes_views/node_details_view.dart | 16 ++++++++-------- lib/utilities/enums/coin_enum.dart | 3 ++- lib/widgets/node_card.dart | 5 +++-- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index c4c10a618..5ae04920d 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -196,10 +196,11 @@ class _AddEditNodeViewState extends ConsumerState { } catch (_) {} break; case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: try { - testPassed = await testStellarNodeConnection(formData.host!, formData.port!); - } catch(_) {} + testPassed = + await testStellarNodeConnection(formData.host!, formData.port!); + } catch (_) {} break; case Coin.nano: @@ -749,7 +750,7 @@ class _NodeFormState extends ConsumerState { case Coin.banano: case Coin.eCash: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: return false; case Coin.ethereum: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 0b339469e..d306260d6 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/utilities/test_eth_node_connection.dart'; import 'package:stackwallet/utilities/test_monero_node_connection.dart'; +import 'package:stackwallet/utilities/test_stellar_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -172,20 +173,19 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.nano: case Coin.banano: - case Coin.tezos: case Coin.stellar: case Coin.stellarTestNet: - try { - testPassed = await testStellarNodeConnection(node!.host, node.port); - } catch(_) { - testPassed = false; - } + try { + testPassed = await testStellarNodeConnection(node!.host, node.port); + } catch (_) { + testPassed = false; + } break; case Coin.nano: case Coin.banano: - + case Coin.tezos: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node } if (testPassed) { diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index f4a4ca28f..b8af7f53c 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -262,7 +262,8 @@ extension CoinExt on Coin { case Coin.ethereum: case Coin.eCash: case Coin.stellar: - case Coin.stellarTestnet: + case Coin.stellarTestNet: + case Coin.tezos: return true; case Coin.epicCash: diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index a75b09c64..b63c21b93 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -29,6 +29,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/utilities/test_eth_node_connection.dart'; import 'package:stackwallet/utilities/test_monero_node_connection.dart'; +import 'package:stackwallet/utilities/test_stellar_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -199,7 +200,7 @@ class _NodeCardState extends ConsumerState { case Coin.stellarTestNet: try { testPassed = await testStellarNodeConnection(node.host, node.port); - } catch(_) { + } catch (_) { testPassed = false; } break; @@ -207,7 +208,7 @@ class _NodeCardState extends ConsumerState { case Coin.nano: case Coin.banano: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node } if (testPassed) { From 02b67c2a2e0de3743550ea7f5664158ab93d0075 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 23 Aug 2023 11:08:32 -0600 Subject: [PATCH 26/35] add parameter to initializeNew for tezos --- lib/services/coins/tezos/tezos_wallet.dart | 50 +++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 1fb192190..6b0d10cc6 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -186,16 +186,14 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future confirmSend({required Map txData}) async { try { - final amount = txData["recipientAmt"] as Amount; - final amountInMicroTez = - amount.decimal * Decimal.fromInt(1000000); + 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; + final secretKey = + Keystore.fromMnemonic((await mnemonicString)!).secretKey; Logging.instance.log(secretKey, level: LogLevel.Info); final sourceKeyStore = Keystore.fromSecretKey(secretKey); @@ -208,9 +206,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { 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; + thisFee = thisFee + 32; + sendAmount = microtezToInt - thisFee; + gasLimit = _gasLimit + 320; } final operation = await client.transferOperation( @@ -218,8 +216,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { destination: destinationAddress, amount: sendAmount, customFee: feeInMicroTez, - customGasLimit: gasLimit - ); + customGasLimit: gasLimit); await operation.executeAndMonitor(); return operation.result.id as String; } catch (e) { @@ -239,7 +236,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @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 get(Uri.parse(api))).body)[0]; double totalFees = response[4] as double; @@ -300,7 +296,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future initializeNew() async { + 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!"); @@ -409,7 +407,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { try { String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/" "${await currentReceivingAddress}/balance"; - var response = jsonDecode(await get(Uri.parse(balanceCall)).then((value) => value.body)); + var response = jsonDecode( + await get(Uri.parse(balanceCall)).then((value) => value.body)); Amount balanceInAmount = Amount( rawValue: BigInt.parse(response.toString()), fractionDigits: coin.decimals); @@ -431,7 +430,8 @@ 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 get(Uri.parse(transactionsCall)).then((value) => value.body)); + var response = jsonDecode( + await get(Uri.parse(transactionsCall)).then((value) => value.body)); List> txs = []; for (var tx in response as List) { if (tx["type"] == "transaction") { @@ -446,18 +446,16 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { walletId: walletId, txid: tx["hash"].toString(), timestamp: DateTime.parse(tx["timestamp"].toString()) - .toUtc() - .millisecondsSinceEpoch ~/ + .toUtc() + .millisecondsSinceEpoch ~/ 1000, type: txType, subType: TransactionSubType.none, amount: tx["amount"] as int, amountString: Amount( - rawValue: BigInt.parse( - (tx["amount"] as int) - .toInt() - .toString()), - fractionDigits: coin.decimals) + rawValue: + BigInt.parse((tx["amount"] as int).toInt().toString()), + fractionDigits: coin.decimals) .toJsonString(), fee: tx["bakerFee"] as int, height: int.parse(tx["level"].toString()), @@ -561,14 +559,14 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { ); final address = txData["address"] is String - ? await db.getAddress(walletId, txData["address"] as String) - : null; + ? await db.getAddress(walletId, txData["address"] as String) + : null; await db.addNewTransactionData( - [ + [ Tuple2(transaction, address), - ], - walletId, + ], + walletId, ); } From 63ab0b09e897dc993b9c0fdbe1bfb74be4ea2835 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 11:29:10 -0600 Subject: [PATCH 27/35] revert enum name --- .../add_edit_node_view.dart | 4 +-- .../manage_nodes_views/node_details_view.dart | 2 +- lib/services/coins/coin_service.dart | 2 +- lib/themes/color_theme.dart | 2 +- lib/themes/stack_colors.dart | 2 +- lib/utilities/address_utils.dart | 2 +- lib/utilities/amount/amount_unit.dart | 2 +- lib/utilities/block_explorers.dart | 2 +- lib/utilities/constants.dart | 10 ++++---- lib/utilities/default_nodes.dart | 25 +++++++++---------- lib/utilities/enums/coin_enum.dart | 24 +++++++++--------- .../enums/derive_path_type_enum.dart | 2 +- lib/widgets/node_card.dart | 2 +- lib/widgets/node_options_sheet.dart | 2 +- 14 files changed, 41 insertions(+), 42 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 5ae04920d..8a09a3d03 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -196,7 +196,7 @@ class _AddEditNodeViewState extends ConsumerState { } catch (_) {} break; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: try { testPassed = await testStellarNodeConnection(formData.host!, formData.port!); @@ -750,7 +750,7 @@ class _NodeFormState extends ConsumerState { case Coin.banano: case Coin.eCash: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return false; case Coin.ethereum: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index d306260d6..de3a4b5e5 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -174,7 +174,7 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: try { testPassed = await testStellarNodeConnection(node!.host, node.port); } catch (_) { diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 72478b353..360372095 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -229,7 +229,7 @@ abstract class CoinServiceAPI { tracker: tracker, ); - case Coin.stellarTestNet: + case Coin.stellarTestnet: return StellarWallet( walletId: walletId, walletName: walletName, diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index 0e6816dd8..abec28d4e 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -65,7 +65,7 @@ class CoinThemeColorDefault { case Coin.particl: return particl; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return stellar; case Coin.nano: return nano; diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index fde5219cf..cbec0077a 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1708,7 +1708,7 @@ class StackColors extends ThemeExtension { case Coin.particl: return _coin.particl; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return _coin.stellar; case Coin.nano: return _coin.nano; diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index b4974d8c6..3fa895282 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -143,7 +143,7 @@ class AddressUtils { return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: return Address.validateAddress(address, dogecointestnet); - case Coin.stellarTestNet: + case Coin.stellarTestnet: return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); } } diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 00055a0fe..6a646fd11 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -51,7 +51,7 @@ enum AmountUnit { case Coin.eCash: case Coin.epicCash: case Coin.stellar: // TODO: check if this is correct - case Coin.stellarTestNet: + case Coin.stellarTestnet: case Coin.tezos: return AmountUnit.values.sublist(0, 4); diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index ddc055fb1..bb4ac06fb 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -60,7 +60,7 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://www.nanolooker.com/block/$txid"); case Coin.banano: return Uri.parse("https://www.bananolooker.com/block/$txid"); - case Coin.stellarTestNet: + case Coin.stellarTestnet: return Uri.parse("https://testnet.stellarchain.io/transactions/$txid"); case Coin.tezos: return Uri.parse("https://tzstats.com/$txid"); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 1bd3a5a50..1253e08f1 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -102,7 +102,7 @@ abstract class Constants { return _satsPerCoinECash; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return _satsPerCoinStellar; case Coin.tezos: @@ -146,7 +146,7 @@ abstract class Constants { return _decimalPlacesECash; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return _decimalPlacesStellar; case Coin.tezos: @@ -174,7 +174,7 @@ abstract class Constants { case Coin.particl: case Coin.nano: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: values.addAll([24, 12]); break; case Coin.banano: @@ -238,7 +238,7 @@ abstract class Constants { return 1; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return 5; case Coin.tezos: @@ -271,7 +271,7 @@ abstract class Constants { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: case Coin.tezos: return 24; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 7fed3d7fd..210959f65 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -191,8 +191,7 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.stellar.name, isFailover: true, - isDown: false - ); + isDown: false); static NodeModel get tezos => NodeModel( // TODO: Change this to stack wallet one @@ -289,16 +288,16 @@ abstract class DefaultNodes { ); static NodeModel get stellarTestnet => NodeModel( - host: "https://horizon-testnet.stellar.org/", - port: 50022, - name: defaultName, - id: _nodeId(Coin.stellarTestNet), - useSSL: true, - enabled: true, - coinName: Coin.stellarTestNet.name, - isFailover: true, - isDown: false, - ); + host: "https://horizon-testnet.stellar.org/", + port: 50022, + name: defaultName, + id: _nodeId(Coin.stellarTestnet), + useSSL: true, + enabled: true, + coinName: Coin.stellarTestnet.name, + isFailover: true, + isDown: false, + ); static NodeModel getNodeFor(Coin coin) { switch (coin) { @@ -365,7 +364,7 @@ abstract class DefaultNodes { case Coin.dogecoinTestNet: return dogecoinTestnet; - case Coin.stellarTestNet: + case Coin.stellarTestnet: return stellarTestnet; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index b8af7f53c..f5a42e8da 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -60,7 +60,7 @@ enum Coin { dogecoinTestNet, firoTestNet, litecoinTestNet, - stellarTestNet, + stellarTestnet, } final int kTestNetCoinCount = 5; // Util.isDesktop ? 5 : 4; @@ -111,7 +111,7 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; - case Coin.stellarTestNet: + case Coin.stellarTestnet: return "tStellar"; } } @@ -160,7 +160,7 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; - case Coin.stellarTestNet: + case Coin.stellarTestnet: return "tXLM"; } } @@ -210,7 +210,7 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; - case Coin.stellarTestNet: + case Coin.stellarTestnet: return "stellar"; } } @@ -240,7 +240,7 @@ extension CoinExt on Coin { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return false; } } @@ -262,7 +262,7 @@ extension CoinExt on Coin { case Coin.ethereum: case Coin.eCash: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: case Coin.tezos: return true; @@ -300,7 +300,7 @@ extension CoinExt on Coin { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return false; } } @@ -330,7 +330,7 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return true; } } @@ -370,7 +370,7 @@ extension CoinExt on Coin { case Coin.firoTestNet: return Coin.firo; - case Coin.stellarTestNet: + case Coin.stellarTestnet: return Coin.stellar; } } @@ -413,7 +413,7 @@ extension CoinExt on Coin { return particl.MINIMUM_CONFIRMATIONS; case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: return xlm.MINIMUM_CONFIRMATIONS; case Coin.tezos: @@ -534,7 +534,7 @@ Coin coinFromPrettyName(String name) { case "stellarTestnet": case "stellarTestNet": case "tStellar": - return Coin.stellarTestNet; + return Coin.stellarTestnet; default: throw ArgumentError.value( @@ -590,7 +590,7 @@ Coin coinFromTickerCaseInsensitive(String ticker) { case "ban": return Coin.banano; case "txlm": - return Coin.stellarTestNet; + return Coin.stellarTestnet; default: throw ArgumentError.value( ticker, "name", "No Coin enum value with that ticker"); diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index f3c456f67..5b94f41f6 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -50,7 +50,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.nano: case Coin.banano: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: case Coin.tezos: // TODO: Is this true? throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index b63c21b93..0f65fcfd5 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -197,7 +197,7 @@ class _NodeCardState extends ConsumerState { case Coin.banano: case Coin.tezos: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: try { testPassed = await testStellarNodeConnection(node.host, node.port); } catch (_) { diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 63fd2b13e..953ac78a1 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -179,7 +179,7 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.banano: case Coin.tezos: case Coin.stellar: - case Coin.stellarTestNet: + case Coin.stellarTestnet: throw UnimplementedError(); //TODO: check network/node } From 4de632bdb47d8ee11310bc57ae957c202caeb4cd Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 11:35:04 -0600 Subject: [PATCH 28/35] fix switch statement logic --- .../manage_nodes_views/add_edit_node_view.dart | 3 +-- .../manage_nodes_views/node_details_view.dart | 8 +++----- lib/widgets/node_card.dart | 7 ++----- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 8a09a3d03..2bb833fd1 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -205,9 +205,8 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.nano: case Coin.banano: - throw UnimplementedError(); - //TODO: check network/node case Coin.tezos: + throw UnimplementedError(); //TODO: check network/node } diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index de3a4b5e5..c05cbbca5 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -173,6 +173,9 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.nano: case Coin.banano: + case Coin.tezos: + throw UnimplementedError(); + //TODO: check network/node case Coin.stellar: case Coin.stellarTestnet: try { @@ -181,11 +184,6 @@ class _NodeDetailsViewState extends ConsumerState { testPassed = false; } break; - case Coin.nano: - case Coin.banano: - case Coin.tezos: - throw UnimplementedError(); - //TODO: check network/node } if (testPassed) { diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 0f65fcfd5..44b8a696a 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -196,6 +196,8 @@ class _NodeCardState extends ConsumerState { case Coin.nano: case Coin.banano: case Coin.tezos: + //TODO: check network/node + throw UnimplementedError(); case Coin.stellar: case Coin.stellarTestnet: try { @@ -204,11 +206,6 @@ class _NodeCardState extends ConsumerState { testPassed = false; } break; - - case Coin.nano: - case Coin.banano: - throw UnimplementedError(); - //TODO: check network/node } if (testPassed) { From 2e64d356294486af92b8b335085eded4f14aa0ed Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 11:49:21 -0600 Subject: [PATCH 29/35] tezos does not used the bip39 lib directly. Do not show new wallet options. Needs more investigation --- lib/utilities/enums/coin_enum.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index f5a42e8da..77af4b4d1 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -263,7 +263,6 @@ extension CoinExt on Coin { case Coin.eCash: case Coin.stellar: case Coin.stellarTestnet: - case Coin.tezos: return true; case Coin.epicCash: @@ -271,6 +270,7 @@ extension CoinExt on Coin { case Coin.wownero: case Coin.nano: case Coin.banano: + case Coin.tezos: return false; } } From 19a6e0b8263d9c89e5af278891b8d44062ee8abe Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 11:58:16 -0600 Subject: [PATCH 30/35] fix tezos refresh function --- lib/services/coins/tezos/tezos_wallet.dart | 68 ++++++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index 6b0d10cc6..bf0a8cf5d 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -13,6 +13,8 @@ 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'; @@ -499,11 +501,67 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future refresh() { - updateChainHeight(); - updateBalance(); - updateTransactions(); - return Future.value(); + 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 stellar wallet $walletId: '$walletName': $e\n$s", + level: LogLevel.Warning, + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + } + + refreshMutex = false; } @override From 31cac4b9502eb02b5ccc190d93dacd2e764d4cf5 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 14:12:11 -0600 Subject: [PATCH 31/35] code formatting --- .../coins/stellar/stellar_wallet.dart | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/services/coins/stellar/stellar_wallet.dart b/lib/services/coins/stellar/stellar_wallet.dart index a9f9d97df..c5ee3114b 100644 --- a/lib/services/coins/stellar/stellar_wallet.dart +++ b/lib/services/coins/stellar/stellar_wallet.dart @@ -1,6 +1,6 @@ import 'dart:async'; + import 'package:bip39/bip39.dart' as bip39; -import 'package:http/http.dart' as http; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart' as SWBalance; @@ -37,7 +37,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { late StellarSDK stellarSdk; late Network stellarNetwork; - StellarWallet({ required String walletId, required String walletName, @@ -54,7 +53,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); - if (coin.isTestNet) { stellarNetwork = Network.TESTNET; } else { @@ -66,7 +64,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { void _updateNode() { _xlmNode = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); stellarSdk = StellarSDK("${_xlmNode!.host}:${_xlmNode!.port}"); } @@ -212,13 +210,12 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { } transaction.sign(senderKeyPair, stellarNetwork); try { - SubmitTransactionResponse response = - await stellarSdk.submitTransaction(transaction).onError((error, stackTrace) => throw (error.toString())); + SubmitTransactionResponse response = await stellarSdk + .submitTransaction(transaction) + .onError((error, stackTrace) => throw (error.toString())); if (!response.success) { - throw ( - "${response.extras?.resultCodes?.transactionResultCode}" - " ::: ${response.extras?.resultCodes?.operationsResultCodes}" - ); + throw ("${response.extras?.resultCodes?.transactionResultCode}" + " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); } return response.hash!; } catch (e, s) { @@ -248,7 +245,8 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future estimateFeeFor(Amount amount, int feeRate) async { var baseFee = await getBaseFee(); - return Amount(rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals); + return Amount( + rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals); } @override @@ -276,7 +274,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get fees async { - int fee = await getBaseFee(); return FeeObject( numberOfBlocksFast: 10, @@ -402,7 +399,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { {required String address, required Amount amount, Map? args}) async { - try { final feeRate = args?["feeRate"]; var fee = 1000; @@ -433,12 +429,13 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future recoverFromMnemonic( - {required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height}) async { + Future recoverFromMnemonic({ + required String mnemonic, + String? mnemonicPassphrase, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { if ((await mnemonicString) != null || (await this.mnemonicPassphrase) != null) { throw Exception("Attempted to overwrite mnemonic on restore!"); From 4b5686d8a4c009146ec853ab3481c47d7bde41c9 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 14:31:34 -0600 Subject: [PATCH 32/35] handle stellar rescan properly --- .../coins/stellar/stellar_wallet.dart | 173 ++++++++++++++---- 1 file changed, 133 insertions(+), 40 deletions(-) diff --git a/lib/services/coins/stellar/stellar_wallet.dart b/lib/services/coins/stellar/stellar_wallet.dart index c5ee3114b..fc77bfe72 100644 --- a/lib/services/coins/stellar/stellar_wallet.dart +++ b/lib/services/coins/stellar/stellar_wallet.dart @@ -28,6 +28,7 @@ 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/utilities/test_stellar_node_connection.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; import 'package:tuple/tuple.dart'; @@ -286,16 +287,65 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future fullRescan( - int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async { - await _prefs.init(); - await updateTransactions(); - await updateChainHeight(); - await updateBalance(); + 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 _recoverWalletFromBIP32SeedPhrase( + 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, + ), + ); + + // restore from backup + // await _rescanRestore(); + + Logging.instance.log( + "Exception rethrown from fullRescan(): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } finally { + longMutex = false; + } } @override Future generateNewAddress() { - // TODO: implement generateNewAddress + // not used for stellar(?) throw UnimplementedError(); } @@ -428,6 +478,44 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { } } + Future _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + required String mnemonicPassphrase, + bool isRescan = false, + }) async { + final Wallet wallet = await Wallet.from( + mnemonic, + passphrase: mnemonicPassphrase, + ); + final KeyPair keyPair = await wallet.getKeyPair(index: 0); + final String address = keyPair.accountId; + String secretSeed = + keyPair.secretSeed; //This will be required for sending a tx + + await _secureStore.write( + key: '${_walletId}_secretSeed', + value: secretSeed, + ); + + final swAddress = SWAddress.Address( + walletId: walletId, + value: address, + publicKey: keyPair.publicKey, + derivationIndex: 0, + derivationPath: null, + type: SWAddress.AddressType.unknown, + subType: SWAddress.AddressSubType.unknown, + ); + + if (isRescan) { + await db.updateOrPutAddresses([swAddress]); + } else { + await db.putAddress(swAddress); + } + } + + bool longMutex = false; + @override Future recoverFromMnemonic({ required String mnemonic, @@ -436,37 +524,41 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { required int maxNumberOfIndexesToCheck, required int height, }) async { - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on restore!"); + 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 _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic, + mnemonicPassphrase: mnemonicPassphrase ?? "", + isRescan: false, + ); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + + rethrow; + } finally { + longMutex = false; } - - var wallet = await Wallet.from(mnemonic); - var keyPair = await wallet.getKeyPair(index: 0); - var address = keyPair.accountId; - var secretSeed = keyPair.secretSeed; - - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - await _secureStore.write(key: '${_walletId}_secretSeed', value: secretSeed); - - final swAddress = SWAddress.Address( - walletId: walletId, - value: address, - publicKey: keyPair.publicKey, - derivationIndex: 0, - derivationPath: null, - type: SWAddress.AddressType.unknown, // TODO: set type - subType: SWAddress.AddressSubType.unknown); - - await db.putAddress(swAddress); - - await Future.wait( - [updateCachedId(walletId), updateCachedIsFavorite(false)]); } Future updateChainHeight() async { @@ -622,6 +714,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance.log( "Exception rethrown from updateTransactions(): $e\n$s", level: LogLevel.Error); + rethrow; } } @@ -659,6 +752,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { "ERROR GETTING BALANCE $e\n$s", level: LogLevel.Info, ); + rethrow; } } @@ -733,9 +827,8 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { int get storedChainHeight => getCachedChainHeight(); @override - Future testNetworkConnection() { - // TODO: implement testNetworkConnection - throw UnimplementedError(); + Future testNetworkConnection() async { + return await testStellarNodeConnection(_xlmNode!.host, _xlmNode!.port); } @override @@ -786,7 +879,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - // TODO: implement utxos + // not used Future> get utxos => throw UnimplementedError(); @override From 3cd31d1bf26fa26dccbb161966d6f3c16a7eb125 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 14:56:40 -0600 Subject: [PATCH 33/35] clean up --- lib/services/coins/stellar/stellar_wallet.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/services/coins/stellar/stellar_wallet.dart b/lib/services/coins/stellar/stellar_wallet.dart index fc77bfe72..b12954628 100644 --- a/lib/services/coins/stellar/stellar_wallet.dart +++ b/lib/services/coins/stellar/stellar_wallet.dart @@ -330,9 +330,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { ), ); - // restore from backup - // await _rescanRestore(); - Logging.instance.log( "Exception rethrown from fullRescan(): $e\n$s", level: LogLevel.Error, From 6e4a23007b5fe804e87e5afe7deac27308192e8e Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 14:58:22 -0600 Subject: [PATCH 34/35] fix some rescan functionality --- lib/services/coins/tezos/tezos_wallet.dart | 180 ++++++++++++++++----- 1 file changed, 141 insertions(+), 39 deletions(-) diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index bf0a8cf5d..6e128478e 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -275,13 +275,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { ); } - @override - Future fullRescan( - int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { - refresh(); - return Future.value(); - } - @override Future generateNewAddress() { // TODO: implement generateNewAddress @@ -321,11 +314,11 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { final address = Address( walletId: walletId, value: newKeystore.address, - publicKey: [], // TODO: Add public key + publicKey: [], derivationIndex: 0, derivationPath: null, type: AddressType.unknown, - subType: AddressSubType.unknown, + subType: AddressSubType.receiving, ); await db.putAddress(address); @@ -369,40 +362,131 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future get mnemonicString => _secureStore.read(key: '${_walletId}_mnemonic'); - @override - Future recoverFromMnemonic( - {required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height}) async { - 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 ?? "", + 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.fromMnemonic(mnemonic).address, - publicKey: [], // TODO: Add public key + value: keystore.address, + publicKey: [], derivationIndex: 0, derivationPath: null, type: AddressType.unknown, - subType: AddressSubType.unknown, + subType: AddressSubType.receiving, ); - await db.putAddress(address); + if (isRescan) { + await db.updateOrPutAddresses([address]); + } else { + await db.putAddress(address); + } + } - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); + 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 { @@ -438,10 +522,17 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { for (var tx in response as List) { if (tx["type"] == "transaction") { TransactionType txType; - if (tx["sender"]["address"] == (await currentReceivingAddress)) { + final String myAddress = await currentReceivingAddress; + final String senderAddress = tx["sender"]["address"] as String; + final String targetAddress = tx["target"]["address"] as String; + if (senderAddress == myAddress && targetAddress == myAddress) { + txType = TransactionType.sentToSelf; + } else if (senderAddress == myAddress) { txType = TransactionType.outgoing; - } else { + } else if (targetAddress == myAddress) { txType = TransactionType.incoming; + } else { + txType = TransactionType.unknown; } var theTx = Transaction( @@ -470,14 +561,25 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { nonce: 0, numberOfMessages: null, ); - var theAddress = Address( + 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: tx["target"]["address"].toString(), - publicKey: [], // TODO: Add public key + value: targetAddress, + publicKey: [], derivationIndex: 0, derivationPath: null, type: AddressType.unknown, - subType: AddressSubType.unknown, + subType: subType, ); txs.add(Tuple2(theTx, theAddress)); } From 5b4e4c49037d9240134c5931ae3a9af67f9db544 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 23 Aug 2023 15:05:16 -0600 Subject: [PATCH 35/35] fix price tests to include tezos --- test/price_test.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/price_test.dart b/test/price_test.dart index 6741d5445..e5383bef8 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -28,7 +28,8 @@ void main() { Uri.parse( "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids" "=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,bitcoin-cash" - ",namecoin,wownero,ethereum,particl,nano,banano,stellar&order=market_cap_desc&per_page=50" + ",namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos" + "&order=market_cap_desc&per_page=50" "&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -114,6 +115,7 @@ void main() { 'Coin.nano: [0, 0.0], ' 'Coin.particl: [0, 0.0], ' 'Coin.stellar: [0, 0.0], ' + 'Coin.tezos: [0, 0.0], ' 'Coin.wownero: [0, 0.0], ' 'Coin.bitcoinTestNet: [0, 0.0], ' 'Coin.bitcoincashTestnet: [0, 0.0], ' @@ -128,6 +130,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" + ",tezos" "&order=market_cap_desc&per_page=50&page=1&sparkline=false", ), headers: {'Content-Type': 'application/json'})).called(1); @@ -143,6 +146,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&" "ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" + ",tezos" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -228,7 +232,10 @@ void main() { 'Coin.firo: [0.0001096, -0.89304], ' 'Coin.litecoin: [0, 0.0], ' 'Coin.namecoin: [0, 0.0], ' - 'Coin.nano: [0, 0.0], Coin.particl: [0, 0.0], Coin.stellar: [0, 0.0], ' + 'Coin.nano: [0, 0.0], ' + 'Coin.particl: [0, 0.0], ' + 'Coin.stellar: [0, 0.0], ' + 'Coin.tezos: [0, 0.0], ' 'Coin.wownero: [0, 0.0], ' 'Coin.bitcoinTestNet: [0, 0.0], ' 'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], ' @@ -244,6 +251,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids" "=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" + ",tezos" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); @@ -258,6 +266,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" + ",tezos" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -343,6 +352,7 @@ void main() { 'Coin.nano: [0, 0.0], ' 'Coin.particl: [0, 0.0], ' 'Coin.stellar: [0, 0.0], ' + 'Coin.tezos: [0, 0.0], ' 'Coin.wownero: [0, 0.0], ' 'Coin.bitcoinTestNet: [0, 0.0], ' 'Coin.bitcoincashTestnet: [0, 0.0], ' @@ -362,6 +372,7 @@ void main() { "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" + ",tezos" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' @@ -390,6 +401,7 @@ void main() { 'Coin.nano: [0, 0.0], ' 'Coin.particl: [0, 0.0], ' 'Coin.stellar: [0, 0.0], ' + 'Coin.tezos: [0, 0.0], ' 'Coin.wownero: [0, 0.0], ' 'Coin.bitcoinTestNet: [0, 0.0], ' 'Coin.bitcoincashTestnet: [0, 0.0], '