diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index fe11a5463..eb20d7e6e 100644 Binary files a/assets/default_themes/dark.zip and b/assets/default_themes/dark.zip differ diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip index 1453d6ba0..0048f661c 100644 Binary files a/assets/default_themes/light.zip and b/assets/default_themes/light.zip differ 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 692e34633..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 @@ -195,19 +195,19 @@ class _AddEditNodeViewState extends ConsumerState { // await client.getSyncStatus(); } catch (_) {} break; - case Coin.stellar: case Coin.stellarTestnet: try { - testPassed = await testStellarNodeConnection(formData.host!, formData.port!); - } catch(_) {} + testPassed = + await testStellarNodeConnection(formData.host!, formData.port!); + } catch (_) {} break; case Coin.nano: case Coin.banano: - + case Coin.tezos: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node } if (showFlushBar && mounted) { @@ -738,6 +738,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/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 3354ca396..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 @@ -171,19 +171,19 @@ class _NodeDetailsViewState extends ConsumerState { } break; - case Coin.stellar: - case Coin.stellarTestnet: - 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 + case Coin.stellar: + case Coin.stellarTestnet: + try { + testPassed = await testStellarNodeConnection(node!.host, node.port); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 94fa569e4..360372095 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -28,6 +28,7 @@ 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/stellar/stellar_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'; @@ -228,6 +229,24 @@ 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, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + tracker: tracker, + ); + case Coin.wownero: return WowneroWallet( walletId: walletId, @@ -285,15 +304,6 @@ abstract class CoinServiceAPI { cachedClient: cachedClient, tracker: tracker, ); - - case Coin.stellarTestnet: - return StellarWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStore: secureStorageInterface, - tracker: tracker, - ); } } diff --git a/lib/services/coins/stellar/stellar_wallet.dart b/lib/services/coins/stellar/stellar_wallet.dart index a9f9d97df..b12954628 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; @@ -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'; @@ -37,7 +38,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { late StellarSDK stellarSdk; late Network stellarNetwork; - StellarWallet({ required String walletId, required String walletName, @@ -54,7 +54,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); - if (coin.isTestNet) { stellarNetwork = Network.TESTNET; } else { @@ -66,7 +65,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 +211,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 +246,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 +275,6 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get fees async { - int fee = await getBaseFee(); return FeeObject( numberOfBlocksFast: 10, @@ -289,16 +287,62 @@ 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, + ), + ); + + 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(); } @@ -402,7 +446,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; @@ -432,44 +475,87 @@ 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 { - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - - 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 ?? "", + 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, ); - 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); + walletId: walletId, + value: address, + publicKey: keyPair.publicKey, + derivationIndex: 0, + derivationPath: null, + type: SWAddress.AddressType.unknown, + subType: SWAddress.AddressSubType.unknown, + ); - await db.putAddress(swAddress); + if (isRescan) { + await db.updateOrPutAddresses([swAddress]); + } else { + await db.putAddress(swAddress); + } + } - await Future.wait( - [updateCachedId(walletId), updateCachedIsFavorite(false)]); + bool 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 _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; + } } Future updateChainHeight() async { @@ -625,6 +711,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance.log( "Exception rethrown from updateTransactions(): $e\n$s", level: LogLevel.Error); + rethrow; } } @@ -662,6 +749,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { "ERROR GETTING BALANCE $e\n$s", level: LogLevel.Info, ); + rethrow; } } @@ -736,9 +824,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 @@ -789,7 +876,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - // TODO: implement utxos + // not used Future> get utxos => throw UnimplementedError(); @override diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart new file mode 100644 index 000000000..6e128478e --- /dev/null +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -0,0 +1,741 @@ +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'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/wallet_cache.dart'; +import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tezart/tezart.dart'; +import 'package:tuple/tuple.dart'; + +const int MINIMUM_CONFIRMATIONS = 1; +const int _gasLimit = 10200; + +class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { + TezosWallet({ + required String walletId, + required String walletName, + required Coin coin, + required SecureStorageInterface secureStore, + required TransactionNotificationTracker tracker, + MainDB? mockableOverride, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _secureStore = secureStore; + initCache(walletId, coin); + initWalletDB(mockableOverride: mockableOverride); + } + + NodeModel? _xtzNode; + + NodeModel getCurrentNode() { + return _xtzNode ?? + NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: Coin.tezos) ?? + DefaultNodes.getNodeFor(Coin.tezos); + } + + Future getKeystore() async { + return Keystore.fromMnemonic((await mnemonicString).toString()); + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + @override + set walletName(String name) => _walletName = name; + + @override + set isFavorite(bool markFavorite) { + _isFavorite = markFavorite; + updateCachedIsFavorite(markFavorite); + } + + @override + bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); + bool? _isFavorite; + + @override + Coin get coin => _coin; + late Coin _coin; + + late SecureStorageInterface _secureStore; + late final TransactionNotificationTracker txTracker; + final _prefs = Prefs.instance; + + Timer? timer; + bool _shouldAutoSync = false; + Timer? _networkAliveTimer; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + status, + walletId, + coin, + ), + ); + + _isConnected = hasNetwork; + if (hasNetwork) { + unawaited(refresh()); + } + } + } + + @override + Balance get balance => _balance ??= getCachedBalance(); + Balance? _balance; + + @override + Future> prepareSend( + {required String address, + required Amount amount, + Map? args}) async { + try { + if (amount.decimals != coin.decimals) { + throw Exception("Amount decimals do not match coin decimals!"); + } + var fee = int.parse((await estimateFeeFor( + amount, (args!["feeRate"] as FeeRateType).index)) + .raw + .toString()); + Map txData = { + "fee": fee, + "address": address, + "recipientAmt": amount, + }; + return Future.value(txData); + } catch (e) { + return Future.error(e); + } + } + + @override + Future confirmSend({required Map txData}) async { + try { + final amount = txData["recipientAmt"] as Amount; + final amountInMicroTez = amount.decimal * Decimal.fromInt(1000000); + final microtezToInt = int.parse(amountInMicroTez.toString()); + + final int feeInMicroTez = int.parse(txData["fee"].toString()); + final String destinationAddress = txData["address"] as String; + final secretKey = + Keystore.fromMnemonic((await mnemonicString)!).secretKey; + + Logging.instance.log(secretKey, level: LogLevel.Info); + final sourceKeyStore = Keystore.fromSecretKey(secretKey); + final client = TezartClient(getCurrentNode().host); + + int? sendAmount = microtezToInt; + int gasLimit = _gasLimit; + int thisFee = feeInMicroTez; + + if (balance.spendable == txData["recipientAmt"] as Amount) { + //Fee guides for emptying a tz account + // https://github.com/TezTech/eztz/blob/master/PROTO_004_FEES.md + thisFee = thisFee + 32; + sendAmount = microtezToInt - thisFee; + gasLimit = _gasLimit + 320; + } + + final operation = await client.transferOperation( + source: sourceKeyStore, + destination: destinationAddress, + amount: sendAmount, + customFee: feeInMicroTez, + customGasLimit: gasLimit); + await operation.executeAndMonitor(); + return operation.result.id as String; + } catch (e) { + Logging.instance.log(e.toString(), level: LogLevel.Error); + return Future.error(e); + } + } + + @override + Future get currentReceivingAddress async { + var mneString = await mnemonicString; + if (mneString == null) { + throw Exception("No mnemonic found!"); + } + return Future.value((Keystore.fromMnemonic(mneString)).address); + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + 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(); + + return Amount( + rawValue: BigInt.from(feePerTx), + fractionDigits: coin.decimals, + ); + } + + @override + Future exit() { + _hasCalledExit = true; + return Future.value(); + } + + @override + Future get fees async { + 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 - Since there is only one fee no need to set blocks + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: feePerTx, + medium: feePerTx, + slow: feePerTx, + ); + } + + @override + Future generateNewAddress() { + // TODO: implement generateNewAddress + throw UnimplementedError(); + } + + @override + bool get hasCalledExit => _hasCalledExit; + bool _hasCalledExit = false; + + @override + Future initializeExisting() async { + await _prefs.init(); + } + + @override + Future initializeNew( + ({String mnemonicPassphrase, int wordCount})? data, + ) async { + if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + + await _prefs.init(); + + var newKeystore = Keystore.random(); + await _secureStore.write( + key: '${_walletId}_mnemonic', + value: newKeystore.mnemonic, + ); + await _secureStore.write( + key: '${_walletId}_mnemonicPassphrase', + value: "", + ); + + final address = Address( + walletId: walletId, + value: newKeystore.address, + publicKey: [], + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.receiving, + ); + + await db.putAddress(address); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } + + @override + bool get isConnected => _isConnected; + bool _isConnected = false; + + @override + bool get isRefreshing => refreshMutex; + bool refreshMutex = false; + + @override + // TODO: implement maxFee + Future get maxFee => throw UnimplementedError(); + + @override + Future> get mnemonic async { + final mnemonic = await mnemonicString; + final mnemonicPassphrase = await this.mnemonicPassphrase; + if (mnemonic == null) { + throw Exception("No mnemonic found!"); + } + if (mnemonicPassphrase == null) { + throw Exception("No mnemonic passphrase found!"); + } + return mnemonic.split(" "); + } + + @override + Future get mnemonicPassphrase => + _secureStore.read(key: '${_walletId}_mnemonicPassphrase'); + + @override + Future get mnemonicString => + _secureStore.read(key: '${_walletId}_mnemonic'); + + Future _recoverWalletFromSeedPhrase({ + required String mnemonic, + required String mnemonicPassphrase, + bool isRescan = false, + }) async { + final keystore = Keystore.fromMnemonic( + mnemonic, + password: mnemonicPassphrase, + ); + + final address = Address( + walletId: walletId, + value: keystore.address, + publicKey: [], + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.receiving, + ); + + if (isRescan) { + await db.updateOrPutAddresses([address]); + } else { + await db.putAddress(address); + } + } + + bool longMutex = false; + @override + Future fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + try { + Logging.instance.log("Starting full rescan!", level: LogLevel.Info); + longMutex = true; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + final _mnemonic = await mnemonicString; + final _mnemonicPassphrase = await mnemonicPassphrase; + + await db.deleteWalletBlockchainData(walletId); + + await _recoverWalletFromSeedPhrase( + mnemonic: _mnemonic!, + mnemonicPassphrase: _mnemonicPassphrase!, + isRescan: true, + ); + + await refresh(); + Logging.instance.log("Full rescan complete!", level: LogLevel.Info); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } catch (e, s) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + + Logging.instance.log( + "Exception rethrown from fullRescan(): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } finally { + longMutex = false; + } + } + + @override + Future recoverFromMnemonic({ + required String mnemonic, + String? mnemonicPassphrase, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + longMutex = true; + try { + if ((await mnemonicString) != null || + (await this.mnemonicPassphrase) != null) { + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + await _secureStore.write( + key: '${_walletId}_mnemonicPassphrase', + value: mnemonicPassphrase ?? "", + ); + + await _recoverWalletFromSeedPhrase( + mnemonic: mnemonic, + mnemonicPassphrase: mnemonicPassphrase ?? "", + isRescan: false, + ); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + + await refresh(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + + rethrow; + } finally { + longMutex = false; + } + } + + Future updateBalance() async { + try { + 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(response.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 { + 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) { + if (tx["type"] == "transaction") { + TransactionType txType; + final String myAddress = await currentReceivingAddress; + final String senderAddress = tx["sender"]["address"] as String; + final String targetAddress = tx["target"]["address"] as String; + if (senderAddress == myAddress && targetAddress == myAddress) { + txType = TransactionType.sentToSelf; + } else if (senderAddress == myAddress) { + txType = TransactionType.outgoing; + } else if (targetAddress == myAddress) { + txType = TransactionType.incoming; + } else { + txType = TransactionType.unknown; + } + + 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, + ); + 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: targetAddress, + publicKey: [], + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: subType, + ); + txs.add(Tuple2(theTx, theAddress)); + } + } + Logging.instance.log("Transactions: $txs", level: LogLevel.Info); + await db.addNewTransactionData(txs, walletId); + } + + Future updateChainHeight() async { + 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 + 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 + int get storedChainHeight => getCachedChainHeight(); + + @override + Future testNetworkConnection() async { + try { + await get(Uri.parse( + "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell")); + return true; + } catch (e) { + return false; + } + } + + @override + Future> get transactions => + db.getTransactions(walletId).findAll(); + + @override + Future updateNode(bool shouldRefresh) async { + _xtzNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + if (shouldRefresh) { + await refresh(); + } + } + + @override + Future updateSentCachedTxData(Map txData) async { + final transaction = Transaction( + walletId: walletId, + txid: txData["txid"] as String, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.outgoing, + subType: TransactionSubType.none, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), + fee: txData["fee"] as int, + height: null, + isCancelled: false, + isLelantus: false, + otherData: null, + slateId: null, + nonce: null, + inputs: [], + outputs: [], + numberOfMessages: null, + ); + + final address = txData["address"] is String + ? await db.getAddress(walletId, txData["address"] as String) + : null; + + await db.addNewTransactionData( + [ + Tuple2(transaction, address), + ], + walletId, + ); + } + + @override + // TODO: implement utxos + Future> get utxos => throw UnimplementedError(); + + @override + bool validateAddress(String address) { + return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); + } +} diff --git a/lib/services/price.dart b/lib/services/price.dart index 66842c9d8..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" - "&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/themes/color_theme.dart b/lib/themes/color_theme.dart index c78d35c11..abec28d4e 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -31,6 +31,7 @@ class CoinThemeColorDefault { Color get stellar => const Color(0xFF6600FF); Color get nano => const Color(0xFF209CE9); Color get banano => const Color(0xFFFBDD11); + Color get tezos => const Color(0xFF0F61FF); Color forCoin(Coin coin) { switch (coin) { @@ -70,6 +71,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 5dbd55fe1..cbec0077a 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1714,6 +1714,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 8766d706a..3fa895282 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -111,6 +111,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/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index ccdd4e095..6a646fd11 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -52,6 +52,7 @@ enum AmountUnit { case Coin.epicCash: case Coin.stellar: // TODO: check if this is correct case Coin.stellarTestnet: + case Coin.tezos: return AmountUnit.values.sublist(0, 4); case Coin.monero: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 831332320..bb4ac06fb 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -62,6 +62,8 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://www.bananolooker.com/block/$txid"); 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 ae357c5b3..1253e08f1 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -34,7 +34,6 @@ abstract class Constants { // just use enable exchange flag // static bool enableBuy = enableExchange; // // true; // true for development, - static final BigInt _satsPerCoinECash = BigInt.from(100); static final BigInt _satsPerCoinEthereum = BigInt.from(1000000000000000000); static final BigInt _satsPerCoinMonero = BigInt.from(1000000000000); @@ -43,8 +42,10 @@ 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; static const int _decimalPlacesNano = 30; static const int _decimalPlacesBanano = 29; @@ -53,6 +54,7 @@ abstract class Constants { static const int _decimalPlacesEthereum = 18; static const int _decimalPlacesECash = 2; static const int _decimalPlacesStellar = 7; + static const int _decimalPlacesTezos = 6; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -102,6 +104,9 @@ abstract class Constants { case Coin.stellar: case Coin.stellarTestnet: return _satsPerCoinStellar; + + case Coin.tezos: + return _satsPerCoinTezos; } } @@ -143,6 +148,9 @@ abstract class Constants { case Coin.stellar: case Coin.stellarTestnet: return _decimalPlacesStellar; + + case Coin.tezos: + return _decimalPlacesTezos; } } @@ -172,6 +180,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]); @@ -230,6 +240,9 @@ abstract class Constants { case Coin.stellar: case Coin.stellarTestnet: return 5; + + case Coin.tezos: + return 60; } } @@ -259,6 +272,7 @@ abstract class Constants { case Coin.banano: case Coin.stellar: case Coin.stellarTestnet: + case Coin.tezos: return 24; case Coin.monero: diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 9a6a0d914..210959f65 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -191,8 +191,19 @@ 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 + 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", @@ -277,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) { @@ -335,6 +346,9 @@ abstract class DefaultNodes { 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 6fe2ffa87..77af4b4d1 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -28,6 +28,7 @@ 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/stellar/stellar_wallet.dart' as xlm; +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'; @@ -46,6 +47,7 @@ enum Coin { nano, particl, stellar, + tezos, wownero, /// @@ -61,7 +63,7 @@ enum Coin { 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 { @@ -89,6 +91,8 @@ extension CoinExt on Coin { return "Particl"; case Coin.stellar: return "Stellar"; + case Coin.tezos: + return "Tezos"; case Coin.wownero: return "Wownero"; case Coin.namecoin: @@ -136,6 +140,8 @@ extension CoinExt on Coin { return "PART"; case Coin.stellar: return "XLM"; + case Coin.tezos: + return "XTZ"; case Coin.wownero: return "WOW"; case Coin.namecoin: @@ -184,6 +190,8 @@ extension CoinExt on Coin { return "particl"; case Coin.stellar: return "stellar"; + case Coin.tezos: + return "tezos"; case Coin.wownero: return "wownero"; case Coin.namecoin: @@ -227,6 +235,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: @@ -261,6 +270,7 @@ extension CoinExt on Coin { case Coin.wownero: case Coin.nano: case Coin.banano: + case Coin.tezos: return false; } } @@ -280,6 +290,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: @@ -306,6 +317,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: @@ -335,6 +347,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: @@ -403,6 +416,9 @@ extension CoinExt on Coin { case Coin.stellarTestnet: return xlm.MINIMUM_CONFIRMATIONS; + case Coin.tezos: + return tezos.MINIMUM_CONFIRMATIONS; + case Coin.wownero: return wow.MINIMUM_CONFIRMATIONS; @@ -466,6 +482,10 @@ Coin coinFromPrettyName(String name) { case "stellar": return Coin.stellar; + case "Tezos": + case "tezos": + return Coin.tezos; + case "Namecoin": case "namecoin": return Coin.namecoin; @@ -512,6 +532,7 @@ Coin coinFromPrettyName(String name) { case "Stellar Testnet": case "stellarTestnet": + case "stellarTestNet": case "tStellar": return Coin.stellarTestnet; @@ -550,6 +571,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.particl; case "xlm": return Coin.stellar; + 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 07785a5ee..5b94f41f6 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -51,6 +51,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.banano: case Coin.stellar: 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 81fe62357..44b8a696a 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -193,19 +193,19 @@ class _NodeCardState extends ConsumerState { } break; + case Coin.nano: + case Coin.banano: + case Coin.tezos: + //TODO: check network/node + throw UnimplementedError(); case Coin.stellar: case Coin.stellarTestnet: try { testPassed = await testStellarNodeConnection(node.host, node.port); - } catch(_) { + } catch (_) { testPassed = false; } break; - - case Coin.nano: - case Coin.banano: - throw UnimplementedError(); - //TODO: check network/node } if (testPassed) { diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index b55f818a2..953ac78a1 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -1,6 +1,6 @@ -/* +/* * This file is part of Stack Wallet. - * + * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. @@ -177,10 +177,11 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.nano: case Coin.banano: + case Coin.tezos: case Coin.stellar: case Coin.stellarTestnet: throw UnimplementedError(); - //TODO: check network/node + //TODO: check network/node } if (testPassed) { diff --git a/pubspec.lock b/pubspec.lock index cfdf7a710..c01aca249 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: @@ -454,10 +462,10 @@ packages: dependency: transitive 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: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" + url: "https://pub.dev" + source: hosted + version: "1.2.0-beta-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: @@ -1471,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: @@ -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 cfdb08711..dbb5ecfe5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,7 +138,8 @@ 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: flutter_test: @@ -199,7 +200,9 @@ dependency_overrides: url: https://github.com/cypherstack/stack-bip39.git ref: 0cd6d54e2860bea68fc50c801cb9db2a760192fb - + crypto: 3.0.2 + analyzer: ^5.2.0 + pinenacl: ^0.3.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec 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], '