Merge branch 'staging' into desktop

This commit is contained in:
julian 2022-10-26 16:52:45 -06:00
commit baca31cfba
47 changed files with 3715 additions and 500 deletions

View file

@ -50,48 +50,36 @@ jobs:
$encodedBytes = [System.Convert]::FromBase64String($env:CHANGE_NOW); $encodedBytes = [System.Convert]::FromBase64String($env:CHANGE_NOW);
Set-Content $secretFileExchange -Value $encodedBytes -AsByteStream; Set-Content $secretFileExchange -Value $encodedBytes -AsByteStream;
$secretFileExchangeHash = Get-FileHash $secretFileExchange; $secretFileExchangeHash = Get-FileHash $secretFileExchange;
Write-Output "::set-output name=SECRET_FILE_EXCHANGE::$secretFileExchange";
Write-Output "::set-output name=SECRET_FILE_EXCHANGE_HASH::$($secretFileExchangeHash.Hash)";
Write-Output "Secret file $secretFileExchange has hash $($secretFileExchangeHash.Hash)"; Write-Output "Secret file $secretFileExchange has hash $($secretFileExchangeHash.Hash)";
$secretFileBitcoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart"; $secretFileBitcoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:BITCOIN_TEST); $encodedBytes = [System.Convert]::FromBase64String($env:BITCOIN_TEST);
Set-Content $secretFileBitcoin -Value $encodedBytes -AsByteStream; Set-Content $secretFileBitcoin -Value $encodedBytes -AsByteStream;
$secretFileBitcoinHash = Get-FileHash $secretFileBitcoin; $secretFileBitcoinHash = Get-FileHash $secretFileBitcoin;
Write-Output "::set-output name=SECRET_FILE_BITCOIN::$secretFileBitcoin";
Write-Output "::set-output name=SECRET_FILE_BITCOIN_HASH::$($secretFileBitcoinHash.Hash)";
Write-Output "Secret file $secretFileBitcoin has hash $($secretFileBitcoinHash.Hash)"; Write-Output "Secret file $secretFileBitcoin has hash $($secretFileBitcoinHash.Hash)";
$secretFileDogecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart"; $secretFileDogecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:DOGECOIN_TEST); $encodedBytes = [System.Convert]::FromBase64String($env:DOGECOIN_TEST);
Set-Content $secretFileDogecoin -Value $encodedBytes -AsByteStream; Set-Content $secretFileDogecoin -Value $encodedBytes -AsByteStream;
$secretFileDogecoinHash = Get-FileHash $secretFileDogecoin; $secretFileDogecoinHash = Get-FileHash $secretFileDogecoin;
Write-Output "::set-output name=SECRET_FILE_DOGECOIN::$secretFileDogecoin";
Write-Output "::set-output name=SECRET_FILE_DOGECOIN_HASH::$($secretFileDogecoinHash.Hash)";
Write-Output "Secret file $secretFileDogecoin has hash $($secretFileDogecoinHash.Hash)"; Write-Output "Secret file $secretFileDogecoin has hash $($secretFileDogecoinHash.Hash)";
$secretFileFiro = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/firo/firo_wallet_test_parameters.dart"; $secretFileFiro = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/firo/firo_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:FIRO_TEST); $encodedBytes = [System.Convert]::FromBase64String($env:FIRO_TEST);
Set-Content $secretFileFiro -Value $encodedBytes -AsByteStream; Set-Content $secretFileFiro -Value $encodedBytes -AsByteStream;
$secretFileFiroHash = Get-FileHash $secretFileFiro; $secretFileFiroHash = Get-FileHash $secretFileFiro;
Write-Output "::set-output name=SECRET_FILE_FIRO::$secretFileFiro";
Write-Output "::set-output name=SECRET_FILE_FIRO_HASH::$($secretFileFiroHash.Hash)";
Write-Output "Secret file $secretFileFiro has hash $($secretFileFiroHash.Hash)"; Write-Output "Secret file $secretFileFiro has hash $($secretFileFiroHash.Hash)";
$secretFileBitcoinCash = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart"; $secretFileBitcoinCash = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:BITCOINCASH_TEST); $encodedBytes = [System.Convert]::FromBase64String($env:BITCOINCASH_TEST);
Set-Content $secretFileBitcoinCash -Value $encodedBytes -AsByteStream; Set-Content $secretFileBitcoinCash -Value $encodedBytes -AsByteStream;
$secretFileBitcoinCashHash = Get-FileHash $secretFileBitcoinCash; $secretFileBitcoinCashHash = Get-FileHash $secretFileBitcoinCash;
Write-Output "::set-output name=SECRET_FILE_BITCOINCASH::$secretFileBitcoinCash";
Write-Output "::set-output name=SECRET_FILE_BITCOINCASH_HASH::$($secretFileBitcoinCashHash.Hash)";
Write-Output "Secret file $secretFileBitcoinCash has hash $($secretFileBitcoinCashHash.Hash)"; Write-Output "Secret file $secretFileBitcoinCash has hash $($secretFileBitcoinCashHash.Hash)";
$secretFileNamecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/namecoin/namecoin_wallet_test_parameters.dart"; $secretFileNamecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/namecoin/namecoin_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:NAMECOIN_TEST); $encodedBytes = [System.Convert]::FromBase64String($env:NAMECOIN_TEST);
Set-Content $secretFileNamecoin -Value $encodedBytes -AsByteStream; Set-Content $secretFileNamecoin -Value $encodedBytes -AsByteStream;
$secretFileNamecoinHash = Get-FileHash $secretFileNamecoin; $secretFileNamecoinHash = Get-FileHash $secretFileNamecoin;
Write-Output "::set-output name=SECRET_FILE_NAMECOIN::$secretFileNamecoin";
Write-Output "::set-output name=SECRET_FILE_NAMECOIN_HASH::$($secretFileNamecoinHash.Hash)";
Write-Output "Secret file $secretFileNamecoin has hash $($secretFileNamecoinHash.Hash)"; Write-Output "Secret file $secretFileNamecoin has hash $($secretFileNamecoinHash.Hash)";
shell: pwsh shell: pwsh
@ -114,18 +102,18 @@ jobs:
file: coverage/lcov.info file: coverage/lcov.info
- name: Delete temp files - name: Delete temp files
run: | run: |
Remove-Item -Path $env:CHANGE_NOW; $secretFileExchange = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "lib/external_api_keys.dart";
Remove-Item -Path $env:BITCOIN_TEST; $secretFileBitcoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart";
Remove-Item -Path $env:DOGECOIN_TEST; $secretFileDogecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart";
Remove-Item -Path $env:FIRO_TEST; $secretFileFiro = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/firo/firo_wallet_test_parameters.dart";
Remove-Item -Path $env:BITCOINCASH_TEST; $secretFileBitcoinCash = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart";
Remove-Item -Path $env:NAMECOIN_TEST; $secretFileNamecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/namecoin/namecoin_wallet_test_parameters.dart";
Remove-Item -Path $secretFileExchange;
Remove-Item -Path $secretFileBitcoin;
Remove-Item -Path $secretFileDogecoin;
Remove-Item -Path $secretFileFiro;
Remove-Item -Path $secretFileBitcoinCash;
Remove-Item -Path $secretFileNamecoin;
shell: pwsh shell: pwsh
if: always() if: always()
env:
CHANGE_NOW: ${{ steps.secret-file1.outputs.SECRET_FILE_EXCHANGE }}
BITCOIN_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_BITCOIN }}
DOGECOIN_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_DOGECOIN }}
FIRO_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_FIRO }}
BITCOINCASH_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_BITCOINCASH }}
NAMECOIN_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_NAMECOIN }}

View file

@ -20,6 +20,7 @@
<application <application
android:name="${applicationName}" android:name="${applicationName}"
android:label="Stack Wallet" android:label="Stack Wallet"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="false" android:allowBackup="false"
android:fullBackupContent="false"> android:fullBackupContent="false">

View file

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.7.20'
repositories { repositories {
google() google()
jcenter() jcenter()

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip

View file

@ -454,7 +454,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 78; CURRENT_PROJECT_VERSION = 79;
DEVELOPMENT_TEAM = 4DQKUWSG6C; DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -508,7 +508,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
); );
MARKETING_VERSION = 1.5.8; MARKETING_VERSION = 1.5.9;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -641,7 +641,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 78; CURRENT_PROJECT_VERSION = 79;
DEVELOPMENT_TEAM = 4DQKUWSG6C; DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -695,7 +695,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
); );
MARKETING_VERSION = 1.5.8; MARKETING_VERSION = 1.5.9;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -720,7 +720,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 78; CURRENT_PROJECT_VERSION = 79;
DEVELOPMENT_TEAM = 4DQKUWSG6C; DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -774,7 +774,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
); );
MARKETING_VERSION = 1.5.8; MARKETING_VERSION = 1.5.9;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -1,9 +1,12 @@
import 'dart:convert';
import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:string_validator/string_validator.dart';
class CachedElectrumX { class CachedElectrumX {
final ElectrumX? electrumXClient; final ElectrumX? electrumXClient;
@ -94,10 +97,32 @@ class CachedElectrumX {
// update set with new data // update set with new data
if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) { if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) {
set["setHash"] = newSet["setHash"]; set["setHash"] = !isHexadecimal(newSet["setHash"] as String)
set["blockHash"] = newSet["blockHash"]; ? base64ToReverseHex(newSet["setHash"] as String)
: newSet["setHash"];
set["blockHash"] = !isHexadecimal(newSet["blockHash"] as String)
? base64ToHex(newSet["blockHash"] as String)
: newSet["blockHash"];
for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) { for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) {
set["coins"].insert(0, newSet["coins"][i]); dynamic newCoin = newSet["coins"][i];
List translatedCoin = [];
translatedCoin.add(!isHexadecimal(newCoin[0] as String)
? base64ToHex(newCoin[0] as String)
: newCoin[0]);
translatedCoin.add(!isHexadecimal(newCoin[1] as String)
? base64ToReverseHex(newCoin[1] as String)
: newCoin[1]);
try {
translatedCoin.add(!isHexadecimal(newCoin[2] as String)
? base64ToHex(newCoin[2] as String)
: newCoin[2]);
} catch (e, s) {
translatedCoin.add(newCoin[2]);
}
translatedCoin.add(!isHexadecimal(newCoin[3] as String)
? base64ToReverseHex(newCoin[3] as String)
: newCoin[3]);
set["coins"].insert(0, translatedCoin);
} }
// save set to db // save set to db
await DB.instance.put<dynamic>( await DB.instance.put<dynamic>(
@ -118,6 +143,17 @@ class CachedElectrumX {
} }
} }
String base64ToHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
String base64ToReverseHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.reversed
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
/// Call electrumx getTransaction on a per coin basis, storing the result in local db if not already there. /// Call electrumx getTransaction on a per coin basis, storing the result in local db if not already there.
/// ///
/// ElectrumX api only called if the tx does not exist in local db /// ElectrumX api only called if the tx does not exist in local db
@ -189,7 +225,15 @@ class CachedElectrumX {
); );
final serials = await client.getUsedCoinSerials(startNumber: startNumber); final serials = await client.getUsedCoinSerials(startNumber: startNumber);
cachedSerials.addAll(serials["serials"] as List); List newSerials = [];
for (var element in (serials["serials"] as List)) {
if (!isHexadecimal(element as String)) {
newSerials.add(base64ToHex(element));
} else {
newSerials.add(element);
}
}
cachedSerials.addAll(newSerials);
await DB.instance.put<dynamic>( await DB.instance.put<dynamic>(
boxName: DB.instance.boxNameUsedSerialsCache(coin: coin), boxName: DB.instance.boxNameUsedSerialsCache(coin: coin),

View file

@ -9,6 +9,7 @@ import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/models/notification_model.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/models/trade_wallet_lookup.dart';
import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/services/wallets_service.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
@ -142,6 +143,17 @@ class DB {
_loadSharedCoinCacheBoxes(), _loadSharedCoinCacheBoxes(),
]); ]);
_initialized = true; _initialized = true;
try {
if (_boxPrefs.get("familiarity") == null) {
await _boxPrefs.put("familiarity", 0);
}
int count = _boxPrefs.get("familiarity") as int;
await _boxPrefs.put("familiarity", count + 1);
Constants.exchangeForExperiencedUsers(count + 1);
} catch (e, s) {
print("$e $s");
}
} }
} }

View file

@ -146,7 +146,12 @@ void main() async {
boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ??
0; 0;
if (dbVersion < Constants.currentHiveDbVersion) { if (dbVersion < Constants.currentHiveDbVersion) {
await DbVersionMigrator().migrate(dbVersion); try {
await DbVersionMigrator().migrate(dbVersion);
} catch (e, s) {
Logging.instance.log("Cannot migrate database\n$e $s",
level: LogLevel.Error, printFullLength: true);
}
} }
monero.onStartup(); monero.onStartup();
@ -234,7 +239,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
// unawaited(_nodeService.updateCommunityNodes()); // unawaited(_nodeService.updateCommunityNodes());
// run without awaiting // run without awaiting
if (Constants.enableExchange && _prefs.externalCalls) { if (Constants.enableExchange &&
_prefs.externalCalls &&
await _prefs.isExternalCallsSet()) {
unawaited(ExchangeDataLoadingService().loadAll(ref)); unawaited(ExchangeDataLoadingService().loadAll(ref));
} }

View file

@ -276,6 +276,12 @@ class ExchangeFormState extends ChangeNotifier {
void _onExchangeRateTypeChanged() { void _onExchangeRateTypeChanged() {
print("_onExchangeRateTypeChanged"); print("_onExchangeRateTypeChanged");
updateRanges(shouldNotifyListeners: true).then(
(_) => updateEstimate(
shouldNotifyListeners: true,
reversed: reversed,
),
);
} }
void _onExchangeTypeChanged() { void _onExchangeTypeChanged() {

View file

@ -144,6 +144,8 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
Future<void> chooseDate() async { Future<void> chooseDate() async {
final height = MediaQuery.of(context).size.height; final height = MediaQuery.of(context).size.height;
final fetchedColor =
Theme.of(context).extension<StackColors>()!.accentColorDark;
// check and hide keyboard // check and hide keyboard
if (FocusScope.of(context).hasFocus) { if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@ -155,8 +157,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
initialDate: DateTime.now(), initialDate: DateTime.now(),
height: height * 0.5, height: height * 0.5,
theme: ThemeData( theme: ThemeData(
primarySwatch: Util.createMaterialColor( primarySwatch: Util.createMaterialColor(fetchedColor),
Theme.of(context).extension<StackColors>()!.accentColorDark),
), ),
//TODO pick a better initial date //TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched // 2007 chosen as that is just before bitcoin launched
@ -272,6 +273,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
// if (!isDesktop) // if (!isDesktop)
RestoreFromDatePicker( RestoreFromDatePicker(
onTap: chooseDate, onTap: chooseDate,
controller: _dateController,
), ),
// if (isDesktop) // if (isDesktop)

View file

@ -7,10 +7,14 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
class RestoreFromDatePicker extends StatefulWidget { class RestoreFromDatePicker extends StatefulWidget {
const RestoreFromDatePicker({Key? key, required this.onTap}) const RestoreFromDatePicker({
: super(key: key); Key? key,
required this.onTap,
required this.controller,
}) : super(key: key);
final VoidCallback onTap; final VoidCallback onTap;
final TextEditingController controller;
@override @override
State<RestoreFromDatePicker> createState() => _RestoreFromDatePickerState(); State<RestoreFromDatePicker> createState() => _RestoreFromDatePickerState();
@ -23,17 +27,11 @@ class _RestoreFromDatePickerState extends State<RestoreFromDatePicker> {
@override @override
void initState() { void initState() {
onTap = widget.onTap; onTap = widget.onTap;
_dateController = TextEditingController(); _dateController = widget.controller;
super.initState(); super.initState();
} }
@override
void dispose() {
_dateController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(

View file

@ -1,11 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:stackwallet/models/isar/models/log.dart'; import 'package:stackwallet/models/isar/models/log.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart';
@ -24,6 +28,13 @@ import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
@ -272,21 +283,77 @@ class _DebugViewState extends ConsumerState<DebugView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// BlueTextButton( BlueTextButton(
// text: ref.watch(debugServiceProvider text: "Save Debug Info to clipboard",
// .select((value) => value.isPaused)) onTap: () async {
// ? "Unpause" try {
// : "Pause", final packageInfo =
// onTap: () { await PackageInfo.fromPlatform();
// ref final version = packageInfo.version;
// .read(debugServiceProvider) final build = packageInfo.buildNumber;
// .togglePauseUiUpdates(); final signature = packageInfo.buildSignature;
// }, final appName = packageInfo.appName;
// ), String firoCommit =
FIRO_VERSIONS.getPluginVersion();
String epicCashCommit =
EPIC_VERSIONS.getPluginVersion();
String moneroCommit =
MONERO_VERSIONS.getPluginVersion();
DeviceInfoPlugin deviceInfoPlugin =
DeviceInfoPlugin();
final deviceInfo =
await deviceInfoPlugin.deviceInfo;
var deviceInfoMap = deviceInfo.toMap();
deviceInfoMap.remove("systemFeatures");
final logs = filtered(
ref.watch(debugServiceProvider.select(
(value) => value.recentLogs)),
_searchTerm)
.reversed
.toList(growable: false);
List errorLogs = [];
for (var log in logs) {
if (log.logLevel == LogLevel.Error ||
log.logLevel == LogLevel.Fatal) {
errorLogs.add(
"${log.logLevel}: ${log.message}");
}
}
final finalDebugMap = {
"version": version,
"build": build,
"signature": signature,
"appName": appName,
"firoCommit": firoCommit,
"epicCashCommit": epicCashCommit,
"moneroCommit": moneroCommit,
"deviceInfoMap": deviceInfoMap,
"errorLogs": errorLogs,
};
Logging.instance.log(
json.encode(finalDebugMap),
level: LogLevel.Info,
printFullLength: true);
const ClipboardInterface clipboard =
ClipboardWrapper();
await clipboard.setData(
ClipboardData(
text: json.encode(finalDebugMap)),
);
} catch (e, s) {
Logging.instance
.log("$e $s", level: LogLevel.Error);
}
},
),
const Spacer(), const Spacer(),
BlueTextButton( BlueTextButton(
text: "Save logs to file", text: "Save logs to file",
onTap: () async { onTap: () async {
final systemfile = StackFileSystem();
await systemfile.prepareStorage();
Directory rootPath = Directory rootPath =
(await getApplicationDocumentsDirectory()); (await getApplicationDocumentsDirectory());
@ -313,8 +380,9 @@ class _DebugViewState extends ConsumerState<DebugView> {
} else { } else {
path = await FilePicker.platform path = await FilePicker.platform
.getDirectoryPath( .getDirectoryPath(
dialogTitle: "Choose Backup location", dialogTitle: "Choose Log Save Location",
initialDirectory: dir.path, initialDirectory:
systemfile.startPath!.path,
lockParentWindow: true, lockParentWindow: true,
); );
} }
@ -336,9 +404,17 @@ class _DebugViewState extends ConsumerState<DebugView> {
), ),
)); ));
final filename = await ref bool logssaved = true;
.read(debugServiceProvider) var filename;
.exportToFile(path, eventBus); try {
filename = await ref
.read(debugServiceProvider)
.exportToFile(path, eventBus);
} catch (e, s) {
logssaved = false;
Logging.instance
.log("$e $s", level: LogLevel.Error);
}
shouldPop = true; shouldPop = true;
@ -350,7 +426,9 @@ class _DebugViewState extends ConsumerState<DebugView> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => StackOkDialog( builder: (context) => StackOkDialog(
title: "Logs saved to", title: logssaved
? "Logs saved to"
: "Error Saving Logs",
message: "${path!}/$filename", message: "${path!}/$filename",
), ),
), ),
@ -360,7 +438,9 @@ class _DebugViewState extends ConsumerState<DebugView> {
showFloatingFlushBar( showFloatingFlushBar(
type: FlushBarType.info, type: FlushBarType.info,
context: context, context: context,
message: 'Logs file saved', message: logssaved
? 'Logs file saved'
: "Error Saving Logs",
), ),
); );
} }

View file

@ -687,27 +687,14 @@ abstract class SWB {
uiState?.walletStates = walletStates; uiState?.walletStates = walletStates;
List<Future<bool>> restoreStatuses = []; List<Future<bool>> restoreStatuses = [];
final List<Tuple2<dynamic, Manager>> firoWallets = [];
final List<Tuple2<dynamic, Manager>> firoTestnetWallets = [];
final List<Tuple2<dynamic, Manager>> epicCashWallets = [];
// start restoring wallets // start restoring wallets
for (final tuple in managers) { for (final tuple in managers) {
// check if cancel was requested and restore previous state // check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) { if (_checkShouldCancel(preRestoreState)) {
return false; return false;
} }
final bools = await asyncRestore(tuple, uiState, walletsService);
if (tuple.item2.coin == Coin.firoTestNet) { restoreStatuses.add(Future(() => bools));
firoTestnetWallets.add(tuple);
continue;
} else if (tuple.item2.coin == Coin.firo) {
firoWallets.add(tuple);
continue;
} else if (tuple.item2.coin == Coin.epicCash) {
epicCashWallets.add(tuple);
continue;
}
restoreStatuses.add(asyncRestore(tuple, uiState, walletsService));
} }
// check if cancel was requested and restore previous state // check if cancel was requested and restore previous state
@ -715,153 +702,6 @@ abstract class SWB {
return false; return false;
} }
if (firoTestnetWallets.isNotEmpty) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
for (final wallet in firoTestnetWallets) {
uiState?.update(
walletId: wallet.item2.walletId,
restoringStatus: StackRestoringStatus.restoring,
);
}
// try using node from backup first
NodeModel node = nodeService.getPrimaryNodeFor(coin: Coin.firoTestNet) ??
DefaultNodes.getNodeFor(Coin.firoTestNet);
final electrumxNode = ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
);
final failovers = nodeService.failoverNodesFor(coin: Coin.firoTestNet);
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
final cachedClient = CachedElectrumX.from(
node: electrumxNode,
prefs: _prefs,
failovers: failovers
.map(
(e) => ElectrumXNode(
address: e.host,
port: e.port,
name: e.name,
id: e.id,
useSSL: e.useSSL,
),
)
.toList(),
);
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
// Anonymity Set often fails when gathering from the server
const int maxTries = 5;
for (int j = 0; j < maxTries; j++) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
try {
await cachedClient.getAnonymitySet(
groupId: "1",
coin: Coin.firoTestNet,
);
break;
} catch (_) {
continue;
}
}
}
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
if (firoWallets.isNotEmpty) {
for (final wallet in firoWallets) {
uiState?.update(
walletId: wallet.item2.walletId,
restoringStatus: StackRestoringStatus.restoring,
);
}
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
// try using node from backup first
NodeModel node = nodeService.getPrimaryNodeFor(coin: Coin.firo) ??
DefaultNodes.getNodeFor(Coin.firo);
final electrumxNode = ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
);
final failovers = nodeService.failoverNodesFor(coin: Coin.firoTestNet);
final cachedClient = CachedElectrumX.from(
node: electrumxNode,
prefs: _prefs,
failovers: failovers
.map(
(e) => ElectrumXNode(
address: e.host,
port: e.port,
name: e.name,
id: e.id,
useSSL: e.useSSL,
),
)
.toList());
// Anonymity Set often fails when gathering from the server
const int maxTries = 5;
for (int j = 0; j < maxTries; j++) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
try {
await cachedClient.getAnonymitySet(
groupId: "1",
coin: Coin.firo,
);
break;
} catch (_) {
continue;
}
}
}
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
for (final tuple in firoTestnetWallets) {
restoreStatuses.add(asyncRestore(tuple, uiState, walletsService));
}
for (final tuple in firoWallets) {
restoreStatuses.add(asyncRestore(tuple, uiState, walletsService));
}
for (Future<bool> status in restoreStatuses) { for (Future<bool> status in restoreStatuses) {
// check if cancel was requested and restore previous state // check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) { if (_checkShouldCancel(preRestoreState)) {
@ -869,13 +709,7 @@ abstract class SWB {
} }
await status; await status;
} }
for (int i = 0; i < epicCashWallets.length; i++) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
await asyncRestore(epicCashWallets[i], uiState, walletsService);
}
if (!Platform.isLinux) await Wakelock.disable(); if (!Platform.isLinux) await Wakelock.disable();
// check if cancel was requested and restore previous state // check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) { if (_checkShouldCancel(preRestoreState)) {

View file

@ -445,7 +445,7 @@ class _StackRestoreProgressViewState
}, },
style: Theme.of(context) style: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.getSecondaryEnabledButtonColor(context), .getPrimaryEnabledButtonColor(context),
child: Text( child: Text(
_success ? "OK" : "Cancel restore process", _success ? "OK" : "Cancel restore process",
style: STextStyles.button(context).copyWith( style: STextStyles.button(context).copyWith(

View file

@ -43,7 +43,7 @@ import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
const int MINIMUM_CONFIRMATIONS = 3; const int MINIMUM_CONFIRMATIONS = 1;
const int DUST_LIMIT = 546; const int DUST_LIMIT = 546;
const String GENESIS_HASH_MAINNET = const String GENESIS_HASH_MAINNET =
@ -265,6 +265,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
DerivePathType addressType({required String address}) { DerivePathType addressType({required String address}) {
Uint8List? decodeBase58; Uint8List? decodeBase58;
Segwit? decodeBech32; Segwit? decodeBech32;
try {
if (Bitbox.Address.detectFormat(address) == 0) {
address = Bitbox.Address.toLegacyAddress(address);
}
} catch (e, s) {}
try { try {
decodeBase58 = bs58check.decode(address); decodeBase58 = bs58check.decode(address);
} catch (err) { } catch (err) {
@ -825,9 +830,6 @@ class BitcoinCashWallet extends CoinServiceAPI {
/// Refreshes display data for the wallet /// Refreshes display data for the wallet
@override @override
Future<void> refresh() async { Future<void> refresh() async {
final bchaddr = Bitbox.Address.toCashAddress(await currentReceivingAddress);
print("bchaddr: $bchaddr ${await currentReceivingAddress}");
if (refreshMutex) { if (refreshMutex) {
Logging.instance.log("$walletId $walletName refreshMutex denied", Logging.instance.log("$walletId $walletName refreshMutex denied",
level: LogLevel.Info); level: LogLevel.Info);
@ -1384,7 +1386,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
initialChangeAddressP2SH, 1, DerivePathType.bip49); initialChangeAddressP2SH, 1, DerivePathType.bip49);
// this._currentReceivingAddress = Future(() => initialReceivingAddress); // this._currentReceivingAddress = Future(() => initialReceivingAddress);
_currentReceivingAddressP2PKH = Future(() => initialReceivingAddressP2PKH);
var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44);
_currentReceivingAddressP2PKH = Future(() => newaddr);
_currentReceivingAddressP2SH = Future(() => initialReceivingAddressP2SH); _currentReceivingAddressP2SH = Future(() => initialReceivingAddressP2SH);
Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
@ -1521,6 +1525,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
print("Array key is ${jsonEncode(arrayKey)}"); print("Array key is ${jsonEncode(arrayKey)}");
final internalChainArray = final internalChainArray =
DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); DB.instance.get<dynamic>(boxName: walletId, key: arrayKey);
if (derivePathType == DerivePathType.bip44) {
if (Bitbox.Address.detectFormat(internalChainArray.last as String) == 1) {
return Bitbox.Address.toCashAddress(internalChainArray.last as String);
}
}
return internalChainArray.last as String; return internalChainArray.last as String;
} }
@ -1986,6 +1995,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
/// Returns the scripthash or throws an exception on invalid bch address /// Returns the scripthash or throws an exception on invalid bch address
String _convertToScriptHash(String bchAddress, NetworkType network) { String _convertToScriptHash(String bchAddress, NetworkType network) {
try { try {
if (Bitbox.Address.detectFormat(bchAddress) == 0) {
bchAddress = Bitbox.Address.toLegacyAddress(bchAddress);
}
final output = Address.addressToOutputScript(bchAddress, network); final output = Address.addressToOutputScript(bchAddress, network);
final hash = sha256.convert(output.toList(growable: false)).toString(); final hash = sha256.convert(output.toList(growable: false)).toString();
@ -2058,11 +2070,27 @@ class BitcoinCashWallet extends CoinServiceAPI {
} }
Future<TransactionData> _fetchTransactionData() async { Future<TransactionData> _fetchTransactionData() async {
final List<String> allAddresses = await _fetchAllOwnAddresses(); List<String> allAddressesOld = await _fetchAllOwnAddresses();
List<String> allAddresses = [];
for (String address in allAddressesOld) {
if (Bitbox.Address.detectFormat(address) == 1) {
allAddresses.add(Bitbox.Address.toCashAddress(address));
} else {
allAddresses.add(address);
}
}
final changeAddressesP2PKH = var changeAddressesP2PKHOld =
DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH') DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH')
as List<dynamic>; as List<dynamic>;
List<dynamic> changeAddressesP2PKH = [];
for (var address in changeAddressesP2PKHOld) {
if (Bitbox.Address.detectFormat(address as String) == 1) {
changeAddressesP2PKH.add(Bitbox.Address.toCashAddress(address));
} else {
changeAddressesP2PKH.add(address);
}
}
final List<Map<String, dynamic>> allTxHashes = final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory(allAddresses); await _fetchHistory(allAddresses);
@ -2087,7 +2115,16 @@ class BitcoinCashWallet extends CoinServiceAPI {
if (txHeight > 0 && if (txHeight > 0 &&
txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
allTxHashes.remove(tx); print(cachedTransactions.findTransaction(tx["tx_hash"] as String));
print(unconfirmedCachedTransactions[tx["tx_hash"] as String]);
final cachedTx =
cachedTransactions.findTransaction(tx["tx_hash"] as String);
if (!(cachedTx != null &&
addressType(address: cachedTx.address) ==
DerivePathType.bip44 &&
Bitbox.Address.detectFormat(cachedTx.address) == 1)) {
allTxHashes.remove(tx);
}
} }
} }
} }
@ -2096,7 +2133,6 @@ class BitcoinCashWallet extends CoinServiceAPI {
List<Map<String, dynamic>> allTransactions = []; List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) { for (final txHash in allTxHashes) {
Logging.instance.log("bch: $txHash", level: LogLevel.Info);
final tx = await cachedElectrumXClient.getTransaction( final tx = await cachedElectrumXClient.getTransaction(
txHash: txHash["tx_hash"] as String, txHash: txHash["tx_hash"] as String,
verbose: true, verbose: true,
@ -2166,7 +2202,8 @@ class BitcoinCashWallet extends CoinServiceAPI {
.log("recipientsArray: $recipientsArray", level: LogLevel.Info); .log("recipientsArray: $recipientsArray", level: LogLevel.Info);
final foundInSenders = final foundInSenders =
allAddresses.any((element) => sendersArray.contains(element)); allAddresses.any((element) => sendersArray.contains(element)) ||
allAddressesOld.any((element) => sendersArray.contains(element));
Logging.instance Logging.instance
.log("foundInSenders: $foundInSenders", level: LogLevel.Info); .log("foundInSenders: $foundInSenders", level: LogLevel.Info);
@ -2228,7 +2265,8 @@ class BitcoinCashWallet extends CoinServiceAPI {
.toBigInt() .toBigInt()
.toInt(); .toInt();
totalOut += value; totalOut += value;
if (allAddresses.contains(address)) { if (allAddresses.contains(address) ||
allAddressesOld.contains(address)) {
outputAmtAddressedToWallet += value; outputAmtAddressedToWallet += value;
} }
} }
@ -2743,7 +2781,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String; String address = output["scriptPubKey"]["addresses"][0] as String;
if (Bitbox.Address.detectFormat(address) == 0) {
address = Bitbox.Address.toLegacyAddress(address);
}
if (!addressTxid.containsKey(address)) { if (!addressTxid.containsKey(address)) {
addressTxid[address] = <String>[]; addressTxid[address] = <String>[];
} }
@ -2772,8 +2813,13 @@ class BitcoinCashWallet extends CoinServiceAPI {
derivePathType: DerivePathType.bip44, derivePathType: DerivePathType.bip44,
); );
for (int i = 0; i < p2pkhLength; i++) { for (int i = 0; i < p2pkhLength; i++) {
String address = addressesP2PKH[i];
if (Bitbox.Address.detectFormat(address) == 0) {
address = Bitbox.Address.toLegacyAddress(address);
}
// receives // receives
final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; final receiveDerivation = receiveDerivations[address];
// if a match exists it will not be null // if a match exists it will not be null
if (receiveDerivation != null) { if (receiveDerivation != null) {
final data = P2PKH( final data = P2PKH(
@ -2783,7 +2829,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
network: _network, network: _network,
).data; ).data;
for (String tx in addressTxid[addressesP2PKH[i]]!) { for (String tx in addressTxid[address]!) {
results[tx] = { results[tx] = {
"output": data.output, "output": data.output,
"keyPair": ECPair.fromWIF( "keyPair": ECPair.fromWIF(
@ -2794,7 +2840,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
} }
} else { } else {
// if its not a receive, check change // if its not a receive, check change
final changeDerivation = changeDerivations[addressesP2PKH[i]]; final changeDerivation = changeDerivations[address];
// if a match exists it will not be null // if a match exists it will not be null
if (changeDerivation != null) { if (changeDerivation != null) {
final data = P2PKH( final data = P2PKH(
@ -2804,7 +2850,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
network: _network, network: _network,
).data; ).data;
for (String tx in addressTxid[addressesP2PKH[i]]!) { for (String tx in addressTxid[address]!) {
results[tx] = { results[tx] = {
"output": data.output, "output": data.output,
"keyPair": ECPair.fromWIF( "keyPair": ECPair.fromWIF(
@ -3377,8 +3423,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
0, 0,
DerivePathType DerivePathType
.bip44); // Add that new receiving address to the array of receiving addresses .bip44); // Add that new receiving address to the array of receiving addresses
_currentReceivingAddressP2PKH = Future(() => var newaddr = await _getCurrentAddressForChain(0, DerivePathType.bip44);
newReceivingAddress); // Set the new receiving address that the service _currentReceivingAddressP2PKH = Future(
() => newaddr); // Set the new receiving address that the service
return true; return true;
} catch (e, s) { } catch (e, s) {

View file

@ -1990,7 +1990,6 @@ class EpicCashWallet extends CoinServiceAPI {
Future<bool> refreshIfThereIsNewData() async { Future<bool> refreshIfThereIsNewData() async {
if (_hasCalledExit) return false; if (_hasCalledExit) return false;
Logging.instance.log("Can we do this here?", level: LogLevel.Fatal);
// TODO returning true here signals this class to call refresh() after which it will fire an event that notifies the UI that new data has been fetched/found for this wallet // TODO returning true here signals this class to call refresh() after which it will fire an event that notifies the UI that new data has been fetched/found for this wallet
return true; return true;
// TODO: do a quick check to see if there is any new data that would require a refresh // TODO: do a quick check to see if there is any new data that would require a refresh

View file

@ -39,6 +39,19 @@ class NodeService extends ChangeNotifier {
key: savedNode.id, key: savedNode.id,
value: defaultNode.copyWith(enabled: savedNode.enabled)); value: defaultNode.copyWith(enabled: savedNode.enabled));
} }
// check if a default node is the primary node for the crypto currency
// and update it if needed
final coin = coinFromPrettyName(defaultNode.coinName);
final primaryNode = getPrimaryNodeFor(coin: coin);
if (primaryNode != null && primaryNode.id == defaultNode.id) {
await setPrimaryNodeFor(
coin: coin,
node: defaultNode.copyWith(
enabled: primaryNode.enabled,
),
);
}
} }
} }

View file

@ -78,12 +78,12 @@ class PriceAPI {
} }
final externalCalls = Prefs.instance.externalCalls; final externalCalls = Prefs.instance.externalCalls;
if (!Logger.isTestEnv && !externalCalls) { if ((!Logger.isTestEnv && !externalCalls) ||
!(await Prefs.instance.isExternalCallsSet())) {
Logging.instance.log("User does not want to use external calls", Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info); level: LogLevel.Info);
return _cachedPrices; return _cachedPrices;
} }
Map<Coin, Tuple2<Decimal, double>> result = {}; Map<Coin, Tuple2<Decimal, double>> result = {};
try { try {
final uri = Uri.parse( final uri = Uri.parse(
@ -123,7 +123,8 @@ class PriceAPI {
static Future<List<String>?> availableBaseCurrencies() async { static Future<List<String>?> availableBaseCurrencies() async {
final externalCalls = Prefs.instance.externalCalls; final externalCalls = Prefs.instance.externalCalls;
if (!Logger.isTestEnv && !externalCalls) { if ((!Logger.isTestEnv && !externalCalls) ||
!(await Prefs.instance.isExternalCallsSet())) {
Logging.instance.log("User does not want to use external calls", Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info); level: LogLevel.Info);
return null; return null;

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/util.dart';
class _LayoutSizing { class _LayoutSizing {
const _LayoutSizing(); const _LayoutSizing();
@ -14,7 +15,12 @@ class _LayoutSizing {
abstract class Constants { abstract class Constants {
static const size = _LayoutSizing(); static const size = _LayoutSizing();
static final bool enableExchange = !Platform.isIOS; static void exchangeForExperiencedUsers(int count) {
enableExchange =
Util.isDesktop || Platform.isAndroid || count > 5 || !Platform.isIOS;
}
static bool enableExchange = Util.isDesktop || !Platform.isIOS;
//TODO: correct for monero? //TODO: correct for monero?
static const int satsPerCoinMonero = 1000000000000; static const int satsPerCoinMonero = 1000000000000;
@ -36,7 +42,7 @@ abstract class Constants {
// Enable Logger.print statements // Enable Logger.print statements
static const bool disableLogger = false; static const bool disableLogger = false;
static const int currentHiveDbVersion = 2; static const int currentHiveDbVersion = 3;
static List<int> possibleLengthsForCoin(Coin coin) { static List<int> possibleLengthsForCoin(Coin coin) {
final List<int> values = []; final List<int> values = [];

View file

@ -143,6 +143,18 @@ class DbVersionMigrator {
// try to continue migrating // try to continue migrating
return await migrate(2); return await migrate(2);
case 2:
await Hive.openBox<dynamic>(DB.boxNamePrefs);
final prefs = Prefs.instance;
await prefs.init();
if (!(await prefs.isExternalCallsSet())) {
prefs.externalCalls = true;
}
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 3);
return await migrate(3);
default: default:
// finally return // finally return

View file

@ -70,28 +70,24 @@ abstract class DefaultNodes {
isDown: false, isDown: false,
); );
// TODO: eventually enable ssl and set scheme to https
// currently get certificate failure
static NodeModel get monero => NodeModel( static NodeModel get monero => NodeModel(
host: "http://monero.stackwallet.com", host: "https://monero.stackwallet.com",
port: 18081, port: 18081,
name: defaultName, name: defaultName,
id: _nodeId(Coin.monero), id: _nodeId(Coin.monero),
useSSL: false, useSSL: true,
enabled: true, enabled: true,
coinName: Coin.monero.name, coinName: Coin.monero.name,
isFailover: true, isFailover: true,
isDown: false, isDown: false,
); );
// TODO: eventually enable ssl and set scheme to https
// currently get certificate failure
static NodeModel get wownero => NodeModel( static NodeModel get wownero => NodeModel(
host: "http://eu-west-2.wow.xmr.pm", host: "https://wownero.stackwallet.com",
port: 34568, port: 34568,
name: defaultName, name: defaultName,
id: _nodeId(Coin.wownero), id: _nodeId(Coin.wownero),
useSSL: false, useSSL: true,
enabled: true, enabled: true,
coinName: Coin.wownero.name, coinName: Coin.wownero.name,
isFailover: true, isFailover: true,

View file

@ -181,25 +181,32 @@ Coin coinFromPrettyName(String name) {
case "Bitcoin": case "Bitcoin":
case "bitcoin": case "bitcoin":
return Coin.bitcoin; return Coin.bitcoin;
case "Bitcoincash": case "Bitcoincash":
case "bitcoincash": case "bitcoincash":
case "Bitcoin Cash": case "Bitcoin Cash":
return Coin.bitcoincash; return Coin.bitcoincash;
case "Dogecoin": case "Dogecoin":
case "dogecoin": case "dogecoin":
return Coin.dogecoin; return Coin.dogecoin;
case "Epic Cash": case "Epic Cash":
case "epicCash": case "epicCash":
return Coin.epicCash; return Coin.epicCash;
case "Firo": case "Firo":
case "firo": case "firo":
return Coin.firo; return Coin.firo;
case "Monero": case "Monero":
case "monero": case "monero":
return Coin.monero; return Coin.monero;
case "Namecoin": case "Namecoin":
case "namecoin": case "namecoin":
return Coin.namecoin; return Coin.namecoin;
case "Bitcoin Testnet": case "Bitcoin Testnet":
case "tBitcoin": case "tBitcoin":
case "bitcoinTestNet": case "bitcoinTestNet":
@ -208,19 +215,24 @@ Coin coinFromPrettyName(String name) {
case "Bitcoincash Testnet": case "Bitcoincash Testnet":
case "tBitcoin Cash": case "tBitcoin Cash":
case "Bitcoin Cash Testnet": case "Bitcoin Cash Testnet":
case "bitcoincashTestnet":
return Coin.bitcoincashTestnet; return Coin.bitcoincashTestnet;
case "Firo Testnet": case "Firo Testnet":
case "tFiro": case "tFiro":
case "firoTestNet": case "firoTestNet":
return Coin.firoTestNet; return Coin.firoTestNet;
case "Dogecoin Testnet": case "Dogecoin Testnet":
case "tDogecoin": case "tDogecoin":
case "dogecoinTestNet": case "dogecoinTestNet":
return Coin.dogecoinTestNet; return Coin.dogecoinTestNet;
case "Wownero": case "Wownero":
case "tWownero": case "tWownero":
case "wownero": case "wownero":
return Coin.wownero; return Coin.wownero;
default: default:
throw ArgumentError.value( throw ArgumentError.value(
name, "name", "No Coin enum value with that prettyName"); name, "name", "No Coin enum value with that prettyName");

View file

@ -571,4 +571,13 @@ class Prefs extends ChangeNotifier {
boxName: DB.boxNamePrefs, key: "externalCalls") as bool? ?? boxName: DB.boxNamePrefs, key: "externalCalls") as bool? ??
true; true;
} }
Future<bool> isExternalCallsSet() async {
if (await DB.instance
.get<dynamic>(boxName: DB.boxNamePrefs, key: "externalCalls") ==
null) {
return false;
}
return true;
}
} }

View file

@ -306,10 +306,10 @@ class NodeOptionsSheet extends ConsumerWidget {
style: status == "Connected" style: status == "Connected"
? Theme.of(context) ? Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.getPrimaryEnabledButtonColor(context) .getPrimaryDisabledButtonColor(context)
: Theme.of(context) : Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.getPrimaryDisabledButtonColor(context), .getPrimaryEnabledButtonColor(context),
onPressed: status == "Connected" onPressed: status == "Connected"
? null ? null
: () async { : () async {

View file

@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import connectivity_plus_macos import connectivity_plus_macos
import device_info_plus
import devicelocale import devicelocale
import flutter_libepiccash import flutter_libepiccash
import flutter_local_notifications import flutter_local_notifications
@ -22,6 +23,7 @@ import window_size
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin"))
FlutterLibepiccashPlugin.register(with: registry.registrar(forPlugin: "FlutterLibepiccashPlugin")) FlutterLibepiccashPlugin.register(with: registry.registrar(forPlugin: "FlutterLibepiccashPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))

View file

@ -380,6 +380,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "7.0.1"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
devicelocale: devicelocale:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1396,7 +1410,14 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
string_validator:
dependency: "direct main"
description:
name: string_validator
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
sync_http: sync_http:
dependency: transitive dependency: transitive
description: description:

View file

@ -11,7 +11,7 @@ description: Stack Wallet
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.5.8+78 version: 1.5.9+79
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
@ -115,6 +115,7 @@ dependencies:
wakelock: ^0.6.2 wakelock: ^0.6.2
intl: ^0.17.0 intl: ^0.17.0
devicelocale: ^0.5.0 devicelocale: ^0.5.0
device_info_plus: ^7.0.1
keyboard_dismisser: ^3.0.0 keyboard_dismisser: ^3.0.0
another_flushbar: ^1.10.28 another_flushbar: ^1.10.28
tuple: ^2.0.0 tuple: ^2.0.0
@ -132,6 +133,7 @@ dependencies:
isar: 3.0.0-dev.10 isar: 3.0.0-dev.10
isar_flutter_libs: 3.0.0-dev.10 # contains the binaries isar_flutter_libs: 3.0.0-dev.10 # contains the binaries
dropdown_button2: 1.7.2 dropdown_button2: 1.7.2
string_validator: ^0.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/models/notification_model.dart';
import 'package:stackwallet/notifications/notification_card.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
void main() {
testWidgets("test notification card", (widgetTester) async {
final key = UniqueKey();
final notificationCard = NotificationCard(
key: key,
notification: NotificationModel(
id: 1,
title: "notification title",
description: "notification description",
iconAssetName: Assets.svg.iconFor(coin: Coin.bitcoin),
date: DateTime.parse("1662544771"),
walletId: "wallet id",
read: true,
shouldWatchForUpdates: true,
coinName: "Bitcoin"),
);
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: notificationCard,
),
),
);
expect(find.byWidget(notificationCard), findsOneWidget);
expect(find.text("notification title"), findsOneWidget);
expect(find.text("notification description"), findsOneWidget);
expect(find.byType(SvgPicture), findsOneWidget);
});
}

View file

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/locale_service.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/wallets.dart';
import 'package:stackwallet/services/wallets_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'send_view_test.mocks.dart';
@GenerateMocks([
Wallets,
WalletsService,
NodeService,
BitcoinWallet,
LocaleService,
Prefs,
], customMocks: [
MockSpec<Manager>(returnNullOnMissingStub: true),
MockSpec<CoinServiceAPI>(returnNullOnMissingStub: true),
])
void main() {
testWidgets("Send to valid address", (widgetTester) async {
final mockWallets = MockWallets();
final mockWalletsService = MockWalletsService();
final mockNodeService = MockNodeService();
final CoinServiceAPI wallet = MockBitcoinWallet();
final mockLocaleService = MockLocaleService();
final mockPrefs = MockPrefs();
when(wallet.coin).thenAnswer((_) => Coin.bitcoin);
when(wallet.walletName).thenAnswer((_) => "some wallet");
when(wallet.walletId).thenAnswer((_) => "wallet id");
final manager = Manager(wallet);
when(mockWallets.getManagerProvider("wallet id")).thenAnswer(
(realInvocation) => ChangeNotifierProvider((ref) => manager));
when(mockWallets.getManager("wallet id"))
.thenAnswer((realInvocation) => manager);
when(mockLocaleService.locale).thenAnswer((_) => "en_US");
when(mockPrefs.currency).thenAnswer((_) => "USD");
when(wallet.validateAddress("send to address"))
.thenAnswer((realInvocation) => true);
await widgetTester.pumpWidget(
ProviderScope(
overrides: [
walletsChangeNotifierProvider.overrideWithValue(mockWallets),
walletsServiceChangeNotifierProvider
.overrideWithValue(mockWalletsService),
nodeServiceChangeNotifierProvider.overrideWithValue(mockNodeService),
localeServiceChangeNotifierProvider
.overrideWithValue(mockLocaleService),
prefsChangeNotifierProvider.overrideWithValue(mockPrefs),
// previewTxButtonStateProvider
],
child: MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(
LightColors(),
),
],
),
home: SendView(
walletId: "wallet id",
coin: Coin.bitcoin,
autoFillData: SendViewAutoFillData(
address: "send to address", contactLabel: "contact label"),
),
),
),
);
expect(find.text("Send to"), findsOneWidget);
expect(find.text("Amount"), findsOneWidget);
expect(find.text("Note (optional)"), findsOneWidget);
expect(find.text("Transaction fee (estimated)"), findsOneWidget);
verify(manager.validateAddress("send to address")).called(1);
});
testWidgets("Send to invalid address", (widgetTester) async {
final mockWallets = MockWallets();
final mockWalletsService = MockWalletsService();
final mockNodeService = MockNodeService();
final CoinServiceAPI wallet = MockBitcoinWallet();
final mockLocaleService = MockLocaleService();
final mockPrefs = MockPrefs();
when(wallet.coin).thenAnswer((_) => Coin.bitcoin);
when(wallet.walletName).thenAnswer((_) => "some wallet");
when(wallet.walletId).thenAnswer((_) => "wallet id");
final manager = Manager(wallet);
when(mockWallets.getManagerProvider("wallet id")).thenAnswer(
(realInvocation) => ChangeNotifierProvider((ref) => manager));
when(mockWallets.getManager("wallet id"))
.thenAnswer((realInvocation) => manager);
when(mockLocaleService.locale).thenAnswer((_) => "en_US");
when(mockPrefs.currency).thenAnswer((_) => "USD");
when(wallet.validateAddress("send to address"))
.thenAnswer((realInvocation) => false);
// when(manager.isOwnAddress("send to address"))
// .thenAnswer((realInvocation) => Future(() => true));
await widgetTester.pumpWidget(
ProviderScope(
overrides: [
walletsChangeNotifierProvider.overrideWithValue(mockWallets),
walletsServiceChangeNotifierProvider
.overrideWithValue(mockWalletsService),
nodeServiceChangeNotifierProvider.overrideWithValue(mockNodeService),
localeServiceChangeNotifierProvider
.overrideWithValue(mockLocaleService),
prefsChangeNotifierProvider.overrideWithValue(mockPrefs),
// previewTxButtonStateProvider
],
child: MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(
LightColors(),
),
],
),
home: SendView(
walletId: "wallet id",
coin: Coin.bitcoin,
autoFillData: SendViewAutoFillData(
address: "send to address", contactLabel: "contact label"),
),
),
),
);
expect(find.text("Send to"), findsOneWidget);
expect(find.text("Amount"), findsOneWidget);
expect(find.text("Note (optional)"), findsOneWidget);
expect(find.text("Transaction fee (estimated)"), findsOneWidget);
expect(find.text("Invalid address"), findsOneWidget);
verify(manager.validateAddress("send to address")).called(1);
});
}

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,9 @@ void main() {
setUp(() async { setUp(() async {
await setUpTestHive(); await setUpTestHive();
await Hive.openBox<dynamic>(DB.boxNamePriceCache); await Hive.openBox<dynamic>(DB.boxNamePriceCache);
await Hive.openBox<dynamic>(DB.boxNamePrefs);
await DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs, key: "externalCalls", value: true);
}); });
test("getPricesAnd24hChange fetch", () async { test("getPricesAnd24hChange fetch", () async {

View file

@ -26,7 +26,7 @@ import 'bitcoincash_wallet_test_parameters.dart';
void main() { void main() {
group("bitcoincash constants", () { group("bitcoincash constants", () {
test("bitcoincash minimum confirmations", () async { test("bitcoincash minimum confirmations", () async {
expect(MINIMUM_CONFIRMATIONS, 3); expect(MINIMUM_CONFIRMATIONS, 1);
}); });
test("bitcoincash dust limit", () async { test("bitcoincash dust limit", () async {
expect(DUST_LIMIT, 546); expect(DUST_LIMIT, 546);
@ -831,18 +831,9 @@ void main() {
await bch?.initializeNew(); await bch?.initializeNew();
await bch?.initializeExisting(); await bch?.initializeExisting();
expect( expect(bch?.validateAddress(await bch!.currentReceivingAddress), true);
Address.validateAddress( expect(bch?.validateAddress(await bch!.currentReceivingAddress), true);
await bch!.currentReceivingAddress, bitcoincashtestnet), expect(bch?.validateAddress(await bch!.currentReceivingAddress), true);
true);
expect(
Address.validateAddress(
await bch!.currentReceivingAddress, bitcoincashtestnet),
true);
expect(
Address.validateAddress(
await bch!.currentReceivingAddress, bitcoincashtestnet),
true);
verifyNever(client?.ping()).called(0); verifyNever(client?.ping()).called(0);
verify(client?.getServerFeatures()).called(1); verify(client?.getServerFeatures()).called(1);
@ -884,8 +875,7 @@ void main() {
expect(addresses?.length, 2); expect(addresses?.length, 2);
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
expect( expect(bch?.validateAddress(addresses![i]), true);
Address.validateAddress(addresses![i], bitcoincashtestnet), true);
} }
verifyNever(client?.ping()).called(0); verifyNever(client?.ping()).called(0);

View file

@ -21,207 +21,46 @@ class MockedFunctions extends Mock {
@GenerateMocks([AddressBookService]) @GenerateMocks([AddressBookService])
void main() { void main() {
group('Navigation tests', () { testWidgets('test returns Contact Address Entry', (widgetTester) async {
late AddressBookService service; final service = MockAddressBookService();
setUp(() {
service = MockAddressBookService();
when(service.getContactById("some id")) when(service.getContactById("default"))
.thenAnswer((realInvocation) => Contact( .thenAnswer((realInvocation) => Contact(
name: "John Doe", name: "John Doe",
addresses: [ addresses: [
const ContactAddressEntry( const ContactAddressEntry(
coin: Coin.bitcoincash, coin: Coin.bitcoincash,
address: "some bch address", address: "some bch address",
label: "Bills") label: "Bills")
], ],
isFavorite: true)); isFavorite: true));
});
testWidgets('test returns Contact Address Entry', (widgetTester) async { await widgetTester.pumpWidget(
await widgetTester.pumpWidget( ProviderScope(
ProviderScope( overrides: [
overrides: [ addressBookServiceProvider.overrideWithValue(
addressBookServiceProvider.overrideWithValue( service,
service, ),
), ],
], child: MaterialApp(
child: MaterialApp( theme: ThemeData(
theme: ThemeData( extensions: [
extensions: [ StackColors.fromStackColorTheme(
StackColors.fromStackColorTheme( LightColors(),
LightColors(), ),
), ],
], ),
), home: const AddressBookCard(
home: const AddressBookCard( contactId: "default",
contactId: "some id",
),
), ),
), ),
); ),
);
expect(find.text("John Doe"), findsOneWidget); expect(find.text("John Doe"), findsOneWidget);
expect(find.text(Coin.bitcoincash.ticker), findsOneWidget); expect(find.text("BCH"), findsOneWidget);
}); expect(find.text(Coin.bitcoincash.ticker), findsOneWidget);
// testWidgets("Test button press opens dialog", (widgetTester) async { await widgetTester.tap(find.byType(RawMaterialButton));
// // final service = MockAddressBookService();
//
// when(service.getContactById("some id"))
// .thenAnswer((realInvocation) => Contact(
// name: "John Doe",
// addresses: [
// const ContactAddressEntry(
// coin: Coin.bitcoincash,
// address: "some bch address",
// label: "Bills")
// ],
// isFavorite: true));
//
// await widgetTester.pumpWidget(
// ProviderScope(
// overrides: [
// addressBookServiceProvider.overrideWithValue(
// service,
// ),
// ],
// child: MaterialApp(
// theme: ThemeData(
// extensions: [
// StackColors.fromStackColorTheme(
// LightColors(),
// ),
// ],
// ),
// home: const AddressBookCard(
// contactId: "some id",
// ),
// ),
// ),
// );
// //
// // when(service.getContactById("03177ce0-4af4-11ed-9617-af8aa7a3796f"))
// // .thenAnswer((realInvocation) => Contact(
// // name: "John Doe",
// // addresses: [
// // const ContactAddressEntry(
// // coin: Coin.bitcoincash,
// // address: "some bch address",
// // label: "Bills")
// // ],
// // isFavorite: true));
// await widgetTester.tap(find.byType(RawMaterialButton));
// // verify(MockedFunctions().showDialog()).called(1);
// await widgetTester.pump();
// when(service.getContactById("03177ce0-4af4-11ed-9617-af8aa7a3796f"))
// .thenAnswer((realInvocation) => Contact(
// name: "John Doe",
// addresses: [
// const ContactAddressEntry(
// coin: Coin.bitcoincash,
// address: "some bch address",
// label: "Bills")
// ],
// isFavorite: true));
//
// expect(
// find.byWidget(const ContactPopUp(
// contactId: "03177ce0-4af4-11ed-9617-af8aa7a3796f")),
// findsOneWidget);
// // await widgetTester.pump();
// // // when(contact)
// // await widgetTester.pump();
// });
}); });
// testWidgets('test returns Contact Address Entry', (widgetTester) async {
// // final service = MockAddressBookService();
// // when(service.getContactById("some id"))
// // .thenAnswer((realInvocation) => Contact(
// // name: "John Doe",
// // addresses: [
// // const ContactAddressEntry(
// // coin: Coin.bitcoincash,
// // address: "some bch address",
// // label: "Bills")
// // ],
// // isFavorite: true));
//
// await widgetTester.pumpWidget(
// ProviderScope(
// overrides: [
// addressBookServiceProvider.overrideWithValue(
// serv,
// ),
// ],
// child: MaterialApp(
// theme: ThemeData(
// extensions: [
// StackColors.fromStackColorTheme(
// LightColors(),
// ),
// ],
// ),
// home: const AddressBookCard(
// contactId: "some id",
// ),
// ),
// ),
// );
//
// expect(find.text("John Doe"), findsOneWidget);
// expect(find.text(Coin.bitcoincash.ticker), findsOneWidget);
// });
// testWidgets("Test button press opens dialog", (widgetTester) async {
// final service = MockAddressBookService();
//
// // when(service.getContactById("some id"))
// // .thenAnswer((realInvocation) => Contact(
// // name: "John Doe",
// // addresses: [
// // const ContactAddressEntry(
// // coin: Coin.bitcoincash,
// // address: "some bch address",
// // label: "Bills")
// // ],
// // isFavorite: true));
//
// await widgetTester.pumpWidget(
// ProviderScope(
// overrides: [
// addressBookServiceProvider.overrideWithValue(
// service,
// ),
// ],
// child: MaterialApp(
// theme: ThemeData(
// extensions: [
// StackColors.fromStackColorTheme(
// LightColors(),
// ),
// ],
// ),
// home: const AddressBookCard(
// contactId: "03177ce0-4af4-11ed-9617-af8aa7a3796f",
// ),
// ),
// ),
// );
// //
// // when(service.getContactById("03177ce0-4af4-11ed-9617-af8aa7a3796f"))
// // .thenAnswer((realInvocation) => Contact(
// // name: "John Doe",
// // addresses: [
// // const ContactAddressEntry(
// // coin: Coin.bitcoincash,
// // address: "some bch address",
// // label: "Bills")
// // ],
// // isFavorite: true));
// // await widgetTester.tap(find.byType(RawMaterialButton));
// // // when(contact)
// // await widgetTester.pump();
// });
} }

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/widgets/custom_buttons/favorite_toggle.dart';
void main() {
testWidgets("Test widget build", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
ProviderScope(
overrides: [],
child: MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(
LightColors(),
),
],
),
home: FavoriteToggle(
onChanged: null,
key: key,
),
),
),
);
expect(find.byType(FavoriteToggle), findsOneWidget);
expect(find.byType(SvgPicture), findsOneWidget);
});
}

View file

@ -1,3 +1,4 @@
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart';
@ -6,8 +7,7 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart';
void main() { void main() {
testWidgets("Test wiget displays correct text", (widgetTester) async { testWidgets("Test wiget displays correct text", (widgetTester) async {
const customLoadingOverlay = final eventBus = EventBus();
CustomLoadingOverlay(message: "Updating exchange rate", eventBus: null);
await widgetTester.pumpWidget( await widgetTester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: ThemeData(
@ -15,8 +15,9 @@ void main() {
StackColors.fromStackColorTheme(LightColors()), StackColors.fromStackColorTheme(LightColors()),
], ],
), ),
home: const Material( home: Material(
child: customLoadingOverlay, child: CustomLoadingOverlay(
message: "Updating exchange rate", eventBus: eventBus),
), ),
), ),
); );

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/custom_text_button.dart';
void main() {
testWidgets("Test text button ", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: CustomTextButtonBase(
key: key,
width: 200,
height: 300,
textButton:
const TextButton(onPressed: null, child: Text("Some Text")),
),
),
),
);
expect(find.byType(CustomTextButtonBase), findsOneWidget);
});
}

View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
void main() {
testWidgets("Test DesktopAppBar widget", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: DesktopAppBar(
key: key,
isCompactHeight: false,
leading: const AppBarBackButton(),
trailing: const ExitToMyStackButton(),
center: const Text("Some Text"),
),
),
),
);
expect(find.byType(DesktopAppBar), findsOneWidget);
});
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart' as mockingjay;
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
void main() {
testWidgets("test DesktopDialog button pressed", (widgetTester) async {
final key = UniqueKey();
final navigator = mockingjay.MockNavigator();
await widgetTester.pumpWidget(
ProviderScope(
overrides: [],
child: MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: mockingjay.MockNavigatorProvider(
navigator: navigator,
child: DesktopDialogCloseButton(
key: key,
onPressedOverride: null,
)),
),
),
);
await widgetTester.tap(find.byType(AppBarIconButton));
await widgetTester.pumpAndSettle();
mockingjay.verify(() => navigator.pop()).called(1);
});
}

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
void main() {
testWidgets("test DesktopDialog builds", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: DesktopDialog(
key: key,
child: const DesktopDialogCloseButton(),
),
),
),
);
expect(find.byType(DesktopDialog), findsOneWidget);
});
}

View file

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
void main() {
testWidgets("test DesktopScaffold", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: DesktopScaffold(
key: key,
body: const SizedBox(),
),
),
),
);
});
testWidgets("Test MasterScaffold for non desktop", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: MasterScaffold(
key: key,
body: const SizedBox(),
appBar: AppBar(),
isDesktop: false,
),
),
),
);
});
testWidgets("Test MasterScaffold for desktop", (widgetTester) async {
final key = UniqueKey();
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: MasterScaffold(
key: key,
body: const SizedBox(),
appBar: AppBar(),
isDesktop: true,
),
),
),
);
});
}

View file

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
void main() {
testWidgets("test address book icon widget", (widgetTester) async {
final key = UniqueKey();
final addressBookIcon = AddressBookIcon(
key: key,
);
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: addressBookIcon,
),
),
);
expect(find.byWidget(addressBookIcon), findsOneWidget);
expect(find.byType(SvgPicture), findsOneWidget);
});
}

View file

@ -18,6 +18,7 @@ import 'package:stackwallet/widgets/managed_favorite.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'managed_favorite_test.mocks.dart'; import 'managed_favorite_test.mocks.dart';
@ -45,7 +46,8 @@ void main() {
.thenAnswer((realInvocation) => manager); .thenAnswer((realInvocation) => manager);
when(manager.isFavorite).thenAnswer((realInvocation) => false); when(manager.isFavorite).thenAnswer((realInvocation) => false);
const managedFavorite = ManagedFavorite(walletId: "some wallet id"); final key = UniqueKey();
// const managedFavorite = ManagedFavorite(walletId: "some wallet id", key: key,);
await widgetTester.pumpWidget( await widgetTester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
@ -59,8 +61,11 @@ void main() {
), ),
], ],
), ),
home: const Material( home: Material(
child: managedFavorite, child: ManagedFavorite(
walletId: "some wallet id",
key: key,
),
), ),
), ),
), ),

View file

@ -82,7 +82,7 @@ void main() {
(realInvocation) => NodeModel( (realInvocation) => NodeModel(
host: "127.0.0.1", host: "127.0.0.1",
port: 2000, port: 2000,
name: "Stack Default", name: "Some other node name",
id: "node id", id: "node id",
useSSL: true, useSSL: true,
enabled: true, enabled: true,
@ -94,7 +94,7 @@ void main() {
NodeModel( NodeModel(
host: "127.0.0.1", host: "127.0.0.1",
port: 2000, port: 2000,
name: "Stack Default", name: "Some other node name",
id: "node id", id: "node id",
useSSL: true, useSSL: true,
enabled: true, enabled: true,
@ -122,7 +122,7 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text("Stack Default"), findsOneWidget); expect(find.text("Some other node name"), findsOneWidget);
expect(find.text("Connected"), findsOneWidget); expect(find.text("Connected"), findsOneWidget);
expect(find.byType(Text), findsNWidgets(2)); expect(find.byType(Text), findsNWidgets(2));
expect(find.byType(SvgPicture), findsWidgets); expect(find.byType(SvgPicture), findsWidgets);

View file

@ -29,7 +29,7 @@ void main() {
(realInvocation) => NodeModel( (realInvocation) => NodeModel(
host: "127.0.0.1", host: "127.0.0.1",
port: 2000, port: 2000,
name: "Stack Default", name: "Some other name",
id: "node id", id: "node id",
useSSL: true, useSSL: true,
enabled: true, enabled: true,
@ -41,7 +41,7 @@ void main() {
(realInvocation) => NodeModel( (realInvocation) => NodeModel(
host: "127.0.0.1", host: "127.0.0.1",
port: 2000, port: 2000,
name: "Stack Default", name: "Some other name",
id: "node id", id: "node id",
useSSL: true, useSSL: true,
enabled: true, enabled: true,
@ -72,7 +72,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text("Node options"), findsOneWidget); expect(find.text("Node options"), findsOneWidget);
expect(find.text("Stack Default"), findsOneWidget); expect(find.text("Some other name"), findsOneWidget);
expect(find.text("Connected"), findsOneWidget); expect(find.text("Connected"), findsOneWidget);
expect(find.byType(SvgPicture), findsNWidgets(2)); expect(find.byType(SvgPicture), findsNWidgets(2));
expect(find.text("Details"), findsOneWidget); expect(find.text("Details"), findsOneWidget);
@ -204,7 +204,6 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text("Node options"), findsOneWidget); expect(find.text("Node options"), findsOneWidget);
// expect(find.text("Stack Default"), findsOneWidget);
expect(find.text("Disconnected"), findsOneWidget); expect(find.text("Disconnected"), findsOneWidget);
await tester.tap(find.text("Connect")); await tester.tap(find.text("Connect"));

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/shake/shake.dart';
void main() {
testWidgets("Widget build", (widgetTester) async {
await widgetTester.pumpWidget(
MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(LightColors()),
],
),
home: Material(
child: Shake(
animationRange: 10,
controller: ShakeController(),
animationDuration: const Duration(milliseconds: 200),
child: Column(
children: const [
Center(
child: Text("Enter Pin"),
)
],
)),
),
),
);
expect(find.byType(Shake), findsOneWidget);
expect(find.byType(Text), findsOneWidget);
});
}

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/trade_card.dart';
void main() {
testWidgets("Test Trade card builds", (widgetTester) async {
final trade = Trade(
uuid: "uuid",
tradeId: "trade id",
rateType: "Estimate rate",
direction: "",
timestamp: DateTime.parse("1662544771"),
updatedAt: DateTime.parse("1662544771"),
payInCurrency: "BTC",
payInAmount: "10",
payInAddress: "btc address",
payInNetwork: "",
payInExtraId: "",
payInTxid: "",
payOutCurrency: "xmr",
payOutAmount: "10",
payOutAddress: "xmr address",
payOutNetwork: "",
payOutExtraId: "",
payOutTxid: "",
refundAddress: "refund address",
refundExtraId: "",
status: "Failed",
exchangeName: "Some Exchange");
await widgetTester.pumpWidget(
ProviderScope(
overrides: [],
child: MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(
LightColors(),
),
],
),
home: TradeCard(trade: trade, onTap: () {}),
),
),
);
expect(find.byType(TradeCard), findsOneWidget);
expect(find.text("BTC → XMR"), findsOneWidget);
expect(find.text("Some Exchange"), findsOneWidget);
});
}

View file

@ -131,6 +131,99 @@ void main() {
verifyNoMoreInteractions(mockLocaleService); verifyNoMoreInteractions(mockLocaleService);
}); });
testWidgets("Anonymized confirmed tx displays correctly", (tester) async {
final mockManager = MockManager();
final mockLocaleService = MockLocaleService();
final wallets = MockWallets();
final mockPrefs = MockPrefs();
final mockPriceService = MockPriceService();
final tx = Transaction(
txid: "some txid",
confirmedStatus: true,
timestamp: 1648595998,
txType: "Anonymized",
amount: 100000000,
aliens: [],
worthNow: "0.01",
worthAtBlockTimestamp: "0.01",
fees: 3794,
inputSize: 1,
outputSize: 1,
inputs: [],
outputs: [],
address: "",
height: 450123,
subType: "mint",
confirmations: 10,
isCancelled: false);
final CoinServiceAPI wallet = MockFiroWallet();
when(wallet.coin.ticker).thenAnswer((_) => "FIRO");
when(mockLocaleService.locale).thenAnswer((_) => "en_US");
when(mockPrefs.currency).thenAnswer((_) => "USD");
when(mockPrefs.externalCalls).thenAnswer((_) => true);
when(mockPriceService.getPrice(Coin.firo))
.thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00));
when(wallet.coin).thenAnswer((_) => Coin.firo);
when(wallets.getManager("wallet-id"))
.thenAnswer((realInvocation) => Manager(wallet));
//
await tester.pumpWidget(
ProviderScope(
overrides: [
walletsChangeNotifierProvider.overrideWithValue(wallets),
localeServiceChangeNotifierProvider
.overrideWithValue(mockLocaleService),
prefsChangeNotifierProvider.overrideWithValue(mockPrefs),
priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService)
],
child: MaterialApp(
theme: ThemeData(
extensions: [
StackColors.fromStackColorTheme(
LightColors(),
),
],
),
home: TransactionCard(transaction: tx, walletId: "wallet-id"),
),
),
);
//
final title = find.text("Anonymized");
// final price1 = find.text("0.00 USD");
final amount = find.text("1.00000000 FIRO");
final icon = find.byIcon(FeatherIcons.arrowUp);
expect(title, findsOneWidget);
// expect(price1, findsOneWidget);
expect(amount, findsOneWidget);
// expect(icon, findsOneWidget);
//
await tester.pumpAndSettle(Duration(seconds: 2));
//
// final price2 = find.text("\$10.00");
// expect(price2, findsOneWidget);
//
// verify(mockManager.addListener(any)).called(1);
verify(mockLocaleService.addListener(any)).called(1);
verify(mockPrefs.currency).called(1);
verify(mockPriceService.getPrice(Coin.firo)).called(1);
verify(wallet.coin.ticker).called(1);
verify(mockLocaleService.locale).called(1);
verifyNoMoreInteractions(mockManager);
verifyNoMoreInteractions(mockLocaleService);
});
testWidgets("Received unconfirmed tx displays correctly", (tester) async { testWidgets("Received unconfirmed tx displays correctly", (tester) async {
final mockManager = MockManager(); final mockManager = MockManager();
final mockLocaleService = MockLocaleService(); final mockLocaleService = MockLocaleService();