mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-04-15 02:31:57 +00:00
WIP spark names
This commit is contained in:
parent
bee49df2a9
commit
8eb91507d2
15 changed files with 4226 additions and 1534 deletions
lib
electrumx_rpc
pages
spark_names
buy_spark_name_view.dartconfirm_spark_name_transaction_view.dartspark_names_home_view.dart
sub_widgets
wallet_view
pages_desktop_specific/my_stack_view/wallet_view/sub_widgets
route_generator.dartwallets
|
@ -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);
|
||||
|
|
423
lib/pages/spark_names/buy_spark_name_view.dart
Normal file
423
lib/pages/spark_names/buy_spark_name_view.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
975
lib/pages/spark_names/confirm_spark_name_transaction_view.dart
Normal file
975
lib/pages/spark_names/confirm_spark_name_transaction_view.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
231
lib/pages/spark_names/spark_names_home_view.dart
Normal file
231
lib/pages/spark_names/spark_names_home_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
225
lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart
Normal file
225
lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
565
lib/pages/spark_names/sub_widgets/spark_name_details.dart
Normal file
565
lib/pages/spark_names/sub_widgets/spark_name_details.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
|
|
|
@ -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, '
|
||||
'}';
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue