Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import 'models/models.dart';
import 'models/node_model.dart';
import 'models/notification_model.dart';
import 'models/trade_wallet_lookup.dart';
import 'pages/already_running_view.dart';
import 'pages/campfire_migrate_view.dart';
import 'pages/home_view/home_view.dart';
import 'pages/intro_view.dart';
Expand Down Expand Up @@ -178,8 +179,57 @@ void main(List<String> args) async {
(await StackFileSystem.applicationHiveDirectory()).path,
);

await DB.instance.hive.openBox<dynamic>(DB.boxNameDBInfo);
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
try {
await DB.instance.hive.openBox<dynamic>(DB.boxNameDBInfo);
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
} on FileSystemException catch (e) {
if (e.osError?.errorCode == 11 || e.message.contains('lock failed')) {
// Another instance already holds the Hive database lock.
// Try to bootstrap just enough of the theme system (Isar is independent
// of Hive) so the error screen looks like a real Stack Wallet screen.
Widget errorApp;
try {
await StackFileSystem.initThemesDir();
await MainDB.instance.initMainDB();
ThemeService.instance.init(MainDB.instance);
errorApp = const ProviderScope(child: AlreadyRunningApp());
} catch (_) {
// Isar is also unavailable (e.g., another error). Fall back to a
// minimal but still Inter-font styled screen.
errorApp = MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(fontFamily: GoogleFonts.inter().fontFamily),
home: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppConfig.appName,
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'is already running.\n'
'Close the other window and try again.',
textAlign: TextAlign.center,
style: GoogleFonts.inter(fontSize: 16),
),
],
),
),
),
);
}
runApp(errorApp);
return;
}
rethrow;
}
await Prefs.instance.init();

await Logging.instance.initialize(
Expand Down
191 changes: 191 additions & 0 deletions lib/pages/already_running_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:google_fonts/google_fonts.dart';

import '../app_config.dart';
import '../themes/stack_colors.dart';
import '../themes/theme_providers.dart';
import '../themes/theme_service.dart';
import '../utilities/stack_file_system.dart';
import '../utilities/text_styles.dart';
import '../utilities/util.dart';
import '../widgets/app_icon.dart';
import '../widgets/background.dart';

/// Root app widget for the "already running" error path.
///
/// Mirrors the theme bootstrap performed by [MaterialAppWithTheme] in main.dart
/// but without touching Hive. Requires Isar + ThemeService to already be
/// initialized before [runApp] is called.
class AlreadyRunningApp extends ConsumerStatefulWidget {
const AlreadyRunningApp({super.key});

@override
ConsumerState<AlreadyRunningApp> createState() => _AlreadyRunningAppState();
}

class _AlreadyRunningAppState extends ConsumerState<AlreadyRunningApp> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(applicationThemesDirectoryPathProvider.notifier).state =
StackFileSystem.themesDir!.path;
// The first instance already verified/installed the light theme, so
// getTheme cannot return null here.
ref.read(themeProvider.state).state = ref
.read(pThemeService)
.getTheme(themeId: "light")!;
});
}

@override
Widget build(BuildContext context) {
final colorScheme = ref.watch(colorProvider.state).state;
return MaterialApp(
debugShowCheckedModeBanner: false,
title: AppConfig.appName,
theme: ThemeData(
extensions: [colorScheme],
fontFamily: GoogleFonts.inter().fontFamily,
splashColor: Colors.transparent,
),
home: const AlreadyRunningView(),
);
}
}

/// Error screen shown when this is a second instance of the app.
///
/// Mirrors [IntroView]'s layout: themed background, logo, app name heading,
/// short description subtitle, then the error message (in label style, smaller
/// than the subtitle) in place of the action buttons.
class AlreadyRunningView extends ConsumerWidget {
const AlreadyRunningView({super.key});

static const _errorMessage =
"${AppConfig.appName} is already running. "
"Close the other window and try again.";

@override
Widget build(BuildContext context, WidgetRef ref) {
final isDesktop = Util.isDesktop;
final colors = Theme.of(context).extension<StackColors>()!;
final stack = ref.watch(
themeProvider.select((value) => value.assets.stack),
);

return Background(
child: Scaffold(
backgroundColor: colors.background,
body: SafeArea(
child: Center(
child: !isDesktop
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(flex: 2),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: SizedBox(
width: 266,
height: 266,
child: stack.endsWith(".png")
? Image.file(File(stack))
: SvgPicture.file(
File(stack),
width: 266,
height: 266,
),
),
),
),
const Spacer(flex: 1),
Text(
AppConfig.appName,
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: Text(
AppConfig.shortDescriptionText,
textAlign: TextAlign.center,
style: STextStyles.subtitle(context),
),
),
const Spacer(flex: 4),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Text(
_errorMessage,
textAlign: TextAlign.center,
style: STextStyles.label(context),
),
),
],
)
: SizedBox(
width: 350,
height: 540,
child: Column(
children: [
const Spacer(flex: 2),
const SizedBox(
width: 130,
height: 130,
child: AppIcon(),
),
const Spacer(flex: 42),
Text(
AppConfig.appName,
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(
context,
).copyWith(fontSize: 40),
),
const Spacer(flex: 24),
Text(
AppConfig.shortDescriptionText,
textAlign: TextAlign.center,
style: STextStyles.subtitle(
context,
).copyWith(fontSize: 24),
),
const Spacer(flex: 42),
Text(
_errorMessage,
textAlign: TextAlign.center,
style: STextStyles.label(
context,
).copyWith(fontSize: 18),
),
const Spacer(flex: 65),
],
),
),
),
),
),
);
}
}
2 changes: 1 addition & 1 deletion lib/wallets/crypto_currency/coins/particl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface {
}

@override
int get transactionVersion => 1;
int get transactionVersion => 160;

@override
BigInt get defaultFeeRate => BigInt.from(20000);
Expand Down
63 changes: 30 additions & 33 deletions lib/wallets/wallet/impl/particl_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,36 @@ class ParticlWallet<T extends ElectrumXCurrencyInterface>
String? blockedReason;
String? utxoLabel;

// Only check the specific output this UTXO corresponds to, not all outputs.
final vout = jsonUTXO["tx_pos"] as int;
final outputs = jsonTX["vout"] as List? ?? [];

for (final output in outputs) {
if (output is Map) {
if (output['ct_fee'] != null) {
// Blind output, ignore for now.
blocked = true;
blockedReason = "Blind output.";
utxoLabel = "Unsupported output type.";
} else if (output['rangeproof'] != null) {
// Private RingCT output, ignore for now.
blocked = true;
blockedReason = "Confidential output.";
utxoLabel = "Unsupported output type.";
} else if (output['data_hex'] != null) {
// Data output, ignore for now.
final output = outputs.cast<Map<String, dynamic>?>().firstWhere(
(e) => e?["n"] == vout,
orElse: () => null,
);

if (output != null) {
if (output['ct_fee'] != null) {
blocked = true;
blockedReason = "Blind output.";
utxoLabel = "Unsupported output type.";
} else if (output['rangeproof'] != null) {
blocked = true;
blockedReason = "Confidential output.";
utxoLabel = "Unsupported output type.";
} else if (output['data_hex'] != null) {
blocked = true;
blockedReason = "Data output.";
utxoLabel = "Unsupported output type.";
} else if (output['scriptPubKey'] != null) {
if (output['scriptPubKey']?['asm'] is String &&
(output['scriptPubKey']['asm'] as String).contains(
"OP_ISCOINSTAKE",
)) {
blocked = true;
blockedReason = "Data output.";
blockedReason = "Spending staking";
utxoLabel = "Unsupported output type.";
} else if (output['scriptPubKey'] != null) {
if (output['scriptPubKey']?['asm'] is String &&
(output['scriptPubKey']['asm'] as String).contains(
"OP_ISCOINSTAKE",
)) {
blocked = true;
blockedReason = "Spending staking";
utxoLabel = "Unsupported output type.";
}
}
}
}
Expand Down Expand Up @@ -237,17 +239,12 @@ class ParticlWallet<T extends ElectrumXCurrencyInterface>
addresses.addAll(prevOut.addresses);
}

InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: map["scriptSig"]?["hex"] as String?,
scriptSigAsm: map["scriptSig"]?["asm"] as String?,
sequence: map["sequence"] as int?,
InputV2 input = InputV2.fromElectrumxJson(
json: map,
outpoint: outpoint,
valueStringSats: valueStringSats,
addresses: addresses,
witness: map["witness"] as String?,
valueStringSats: valueStringSats,
coinbase: coinbase,
innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?,
// Need addresses before we can know if the wallet owns this input.
walletOwns: false,
);

Expand Down Expand Up @@ -454,7 +451,7 @@ class ParticlWallet<T extends ElectrumXCurrencyInterface>

tempInputs.add(
InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: txb.inputs.first.script?.toHex,
scriptSigHex: txb.inputs[i].script?.toHex,
scriptSigAsm: null,
sequence: 0xffffffff - 1,
outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor(
Expand Down
Loading