Merge pull request #149 from cypherstack/staging

Staging
This commit is contained in:
Rylee Davis 2022-10-17 16:43:25 -06:00 committed by GitHub
commit 5c9c06bf7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 6993 additions and 5445 deletions

View file

@ -17,15 +17,22 @@ Highlights include:
- Custom Nodes.
- Open source software.
## Build and run
## Building
### Prerequisites
- The only OS supported for building is Ubuntu 20.04
- A machine with at least 100 GB of Storage
The following prerequisities can be installed with the setup script `scripts/setup.sh` or manually as described below:
- Flutter 3.0.5 [(install manually or with git, do not install with snap)](https://docs.flutter.dev/get-started/install)
- Dart SDK Requirement (>=2.17.0, up until <3.0.0)
- Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies)
After that download the project and init the submodules
### Scripted setup
[`scripts/setup.sh`](https://github.com/cypherstack/stack_wallet/blob/main/scripts/setup.sh) is provided as a tool to set up a stock Ubuntu 20.04 installation for building: download the script and run it anywhere. This script should skip the entire [Manual setup](#manual-setup) section below and prepare you for [running](#running). It will set up the stack_wallet repository in `~/projects/stack_wallet` and build it there.
### Manual setup
After installing the prerequisites listed above, download the code and init the submodules
```
git clone https://github.com/cypherstack/stack_wallet.git
cd stack_wallet
@ -57,7 +64,6 @@ cd scripts
cd ..
```
Remove pre-installed system libraries for the following packages built by cryptography plugins in the crypto_plugins folder: `boost iconv libjson-dev libsecret openssl sodium unbound zmq`. You can use
```
sudo apt list --installed | grep boost
@ -76,7 +82,7 @@ cd scripts/android/
cd ../..
```
Building plugins for testing on Linux
Building plugins for Linux
```
cd scripts/linux/
@ -85,10 +91,33 @@ cd scripts/linux/
cd ../..
```
Finally, plug in your android device or use the emulator available via Android Studio and then run the following commands:
## Running
### Android
Plug in your android device or use the emulator available via Android Studio and then run the following commands:
```
flutter pub get
flutter run
flutter run android
```
Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work
### Linux
Plug in your android device or use the emulator available via Android Studio and then run the following commands:
```
flutter pub get Linux
flutter run linux
```
## Android Studio
Android Studio is the recommended IDE for development, not just for launching on Android devices and emulators but also for Linux desktop development. Install it and configure it as follows:
```
# setup android studio
sudo apt install -y openjdk-11-jdk
sudo snap install android-studio --classic
```
Use Tools > SDK Manager to install the SDK Tools > Android SDK (API 30), SDK Tools > NDK, SDK Tools > Android SDK command line tools, and SDK Tools > CMake
Then install the Flutter plugin and restart the IDE. In Android Studio's options for the Flutter language, enable auto format on save to match the project's code style. If you have problems with the Dart SDK, make sure to run `flutter` in a terminal to download it (use `source ~/.bashrc` to update your environment variables if you're still using the same terminal from which you ran `setup.sh`)
Make a Pixel 4 (API 30) x86_64 emulator with 2GB of storage space for emulation

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.55104 16.4654C5.53144 16.5046 5.51184 16.5439 5.49224 16.5831C5.29624 17.0541 5.19824 17.5643 5.19824 18.0745C5.19824 18.5847 5.29624 19.095 5.49224 19.5659C5.68824 20.0369 5.98224 20.4686 6.35464 20.8219C6.72704 21.1751 7.15824 21.4695 7.62864 21.6657C8.09904 21.8619 8.62824 21.96 9.13784 21.96C9.64744 21.96 10.1766 21.8619 10.647 21.6657C11.1174 21.4695 11.5682 21.1751 11.921 20.8219L19.1926 13.6395C19.957 12.8938 20.3686 11.8734 20.3686 10.8137C20.3686 9.87174 20.0746 8.94942 19.4278 8.22334C19.1142 8.08597 18.7614 8.04672 18.4086 8.10559C18.095 8.16447 17.8206 8.34108 17.5854 8.55694L10.7058 15.5823C10.5686 15.7197 10.4314 15.8178 10.2746 15.8963C10.353 15.8374 10.451 15.7982 10.5294 15.7393H10.5098C10.5098 15.7393 10.5294 15.7393 10.5294 15.7197C10.5294 15.7197 10.549 15.7197 10.549 15.7C10.5294 15.7 10.5294 15.7197 10.5098 15.7197C9.47104 16.4065 8.21664 16.7597 6.96224 16.6812C6.47224 16.6812 6.00184 16.6027 5.55104 16.4654Z" fill="#104ADE"/>
<path d="M10.5122 15.739C10.5318 15.739 10.5318 15.7194 10.5514 15.7194C10.277 15.8175 10.0026 15.8567 9.7086 15.8371C9.4146 15.7979 9.1598 15.6997 8.9246 15.5428C8.709 15.4054 8.5522 15.2091 8.415 14.9933C8.2974 14.7774 8.219 14.5419 8.1798 14.2868C8.1602 14.0317 8.1994 13.7766 8.2778 13.5411C8.3562 13.3056 8.4934 13.0898 8.6698 12.9132L10.963 10.6172L14.3146 7.30072C14.3146 7.30072 14.3146 7.30072 14.3342 7.2811L14.3538 7.26147L14.8634 6.77088C15.1182 6.71201 15.373 6.69238 15.6474 6.69238C16.2354 6.69238 16.745 6.75125 17.235 6.88862C17.9602 7.04561 18.6266 7.39884 19.1558 7.92869C19.2538 8.02681 19.3518 8.12493 19.4302 8.24267C19.1166 8.1053 18.7442 8.06605 18.411 8.12493C18.0974 8.1838 17.823 8.36041 17.5878 8.57627L10.7082 15.582C10.571 15.7194 10.4338 15.8175 10.277 15.896C10.3554 15.8371 10.4534 15.7979 10.5318 15.739C10.5122 15.739 10.5122 15.739 10.5122 15.739Z" fill="#0038C7"/>
<path d="M16.0775 2.43415C15.9011 2.00243 15.6267 1.59033 15.2739 1.25672C14.8819 0.864242 14.4115 0.55026 13.9019 0.354021C13.3923 0.138158 12.8435 0.0400391 12.2751 0.0400391C11.7263 0.0400391 11.1579 0.157782 10.6483 0.354021C10.1387 0.569884 9.66834 0.864242 9.27634 1.25672L3.29834 7.16351C2.23994 8.20357 1.65194 9.59687 1.63234 11.0687C1.61274 12.5404 2.16154 13.9534 3.18074 15.0327L3.23954 15.0916L3.29834 15.1504C3.35714 15.2093 3.41594 15.2682 3.47474 15.3074C4.43514 16.1316 5.65034 16.6222 6.92434 16.7007C8.19834 16.7792 9.47234 16.426 10.5111 15.7195C10.2367 15.8176 9.96234 15.8569 9.66834 15.8373C9.37434 15.798 9.11954 15.6999 8.88434 15.5429C8.66874 15.4055 8.51194 15.2093 8.37474 14.9934C8.25714 14.7776 8.17874 14.5421 8.13954 14.287C8.11994 14.0319 8.15914 13.7768 8.23754 13.5413C8.31594 13.3058 8.45314 13.0899 8.62954 12.9133L10.9227 10.6173L14.2743 7.30087C14.2939 7.28125 14.2939 7.28125 14.3135 7.26163L15.2151 6.35893C15.7247 5.84871 16.0775 5.20112 16.2147 4.51428C16.4303 3.82745 16.3519 3.10136 16.0775 2.43415Z" fill="#0F75FC"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
@ -451,7 +451,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 63;
CURRENT_PROJECT_VERSION = 75;
DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -505,7 +505,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
);
MARKETING_VERSION = 1.4.48;
MARKETING_VERSION = 1.5.5;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -635,7 +635,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 63;
CURRENT_PROJECT_VERSION = 75;
DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -689,7 +689,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
);
MARKETING_VERSION = 1.4.48;
MARKETING_VERSION = 1.5.5;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -711,7 +711,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 63;
CURRENT_PROJECT_VERSION = 75;
DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -765,7 +765,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
);
MARKETING_VERSION = 1.4.48;
MARKETING_VERSION = 1.5.5;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -4,6 +4,7 @@ import 'package:cw_core/wallet_info.dart' as xmr;
import 'package:hive/hive.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/notification_model.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
@ -22,6 +23,7 @@ class DB {
"watchedTxNotificationModels";
static const String boxNameWatchedTrades = "watchedTradesNotificationModels";
static const String boxNameTrades = "exchangeTransactionsBox";
static const String boxNameTradesV2 = "exchangeTradesBox";
static const String boxNameTradeNotes = "tradeNotesBox";
static const String boxNameTradeLookup = "tradeToTxidLookUpBox";
static const String boxNameFavoriteWallets = "favoriteWallets";
@ -48,6 +50,7 @@ class DB {
late final Box<NotificationModel> _boxWatchedTransactions;
late final Box<NotificationModel> _boxWatchedTrades;
late final Box<ExchangeTransaction> _boxTrades;
late final Box<Trade> _boxTradesV2;
late final Box<String> _boxTradeNotes;
late final Box<String> _boxFavoriteWallets;
late final Box<xmr.WalletInfo> _walletInfoSource;
@ -125,6 +128,7 @@ class DB {
_boxWatchedTrades =
await Hive.openBox<NotificationModel>(boxNameWatchedTrades);
_boxTrades = await Hive.openBox<ExchangeTransaction>(boxNameTrades);
_boxTradesV2 = await Hive.openBox<Trade>(boxNameTradesV2);
_boxTradeNotes = await Hive.openBox<String>(boxNameTradeNotes);
_boxTradeLookup =
await Hive.openBox<TradeWalletLookup>(boxNameTradeLookup);

View file

@ -18,12 +18,12 @@ import 'package:path_provider/path_provider.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/models/log.dart';
import 'package:stackwallet/models/models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/notification_model.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
import 'package:stackwallet/pages/exchange_view/exchange_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/pages/loading_view.dart';
@ -31,13 +31,6 @@ import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart';
import 'package:stackwallet/providers/exchange/available_floating_rate_pairs_state_provider.dart';
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
import 'package:stackwallet/providers/exchange/changenow_initial_load_status.dart';
import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/exchange/fixed_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/exchange/fixed_rate_market_pairs_provider.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/global/base_currencies_provider.dart';
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
@ -46,6 +39,8 @@ import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/color_theme_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/debug_service.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/services/locale_service.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/notifications_api.dart';
@ -124,6 +119,8 @@ void main() async {
Hive.registerAdapter(ExchangeTransactionAdapter());
Hive.registerAdapter(ExchangeTransactionStatusAdapter());
Hive.registerAdapter(TradeAdapter());
// reference lookup data adapter
Hive.registerAdapter(TradeWalletLookupAdapter());
@ -225,7 +222,6 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
nodeService: _nodeService,
tradesService: _tradesService,
prefs: _prefs,
changeNow: ref.read(changeNowProvider),
);
await _prefs.init();
ref.read(priceAnd24hChangeNotifierProvider).start(true);
@ -234,6 +230,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
// TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
// unawaited(_nodeService.updateCommunityNodes());
// run without awaiting
if (Constants.enableExchange && _prefs.externalCalls) {
unawaited(ExchangeDataLoadingService().loadAll(ref));
}
if (_prefs.isAutoBackupEnabled) {
switch (_prefs.backupFrequencyType) {
case BackupFrequencyType.everyTenMinutes:
@ -251,112 +252,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
}
}
Future<void> _loadChangeNowStandardCurrencies() async {
if (ref
.read(availableChangeNowCurrenciesStateProvider.state)
.state
.isNotEmpty &&
ref
.read(availableFloatingRatePairsStateProvider.state)
.state
.isNotEmpty) {
return;
}
final response = await ref.read(changeNowProvider).getAvailableCurrencies();
final response2 =
await ref.read(changeNowProvider).getAvailableFloatingRatePairs();
if (response.value != null) {
ref.read(availableChangeNowCurrenciesStateProvider.state).state =
response.value!;
if (response2.value != null) {
ref.read(availableFloatingRatePairsStateProvider.state).state =
response2.value!;
if (response.value!.length > 1) {
if (ref.read(estimatedRateExchangeFormProvider).from == null) {
if (response.value!.where((e) => e.ticker == "btc").isNotEmpty) {
await ref.read(estimatedRateExchangeFormProvider).updateFrom(
response.value!.firstWhere((e) => e.ticker == "btc"), false);
}
}
if (ref.read(estimatedRateExchangeFormProvider).to == null) {
if (response.value!.where((e) => e.ticker == "doge").isNotEmpty) {
await ref.read(estimatedRateExchangeFormProvider).updateTo(
response.value!.firstWhere((e) => e.ticker == "doge"), false);
}
}
}
} else {
Logging.instance.log(
"Failed to load changeNOW available floating rate pairs: ${response2.exception?.errorMessage}",
level: LogLevel.Error);
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.failed;
return;
}
} else {
Logging.instance.log(
"Failed to load changeNOW currencies: ${response.exception?.errorMessage}",
level: LogLevel.Error);
await Future<void>.delayed(const Duration(seconds: 1));
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.failed;
return;
}
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.success;
}
Future<void> _loadFixedRateMarkets() async {
Logging.instance.log("Starting initial fixed rate market data loading...",
level: LogLevel.Info);
if (ref.read(fixedRateMarketPairsStateProvider.state).state.isNotEmpty) {
return;
}
final response3 =
await ref.read(changeNowProvider).getAvailableFixedRateMarkets();
if (response3.value != null) {
ref.read(fixedRateMarketPairsStateProvider.state).state =
response3.value!;
if (ref.read(fixedRateExchangeFormProvider).market == null) {
final matchingMarkets =
response3.value!.where((e) => e.to == "doge" && e.from == "btc");
if (matchingMarkets.isNotEmpty) {
await ref
.read(fixedRateExchangeFormProvider)
.updateMarket(matchingMarkets.first, true);
}
}
Logging.instance.log("Initial fixed rate market data loading complete.",
level: LogLevel.Info);
} else {
Logging.instance.log(
"Failed to load changeNOW fixed rate markets: ${response3.exception?.errorMessage}",
level: LogLevel.Error);
ref.read(changeNowFixedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.failed;
return;
}
ref.read(changeNowFixedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.success;
}
Future<void> _loadChangeNowData() async {
List<Future<dynamic>> concurrentFutures = [];
concurrentFutures.add(_loadChangeNowStandardCurrencies());
if (kFixedRateEnabled) {
concurrentFutures.add(_loadFixedRateMarkets());
}
}
@override
void initState() {
ref.read(exchangeFormStateProvider).exchange = ChangeNowExchange();
final colorScheme = DB.instance
.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") as String?;
@ -637,11 +535,6 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
if (_wallets.hasWallets || _prefs.hasPin) {
// return HomeView();
// run without awaiting
if (Constants.enableExchange) {
_loadChangeNowData();
}
String? startupWalletId;
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
startupWalletId =

View file

@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
class AvailableFloatingRatePair {
final String fromTicker;
final String toTicker;
AvailableFloatingRatePair({
required this.fromTicker,
required this.toTicker,
});
@override
bool operator ==(other) {
return other is AvailableFloatingRatePair &&
fromTicker == other.fromTicker &&
toTicker == other.toTicker;
}
@override
int get hashCode => hashValues(fromTicker, toTicker);
@override
String toString() {
return "${fromTicker}_$toTicker";
}
}

View file

@ -1,24 +0,0 @@
enum ChangeNowExceptionType { generic, serializeResponseError }
class ChangeNowException implements Exception {
String errorMessage;
ChangeNowExceptionType type;
ChangeNowException(this.errorMessage, this.type);
@override
String toString() {
return errorMessage;
}
}
class ChangeNowResponse<T> {
late final T? value;
late final ChangeNowException? exception;
ChangeNowResponse({this.value, this.exception});
@override
String toString() {
return "{ error: $exception, value: $value }";
}
}

View file

@ -0,0 +1,24 @@
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
class CNAvailableCurrencies {
final List<Currency> currencies = [];
final List<Pair> pairs = [];
final List<FixedRateMarket> markets = [];
void updateCurrencies(List<Currency> newCurrencies) {
currencies.clear();
currencies.addAll(newCurrencies);
}
void updateFloatingPairs(List<Pair> newPairs) {
pairs.clear();
pairs.addAll(newPairs);
}
void updateMarkets(List<FixedRateMarket> newMarkets) {
markets.clear();
markets.addAll(newMarkets);
}
}

View file

@ -35,8 +35,10 @@ class EstimatedExchangeAmount {
factory EstimatedExchangeAmount.fromJson(Map<String, dynamic> json) {
try {
return EstimatedExchangeAmount(
estimatedAmount: Decimal.parse(json["estimatedAmount"].toString()),
transactionSpeedForecast: json["transactionSpeedForecast"] as String,
estimatedAmount: Decimal.parse(json["estimatedAmount"]?.toString() ??
json["estimatedDeposit"].toString()),
transactionSpeedForecast:
json["transactionSpeedForecast"] as String? ?? "",
warningMessage: json["warningMessage"] as String?,
rateId: json["rateId"] as String?,
networkFee: Decimal.tryParse(json["networkFee"].toString()),

View file

@ -5,6 +5,8 @@ import 'package:uuid/uuid.dart';
part '../../type_adaptors/exchange_transaction.g.dart';
@Deprecated(
"Do not use. Migrated to Trade in db_version_migration to hive_data_version 2")
// @HiveType(typeId: 13)
class ExchangeTransaction {
/// You can use it to get transaction status at the Transaction status API endpoint

View file

@ -1,282 +0,0 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/services/change_now/change_now.dart';
import 'package:stackwallet/utilities/logger.dart';
class EstimatedRateExchangeFormState extends ChangeNotifier {
/// used in testing to inject mock
ChangeNow? cnTesting;
Decimal? _fromAmount;
Decimal? _toAmount;
Decimal? _minFromAmount;
Decimal? _minToAmount;
Decimal? rate;
Currency? _from;
Currency? _to;
void Function(String)? _onError;
Currency? get from => _from;
Currency? get to => _to;
String get fromAmountString =>
_fromAmount == null ? "" : _fromAmount!.toStringAsFixed(8);
String get toAmountString =>
_toAmount == null ? "" : _toAmount!.toStringAsFixed(8);
String get rateDisplayString {
if (rate == null || from == null || to == null) {
return "N/A";
} else {
return "1 ${from!.ticker.toUpperCase()} ~${rate!.toStringAsFixed(8)} ${to!.ticker.toUpperCase()}";
}
}
bool get canExchange {
return _fromAmount != null &&
_fromAmount != Decimal.zero &&
_toAmount != null &&
rate != null &&
minimumSendWarning.isEmpty;
}
String get minimumSendWarning {
if (_from != null &&
_fromAmount != null &&
_minFromAmount != null &&
_fromAmount! < _minFromAmount!) {
return "Minimum amount ${_minFromAmount!.toString()} ${from!.ticker.toUpperCase()}";
}
return "";
}
Future<void> init(Currency? from, Currency? to) async {
_from = from;
_to = to;
}
void clearAmounts(bool shouldNotifyListeners) {
_fromAmount = null;
_toAmount = null;
_minFromAmount = null;
_minToAmount = null;
rate = null;
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> swap() async {
final Decimal? newToAmount = _fromAmount;
final Decimal? newFromAmount = _toAmount;
final Decimal? newMinFromAmount = _minToAmount;
final Decimal? newMinToAmount = _minFromAmount;
final Currency? newTo = from;
final Currency? newFrom = to;
_fromAmount = newFromAmount;
_toAmount = newToAmount;
_minToAmount = newMinToAmount;
_minFromAmount = newMinFromAmount;
// rate = newRate;
_to = newTo;
_from = newFrom;
await _updateMinFromAmount(shouldNotifyListeners: false);
await updateRate();
notifyListeners();
}
Future<void> updateTo(Currency to, bool shouldNotifyListeners) async {
try {
_to = to;
if (_from == null) {
rate = null;
notifyListeners();
return;
}
await _updateMinFromAmount(shouldNotifyListeners: shouldNotifyListeners);
await updateRate(shouldNotifyListeners: shouldNotifyListeners);
debugPrint(
"_updated TO: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$_fromAmount _toAmount=$_toAmount rate:$rate");
if (shouldNotifyListeners) {
notifyListeners();
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
}
Future<void> updateFrom(Currency from, bool shouldNotifyListeners) async {
try {
_from = from;
if (_to == null) {
rate = null;
notifyListeners();
return;
}
await _updateMinFromAmount(shouldNotifyListeners: shouldNotifyListeners);
await updateRate(shouldNotifyListeners: shouldNotifyListeners);
debugPrint(
"_updated FROM: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$_fromAmount _toAmount=$_toAmount rate:$rate");
if (shouldNotifyListeners) {
notifyListeners();
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateMinFromAmount(
{required bool shouldNotifyListeners}) async {
_minFromAmount = await getStandardMinExchangeAmount(from: from!, to: to!);
if (shouldNotifyListeners) {
notifyListeners();
}
}
// Future<void> setToAmountAndCalculateFromAmount(
// Decimal newToAmount,
// bool shouldNotifyListeners,
// ) async {
// if (newToAmount == Decimal.zero) {
// _fromAmount = Decimal.zero;
// }
//
// _toAmount = newToAmount;
// await updateRate();
// if (shouldNotifyListeners) {
// notifyListeners();
// }
// }
Future<void> setFromAmountAndCalculateToAmount(
Decimal newFromAmount,
bool shouldNotifyListeners,
) async {
if (newFromAmount == Decimal.zero) {
_toAmount = Decimal.zero;
}
_fromAmount = newFromAmount;
await updateRate(shouldNotifyListeners: shouldNotifyListeners);
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<Decimal?> getStandardEstimatedToAmount({
required Decimal fromAmount,
required Currency from,
required Currency to,
}) async {
final response =
await (cnTesting ?? ChangeNow.instance).getEstimatedExchangeAmount(
fromTicker: from.ticker,
toTicker: to.ticker,
fromAmount: fromAmount,
);
if (response.value != null) {
return response.value!.estimatedAmount;
} else {
_onError?.call(
"Failed to fetch estimated amount: ${response.exception?.toString()}");
return null;
}
}
// Future<Decimal?> getStandardEstimatedFromAmount({
// required Decimal toAmount,
// required Currency from,
// required Currency to,
// }) async {
// final response = await (cnTesting ?? ChangeNow.instance)
// .getEstimatedExchangeAmount(
// fromTicker: from.ticker,
// toTicker: to.ticker,
// fromAmount: toAmount, );
//
// if (response.value != null) {
// return response.value!.fromAmount;
// } else {
// _onError?.call(
// "Failed to fetch estimated amount: ${response.exception?.toString()}");
// return null;
// }
// }
Future<Decimal?> getStandardMinExchangeAmount({
required Currency from,
required Currency to,
}) async {
final response = await (cnTesting ?? ChangeNow.instance)
.getMinimalExchangeAmount(fromTicker: from.ticker, toTicker: to.ticker);
if (response.value != null) {
return response.value!;
} else {
_onError?.call(
"Could not update minimal exchange amounts: ${response.exception?.toString()}");
return null;
}
}
void setOnError({
required void Function(String)? onError,
bool shouldNotifyListeners = false,
}) {
_onError = onError;
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> updateRate({bool shouldNotifyListeners = false}) async {
rate = null;
final amount = _fromAmount;
final minAmount = _minFromAmount;
if (amount != null && amount > Decimal.zero) {
Decimal? amt;
if (minAmount != null) {
if (minAmount <= amount) {
amt = await getStandardEstimatedToAmount(
fromAmount: amount, from: _from!, to: _to!);
if (amt != null) {
rate = (amt / amount).toDecimal(scaleOnInfinitePrecision: 12);
}
}
}
if (rate != null && amt != null) {
_toAmount = amt;
}
}
if (shouldNotifyListeners) {
notifyListeners();
}
}
}

View file

@ -0,0 +1,422 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/utilities/logger.dart';
class ExchangeFormState extends ChangeNotifier {
Exchange? _exchange;
Exchange? get exchange => _exchange;
set exchange(Exchange? value) {
_exchange = value;
_onExchangeTypeChanged();
}
ExchangeRateType _exchangeType = ExchangeRateType.estimated;
ExchangeRateType get exchangeType => _exchangeType;
set exchangeType(ExchangeRateType value) {
_exchangeType = value;
_onExchangeRateTypeChanged();
}
bool reversed = false;
Decimal? fromAmount;
Decimal? toAmount;
Decimal? minAmount;
Decimal? maxAmount;
Decimal? rate;
Estimate? estimate;
FixedRateMarket? _market;
FixedRateMarket? get market => _market;
Currency? _from;
Currency? _to;
@override
String toString() {
return 'ExchangeFormState: {_exchange: $_exchange, _exchangeType: $_exchangeType, reversed: $reversed, fromAmount: $fromAmount, toAmount: $toAmount, minAmount: $minAmount, maxAmount: $maxAmount, rate: $rate, estimate: $estimate, _market: $_market, _from: $_from, _to: $_to, _onError: $_onError}';
}
String? get fromTicker {
switch (exchangeType) {
case ExchangeRateType.estimated:
return _from?.ticker;
case ExchangeRateType.fixed:
switch (exchange?.name) {
case SimpleSwapExchange.exchangeName:
return _from?.ticker;
case ChangeNowExchange.exchangeName:
return market?.from;
default:
return null;
}
}
}
String? get toTicker {
switch (exchangeType) {
case ExchangeRateType.estimated:
return _to?.ticker;
case ExchangeRateType.fixed:
switch (exchange?.name) {
case SimpleSwapExchange.exchangeName:
return _to?.ticker;
case ChangeNowExchange.exchangeName:
return market?.to;
default:
return null;
}
}
}
void Function(String)? _onError;
Currency? get from => _from;
Currency? get to => _to;
void setCurrencies(Currency from, Currency to) {
_from = from;
_to = to;
}
String get warning {
if (reversed) {
if (toTicker != null && toAmount != null) {
if (minAmount != null &&
toAmount! < minAmount! &&
toAmount! > Decimal.zero) {
return "Minimum amount ${minAmount!.toString()} ${toTicker!.toUpperCase()}";
} else if (maxAmount != null && toAmount! > maxAmount!) {
return "Maximum amount ${maxAmount!.toString()} ${toTicker!.toUpperCase()}";
}
}
} else {
if (fromTicker != null && fromAmount != null) {
if (minAmount != null &&
fromAmount! < minAmount! &&
fromAmount! > Decimal.zero) {
return "Minimum amount ${minAmount!.toString()} ${fromTicker!.toUpperCase()}";
} else if (maxAmount != null && fromAmount! > maxAmount!) {
return "Maximum amount ${maxAmount!.toString()} ${fromTicker!.toUpperCase()}";
}
}
}
return "";
}
String get fromAmountString => fromAmount?.toStringAsFixed(8) ?? "";
String get toAmountString => toAmount?.toStringAsFixed(8) ?? "";
bool get canExchange {
if (exchange?.name == ChangeNowExchange.exchangeName &&
exchangeType == ExchangeRateType.fixed) {
return _market != null &&
fromAmount != null &&
toAmount != null &&
warning.isEmpty;
} else {
return fromAmount != null &&
fromAmount != Decimal.zero &&
toAmount != null &&
rate != null &&
warning.isEmpty;
}
}
void clearAmounts(bool shouldNotifyListeners) {
fromAmount = null;
toAmount = null;
minAmount = null;
maxAmount = null;
rate = null;
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> setFromAmountAndCalculateToAmount(
Decimal newFromAmount,
bool shouldNotifyListeners,
) async {
if (newFromAmount == Decimal.zero) {
toAmount = Decimal.zero;
}
fromAmount = newFromAmount;
reversed = false;
await updateRanges(shouldNotifyListeners: false);
await updateEstimate(
shouldNotifyListeners: false,
reversed: reversed,
);
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> setToAmountAndCalculateFromAmount(
Decimal newToAmount,
bool shouldNotifyListeners,
) async {
if (newToAmount == Decimal.zero) {
fromAmount = Decimal.zero;
}
toAmount = newToAmount;
reversed = true;
await updateRanges(shouldNotifyListeners: false);
await updateEstimate(
shouldNotifyListeners: false,
reversed: reversed,
);
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> updateTo(Currency to, bool shouldNotifyListeners) async {
try {
_to = to;
if (_from == null) {
rate = null;
notifyListeners();
return;
}
await updateRanges(shouldNotifyListeners: false);
await updateEstimate(
shouldNotifyListeners: false,
reversed: reversed,
);
debugPrint(
"_updated TO: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$fromAmount _toAmount=$toAmount rate:$rate for: $exchange");
if (shouldNotifyListeners) {
notifyListeners();
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
}
Future<void> updateFrom(Currency from, bool shouldNotifyListeners) async {
try {
_from = from;
if (_to == null) {
rate = null;
notifyListeners();
return;
}
await updateRanges(shouldNotifyListeners: false);
await updateEstimate(
shouldNotifyListeners: false,
reversed: reversed,
);
debugPrint(
"_updated FROM: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$fromAmount _toAmount=$toAmount rate:$rate for: $exchange");
if (shouldNotifyListeners) {
notifyListeners();
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
}
Future<void> updateMarket(
FixedRateMarket? market,
bool shouldNotifyListeners,
) async {
_market = market;
if (_market == null) {
fromAmount = null;
toAmount = null;
} else {
if (fromAmount != null) {
if (fromAmount! <= Decimal.zero) {
toAmount = Decimal.zero;
} else {
await updateRanges(shouldNotifyListeners: false);
await updateEstimate(
shouldNotifyListeners: false,
reversed: reversed,
);
}
}
}
if (shouldNotifyListeners) {
notifyListeners();
}
}
void _onExchangeRateTypeChanged() {
print("_onExchangeRateTypeChanged");
}
void _onExchangeTypeChanged() {
updateRanges(shouldNotifyListeners: true).then(
(_) => updateEstimate(
shouldNotifyListeners: true,
reversed: reversed,
),
);
}
Future<void> updateRanges({required bool shouldNotifyListeners}) async {
if (exchange?.name == SimpleSwapExchange.exchangeName) {
reversed = false;
}
final _fromTicker = reversed ? toTicker : fromTicker;
final _toTicker = reversed ? fromTicker : toTicker;
if (_fromTicker == null || _toTicker == null) {
Logging.instance.log(
"Tried to $runtimeType.updateRanges where (from: $_fromTicker || to: $_toTicker) for: $exchange",
level: LogLevel.Info,
);
return;
}
final response = await exchange?.getRange(
_fromTicker,
_toTicker,
exchangeType == ExchangeRateType.fixed,
);
if (response?.value == null) {
Logging.instance.log(
"Tried to $runtimeType.updateRanges for: $exchange where response: $response",
level: LogLevel.Info,
);
return;
}
final range = response!.value!;
minAmount = range.min;
maxAmount = range.max;
debugPrint(
"updated range for: $exchange for $_fromTicker-$_toTicker: $range");
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> updateEstimate({
required bool shouldNotifyListeners,
required bool reversed,
}) async {
if (exchange?.name == SimpleSwapExchange.exchangeName) {
reversed = false;
}
final amount = reversed ? toAmount : fromAmount;
if (fromTicker == null ||
toTicker == null ||
amount == null ||
amount <= Decimal.zero) {
Logging.instance.log(
"Tried to $runtimeType.updateEstimate for: $exchange where (from: $fromTicker || to: $toTicker || amount: $amount)",
level: LogLevel.Info,
);
return;
}
final response = await exchange?.getEstimate(
fromTicker!,
toTicker!,
amount,
exchangeType == ExchangeRateType.fixed,
reversed,
);
if (response?.value == null) {
Logging.instance.log(
"Tried to $runtimeType.updateEstimate for: $exchange where response: $response",
level: LogLevel.Info,
);
return;
}
estimate = response!.value!;
if (reversed) {
fromAmount = estimate!.estimatedAmount;
} else {
toAmount = estimate!.estimatedAmount;
}
rate = (toAmount! / fromAmount!).toDecimal(scaleOnInfinitePrecision: 12);
debugPrint(
"updated estimate for: $exchange for $fromTicker-$toTicker: $estimate");
if (shouldNotifyListeners) {
notifyListeners();
}
}
void setOnError({
required void Function(String)? onError,
bool shouldNotifyListeners = false,
}) {
_onError = onError;
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> swap({FixedRateMarket? market}) async {
final Decimal? newToAmount = fromAmount;
final Decimal? newFromAmount = toAmount;
fromAmount = newFromAmount;
toAmount = newToAmount;
minAmount = null;
maxAmount = null;
if (exchangeType == ExchangeRateType.fixed &&
exchange?.name == ChangeNowExchange.exchangeName) {
await updateMarket(market, false);
} else {
final Currency? newTo = from;
final Currency? newFrom = to;
_to = newTo;
_from = newFrom;
await updateRanges(shouldNotifyListeners: false);
await updateEstimate(
shouldNotifyListeners: false,
reversed: reversed,
);
}
notifyListeners();
}
}

View file

@ -1,178 +0,0 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
import 'package:stackwallet/services/change_now/change_now.dart';
import 'package:stackwallet/utilities/logger.dart';
class FixedRateExchangeFormState extends ChangeNotifier {
Decimal? _fromAmount;
Decimal? _toAmount;
FixedRateMarket? _market;
FixedRateMarket? get market => _market;
CNExchangeEstimate? _estimate;
CNExchangeEstimate? get estimate => _estimate;
Decimal? get rate {
if (_estimate == null) {
return null;
} else {
return (_estimate!.toAmount / _estimate!.fromAmount)
.toDecimal(scaleOnInfinitePrecision: 12);
}
}
Future<void> swap(FixedRateMarket reverseFixedRateMarket) async {
final Decimal? tmp = _fromAmount;
_fromAmount = _toAmount;
_toAmount = tmp;
await updateMarket(reverseFixedRateMarket, false);
await updateRateEstimate(CNEstimateType.direct);
_toAmount = _estimate?.toAmount ?? Decimal.zero;
notifyListeners();
}
String get fromAmountString =>
_fromAmount == null ? "" : _fromAmount!.toStringAsFixed(8);
String get toAmountString =>
_toAmount == null ? "" : _toAmount!.toStringAsFixed(8);
Future<void> updateMarket(
FixedRateMarket? market,
bool shouldNotifyListeners,
) async {
_market = market;
if (_market == null) {
_fromAmount = null;
_toAmount = null;
} else {
if (_fromAmount != null) {
if (_fromAmount! <= Decimal.zero) {
_toAmount = Decimal.zero;
} else {
await updateRateEstimate(CNEstimateType.direct);
}
}
}
if (shouldNotifyListeners) {
notifyListeners();
}
}
String get rateDisplayString {
if (_market == null || _estimate == null) {
return "N/A";
} else {
return "1 ${_estimate!.fromCurrency.toUpperCase()} ~${rate!.toStringAsFixed(8)} ${_estimate!.toCurrency.toUpperCase()}";
}
}
bool get canExchange {
return _market != null &&
_fromAmount != null &&
_toAmount != null &&
sendAmountWarning.isEmpty;
}
String get sendAmountWarning {
if (_market != null && _fromAmount != null) {
if (_fromAmount! < _market!.min) {
return "Minimum amount ${_market!.min.toString()} ${_market!.from.toUpperCase()}";
} else if (_fromAmount! > _market!.max) {
return "Maximum amount ${_market!.max.toString()} ${_market!.from.toUpperCase()}";
}
}
return "";
}
Future<void> setToAmountAndCalculateFromAmount(
Decimal newToAmount,
bool shouldNotifyListeners,
) async {
_toAmount = newToAmount;
if (shouldNotifyListeners) {
await updateRateEstimate(CNEstimateType.reverse);
notifyListeners();
}
}
Future<void> setFromAmountAndCalculateToAmount(
Decimal newFromAmount,
bool shouldNotifyListeners,
) async {
_fromAmount = newFromAmount;
if (shouldNotifyListeners) {
await updateRateEstimate(CNEstimateType.direct);
notifyListeners();
}
}
void Function(String)? _onError;
void setOnError({
required void Function(String)? onError,
bool shouldNotifyListeners = false,
}) {
_onError = onError;
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> updateRateEstimate(CNEstimateType direction) async {
if (market != null) {
Decimal? amount;
// set amount based on trade estimate direction
switch (direction) {
case CNEstimateType.direct:
if (_fromAmount != null
// &&
// market!.min >= _fromAmount! &&
// _fromAmount! <= market!.max
) {
amount = _fromAmount!;
}
break;
case CNEstimateType.reverse:
if (_toAmount != null
// &&
// market!.min >= _toAmount! &&
// _toAmount! <= market!.max
) {
amount = _toAmount!;
}
break;
}
if (amount != null && market != null && amount > Decimal.zero) {
final response = await ChangeNow.instance.getEstimatedExchangeAmountV2(
fromTicker: market!.from,
toTicker: market!.to,
fromOrTo: direction,
flow: CNFlowType.fixedRate,
amount: amount,
);
if (response.value != null) {
// update estimate if response succeeded
_estimate = response.value;
_toAmount = _estimate?.toAmount;
_fromAmount = _estimate?.fromAmount;
notifyListeners();
} else if (response.exception != null) {
Logging.instance.log("updateRateEstimate(): ${response.exception}",
level: LogLevel.Warning);
}
}
}
}
}

View file

@ -1,5 +1,5 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
class IncompleteExchangeModel {
@ -13,12 +13,14 @@ class IncompleteExchangeModel {
final ExchangeRateType rateType;
final bool reversed;
String? recipientAddress;
String? refundAddress;
String? rateId;
ExchangeTransaction? trade;
Trade? trade;
IncompleteExchangeModel({
required this.sendTicker,
@ -27,6 +29,7 @@ class IncompleteExchangeModel {
required this.sendAmount,
required this.receiveAmount,
required this.rateType,
required this.reversed,
this.rateId,
});
}

View file

@ -5,12 +5,18 @@ class Currency {
/// Currency name
final String name;
/// Currency network
final String network;
/// Currency logo url
final String image;
/// Indicates if a currency has an Extra ID
final bool hasExternalId;
/// external id if it exists
final String? externalId;
/// Indicates if a currency is a fiat currency (EUR, USD)
final bool isFiat;
@ -30,8 +36,10 @@ class Currency {
Currency({
required this.ticker,
required this.name,
required this.network,
required this.image,
required this.hasExternalId,
this.externalId,
required this.isFiat,
required this.featured,
required this.isStable,
@ -44,8 +52,10 @@ class Currency {
return Currency(
ticker: json["ticker"] as String,
name: json["name"] as String,
network: json["network"] as String? ?? "",
image: json["image"] as String,
hasExternalId: json["hasExternalId"] as bool,
externalId: json["externalId"] as String?,
isFiat: json["isFiat"] as bool,
featured: json["featured"] as bool,
isStable: json["isStable"] as bool,
@ -61,8 +71,10 @@ class Currency {
final map = {
"ticker": ticker,
"name": name,
"network": network,
"image": image,
"hasExternalId": hasExternalId,
"externalId": externalId,
"isFiat": isFiat,
"featured": featured,
"isStable": isStable,
@ -79,8 +91,10 @@ class Currency {
Currency copyWith({
String? ticker,
String? name,
String? network,
String? image,
bool? hasExternalId,
String? externalId,
bool? isFiat,
bool? featured,
bool? isStable,
@ -90,8 +104,10 @@ class Currency {
return Currency(
ticker: ticker ?? this.ticker,
name: name ?? this.name,
network: network ?? this.network,
image: image ?? this.image,
hasExternalId: hasExternalId ?? this.hasExternalId,
externalId: externalId ?? this.externalId,
isFiat: isFiat ?? this.isFiat,
featured: featured ?? this.featured,
isStable: isStable ?? this.isStable,

View file

@ -0,0 +1,46 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/utilities/logger.dart';
class Estimate {
final Decimal estimatedAmount;
final bool fixedRate;
final bool reversed;
final String? warningMessage;
final String? rateId;
Estimate({
required this.estimatedAmount,
required this.fixedRate,
required this.reversed,
this.warningMessage,
this.rateId,
});
factory Estimate.fromMap(Map<String, dynamic> map) {
try {
return Estimate(
estimatedAmount: Decimal.parse(map["estimatedAmount"] as String),
fixedRate: map["fixedRate"] as bool,
reversed: map["reversed"] as bool,
warningMessage: map["warningMessage"] as String?,
rateId: map["rateId"] as String?,
);
} catch (e, s) {
Logging.instance.log("Estimate.fromMap(): $e\n$s", level: LogLevel.Error);
rethrow;
}
}
Map<String, dynamic> toMap() {
return {
"estimatedAmount": estimatedAmount.toString(),
"fixedRate": fixedRate,
"reversed": reversed,
"warningMessage": warningMessage,
"rateId": rateId,
};
}
@override
String toString() => "Estimate: ${toMap()}";
}

View file

@ -1,4 +1,5 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/utilities/logger.dart';
class FixedRateMarket {
/// Currency ticker
@ -20,7 +21,7 @@ class FixedRateMarket {
/// Network fee for transferring funds between wallets, it should
/// be deducted from the result.
final Decimal minerFee;
final Decimal? minerFee;
FixedRateMarket({
required this.from,
@ -31,7 +32,7 @@ class FixedRateMarket {
required this.minerFee,
});
factory FixedRateMarket.fromJson(Map<String, dynamic> json) {
factory FixedRateMarket.fromMap(Map<String, dynamic> json) {
try {
return FixedRateMarket(
from: json["from"] as String,
@ -39,15 +40,19 @@ class FixedRateMarket {
min: Decimal.parse(json["min"].toString()),
max: Decimal.parse(json["max"].toString()),
rate: Decimal.parse(json["rate"].toString()),
minerFee: Decimal.parse(json["minerFee"].toString()),
minerFee: Decimal.tryParse(json["minerFee"].toString()),
);
} catch (e, s) {
Logging.instance.log(
"FixedRateMarket.fromMap(): $e\n$s",
level: LogLevel.Error,
);
} catch (e) {
rethrow;
}
}
Map<String, dynamic> toJson() {
final map = {
Map<String, dynamic> toMap() {
return {
"from": from,
"to": to,
"min": min,
@ -55,8 +60,6 @@ class FixedRateMarket {
"rate": rate,
"minerFee": minerFee,
};
return map;
}
FixedRateMarket copyWith({
@ -78,7 +81,5 @@ class FixedRateMarket {
}
@override
String toString() {
return "FixedRateMarket: ${toJson()}";
}
String toString() => "FixedRateMarket: ${toMap()}";
}

View file

@ -0,0 +1,73 @@
import 'dart:ui';
import 'package:stackwallet/utilities/logger.dart';
class Pair {
final String from;
final String fromNetwork;
final String to;
final String toNetwork;
final bool fixedRate;
final bool floatingRate;
Pair({
required this.from,
required this.fromNetwork,
required this.to,
required this.toNetwork,
required this.fixedRate,
required this.floatingRate,
});
factory Pair.fromMap(Map<String, dynamic> map) {
try {
return Pair(
from: map["from"] as String,
fromNetwork: map["fromNetwork"] as String,
to: map["to"] as String,
toNetwork: map["toNetwork"] as String,
fixedRate: map["fixedRate"] as bool,
floatingRate: map["floatingRate"] as bool,
);
} catch (e, s) {
Logging.instance.log("Pair.fromMap(): $e\n$s", level: LogLevel.Error);
rethrow;
}
}
Map<String, dynamic> toMap() {
return {
"from": from,
"fromNetwork": fromNetwork,
"to": to,
"toNetwork": toNetwork,
"fixedRate": fixedRate,
"floatingRate": floatingRate,
};
}
@override
bool operator ==(other) =>
other is Pair &&
from == other.from &&
fromNetwork == other.fromNetwork &&
to == other.to &&
toNetwork == other.toNetwork &&
fixedRate == other.fixedRate &&
floatingRate == other.floatingRate;
@override
int get hashCode => hashValues(
from,
fromNetwork,
to,
toNetwork,
fixedRate,
floatingRate,
);
@override
String toString() => "Pair: ${toMap()}";
}

View file

@ -0,0 +1,32 @@
import 'package:decimal/decimal.dart';
class Range {
final Decimal? min;
final Decimal? max;
Range({this.min, this.max});
Range copyWith({
Decimal? min,
Decimal? max,
}) {
return Range(
min: min ?? this.min,
max: max ?? this.max,
);
}
Map<String, dynamic> toMap() {
final map = {
"min": min?.toString(),
"max": max?.toString(),
};
return map;
}
@override
String toString() {
return "Range: ${toMap()}";
}
}

View file

@ -0,0 +1,239 @@
import 'package:hive/hive.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
part 'trade.g.dart';
@HiveType(typeId: Trade.typeId)
class Trade {
static const typeId = 22;
@HiveField(0)
final String uuid;
@HiveField(1)
final String tradeId;
@HiveField(2)
final String rateType;
@HiveField(3)
final String direction;
@HiveField(4)
final DateTime timestamp;
@HiveField(5)
final DateTime updatedAt;
@HiveField(6)
final String payInCurrency;
@HiveField(7)
final String payInAmount;
@HiveField(8)
final String payInAddress;
@HiveField(9)
final String payInNetwork;
@HiveField(10)
final String payInExtraId;
@HiveField(11)
final String payInTxid;
@HiveField(12)
final String payOutCurrency;
@HiveField(13)
final String payOutAmount;
@HiveField(14)
final String payOutAddress;
@HiveField(15)
final String payOutNetwork;
@HiveField(16)
final String payOutExtraId;
@HiveField(17)
final String payOutTxid;
@HiveField(18)
final String refundAddress;
@HiveField(19)
final String refundExtraId;
@HiveField(20)
final String status;
@HiveField(21)
final String exchangeName;
const Trade({
required this.uuid,
required this.tradeId,
required this.rateType,
required this.direction,
required this.timestamp,
required this.updatedAt,
required this.payInCurrency,
required this.payInAmount,
required this.payInAddress,
required this.payInNetwork,
required this.payInExtraId,
required this.payInTxid,
required this.payOutCurrency,
required this.payOutAmount,
required this.payOutAddress,
required this.payOutNetwork,
required this.payOutExtraId,
required this.payOutTxid,
required this.refundAddress,
required this.refundExtraId,
required this.status,
required this.exchangeName,
});
Trade copyWith({
String? tradeId,
String? rateType,
String? direction,
DateTime? timestamp,
DateTime? updatedAt,
String? payInCurrency,
String? payInAmount,
String? payInAddress,
String? payInNetwork,
String? payInExtraId,
String? payInTxid,
String? payOutCurrency,
String? payOutAmount,
String? payOutAddress,
String? payOutNetwork,
String? payOutExtraId,
String? payOutTxid,
String? refundAddress,
String? refundExtraId,
String? status,
String? exchangeName,
}) {
return Trade(
uuid: uuid,
tradeId: tradeId ?? this.tradeId,
rateType: rateType ?? this.rateType,
direction: direction ?? this.direction,
timestamp: timestamp ?? this.timestamp,
updatedAt: updatedAt ?? this.updatedAt,
payInCurrency: payInCurrency ?? this.payInCurrency,
payInAmount: payInAmount ?? this.payInAmount,
payInAddress: payInAddress ?? this.payInAddress,
payInNetwork: payInNetwork ?? this.payInNetwork,
payInExtraId: payInExtraId ?? this.payInExtraId,
payInTxid: payInTxid ?? this.payInTxid,
payOutCurrency: payOutCurrency ?? this.payOutCurrency,
payOutAmount: payOutAmount ?? this.payOutAmount,
payOutAddress: payOutAddress ?? this.payOutAddress,
payOutNetwork: payOutNetwork ?? this.payOutNetwork,
payOutExtraId: payOutExtraId ?? this.payOutExtraId,
payOutTxid: payOutTxid ?? this.payOutTxid,
refundAddress: refundAddress ?? this.refundAddress,
refundExtraId: refundExtraId ?? this.refundExtraId,
status: status ?? this.status,
exchangeName: exchangeName ?? this.exchangeName,
);
}
Map<String, String> toMap() {
return {
"uuid": uuid,
"tradeId": tradeId,
"rateType": rateType,
"direction": direction,
"timestamp": timestamp.toIso8601String(),
"updatedAt": updatedAt.toIso8601String(),
"payInCurrency": payInCurrency,
"payInAmount": payInAmount,
"payInAddress": payInAddress,
"payInNetwork": payInNetwork,
"payInExtraId": payInExtraId,
"payInTxid": payInTxid,
"payOutCurrency": payOutCurrency,
"payOutAmount": payOutAmount,
"payOutAddress": payOutAddress,
"payOutNetwork": payOutNetwork,
"payOutExtraId": payOutExtraId,
"payOutTxid": payOutTxid,
"refundAddress": refundAddress,
"refundExtraId": refundExtraId,
"status": status,
"exchangeName": exchangeName,
};
}
factory Trade.fromMap(Map<String, dynamic> map) {
return Trade(
uuid: map["uuid"] as String,
tradeId: map["tradeId"] as String,
rateType: map["rateType"] as String,
direction: map["direction"] as String,
timestamp: DateTime.parse(map["timestamp"] as String),
updatedAt: DateTime.parse(map["updatedAt"] as String),
payInCurrency: map["payInCurrency"] as String,
payInAmount: map["payInAmount"] as String,
payInAddress: map["payInAddress"] as String,
payInNetwork: map["payInNetwork"] as String,
payInExtraId: map["payInExtraId"] as String,
payInTxid: map["payInTxid"] as String,
payOutCurrency: map["payOutCurrency"] as String,
payOutAmount: map["payOutAmount"] as String,
payOutAddress: map["payOutAddress"] as String,
payOutNetwork: map["payOutNetwork"] as String,
payOutExtraId: map["payOutExtraId"] as String,
payOutTxid: map["payOutTxid"] as String,
refundAddress: map["refundAddress"] as String,
refundExtraId: map["refundExtraId"] as String,
status: map["status"] as String,
exchangeName: map["exchangeName"] as String,
);
}
factory Trade.fromExchangeTransaction(
ExchangeTransaction exTx, bool reversed) {
return Trade(
uuid: exTx.uuid,
tradeId: exTx.id,
rateType: "",
direction: reversed ? "reverse" : "direct",
timestamp: exTx.date,
updatedAt: DateTime.tryParse(exTx.statusObject!.updatedAt) ?? exTx.date,
payInCurrency: exTx.fromCurrency,
payInAmount: exTx.statusObject!.amountSendDecimal.isEmpty
? exTx.statusObject!.expectedSendAmountDecimal
: exTx.statusObject!.amountSendDecimal,
payInAddress: exTx.payinAddress,
payInNetwork: "",
payInExtraId: exTx.payinExtraId,
payInTxid: exTx.statusObject!.payinHash,
payOutCurrency: exTx.toCurrency,
payOutAmount: exTx.amount,
payOutAddress: exTx.payoutAddress,
payOutNetwork: "",
payOutExtraId: exTx.payoutExtraId,
payOutTxid: exTx.statusObject!.payoutHash,
refundAddress: exTx.refundAddress,
refundExtraId: exTx.refundExtraId,
status: exTx.statusObject!.status.name,
exchangeName: ChangeNowExchange.exchangeName,
);
}
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,104 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'trade.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class TradeAdapter extends TypeAdapter<Trade> {
@override
final int typeId = 22;
@override
Trade read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Trade(
uuid: fields[0] as String,
tradeId: fields[1] as String,
rateType: fields[2] as String,
direction: fields[3] as String,
timestamp: fields[4] as DateTime,
updatedAt: fields[5] as DateTime,
payInCurrency: fields[6] as String,
payInAmount: fields[7] as String,
payInAddress: fields[8] as String,
payInNetwork: fields[9] as String,
payInExtraId: fields[10] as String,
payInTxid: fields[11] as String,
payOutCurrency: fields[12] as String,
payOutAmount: fields[13] as String,
payOutAddress: fields[14] as String,
payOutNetwork: fields[15] as String,
payOutExtraId: fields[16] as String,
payOutTxid: fields[17] as String,
refundAddress: fields[18] as String,
refundExtraId: fields[19] as String,
status: fields[20] as String,
exchangeName: fields[21] as String,
);
}
@override
void write(BinaryWriter writer, Trade obj) {
writer
..writeByte(22)
..writeByte(0)
..write(obj.uuid)
..writeByte(1)
..write(obj.tradeId)
..writeByte(2)
..write(obj.rateType)
..writeByte(3)
..write(obj.direction)
..writeByte(4)
..write(obj.timestamp)
..writeByte(5)
..write(obj.updatedAt)
..writeByte(6)
..write(obj.payInCurrency)
..writeByte(7)
..write(obj.payInAmount)
..writeByte(8)
..write(obj.payInAddress)
..writeByte(9)
..write(obj.payInNetwork)
..writeByte(10)
..write(obj.payInExtraId)
..writeByte(11)
..write(obj.payInTxid)
..writeByte(12)
..write(obj.payOutCurrency)
..writeByte(13)
..write(obj.payOutAmount)
..writeByte(14)
..write(obj.payOutAddress)
..writeByte(15)
..write(obj.payOutNetwork)
..writeByte(16)
..write(obj.payOutExtraId)
..writeByte(17)
..write(obj.payOutTxid)
..writeByte(18)
..write(obj.refundAddress)
..writeByte(19)
..write(obj.refundExtraId)
..writeByte(20)
..write(obj.status)
..writeByte(21)
..write(obj.exchangeName);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TradeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View file

@ -0,0 +1,30 @@
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
class SPAvailableCurrencies {
final List<Currency> floatingRateCurrencies = [];
final List<Currency> fixedRateCurrencies = [];
final List<Pair> floatingRatePairs = [];
final List<Pair> fixedRatePairs = [];
void updateFloatingCurrencies(List<Currency> newCurrencies) {
floatingRateCurrencies.clear();
floatingRateCurrencies.addAll(newCurrencies);
}
void updateFixedCurrencies(List<Currency> newCurrencies) {
fixedRateCurrencies.clear();
fixedRateCurrencies.addAll(newCurrencies);
}
void updateFloatingPairs(List<Pair> newPairs) {
floatingRatePairs.clear();
floatingRatePairs.addAll(newPairs);
}
void updateFixedPairs(List<Pair> newPairs) {
fixedRatePairs.clear();
fixedRatePairs.addAll(newPairs);
}
}

View file

@ -0,0 +1,99 @@
import 'package:stackwallet/utilities/logger.dart';
class SPCurrency {
/// currency name
final String name;
/// currency symbol
final String symbol;
/// currency network
final String network;
/// has this currency extra id parameter
final bool hasExtraId;
/// name of extra id (if exists)
final String? extraId;
/// relative url for currency icon svg
final String image;
/// informational messages about the currency they are changing
final List<dynamic> warningsFrom;
/// informational messages about the currency for which they are exchanged
final List<dynamic> warningsTo;
SPCurrency({
required this.name,
required this.symbol,
required this.network,
required this.hasExtraId,
required this.extraId,
required this.image,
required this.warningsFrom,
required this.warningsTo,
});
factory SPCurrency.fromJson(Map<String, dynamic> json) {
try {
return SPCurrency(
name: json["name"] as String,
symbol: json["symbol"] as String,
network: json["network"] as String? ?? "",
hasExtraId: json["has_extra_id"] as bool,
extraId: json["extra_id"] as String?,
image: json["image"] as String,
warningsFrom: json["warnings_from"] as List<dynamic>,
warningsTo: json["warnings_to"] as List<dynamic>,
);
} catch (e, s) {
Logging.instance.log("SPCurrency.fromJson failed to parse: $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Map<String, dynamic> toJson() {
final map = {
"name": name,
"symbol": symbol,
"network": network,
"has_extra_id": hasExtraId,
"extra_id": extraId,
"image": image,
"warnings_from": warningsFrom,
"warnings_to": warningsTo,
};
return map;
}
SPCurrency copyWith({
String? name,
String? symbol,
String? network,
bool? hasExtraId,
String? extraId,
String? image,
List<dynamic>? warningsFrom,
List<dynamic>? warningsTo,
}) {
return SPCurrency(
name: name ?? this.name,
symbol: symbol ?? this.symbol,
network: network ?? this.network,
hasExtraId: hasExtraId ?? this.hasExtraId,
extraId: extraId ?? this.extraId,
image: image ?? this.image,
warningsFrom: warningsFrom ?? this.warningsFrom,
warningsTo: warningsTo ?? this.warningsTo,
);
}
@override
String toString() {
return "SPCurrency: ${toJson()}";
}
}

View file

@ -275,7 +275,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
// without using them
await manager.recoverFromMnemonic(
mnemonic: mnemonic,
maxUnusedAddressGap: 20,
maxUnusedAddressGap: widget.coin == Coin.firo ? 50 : 20,
maxNumberOfIndexesToCheck: 1000,
height: height,
);

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart';
@ -34,7 +34,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
final Map<String, dynamic> transactionInfo;
final String walletId;
final String routeOnSuccessName;
final ExchangeTransaction trade;
final Trade trade;
@override
ConsumerState<ConfirmChangeNowSendView> createState() =>
@ -46,7 +46,7 @@ class _ConfirmChangeNowSendViewState
late final Map<String, dynamic> transactionInfo;
late final String walletId;
late final String routeOnSuccessName;
late final ExchangeTransaction trade;
late final Trade trade;
Future<void> _attemptSend(BuildContext context) async {
unawaited(showDialog<void>(
@ -75,7 +75,7 @@ class _ConfirmChangeNowSendViewState
tradeWalletLookup: TradeWalletLookup(
uuid: const Uuid().v1(),
txid: txid,
tradeId: trade.id,
tradeId: trade.tradeId,
walletIds: [walletId],
),
);
@ -207,7 +207,7 @@ class _ConfirmChangeNowSendViewState
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"ChangeNOW address",
"${trade.exchangeName} address",
style: STextStyles.smallMed12(context),
),
const SizedBox(
@ -309,7 +309,7 @@ class _ConfirmChangeNowSendViewState
style: STextStyles.smallMed12(context),
),
Text(
trade.id,
trade.tradeId,
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';

File diff suppressed because it is too large Load diff

View file

@ -70,7 +70,7 @@ class _ExchangeLoadingOverlayViewState
.overlay
.withOpacity(0.7),
child: const CustomLoadingOverlay(
message: "Loading ChangeNOW data", eventBus: null),
message: "Loading Exchange data", eventBus: null),
),
if ((_statusEst == ChangeNowLoadStatus.failed ||
_statusFixed == ChangeNowLoadStatus.failed) &&
@ -85,9 +85,9 @@ class _ExchangeLoadingOverlayViewState
mainAxisAlignment: MainAxisAlignment.end,
children: [
StackDialog(
title: "Failed to fetch ChangeNow data",
title: "Failed to fetch Exchange data",
message:
"ChangeNOW requires a working internet connection. Tap OK to try fetching again.",
"Exchange requires a working internet connection. Tap OK to try fetching again.",
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!

View file

@ -348,23 +348,9 @@ class _Step2ViewState extends ConsumerState<Step2View> {
"sendViewScanQrButtonKey"),
onTap: () async {
try {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
final qrResult =
await scanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
final results =
AddressUtils.parseUri(
qrResult.rawContent);
@ -385,16 +371,10 @@ class _Step2ViewState extends ConsumerState<Step2View> {
setState(() {});
}
} on PlatformException catch (e, s) {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
@ -585,23 +565,9 @@ class _Step2ViewState extends ConsumerState<Step2View> {
"sendViewScanQrButtonKey"),
onTap: () async {
try {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
final qrResult =
await scanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
final results =
AddressUtils.parseUri(
qrResult.rawContent);
@ -622,16 +588,10 @@ class _Step2ViewState extends ConsumerState<Step2View> {
setState(() {});
}
} on PlatformException catch (e, s) {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
@ -680,8 +640,9 @@ class _Step2ViewState extends ConsumerState<Step2View> {
child: TextButton(
onPressed: () {
Navigator.of(context).pushNamed(
Step3View.routeName,
arguments: model);
Step3View.routeName,
arguments: model,
);
},
style: Theme.of(context)
.extension<StackColors>()!

View file

@ -2,14 +2,14 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/change_now/change_now_response.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart';
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
import 'package:stackwallet/providers/exchange/exchange_provider.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/notifications_api.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
@ -243,33 +243,24 @@ class _Step3ViewState extends ConsumerState<Step3View> {
),
);
ChangeNowResponse<ExchangeTransaction>
response;
if (model.rateType ==
ExchangeRateType.estimated) {
response = await ref
.read(changeNowProvider)
.createStandardExchangeTransaction(
fromTicker: model.sendTicker,
toTicker: model.receiveTicker,
receivingAddress:
model.recipientAddress!,
amount: model.sendAmount,
refundAddress: model.refundAddress!,
);
} else {
response = await ref
.read(changeNowProvider)
.createFixedRateExchangeTransaction(
fromTicker: model.sendTicker,
toTicker: model.receiveTicker,
receivingAddress:
model.recipientAddress!,
amount: model.sendAmount,
refundAddress: model.refundAddress!,
rateId: model.rateId!,
);
}
final ExchangeResponse<Trade> response =
await ref
.read(exchangeProvider)
.createTrade(
from: model.sendTicker,
to: model.receiveTicker,
fixedRate: model.rateType !=
ExchangeRateType.estimated,
amount: model.reversed
? model.receiveAmount
: model.sendAmount,
addressTo: model.recipientAddress!,
extraId: null,
addressRefund: model.refundAddress!,
refundExtraId: "",
rateId: model.rateId,
reversed: model.reversed,
);
if (response.value == null) {
if (mounted) {
@ -293,20 +284,9 @@ class _Step3ViewState extends ConsumerState<Step3View> {
shouldNotifyListeners: true,
);
final statusResponse = await ref
.read(changeNowProvider)
.getTransactionStatus(
id: response.value!.id);
String status = response.value!.status;
String status = "Waiting";
if (statusResponse.value != null) {
status = statusResponse.value!.status.name;
}
model.trade = response.value!.copyWith(
statusString: status,
statusObject: statusResponse.value!,
);
model.trade = response.value!;
// extra info if status is waiting
if (status == "Waiting") {
@ -318,12 +298,12 @@ class _Step3ViewState extends ConsumerState<Step3View> {
}
unawaited(NotificationApi.showNotification(
changeNowId: model.trade!.id,
changeNowId: model.trade!.tradeId,
title: status,
body: "Trade ID ${model.trade!.id}",
body: "Trade ID ${model.trade!.tradeId}",
walletId: "",
iconAssetName: Assets.svg.arrowRotate,
date: model.trade!.date,
date: model.trade!.timestamp,
shouldWatchForUpdates: true,
coinName: "coinName",
));

View file

@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart';
@ -13,8 +12,6 @@ import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
@ -51,7 +48,6 @@ class _Step4ViewState extends ConsumerState<Step4View> {
late final ClipboardInterface clipboard;
String _statusString = "New";
ChangeNowTransactionStatus _status = ChangeNowTransactionStatus.New;
Timer? _statusTimer;
@ -69,13 +65,11 @@ class _Step4ViewState extends ConsumerState<Step4View> {
}
Future<void> _updateStatus() async {
final statusResponse = await ref
.read(changeNowProvider)
.getTransactionStatus(id: model.trade!.id);
final statusResponse =
await ref.read(exchangeProvider).updateTrade(model.trade!);
String status = "Waiting";
if (statusResponse.value != null) {
_status = statusResponse.value!.status;
status = _status.name;
status = statusResponse.value!.status;
}
// extra info if status is waiting
@ -112,7 +106,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
@override
Widget build(BuildContext context) {
final bool isWalletCoin =
_isWalletCoinAndHasWallet(model.trade!.fromCurrency, ref);
_isWalletCoinAndHasWallet(model.trade!.payInCurrency, ref);
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
@ -164,7 +158,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
height: 8,
),
Text(
"Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ChangeNOW will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.",
"Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.",
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
@ -272,7 +266,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
GestureDetector(
onTap: () async {
final data = ClipboardData(
text: model.trade!.payinAddress);
text: model.trade!.payInAddress);
await clipboard.setData(data);
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
@ -305,7 +299,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
height: 4,
),
Text(
model.trade!.payinAddress,
model.trade!.payInAddress,
style: STextStyles.itemSubtitle12(context),
),
],
@ -325,7 +319,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
Row(
children: [
Text(
model.trade!.id,
model.trade!.tradeId,
style: STextStyles.itemSubtitle12(context),
),
const SizedBox(
@ -333,8 +327,8 @@ class _Step4ViewState extends ConsumerState<Step4View> {
),
GestureDetector(
onTap: () async {
final data =
ClipboardData(text: model.trade!.id);
final data = ClipboardData(
text: model.trade!.tradeId);
await clipboard.setData(data);
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
@ -372,7 +366,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
STextStyles.itemSubtitle(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.colorForStatus(_status),
.colorForStatus(_statusString),
),
),
],
@ -408,7 +402,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
child: QrImage(
// TODO: grab coin uri scheme from somewhere
// data: "${coin.uriScheme}:$receivingAddress",
data: model.trade!.payinAddress,
data: model.trade!.payInAddress,
size: MediaQuery.of(context)
.size
.width /
@ -496,7 +490,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
Format.decimalAmountToSatoshis(
model.sendAmount);
final address =
model.trade!.payinAddress;
model.trade!.payInAddress;
try {
bool wasCancelled = false;
@ -534,7 +528,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
}
txData["note"] =
"${model.trade!.fromCurrency.toUpperCase()}/${model.trade!.toCurrency.toUpperCase()} exchange";
"${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange";
txData["address"] = address;
if (mounted) {
@ -611,10 +605,10 @@ class _Step4ViewState extends ConsumerState<Step4View> {
coin:
coinFromTickerCaseInsensitive(
model.trade!
.fromCurrency),
.payInCurrency),
amount: model.sendAmount,
address:
model.trade!.payinAddress,
model.trade!.payInAddress,
trade: model.trade!,
);
},

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
@ -36,7 +36,7 @@ class SendFromView extends ConsumerStatefulWidget {
final Coin coin;
final Decimal amount;
final String address;
final ExchangeTransaction trade;
final Trade trade;
@override
ConsumerState<SendFromView> createState() => _SendFromViewState();
@ -46,7 +46,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> {
late final Coin coin;
late final Decimal amount;
late final String address;
late final ExchangeTransaction trade;
late final Trade trade;
String formatAmount(Decimal amount, Coin coin) {
switch (coin) {
@ -148,7 +148,7 @@ class SendFromCard extends ConsumerStatefulWidget {
final String walletId;
final Decimal amount;
final String address;
final ExchangeTransaction trade;
final Trade trade;
@override
ConsumerState<SendFromCard> createState() => _SendFromCardState();
@ -158,7 +158,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
late final String walletId;
late final Decimal amount;
late final String address;
late final ExchangeTransaction trade;
late final Trade trade;
@override
void initState() {
@ -230,7 +230,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
}
txData["note"] =
"${trade.fromCurrency.toUpperCase()}/${trade.toCurrency.toUpperCase()} exchange";
"${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange";
txData["address"] = address;
if (mounted) {

View file

@ -0,0 +1,380 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class ExchangeProviderOptions extends ConsumerWidget {
const ExchangeProviderOptions({
Key? key,
required this.from,
required this.to,
required this.fromAmount,
required this.toAmount,
required this.fixedRate,
required this.reversed,
}) : super(key: key);
final String? from;
final String? to;
final Decimal? fromAmount;
final Decimal? toAmount;
final bool fixedRate;
final bool reversed;
@override
Widget build(BuildContext context, WidgetRef ref) {
return RoundedWhiteContainer(
child: Column(
children: [
GestureDetector(
onTap: () {
if (ref.read(currentExchangeNameStateProvider.state).state !=
ChangeNowExchange.exchangeName) {
ref.read(currentExchangeNameStateProvider.state).state =
ChangeNowExchange.exchangeName;
}
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: ChangeNowExchange.exchangeName,
groupValue: ref
.watch(currentExchangeNameStateProvider.state)
.state,
onChanged: (value) {
if (value is String) {
ref
.read(currentExchangeNameStateProvider.state)
.state = value;
}
},
),
),
const SizedBox(
width: 14,
),
SvgPicture.asset(
Assets.exchange.changeNow,
width: 24,
height: 24,
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ChangeNowExchange.exchangeName,
style: STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark2,
),
),
if (from != null &&
to != null &&
toAmount != null &&
toAmount! > Decimal.zero &&
fromAmount != null &&
fromAmount! > Decimal.zero)
FutureBuilder(
future: ChangeNowExchange().getEstimate(
from!,
to!,
reversed ? toAmount! : fromAmount!,
fixedRate,
reversed,
),
builder: (context,
AsyncSnapshot<ExchangeResponse<Estimate>>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
final estimate = snapshot.data?.value;
if (estimate != null) {
Decimal rate;
if (estimate.reversed) {
rate =
(toAmount! / estimate.estimatedAmount)
.toDecimal(
scaleOnInfinitePrecision: 12);
} else {
rate =
(estimate.estimatedAmount / fromAmount!)
.toDecimal(
scaleOnInfinitePrecision: 12);
}
return Text(
"1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed(
value: rate,
locale: ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
decimalPlaces: to!.toUpperCase() ==
Coin.monero.ticker.toUpperCase()
? Constants.decimalPlacesMonero
: Constants.decimalPlaces,
)} ${to!.toUpperCase()}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}",
level: LogLevel.Warning,
);
return Text(
"Failed to fetch rate",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading",
"Loading.",
"Loading..",
"Loading...",
],
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
},
),
if (!(from != null &&
to != null &&
toAmount != null &&
toAmount! > Decimal.zero &&
fromAmount != null &&
fromAmount! > Decimal.zero))
Text(
"n/a",
style: STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
const SizedBox(
height: 16,
),
GestureDetector(
onTap: () {
if (ref.read(currentExchangeNameStateProvider.state).state !=
SimpleSwapExchange.exchangeName) {
ref.read(currentExchangeNameStateProvider.state).state =
SimpleSwapExchange.exchangeName;
}
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: SimpleSwapExchange.exchangeName,
groupValue: ref
.watch(currentExchangeNameStateProvider.state)
.state,
onChanged: (value) {
if (value is String) {
ref
.read(currentExchangeNameStateProvider.state)
.state = value;
}
},
),
),
const SizedBox(
width: 14,
),
SvgPicture.asset(
Assets.exchange.simpleSwap,
width: 24,
height: 24,
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SimpleSwapExchange.exchangeName,
style: STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark2,
),
),
if (from != null &&
to != null &&
toAmount != null &&
toAmount! > Decimal.zero &&
fromAmount != null &&
fromAmount! > Decimal.zero)
FutureBuilder(
future: SimpleSwapExchange().getEstimate(
from!,
to!,
// reversed ? toAmount! : fromAmount!,
fromAmount!,
fixedRate,
// reversed,
false,
),
builder: (context,
AsyncSnapshot<ExchangeResponse<Estimate>>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
final estimate = snapshot.data?.value;
if (estimate != null) {
Decimal rate = (estimate.estimatedAmount /
fromAmount!)
.toDecimal(scaleOnInfinitePrecision: 12);
return Text(
"1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed(
value: rate,
locale: ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
decimalPlaces: to!.toUpperCase() ==
Coin.monero.ticker.toUpperCase()
? Constants.decimalPlacesMonero
: Constants.decimalPlaces,
)} ${to!.toUpperCase()}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}",
level: LogLevel.Warning,
);
return Text(
"Failed to fetch rate",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading",
"Loading.",
"Loading..",
"Loading...",
],
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
},
),
// if (!(from != null &&
// to != null &&
// (reversed
// ? toAmount != null && toAmount! > Decimal.zero
// : fromAmount != null &&
// fromAmount! > Decimal.zero)))
if (!(from != null &&
to != null &&
toAmount != null &&
toAmount! > Decimal.zero &&
fromAmount != null &&
fromAmount! > Decimal.zero))
Text(
"n/a",
style: STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class RateTypeToggle extends ConsumerWidget {
const RateTypeToggle({
Key? key,
this.onChanged,
}) : super(key: key);
final void Function(ExchangeRateType)? onChanged;
@override
Widget build(BuildContext context, WidgetRef ref) {
debugPrint("BUILD: $runtimeType");
final estimated = ref.watch(prefsChangeNotifierProvider
.select((value) => value.exchangeRateType)) ==
ExchangeRateType.estimated;
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (!estimated) {
ref.read(prefsChangeNotifierProvider).exchangeRateType =
ExchangeRateType.estimated;
onChanged?.call(ExchangeRateType.estimated);
}
},
child: RoundedContainer(
color: estimated
? Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG
: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
Assets.svg.lock,
width: 12,
height: 14,
color: estimated
? Theme.of(context).extension<StackColors>()!.textDark
: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
const SizedBox(
width: 5,
),
Text(
"Estimate rate",
style: STextStyles.smallMed12(context).copyWith(
color: estimated
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
if (estimated) {
ref.read(prefsChangeNotifierProvider).exchangeRateType =
ExchangeRateType.fixed;
onChanged?.call(ExchangeRateType.fixed);
}
},
child: RoundedContainer(
color: !estimated
? Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG
: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
Assets.svg.lock,
width: 12,
height: 14,
color: !estimated
? Theme.of(context).extension<StackColors>()!.textDark
: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
const SizedBox(
width: 5,
),
Text(
"Fixed rate",
style: STextStyles.smallMed12(context).copyWith(
color: !estimated
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
),
),
],
),
);
}
}

View file

@ -13,10 +13,11 @@ import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart';
import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
import 'package:stackwallet/providers/exchange/trade_note_service_provider.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -82,18 +83,16 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
final trade = ref
.read(tradesServiceProvider)
.trades
.firstWhere((e) => e.id == tradeId);
.firstWhere((e) => e.tradeId == tradeId);
if (mounted && trade.statusObject == null ||
trade.statusObject!.amountSendDecimal.isEmpty) {
final status = await ref
.read(changeNowProvider)
.getTransactionStatus(id: trade.id);
if (mounted) {
final exchange = Exchange.fromName(trade.exchangeName);
final response = await exchange.updateTrade(trade);
if (mounted && status.value != null) {
await ref.read(tradesServiceProvider).edit(
trade: trade.copyWith(statusObject: status.value),
shouldNotifyListeners: true);
if (mounted && response.value != null) {
await ref
.read(tradesServiceProvider)
.edit(trade: response.value!, shouldNotifyListeners: true);
}
}
});
@ -132,23 +131,29 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
final bool sentFromStack =
transactionIfSentFromStack != null && walletId != null;
final trade = ref.watch(tradesServiceProvider
.select((value) => value.trades.firstWhere((e) => e.id == tradeId)));
final trade = ref.watch(tradesServiceProvider.select(
(value) => value.trades.firstWhere((e) => e.tradeId == tradeId)));
final bool hasTx = sentFromStack ||
!(trade.statusObject?.status == ChangeNowTransactionStatus.New ||
trade.statusObject?.status == ChangeNowTransactionStatus.Waiting ||
trade.statusObject?.status == ChangeNowTransactionStatus.Refunded ||
trade.statusObject?.status == ChangeNowTransactionStatus.Failed);
!(trade.status == "New" ||
trade.status == "new" ||
trade.status == "Waiting" ||
trade.status == "waiting" ||
trade.status == "Refunded" ||
trade.status == "refunded" ||
trade.status == "Closed" ||
trade.status == "closed" ||
trade.status == "Expired" ||
trade.status == "expired" ||
trade.status == "Failed" ||
trade.status == "failed");
debugPrint("sentFromStack: $sentFromStack");
debugPrint("hasTx: $hasTx");
debugPrint("trade: ${trade.toString()}");
final sendAmount = Decimal.tryParse(
trade.statusObject?.amountSendDecimal ?? "") ??
Decimal.tryParse(trade.statusObject?.expectedSendAmountDecimal ?? "") ??
Decimal.parse("-1");
final sendAmount =
Decimal.tryParse(trade.payInAmount) ?? Decimal.parse("-1");
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -180,7 +185,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
"${trade.fromCurrency.toUpperCase()}${trade.toCurrency.toUpperCase()}",
"${trade.payInCurrency.toUpperCase()}${trade.payOutCurrency.toUpperCase()}",
style: STextStyles.titleBold12(context),
),
const SizedBox(
@ -190,7 +195,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
"${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
), decimalPlaces: trade.fromCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.fromCurrency.toUpperCase()}",
), decimalPlaces: trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}",
style: STextStyles.itemSubtitle(context),
),
],
@ -203,9 +208,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
),
child: Center(
child: SvgPicture.asset(
_fetchIconAssetForStatus(
trade.statusObject?.status.name ??
trade.statusString),
_fetchIconAssetForStatus(trade.status),
width: 32,
height: 32,
),
@ -229,15 +232,11 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
height: 4,
),
SelectableText(
trade.statusObject?.status.name ?? trade.statusString,
trade.status,
style: STextStyles.itemSubtitle(context).copyWith(
color: trade.statusObject != null
? Theme.of(context)
.extension<StackColors>()!
.colorForStatus(trade.statusObject!.status)
: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
color: Theme.of(context)
.extension<StackColors>()!
.colorForStatus(trade.status),
),
),
// ),
@ -258,8 +257,8 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
text: TextSpan(
text:
"You must send at least ${sendAmount.toStringAsFixed(
trade.fromCurrency.toLowerCase() == "xmr" ? 12 : 8,
)} ${trade.fromCurrency.toUpperCase()}. ",
trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8,
)} ${trade.payInCurrency.toUpperCase()}. ",
style: STextStyles.label700(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -269,10 +268,10 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
TextSpan(
text:
"If you send less than ${sendAmount.toStringAsFixed(
trade.fromCurrency.toLowerCase() == "xmr"
trade.payInCurrency.toLowerCase() == "xmr"
? 12
: 8,
)} ${trade.fromCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.",
)} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.",
style: STextStyles.label(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -308,7 +307,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
GestureDetector(
onTap: () {
final Coin coin = coinFromTickerCaseInsensitive(
trade.fromCurrency);
trade.payInCurrency);
Navigator.of(context).pushNamed(
TransactionDetailsView.routeName,
@ -334,14 +333,14 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"ChangeNOW address",
"${trade.exchangeName} address",
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
height: 4,
),
SelectableText(
trade.payinAddress,
trade.payInAddress,
style: STextStyles.itemSubtitle12(context),
),
],
@ -360,12 +359,12 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Send ${trade.fromCurrency.toUpperCase()} to this address",
"Send ${trade.payInCurrency.toUpperCase()} to this address",
style: STextStyles.itemSubtitle(context),
),
GestureDetector(
onTap: () async {
final address = trade.payinAddress;
final address = trade.payInAddress;
await Clipboard.setData(
ClipboardData(
text: address,
@ -403,7 +402,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
height: 4,
),
SelectableText(
trade.payinAddress,
trade.payInAddress,
style: STextStyles.itemSubtitle12(context),
),
const SizedBox(
@ -425,7 +424,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
children: [
Center(
child: Text(
"Send ${trade.fromCurrency.toUpperCase()} to this address",
"Send ${trade.payInCurrency.toUpperCase()} to this address",
style:
STextStyles.pageTitleH2(context),
),
@ -440,7 +439,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
width: width + 20,
height: width + 20,
child: QrImage(
data: trade.payinAddress,
data: trade.payInAddress,
size: width,
backgroundColor: Theme.of(
context)
@ -658,7 +657,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
// child:
SelectableText(
Format.extractDateFrom(
trade.date.millisecondsSinceEpoch ~/ 1000),
trade.timestamp.millisecondsSinceEpoch ~/ 1000),
style: STextStyles.itemSubtitle12(context),
),
// ),
@ -677,16 +676,10 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
"Exchange",
style: STextStyles.itemSubtitle(context),
),
// Flexible(
// child: FittedBox(
// fit: BoxFit.scaleDown,
// child:
SelectableText(
"ChangeNOW",
trade.exchangeName,
style: STextStyles.itemSubtitle12(context),
),
// ),
// ),
],
),
),
@ -704,7 +697,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
Row(
children: [
Text(
trade.id,
trade.tradeId,
style: STextStyles.itemSubtitle12(context),
),
const SizedBox(
@ -712,7 +705,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
),
GestureDetector(
onTap: () async {
final data = ClipboardData(text: trade.id);
final data = ClipboardData(text: trade.tradeId);
await clipboard.setData(data);
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
@ -747,40 +740,50 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
const SizedBox(
height: 4,
),
GestureDetector(
onTap: () {
final url =
"https://changenow.io/exchange/txs/${trade.id}";
launchUrl(
Uri.parse(url),
mode: LaunchMode.externalApplication,
);
},
child: Text(
"https://changenow.io/exchange/txs/${trade.id}",
style: STextStyles.link2(context),
),
),
Builder(builder: (context) {
late final String url;
switch (trade.exchangeName) {
case ChangeNowExchange.exchangeName:
url =
"https://changenow.io/exchange/txs/${trade.tradeId}";
break;
case SimpleSwapExchange.exchangeName:
url =
"https://simpleswap.io/exchange?id=${trade.tradeId}";
break;
}
return GestureDetector(
onTap: () {
launchUrl(
Uri.parse(url),
mode: LaunchMode.externalApplication,
);
},
child: Text(
url,
style: STextStyles.link2(context),
),
);
}),
],
),
),
const SizedBox(
height: 12,
),
if (isStackCoin(trade.fromCurrency) &&
trade.statusObject != null &&
(trade.statusObject!.status ==
ChangeNowTransactionStatus.New ||
trade.statusObject!.status ==
ChangeNowTransactionStatus.Waiting))
if (isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
SecondaryButton(
label: "Send from Stack",
onPressed: () {
final amount = sendAmount;
final address = trade.payinAddress;
final address = trade.payInAddress;
final coin =
coinFromTickerCaseInsensitive(trade.fromCurrency);
coinFromTickerCaseInsensitive(trade.payInCurrency);
Navigator.of(context).pushNamed(
SendFromView.routeName,

File diff suppressed because it is too large Load diff

View file

@ -13,9 +13,11 @@ import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
import 'package:stackwallet/providers/global/notifications_provider.dart';
import 'package:stackwallet/providers/ui/home_view_index_provider.dart';
import 'package:stackwallet/providers/ui/unread_notifications_provider.dart';
import 'package:stackwallet/services/change_now/change_now_loading_service.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
@ -41,7 +43,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
bool _exitEnabled = false;
final _cnLoadingService = ChangeNowLoadingService();
final _exchangeDataLoadingService = ExchangeDataLoadingService();
Future<bool> _onWillPop() async {
// go to home view when tapping back on the main exchange view
@ -81,7 +83,14 @@ class _HomeViewState extends ConsumerState<HomeView> {
void _loadCNData() {
// unawaited future
_cnLoadingService.loadAll(ref);
//
final externalCalls = Prefs.instance.externalCalls;
if (externalCalls) {
_exchangeDataLoadingService.loadAll(ref);
} else {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
}
}
@override
@ -279,6 +288,9 @@ class _HomeViewState extends ConsumerState<HomeView> {
_ref.listen(homeViewPageIndexStateProvider,
(previous, next) {
if (next is int) {
if (next == 1) {
_loadCNData();
}
if (next >= 0 && next <= 1) {
_pageController.animateToPage(
next,

View file

@ -1,16 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/exchange_view/exchange_view.dart';
import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart';
import 'package:stackwallet/providers/exchange/available_floating_rate_pairs_state_provider.dart';
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/exchange/fixed_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/exchange/fixed_rate_market_pairs_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
@ -23,114 +14,17 @@ class HomeViewButtonBar extends ConsumerStatefulWidget {
}
class _HomeViewButtonBarState extends ConsumerState<HomeViewButtonBar> {
final DateTime _lastRefreshed = DateTime.now();
final Duration _refreshInterval = const Duration(hours: 1);
Future<void> _loadChangeNowData(
BuildContext context,
WidgetRef ref,
) async {
List<Future<void>> futures = [];
if (kFixedRateEnabled) {
futures.add(_loadFixedRateMarkets(context, ref));
}
futures.add(_loadStandardCurrencies(context, ref));
await Future.wait(futures);
}
Future<void> _loadStandardCurrencies(
BuildContext context,
WidgetRef ref,
) async {
final response = await ref.read(changeNowProvider).getAvailableCurrencies();
final response2 =
await ref.read(changeNowProvider).getAvailableFloatingRatePairs();
if (response.value != null && response2.value != null) {
ref.read(availableChangeNowCurrenciesStateProvider.state).state =
response.value!;
ref.read(availableFloatingRatePairsStateProvider.state).state =
response2.value!;
if (response.value!.length > 1) {
if (ref.read(estimatedRateExchangeFormProvider).from == null) {
if (response.value!.where((e) => e.ticker == "btc").isNotEmpty) {
await ref.read(estimatedRateExchangeFormProvider).updateFrom(
response.value!.firstWhere((e) => e.ticker == "btc"), true);
}
}
if (ref.read(estimatedRateExchangeFormProvider).to == null) {
if (response.value!.where((e) => e.ticker == "doge").isNotEmpty) {
await ref.read(estimatedRateExchangeFormProvider).updateTo(
response.value!.firstWhere((e) => e.ticker == "doge"), true);
}
}
}
Logging.instance
.log("loaded floating rate change now data", level: LogLevel.Info);
} else {
Logging.instance.log(
"Failed to load changeNOW floating rate market data: \n${response.exception?.errorMessage}\n${response2.exception?.toString()}",
level: LogLevel.Error);
unawaited(showDialog<dynamic>(
context: context,
barrierDismissible: true,
builder: (_) => StackDialog(
title: "Failed to fetch available currencies",
message:
"${response.exception?.toString()}\n\n${response2.exception?.toString()}",
),
));
}
}
Future<void> _loadFixedRateMarkets(
BuildContext context,
WidgetRef ref,
) async {
final response3 =
await ref.read(changeNowProvider).getAvailableFixedRateMarkets();
if (response3.value != null) {
ref.read(fixedRateMarketPairsStateProvider.state).state =
response3.value!;
if (ref.read(fixedRateExchangeFormProvider).market == null) {
final matchingMarkets =
response3.value!.where((e) => e.to == "doge" && e.from == "btc");
if (matchingMarkets.isNotEmpty) {
await ref
.read(fixedRateExchangeFormProvider)
.updateMarket(matchingMarkets.first, true);
}
}
Logging.instance
.log("loaded fixed rate change now data", level: LogLevel.Info);
} else {
Logging.instance.log(
"Failed to load changeNOW fixed rate markets: ${response3.exception?.errorMessage}",
level: LogLevel.Error);
unawaited(showDialog<dynamic>(
context: context,
barrierDismissible: true,
builder: (_) => StackDialog(
title: "ChangeNOW API call failed",
message: "${response3.exception?.toString()}",
),
));
}
}
// final DateTime _lastRefreshed = DateTime.now();
// final Duration _refreshInterval = const Duration(hours: 1);
@override
void initState() {
ref.read(estimatedRateExchangeFormProvider).setOnError(
ref.read(exchangeFormStateProvider).setOnError(
onError: (String message) => showDialog<dynamic>(
context: context,
barrierDismissible: true,
builder: (_) => StackDialog(
title: "ChangeNOW API Call Failed",
title: "Exchange API Call Failed",
message: message,
),
),
@ -206,29 +100,14 @@ class _HomeViewButtonBarState extends ConsumerState<HomeViewButtonBar> {
if (selectedIndex != 1) {
ref.read(homeViewPageIndexStateProvider.state).state = 1;
}
DateTime now = DateTime.now();
if (now.difference(_lastRefreshed) > _refreshInterval) {
// bool okPressed = false;
// showDialog<dynamic>(
// context: context,
// barrierDismissible: false,
// builder: (_) => const StackDialog(
// // builder: (_) => StackOkDialog(
// title: "Refreshing ChangeNOW data",
// message: "This may take a while",
// // onOkPressed: (value) {
// // if (value == "OK") {
// // okPressed = true;
// // }
// // },
// ),
// );
await _loadChangeNowData(context, ref);
// if (!okPressed && mounted) {
// Navigator.of(context).pop();
// }
// DateTime now = DateTime.now();
if (ref.read(prefsChangeNotifierProvider).externalCalls) {
print("loading?");
await ExchangeDataLoadingService().loadAll(ref);
}
// if (now.difference(_lastRefreshed) > _refreshInterval) {
// await ExchangeDataLoadingService().loadAll(ref);
// }
},
child: Text(
"Exchange",

View file

@ -1,17 +1,21 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
import 'package:stackwallet/pages/stack_privacy_calls.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:stackwallet/utilities/prefs.dart';
class IntroView extends StatefulWidget {
const IntroView({Key? key}) : super(key: key);
static const String routeName = "/introView";
@override
State<IntroView> createState() => _IntroViewState();
}
@ -240,7 +244,11 @@ class GetStartedButton extends StatelessWidget {
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {
Navigator.of(context).pushNamed(CreatePinView.routeName);
Prefs.instance.externalCalls = true;
Navigator.of(context).pushNamed(
StackPrivacyCalls.routeName,
arguments: false,
);
},
child: Text(
"Get started",
@ -255,7 +263,10 @@ class GetStartedButton extends StatelessWidget {
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {
Navigator.of(context).pushNamed(CreatePasswordView.routeName);
Navigator.of(context).pushNamed(
StackPrivacyCalls.routeName,
arguments: false,
);
},
child: Text(
"Get started",

View file

@ -27,6 +27,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_text.dart';
@ -1107,115 +1108,120 @@ class _SendViewState extends ConsumerState<SendView> {
),
),
),
const SizedBox(
height: 8,
),
TextField(
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
if (Prefs.instance.externalCalls)
const SizedBox(
height: 8,
),
key: const Key("amountInputFieldFiatTextFieldKey"),
controller: baseAmountController,
focusNode: _baseFocus,
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
// regex to validate a fiat amount with 2 decimal places
TextInputFormatter.withFunction((oldValue,
newValue) =>
RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
.hasMatch(newValue.text)
? newValue
: oldValue),
],
onChanged: (baseAmountString) {
if (baseAmountString.isNotEmpty &&
baseAmountString != "." &&
baseAmountString != ",") {
final baseAmount = baseAmountString.contains(",")
? Decimal.parse(
baseAmountString.replaceFirst(",", "."))
: Decimal.parse(baseAmountString);
if (Prefs.instance.externalCalls)
TextField(
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
key: const Key("amountInputFieldFiatTextFieldKey"),
controller: baseAmountController,
focusNode: _baseFocus,
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
// regex to validate a fiat amount with 2 decimal places
TextInputFormatter.withFunction((oldValue,
newValue) =>
RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
.hasMatch(newValue.text)
? newValue
: oldValue),
],
onChanged: (baseAmountString) {
if (baseAmountString.isNotEmpty &&
baseAmountString != "." &&
baseAmountString != ",") {
final baseAmount = baseAmountString
.contains(",")
? Decimal.parse(
baseAmountString.replaceFirst(",", "."))
: Decimal.parse(baseAmountString);
var _price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
var _price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (_price == Decimal.zero) {
_amountToSend = Decimal.zero;
if (_price == Decimal.zero) {
_amountToSend = Decimal.zero;
} else {
_amountToSend = baseAmount <= Decimal.zero
? Decimal.zero
: (baseAmount / _price).toDecimal(
scaleOnInfinitePrecision:
Constants.decimalPlaces);
}
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final amountString =
Format.localizedStringAsFixed(
value: _amountToSend!,
locale: ref
.read(localeServiceChangeNotifierProvider)
.locale,
decimalPlaces: Constants.decimalPlaces,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
_amountToSend = baseAmount <= Decimal.zero
? Decimal.zero
: (baseAmount / _price).toDecimal(
scaleOnInfinitePrecision:
Constants.decimalPlaces);
_amountToSend = Decimal.zero;
_cryptoAmountChangeLock = true;
cryptoAmountController.text = "";
_cryptoAmountChangeLock = false;
}
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final amountString =
Format.localizedStringAsFixed(
value: _amountToSend!,
locale: ref
.read(localeServiceChangeNotifierProvider)
.locale,
decimalPlaces: Constants.decimalPlaces,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
_amountToSend = Decimal.zero;
_cryptoAmountChangeLock = true;
cryptoAmountController.text = "";
_cryptoAmountChangeLock = false;
}
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(
// _amountToSend!));
// });
_updatePreviewButtonState(_address, _amountToSend);
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
top: 12,
right: 12,
),
hintText: "0",
hintStyle: STextStyles.fieldLabel(context).copyWith(
fontSize: 14,
),
prefixIcon: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency)),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(
// _amountToSend!));
// });
_updatePreviewButtonState(
_address, _amountToSend);
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
top: 12,
right: 12,
),
hintText: "0",
hintStyle:
STextStyles.fieldLabel(context).copyWith(
fontSize: 14,
),
prefixIcon: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency)),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
),
),
const SizedBox(
height: 12,
),
@ -1319,6 +1325,12 @@ class _SendViewState extends ConsumerState<SendView> {
cryptoAmountController
.text) ??
Decimal.zero,
updateChosen: (String fee) {
setState(() {
_calculateFeesFuture =
Future(() => fee);
});
},
),
);
},

View file

@ -32,10 +32,12 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget {
Key? key,
required this.walletId,
required this.amount,
required this.updateChosen,
}) : super(key: key);
final String walletId;
final Decimal amount;
final Function updateChosen;
@override
ConsumerState<TransactionFeeSelectionSheet> createState() =>
@ -223,6 +225,10 @@ class _TransactionFeeSelectionSheetState
ref.read(feeRateTypeStateProvider.state).state =
FeeRateType.fast;
}
String? fee = getAmount(FeeRateType.fast);
if (fee != null) {
widget.updateChosen(fee);
}
Navigator.of(context).pop();
},
child: Container(
@ -352,6 +358,10 @@ class _TransactionFeeSelectionSheetState
ref.read(feeRateTypeStateProvider.state).state =
FeeRateType.average;
}
String? fee = getAmount(FeeRateType.average);
if (fee != null) {
widget.updateChosen(fee);
}
Navigator.of(context).pop();
},
child: Container(
@ -410,8 +420,8 @@ class _TransactionFeeSelectionSheetState
FutureBuilder(
future: feeFor(
coin: manager.coin,
feeRateType: FeeRateType.fast,
feeRate: feeObject!.fast,
feeRateType: FeeRateType.average,
feeRate: feeObject!.medium,
amount: Format
.decimalAmountToSatoshis(
amount)),
@ -479,6 +489,11 @@ class _TransactionFeeSelectionSheetState
ref.read(feeRateTypeStateProvider.state).state =
FeeRateType.slow;
}
String? fee = getAmount(FeeRateType.slow);
print("fee $fee");
if (fee != null) {
widget.updateChosen(fee);
}
Navigator.of(context).pop();
},
child: Container(
@ -608,4 +623,45 @@ class _TransactionFeeSelectionSheetState
),
);
}
String? getAmount(FeeRateType feeRateType) {
try {
print(feeRateType);
var amount = Format.decimalAmountToSatoshis(this.amount);
print(amount);
print(ref.read(feeSheetSessionCacheProvider).fast);
print(ref.read(feeSheetSessionCacheProvider).average);
print(ref.read(feeSheetSessionCacheProvider).slow);
switch (feeRateType) {
case FeeRateType.fast:
if (ref.read(feeSheetSessionCacheProvider).fast[amount] != null) {
return (ref.read(feeSheetSessionCacheProvider).fast[amount]
as Decimal)
.toString();
}
return null;
case FeeRateType.average:
if (ref.read(feeSheetSessionCacheProvider).average[amount] != null) {
return (ref.read(feeSheetSessionCacheProvider).average[amount]
as Decimal)
.toString();
}
return null;
case FeeRateType.slow:
print(ref.read(feeSheetSessionCacheProvider).slow);
print(ref.read(feeSheetSessionCacheProvider).slow[amount]);
if (ref.read(feeSheetSessionCacheProvider).slow[amount] != null) {
return (ref.read(feeSheetSessionCacheProvider).slow[amount]
as Decimal)
.toString();
}
return null;
}
} catch (e, s) {
print("$e $s");
return null;
}
}
}

View file

@ -8,6 +8,9 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/pages/stack_privacy_calls.dart';
class AdvancedSettingsView extends StatelessWidget {
const AdvancedSettingsView({
@ -115,6 +118,62 @@ class AdvancedSettingsView extends StatelessWidget {
},
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Consumer(
builder: (_, ref, __) {
final externalCalls = ref.watch(
prefsChangeNotifierProvider
.select((value) => value.externalCalls),
);
return RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
Navigator.of(context).pushNamed(
StackPrivacyCalls.routeName,
arguments: true,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 20,
),
child: Row(
children: [
RichText(
textAlign: TextAlign.left,
text: TextSpan(
children: [
TextSpan(
text: "Stack Experience",
style: STextStyles.titleBold12(context),
),
TextSpan(
text: externalCalls
? "\nEasy crypto"
: "\nIncognito",
style: STextStyles.label(context)
.copyWith(fontSize: 15.0),
)
],
),
),
],
),
),
);
},
),
),
],
),
),

View file

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/utilities/delete_everything.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class DeleteAccountView extends StatefulWidget {
const DeleteAccountView({Key? key}) : super(key: key);
static const String routeName = "/deleteAccountView";
@override
State<DeleteAccountView> createState() => _DeleteAccountViewState();
}
class _DeleteAccountViewState extends State<DeleteAccountView> {
final isDesktop = Util.isDesktop;
Future<void> onConfirmDeleteAccount() async {
// TODO delete everything then pop to intro view
await showDialog(
barrierDismissible: true,
context: context,
builder: (_) => StackDialog(
title: "Are you sure you want to delete all Wallets?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
onPressed: () {
Navigator.pop(context);
},
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () async {
await deleteEverything();
await Navigator.of(context).pushNamedAndRemoveUntil(
IntroView.routeName,
(route) => false,
);
},
child: Text(
"Delete",
style: STextStyles.button(context),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return MasterScaffold(
isDesktop: isDesktop,
appBar: isDesktop
? DesktopAppBar(isCompactHeight: true)
: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Delete account",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(14),
child: Column(
children: [
RoundedWhiteContainer(
child: Text(
"There is no account to delete, but Apple requires that we have a way to 'delete accounts' in the app and will reject our app updates if we don't, so here it is. Clicking this will delete all app data (not from our servers, because we never had it in the first place).\n\nWhen you click confirm, all app data will be deleted, including wallets and preferences, and you will be taken back to the very first onboarding screen. BE SURE TO BACKUP ALL SEEDS!!\n\nAre you sure you want to delete your \"account\"?",
style: STextStyles.smallMed12(context),
),
),
const Spacer(),
PrimaryButton(
label: "Confirm",
onPressed: onConfirmDeleteAccount,
)
],
),
),
);
}
}

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
@ -5,6 +7,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/about_view
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/currency_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/delete_account_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/language_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/security_views/security_view.dart';
@ -20,6 +23,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/utilities/delete_everything.dart';
class GlobalSettingsView extends StatelessWidget {
const GlobalSettingsView({
Key? key,
@ -188,6 +193,20 @@ class GlobalSettingsView extends StatelessWidget {
AppearanceSettingsView.routeName);
},
),
if (Platform.isIOS)
const SizedBox(
height: 8,
),
if (Platform.isIOS)
SettingsListButton(
iconAssetName: Assets.svg.circleAlert,
iconSize: 16,
title: "Delete account",
onPressed: () async {
await Navigator.of(context)
.pushNamed(DeleteAccountView.routeName);
},
),
const SizedBox(
height: 8,
),

View file

@ -11,6 +11,7 @@ import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/stack_restoring_ui_state.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
@ -401,7 +402,7 @@ abstract class SWB {
// without using them
await manager.recoverFromMnemonic(
mnemonic: mnemonic,
maxUnusedAddressGap: 20,
maxUnusedAddressGap: manager.coin == Coin.firo ? 50 : 20,
maxNumberOfIndexesToCheck: 1000,
height: restoreHeight,
);
@ -996,10 +997,14 @@ abstract class SWB {
// primary nodes
if (primaryNodes != null) {
for (var node in primaryNodes) {
await nodeService.setPrimaryNodeFor(
coin: coinFromPrettyName(node['coinName'] as String),
node: nodeService.getNodeById(id: node['id'] as String)!,
);
try {
await nodeService.setPrimaryNodeFor(
coin: coinFromPrettyName(node['coinName'] as String),
node: nodeService.getNodeById(id: node['id'] as String)!,
);
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
}
}
}
await nodeService.updateDefaults();
@ -1026,8 +1031,7 @@ abstract class SWB {
// trade existed before attempted restore so we don't delete it, only
// revert data to pre restore state
await tradesService.edit(
trade: ExchangeTransaction.fromJson(
tradeData as Map<String, dynamic>),
trade: Trade.fromMap(tradeData as Map<String, dynamic>),
shouldNotifyListeners: true);
} else {
// trade did not exist before so we delete it
@ -1048,7 +1052,7 @@ abstract class SWB {
}
} else {
// grab all trade IDs of (reverted to pre state) trades
final idsToKeep = tradesService.trades.map((e) => e.id);
final idsToKeep = tradesService.trades.map((e) => e.tradeId);
// delete all notes that don't correspond to an id that we have
for (final noteEntry in currentNotes.entries) {
@ -1175,10 +1179,14 @@ abstract class SWB {
}
if (primaryNodes != null) {
for (var node in primaryNodes) {
await nodeService.setPrimaryNodeFor(
coin: coinFromPrettyName(node['coinName'] as String),
node: nodeService.getNodeById(id: node['id'] as String)!,
);
try {
await nodeService.setPrimaryNodeFor(
coin: coinFromPrettyName(node['coinName'] as String),
node: nodeService.getNodeById(id: node['id'] as String)!,
);
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
}
}
}
await nodeService.updateDefaults();
@ -1189,16 +1197,44 @@ abstract class SWB {
) async {
final tradesService = TradesService();
for (int i = 0; i < trades.length - 1; i++) {
ExchangeTransaction? exTx;
try {
exTx = ExchangeTransaction.fromJson(trades[i] as Map<String, dynamic>);
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
}
Trade trade;
if (exTx != null) {
trade = Trade.fromExchangeTransaction(exTx, false);
} else {
trade = Trade.fromMap(trades[i] as Map<String, dynamic>);
}
await tradesService.add(
trade: ExchangeTransaction.fromJson(trades[i] as Map<String, dynamic>),
trade: trade,
shouldNotifyListeners: false,
);
}
// only call notifyListeners on last one added
if (trades.isNotEmpty) {
ExchangeTransaction? exTx;
try {
exTx =
ExchangeTransaction.fromJson(trades.last as Map<String, dynamic>);
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
}
Trade trade;
if (exTx != null) {
trade = Trade.fromExchangeTransaction(exTx, false);
} else {
trade = Trade.fromMap(trades.last as Map<String, dynamic>);
}
await tradesService.add(
trade:
ExchangeTransaction.fromJson(trades.last as Map<String, dynamic>),
trade: trade,
shouldNotifyListeners: true,
);
}

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class StackFileSystem {
Directory? rootPath;
@ -14,6 +15,9 @@ class StackFileSystem {
final bool isDesktop = !(Platform.isAndroid || Platform.isIOS);
Future<Directory> prepareStorage() async {
if (Platform.isAndroid) {
await Permission.storage.request();
}
rootPath = (await getApplicationDocumentsDirectory());
debugPrint(rootPath!.absolute.toString());
if (Platform.isAndroid) {
@ -22,13 +26,13 @@ class StackFileSystem {
debugPrint(rootPath!.absolute.toString());
Directory sampleFolder =
Directory('${rootPath!.path}/Documents/Stack_backups');
Directory('${rootPath!.path}Documents/Stack_backups');
if (Platform.isIOS) {
sampleFolder = Directory(rootPath!.path);
}
try {
if (!sampleFolder.existsSync()) {
sampleFolder.createSync();
sampleFolder.createSync(recursive: true);
}
} catch (e, s) {
debugPrint("$e $s");
@ -65,8 +69,7 @@ class StackFileSystem {
result = await FilePicker.platform.pickFiles(
dialogTitle: "Load backup file",
initialDirectory: startPath!.path,
type: FileType.custom,
allowedExtensions: ['bin'],
type: FileType.any,
allowCompression: false,
lockParentWindow: true,
);

View file

@ -389,7 +389,7 @@ class _StackRestoreProgressViewState
height: 20,
child: _getIconForState(state),
),
title: "ChangeNOW history",
title: "Exchange history",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",

View file

@ -35,7 +35,8 @@ class _RestoringWalletCardState extends ConsumerState<RestoringWalletCard> {
case StackRestoringStatus.waiting:
return SvgPicture.asset(
Assets.svg.loader,
color: Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
color:
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
);
case StackRestoringStatus.restoring:
return const LoadingIndicator();
@ -95,7 +96,10 @@ class _RestoringWalletCardState extends ConsumerState<RestoringWalletCard> {
try {
final mnemonicList = await manager.mnemonic;
const maxUnusedAddressGap = 20;
int maxUnusedAddressGap = 20;
if (coin == Coin.firo) {
maxUnusedAddressGap = 50;
}
const maxNumberOfIndexesToCheck = 1000;
if (mnemonicList.isEmpty) {
@ -155,14 +159,17 @@ class _RestoringWalletCardState extends ConsumerState<RestoringWalletCard> {
? Container(
height: 20,
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
borderRadius: BorderRadius.circular(
1000,
),
),
child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
1000,
@ -187,7 +194,9 @@ class _RestoringWalletCardState extends ConsumerState<RestoringWalletCard> {
child: Text(
"Show recovery phrase",
style: STextStyles.infoSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),

View file

@ -77,7 +77,8 @@ class _WalletNetworkSettingsViewState
Future<void> _attemptRescan() async {
if (!Platform.isLinux) Wakelock.enable();
const int maxUnusedAddressGap = 20;
int maxUnusedAddressGap = 20;
const int maxNumberOfIndexesToCheck = 1000;
showDialog<dynamic>(
@ -88,6 +89,13 @@ class _WalletNetworkSettingsViewState
);
try {
if (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.coin ==
Coin.firo) {
maxUnusedAddressGap = 50;
}
await ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)

View file

@ -0,0 +1,553 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class StackPrivacyCalls extends ConsumerStatefulWidget {
const StackPrivacyCalls({
Key? key,
required this.isSettings,
}) : super(key: key);
final bool isSettings;
static const String routeName = "/stackPrivacy";
@override
ConsumerState<StackPrivacyCalls> createState() => _StackPrivacyCalls();
}
class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> {
late final bool isDesktop;
late bool isEasy;
late bool infoToggle;
@override
void initState() {
isDesktop = Util.isDesktop;
isEasy = ref.read(prefsChangeNotifierProvider).externalCalls;
infoToggle = isEasy;
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 40, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Choose your Stack experience",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 8,
),
Text(
"You can change it later in Settings",
style: STextStyles.subtitle(context),
),
const SizedBox(
height: 36,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: PrivacyToggle(
externalCallsEnabled: isEasy,
onChanged: (externalCalls) {
isEasy = externalCalls;
setState(() {
infoToggle = isEasy;
});
},
),
),
const SizedBox(
height: 36,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: RoundedWhiteContainer(
child: Center(
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style:
STextStyles.label(context).copyWith(fontSize: 12.0),
children: infoToggle
? [
const TextSpan(
text:
"Exchange data preloaded for a seamless experience."),
const TextSpan(
text:
"\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."),
TextSpan(
text:
"\n\nRecommended for most crypto users.",
style: TextStyle(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontWeight: FontWeight.w600,
),
),
]
: [
const TextSpan(
text:
"Exchange data not preloaded (slower experience)."),
const TextSpan(
text:
"\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."),
TextSpan(
text:
"\n\nRecommended for the privacy conscious.",
style: TextStyle(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
const Spacer(
flex: 4,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Row(
children: [
Expanded(
child: ContinueButton(
isDesktop: isDesktop,
label: !widget.isSettings ? "Continue" : "Save changes",
onPressed: () {
ref.read(prefsChangeNotifierProvider).externalCalls =
isEasy;
if (!widget.isSettings) {
if (isDesktop) {
Navigator.of(context).pushNamed(
CreatePasswordView.routeName,
);
} else {
Navigator.of(context).pushNamed(
CreatePinView.routeName,
);
}
} else {
Navigator.pop(context);
}
},
),
),
],
),
),
],
),
),
),
);
}
}
class PrivacyToggle extends StatefulWidget {
const PrivacyToggle({
Key? key,
required this.externalCallsEnabled,
this.onChanged,
}) : super(key: key);
final bool externalCallsEnabled;
final void Function(bool)? onChanged;
@override
State<PrivacyToggle> createState() => _PrivacyToggleState();
}
class _PrivacyToggleState extends State<PrivacyToggle> {
late bool externalCallsEnabled;
@override
void initState() {
// initial toggle state
externalCallsEnabled = widget.externalCallsEnabled;
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: RawMaterialButton(
fillColor: Theme.of(context).extension<StackColors>()!.popupBG,
shape: RoundedRectangleBorder(
side: !externalCallsEnabled
? BorderSide.none
: BorderSide(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
width: 2,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius * 2,
),
),
onPressed: () {
setState(() {
// update toggle state
externalCallsEnabled = true;
});
// call callback with newly set value
widget.onChanged?.call(externalCallsEnabled);
},
child: Padding(
padding: const EdgeInsets.all(
12,
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SvgPicture.asset(
Assets.svg.personaEasy,
width: 140,
height: 140,
),
Center(
child: Text(
"Easy Crypto",
style: STextStyles.label(context).copyWith(
fontWeight: FontWeight.bold,
),
)),
Center(
child: Text(
"Recommended",
style: STextStyles.label(context),
),
),
],
),
if (externalCallsEnabled)
Positioned(
top: 4,
right: 4,
child: SvgPicture.asset(
Assets.svg.checkCircle,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
),
if (!externalCallsEnabled)
Positioned(
top: 4,
right: 4,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1000),
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
),
),
),
],
),
),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: RawMaterialButton(
elevation: 0,
fillColor: Theme.of(context).extension<StackColors>()!.popupBG,
shape: RoundedRectangleBorder(
side: externalCallsEnabled
? BorderSide.none
: BorderSide(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
width: 2,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius * 2,
),
),
onPressed: () {
setState(() {
// update toggle state
externalCallsEnabled = false;
});
// call callback with newly set value
widget.onChanged?.call(externalCallsEnabled);
},
child: Padding(
padding: const EdgeInsets.all(
12,
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SvgPicture.asset(
Assets.svg.personaIncognito,
width: 140,
height: 140,
),
Center(
child: Text(
"Incognito",
style: STextStyles.label(context).copyWith(
fontWeight: FontWeight.bold,
),
),
),
Center(
child: Text(
"Privacy conscious",
style: STextStyles.label(context),
),
),
],
),
if (!externalCallsEnabled)
Positioned(
top: 4,
right: 4,
child: SvgPicture.asset(
Assets.svg.checkCircle,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
),
if (externalCallsEnabled)
Positioned(
top: 4,
right: 4,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1000),
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
),
),
),
],
),
),
),
),
],
);
}
}
class ContinueButton extends ConsumerWidget {
const ContinueButton({
Key? key,
required this.isDesktop,
required this.onPressed,
required this.label,
}) : super(key: key);
final String label;
final bool isDesktop;
final VoidCallback onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
if (isDesktop) {
return SizedBox(
width: 328,
height: 70,
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: onPressed,
child: Text(
label,
style: STextStyles.button(context).copyWith(fontSize: 20),
),
),
);
} else {
return TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: onPressed,
child: Text(
label,
style: STextStyles.button(context),
),
);
}
}
}
// class CustomRadio extends StatefulWidget {
// CustomRadio(this.upperCall, {Key? key}) : super(key: key);
//
// Function upperCall;
//
// @override
// createState() {
// return CustomRadioState();
// }
// }
//
// class CustomRadioState extends State<CustomRadio> {
// List<RadioModel> sampleData = <RadioModel>[];
//
// @override
// void initState() {
// super.initState();
// sampleData.add(
// RadioModel(true, Assets.svg.personaEasy, 'Easy Crypto', 'Recommended'));
// sampleData.add(RadioModel(
// false, Assets.svg.personaIncognito, 'Incognito', 'Privacy conscious'));
// }
//
// @override
// Widget build(BuildContext context) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// InkWell(
// onTap: () {
// setState(() {
// // if (!sampleData[0].isSelected) {
// widget.upperCall.call(true);
// // }
// for (var element in sampleData) {
// element.isSelected = false;
// }
// sampleData[0].isSelected = true;
// });
// },
// child: RadioItem(sampleData[0]),
// ),
// InkWell(
// onTap: () {
// setState(() {
// // if (!sampleData[1].isSelected) {
// widget.upperCall.call(false);
// // }
// for (var element in sampleData) {
// element.isSelected = false;
// }
// sampleData[1].isSelected = true;
// });
// },
// child: RadioItem(sampleData[1]),
// )
// ],
// );
// }
// }
//
// class RadioItem extends StatelessWidget {
// final RadioModel _item;
// const RadioItem(this._item, {Key? key}) : super(key: key);
// @override
// Widget build(BuildContext context) {
// return Container(
// margin: const EdgeInsets.all(15.0),
// child: RoundedWhiteContainer(
// borderColor: _item.isSelected ? const Color(0xFF0056D2) : null,
// child: Center(
// child: Column(
// children: [
// SvgPicture.asset(
// _item.svg,
// // color: Theme.of(context).extension<StackColors>()!.textWhite,
// width: 140,
// height: 140,
// ),
// RichText(
// textAlign: TextAlign.center,
// text: TextSpan(
// style: STextStyles.label(context).copyWith(fontSize: 12.0),
// children: [
// TextSpan(
// text: _item.topText,
// style: TextStyle(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textDark,
// fontWeight: FontWeight.bold)),
// TextSpan(text: "\n${_item.bottomText}"),
// ],
// ),
// ),
// ],
// )),
// ),
// );
// }
// }
//
// class RadioModel {
// bool isSelected;
// final String svg;
// final String topText;
// final String bottomText;
//
// RadioModel(this.isSelected, this.svg, this.topText, this.bottomText);
// }

View file

@ -3,7 +3,9 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -13,9 +15,6 @@ import 'package:stackwallet/widgets/trade_card.dart';
import 'package:stackwallet/widgets/transaction_card.dart';
import 'package:tuple/tuple.dart';
import '../../../providers/global/trades_service_provider.dart';
import '../../exchange_view/trade_details_view.dart';
class TransactionsList extends ConsumerStatefulWidget {
const TransactionsList({
Key? key,
@ -135,9 +134,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
.read(tradesServiceProvider)
.trades
.where((e) =>
e.statusObject != null &&
(e.statusObject!.payinHash == tx.txid ||
e.statusObject!.payoutHash == tx.txid));
e.payInTxid == tx.txid || e.payOutTxid == tx.txid);
if (tx.txType == "Sent" && matchingTrades.isNotEmpty) {
final trade = matchingTrades.first;
return Container(
@ -164,7 +161,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
Navigator.of(context).pushNamed(
TradeDetailsView.routeName,
arguments: Tuple4(
trade.id,
trade.tradeId,
tx,
widget.walletId,
ref.read(managerProvider).walletName,

View file

@ -1,3 +1,4 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/providers.dart';
@ -8,6 +9,8 @@ import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
class WalletBalanceToggleSheet extends ConsumerWidget {
const WalletBalanceToggleSheet({
Key? key,
@ -23,6 +26,22 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).coin));
Future<Decimal>? totalBalanceFuture;
Future<Decimal>? availableBalanceFuture;
if (coin == Coin.firo || coin == Coin.firoTestNet) {
final firoWallet = ref
.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)))
.wallet as FiroWallet;
totalBalanceFuture = firoWallet.availablePublicBalance();
availableBalanceFuture = firoWallet.availablePrivateBalance();
} else {
final wallet = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
totalBalanceFuture = wallet.totalBalance;
availableBalanceFuture = wallet.availableBalance;
}
return Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
@ -125,15 +144,35 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
const SizedBox(
height: 2,
),
Text(
"Current spendable (unlocked) balance",
style:
STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
FutureBuilder(
future: availableBalanceFuture,
builder: (fbContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
return Text(
"${snapshot.data!}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
return Text(
"",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
}),
],
),
if (coin == Coin.firo || coin == Coin.firoTestNet)
@ -147,15 +186,35 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
const SizedBox(
height: 2,
),
Text(
"Current private spendable (unlocked) balance",
style:
STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
FutureBuilder(
future: availableBalanceFuture,
builder: (fbContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
return Text(
"${snapshot.data!}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
return Text(
"",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
}),
],
),
],
@ -219,15 +278,35 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
const SizedBox(
height: 2,
),
Text(
"Total wallet balance",
style:
STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
FutureBuilder(
future: totalBalanceFuture,
builder: (fbContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
return Text(
"${snapshot.data!}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
return Text(
"",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
}),
],
),
if (coin == Coin.firo || coin == Coin.firoTestNet)
@ -241,15 +320,35 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
const SizedBox(
height: 2,
),
Text(
"Current public spendable (unlocked) balance",
style:
STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
FutureBuilder(
future: totalBalanceFuture,
builder: (fbContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
return Text(
"${snapshot.data!}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
return Text(
"",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
}),
],
),
],

View file

@ -18,20 +18,19 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart';
import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart';
import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/providers/ui/unread_notifications_provider.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart';
import 'package:stackwallet/services/change_now/change_now_loading_service.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_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/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
@ -46,6 +45,12 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
/// [eventBus] should only be set during testing
class WalletView extends ConsumerStatefulWidget {
const WalletView({
@ -79,7 +84,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
late StreamSubscription<dynamic> _syncStatusSubscription;
late StreamSubscription<dynamic> _nodeStatusSubscription;
final _cnLoadingService = ChangeNowLoadingService();
final _cnLoadingService = ExchangeDataLoadingService();
@override
void initState() {
@ -230,56 +235,71 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
void _onExchangePressed(BuildContext context) async {
final _cnLoadingService = ExchangeDataLoadingService();
final externalCalls = Prefs.instance.externalCalls;
if (!externalCalls) {
print("loading?");
unawaited(_cnLoadingService.loadAll(ref));
}
final coin = ref.read(managerProvider).coin;
if (coin == Coin.epicCash) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "ChangeNOW not available for Epic Cash",
title: "Exchange not available for Epic Cash",
),
);
} else if (coin.name.endsWith("TestNet")) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "ChangeNOW not available for test net coins",
title: "Exchange not available for test net coins",
),
);
} else {
ref.read(currentExchangeNameStateProvider.state).state =
ChangeNowExchange.exchangeName;
final walletId = ref.read(managerProvider).walletId;
ref.read(prefsChangeNotifierProvider).exchangeRateType =
ExchangeRateType.estimated;
ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider);
ref.read(exchangeFormStateProvider).exchangeType =
ExchangeRateType.estimated;
final currencies = ref
.read(availableChangeNowCurrenciesStateProvider.state)
.state
.read(availableChangeNowCurrenciesProvider)
.currencies
.where((element) =>
element.ticker.toLowerCase() == coin.ticker.toLowerCase());
if (currencies.isNotEmpty) {
unawaited(ref
.read(estimatedRateExchangeFormProvider)
.updateFrom(currencies.first, false));
unawaited(ref.read(estimatedRateExchangeFormProvider).updateTo(
ref
.read(availableChangeNowCurrenciesStateProvider.state)
.state
.firstWhere(
(element) =>
element.ticker.toLowerCase() != coin.ticker.toLowerCase(),
),
false));
ref.read(exchangeFormStateProvider).setCurrencies(
currencies.first,
ref
.read(availableChangeNowCurrenciesProvider)
.currencies
.firstWhere(
(element) =>
element.ticker.toLowerCase() !=
coin.ticker.toLowerCase(),
),
);
}
unawaited(Navigator.of(context).pushNamed(
WalletInitiatedExchangeView.routeName,
arguments: Tuple3(
walletId,
coin,
_loadCNData,
),
));
if (mounted) {
unawaited(
Navigator.of(context).pushNamed(
WalletInitiatedExchangeView.routeName,
arguments: Tuple3(
walletId,
coin,
_loadCNData,
),
),
);
}
}
}
@ -351,7 +371,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
void _loadCNData() {
// unawaited future
_cnLoadingService.loadAll(ref, coin: ref.read(managerProvider).coin);
final externalCalls = DB.instance
.get<dynamic>(boxName: DB.boxNamePrefs, key: "externalCalls") as bool?;
if (externalCalls ?? false) {
_cnLoadingService.loadAll(ref, coin: ref.read(managerProvider).coin);
} else {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
}
}
@override

View file

@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
@ -67,6 +68,8 @@ class WalletListItem extends ConsumerWidget {
builder: (_, ref, __) {
final tuple = ref.watch(priceAnd24hChangeNotifierProvider
.select((value) => value.getPrice(coin)));
final calls =
ref.watch(prefsChangeNotifierProvider).externalCalls;
final priceString = Format.localizedStringAsFixed(
value: tuple.item1,
@ -100,10 +103,11 @@ class WalletListItem extends ConsumerWidget {
style: STextStyles.titleBold12(context),
),
const Spacer(),
Text(
"$priceString $currency/${coin.ticker}",
style: STextStyles.itemSubtitle(context),
),
if (calls)
Text(
"$priceString $currency/${coin.ticker}",
style: STextStyles.itemSubtitle(context),
),
],
),
const SizedBox(
@ -116,12 +120,13 @@ class WalletListItem extends ConsumerWidget {
style: STextStyles.itemSubtitle(context),
),
const Spacer(),
Text(
"${percentChange.toStringAsFixed(2)}%",
style: STextStyles.itemSubtitle(context).copyWith(
color: percentChangedColor,
if (calls)
Text(
"${percentChange.toStringAsFixed(2)}%",
style: STextStyles.itemSubtitle(context).copyWith(
color: percentChangedColor,
),
),
),
],
),
],

View file

@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/change_now/cn_available_currencies.dart';
final availableChangeNowCurrenciesProvider = Provider<CNAvailableCurrencies>(
(ref) => CNAvailableCurrencies(),
);

View file

@ -1,5 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart';
final availableChangeNowCurrenciesStateProvider =
StateProvider<List<Currency>>((ref) => <Currency>[]);

View file

@ -1,6 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart';
final availableFloatingRatePairsStateProvider =
StateProvider<List<AvailableFloatingRatePair>>(
(ref) => <AvailableFloatingRatePair>[]);

View file

@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/simpleswap/sp_available_currencies.dart';
final availableSimpleswapCurrenciesProvider = Provider<SPAvailableCurrencies>(
(ref) => SPAvailableCurrencies(),
);

View file

@ -1,4 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/services/change_now/change_now.dart';
final changeNowProvider = Provider<ChangeNow>((ref) => ChangeNow.instance);

View file

@ -1,13 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum ChangeNowLoadStatus {
waiting,
loading,
success,
failed,
}
final changeNowEstimatedInitialLoadStatusStateProvider =
StateProvider<ChangeNowLoadStatus>((ref) => ChangeNowLoadStatus.loading);
StateProvider<ChangeNowLoadStatus>((ref) => ChangeNowLoadStatus.waiting);
final changeNowFixedInitialLoadStatusStateProvider =
StateProvider<ChangeNowLoadStatus>((ref) => ChangeNowLoadStatus.loading);
StateProvider<ChangeNowLoadStatus>((ref) => ChangeNowLoadStatus.waiting);

View file

@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
final currentExchangeNameStateProvider = StateProvider<String>(
(ref) => ChangeNowExchange.exchangeName,
);

View file

@ -1,5 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/estimated_rate_exchange_form_state.dart';
final estimatedRateExchangeFormProvider =
ChangeNotifierProvider((ref) => EstimatedRateExchangeFormState());

View file

@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/exchange_form_state.dart';
final exchangeFormStateProvider = ChangeNotifierProvider<ExchangeFormState>(
(ref) => ExchangeFormState(),
);

View file

@ -0,0 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
final exchangeProvider = Provider<Exchange>(
(ref) => Exchange.fromName(
ref.watch(currentExchangeNameStateProvider.state).state,
),
);

View file

@ -1,6 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/fixed_rate_exchange_form_state.dart';
final fixedRateExchangeFormProvider =
ChangeNotifierProvider<FixedRateExchangeFormState>(
(ref) => FixedRateExchangeFormState());

View file

@ -1,5 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
final fixedRateMarketPairsStateProvider =
StateProvider<List<FixedRateMarket>>((ref) => []);

View file

@ -1,3 +1,13 @@
export './exchange/available_changenow_currencies_provider.dart';
export './exchange/available_simpleswap_currencies_provider.dart';
export './exchange/changenow_initial_load_status.dart';
export './exchange/current_exchange_name_state_provider.dart';
export './exchange/exchange_flow_is_active_state_provider.dart';
export './exchange/exchange_form_state_provider.dart';
export './exchange/exchange_provider.dart';
export './exchange/exchange_send_from_wallet_id_provider.dart';
export './exchange/trade_note_service_provider.dart';
export './exchange/trade_sent_from_stack_lookup_provider.dart';
export './global/favorites_provider.dart';
export './global/locale_provider.dart';
export './global/node_service_provider.dart';

View file

@ -3,8 +3,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
@ -33,6 +33,7 @@ import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart';
import 'package:stackwallet/pages/notification_views/notifications_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
@ -43,6 +44,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_v
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/currency_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/delete_account_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/global_settings_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/hidden_settings.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/language_view.dart';
@ -73,6 +75,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
import 'package:stackwallet/pages/stack_privacy_calls.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
@ -97,6 +100,18 @@ class RouteGenerator {
final args = settings.arguments;
switch (settings.name) {
case IntroView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const IntroView(),
settings: RouteSettings(name: settings.name));
case DeleteAccountView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DeleteAccountView(),
settings: RouteSettings(name: settings.name));
case HomeView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -120,6 +135,19 @@ class RouteGenerator {
builder: (_) => const CreatePinView(),
settings: RouteSettings(name: settings.name));
case StackPrivacyCalls.routeName:
if (args is bool) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => StackPrivacyCalls(isSettings: args),
settings: RouteSettings(name: settings.name),
);
}
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => StackPrivacyCalls(isSettings: false),
settings: RouteSettings(name: settings.name));
case WalletsView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -898,7 +926,7 @@ class RouteGenerator {
return _routeError("${settings.name} invalid args: ${args.toString()}");
case SendFromView.routeName:
if (args is Tuple4<Coin, Decimal, String, ExchangeTransaction>) {
if (args is Tuple4<Coin, Decimal, String, Trade>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => SendFromView(

View file

@ -753,10 +753,11 @@ Future<Map<String, dynamic>?> getInitialAnonymitySetCache(
);
final response = jsonDecode(anonSetResult.body.toString());
Logging.instance.log(response, level: LogLevel.Info);
if (response['status'] == 'success') {
final anonResponse = jsonDecode(response['result'] as String);
final setData = Map<String, dynamic>.from(anonResponse["result"] as Map);
final setData = Map<String, dynamic>.from(anonResponse as Map);
return setData;
} else {
return null;
@ -2040,7 +2041,8 @@ class FiroWallet extends CoinServiceAPI {
case "Sent":
unawaited(
NotificationApi.showNotification(
title: "Outgoing transaction",
title:
tx.subType == "mint" ? "Anonymizing" : "Outgoing transaction",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
@ -2076,7 +2078,9 @@ class FiroWallet extends CoinServiceAPI {
} else if (tx.txType == "Sent" && tx.subType == "join") {
unawaited(
NotificationApi.showNotification(
title: "Outgoing transaction confirmed",
title: tx.subType == "mint"
? "Anonymized"
: "Outgoing transaction confirmed",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
@ -4493,6 +4497,7 @@ class FiroWallet extends CoinServiceAPI {
}
}
// TODO: investigate the bug here where chosen is null, conditions, given one mint
spendVal += chosen!.amount;
coinsToSpend.insert(coinsToSpend.length, chosen);
}
@ -4514,36 +4519,61 @@ class FiroWallet extends CoinServiceAPI {
Future<int> estimateJoinSplitFee(
int spendAmount,
) async {
int fee;
int size;
for (fee = 0;;) {
int currentRequired = spendAmount;
var map = await getCoinsToJoinSplit(currentRequired);
if (map is bool && !map) {
return 0;
}
List<DartLelantusEntry> coinsToBeSpent =
map['coinsToSpend'] as List<DartLelantusEntry>;
// 1054 is constant part, mainly Schnorr and Range proofs, 2560 is for each sigma/aux data
// 179 other parts of tx, assuming 1 utxo and 1 jmint
size = 1054 + 2560 * coinsToBeSpent.length + 180;
// uint64_t feeNeeded = GetMinimumFee(size, DEFAULT_TX_CONFIRM_TARGET);
int feeNeeded =
size; //TODO(Levon) temporary, use real estimation methods here
if (fee >= feeNeeded) {
break;
}
fee = feeNeeded;
var lelantusEntry = await _getLelantusEntry();
final balance = await availableBalance;
int spendAmount =
(balance * Decimal.fromInt(Constants.satsPerCoin)).toBigInt().toInt();
if (spendAmount == 0 || lelantusEntry.isEmpty) {
return LelantusFeeData(0, 0, []).fee;
}
ReceivePort receivePort = await getIsolate({
"function": "estimateJoinSplit",
"spendAmount": spendAmount,
"subtractFeeFromAmount": true,
"lelantusEntries": lelantusEntry,
"coin": coin,
});
return fee;
final message = await receivePort.first;
if (message is String) {
Logging.instance.log("this is a string", level: LogLevel.Error);
stop(receivePort);
throw Exception("_fetchMaxFee isolate failed");
}
stop(receivePort);
Logging.instance.log('Closing estimateJoinSplit!', level: LogLevel.Info);
return (message as LelantusFeeData).fee;
}
// int fee;
// int size;
//
// for (fee = 0;;) {
// int currentRequired = spendAmount;
//
// TODO: investigate the bug here
// var map = await getCoinsToJoinSplit(currentRequired);
// if (map is bool && !map) {
// return 0;
// }
//
// List<DartLelantusEntry> coinsToBeSpent =
// map['coinsToSpend'] as List<DartLelantusEntry>;
//
// // 1054 is constant part, mainly Schnorr and Range proofs, 2560 is for each sigma/aux data
// // 179 other parts of tx, assuming 1 utxo and 1 jmint
// size = 1054 + 2560 * coinsToBeSpent.length + 180;
// // uint64_t feeNeeded = GetMinimumFee(size, DEFAULT_TX_CONFIRM_TARGET);
// int feeNeeded =
// size; //TODO(Levon) temporary, use real estimation methods here
//
// if (fee >= feeNeeded) {
// break;
// }
//
// fee = feeNeeded;
// }
//
// return fee;
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {

View file

@ -1447,6 +1447,16 @@ class MoneroWallet extends CoinServiceAPI {
Future<PendingTransaction>? awaitPendingTransaction;
try {
// check for send all
bool isSendAll = false;
final balance = await availableBalance;
final satInDecimal = ((Decimal.fromInt(satoshiAmount) /
Decimal.fromInt(Constants.satsPerCoinMonero))
.toDecimal() *
Decimal.fromInt(10000));
if (satInDecimal == balance) {
isSendAll = true;
}
Logging.instance
.log("$toAddress $amount $args", level: LogLevel.Info);
String amountToSend = moneroAmountToString(amount: amount * 10000);
@ -1454,13 +1464,16 @@ class MoneroWallet extends CoinServiceAPI {
monero_output.Output output = monero_output.Output(walletBase!);
output.address = toAddress;
output.sendAll = isSendAll;
output.setCryptoAmount(amountToSend);
List<monero_output.Output> outputs = [output];
Object tmp = monero.createMoneroTransactionCreationCredentials(
outputs: outputs, priority: feePriority);
awaitPendingTransaction = walletBase!.createTransaction(tmp);
await prepareSendMutex.protect(() async {
awaitPendingTransaction = walletBase!.createTransaction(tmp);
});
} catch (e, s) {
Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s",
level: LogLevel.Warning);
@ -1499,30 +1512,57 @@ class MoneroWallet extends CoinServiceAPI {
}
}
Mutex prepareSendMutex = Mutex();
Mutex estimateFeeMutex = Mutex();
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
MoneroTransactionPriority? priority;
FeeRateType feeRateType = FeeRateType.slow;
switch (feeRate) {
case 1:
priority = MoneroTransactionPriority.regular;
feeRateType = FeeRateType.slow;
break;
case 2:
priority = MoneroTransactionPriority.medium;
feeRateType = FeeRateType.average;
break;
case 3:
priority = MoneroTransactionPriority.fast;
feeRateType = FeeRateType.average;
break;
case 4:
priority = MoneroTransactionPriority.fastest;
feeRateType = FeeRateType.fast;
break;
case 0:
default:
priority = MoneroTransactionPriority.slow;
feeRateType = FeeRateType.slow;
break;
}
final fee =
(walletBase?.calculateEstimatedFee(priority, satoshiAmount) ?? 0) ~/
10000;
var aprox;
await estimateFeeMutex.protect(() async {
{
try {
aprox = (await prepareSend(
// This address is only used for getting an approximate fee, never for sending
address:
"8347huhmj6Ggzr1BpZPJAD5oa96ob5Fe8GtQdGZDYVVYVsCgtUNH3pEEzExDuaAVZdC16D4FkAb24J6wUfsKkcZtC8EPXB7",
satoshiAmount: satoshiAmount,
args: {"feeRate": feeRateType}))['fee'];
await Future.delayed(const Duration(milliseconds: 1000));
} catch (e, s) {
Logging.instance.log("$feeRateType $e $s", level: LogLevel.Error);
aprox = -9999999999999999;
}
}
});
print("this is the aprox fee $aprox for $satoshiAmount");
final fee = (aprox as int);
return fee;
}

View file

@ -2444,8 +2444,8 @@ class NamecoinWallet extends CoinServiceAPI {
for (final out in tx["vout"] as List) {
if (prevOut == out["n"]) {
String? address = out["scriptPubKey"]["address"] as String?;
if (address == null && out["scriptPubKey"]["addresses"] != null) {
address = out["scriptPubKey"]["addresses"][0] as String?;
if (address == null && out["scriptPubKey"]["address"] != null) {
address = out["scriptPubKey"]["address"] as String?;
}
if (address != null) {
@ -2459,8 +2459,8 @@ class NamecoinWallet extends CoinServiceAPI {
for (final output in txObject["vout"] as List) {
String? address = output["scriptPubKey"]["address"] as String?;
if (address == null && output["scriptPubKey"]["addresses"] != null) {
address = output["scriptPubKey"]["addresses"][0] as String?;
if (address == null && output["scriptPubKey"]["address"] != null) {
address = output["scriptPubKey"]["address"] as String?;
}
if (address != null) {
recipientsArray.add(address);
@ -2528,8 +2528,8 @@ class NamecoinWallet extends CoinServiceAPI {
// add up received tx value
for (final output in txObject["vout"] as List) {
String? address = output["scriptPubKey"]["address"] as String?;
if (address == null && output["scriptPubKey"]["addresses"] != null) {
address = output["scriptPubKey"]["addresses"][0] as String?;
if (address == null && output["scriptPubKey"]["address"] != null) {
address = output["scriptPubKey"]["address"] as String?;
}
if (address != null) {
final value = (Decimal.parse(output["value"].toString()) *
@ -3049,7 +3049,7 @@ class NamecoinWallet extends CoinServiceAPI {
for (final output in tx["vout"] as List) {
final n = output["n"];
if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String;
final address = output["scriptPubKey"]["address"] as String;
if (!addressTxid.containsKey(address)) {
addressTxid[address] = <String>[];
}

View file

@ -1453,6 +1453,16 @@ class WowneroWallet extends CoinServiceAPI {
Future<PendingTransaction>? awaitPendingTransaction;
try {
// check for send all
bool isSendAll = false;
final balance = await availableBalance;
final satInDecimal = ((Decimal.fromInt(satoshiAmount) /
Decimal.fromInt(Constants.satsPerCoinWownero))
.toDecimal() *
Decimal.fromInt(1000));
if (satInDecimal == balance) {
isSendAll = true;
}
Logging.instance
.log("$toAddress $amount $args", level: LogLevel.Info);
String amountToSend = wowneroAmountToString(amount: amount * 1000);
@ -1460,13 +1470,16 @@ class WowneroWallet extends CoinServiceAPI {
wownero_output.Output output = wownero_output.Output(walletBase!);
output.address = toAddress;
output.sendAll = isSendAll;
output.setCryptoAmount(amountToSend);
List<wownero_output.Output> outputs = [output];
Object tmp = wownero.createWowneroTransactionCreationCredentials(
outputs: outputs, priority: feePriority);
awaitPendingTransaction = walletBase!.createTransaction(tmp);
await prepareSendMutex.protect(() async {
awaitPendingTransaction = walletBase!.createTransaction(tmp);
});
} catch (e, s) {
Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s",
level: LogLevel.Warning);
@ -1505,30 +1518,55 @@ class WowneroWallet extends CoinServiceAPI {
}
}
Mutex prepareSendMutex = Mutex();
Mutex estimateFeeMutex = Mutex();
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
MoneroTransactionPriority? priority;
FeeRateType feeRateType = FeeRateType.slow;
switch (feeRate) {
case 1:
priority = MoneroTransactionPriority.regular;
feeRateType = FeeRateType.slow;
break;
case 2:
priority = MoneroTransactionPriority.medium;
feeRateType = FeeRateType.average;
break;
case 3:
priority = MoneroTransactionPriority.fast;
feeRateType = FeeRateType.average;
break;
case 4:
priority = MoneroTransactionPriority.fastest;
feeRateType = FeeRateType.fast;
break;
case 0:
default:
priority = MoneroTransactionPriority.slow;
feeRateType = FeeRateType.slow;
break;
}
final fee =
(walletBase?.calculateEstimatedFee(priority, satoshiAmount) ?? 0) ~/
10000;
var aprox;
await estimateFeeMutex.protect(() async {
{
try {
aprox = (await prepareSend(
// This address is only used for getting an approximate fee, never for sending
address:
"WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy",
satoshiAmount: satoshiAmount,
args: {"feeRate": feeRateType}))['fee'];
await Future.delayed(const Duration(milliseconds: 500));
} catch (e, s) {
aprox = -9999999999999999;
}
}
});
print("this is the aprox fee $aprox for $satoshiAmount");
final fee = (aprox as int);
return fee;
}

View file

@ -4,25 +4,27 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/external_api_keys.dart';
import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart';
import 'package:stackwallet/models/exchange/change_now/change_now_response.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/models/exchange/change_now/estimated_exchange_amount.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/utilities/logger.dart';
class ChangeNow {
class ChangeNowAPI {
static const String scheme = "https";
static const String authority = "api.changenow.io";
static const String apiVersion = "/v1";
static const String apiVersionV2 = "/v2";
ChangeNow._();
static final ChangeNow _instance = ChangeNow._();
static ChangeNow get instance => _instance;
ChangeNowAPI._();
static final ChangeNowAPI _instance = ChangeNowAPI._();
static ChangeNowAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing
http.Client? client;
@ -100,7 +102,7 @@ class ChangeNow {
///
/// Set [active] to true to return only active currencies.
/// Set [fixedRate] to true to return only currencies available on a fixed-rate flow.
Future<ChangeNowResponse<List<Currency>>> getAvailableCurrencies({
Future<ExchangeResponse<List<Currency>>> getAvailableCurrencies({
bool? fixedRate,
bool? active,
}) async {
@ -129,26 +131,26 @@ class ChangeNow {
} catch (e, s) {
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Error: $jsonArray",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
ChangeNowResponse<List<Currency>> _parseAvailableCurrenciesJson(
ExchangeResponse<List<Currency>> _parseAvailableCurrenciesJson(
List<dynamic> jsonArray) {
try {
List<Currency> currencies = [];
@ -158,13 +160,13 @@ class ChangeNow {
currencies
.add(Currency.fromJson(Map<String, dynamic>.from(json as Map)));
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException("Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError));
return ExchangeResponse(
exception: ExchangeException("Failed to serialize $json",
ExchangeExceptionType.serializeResponseError));
}
}
return ChangeNowResponse(value: currencies);
return ExchangeResponse(value: currencies);
} catch (_) {
rethrow;
}
@ -175,7 +177,7 @@ class ChangeNow {
///
/// Required [ticker] to fetch paired currencies for.
/// Set [fixedRate] to true to return only currencies available on a fixed-rate flow.
Future<ChangeNowResponse<List<Currency>>> getPairedCurrencies({
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies({
required String ticker,
bool? fixedRate,
}) async {
@ -199,10 +201,10 @@ class ChangeNow {
currencies
.add(Currency.fromJson(Map<String, dynamic>.from(json as Map)));
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
@ -210,18 +212,18 @@ class ChangeNow {
} catch (e, s) {
Logging.instance.log("getPairedCurrencies exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException("Error: $jsonArray",
ChangeNowExceptionType.serializeResponseError));
return ExchangeResponse(
exception: ExchangeException("Error: $jsonArray",
ExchangeExceptionType.serializeResponseError));
}
return ChangeNowResponse(value: currencies);
return ExchangeResponse(value: currencies);
} catch (e, s) {
Logging.instance
.log("getPairedCurrencies exception: $e\n$s", level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
@ -230,7 +232,7 @@ class ChangeNow {
/// The API endpoint returns minimal payment amount required to make
/// an exchange of [fromTicker] to [toTicker].
/// If you try to exchange less, the transaction will most likely fail.
Future<ChangeNowResponse<Decimal>> getMinimalExchangeAmount({
Future<ExchangeResponse<Decimal>> getMinimalExchangeAmount({
required String fromTicker,
required String toTicker,
String? apiKey,
@ -245,22 +247,62 @@ class ChangeNow {
try {
final value = Decimal.parse(json["minAmount"].toString());
return ChangeNowResponse(value: value);
return ExchangeResponse(value: value);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getMinimalExchangeAmount exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
/// The API endpoint returns minimal payment amount and maximum payment amount
/// required to make an exchange. If you try to exchange less than minimum or
/// more than maximum, the transaction will most likely fail. Any pair of
/// assets has minimum amount and some of pairs have maximum amount.
Future<ExchangeResponse<Range>> getRange({
required String fromTicker,
required String toTicker,
required bool isFixedRate,
String? apiKey,
}) async {
Map<String, dynamic>? params = {"api_key": apiKey ?? kChangeNowApiKey};
final uri = _buildUri(
"/exchange-range${isFixedRate ? "/fixed-rate" : ""}/${fromTicker}_$toTicker",
params);
try {
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
return ExchangeResponse(
value: Range(
max: Decimal.tryParse(json["maxAmount"]?.toString() ?? ""),
min: Decimal.tryParse(json["minAmount"]?.toString() ?? ""),
),
);
} catch (e, s) {
Logging.instance.log(
"getRange exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
@ -268,8 +310,7 @@ class ChangeNow {
/// Get estimated amount of [toTicker] cryptocurrency to receive
/// for [fromAmount] of [fromTicker]
Future<ChangeNowResponse<EstimatedExchangeAmount>>
getEstimatedExchangeAmount({
Future<ExchangeResponse<Estimate>> getEstimatedExchangeAmount({
required String fromTicker,
required String toTicker,
required Decimal fromAmount,
@ -289,22 +330,94 @@ class ChangeNow {
try {
final value = EstimatedExchangeAmount.fromJson(
Map<String, dynamic>.from(json as Map));
return ChangeNowResponse(value: value);
return ExchangeResponse(
value: Estimate(
estimatedAmount: value.estimatedAmount,
fixedRate: false,
reversed: false,
rateId: value.rateId,
warningMessage: value.warningMessage,
),
);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getEstimatedExchangeAmount exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
/// Get estimated amount of [toTicker] cryptocurrency to receive
/// for [fromAmount] of [fromTicker]
Future<ExchangeResponse<Estimate>> getEstimatedExchangeAmountFixedRate({
required String fromTicker,
required String toTicker,
required Decimal fromAmount,
required bool reversed,
bool useRateId = true,
String? apiKey,
}) async {
Map<String, dynamic> params = {
"api_key": apiKey ?? kChangeNowApiKey,
"useRateId": useRateId.toString(),
};
late final Uri uri;
if (reversed) {
uri = _buildUri(
"/exchange-deposit/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker",
params,
);
} else {
uri = _buildUri(
"/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker",
params,
);
}
try {
// simple json object is expected here
final json = await _makeGetRequest(uri);
try {
final value = EstimatedExchangeAmount.fromJson(
Map<String, dynamic>.from(json as Map));
return ExchangeResponse(
value: Estimate(
estimatedAmount: value.estimatedAmount,
fixedRate: true,
reversed: reversed,
rateId: value.rateId,
warningMessage: value.warningMessage,
),
);
} catch (_) {
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getEstimatedExchangeAmount exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
@ -313,7 +426,7 @@ class ChangeNow {
// old v1 version
/// This API endpoint returns fixed-rate estimated exchange amount of
/// [toTicker] cryptocurrency to receive for [fromAmount] of [fromTicker]
// Future<ChangeNowResponse<EstimatedExchangeAmount>>
// Future<ExchangeResponse<EstimatedExchangeAmount>>
// getEstimatedFixedRateExchangeAmount({
// required String fromTicker,
// required String toTicker,
@ -342,12 +455,12 @@ class ChangeNow {
// try {
// final value = EstimatedExchangeAmount.fromJson(
// Map<String, dynamic>.from(json as Map));
// return ChangeNowResponse(value: value);
// return ExchangeResponse(value: value);
// } catch (_) {
// return ChangeNowResponse(
// exception: ChangeNowException(
// return ExchangeResponse(
// exception: ExchangeException(
// "Failed to serialize $json",
// ChangeNowExceptionType.serializeResponseError,
// ExchangeExceptionType.serializeResponseError,
// ),
// );
// }
@ -355,10 +468,10 @@ class ChangeNow {
// Logging.instance.log(
// "getEstimatedFixedRateExchangeAmount exception: $e\n$s",
// level: LogLevel.Error);
// return ChangeNowResponse(
// exception: ChangeNowException(
// return ExchangeResponse(
// exception: ExchangeException(
// e.toString(),
// ChangeNowExceptionType.generic,
// ExchangeExceptionType.generic,
// ),
// );
// }
@ -366,7 +479,7 @@ class ChangeNow {
/// Get estimated amount of [toTicker] cryptocurrency to receive
/// for [fromAmount] of [fromTicker]
Future<ChangeNowResponse<CNExchangeEstimate>> getEstimatedExchangeAmountV2({
Future<ExchangeResponse<CNExchangeEstimate>> getEstimatedExchangeAmountV2({
required String fromTicker,
required String toTicker,
required CNEstimateType fromOrTo,
@ -413,22 +526,22 @@ class ChangeNow {
try {
final value =
CNExchangeEstimate.fromJson(Map<String, dynamic>.from(json as Map));
return ChangeNowResponse(value: value);
return ExchangeResponse(value: value);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getEstimatedExchangeAmountV2 exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
@ -438,8 +551,7 @@ class ChangeNow {
/// fixed-rate flow. Some currencies get enabled or disabled from time to
/// time and the market info gets updates, so make sure to refresh the list
/// occasionally. One time per minute is sufficient.
Future<ChangeNowResponse<List<FixedRateMarket>>>
getAvailableFixedRateMarkets({
Future<ExchangeResponse<List<FixedRateMarket>>> getAvailableFixedRateMarkets({
String? apiKey,
}) async {
final uri = _buildUri(
@ -456,40 +568,40 @@ class ChangeNow {
} catch (e, s) {
Logging.instance.log("getAvailableFixedRateMarkets exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Error: $jsonArray",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getAvailableFixedRateMarkets exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
ChangeNowResponse<List<FixedRateMarket>> _parseFixedRateMarketsJson(
ExchangeResponse<List<FixedRateMarket>> _parseFixedRateMarketsJson(
List<dynamic> jsonArray) {
try {
List<FixedRateMarket> markets = [];
for (final json in jsonArray) {
try {
markets.add(
FixedRateMarket.fromJson(Map<String, dynamic>.from(json as Map)));
FixedRateMarket.fromMap(Map<String, dynamic>.from(json as Map)));
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException("Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError));
return ExchangeResponse(
exception: ExchangeException("Failed to serialize $json",
ExchangeExceptionType.serializeResponseError));
}
}
return ChangeNowResponse(value: markets);
return ExchangeResponse(value: markets);
} catch (_) {
rethrow;
}
@ -497,7 +609,7 @@ class ChangeNow {
/// The API endpoint creates a transaction, generates an address for
/// sending funds and returns transaction attributes.
Future<ChangeNowResponse<ExchangeTransaction>>
Future<ExchangeResponse<ExchangeTransaction>>
createStandardExchangeTransaction({
required String fromTicker,
required String toTicker,
@ -535,12 +647,12 @@ class ChangeNow {
try {
final value = ExchangeTransaction.fromJson(
Map<String, dynamic>.from(json as Map));
return ChangeNowResponse(value: value);
return ExchangeResponse(value: value);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
@ -548,10 +660,10 @@ class ChangeNow {
Logging.instance.log(
"createStandardExchangeTransaction exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
@ -559,13 +671,14 @@ class ChangeNow {
/// The API endpoint creates a transaction, generates an address for
/// sending funds and returns transaction attributes.
Future<ChangeNowResponse<ExchangeTransaction>>
Future<ExchangeResponse<ExchangeTransaction>>
createFixedRateExchangeTransaction({
required String fromTicker,
required String toTicker,
required String receivingAddress,
required Decimal amount,
required String rateId,
required bool reversed,
String extraId = "",
String userId = "",
String contactEmail = "",
@ -577,7 +690,6 @@ class ChangeNow {
"from": fromTicker,
"to": toTicker,
"address": receivingAddress,
"amount": amount.toString(),
"flow": "fixed-rate",
"extraId": extraId,
"userId": userId,
@ -587,8 +699,16 @@ class ChangeNow {
"rateId": rateId,
};
if (reversed) {
map["result"] = amount.toString();
} else {
map["amount"] = amount.toString();
}
final uri = _buildUri(
"/transactions/fixed-rate/${apiKey ?? kChangeNowApiKey}", null);
"/transactions/fixed-rate${reversed ? "/from-result" : ""}/${apiKey ?? kChangeNowApiKey}",
null,
);
try {
// simple json object is expected here
@ -600,12 +720,12 @@ class ChangeNow {
try {
final value = ExchangeTransaction.fromJson(
Map<String, dynamic>.from(json as Map));
return ChangeNowResponse(value: value);
return ExchangeResponse(value: value);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
@ -613,16 +733,16 @@ class ChangeNow {
Logging.instance.log(
"createFixedRateExchangeTransaction exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
Future<ChangeNowResponse<ExchangeTransactionStatus>> getTransactionStatus({
Future<ExchangeResponse<ExchangeTransactionStatus>> getTransactionStatus({
required String id,
String? apiKey,
}) async {
@ -636,29 +756,28 @@ class ChangeNow {
try {
final value = ExchangeTransactionStatus.fromJson(
Map<String, dynamic>.from(json as Map));
return ChangeNowResponse(value: value);
return ExchangeResponse(value: value);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance
.log("getTransactionStatus exception: $e\n$s", level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
Future<ChangeNowResponse<List<AvailableFloatingRatePair>>>
getAvailableFloatingRatePairs({
Future<ExchangeResponse<List<Pair>>> getAvailableFloatingRatePairs({
bool includePartners = false,
}) async {
final uri = _buildUri("/market-info/available-pairs",
@ -675,41 +794,49 @@ class ChangeNow {
} catch (e, s) {
Logging.instance.log("getAvailableFloatingRatePairs exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
"Error: $jsonArray",
ChangeNowExceptionType.serializeResponseError,
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getAvailableFloatingRatePairs exception: $e\n$s",
level: LogLevel.Error);
return ChangeNowResponse(
exception: ChangeNowException(
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ChangeNowExceptionType.generic,
ExchangeExceptionType.generic,
),
);
}
}
ChangeNowResponse<List<AvailableFloatingRatePair>>
_parseAvailableFloatingRatePairsJson(List<dynamic> jsonArray) {
ExchangeResponse<List<Pair>> _parseAvailableFloatingRatePairsJson(
List<dynamic> jsonArray) {
try {
List<AvailableFloatingRatePair> pairs = [];
List<Pair> pairs = [];
for (final json in jsonArray) {
try {
final List<String> stringPair = (json as String).split("_");
pairs.add(AvailableFloatingRatePair(
fromTicker: stringPair[0], toTicker: stringPair[1]));
pairs.add(
Pair(
from: stringPair[0],
to: stringPair[1],
fromNetwork: "",
toNetwork: "",
fixedRate: false,
floatingRate: true,
),
);
} catch (_) {
return ChangeNowResponse(
exception: ChangeNowException("Failed to serialize $json",
ChangeNowExceptionType.serializeResponseError));
return ExchangeResponse(
exception: ExchangeException("Failed to serialize $json",
ExchangeExceptionType.serializeResponseError));
}
}
return ChangeNowResponse(value: pairs);
return ExchangeResponse(value: pairs);
} catch (_) {
rethrow;
}

View file

@ -0,0 +1,226 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_api.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:uuid/uuid.dart';
class ChangeNowExchange extends Exchange {
static const exchangeName = "ChangeNOW";
@override
String get name => exchangeName;
@override
Future<ExchangeResponse<Trade>> createTrade({
required String from,
required String to,
required bool fixedRate,
required Decimal amount,
required String addressTo,
String? extraId,
required String addressRefund,
required String refundExtraId,
String? rateId,
required bool reversed,
}) async {
late final ExchangeResponse<ExchangeTransaction> response;
if (fixedRate) {
response = await ChangeNowAPI.instance.createFixedRateExchangeTransaction(
fromTicker: from,
toTicker: to,
receivingAddress: addressTo,
amount: amount,
rateId: rateId!,
extraId: extraId ?? "",
refundAddress: addressRefund,
refundExtraId: refundExtraId,
reversed: reversed,
);
} else {
response = await ChangeNowAPI.instance.createStandardExchangeTransaction(
fromTicker: from,
toTicker: to,
receivingAddress: addressTo,
amount: amount,
extraId: extraId ?? "",
refundAddress: addressRefund,
refundExtraId: refundExtraId,
);
}
if (response.exception != null) {
return ExchangeResponse(exception: response.exception);
}
final statusResponse = await ChangeNowAPI.instance
.getTransactionStatus(id: response.value!.id);
if (statusResponse.exception != null) {
return ExchangeResponse(exception: statusResponse.exception);
}
return ExchangeResponse(
value: Trade.fromExchangeTransaction(
response.value!.copyWith(
statusObject: statusResponse.value!,
),
reversed,
),
);
}
@override
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
bool fixedRate,
) async {
return await ChangeNowAPI.instance.getAvailableCurrencies(
fixedRate: fixedRate ? true : null,
active: true,
);
}
@override
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
// TODO: implement getAllPairs
throw UnimplementedError();
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
String from,
String to,
Decimal amount,
bool fixedRate,
bool reversed,
) async {
late final ExchangeResponse<Estimate> response;
if (fixedRate) {
response =
await ChangeNowAPI.instance.getEstimatedExchangeAmountFixedRate(
fromTicker: from,
toTicker: to,
fromAmount: amount,
reversed: reversed,
);
} else {
response = await ChangeNowAPI.instance.getEstimatedExchangeAmount(
fromTicker: from,
toTicker: to,
fromAmount: amount,
);
}
return response;
}
@override
Future<ExchangeResponse<Range>> getRange(
String from,
String to,
bool fixedRate,
) async {
return await ChangeNowAPI.instance.getRange(
fromTicker: from,
toTicker: to,
isFixedRate: fixedRate,
);
}
@override
Future<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
) async {
// TODO: implement getPairsFor
throw UnimplementedError();
}
@override
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
final response =
await ChangeNowAPI.instance.getTransactionStatus(id: tradeId);
if (response.exception != null) {
return ExchangeResponse(exception: response.exception);
}
final t = response.value!;
final timestamp = DateTime.tryParse(t.createdAt) ?? DateTime.now();
final trade = Trade(
uuid: const Uuid().v1(),
tradeId: tradeId,
rateType: "",
direction: "",
timestamp: timestamp,
updatedAt: DateTime.tryParse(t.updatedAt) ?? timestamp,
payInCurrency: t.fromCurrency,
payInAmount: t.expectedSendAmountDecimal,
payInAddress: t.payinAddress,
payInNetwork: "",
payInExtraId: t.payinExtraId,
payInTxid: t.payinHash,
payOutCurrency: t.toCurrency,
payOutAmount: t.expectedReceiveAmountDecimal,
payOutAddress: t.payoutAddress,
payOutNetwork: "",
payOutExtraId: t.payoutExtraId,
payOutTxid: t.payoutHash,
refundAddress: t.refundAddress,
refundExtraId: t.refundExtraId,
status: t.status.name,
exchangeName: ChangeNowExchange.exchangeName,
);
return ExchangeResponse(value: trade);
}
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
final response =
await ChangeNowAPI.instance.getTransactionStatus(id: trade.tradeId);
if (response.exception != null) {
return ExchangeResponse(exception: response.exception);
}
final t = response.value!;
final timestamp = DateTime.tryParse(t.createdAt) ?? DateTime.now();
final _trade = Trade(
uuid: trade.uuid,
tradeId: trade.tradeId,
rateType: trade.rateType,
direction: trade.direction,
timestamp: timestamp,
updatedAt: DateTime.tryParse(t.updatedAt) ?? timestamp,
payInCurrency: t.fromCurrency,
payInAmount: t.amountSendDecimal.isEmpty
? t.expectedSendAmountDecimal
: t.amountSendDecimal,
payInAddress: t.payinAddress,
payInNetwork: trade.payInNetwork,
payInExtraId: t.payinExtraId,
payInTxid: t.payinHash,
payOutCurrency: t.toCurrency,
payOutAmount: t.amountReceiveDecimal.isEmpty
? t.expectedReceiveAmountDecimal
: t.amountReceiveDecimal,
payOutAddress: t.payoutAddress,
payOutNetwork: trade.payOutNetwork,
payOutExtraId: t.payoutExtraId,
payOutTxid: t.payoutHash,
refundAddress: t.refundAddress,
refundExtraId: t.refundExtraId,
status: t.status.name,
exchangeName: ChangeNowExchange.exchangeName,
);
return ExchangeResponse(value: _trade);
}
@override
Future<ExchangeResponse<List<Trade>>> getTrades() async {
// TODO: implement getTrades
throw UnimplementedError();
}
}

View file

@ -0,0 +1,65 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
abstract class Exchange {
static Exchange fromName(String name) {
switch (name) {
case ChangeNowExchange.exchangeName:
return ChangeNowExchange();
case SimpleSwapExchange.exchangeName:
return SimpleSwapExchange();
default:
throw ArgumentError("Unknown exchange name");
}
}
String get name;
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(bool fixedRate);
Future<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
);
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate);
Future<ExchangeResponse<Trade>> getTrade(String tradeId);
Future<ExchangeResponse<Trade>> updateTrade(Trade trade);
Future<ExchangeResponse<List<Trade>>> getTrades();
Future<ExchangeResponse<Range>> getRange(
String from,
String to,
bool fixedRate,
);
Future<ExchangeResponse<Estimate>> getEstimate(
String from,
String to,
Decimal amount,
bool fixedRate,
bool reversed,
);
Future<ExchangeResponse<Trade>> createTrade({
required String from,
required String to,
required bool fixedRate,
required Decimal amount,
required String addressTo,
String? extraId,
required String addressRefund,
required String refundExtraId,
String? rateId,
required bool reversed,
});
}

View file

@ -1,23 +1,21 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart';
import 'package:stackwallet/providers/exchange/available_floating_rate_pairs_state_provider.dart';
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
import 'package:stackwallet/providers/exchange/changenow_initial_load_status.dart';
import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/exchange/fixed_rate_exchange_form_provider.dart';
import 'package:stackwallet/providers/exchange/fixed_rate_market_pairs_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_api.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
class ChangeNowLoadingService {
class ExchangeDataLoadingService {
Future<void> loadAll(WidgetRef ref, {Coin? coin}) async {
try {
await Future.wait([
_loadFixedRateMarkets(ref, coin: coin),
_loadChangeNowStandardCurrencies(ref, coin: coin),
loadSimpleswapFixedRateCurrencies(ref),
loadSimpleswapFloatingRateCurrencies(ref),
]);
} catch (e, s) {
Logging.instance.log("ChangeNowLoadingService.loadAll failed: $e\n$s",
Logging.instance.log("ExchangeDataLoadingService.loadAll failed: $e\n$s",
level: LogLevel.Error);
}
}
@ -33,12 +31,13 @@ class ChangeNowLoadingService {
ChangeNowLoadStatus.loading;
final response3 =
await ref.read(changeNowProvider).getAvailableFixedRateMarkets();
await ChangeNowAPI.instance.getAvailableFixedRateMarkets();
if (response3.value != null) {
ref.read(fixedRateMarketPairsStateProvider.state).state =
response3.value!;
ref
.read(availableChangeNowCurrenciesProvider)
.updateMarkets(response3.value!);
if (ref.read(fixedRateExchangeFormProvider).market == null) {
if (ref.read(exchangeFormStateProvider).market == null) {
String fromTicker = "btc";
String toTicker = "xmr";
@ -50,7 +49,7 @@ class ChangeNowLoadingService {
.where((e) => e.to == toTicker && e.from == fromTicker);
if (matchingMarkets.isNotEmpty) {
await ref
.read(fixedRateExchangeFormProvider)
.read(exchangeFormStateProvider)
.updateMarket(matchingMarkets.first, true);
}
}
@ -68,8 +67,10 @@ class ChangeNowLoadingService {
ChangeNowLoadStatus.success;
}
Future<void> _loadChangeNowStandardCurrencies(WidgetRef ref,
{Coin? coin}) async {
Future<void> _loadChangeNowStandardCurrencies(
WidgetRef ref, {
Coin? coin,
}) async {
if (ref
.read(changeNowEstimatedInitialLoadStatusStateProvider.state)
.state ==
@ -81,15 +82,18 @@ class ChangeNowLoadingService {
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.loading;
final response = await ref.read(changeNowProvider).getAvailableCurrencies();
final response = await ChangeNowAPI.instance.getAvailableCurrencies();
final response2 =
await ref.read(changeNowProvider).getAvailableFloatingRatePairs();
await ChangeNowAPI.instance.getAvailableFloatingRatePairs();
if (response.value != null) {
ref.read(availableChangeNowCurrenciesStateProvider.state).state =
response.value!;
ref
.read(availableChangeNowCurrenciesProvider)
.updateCurrencies(response.value!);
if (response2.value != null) {
ref.read(availableFloatingRatePairsStateProvider.state).state =
response2.value!;
ref
.read(availableChangeNowCurrenciesProvider)
.updateFloatingPairs(response2.value!);
String fromTicker = "btc";
String toTicker = "xmr";
@ -99,18 +103,18 @@ class ChangeNowLoadingService {
}
if (response.value!.length > 1) {
if (ref.read(estimatedRateExchangeFormProvider).from == null) {
if (ref.read(exchangeFormStateProvider).from == null) {
if (response.value!
.where((e) => e.ticker == fromTicker)
.isNotEmpty) {
await ref.read(estimatedRateExchangeFormProvider).updateFrom(
await ref.read(exchangeFormStateProvider).updateFrom(
response.value!.firstWhere((e) => e.ticker == fromTicker),
false);
}
}
if (ref.read(estimatedRateExchangeFormProvider).to == null) {
if (ref.read(exchangeFormStateProvider).to == null) {
if (response.value!.where((e) => e.ticker == toTicker).isNotEmpty) {
await ref.read(estimatedRateExchangeFormProvider).updateTo(
await ref.read(exchangeFormStateProvider).updateTo(
response.value!.firstWhere((e) => e.ticker == toTicker),
false);
}
@ -137,4 +141,62 @@ class ChangeNowLoadingService {
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.success;
}
Future<void> loadSimpleswapFloatingRateCurrencies(WidgetRef ref) async {
final exchange = SimpleSwapExchange();
final responseCurrencies = await exchange.getAllCurrencies(false);
if (responseCurrencies.value != null) {
ref
.read(availableSimpleswapCurrenciesProvider)
.updateFloatingCurrencies(responseCurrencies.value!);
final responsePairs = await exchange.getAllPairs(false);
if (responsePairs.value != null) {
ref
.read(availableSimpleswapCurrenciesProvider)
.updateFloatingPairs(responsePairs.value!);
} else {
Logging.instance.log(
"loadSimpleswapFloatingRateCurrencies: $responsePairs",
level: LogLevel.Warning,
);
}
} else {
Logging.instance.log(
"loadSimpleswapFloatingRateCurrencies: $responseCurrencies",
level: LogLevel.Warning,
);
}
}
Future<void> loadSimpleswapFixedRateCurrencies(WidgetRef ref) async {
final exchange = SimpleSwapExchange();
final responseCurrencies = await exchange.getAllCurrencies(true);
if (responseCurrencies.value != null) {
ref
.read(availableSimpleswapCurrenciesProvider)
.updateFixedCurrencies(responseCurrencies.value!);
final responsePairs = await exchange.getAllPairs(true);
if (responsePairs.value != null) {
ref
.read(availableSimpleswapCurrenciesProvider)
.updateFixedPairs(responsePairs.value!);
} else {
Logging.instance.log(
"loadSimpleswapFixedRateCurrencies: $responsePairs",
level: LogLevel.Warning,
);
}
} else {
Logging.instance.log(
"loadSimpleswapFixedRateCurrencies: $responseCurrencies",
level: LogLevel.Warning,
);
}
}
}

View file

@ -0,0 +1,24 @@
enum ExchangeExceptionType { generic, serializeResponseError }
class ExchangeException implements Exception {
String errorMessage;
ExchangeExceptionType type;
ExchangeException(this.errorMessage, this.type);
@override
String toString() {
return errorMessage;
}
}
class ExchangeResponse<T> {
late final T? value;
late final ExchangeException? exception;
ExchangeResponse({this.value, this.exception});
@override
String toString() {
return "{error: $exception, value: $value}";
}
}

View file

@ -0,0 +1,504 @@
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/external_api_keys.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/exchange/simpleswap/sp_currency.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
class SimpleSwapAPI {
static const String scheme = "https";
static const String authority = "api.simpleswap.io";
SimpleSwapAPI._();
static final SimpleSwapAPI _instance = SimpleSwapAPI._();
static SimpleSwapAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing
http.Client? client;
Uri _buildUri(String path, Map<String, String>? params) {
return Uri.https(authority, path, params);
}
Future<dynamic> _makeGetRequest(Uri uri) async {
final client = this.client ?? http.Client();
int code = -1;
try {
final response = await client.get(
uri,
);
code = response.statusCode;
final parsed = jsonDecode(response.body);
return parsed;
} catch (e, s) {
Logging.instance.log(
"_makeRequest($uri) HTTP:$code threw: $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<dynamic> _makePostRequest(
Uri uri,
Map<String, dynamic> body,
) async {
final client = this.client ?? http.Client();
try {
final response = await client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body);
return parsed;
}
throw Exception("response: ${response.body}");
} catch (e, s) {
Logging.instance
.log("_makeRequest($uri) threw: $e\n$s", level: LogLevel.Error);
rethrow;
}
}
Future<ExchangeResponse<Trade>> createNewExchange({
required bool isFixedRate,
required String currencyFrom,
required String currencyTo,
required String addressTo,
required String userRefundAddress,
required String userRefundExtraId,
required String amount,
String? extraIdTo,
String? apiKey,
}) async {
Map<String, dynamic> body = {
"fixed": isFixedRate,
"currency_from": currencyFrom,
"currency_to": currencyTo,
"addressTo": addressTo,
"userRefundAddress": userRefundAddress,
"userRefundExtraId": userRefundExtraId,
"amount": double.parse(amount),
"extraIdTo": extraIdTo,
};
final uri =
_buildUri("/create_exchange", {"api_key": apiKey ?? kSimpleSwapApiKey});
try {
final jsonObject = await _makePostRequest(uri, body);
final json = Map<String, dynamic>.from(jsonObject as Map);
final trade = Trade(
uuid: const Uuid().v1(),
tradeId: json["id"] as String,
rateType: json["type"] as String,
direction: "direct",
timestamp: DateTime.parse(json["timestamp"] as String),
updatedAt: DateTime.parse(json["updated_at"] as String),
payInCurrency: json["currency_from"] as String,
payInAmount: json["amount_from"] as String,
payInAddress: json["address_from"] as String,
payInNetwork: "",
payInExtraId: json["extra_id_from"] as String? ?? "",
payInTxid: json["tx_from"] as String? ?? "",
payOutCurrency: json["currency_to"] as String,
payOutAmount: json["amount_to"] as String,
payOutAddress: json["address_to"] as String,
payOutNetwork: "",
payOutExtraId: json["extra_id_to"] as String? ?? "",
payOutTxid: json["tx_to"] as String? ?? "",
refundAddress: json["user_refund_address"] as String,
refundExtraId: json["user_refund_extra_id"] as String,
status: json["status"] as String,
exchangeName: SimpleSwapExchange.exchangeName,
);
return ExchangeResponse(value: trade, exception: null);
} catch (e, s) {
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
value: null,
);
}
}
Future<ExchangeResponse<List<SPCurrency>>> getAllCurrencies({
String? apiKey,
required bool fixedRate,
}) async {
final uri = _buildUri(
"/get_all_currencies", {"api_key": apiKey ?? kSimpleSwapApiKey});
try {
final jsonArray = await _makeGetRequest(uri);
return await compute(_parseAvailableCurrenciesJson, jsonArray as List);
} catch (e, s) {
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
ExchangeResponse<List<SPCurrency>> _parseAvailableCurrenciesJson(
List<dynamic> jsonArray) {
try {
List<SPCurrency> currencies = [];
for (final json in jsonArray) {
try {
currencies
.add(SPCurrency.fromJson(Map<String, dynamic>.from(json as Map)));
} catch (_) {
return ExchangeResponse(
exception: ExchangeException("Failed to serialize $json",
ExchangeExceptionType.serializeResponseError));
}
}
return ExchangeResponse(value: currencies);
} catch (e, s) {
Logging.instance.log("_parseAvailableCurrenciesJson exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
Future<ExchangeResponse<SPCurrency>> getCurrency({
required String symbol,
String? apiKey,
}) async {
final uri = _buildUri(
"/get_currency",
{
"api_key": apiKey ?? kSimpleSwapApiKey,
"symbol": symbol,
},
);
try {
final jsonObject = await _makeGetRequest(uri);
return ExchangeResponse(
value: SPCurrency.fromJson(
Map<String, dynamic>.from(jsonObject as Map)));
} catch (e, s) {
Logging.instance
.log("getCurrency exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
/// returns a map where the key currency symbol is a valid pair with any of
/// the symbols in its value list
Future<ExchangeResponse<List<Pair>>> getAllPairs({
required bool isFixedRate,
String? apiKey,
}) async {
final uri = _buildUri(
"/get_all_pairs",
{
"api_key": apiKey ?? kSimpleSwapApiKey,
"fixed": isFixedRate.toString(),
},
);
try {
final jsonObject = await _makeGetRequest(uri);
final result = await compute(
_parseAvailablePairsJson,
Tuple2(jsonObject as Map, isFixedRate),
);
return result;
} catch (e, s) {
Logging.instance
.log("getAllPairs exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
ExchangeResponse<List<Pair>> _parseAvailablePairsJson(
Tuple2<Map<dynamic, dynamic>, bool> args,
) {
try {
List<Pair> pairs = [];
for (final entry in args.item1.entries) {
try {
final from = entry.key as String;
for (final to in entry.value as List) {
pairs.add(
Pair(
from: from,
fromNetwork: "",
to: to as String,
toNetwork: "",
fixedRate: args.item2,
floatingRate: !args.item2,
),
);
}
} catch (_) {
return ExchangeResponse(
exception: ExchangeException("Failed to serialize $json",
ExchangeExceptionType.serializeResponseError));
}
}
return ExchangeResponse(value: pairs);
} catch (e, s) {
Logging.instance.log("_parseAvailableCurrenciesJson exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
/// returns the estimated amount as a string
Future<ExchangeResponse<String>> getEstimated({
required bool isFixedRate,
required String currencyFrom,
required String currencyTo,
required String amount,
String? apiKey,
}) async {
final uri = _buildUri(
"/get_estimated",
{
"api_key": apiKey ?? kSimpleSwapApiKey,
"fixed": isFixedRate.toString(),
"currency_from": currencyFrom,
"currency_to": currencyTo,
"amount": amount,
},
);
try {
final jsonObject = await _makeGetRequest(uri);
return ExchangeResponse(value: jsonObject as String);
} catch (e, s) {
Logging.instance
.log("getEstimated exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
/// returns the exchange for the given id
Future<ExchangeResponse<Trade>> getExchange({
required String exchangeId,
String? apiKey,
Trade? oldTrade,
}) async {
final uri = _buildUri(
"/get_exchange",
{
"api_key": apiKey ?? kSimpleSwapApiKey,
"id": exchangeId,
},
);
try {
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
final ts = DateTime.parse(json["timestamp"] as String);
final trade = Trade(
uuid: oldTrade?.uuid ?? const Uuid().v1(),
tradeId: json["id"] as String,
rateType: json["type"] as String,
direction: "direct",
timestamp: ts,
updatedAt: DateTime.tryParse(json["updated_at"] as String? ?? "") ?? ts,
payInCurrency: json["currency_from"] as String,
payInAmount: json["amount_from"] as String,
payInAddress: json["address_from"] as String,
payInNetwork: "",
payInExtraId: json["extra_id_from"] as String? ?? "",
payInTxid: json["tx_from"] as String? ?? "",
payOutCurrency: json["currency_to"] as String,
payOutAmount: json["amount_to"] as String,
payOutAddress: json["address_to"] as String,
payOutNetwork: "",
payOutExtraId: json["extra_id_to"] as String? ?? "",
payOutTxid: json["tx_to"] as String? ?? "",
refundAddress: json["user_refund_address"] as String,
refundExtraId: json["user_refund_extra_id"] as String,
status: json["status"] as String,
exchangeName: SimpleSwapExchange.exchangeName,
);
return ExchangeResponse(value: trade);
} catch (e, s) {
Logging.instance
.log("getExchange exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
/// returns the minimal exchange amount
Future<ExchangeResponse<Range>> getRange({
required bool isFixedRate,
required String currencyFrom,
required String currencyTo,
String? apiKey,
}) async {
final uri = _buildUri(
"/get_ranges",
{
"api_key": apiKey ?? kSimpleSwapApiKey,
"fixed": isFixedRate.toString(),
"currency_from": currencyFrom,
"currency_to": currencyTo,
},
);
try {
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
return ExchangeResponse(
value: Range(
max: Decimal.tryParse(json["max"] as String? ?? ""),
min: Decimal.tryParse(json["min"] as String? ?? ""),
),
);
} catch (e, s) {
Logging.instance.log("getRange exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
Future<ExchangeResponse<List<FixedRateMarket>>> getFixedRateMarketInfo({
String? apiKey,
}) async {
final uri = _buildUri(
"/get_market_info",
null,
// {
// "api_key": apiKey ?? kSimpleSwapApiKey,
// "fixed": isFixedRate.toString(),
// "currency_from": currencyFrom,
// "currency_to": currencyTo,
// },
);
try {
final jsonArray = await _makeGetRequest(uri);
try {
final result = await compute(
_parseFixedRateMarketsJson,
jsonArray as List,
);
return result;
} catch (e, s) {
Logging.instance.log("getAvailableFixedRateMarkets exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
"Error: $jsonArray",
ExchangeExceptionType.serializeResponseError,
),
);
}
} catch (e, s) {
Logging.instance.log("getAvailableFixedRateMarkets exception: $e\n$s",
level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
ExchangeResponse<List<FixedRateMarket>> _parseFixedRateMarketsJson(
List<dynamic> jsonArray) {
try {
final List<FixedRateMarket> markets = [];
for (final json in jsonArray) {
try {
final map = Map<String, dynamic>.from(json as Map);
markets.add(FixedRateMarket(
from: map["currency_from"] as String,
to: map["currency_to"] as String,
min: Decimal.parse(map["min"] as String),
max: Decimal.parse(map["max"] as String),
rate: Decimal.parse(map["rate"] as String),
minerFee: null,
));
} catch (_) {
return ExchangeResponse(
exception: ExchangeException("Failed to serialize $json",
ExchangeExceptionType.serializeResponseError));
}
}
return ExchangeResponse(value: markets);
} catch (_) {
rethrow;
}
}
}

View file

@ -0,0 +1,149 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_api.dart';
class SimpleSwapExchange extends Exchange {
static const exchangeName = "SimpleSwap";
@override
String get name => exchangeName;
@override
Future<ExchangeResponse<Trade>> createTrade({
required String from,
required String to,
required bool fixedRate,
required Decimal amount,
required String addressTo,
String? extraId,
required String addressRefund,
required String refundExtraId,
String? rateId,
required bool reversed,
}) async {
return await SimpleSwapAPI.instance.createNewExchange(
isFixedRate: fixedRate,
currencyFrom: from,
currencyTo: to,
addressTo: addressTo,
userRefundAddress: addressRefund,
userRefundExtraId: refundExtraId,
amount: amount.toString(),
extraIdTo: extraId,
);
}
@override
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
bool fixedRate,
) async {
final response =
await SimpleSwapAPI.instance.getAllCurrencies(fixedRate: fixedRate);
if (response.value != null) {
final List<Currency> currencies = response.value!
.map((e) => Currency(
ticker: e.symbol,
name: e.name,
network: e.network,
image: e.image,
hasExternalId: e.hasExtraId,
externalId: e.extraId,
isFiat: false,
featured: false,
isStable: false,
supportsFixedRate: fixedRate,
))
.toList();
return ExchangeResponse<List<Currency>>(
value: currencies,
exception: response.exception,
);
}
return ExchangeResponse<List<Currency>>(
value: null,
exception: response.exception,
);
}
@override
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
return await SimpleSwapAPI.instance.getAllPairs(isFixedRate: fixedRate);
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
String from,
String to,
Decimal amount,
bool fixedRate,
bool reversed,
) async {
final response = await SimpleSwapAPI.instance.getEstimated(
isFixedRate: fixedRate,
currencyFrom: from,
currencyTo: to,
amount: amount.toString(),
);
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
return ExchangeResponse(
value: Estimate(
estimatedAmount: Decimal.parse(response.value!),
fixedRate: fixedRate,
reversed: reversed,
),
);
}
@override
Future<ExchangeResponse<Range>> getRange(
String from,
String to,
bool fixedRate,
) async {
return await SimpleSwapAPI.instance.getRange(
isFixedRate: fixedRate,
currencyFrom: from,
currencyTo: to,
);
}
@override
Future<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
) async {
// return await SimpleSwapAPI.instance.ge
throw UnimplementedError();
}
@override
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
return await SimpleSwapAPI.instance.getExchange(exchangeId: tradeId);
}
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
return await SimpleSwapAPI.instance.getExchange(
exchangeId: trade.tradeId,
oldTrade: trade,
);
}
@override
Future<ExchangeResponse<List<Trade>>> getTrades() async {
// TODO: implement getTrades
throw UnimplementedError();
}
}

View file

@ -3,9 +3,11 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/notification_model.dart';
import 'package:stackwallet/services/change_now/change_now.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/notifications_api.dart';
import 'package:stackwallet/services/trade_service.dart';
@ -17,7 +19,6 @@ class NotificationsService extends ChangeNotifier {
late NodeService nodeService;
late TradesService tradesService;
late Prefs prefs;
late ChangeNow changeNow;
NotificationsService._();
static final NotificationsService _instance = NotificationsService._();
@ -27,12 +28,10 @@ class NotificationsService extends ChangeNotifier {
required NodeService nodeService,
required TradesService tradesService,
required Prefs prefs,
required ChangeNow changeNow,
}) async {
this.nodeService = nodeService;
this.tradesService = tradesService;
this.prefs = prefs;
this.changeNow = changeNow;
}
// watched transactions
@ -184,33 +183,52 @@ class NotificationsService extends ChangeNotifier {
for (final notification in _watchedChangeNowTradeNotifications) {
final id = notification.changeNowId!;
final result = await changeNow.getTransactionStatus(id: id);
final trades =
tradesService.trades.where((element) => element.tradeId == id);
ChangeNowTransactionStatus? status = result.value?.status;
if (trades.isEmpty) {
return;
}
final oldTrade = trades.first;
late final ExchangeResponse<Trade> response;
switch (oldTrade.exchangeName) {
case SimpleSwapExchange.exchangeName:
response = await SimpleSwapExchange().updateTrade(oldTrade);
break;
case ChangeNowExchange.exchangeName:
response = await ChangeNowExchange().updateTrade(oldTrade);
break;
default:
return;
}
if (response.value == null) {
return;
}
final trade = response.value!;
// only update if status has changed
if (status != null && status.name != notification.title) {
if (trade.status != notification.title) {
bool shouldWatchForUpdates = true;
// TODO: make sure we set shouldWatchForUpdates to correct value here
switch (status) {
case ChangeNowTransactionStatus.New:
case ChangeNowTransactionStatus.Waiting:
case ChangeNowTransactionStatus.Confirming:
case ChangeNowTransactionStatus.Exchanging:
case ChangeNowTransactionStatus.Verifying:
case ChangeNowTransactionStatus.Sending:
shouldWatchForUpdates = true;
break;
case ChangeNowTransactionStatus.Finished:
case ChangeNowTransactionStatus.Failed:
case ChangeNowTransactionStatus.Refunded:
switch (trade.status) {
case "Refunded":
case "refunded":
case "Failed":
case "failed":
case "closed":
case "expired":
case "Finished":
case "finished":
shouldWatchForUpdates = false;
break;
default:
shouldWatchForUpdates = true;
}
final updatedNotification = notification.copyWith(
title: status.name,
title: trade.status,
shouldWatchForUpdates: shouldWatchForUpdates,
);
@ -220,23 +238,11 @@ class NotificationsService extends ChangeNotifier {
}
// replaces the current notification with the updated one
add(updatedNotification, true);
unawaited(add(updatedNotification, true));
// update the trade in db
if (result.value != null) {
// fetch matching trade from db
final trade = tradesService.trades
.firstWhere((element) => element.id == result.value!.id);
// update status
final updatedTrade = trade.copyWith(
statusObject: result.value!,
statusString: result.value!.status.name,
);
// over write trade stored in db with updated version
tradesService.add(trade: updatedTrade, shouldNotifyListeners: true);
}
// over write trade stored in db with updated version
await tradesService.edit(trade: trade, shouldNotifyListeners: true);
}
}
}

View file

@ -9,6 +9,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/utilities/prefs.dart';
class PriceAPI {
static const refreshInterval = 60;
@ -76,6 +78,13 @@ class PriceAPI {
return _cachedPrices;
}
final externalCalls = Prefs.instance.externalCalls;
if (!Logger.isTestEnv && !externalCalls) {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
return _cachedPrices;
}
Map<Coin, Tuple2<Decimal, double>> result = {};
try {
final uri = Uri.parse(
@ -88,14 +97,8 @@ class PriceAPI {
headers: {'Content-Type': 'application/json'},
);
// debugPrint(coinGeckoResponse.statusCode.toString());
// debugPrint(coinGeckoResponse.body.toString());
// debugPrint(coinGeckoResponse.headers.toString());
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List<dynamic>;
// log(JsonEncoder.withIndent(" ").convert(coinGeckoData));
for (final map in coinGeckoData) {
final String coinName = map["name"] as String;
final coin = coinFromPrettyName(coinName);
@ -120,6 +123,12 @@ class PriceAPI {
}
static Future<List<String>?> availableBaseCurrencies() async {
final externalCalls = Prefs.instance.externalCalls;
if (!Logger.isTestEnv && !externalCalls) {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
return null;
}
const uriString =
"https://api.coingecko.com/api/v3/simple/supported_vs_currencies";
try {

View file

@ -1,22 +1,22 @@
import 'package:flutter/cupertino.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
class TradesService extends ChangeNotifier {
List<ExchangeTransaction> get trades {
final list =
DB.instance.values<ExchangeTransaction>(boxName: DB.boxNameTrades);
List<Trade> get trades {
final list = DB.instance.values<Trade>(boxName: DB.boxNameTradesV2);
list.sort((a, b) =>
b.date.millisecondsSinceEpoch - a.date.millisecondsSinceEpoch);
b.timestamp.millisecondsSinceEpoch -
a.timestamp.millisecondsSinceEpoch);
return list;
}
Future<void> add({
required ExchangeTransaction trade,
required Trade trade,
required bool shouldNotifyListeners,
}) async {
await DB.instance.put<ExchangeTransaction>(
boxName: DB.boxNameTrades, key: trade.uuid, value: trade);
await DB.instance
.put<Trade>(boxName: DB.boxNameTradesV2, key: trade.uuid, value: trade);
if (shouldNotifyListeners) {
notifyListeners();
@ -24,11 +24,10 @@ class TradesService extends ChangeNotifier {
}
Future<void> edit({
required ExchangeTransaction trade,
required Trade trade,
required bool shouldNotifyListeners,
}) async {
if (DB.instance.get<ExchangeTransaction>(
boxName: DB.boxNameTrades, key: trade.uuid) ==
if (DB.instance.get<Trade>(boxName: DB.boxNameTradesV2, key: trade.uuid) ==
null) {
throw Exception("Attempted to edit a trade that does not exist in Hive!");
}
@ -38,7 +37,7 @@ class TradesService extends ChangeNotifier {
}
Future<void> delete({
required ExchangeTransaction trade,
required Trade trade,
required bool shouldNotifyListeners,
}) async {
await deleteByUuid(
@ -49,8 +48,7 @@ class TradesService extends ChangeNotifier {
required String uuid,
required bool shouldNotifyListeners,
}) async {
await DB.instance
.delete<ExchangeTransaction>(boxName: DB.boxNameTrades, key: uuid);
await DB.instance.delete<Trade>(boxName: DB.boxNameTradesV2, key: uuid);
if (shouldNotifyListeners) {
notifyListeners();

View file

@ -7,6 +7,7 @@ abstract class Assets {
static const png = _PNG();
static const lottie = _ANIMATIONS();
static const socials = _SOCIALS();
static const exchange = _EXCHANGE();
}
class _SOCIALS {
@ -18,6 +19,13 @@ class _SOCIALS {
String get telegram => "assets/svg/socials/telegram-brands.svg";
}
class _EXCHANGE {
const _EXCHANGE();
String get changeNow => "assets/svg/exchange_icons/change_now_logo_1.svg";
String get simpleSwap => "assets/svg/exchange_icons/simpleswap-icon.svg";
}
class _SVG {
const _SVG();
@ -52,6 +60,8 @@ class _SVG {
"assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg";
String get polygon => "assets/svg/Polygon.svg";
String get personaIncognito => "assets/svg/persona-incognito-1.svg";
String get personaEasy => "assets/svg/persona-easy-1.svg";
String get drd => "assets/svg/drd-icon.svg";
String get boxAuto => "assets/svg/box-auto.svg";
String get plus => "assets/svg/plus.svg";

View file

@ -36,7 +36,7 @@ abstract class Constants {
// Enable Logger.print statements
static const bool disableLogger = false;
static const int currentHiveDbVersion = 1;
static const int currentHiveDbVersion = 2;
static List<int> possibleLengthsForCoin(Coin coin) {
final List<int> values = [];

View file

@ -2,6 +2,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/lelantus_coin.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/services/node_service.dart';
@ -19,6 +21,10 @@ class DbVersionMigrator {
FlutterSecureStorage(),
),
}) async {
Logging.instance.log(
"Running migrate fromVersion $fromVersion",
level: LogLevel.Warning,
);
switch (fromVersion) {
case 0:
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
@ -114,26 +120,29 @@ class DbVersionMigrator {
// try to continue migrating
return await migrate(1);
// case 1:
// await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
// final walletsService = WalletsService();
// final walletInfoList = await walletsService.walletNames;
// for (final walletInfo in walletInfoList.values) {
// if (walletInfo.coin == Coin.firo) {
// await Hive.openBox<dynamic>(walletInfo.walletId);
// await DB.instance.delete<dynamic>(
// key: "latest_tx_model", boxName: walletInfo.walletId);
// await DB.instance.delete<dynamic>(
// key: "latest_lelantus_tx_model", boxName: walletInfo.walletId);
// }
// }
//
// // update version
// await DB.instance.put<dynamic>(
// boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 2);
//
// // try to continue migrating
// return await migrate(2);
case 1:
await Hive.openBox<ExchangeTransaction>(DB.boxNameTrades);
await Hive.openBox<Trade>(DB.boxNameTradesV2);
final trades =
DB.instance.values<ExchangeTransaction>(boxName: DB.boxNameTrades);
for (final old in trades) {
if (old.statusObject != null) {
final trade = Trade.fromExchangeTransaction(old, false);
await DB.instance.put<Trade>(
boxName: DB.boxNameTradesV2,
key: trade.uuid,
value: trade,
);
}
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 2);
// try to continue migrating
return await migrate(2);
default:
// finally return

View file

@ -0,0 +1,30 @@
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/utilities/logger.dart';
Future<bool> deleteEverything() async {
try {
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameAddressBook);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameDebugInfo);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameNodeModels);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePrimaryNodes);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameAllWalletsData);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameNotifications);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameWatchedTransactions);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameWatchedTrades);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTrades);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradesV2);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradeNotes);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradeLookup);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameFavoriteWallets);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePrefs);
await DB.instance
.deleteBoxFromDisk(boxName: DB.boxNameWalletsToDeleteOnStart);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePriceCache);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameDBInfo);
await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTheme);
return true;
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
return false;
}
}

View file

@ -12,8 +12,7 @@ import 'package:stackwallet/utilities/enums/log_level_enum.dart';
export 'enums/log_level_enum.dart';
class Logging {
static const isArmLinux =
bool.fromEnvironment("IS_ARM");
static const isArmLinux = bool.fromEnvironment("IS_ARM");
static final isTestEnv = Platform.environment["FLUTTER_TEST"] == "true";
Logging._();
static final Logging _instance = Logging._();
@ -45,41 +44,47 @@ class Logging {
core.bool printToConsole = true,
core.bool printFullLength = false,
}) {
if (isTestEnv || isArmLinux) {
Logger.print(object, normalLength: !printFullLength);
return;
}
final now = core.DateTime.now().toUtc();
final log = Log()
..message = object.toString()
..logLevel = level
..timestampInMillisUTC = now.millisecondsSinceEpoch;
if (level == LogLevel.Error || level == LogLevel.Fatal) {
printFullLength = true;
}
try {
if (isTestEnv || isArmLinux) {
Logger.print(object, normalLength: !printFullLength);
return;
}
final now = core.DateTime.now().toUtc();
final log = Log()
..message = object.toString()
..logLevel = level
..timestampInMillisUTC = now.millisecondsSinceEpoch;
if (level == LogLevel.Error || level == LogLevel.Fatal) {
printFullLength = true;
}
isar!.writeTxnSync(() => log.id = isar!.logs.putSync(log));
isar!.writeTxnSync(() => log.id = isar!.logs.putSync(log));
if (printToConsole) {
final core.String logStr = "Log: ${log.toString()}";
final core.int logLength = logStr.length;
if (printToConsole) {
final core.String logStr = "Log: ${log.toString()}";
final core.int logLength = logStr.length;
if (!printFullLength || logLength <= defaultPrintLength) {
debugPrint(logStr);
} else {
core.int start = 0;
core.int endIndex = defaultPrintLength;
core.int tmpLogLength = logLength;
while (endIndex < logLength) {
debugPrint(logStr.substring(start, endIndex));
endIndex += defaultPrintLength;
start += defaultPrintLength;
tmpLogLength -= defaultPrintLength;
}
if (tmpLogLength > 0) {
debugPrint(logStr.substring(start, logLength));
if (!printFullLength || logLength <= defaultPrintLength) {
debugPrint(logStr);
} else {
core.int start = 0;
core.int endIndex = defaultPrintLength;
core.int tmpLogLength = logLength;
while (endIndex < logLength) {
debugPrint(logStr.substring(start, endIndex));
endIndex += defaultPrintLength;
start += defaultPrintLength;
tmpLogLength -= defaultPrintLength;
}
if (tmpLogLength > 0) {
debugPrint(logStr.substring(start, logLength));
}
}
}
} catch (e, s) {
print("problem trying to log");
print("$e $s");
Logger.print(object);
}
}
}

View file

@ -36,6 +36,7 @@ class Prefs extends ChangeNotifier {
_hideBlockExplorerWarning = await _getHideBlockExplorerWarning();
_gotoWalletOnStartup = await _getGotoWalletOnStartup();
_startupWalletId = await _getStartupWalletId();
_externalCalls = await _getHasExternalCalls();
_initialized = true;
}
@ -544,4 +545,30 @@ class Prefs extends ChangeNotifier {
return await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs, key: "startupWalletId") as String?;
}
// incognito mode off by default
// allow external network calls such as exchange data and price info
bool _externalCalls = true;
bool get externalCalls => _externalCalls;
set externalCalls(bool externalCalls) {
if (_externalCalls != externalCalls) {
DB.instance
.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "externalCalls",
value: externalCalls)
.then((_) {
_externalCalls = externalCalls;
notifyListeners();
});
}
}
Future<bool> _getHasExternalCalls() async {
return await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs, key: "externalCalls") as bool? ??
false;
}
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/theme/color_theme.dart';
@ -1435,21 +1434,34 @@ class StackColors extends ThemeExtension<StackColors> {
blurRadius: 4,
);
Color colorForStatus(ChangeNowTransactionStatus status) {
Color colorForStatus(String status) {
switch (status) {
case ChangeNowTransactionStatus.New:
case ChangeNowTransactionStatus.Waiting:
case ChangeNowTransactionStatus.Confirming:
case ChangeNowTransactionStatus.Exchanging:
case ChangeNowTransactionStatus.Sending:
case ChangeNowTransactionStatus.Verifying:
case "New":
case "new":
case "Waiting":
case "waiting":
case "Confirming":
case "confirming":
case "Exchanging":
case "exchanging":
case "Sending":
case "sending":
case "Verifying":
case "verifying":
return const Color(0xFFD3A90F);
case ChangeNowTransactionStatus.Finished:
case "Finished":
case "finished":
return accentColorGreen;
case ChangeNowTransactionStatus.Failed:
case "Failed":
case "failed":
case "closed":
case "expired":
return accentColorRed;
case ChangeNowTransactionStatus.Refunded:
case "Refunded":
case "refunded":
return textSubtitle2;
default:
return const Color(0xFFD3A90F);
}
}

View file

@ -10,6 +10,7 @@ class RoundedContainer extends StatelessWidget {
this.radiusMultiplier = 1.0,
this.width,
this.height,
this.borderColor,
}) : super(key: key);
final Widget? child;
@ -18,6 +19,7 @@ class RoundedContainer extends StatelessWidget {
final double radiusMultiplier;
final double? width;
final double? height;
final Color? borderColor;
@override
Widget build(BuildContext context) {
@ -29,6 +31,7 @@ class RoundedContainer extends StatelessWidget {
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius * radiusMultiplier,
),
border: borderColor == null ? null : Border.all(color: borderColor!),
),
child: Padding(
padding: padding,

View file

@ -10,6 +10,7 @@ class RoundedWhiteContainer extends StatelessWidget {
this.radiusMultiplier = 1.0,
this.width,
this.height,
this.borderColor,
}) : super(key: key);
final Widget? child;
@ -17,6 +18,7 @@ class RoundedWhiteContainer extends StatelessWidget {
final double radiusMultiplier;
final double? width;
final double? height;
final Color? borderColor;
@override
Widget build(BuildContext context) {
@ -27,6 +29,7 @@ class RoundedWhiteContainer extends StatelessWidget {
width: width,
height: height,
child: child,
borderColor: borderColor,
);
}
}

View file

@ -2,8 +2,8 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -16,7 +16,7 @@ class TradeCard extends ConsumerWidget {
required this.onTap,
}) : super(key: key);
final ExchangeTransaction trade;
final Trade trade;
final VoidCallback onTap;
String _fetchIconAssetForStatus(String statusString, BuildContext context) {
@ -62,7 +62,7 @@ class TradeCard extends ConsumerWidget {
child: Center(
child: SvgPicture.asset(
_fetchIconAssetForStatus(
trade.statusObject?.status.name ?? trade.statusString,
trade.status,
context,
),
width: 32,
@ -80,11 +80,11 @@ class TradeCard extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${trade.fromCurrency.toUpperCase()}${trade.toCurrency.toUpperCase()}",
"${trade.payInCurrency.toUpperCase()}${trade.payOutCurrency.toUpperCase()}",
style: STextStyles.itemSubtitle12(context),
),
Text(
"${Decimal.tryParse(trade.statusObject?.amountSendDecimal ?? "") ?? "..."} ${trade.fromCurrency.toUpperCase()}",
"${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}",
style: STextStyles.itemSubtitle12(context),
),
],
@ -96,12 +96,12 @@ class TradeCard extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"ChangeNOW",
trade.exchangeName,
style: STextStyles.label(context),
),
Text(
Format.extractDateFrom(
trade.date.millisecondsSinceEpoch ~/ 1000),
trade.timestamp.millisecondsSinceEpoch ~/ 1000),
style: STextStyles.label(context),
),
],

View file

@ -11,7 +11,7 @@ description: Stack Wallet
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.5.0+70
version: 1.5.7+77
environment:
sdk: ">=2.17.0 <3.0.0"
@ -290,6 +290,8 @@ flutter:
- assets/svg/tx-icon-anonymize-pending.svg
- assets/svg/tx-icon-anonymize-failed.svg
- assets/svg/Polygon.svg
- assets/svg/persona-easy-1.svg
- assets/svg/persona-incognito-1.svg
# coin icons
- assets/svg/coin_icons/Bitcoin.svg
- assets/svg/coin_icons/Bitcoincash.svg
@ -314,6 +316,9 @@ flutter:
- assets/svg/message-question-1.svg
- assets/svg/drd-icon.svg
- assets/svg/box-auto.svg
# exchange icons
- assets/svg/exchange_icons/change_now_logo_1.svg
- assets/svg/exchange_icons/simpleswap-icon.svg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.

51
scripts/setup.sh Normal file
View file

@ -0,0 +1,51 @@
#!/bin/bash
sudo apt update && sudo apt upgrade -y && sudo apt dist-upgrade -y
mkdir "$HOME/development"
mkdir "$HOME/projects"
sudo apt install -y git build-essential curl
export DEVELOPMENT=$HOME/development
export PROJECTS=$HOME/projects
# setup flutter
sudo apt install -y unzip pkg-config clang cmake ninja-build libgtk-3-dev
cd $DEVELOPMENT
git clone https://github.com/flutter/flutter.git
cd flutter
git checkout 3.0.3
export FLUTTER_DIR=$(pwd)/bin
echo 'export PATH="$PATH:'${FLUTTER_DIR}'"' >> ~/.bashrc
source ~/.bashrc
flutter doctor
# setup stack_wallet github
cd $PROJECTS
git clone https://github.com/cypherstack/stack_wallet.git
cd stack_wallet
export STACK_WALLET=$(pwd)
git submodule update --init --recursive
# Create template lib/external_api_keys.dart file if it doesn't already exist
KEYS="$HOME/projects/stack_wallet/lib/external_api_keys.dart"
if ! test -f "$KEYS"; then
echo 'prebuild.sh: creating template lib/external_api_keys.dart file'
echo 'const kChangeNowApiKey = "";' > $KEYS
fi
#install stack wallet dependencies
sudo apt-get install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm
sudo apt-get install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev
sudo apt-get install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless
sudo apt install -y libc6-dev-i386
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
cargo install cargo-ndk
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
# build stack wallet plugins
cd $STACK_WALLET
cd scripts/android
./build_all.sh

View file

@ -276,6 +276,14 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs {
super.noSuchMethod(Invocation.setter(#wifiOnly, wifiOnly),
returnValueForMissingStub: null);
@override
bool get externalCalls =>
(super.noSuchMethod(Invocation.getter(#externalCalls), returnValue: false)
as bool);
@override
set externalCalls(bool? eCalls) =>
super.noSuchMethod(Invocation.setter(#externalCalls, eCalls),
returnValueForMissingStub: null);
@override
bool get showFavoriteWallets =>
(super.noSuchMethod(Invocation.getter(#showFavoriteWallets),
returnValue: false) as bool);

Some files were not shown because too many files have changed in this diff Show more