WIP spark names

This commit is contained in:
julian 2025-03-25 15:05:54 -06:00
parent bee49df2a9
commit 8eb91507d2
15 changed files with 4226 additions and 1534 deletions

View file

@ -93,11 +93,8 @@ class ElectrumXClient {
// StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _electrumAdapterChannel;
ElectrumClient? getElectrumAdapter() =>
ClientManager.sharedInstance.getClient(
cryptoCurrency: cryptoCurrency,
netType: netType,
);
ElectrumClient? getElectrumAdapter() => ClientManager.sharedInstance
.getClient(cryptoCurrency: cryptoCurrency, netType: netType);
late Prefs _prefs;
late TorService _torService;
@ -109,12 +106,10 @@ class ElectrumXClient {
// add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible
static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
(p0) {
p0._torPreferenceListener?.cancel();
p0._torStatusListener?.cancel();
},
);
static final Finalizer<ElectrumXClient> _finalizer = Finalizer((p0) {
p0._torPreferenceListener?.cancel();
p0._torStatusListener?.cancel();
});
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
@ -129,8 +124,9 @@ class ElectrumXClient {
required this.netType,
required List<ElectrumXNode> failovers,
required this.cryptoCurrency,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(
seconds: 60,
),
TorService? torService,
EventBus? globalEventBusForTesting,
}) {
@ -144,46 +140,45 @@ class ElectrumXClient {
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
// Listen for tor status changes.
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
(event) async {
switch (event.newStatus) {
case TorConnectionStatus.connecting:
await _torConnectingLock.acquire();
_requireMutex = true;
break;
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen((
event,
) async {
switch (event.newStatus) {
case TorConnectionStatus.connecting:
await _torConnectingLock.acquire();
_requireMutex = true;
break;
case TorConnectionStatus.connected:
case TorConnectionStatus.disconnected:
if (_torConnectingLock.isLocked) {
_torConnectingLock.release();
}
_requireMutex = false;
break;
}
},
);
case TorConnectionStatus.connected:
case TorConnectionStatus.disconnected:
if (_torConnectingLock.isLocked) {
_torConnectingLock.release();
}
_requireMutex = false;
break;
}
});
// Listen for tor preference changes.
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
(event) async {
// not sure if we need to do anything specific here
// switch (event.status) {
// case TorStatus.enabled:
// case TorStatus.disabled:
// }
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen((
event,
) async {
// not sure if we need to do anything specific here
// switch (event.status) {
// case TorStatus.enabled:
// case TorStatus.disabled:
// }
// setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance
_electrumAdapterChannel = null;
await (await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency))
.$1
?.close();
// setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance
_electrumAdapterChannel = null;
await (await ClientManager.sharedInstance.remove(
cryptoCurrency: cryptoCurrency,
)).$1?.close();
// Also close any chain height services that are currently open.
// await ChainHeightServiceManager.dispose();
},
);
// Also close any chain height services that are currently open.
// await ChainHeightServiceManager.dispose();
});
}
factory ElectrumXClient.from({
@ -252,14 +247,16 @@ class ElectrumXClient {
if (netType == TorPlainNetworkOption.clear) {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency);
await ClientManager.sharedInstance.remove(
cryptoCurrency: cryptoCurrency,
);
}
} else {
if (netType == TorPlainNetworkOption.tor) {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency);
await ClientManager.sharedInstance.remove(
cryptoCurrency: cryptoCurrency,
);
}
}
@ -338,24 +335,22 @@ class ElectrumXClient {
}
if (_requireMutex) {
await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
await _torConnectingLock.protect(
() async => await checkElectrumAdapter(),
);
} else {
await checkElectrumAdapter();
}
try {
final response = await getElectrumAdapter()!.request(
command,
args,
);
final response = await getElectrumAdapter()!.request(command, args);
if (response is Map &&
response.keys.contains("error") &&
response["error"] != null) {
if (response["error"]
.toString()
.contains("No such mempool or blockchain transaction")) {
if (response["error"].toString().contains(
"No such mempool or blockchain transaction",
)) {
throw NoSuchTransactionException(
"No such mempool or blockchain transaction",
args.first.toString(),
@ -399,11 +394,7 @@ class ElectrumXClient {
}
} catch (e, s) {
final errorMessage = e.toString();
Logging.instance.w(
"$host $e",
error: e,
stackTrace: s,
);
Logging.instance.w("$host $e", error: e, stackTrace: s);
if (errorMessage.contains("JSON-RPC error")) {
currentFailoverIndex = _failovers.length;
}
@ -437,8 +428,9 @@ class ElectrumXClient {
}
if (_requireMutex) {
await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
await _torConnectingLock.protect(
() async => await checkElectrumAdapter(),
);
} else {
await checkElectrumAdapter();
}
@ -531,18 +523,19 @@ class ElectrumXClient {
// electrum_adapter returns the result of the request, request() has been
// updated to return a bool on a server.ping command as a special case.
return await request(
requestID: requestID,
command: 'server.ping',
requestTimeout: const Duration(seconds: 30),
retries: retryCount,
).timeout(
const Duration(seconds: 30),
onTimeout: () {
Logging.instance.d(
"ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host",
);
},
) as bool;
requestID: requestID,
command: 'server.ping',
requestTimeout: const Duration(seconds: 30),
retries: retryCount,
).timeout(
const Duration(seconds: 30),
onTimeout: () {
Logging.instance.d(
"ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host",
);
},
)
as bool;
} catch (e) {
rethrow;
}
@ -609,9 +602,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: 'blockchain.transaction.broadcast',
args: [
rawTx,
],
args: [rawTx],
);
return response as String;
} catch (e) {
@ -636,9 +627,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.get_balance',
args: [
scripthash,
],
args: [scripthash],
);
return Map<String, dynamic>.from(response as Map);
} catch (e) {
@ -673,9 +662,7 @@ class ElectrumXClient {
requestID: requestID,
command: 'blockchain.scripthash.get_history',
requestTimeout: const Duration(minutes: 5),
args: [
scripthash,
],
args: [scripthash],
);
result = response;
retryCount--;
@ -731,9 +718,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.listunspent',
args: [
scripthash,
],
args: [scripthash],
);
return List<Map<String, dynamic>>.from(response as List);
} catch (e) {
@ -826,14 +811,10 @@ class ElectrumXClient {
bool verbose = true,
String? requestID,
}) async {
Logging.instance.d(
"attempting to fetch blockchain.transaction.get...",
);
Logging.instance.d("attempting to fetch blockchain.transaction.get...");
await checkElectrumAdapter();
final dynamic response = await getElectrumAdapter()!.getTransaction(txHash);
Logging.instance.d(
"Fetching blockchain.transaction.get finished",
);
Logging.instance.d("Fetching blockchain.transaction.get finished");
if (!verbose) {
return {"rawtx": response as String};
@ -861,16 +842,12 @@ class ElectrumXClient {
String blockhash = "",
String? requestID,
}) async {
Logging.instance.d(
"attempting to fetch lelantus.getanonymityset...",
);
Logging.instance.d("attempting to fetch lelantus.getanonymityset...");
await checkElectrumAdapter();
final Map<String, dynamic> response =
await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
Logging.instance.d(
"Fetching lelantus.getanonymityset finished",
);
final Map<String, dynamic> response = await (getElectrumAdapter()
as FiroElectrumClient)
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
Logging.instance.d("Fetching lelantus.getanonymityset finished");
return response;
}
@ -882,15 +859,11 @@ class ElectrumXClient {
dynamic mints,
String? requestID,
}) async {
Logging.instance.d(
"attempting to fetch lelantus.getmintmetadata...",
);
Logging.instance.d("attempting to fetch lelantus.getmintmetadata...");
await checkElectrumAdapter();
final dynamic response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusMintData(mints: mints);
Logging.instance.d(
"Fetching lelantus.getmintmetadata finished",
);
Logging.instance.d("Fetching lelantus.getmintmetadata finished");
return response;
}
@ -900,9 +873,7 @@ class ElectrumXClient {
String? requestID,
required int startNumber,
}) async {
Logging.instance.d(
"attempting to fetch lelantus.getusedcoinserials...",
);
Logging.instance.d("attempting to fetch lelantus.getusedcoinserials...");
await checkElectrumAdapter();
int retryCount = 3;
@ -912,9 +883,7 @@ class ElectrumXClient {
response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout.
Logging.instance.d(
"Fetching lelantus.getusedcoinserials finished",
);
Logging.instance.d("Fetching lelantus.getusedcoinserials finished");
retryCount--;
}
@ -926,15 +895,11 @@ class ElectrumXClient {
///
/// ex: 1
Future<int> getLelantusLatestCoinId({String? requestID}) async {
Logging.instance.d(
"attempting to fetch lelantus.getlatestcoinid...",
);
Logging.instance.d("attempting to fetch lelantus.getlatestcoinid...");
await checkElectrumAdapter();
final int response =
await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId();
Logging.instance.d(
"Fetching lelantus.getlatestcoinid finished",
);
Logging.instance.d("Fetching lelantus.getlatestcoinid finished");
return response;
}
@ -961,12 +926,12 @@ class ElectrumXClient {
try {
final start = DateTime.now();
await checkElectrumAdapter();
final Map<String, dynamic> response =
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkAnonymitySet(
coinGroupId: coinGroupId,
startBlockHash: startBlockHash,
);
final Map<String, dynamic> response = await (getElectrumAdapter()
as FiroElectrumClient)
.getSparkAnonymitySet(
coinGroupId: coinGroupId,
startBlockHash: startBlockHash,
);
Logging.instance.d(
"Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId"
"=$coinGroupId, startBlockHash=$startBlockHash). "
@ -1053,34 +1018,23 @@ class ElectrumXClient {
/// Returns the latest Spark set id
///
/// ex: 1
Future<int> getSparkLatestCoinId({
String? requestID,
}) async {
Future<int> getSparkLatestCoinId({String? requestID}) async {
try {
Logging.instance.d(
"attempting to fetch spark.getsparklatestcoinid...",
);
Logging.instance.d("attempting to fetch spark.getsparklatestcoinid...");
await checkElectrumAdapter();
final int response = await (getElectrumAdapter() as FiroElectrumClient)
.getSparkLatestCoinId();
Logging.instance.d(
"Fetching spark.getsparklatestcoinid finished",
);
final int response =
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkLatestCoinId();
Logging.instance.d("Fetching spark.getsparklatestcoinid finished");
return response;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
Logging.instance.e(e, error: e, stackTrace: s);
rethrow;
}
}
/// Returns the txids of the current transactions found in the mempool
Future<Set<String>> getMempoolTxids({
String? requestID,
}) async {
Future<Set<String>> getMempoolTxids({String? requestID}) async {
try {
final start = DateTime.now();
final response = await request(
@ -1088,9 +1042,10 @@ class ElectrumXClient {
command: "spark.getmempoolsparktxids",
);
final txids = List<String>.from(response as List)
.map((e) => e.toHexReversedFromBase64)
.toSet();
final txids =
List<String>.from(
response as List,
).map((e) => e.toHexReversedFromBase64).toSet();
Logging.instance.d(
"Finished ElectrumXClient.getMempoolTxids(). "
@ -1099,11 +1054,7 @@ class ElectrumXClient {
return txids;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
Logging.instance.e(e, error: e, stackTrace: s);
rethrow;
}
}
@ -1119,9 +1070,7 @@ class ElectrumXClient {
requestID: requestID,
command: "spark.getmempoolsparktxs",
args: [
{
"txids": txids,
},
{"txids": txids},
],
);
@ -1131,8 +1080,9 @@ class ElectrumXClient {
result.add(
SparkMempoolData(
txid: entry.key,
serialContext:
List<String>.from(entry.value["serial_context"] as List),
serialContext: List<String>.from(
entry.value["serial_context"] as List,
),
// the space after lTags is required lol
lTags: List<String>.from(entry.value["lTags "] as List),
coins: List<String>.from(entry.value["coins"] as List),
@ -1163,9 +1113,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: "spark.getusedcoinstagstxhashes",
args: [
"$startNumber",
],
args: ["$startNumber"],
);
final map = Map<String, dynamic>.from(response as Map);
@ -1179,14 +1127,34 @@ class ElectrumXClient {
return tags;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
Logging.instance.e(e, error: e, stackTrace: s);
rethrow;
}
}
Future<List<String>> getSparkNames({String? requestID}) async {
try {
final start = DateTime.now();
await checkElectrumAdapter();
const command = "spark.getsparknames";
Logging.instance.d(
"[${getElectrumAdapter()?.host}] => attempting to fetch $command...",
);
final response = await request(requestID: requestID, command: command);
Logging.instance.d(
"Finished ElectrumXClient.getSparkNames(). "
"coins.length: ${(response as List).length}"
"Duration=${DateTime.now().difference(start)}",
);
return response.cast();
} catch (e) {
rethrow;
}
}
// ======== New Paginated Endpoints ==========================================
Future<SparkAnonymitySetMeta> getSparkAnonymitySetMeta({
@ -1203,9 +1171,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: command,
args: [
"$coinGroupId",
],
args: ["$coinGroupId"],
);
final map = Map<String, dynamic>.from(response as Map);
@ -1227,11 +1193,7 @@ class ElectrumXClient {
return result;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
Logging.instance.e(e, error: e, stackTrace: s);
rethrow;
}
}
@ -1250,12 +1212,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: command,
args: [
"$coinGroupId",
latestBlock,
"$startIndex",
"$endIndex",
],
args: ["$coinGroupId", latestBlock, "$startIndex", "$endIndex"],
);
final map = Map<String, dynamic>.from(response as Map);
@ -1275,11 +1232,7 @@ class ElectrumXClient {
return result;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
Logging.instance.e(e, error: e, stackTrace: s);
rethrow;
}
}
@ -1296,10 +1249,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: "blockchain.checkifmncollateral",
args: [
txid,
index.toString(),
],
args: [txid, index.toString()],
);
Logging.instance.d(
@ -1310,11 +1260,7 @@ class ElectrumXClient {
return response as bool;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
Logging.instance.e(e, error: e, stackTrace: s);
rethrow;
}
}
@ -1344,9 +1290,7 @@ class ElectrumXClient {
final response = await request(
requestID: requestID,
command: 'blockchain.estimatefee',
args: [
blocks,
],
args: [blocks],
);
try {
if (response == null ||
@ -1371,7 +1315,8 @@ class ElectrumXClient {
}
return Decimal.parse(response.toString());
} catch (e, s) {
final String msg = "Error parsing fee rate. Response: $response"
final String msg =
"Error parsing fee rate. Response: $response"
"\nResult: $response\nError: $e\nStack trace: $s";
Logging.instance.e(msg, error: e, stackTrace: s);
throw Exception(msg);

View file

@ -0,0 +1,423 @@
import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../providers/providers.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/util.dart';
import '../../../wallets/models/tx_data.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/stack_dialog.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/rounded_white_container.dart';
import 'confirm_spark_name_transaction_view.dart';
class BuySparkNameView extends ConsumerStatefulWidget {
const BuySparkNameView({
super.key,
required this.walletId,
required this.name,
});
final String walletId;
final String name;
static const routeName = "/buySparkNameView";
@override
ConsumerState<BuySparkNameView> createState() => _BuySparkNameViewState();
}
class _BuySparkNameViewState extends ConsumerState<BuySparkNameView> {
final additionalInfoController = TextEditingController();
int _years = 1;
Future<TxData> _preRegFuture() async {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as SparkInterface;
final myAddress = await wallet.getCurrentReceivingSparkAddress();
if (myAddress == null) {
throw Exception("No spark address found");
}
final txData = await wallet.prepareSparkNameTransaction(
name: widget.name,
address: myAddress.value,
years: _years,
additionalInfo: additionalInfoController.text,
);
return txData;
}
bool _preRegLock = false;
Future<void> _prepareNameTx() async {
if (_preRegLock) return;
_preRegLock = true;
try {
final txData =
(await showLoading(
whileFuture: _preRegFuture(),
context: context,
message: "Preparing transaction...",
onException: (e) {
throw e;
},
))!;
if (mounted) {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
builder:
(context) => SDialog(
child: SizedBox(
width: 580,
child: ConfirmSparkNameTransactionView(
txData: txData,
walletId: widget.walletId,
),
),
),
);
} else {
await Navigator.of(context).pushNamed(
ConfirmSparkNameTransactionView.routeName,
arguments: (txData, widget.walletId),
);
}
}
} catch (e, s) {
Logging.instance.e("_prepareNameTx failed", error: e, stackTrace: s);
if (mounted) {
String err = e.toString();
if (err.startsWith("Exception: ")) {
err = err.replaceFirst("Exception: ", "");
}
await showDialog<void>(
context: context,
builder:
(_) => StackOkDialog(
title: "Error",
message: err,
desktopPopRootNavigator: Util.isDesktop,
maxWidth: Util.isDesktop ? 600 : null,
),
);
}
} finally {
_preRegLock = false;
}
}
@override
void dispose() {
additionalInfoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(pWalletCoin(widget.walletId));
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) {
return Background(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
leading: const AppBarBackButton(),
titleSpacing: 0,
title: Text(
"Buy name",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (ctx, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: child,
),
),
),
);
},
),
),
),
);
},
child: Column(
crossAxisAlignment:
Util.isDesktop
? CrossAxisAlignment.start
: CrossAxisAlignment.stretch,
children: [
if (!Util.isDesktop)
Text(
"Buy name",
style:
Util.isDesktop
? STextStyles.desktopH3(context)
: STextStyles.pageTitleH2(context),
),
SizedBox(height: Util.isDesktop ? 24 : 16),
// Row(
// mainAxisAlignment:
// Util.isDesktop
// ? MainAxisAlignment.center
// : MainAxisAlignment.start,
// children: [
// Text(
// "Name registration will take approximately 2 to 4 hours.",
// style:
// Util.isDesktop
// ? STextStyles.w500_14(context).copyWith(
// color:
// Theme.of(
// context,
// ).extension<StackColors>()!.textDark3,
// )
// : STextStyles.w500_12(context).copyWith(
// color:
// Theme.of(
// context,
// ).extension<StackColors>()!.textDark3,
// ),
// ),
// ],
// ),
// SizedBox(height: Util.isDesktop ? 24 : 16),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Name",
style:
Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.infoItemLabel,
)
: STextStyles.w500_12(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.infoItemLabel,
),
),
Text(
widget.name,
style:
Util.isDesktop
? STextStyles.w500_14(context)
: STextStyles.w500_12(context),
),
],
),
),
SizedBox(height: Util.isDesktop ? 16 : 8),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style:
Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.infoItemLabel,
)
: STextStyles.w500_12(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.infoItemLabel,
),
),
Text(
ref
.watch(pAmountFormatter(coin))
.format(
Amount.fromDecimal(
Decimal.fromInt(
kStandardSparkNamesFee[widget.name.length] * _years,
),
fractionDigits: coin.fractionDigits,
),
),
style:
Util.isDesktop
? STextStyles.w500_14(context)
: STextStyles.w500_12(context),
),
],
),
),
SizedBox(height: Util.isDesktop ? 16 : 8),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Register for",
style:
Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.infoItemLabel,
)
: STextStyles.w500_12(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.infoItemLabel,
),
),
SizedBox(
width: Util.isDesktop ? 180 : 140,
child: DropdownButtonHideUnderline(
child: DropdownButton2<int>(
value: _years,
items: [
...List.generate(10, (i) => i + 1).map(
(e) => DropdownMenuItem(
value: e,
child: Text(
"$e years",
style: STextStyles.w500_14(context),
),
),
),
],
onChanged: (value) {
if (value is int) {
setState(() {
_years = value;
});
}
},
isExpanded: true,
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
iconStyleData: IconStyleData(
icon: Padding(
padding: const EdgeInsets.only(right: 10),
child: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color:
Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
maxHeight: 250,
decoration: BoxDecoration(
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
),
],
),
),
SizedBox(height: Util.isDesktop ? 16 : 8),
RoundedWhiteContainer(
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: additionalInfoController,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.all(16),
hintStyle: STextStyles.fieldLabel(context),
hintText: "Additional info",
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
),
),
),
SizedBox(height: Util.isDesktop ? 24 : 16),
if (!Util.isDesktop) const Spacer(),
PrimaryButton(
label: "Buy",
// width: Util.isDesktop ? 160 : double.infinity,
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: _prepareNameTx,
),
SizedBox(height: Util.isDesktop ? 32 : 16),
],
),
);
}
}

View file

@ -0,0 +1,975 @@
/*
* 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:async';
import 'dart:io';
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../models/isar/models/transaction_note.dart';
import '../../notifications/show_flush_bar.dart';
import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart';
import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart';
import '../../providers/db/main_db_provider.dart';
import '../../providers/providers.dart';
import '../../route_generator.dart';
import '../../themes/stack_colors.dart';
import '../../themes/theme_providers.dart';
import '../../utilities/amount/amount.dart';
import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/constants.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/icon_widgets/x_icon.dart';
import '../../widgets/rounded_container.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import '../pinpad_views/lock_screen_view.dart';
import '../send_view/sub_widgets/sending_transaction_dialog.dart';
class ConfirmSparkNameTransactionView extends ConsumerStatefulWidget {
const ConfirmSparkNameTransactionView({
super.key,
required this.txData,
required this.walletId,
});
static const String routeName = "/confirmSparkNameTransactionView";
final TxData txData;
final String walletId;
@override
ConsumerState<ConfirmSparkNameTransactionView> createState() =>
_ConfirmSparkNameTransactionViewState();
}
class _ConfirmSparkNameTransactionViewState
extends ConsumerState<ConfirmSparkNameTransactionView> {
late final String walletId;
late final bool isDesktop;
late final FocusNode _noteFocusNode;
late final TextEditingController noteController;
Future<void> _attemptSend() async {
final wallet = ref.read(pWallets).getWallet(walletId);
final coin = wallet.info.coin;
final sendProgressController = ProgressAndSuccessController();
unawaited(
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
return SendingTransactionDialog(
coin: coin,
controller: sendProgressController,
);
},
),
);
final time = Future<dynamic>.delayed(const Duration(milliseconds: 2500));
final List<String> txids = [];
Future<TxData> txDataFuture;
final note = noteController.text;
try {
txDataFuture = wallet.confirmSend(txData: widget.txData);
// await futures in parallel
final futureResults = await Future.wait([txDataFuture, time]);
final txData = (futureResults.first as TxData);
sendProgressController.triggerSuccess?.call();
// await futures in parallel
await Future.wait([
// wait for animation
Future<void>.delayed(const Duration(seconds: 5)),
]);
txids.add(txData.txid!);
ref.refresh(desktopUseUTXOs);
// save note
for (final txid in txids) {
await ref
.read(mainDBProvider)
.putTransactionNote(
TransactionNote(walletId: walletId, txid: txid, value: note),
);
}
unawaited(wallet.refresh());
if (mounted) {
// pop sending dialog
Navigator.of(context, rootNavigator: Util.isDesktop).pop();
// pop confirm send view
Navigator.of(context, rootNavigator: Util.isDesktop).pop();
// pop buy popup
Navigator.of(context, rootNavigator: Util.isDesktop).pop();
}
} catch (e, s) {
const niceError = "Broadcast name transaction failed";
Logging.instance.e(niceError, error: e, stackTrace: s);
if (mounted) {
// pop sending dialog
Navigator.of(context, rootNavigator: Util.isDesktop).pop();
await showDialog<void>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
if (isDesktop) {
return DesktopDialog(
maxWidth: 450,
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(niceError, style: STextStyles.desktopH3(context)),
const SizedBox(height: 24),
Flexible(
child: SingleChildScrollView(
child: SelectableText(
e.toString(),
style: STextStyles.smallMed14(context),
),
),
),
const SizedBox(height: 56),
Row(
children: [
const Spacer(),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Ok",
onPressed: Navigator.of(context).pop,
),
),
],
),
],
),
),
);
} else {
return StackDialog(
title: niceError,
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.accentColorDark,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
}
},
);
}
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
walletId = widget.walletId;
_noteFocusNode = FocusNode();
noteController = TextEditingController();
noteController.text = widget.txData.note ?? "";
super.initState();
}
@override
void dispose() {
noteController.dispose();
_noteFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(pWalletCoin(walletId));
final unit = coin.ticker;
final fee = widget.txData.fee;
final amountWithoutChange = widget.txData.amountWithoutChange!;
return ConditionalParent(
condition: !isDesktop,
builder:
(child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
// if (FocusScope.of(context).hasFocus) {
// FocusScope.of(context).unfocus();
// await Future<void>.delayed(Duration(milliseconds: 50));
// }
Navigator.of(context).pop();
},
),
title: Text(
"Confirm transaction",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: child,
),
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder:
(child) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
AppBarBackButton(
size: 40,
iconSize: 24,
onPressed:
() =>
Navigator.of(context, rootNavigator: true).pop(),
),
Text(
"Confirm transaction",
style: STextStyles.desktopH3(context),
),
],
),
Flexible(child: SingleChildScrollView(child: child)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max,
children: [
if (!isDesktop)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Confirm Name transaction",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(height: 12),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Name", style: STextStyles.smallMed12(context)),
const SizedBox(height: 4),
Text(
widget.txData.sparkNameInfo!.name,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
const SizedBox(height: 12),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Additional info",
style: STextStyles.smallMed12(context),
),
const SizedBox(height: 4),
Text(
widget.txData.sparkNameInfo!.additionalInfo,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
const SizedBox(height: 12),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Recipient",
style: STextStyles.smallMed12(context),
),
const SizedBox(height: 4),
Text(
widget.txData.recipients!.first.address,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
const SizedBox(height: 12),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Registration fee",
style: STextStyles.smallMed12(context),
),
SelectableText(
ref
.watch(pAmountFormatter(coin))
.format(amountWithoutChange),
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
],
),
),
const SizedBox(height: 12),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction fee",
style: STextStyles.smallMed12(context),
),
SelectableText(
ref.watch(pAmountFormatter(coin)).format(fee!),
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
],
),
),
if (widget.txData.fee != null && widget.txData.vSize != null)
const SizedBox(height: 12),
if (widget.txData.fee != null && widget.txData.vSize != null)
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"sats/vByte",
style: STextStyles.smallMed12(context),
),
const SizedBox(height: 4),
SelectableText(
"~${fee.raw.toInt() ~/ widget.txData.vSize!}",
style: STextStyles.itemSubtitle12(context),
),
],
),
),
if (widget.txData.note != null &&
widget.txData.note!.isNotEmpty)
const SizedBox(height: 12),
if (widget.txData.note != null &&
widget.txData.note!.isNotEmpty)
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Note", style: STextStyles.smallMed12(context)),
const SizedBox(height: 4),
SelectableText(
widget.txData.note!,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
],
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 32,
right: 32,
bottom: 50,
),
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
borderColor:
Theme.of(context).extension<StackColors>()!.background,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color:
Theme.of(
context,
).extension<StackColors>()!.background,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
topRight: Radius.circular(
Constants.size.circularBorderRadius,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 22,
),
child: Row(
children: [
SvgPicture.file(
File(
ref.watch(
themeProvider.select(
(value) => value.assets.send,
),
),
),
width: 32,
height: 32,
),
const SizedBox(width: 16),
Text(
"Send $unit Name transaction",
style: STextStyles.desktopTextMedium(context),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Name",
style: STextStyles.desktopTextExtraExtraSmall(
context,
),
),
const SizedBox(height: 2),
SelectableText(
widget.txData.sparkNameInfo!.name,
style: STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textDark,
),
),
],
),
),
Container(
height: 1,
color:
Theme.of(
context,
).extension<StackColors>()!.background,
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Additional info",
style: STextStyles.desktopTextExtraExtraSmall(
context,
),
),
const SizedBox(height: 2),
SelectableText(
widget.txData.sparkNameInfo!.additionalInfo,
style: STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textDark,
),
),
],
),
),
],
),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(left: 32, right: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
"Note (optional)",
style: STextStyles.desktopTextExtraSmall(
context,
).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
textAlign: TextAlign.left,
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
minLines: 1,
maxLines: 5,
autocorrect: isDesktop ? false : true,
enableSuggestions: isDesktop ? false : true,
controller: noteController,
focusNode: _noteFocusNode,
style: STextStyles.desktopTextExtraSmall(
context,
).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldActiveText,
height: 1.8,
),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_noteFocusNode,
context,
desktopMed: true,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
),
suffixIcon:
noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(
() => noteController.text = "",
);
},
),
],
),
),
)
: null,
),
),
),
const SizedBox(height: 20),
],
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(top: 16, left: 32),
child: Text(
"Registration fee",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(top: 10, left: 32, right: 32),
child: RoundedContainer(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
),
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
child: Builder(
builder: (context) {
final externalCalls = ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.externalCalls,
),
);
String fiatAmount = "N/A";
if (externalCalls) {
final price =
ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (price > Decimal.zero) {
fiatAmount = (amountWithoutChange.decimal * price)
.toAmount(fractionDigits: 2)
.fiatString(
locale:
ref
.read(
localeServiceChangeNotifierProvider,
)
.locale,
);
}
}
return Row(
children: [
SelectableText(
ref
.watch(pAmountFormatter(coin))
.format(amountWithoutChange),
style: STextStyles.itemSubtitle(context),
),
if (externalCalls)
Text(
" | ",
style: STextStyles.itemSubtitle(context),
),
if (externalCalls)
SelectableText(
"~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
style: STextStyles.itemSubtitle(context),
),
],
);
},
),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(top: 16, left: 32),
child: Text(
"Recipient",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(top: 10, left: 32, right: 32),
child: RoundedContainer(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
),
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
child: SelectableText(
widget.txData.recipients!.first.address,
style: STextStyles.itemSubtitle(context),
),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(top: 16, left: 32),
child: Text(
"Transaction fee",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(top: 10, left: 32, right: 32),
child: RoundedContainer(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
),
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
child: SelectableText(
ref.watch(pAmountFormatter(coin)).format(fee!),
style: STextStyles.itemSubtitle(context),
),
),
),
if (isDesktop &&
widget.txData.fee != null &&
widget.txData.vSize != null)
Padding(
padding: const EdgeInsets.only(top: 16, left: 32),
child: Text(
"sats/vByte",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
if (isDesktop &&
widget.txData.fee != null &&
widget.txData.vSize != null)
Padding(
padding: const EdgeInsets.only(top: 10, left: 32, right: 32),
child: RoundedContainer(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
),
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
child: SelectableText(
"~${fee!.raw.toInt() ~/ widget.txData.vSize!}",
style: STextStyles.itemSubtitle(context),
),
),
),
if (!isDesktop) const Spacer(),
SizedBox(height: isDesktop ? 23 : 12),
Padding(
padding:
isDesktop
? const EdgeInsets.symmetric(horizontal: 32)
: const EdgeInsets.all(0),
child: RoundedContainer(
padding:
isDesktop
? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
)
: const EdgeInsets.all(12),
color:
Theme.of(
context,
).extension<StackColors>()!.snackBarBackSuccess,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isDesktop ? "Total amount to send" : "Total amount",
style:
isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
)
: STextStyles.titleBold12(context).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
),
),
SelectableText(
ref
.watch(pAmountFormatter(coin))
.format(amountWithoutChange + fee!),
style:
isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
)
: STextStyles.itemSubtitle12(context).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
),
textAlign: TextAlign.right,
),
],
),
),
),
SizedBox(height: isDesktop ? 28 : 16),
Padding(
padding:
isDesktop
? const EdgeInsets.symmetric(horizontal: 32)
: const EdgeInsets.all(0),
child: PrimaryButton(
label: "Send",
buttonHeight: isDesktop ? ButtonHeight.l : null,
onPressed: () async {
final dynamic unlocked;
if (isDesktop) {
unlocked = await showDialog<bool?>(
context: context,
builder:
(context) => DesktopDialog(
maxWidth: 580,
maxHeight: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [DesktopDialogCloseButton()],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: DesktopAuthSend(coin: coin),
),
],
),
),
);
} else {
unlocked = await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder:
(_) => const LockscreenView(
showBackButton: true,
popOnSuccess: true,
routeOnSuccessArguments: true,
routeOnSuccess: "",
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to send transaction",
biometricsAuthenticationTitle:
"Confirm Transaction",
),
settings: const RouteSettings(
name: "/confirmsendlockscreen",
),
),
);
}
if (mounted) {
if (unlocked == true) {
unawaited(_attemptSend());
} else {
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message:
Util.isDesktop
? "Invalid passphrase"
: "Invalid PIN",
context: context,
),
);
}
}
}
},
),
),
if (isDesktop) const SizedBox(height: 32),
],
),
),
);
}
}

View file

@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_app_bar.dart';
import '../../widgets/desktop/desktop_scaffold.dart';
import '../../widgets/toggle.dart';
import 'sub_widgets/buy_spark_name_option_widget.dart';
import 'sub_widgets/manage_spark_names_option_widget.dart';
class SparkNamesHomeView extends ConsumerStatefulWidget {
const SparkNamesHomeView({super.key, required this.walletId});
final String walletId;
static const String routeName = "/sparkNamesHomeView";
@override
ConsumerState<SparkNamesHomeView> createState() =>
_NamecoinNamesHomeViewState();
}
class _NamecoinNamesHomeViewState extends ConsumerState<SparkNamesHomeView> {
bool _onManage = true;
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final isDesktop = Util.isDesktop;
return MasterScaffold(
isDesktop: isDesktop,
appBar:
isDesktop
? DesktopAppBar(
isCompactHeight: true,
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 24, right: 20),
child: AppBarIconButton(
size: 32,
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color:
Theme.of(
context,
).extension<StackColors>()!.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
),
SvgPicture.asset(
Assets.svg.robotHead,
width: 32,
height: 32,
color:
Theme.of(context).extension<StackColors>()!.textDark,
),
const SizedBox(width: 10),
Text("Names", style: STextStyles.desktopH3(context)),
],
),
)
: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
titleSpacing: 0,
title: Text(
"Names",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: ConditionalParent(
condition: !isDesktop,
builder:
(child) => SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
child: child,
),
),
child:
Util.isDesktop
? Padding(
padding: const EdgeInsets.only(top: 24, left: 24, right: 24),
child: Row(
children: [
SizedBox(
width: 460,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Text(
"Register",
style: STextStyles.desktopTextExtraSmall(
context,
).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
],
),
const SizedBox(height: 14),
Flexible(
child: BuySparkNameOptionWidget(
walletId: widget.walletId,
),
),
],
),
),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Text(
"Names",
style: STextStyles.desktopTextExtraSmall(
context,
).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
],
),
const SizedBox(height: 14),
Flexible(
child: SingleChildScrollView(
child: ManageSparkNamesOptionWidget(
walletId: widget.walletId,
),
),
),
],
),
),
],
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 48,
child: Toggle(
key: UniqueKey(),
onColor:
Theme.of(context).extension<StackColors>()!.popupBG,
offColor:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
onText: "Register",
offText: "Names",
isOn: !_onManage,
onValueChanged: (value) {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_onManage = !value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
const SizedBox(height: 16),
Expanded(
child: IndexedStack(
index: _onManage ? 0 : 1,
children: [
BuySparkNameOptionWidget(walletId: widget.walletId),
LayoutBuilder(
builder: (context, constraints) {
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: SingleChildScrollView(
child: IntrinsicHeight(
child: ManageSparkNamesOptionWidget(
walletId: widget.walletId,
),
),
),
);
},
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,352 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../providers/providers.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/dialogs/s_dialog.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/stack_dialog.dart';
import '../buy_spark_name_view.dart';
class BuySparkNameOptionWidget extends ConsumerStatefulWidget {
const BuySparkNameOptionWidget({super.key, required this.walletId});
final String walletId;
@override
ConsumerState<BuySparkNameOptionWidget> createState() =>
_BuySparkNameWidgetState();
}
class _BuySparkNameWidgetState extends ConsumerState<BuySparkNameOptionWidget> {
final _nameController = TextEditingController();
final _nameFieldFocus = FocusNode();
bool _isAvailable = false;
String? _lastLookedUpName;
Future<bool> _checkIsAvailable(String name) async {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as SparkInterface;
final names = await wallet.electrumXClient.getSparkNames();
return !names.map((e) => e.toLowerCase()).contains(name.toLowerCase());
}
bool _lookupLock = false;
Future<void> _lookup() async {
if (_lookupLock) return;
_lookupLock = true;
try {
_isAvailable = false;
_lastLookedUpName = _nameController.text;
final result = await showLoading(
whileFuture: _checkIsAvailable(_lastLookedUpName!),
context: context,
message: "Searching...",
onException: (e) => throw e,
rootNavigator: Util.isDesktop,
delay: const Duration(seconds: 2),
);
_isAvailable = result == true;
if (mounted) {
setState(() {});
}
Logging.instance.i("LOOKUP RESULT: $result");
} catch (e, s) {
Logging.instance.e("_lookup failed", error: e, stackTrace: s);
String? err;
if (e.toString().contains("Contains invalid characters")) {
err = "Contains invalid characters";
}
if (mounted) {
await showDialog<void>(
context: context,
builder:
(_) => StackOkDialog(
title: "Spark name lookup failed",
message: err,
desktopPopRootNavigator: Util.isDesktop,
maxWidth: Util.isDesktop ? 600 : null,
),
);
}
} finally {
_lookupLock = false;
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_nameFieldFocus.requestFocus();
}
});
}
@override
void dispose() {
_nameController.dispose();
_nameFieldFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final double dotBitBoxLength = Util.isDesktop ? 100 : 74;
return Column(
crossAxisAlignment:
Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: Container(
height: 48,
width: 100,
decoration: BoxDecoration(
color:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
Constants.size.circularBorderRadius,
), // Adjust radius as needed
bottomLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
inputFormatters: [
LengthLimitingTextInputFormatter(kMaxNameLength),
],
textInputAction: TextInputAction.search,
focusNode: _nameFieldFocus,
controller: _nameController,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
prefixIcon: Padding(
padding: const EdgeInsets.all(14),
child: SvgPicture.asset(
Assets.svg.search,
width: 20,
height: 20,
color:
Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultSearchIconLeft,
),
),
fillColor: Colors.transparent,
hintText: "Find a spark name",
hintStyle: STextStyles.fieldLabel(context),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
onSubmitted: (_) {
if (_nameController.text.isNotEmpty) {
_lookup();
}
},
onChanged: (value) {
// trigger look up button enabled/disabled state change
setState(() {});
},
),
),
],
),
),
),
],
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.only(right: dotBitBoxLength),
child: Builder(
builder: (context) {
final length = _nameController.text.length;
return Text(
"$length/$kMaxNameLength",
style: STextStyles.w500_10(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textSubtitle2,
),
);
},
),
),
],
),
SizedBox(height: Util.isDesktop ? 24 : 16),
SecondaryButton(
label: "Lookup",
enabled: _nameController.text.isNotEmpty,
// width: Util.isDesktop ? 160 : double.infinity,
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: _lookup,
),
const SizedBox(height: 32),
if (_lastLookedUpName != null)
_NameCard(
walletId: widget.walletId,
isAvailable: _isAvailable,
name: _lastLookedUpName!,
),
],
);
}
}
class _NameCard extends ConsumerWidget {
const _NameCard({
super.key,
required this.walletId,
required this.isAvailable,
required this.name,
});
final String walletId;
final bool isAvailable;
final String name;
@override
Widget build(BuildContext context, WidgetRef ref) {
final availability = isAvailable ? "Available" : "Unavailable";
final color =
isAvailable
? Theme.of(context).extension<StackColors>()!.accentColorGreen
: Theme.of(context).extension<StackColors>()!.accentColorRed;
final style =
(Util.isDesktop
? STextStyles.w500_16(context)
: STextStyles.w500_12(context));
return RoundedWhiteContainer(
padding: EdgeInsets.all(Util.isDesktop ? 24 : 16),
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name, style: style),
const SizedBox(height: 4),
Text(availability, style: style.copyWith(color: color)),
],
),
),
Column(
children: [
PrimaryButton(
label: "Buy name",
enabled: isAvailable,
buttonHeight:
Util.isDesktop ? ButtonHeight.m : ButtonHeight.l,
width: Util.isDesktop ? 140 : 120,
onPressed: () async {
if (context.mounted) {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
builder:
(context) => SDialog(
child: SizedBox(
width: 580,
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Buy name",
style: STextStyles.desktopH3(
context,
),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: BuySparkNameView(
walletId: walletId,
name: name,
),
),
],
),
),
),
);
} else {
await Navigator.of(context).pushNamed(
BuySparkNameView.routeName,
arguments: (walletId: walletId, name: name),
);
}
}
},
),
],
),
],
),
),
);
}
}

View file

@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:namecoin/namecoin.dart';
import '../../../models/isar/models/blockchain_data/utxo.dart';
import '../../../providers/db/main_db_provider.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import 'owned_spark_name_card.dart';
class ManageSparkNamesOptionWidget extends ConsumerStatefulWidget {
const ManageSparkNamesOptionWidget({super.key, required this.walletId});
final String walletId;
@override
ConsumerState<ManageSparkNamesOptionWidget> createState() =>
_ManageSparkNamesWidgetState();
}
class _ManageSparkNamesWidgetState
extends ConsumerState<ManageSparkNamesOptionWidget> {
double _tempWidth = 0;
double? _width;
int _count = 0;
void _sillyHack(double value, int length) {
if (value > _tempWidth) _tempWidth = value;
_count++;
if (_count == length) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_width = _tempWidth;
_tempWidth = 0;
});
}
});
}
}
@override
Widget build(BuildContext context) {
final height = ref.watch(pWalletChainHeight(widget.walletId));
return StreamBuilder(
stream: ref.watch(
mainDBProvider.select(
(s) => s.isar.utxos
.where()
.walletIdEqualTo(widget.walletId)
.filter()
.otherDataIsNotNull()
.watch(fireImmediately: true),
),
),
builder: (context, snapshot) {
List<(UTXO, OpNameData)> list = [];
if (snapshot.hasData) {
list = snapshot.data!
.map((utxo) {
final data = jsonDecode(utxo.otherData!) as Map;
final nameData =
jsonDecode(data["nameOpData"] as String) as Map;
return (
utxo,
OpNameData(nameData.cast(), utxo.blockHeight ?? height),
);
})
.toList(growable: false);
}
return Column(
children: [
...list.map(
(e) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: OwnedSparkNameCard(
key: ValueKey(e),
utxo: e.$1,
opNameData: e.$2,
firstColWidth: _width,
calculatedFirstColWidth:
(value) => _sillyHack(value, list.length),
),
),
),
SizedBox(height: Util.isDesktop ? 14 : 6),
],
);
},
);
}
}

View file

@ -0,0 +1,225 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:namecoin/namecoin.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../providers/global/secure_store_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/namecoin_wallet.dart';
import '../../../widgets/conditional_parent.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/dialogs/s_dialog.dart';
import '../../../widgets/rounded_white_container.dart';
import 'spark_name_details.dart';
class OwnedSparkNameCard extends ConsumerStatefulWidget {
const OwnedSparkNameCard({
super.key,
required this.opNameData,
required this.utxo,
this.firstColWidth,
this.calculatedFirstColWidth,
});
final OpNameData opNameData;
final UTXO utxo;
final double? firstColWidth;
final void Function(double)? calculatedFirstColWidth;
@override
ConsumerState<OwnedSparkNameCard> createState() => _OwnedSparkNameCardState();
}
class _OwnedSparkNameCardState extends ConsumerState<OwnedSparkNameCard> {
String? constructedName, value;
(String, Color) _getExpiry(int currentChainHeight, StackColors theme) {
final String message;
final Color color;
if (widget.utxo.blockHash == null) {
message = "Expires in $blocksNameExpiration+ blocks";
color = theme.accentColorGreen;
} else {
final remaining = widget.opNameData.expiredBlockLeft(
currentChainHeight,
false,
);
final semiRemaining = widget.opNameData.expiredBlockLeft(
currentChainHeight,
true,
);
if (remaining == null) {
color = theme.accentColorRed;
message = "Expired";
} else {
message = "Expires in $remaining blocks";
if (semiRemaining == null) {
color = theme.accentColorYellow;
} else {
color = theme.accentColorGreen;
}
}
}
return (message, color);
}
bool _lock = false;
Future<void> _showDetails() async {
if (_lock) return;
_lock = true;
try {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
builder:
(context) => SDialog(
child: SparkNameDetailsView(
utxoId: widget.utxo.id,
walletId: widget.utxo.walletId,
),
),
);
} else {
await Navigator.of(context).pushNamed(
SparkNameDetailsView.routeName,
arguments: (widget.utxo.id, widget.utxo.walletId),
);
}
} finally {
_lock = false;
}
}
void _setName() {
try {
constructedName = widget.opNameData.constructedName;
value = widget.opNameData.value;
} catch (_) {
if (widget.opNameData.op == OpName.nameNew) {
ref
.read(secureStoreProvider)
.read(
key: nameSaltKeyBuilder(
widget.utxo.txid,
widget.utxo.walletId,
widget.utxo.vout,
),
)
.then((onValue) {
if (onValue != null) {
final data =
(jsonDecode(onValue) as Map).cast<String, String>();
WidgetsBinding.instance.addPostFrameCallback((_) {
constructedName = data["name"]!;
value = data["value"]!;
if (mounted) {
setState(() {});
}
});
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
constructedName = "UNKNOWN";
value = "";
if (mounted) {
setState(() {});
}
});
}
});
}
}
}
@override
void initState() {
super.initState();
_setName();
}
double _callbackWidth = 0;
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final (message, color) = _getExpiry(
ref.watch(pWalletChainHeight(widget.utxo.walletId)),
Theme.of(context).extension<StackColors>()!,
);
return RoundedWhiteContainer(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ConditionalParent(
condition: widget.firstColWidth != null && Util.isDesktop,
builder:
(child) => ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.firstColWidth!),
child: child,
),
child: ConditionalParent(
condition: widget.firstColWidth == null && Util.isDesktop,
builder:
(child) => LayoutBuilder(
builder: (context, constraints) {
if (widget.firstColWidth == null &&
_callbackWidth != constraints.maxWidth) {
_callbackWidth = constraints.maxWidth;
widget.calculatedFirstColWidth?.call(_callbackWidth);
}
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
),
child: child,
);
},
),
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(constructedName ?? ""),
const SizedBox(height: 8),
SelectableText(
message,
style: STextStyles.w500_12(
context,
).copyWith(color: color),
),
],
),
),
),
),
if (Util.isDesktop)
Expanded(
child: SelectableText(
value ?? "",
style: STextStyles.w500_12(context),
),
),
if (Util.isDesktop) const SizedBox(width: 12),
PrimaryButton(
label: "Details",
buttonHeight: Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l,
onPressed: _showDetails,
),
],
),
);
}
}

View file

@ -0,0 +1,565 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:namecoin/namecoin.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/secure_store_provider.dart';
import '../../../providers/global/wallets_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/namecoin_wallet.dart';
import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/custom_buttons/simple_copy_button.dart';
import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/rounded_container.dart';
import '../../wallet_view/transaction_views/transaction_details_view.dart';
class SparkNameDetailsView extends ConsumerStatefulWidget {
const SparkNameDetailsView({
super.key,
required this.utxoId,
required this.walletId,
});
static const routeName = "/sparkNameDetails";
final Id utxoId;
final String walletId;
@override
ConsumerState<SparkNameDetailsView> createState() =>
_SparkNameDetailsViewState();
}
class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
late Stream<UTXO?> streamUTXO;
UTXO? utxo;
OpNameData? opNameData;
String? constructedName, value;
Stream<AddressLabel?>? streamLabel;
AddressLabel? label;
void setUtxo(UTXO? utxo, int currentHeight) {
if (utxo != null) {
this.utxo = utxo;
final data = jsonDecode(utxo.otherData!) as Map;
final nameData = jsonDecode(data["nameOpData"] as String) as Map;
opNameData = OpNameData(
nameData.cast(),
utxo.blockHeight ?? currentHeight,
);
_setName();
}
}
void _setName() {
try {
constructedName = opNameData!.constructedName;
value = opNameData!.value;
} catch (_) {
if (opNameData?.op == OpName.nameNew) {
ref
.read(secureStoreProvider)
.read(
key: nameSaltKeyBuilder(utxo!.txid, widget.walletId, utxo!.vout),
)
.then((onValue) {
if (onValue != null) {
final data =
(jsonDecode(onValue) as Map).cast<String, String>();
WidgetsBinding.instance.addPostFrameCallback((_) {
constructedName = data["name"]!;
value = data["value"]!;
if (mounted) {
setState(() {});
}
});
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
constructedName = "UNKNOWN";
value = "";
if (mounted) {
setState(() {});
}
});
}
});
}
}
}
(String, Color) _getExpiry(int currentChainHeight, StackColors theme) {
final String message;
final Color color;
if (utxo?.blockHash == null) {
message = "Expires in $blocksNameExpiration+ blocks";
color = theme.accentColorGreen;
} else {
final remaining = opNameData?.expiredBlockLeft(currentChainHeight, false);
final semiRemaining = opNameData?.expiredBlockLeft(
currentChainHeight,
true,
);
if (remaining == null) {
color = theme.accentColorRed;
message = "Expired";
} else {
message = "Expires in $remaining blocks";
if (semiRemaining == null) {
color = theme.accentColorYellow;
} else {
color = theme.accentColorGreen;
}
}
}
return (message, color);
}
bool _checkConfirmedUtxo(int currentHeight) {
return (ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet)
.checkUtxoConfirmed(utxo!, currentHeight);
}
@override
void initState() {
super.initState();
setUtxo(
ref
.read(mainDBProvider)
.isar
.utxos
.where()
.idEqualTo(widget.utxoId)
.findFirstSync(),
ref.read(pWalletChainHeight(widget.walletId)),
);
_setName();
if (utxo?.address != null) {
label = ref
.read(mainDBProvider)
.getAddressLabelSync(widget.walletId, utxo!.address!);
if (label != null) {
streamLabel = ref.read(mainDBProvider).watchAddressLabel(id: label!.id);
}
}
streamUTXO = ref.read(mainDBProvider).watchUTXO(id: widget.utxoId);
}
@override
Widget build(BuildContext context) {
final currentHeight = ref.watch(pWalletChainHeight(widget.walletId));
final (message, color) = _getExpiry(
currentHeight,
Theme.of(context).extension<StackColors>()!,
);
return ConditionalParent(
condition: !Util.isDesktop,
builder:
(child) => Background(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
// Theme.of(context).extension<StackColors>()!.background,
leading: const AppBarBackButton(),
title: Text(
"Domain details",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(child: child),
),
),
);
},
),
),
),
),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) {
return SizedBox(
width: 641,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Domain details",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
top: 10,
),
child: RoundedContainer(
padding: EdgeInsets.zero,
color: Colors.transparent,
borderColor:
Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
child: child,
),
),
],
),
);
},
child: StreamBuilder(
stream: streamUTXO,
builder: (context, snapshot) {
if (snapshot.hasData) {
setUtxo(snapshot.data!, currentHeight);
}
return utxo == null
? Center(
child: Text(
"Missing output. Was it used recently?",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.accentColorRed,
),
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// if (!isDesktop)
// const SizedBox(
// height: 10,
// ),
RoundedContainer(
padding: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
constructedName ?? "",
style: STextStyles.pageTitleH2(context),
),
if (Util.isDesktop)
SelectableText(
opNameData!.op.name,
style: STextStyles.w500_14(context),
),
],
),
if (!Util.isDesktop)
SelectableText(
opNameData!.op.name,
style: STextStyles.w500_14(context),
),
],
),
),
const _Div(),
RoundedContainer(
padding:
Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Value",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
),
),
],
),
const SizedBox(height: 4),
SelectableText(
value ?? "",
style: STextStyles.w500_14(context),
),
],
),
),
const _Div(),
RoundedContainer(
padding:
Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Address",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
),
),
Util.isDesktop
? IconCopyButton(data: utxo!.address!)
: SimpleCopyButton(data: utxo!.address!),
],
),
const SizedBox(height: 4),
SelectableText(
utxo!.address!,
style: STextStyles.w500_14(context),
),
],
),
),
if (label != null && label!.value.isNotEmpty) const _Div(),
if (label != null && label!.value.isNotEmpty)
RoundedContainer(
padding:
Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Address label",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
Util.isDesktop
? IconCopyButton(data: label!.value)
: SimpleCopyButton(data: label!.value),
],
),
const SizedBox(height: 4),
SelectableText(
label!.value,
style: STextStyles.w500_14(context),
),
],
),
),
const _Div(),
RoundedContainer(
padding:
Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction ID",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
),
),
Util.isDesktop
? IconCopyButton(data: utxo!.txid)
: SimpleCopyButton(data: utxo!.txid),
],
),
const SizedBox(height: 4),
SelectableText(
utxo!.txid,
style: STextStyles.w500_14(context),
),
],
),
),
const _Div(),
RoundedContainer(
padding:
Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Expiry",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
),
),
const SizedBox(height: 4),
SelectableText(
message,
style: STextStyles.w500_14(
context,
).copyWith(color: color),
),
],
),
),
const _Div(),
RoundedContainer(
padding:
Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
color:
Util.isDesktop
? Colors.transparent
: Theme.of(
context,
).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Confirmations",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
),
),
const SizedBox(height: 4),
SelectableText(
"${utxo!.getConfirmations(currentHeight)}",
style: STextStyles.w500_14(context),
),
],
),
),
],
);
},
),
),
);
}
}
class _Div extends StatelessWidget {
const _Div({super.key});
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return Container(
width: double.infinity,
height: 1.0,
color: Theme.of(context).extension<StackColors>()!.textFieldDefaultBG,
);
} else {
return const SizedBox(height: 12);
}
}
}

View file

@ -98,6 +98,7 @@ import '../send_view/frost_ms/frost_send_view.dart';
import '../send_view/send_view.dart';
import '../settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import '../settings_views/wallet_settings_view/wallet_settings_view.dart';
import '../spark_names/spark_names_home_view.dart';
import '../special/firo_rescan_recovery_error_dialog.dart';
import '../token_view/my_tokens_view.dart';
import 'sub_widgets/transactions_list.dart';
@ -146,8 +147,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
bool _lelantusRescanRecovery = false;
Future<void> _firoRescanRecovery() async {
final success = await (ref.read(pWallets).getWallet(walletId) as FiroWallet)
.firoRescanRecovery();
final success =
await (ref.read(pWallets).getWallet(walletId) as FiroWallet)
.firoRescanRecovery();
if (success) {
// go into wallet
@ -160,10 +162,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
} else {
// show error message dialog w/ options
if (mounted) {
final shouldRetry = await Navigator.of(context).pushNamed(
FiroRescanRecoveryErrorView.routeName,
arguments: walletId,
);
final shouldRetry = await Navigator.of(
context,
).pushNamed(FiroRescanRecoveryErrorView.routeName, arguments: walletId);
if (shouldRetry is bool && shouldRetry) {
await _firoRescanRecovery();
@ -218,41 +219,39 @@ class _WalletViewState extends ConsumerState<WalletView> {
eventBus =
widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance;
_syncStatusSubscription =
eventBus.on<WalletSyncStatusChangedEvent>().listen(
(event) async {
if (event.walletId == widget.walletId) {
// switch (event.newStatus) {
// case WalletSyncStatus.unableToSync:
// break;
// case WalletSyncStatus.synced:
// break;
// case WalletSyncStatus.syncing:
// break;
// }
setState(() {
_currentSyncStatus = event.newStatus;
});
}
},
);
_syncStatusSubscription = eventBus
.on<WalletSyncStatusChangedEvent>()
.listen((event) async {
if (event.walletId == widget.walletId) {
// switch (event.newStatus) {
// case WalletSyncStatus.unableToSync:
// break;
// case WalletSyncStatus.synced:
// break;
// case WalletSyncStatus.syncing:
// break;
// }
setState(() {
_currentSyncStatus = event.newStatus;
});
}
});
_nodeStatusSubscription =
eventBus.on<NodeConnectionStatusChangedEvent>().listen(
(event) async {
if (event.walletId == widget.walletId) {
// switch (event.newStatus) {
// case NodeConnectionStatus.disconnected:
// break;
// case NodeConnectionStatus.connected:
// break;
// }
setState(() {
_currentNodeStatus = event.newStatus;
});
}
},
);
_nodeStatusSubscription = eventBus
.on<NodeConnectionStatusChangedEvent>()
.listen((event) async {
if (event.walletId == widget.walletId) {
// switch (event.newStatus) {
// case NodeConnectionStatus.disconnected:
// break;
// case NodeConnectionStatus.connected:
// break;
// }
setState(() {
_currentNodeStatus = event.newStatus;
});
}
});
super.initState();
}
@ -379,9 +378,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
callerRouteName: WalletView.routeName,
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
await Navigator.of(context).pushNamed(FrostStepScaffold.routeName);
}
Future<void> _onExchangePressed(BuildContext context) async {
@ -390,24 +387,27 @@ class _WalletViewState extends ConsumerState<WalletView> {
if (coin.network.isTestNet) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Exchange not available for test net coins",
),
builder:
(_) => const StackOkDialog(
title: "Exchange not available for test net coins",
),
);
} else {
Future<Currency?> _future;
try {
_future = ExchangeDataLoadingService.instance.isar.currencies
.where()
.tickerEqualToAnyExchangeNameName(coin.ticker)
.findFirst();
_future =
ExchangeDataLoadingService.instance.isar.currencies
.where()
.tickerEqualToAnyExchangeNameName(coin.ticker)
.findFirst();
} catch (_) {
_future = ExchangeDataLoadingService.instance.loadAll().then(
(_) => ExchangeDataLoadingService.instance.isar.currencies
(_) =>
ExchangeDataLoadingService.instance.isar.currencies
.where()
.tickerEqualToAnyExchangeNameName(coin.ticker)
.findFirst(),
);
);
}
final currency = await showLoading(
@ -436,9 +436,10 @@ class _WalletViewState extends ConsumerState<WalletView> {
if (coin.network.isTestNet) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Buy not available for test net coins",
),
builder:
(_) => const StackOkDialog(
title: "Buy not available for test net coins",
),
);
} else {
if (mounted) {
@ -458,13 +459,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
unawaited(
showDialog(
context: context,
builder: (context) => WillPopScope(
child: const CustomLoadingOverlay(
message: "Anonymizing balance",
eventBus: null,
),
onWillPop: () async => shouldPop,
),
builder:
(context) => WillPopScope(
child: const CustomLoadingOverlay(
message: "Anonymizing balance",
eventBus: null,
),
onWillPop: () async => shouldPop,
),
),
);
final firoWallet = ref.read(pWallets).getWallet(walletId) as FiroWallet;
@ -473,9 +475,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
if (publicBalance <= Amount.zero) {
shouldPop = true;
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(WalletView.routeName),
);
Navigator.of(
context,
).popUntil(ModalRoute.withName(WalletView.routeName));
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
@ -492,9 +494,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
await firoWallet.anonymizeAllSpark();
shouldPop = true;
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(WalletView.routeName),
);
Navigator.of(
context,
).popUntil(ModalRoute.withName(WalletView.routeName));
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
@ -506,15 +508,16 @@ class _WalletViewState extends ConsumerState<WalletView> {
} catch (e) {
shouldPop = true;
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(WalletView.routeName),
);
Navigator.of(
context,
).popUntil(ModalRoute.withName(WalletView.routeName));
await showDialog<dynamic>(
context: context,
builder: (_) => StackOkDialog(
title: "Anonymize all failed",
message: "Reason: $e",
),
builder:
(_) => StackOkDialog(
title: "Anonymize all failed",
message: "Reason: $e",
),
);
}
}
@ -549,37 +552,46 @@ class _WalletViewState extends ConsumerState<WalletView> {
eventBus: null,
textColor:
Theme.of(context).extension<StackColors>()!.textDark,
actionButton: _lelantusRescanRecovery
? null
: SecondaryButton(
label: "Cancel",
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => StackDialog(
title: "Warning!",
message: "Skipping this process can completely"
" break your wallet. It is only meant to be done in"
" emergency situations where the migration fails"
" and will not let you continue. Still skip?",
leftButton: SecondaryButton(
label: "Cancel",
onPressed:
Navigator.of(context, rootNavigator: true)
.pop,
),
rightButton: SecondaryButton(
label: "Ok",
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop();
setState(() => _rescanningOnOpen = false);
},
),
),
);
},
),
actionButton:
_lelantusRescanRecovery
? null
: SecondaryButton(
label: "Cancel",
onPressed: () async {
await showDialog<void>(
context: context,
builder:
(context) => StackDialog(
title: "Warning!",
message:
"Skipping this process can completely"
" break your wallet. It is only meant to be done in"
" emergency situations where the migration fails"
" and will not let you continue. Still skip?",
leftButton: SecondaryButton(
label: "Cancel",
onPressed:
Navigator.of(
context,
rootNavigator: true,
).pop,
),
rightButton: SecondaryButton(
label: "Ok",
onPressed: () {
Navigator.of(
context,
rootNavigator: true,
).pop();
setState(
() => _rescanningOnOpen = false,
);
},
),
),
);
},
),
),
),
],
@ -605,15 +617,11 @@ class _WalletViewState extends ConsumerState<WalletView> {
title: Row(
children: [
SvgPicture.file(
File(
ref.watch(coinIconProvider(coin)),
),
File(ref.watch(coinIconProvider(coin))),
width: 24,
height: 24,
),
const SizedBox(
width: 16,
),
const SizedBox(width: 16),
Expanded(
child: Text(
ref.watch(pWalletName(walletId)),
@ -625,15 +633,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
actions: [
const Padding(
padding: EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: SmallTorIcon(),
),
padding: EdgeInsets.only(top: 10, bottom: 10, right: 10),
child: AspectRatio(aspectRatio: 1, child: SmallTorIcon()),
),
Padding(
padding: const EdgeInsets.only(
@ -649,9 +650,10 @@ class _WalletViewState extends ConsumerState<WalletView> {
key: const Key("walletViewRadioButton"),
size: 36,
shadows: const [],
color: Theme.of(context)
.extension<StackColors>()!
.background,
color:
Theme.of(
context,
).extension<StackColors>()!.background,
icon: _buildNetworkIcon(_currentSyncStatus),
onPressed: () {
Navigator.of(context).pushNamed(
@ -680,91 +682,105 @@ class _WalletViewState extends ConsumerState<WalletView> {
key: const Key("walletViewAlertsButton"),
size: 36,
shadows: const [],
color: Theme.of(context)
.extension<StackColors>()!
.background,
icon: ref.watch(
notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(walletId),
),
)
? SvgPicture.file(
File(
ref.watch(
themeProvider.select(
(value) => value.assets.bellNew,
color:
Theme.of(
context,
).extension<StackColors>()!.background,
icon:
ref.watch(
notificationsProvider.select(
(value) => value
.hasUnreadNotificationsFor(walletId),
),
)
? SvgPicture.file(
File(
ref.watch(
themeProvider.select(
(value) => value.assets.bellNew,
),
),
),
width: 20,
height: 20,
color:
ref.watch(
notificationsProvider.select(
(value) => value
.hasUnreadNotificationsFor(
walletId,
),
),
)
? null
: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
)
: SvgPicture.asset(
Assets.svg.bell,
width: 20,
height: 20,
color:
ref.watch(
notificationsProvider.select(
(value) => value
.hasUnreadNotificationsFor(
walletId,
),
),
)
? null
: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
width: 20,
height: 20,
color: ref.watch(
notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(
walletId,
),
),
)
? null
: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
)
: SvgPicture.asset(
Assets.svg.bell,
width: 20,
height: 20,
color: ref.watch(
notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(
walletId,
),
),
)
? null
: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () {
// reset unread state
ref.refresh(unreadNotificationsStateProvider);
Navigator.of(context)
.pushNamed(
NotificationsView.routeName,
arguments: walletId,
)
NotificationsView.routeName,
arguments: walletId,
)
.then((_) {
final Set<int> unreadNotificationIds = ref
.read(unreadNotificationsStateProvider.state)
.state;
if (unreadNotificationIds.isEmpty) return;
final Set<int> unreadNotificationIds =
ref
.read(
unreadNotificationsStateProvider
.state,
)
.state;
if (unreadNotificationIds.isEmpty) return;
final List<Future<dynamic>> futures = [];
for (int i = 0;
i < unreadNotificationIds.length - 1;
i++) {
futures.add(
ref.read(notificationsProvider).markAsRead(
unreadNotificationIds.elementAt(i),
false,
),
);
}
// wait for multiple to update if any
Future.wait(futures).then((_) {
// only notify listeners once
ref.read(notificationsProvider).markAsRead(
unreadNotificationIds.last,
true,
final List<Future<dynamic>> futures = [];
for (
int i = 0;
i < unreadNotificationIds.length - 1;
i++
) {
futures.add(
ref
.read(notificationsProvider)
.markAsRead(
unreadNotificationIds.elementAt(i),
false,
),
);
});
});
}
// wait for multiple to update if any
Future.wait(futures).then((_) {
// only notify listeners once
ref
.read(notificationsProvider)
.markAsRead(
unreadNotificationIds.last,
true,
);
});
});
},
),
),
@ -783,14 +799,16 @@ class _WalletViewState extends ConsumerState<WalletView> {
key: const Key("walletViewSettingsButton"),
size: 36,
shadows: const [],
color: Theme.of(context)
.extension<StackColors>()!
.background,
color:
Theme.of(
context,
).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.bars,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
color:
Theme.of(
context,
).extension<StackColors>()!.accentColorDark,
width: 20,
height: 20,
),
@ -818,29 +836,25 @@ class _WalletViewState extends ConsumerState<WalletView> {
Theme.of(context).extension<StackColors>()!.background,
child: Column(
children: [
const SizedBox(
height: 10,
),
const SizedBox(height: 10),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: WalletSummary(
walletId: walletId,
aspectRatio: 1.75,
initialSyncStatus: ref
.watch(pWallets)
.getWallet(walletId)
.refreshMutex
.isLocked
? WalletSyncStatus.syncing
: WalletSyncStatus.synced,
initialSyncStatus:
ref
.watch(pWallets)
.getWallet(walletId)
.refreshMutex
.isLocked
? WalletSyncStatus.syncing
: WalletSyncStatus.synced,
),
),
),
if (isSparkWallet)
const SizedBox(
height: 10,
),
if (isSparkWallet) const SizedBox(height: 10),
if (isSparkWallet)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -856,51 +870,59 @@ class _WalletViewState extends ConsumerState<WalletView> {
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => StackDialog(
title: "Attention!",
message:
"You're about to anonymize all of your public funds.",
leftButton: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
builder:
(context) => StackDialog(
title: "Attention!",
message:
"You're about to anonymize all of your public funds.",
leftButton: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
"Cancel",
style: STextStyles.button(
context,
).copyWith(
color:
Theme.of(context)
.extension<
StackColors
>()!
.accentColorDark,
),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop();
unawaited(attemptAnonymize());
},
style: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
.getPrimaryEnabledButtonStyle(
context,
),
child: Text(
"Continue",
style: STextStyles.button(
context,
),
),
),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop();
unawaited(attemptAnonymize());
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(
context,
),
child: Text(
"Continue",
style:
STextStyles.button(context),
),
),
),
);
},
child: Text(
"Anonymize funds",
style:
STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
style: STextStyles.button(
context,
).copyWith(
color:
Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
),
@ -908,9 +930,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
],
),
),
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
@ -918,11 +938,13 @@ class _WalletViewState extends ConsumerState<WalletView> {
children: [
Text(
"Transactions",
style:
STextStyles.itemSubtitle(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
style: STextStyles.itemSubtitle(
context,
).copyWith(
color:
Theme.of(
context,
).extension<StackColors>()!.textDark3,
),
),
CustomTextButton(
@ -943,9 +965,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
],
),
),
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -970,11 +990,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
Colors.transparent,
Colors.white,
],
stops: [
0.0,
0.8,
1.0,
],
stops: [0.0, 0.8, 1.0],
).createShader(bounds);
},
child: Container(
@ -989,17 +1005,20 @@ class _WalletViewState extends ConsumerState<WalletView> {
CrossAxisAlignment.stretch,
children: [
Expanded(
child: ref
.read(pWallets)
.getWallet(widget.walletId)
.isarTransactionVersion ==
2
? TransactionsV2List(
walletId: widget.walletId,
)
: TransactionsList(
walletId: walletId,
),
child:
ref
.read(pWallets)
.getWallet(
widget.walletId,
)
.isarTransactionVersion ==
2
? TransactionsV2List(
walletId: widget.walletId,
)
: TransactionsList(
walletId: walletId,
),
),
],
),
@ -1059,10 +1078,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
wallet is BitcoinFrostWallet
? FrostSendView.routeName
: SendView.routeName,
arguments: (
walletId: walletId,
coin: coin,
),
arguments: (walletId: walletId, coin: coin),
);
},
),
@ -1089,10 +1105,11 @@ class _WalletViewState extends ConsumerState<WalletView> {
moreItems: [
if (ref.watch(
pWallets.select(
(value) => value
.getWallet(widget.walletId)
.cryptoCurrency
.hasTokenSupport,
(value) =>
value
.getWallet(widget.walletId)
.cryptoCurrency
.hasTokenSupport,
),
))
WalletNavigationBarItemData(
@ -1111,9 +1128,10 @@ class _WalletViewState extends ConsumerState<WalletView> {
Assets.svg.monkey,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
color:
Theme.of(
context,
).extension<StackColors>()!.bottomNavIconIcon,
),
label: "MonKey",
onTap: () {
@ -1185,6 +1203,17 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (wallet is SparkInterface)
WalletNavigationBarItemData(
label: "Names",
icon: const PaynymNavIcon(),
onTap: () {
Navigator.of(context).pushNamed(
SparkNamesHomeView.routeName,
arguments: widget.walletId,
);
},
),
if (!viewOnly && wallet is PaynymInterface)
WalletNavigationBarItemData(
label: "PayNym",
@ -1193,14 +1222,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
unawaited(
showDialog(
context: context,
builder: (context) => const LoadingIndicator(
width: 100,
),
builder:
(context) => const LoadingIndicator(width: 100),
),
);
final wallet =
ref.read(pWallets).getWallet(widget.walletId);
final wallet = ref
.read(pWallets)
.getWallet(widget.walletId);
final paynymInterface = wallet as PaynymInterface;
@ -1219,10 +1248,10 @@ class _WalletViewState extends ConsumerState<WalletView> {
// check if account exists and for matching code to see if claimed
if (account.value != null &&
account.value!.nonSegwitPaymentCode.claimed
// &&
// account.value!.segwit
) {
account.value!.nonSegwitPaymentCode.claimed
// &&
// account.value!.segwit
) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;

View file

@ -21,6 +21,7 @@ import '../../../../pages/monkey/monkey_view.dart';
import '../../../../pages/namecoin_names/namecoin_names_home_view.dart';
import '../../../../pages/paynym/paynym_claim_view.dart';
import '../../../../pages/paynym/paynym_home_view.dart';
import '../../../../pages/spark_names/spark_names_home_view.dart';
import '../../../../providers/desktop/current_desktop_menu_item.dart';
import '../../../../providers/global/paynym_api_provider.dart';
import '../../../../providers/providers.dart';
@ -99,6 +100,7 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
onFusionPressed: _onFusionPressed,
onChurnPressed: _onChurnPressed,
onNamesPressed: _onNamesPressed,
onSparkNamesPressed: _onSparkNamesPressed,
),
);
}
@ -371,6 +373,14 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
).pushNamed(NamecoinNamesHomeView.routeName, arguments: widget.walletId);
}
void _onSparkNamesPressed() {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(
context,
).pushNamed(SparkNamesHomeView.routeName, arguments: widget.walletId);
}
@override
Widget build(BuildContext context) {
final wallet = ref.watch(pWallets).getWallet(widget.walletId);

View file

@ -67,6 +67,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget {
required this.onFusionPressed,
required this.onChurnPressed,
required this.onNamesPressed,
required this.onSparkNamesPressed,
});
final String walletId;
@ -82,6 +83,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget {
final VoidCallback? onFusionPressed;
final VoidCallback? onChurnPressed;
final VoidCallback? onNamesPressed;
final VoidCallback? onSparkNamesPressed;
@override
ConsumerState<MoreFeaturesDialog> createState() => _MoreFeaturesDialogState();
@ -492,6 +494,13 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
iconAsset: Assets.svg.robotHead,
onPressed: () async => widget.onNamesPressed?.call(),
),
if (wallet is SparkInterface)
_MoreFeaturesItem(
label: "Names",
detail: "Spark names",
iconAsset: Assets.svg.robotHead,
onPressed: () async => widget.onSparkNamesPressed?.call(),
),
if (wallet is SparkInterface && !isViewOnly)
_MoreFeaturesClearSparkCacheItem(
cryptoCurrency: wallet.cryptoCurrency,

File diff suppressed because it is too large Load diff

View file

@ -236,22 +236,9 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
);
case CryptoCurrencyNetwork.test:
// NodeModel(
// host: "firo-testnet.stackwallet.com",
// port: 50002,
// name: DefaultNodes.defaultName,
// id: _nodeId(Coin.firoTestNet),
// useSSL: true,
// enabled: true,
// coinName: Coin.firoTestNet.name,
// isFailover: true,
// isDown: false,
// );
// TODO revert to above eventually
return NodeModel(
host: "95.179.164.13",
port: 51002,
host: "firo-testnet.stackwallet.com",
port: 50002,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: true,

View file

@ -64,15 +64,17 @@ class TxData {
final tezart.OperationsList? tezosOperationsList;
// firo spark specific
final List<
({
String address,
Amount amount,
String memo,
bool isChange,
})>? sparkRecipients;
final List<({String address, Amount amount, String memo, bool isChange})>?
sparkRecipients;
final List<TxData>? sparkMints;
final List<SparkCoin>? usedSparkCoins;
final ({
String additionalInfo,
String name,
Address sparkAddress,
int validBlocks,
})?
sparkNameInfo;
// xelis specific
final String? otherData;
@ -122,6 +124,7 @@ class TxData {
this.tempTx,
this.ignoreCachedBalanceChecks = false,
this.opNameState,
this.sparkNameInfo,
});
Amount? get amount {
@ -201,9 +204,10 @@ class TxData {
}
}
int? get estimatedSatsPerVByte => fee != null && vSize != null
? (fee!.raw ~/ BigInt.from(vSize!)).toInt()
: null;
int? get estimatedSatsPerVByte =>
fee != null && vSize != null
? (fee!.raw ~/ BigInt.from(vSize!)).toInt()
: null;
TxData copyWith({
FeeRateType? feeRateType,
@ -237,19 +241,20 @@ class TxData {
TransactionSubType? txSubType,
List<Map<String, dynamic>>? mintsMapLelantus,
tezart.OperationsList? tezosOperationsList,
List<
({
String address,
Amount amount,
String memo,
bool isChange,
})>?
sparkRecipients,
List<({String address, Amount amount, String memo, bool isChange})>?
sparkRecipients,
List<TxData>? sparkMints,
List<SparkCoin>? usedSparkCoins,
TransactionV2? tempTx,
bool? ignoreCachedBalanceChecks,
NameOpState? opNameState,
({
String additionalInfo,
String name,
Address sparkAddress,
int validBlocks,
})?
sparkNameInfo,
}) {
return TxData(
feeRateType: feeRateType ?? this.feeRateType,
@ -290,11 +295,13 @@ class TxData {
ignoreCachedBalanceChecks:
ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks,
opNameState: opNameState ?? this.opNameState,
sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo,
);
}
@override
String toString() => 'TxData{'
String toString() =>
'TxData{'
'feeRateType: $feeRateType, '
'feeRateAmount: $feeRateAmount, '
'satsPerVByte: $satsPerVByte, '
@ -331,5 +338,6 @@ class TxData {
'tempTx: $tempTx, '
'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, '
'opNameState: $opNameState, '
'sparkNameInfo: $sparkNameInfo, '
'}';
}

View file

@ -26,6 +26,7 @@ import '../../../utilities/enums/derive_path_type_enum.dart';
import '../../../utilities/extensions/extensions.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/prefs.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../../isar/models/spark_coin.dart';
import '../../isar/models/wallet_info.dart';
@ -639,6 +640,26 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
}
extractedTx.setPayload(spend.serializedSpendPayload);
if (txData.sparkNameInfo != null) {
// this is name reg tx
final nameScript = LibSpark.createSparkNameScript(
sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks,
name: txData.sparkNameInfo!.name,
additionalInfo: txData.sparkNameInfo!.additionalInfo,
scalarHex: extractedTx.getHash().toHex,
privateKeyHex: privateKey.toHex,
spendKeyIndex: kDefaultSparkIndex,
diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex,
isTestNet: cryptoCurrency.network != CryptoCurrencyNetwork.main,
);
extractedTx.setPayload(
Uint8List.fromList([...spend.serializedSpendPayload, ...nameScript]),
);
}
final rawTxHex = extractedTx.toHex();
if (isSendAll) {
@ -1975,6 +1996,80 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
return txData.copyWith(sparkMints: await Future.wait(futures));
}
Future<TxData> prepareSparkNameTransaction({
required String name,
required String address,
required int years,
required String additionalInfo,
}) async {
if (years < 1 || years > kMaxNameRegistrationLengthYears) {
throw Exception("Invalid spark name registration period years: $years");
}
if (name.isEmpty || name.length > kMaxNameLength) {
throw Exception("Invalid spark name length: ${name.length}");
}
if (!RegExp(kNameRegexString).hasMatch(name)) {
throw Exception("Invalid symbols found in spark name: $name");
}
if (additionalInfo.toUint8ListFromUtf8.length >
kMaxAdditionalInfoLengthBytes) {
throw Exception(
"Additional info exceeds $kMaxAdditionalInfoLengthBytes bytes.",
);
}
final sparkAddress = await mainDB.getAddress(walletId, address);
if (sparkAddress == null) {
throw Exception("Address '$address' not found in local DB.");
}
if (sparkAddress.type != AddressType.spark) {
throw Exception("Address '$address' is not a spark address.");
}
final data = (
name: name,
additionalInfo: additionalInfo,
validBlocks: years * 365 * 24 * 24,
sparkAddress: sparkAddress,
);
final String destinationAddress;
switch (cryptoCurrency.network) {
case CryptoCurrencyNetwork.main:
destinationAddress = kStage3DevelopmentFundAddressMainNet;
break;
case CryptoCurrencyNetwork.test:
destinationAddress = kStage3DevelopmentFundAddressTestNet;
break;
default:
throw Exception(
"Invalid network '${cryptoCurrency.network}' for spark name registration.",
);
}
final txData = await prepareSendSpark(
txData: TxData(
sparkNameInfo: data,
recipients: [
(
address: destinationAddress,
amount: Amount.fromDecimal(
Decimal.fromInt(kStandardSparkNamesFee[name.length] * years),
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: false,
),
],
),
);
return txData;
}
@override
Future<void> updateBalance() async {
// call to super to update transparent balance (and lelantus balance if