Merge branch 'firo_cache_refactor' into campfire

This commit is contained in:
julian 2024-06-05 07:59:11 -06:00
commit 5fa6aa9328
62 changed files with 1590 additions and 754 deletions

@ -1 +1 @@
Subproject commit 81a4f74ea068d3d1026c8e564ee9b0b28cee20c4
Subproject commit 4b87151d4914606b911f738a8236a6e54a6d8ecb

View file

@ -83,6 +83,21 @@ PODS:
- SDWebImage/Core (5.13.2)
- share_plus (0.0.1):
- Flutter
- sqlite3 (3.46.0):
- sqlite3/common (= 3.46.0)
- sqlite3/common (3.46.0)
- sqlite3/fts5 (3.46.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.46.0):
- sqlite3/common
- sqlite3/rtree (3.46.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- sqlite3 (~> 3.46.0)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- stack_wallet_backup (0.0.1):
- Flutter
- SwiftProtobuf (1.19.0)
@ -117,6 +132,7 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- stack_wallet_backup (from `.symlinks/plugins/stack_wallet_backup/ios`)
- tor_ffi_plugin (from `.symlinks/plugins/tor_ffi_plugin/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -129,6 +145,7 @@ SPEC REPOS:
- MTBBarcodeScanner
- ReachabilitySwift
- SDWebImage
- sqlite3
- SwiftProtobuf
- SwiftyGif
@ -177,6 +194,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
stack_wallet_backup:
:path: ".symlinks/plugins/stack_wallet_backup/ios"
tor_ffi_plugin:
@ -203,7 +222,7 @@ SPEC CHECKSUMS:
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
frostdart: 4c72b69ccac2f13ede744107db046a125acce597
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
integration_test: 13825b8a9334a850581300559b8839134b124670
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
lelantus: 417f0221260013dfc052cae9cf4b741b6479edba
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
@ -214,6 +233,8 @@ SPEC CHECKSUMS:
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d
sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31
stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03
SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780

View file

@ -422,6 +422,20 @@ class DbVersionMigrator with WalletDB {
// try to continue migrating
return await migrate(12, secureStore: secureStore);
case 12:
// migrate
await _v12(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 13,
);
// try to continue migrating
return await migrate(13, secureStore: secureStore);
default:
// finally return
return;
@ -701,4 +715,15 @@ class DbVersionMigrator with WalletDB {
Future<void> _v11(SecureStorageInterface secureStore) async {
await migrateWalletsToIsar(secureStore: secureStore);
}
Future<void> _v12(SecureStorageInterface secureStore) async {
for (final identifier in ["firo", "firoTestNet"]) {
await DB.instance.deleteBoxFromDisk(
boxName: "${identifier}_anonymitySetSparkCache",
);
await DB.instance.deleteBoxFromDisk(
boxName: "${identifier}_sparkUsedCoinsTagsCache",
);
}
}
}

View file

@ -13,6 +13,7 @@ import 'dart:isolate';
import 'package:cw_core/wallet_info.dart' as xmr;
import 'package:hive/hive.dart';
import 'package:mutex/mutex.dart';
import '../../app_config.dart';
import '../../models/exchange/response_objects/trade.dart';
import '../../models/node_model.dart';
@ -55,12 +56,8 @@ class DB {
// firo only
String _boxNameSetCache({required CryptoCurrency currency}) =>
"${currency.identifier}_anonymitySetCache";
String _boxNameSetSparkCache({required CryptoCurrency currency}) =>
"${currency.identifier}_anonymitySetSparkCache";
String _boxNameUsedSerialsCache({required CryptoCurrency currency}) =>
"${currency.identifier}_usedSerialsCache";
String _boxNameSparkUsedCoinsTagsCache({required CryptoCurrency currency}) =>
"${currency.identifier}_sparkUsedCoinsTagsCache";
Box<NodeModel>? _boxNodeModels;
Box<NodeModel>? _boxPrimaryNodes;
@ -81,7 +78,6 @@ class DB {
final Map<String, Box<dynamic>> _txCacheBoxes = {};
final Map<String, Box<dynamic>> _setCacheBoxes = {};
final Map<String, Box<dynamic>> _setSparkCacheBoxes = {};
final Map<String, Box<dynamic>> _usedSerialsCacheBoxes = {};
final Map<String, Box<dynamic>> _getSparkUsedCoinsTagsCacheBoxes = {};
@ -213,16 +209,6 @@ class DB {
await Hive.openBox<dynamic>(_boxNameSetCache(currency: currency));
}
Future<Box<dynamic>> getSparkAnonymitySetCacheBox({
required CryptoCurrency currency,
}) async {
if (_setSparkCacheBoxes[currency.identifier]?.isOpen != true) {
_setSparkCacheBoxes.remove(currency.identifier);
}
return _setSparkCacheBoxes[currency.identifier] ??=
await Hive.openBox<dynamic>(_boxNameSetSparkCache(currency: currency));
}
Future<void> closeAnonymitySetCacheBox({
required CryptoCurrency currency,
}) async {
@ -241,18 +227,6 @@ class DB {
);
}
Future<Box<dynamic>> getSparkUsedCoinsTagsCacheBox({
required CryptoCurrency currency,
}) async {
if (_getSparkUsedCoinsTagsCacheBoxes[currency.identifier]?.isOpen != true) {
_getSparkUsedCoinsTagsCacheBoxes.remove(currency.identifier);
}
return _getSparkUsedCoinsTagsCacheBoxes[currency.identifier] ??=
await Hive.openBox<dynamic>(
_boxNameSparkUsedCoinsTagsCache(currency: currency),
);
}
Future<void> closeUsedSerialsCacheBox({
required CryptoCurrency currency,
}) async {
@ -266,15 +240,9 @@ class DB {
await deleteAll<dynamic>(boxName: _boxNameTxCache(currency: currency));
if (currency is Firo) {
await deleteAll<dynamic>(boxName: _boxNameSetCache(currency: currency));
await deleteAll<dynamic>(
boxName: _boxNameSetSparkCache(currency: currency),
);
await deleteAll<dynamic>(
boxName: _boxNameUsedSerialsCache(currency: currency),
);
await deleteAll<dynamic>(
boxName: _boxNameSparkUsedCoinsTagsCache(currency: currency),
);
}
}

View file

@ -0,0 +1,122 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:mutex/mutex.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:uuid/uuid.dart';
import '../../electrumx_rpc/electrumx_client.dart';
import '../../utilities/extensions/extensions.dart';
import '../../utilities/logger.dart';
import '../../utilities/stack_file_system.dart';
part 'firo_cache_coordinator.dart';
part 'firo_cache_reader.dart';
part 'firo_cache_writer.dart';
part 'firo_cache_worker.dart';
/// Temporary debugging log function for this file
void _debugLog(Object? object) {
if (kDebugMode) {
Logging.instance.log(
object,
level: LogLevel.Debug,
);
}
}
abstract class _FiroCache {
static const String sqliteDbFileName = "firo_ex_cache.sqlite3";
static Database? _db;
static Database get db {
if (_db == null) {
throw Exception(
"FiroCache.init() must be called before accessing FiroCache.db!",
);
}
return _db!;
}
static Future<void>? _initFuture;
static Future<void> init() => _initFuture ??= _init();
static Future<void> _init() async {
final sqliteDir = await StackFileSystem.applicationSQLiteDirectory();
final file = File("${sqliteDir.path}/$sqliteDbFileName");
final exists = await file.exists();
if (!exists) {
await _createDb(file.path);
}
_db = sqlite3.open(
file.path,
mode: OpenMode.readWrite,
);
}
static Future<void> _deleteAllCache() async {
final start = DateTime.now();
db.execute(
"""
DELETE FROM SparkSet;
DELETE FROM SparkCoin;
DELETE FROM SparkSetCoins;
DELETE FROM SparkUsedCoinTags;
VACUUM;
""",
);
_debugLog(
"_deleteAllCache() "
"duration = ${DateTime.now().difference(start)}",
);
}
static Future<void> _createDb(String file) async {
final db = sqlite3.open(
file,
mode: OpenMode.readWriteCreate,
);
db.execute(
"""
CREATE TABLE SparkSet (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
blockHash TEXT NOT NULL,
setHash TEXT NOT NULL,
groupId INTEGER NOT NULL,
timestampUTC INTEGER NOT NULL,
UNIQUE (blockHash, setHash, groupId)
);
CREATE TABLE SparkCoin (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
serialized TEXT NOT NULL,
txHash TEXT NOT NULL,
context TEXT NOT NULL,
UNIQUE(serialized, txHash, context)
);
CREATE TABLE SparkSetCoins (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
setId INTEGER NOT NULL,
coinId INTEGER NOT NULL,
FOREIGN KEY (setId) REFERENCES SparkSet(id),
FOREIGN KEY (coinId) REFERENCES SparkCoin(id)
);
CREATE TABLE SparkUsedCoinTags (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
tag TEXT NOT NULL UNIQUE
);
""",
);
db.dispose();
}
}

View file

@ -0,0 +1,162 @@
part of 'firo_cache.dart';
/// Wrapper class for [_FiroCache] as [_FiroCache] should eventually be handled in a
/// background isolate and [FiroCacheCoordinator] should manage that isolate
abstract class FiroCacheCoordinator {
static _FiroCacheWorker? _worker;
static bool _init = false;
static Future<void> init() async {
if (_init) {
return;
}
_init = true;
await _FiroCache.init();
_worker = await _FiroCacheWorker.spawn();
}
static Future<void> clearSharedCache() async {
return await _FiroCache._deleteAllCache();
}
static Future<String> getSparkCacheSize() async {
final dir = await StackFileSystem.applicationSQLiteDirectory();
final cacheFile = File("${dir.path}/${_FiroCache.sqliteDbFileName}");
final int bytes;
if (await cacheFile.exists()) {
bytes = await cacheFile.length();
} else {
bytes = 0;
}
if (bytes < 1024) {
return '$bytes B';
} else if (bytes < 1048576) {
final double kbSize = bytes / 1024;
return '${kbSize.toStringAsFixed(2)} KB';
} else if (bytes < 1073741824) {
final double mbSize = bytes / 1048576;
return '${mbSize.toStringAsFixed(2)} MB';
} else {
final double gbSize = bytes / 1073741824;
return '${gbSize.toStringAsFixed(2)} GB';
}
}
static Future<void> runFetchAndUpdateSparkUsedCoinTags(
ElectrumXClient client,
) async {
final count = await FiroCacheCoordinator.getUsedCoinTagsLastAddedRowId();
final unhashedTags = await client.getSparkUnhashedUsedCoinsTags(
startNumber: count,
);
if (unhashedTags.isNotEmpty) {
await _worker!.runTask(
FCTask(
func: FCFuncName._updateSparkUsedTagsWith,
data: unhashedTags,
),
);
}
}
static Future<void> runFetchAndUpdateSparkAnonSetCacheForGroupId(
int groupId,
ElectrumXClient client,
) async {
final blockhashResult =
await FiroCacheCoordinator.getLatestSetInfoForGroupId(
groupId,
);
final blockHash = blockhashResult?.blockHash ?? "";
final json = await client.getSparkAnonymitySet(
coinGroupId: groupId.toString(),
startBlockHash: blockHash.toHexReversedFromBase64,
);
await _worker!.runTask(
FCTask(
func: FCFuncName._updateSparkAnonSetCoinsWith,
data: (groupId, json),
),
);
}
// ===========================================================================
static Future<Set<String>> getUsedCoinTags(int startNumber) async {
final result = await _Reader._getSparkUsedCoinTags(
startNumber,
db: _FiroCache.db,
);
return result.map((e) => e["tag"] as String).toSet();
}
/// This should be the equivalent of counting the number of tags in the db.
/// Assuming the integrity of the data. Faster than actually calling count on
/// a table where no records have been deleted. None should be deleted from
/// this table in practice.
static Future<int> getUsedCoinTagsLastAddedRowId() async {
final result = await _Reader._getUsedCoinTagsLastAddedRowId(
db: _FiroCache.db,
);
if (result.isEmpty) {
return 0;
}
return result.first["highestId"] as int? ?? 0;
}
static Future<bool> checkTagIsUsed(
String tag,
) async {
return await _Reader._checkTagIsUsed(
tag,
db: _FiroCache.db,
);
}
static Future<ResultSet> getSetCoinsForGroupId(
int groupId, {
int? newerThanTimeStamp,
}) async {
return await _Reader._getSetCoinsForGroupId(
groupId,
db: _FiroCache.db,
newerThanTimeStamp: newerThanTimeStamp,
);
}
static Future<
({
String blockHash,
String setHash,
int timestampUTC,
})?> getLatestSetInfoForGroupId(
int groupId,
) async {
final result = await _Reader._getLatestSetInfoForGroupId(
groupId,
db: _FiroCache.db,
);
if (result.isEmpty) {
return null;
}
return (
blockHash: result.first["blockHash"] as String,
setHash: result.first["setHash"] as String,
timestampUTC: result.first["timestampUTC"] as int,
);
}
static Future<bool> checkSetInfoForGroupIdExists(
int groupId,
) async {
return await _Reader._checkSetInfoForGroupIdExists(
groupId,
db: _FiroCache.db,
);
}
}

View file

@ -0,0 +1,103 @@
part of 'firo_cache.dart';
/// Keep all fetch queries in this separate file
abstract class _Reader {
// ===========================================================================
// =============== Spark anonymity set queries ===============================
static Future<ResultSet> _getSetCoinsForGroupId(
int groupId, {
required Database db,
int? newerThanTimeStamp,
}) async {
String query = """
SELECT sc.serialized, sc.txHash, sc.context
FROM SparkSet AS ss
JOIN SparkSetCoins AS ssc ON ss.id = ssc.setId
JOIN SparkCoin AS sc ON ssc.coinId = sc.id
WHERE ss.groupId = $groupId
""";
if (newerThanTimeStamp != null) {
query += " AND ss.timestampUTC"
" > $newerThanTimeStamp";
}
return db.select("$query;");
}
static Future<ResultSet> _getLatestSetInfoForGroupId(
int groupId, {
required Database db,
}) async {
final query = """
SELECT ss.blockHash, ss.setHash, ss.timestampUTC
FROM SparkSet ss
WHERE ss.groupId = $groupId
ORDER BY ss.timestampUTC DESC
LIMIT 1;
""";
return db.select("$query;");
}
static Future<bool> _checkSetInfoForGroupIdExists(
int groupId, {
required Database db,
}) async {
final query = """
SELECT EXISTS (
SELECT 1
FROM SparkSet
WHERE groupId = $groupId
) AS setExists;
""";
return db.select("$query;").first["setExists"] == 1;
}
// ===========================================================================
// =============== Spark used coin tags queries ==============================
static Future<ResultSet> _getSparkUsedCoinTags(
int startNumber, {
required Database db,
}) async {
String query = """
SELECT tag
FROM SparkUsedCoinTags
""";
if (startNumber > 0) {
query += " WHERE id >= $startNumber";
}
return db.select("$query;");
}
static Future<ResultSet> _getUsedCoinTagsLastAddedRowId({
required Database db,
}) async {
const query = """
SELECT MAX(id) AS highestId
FROM SparkUsedCoinTags;
""";
return db.select("$query;");
}
static Future<bool> _checkTagIsUsed(
String tag, {
required Database db,
}) async {
final query = """
SELECT EXISTS (
SELECT 1
FROM SparkUsedCoinTags
WHERE tag = '$tag'
) AS tagExists;
""";
return db.select("$query;").first["tagExists"] == 1;
}
}

View file

@ -0,0 +1,120 @@
part of 'firo_cache.dart';
enum FCFuncName {
_updateSparkAnonSetCoinsWith,
_updateSparkUsedTagsWith,
}
class FCTask {
final id = const Uuid().v4();
final FCFuncName func;
final dynamic data;
FCTask({required this.func, required this.data});
}
class _FiroCacheWorker {
final SendPort _commands;
final ReceivePort _responses;
final Map<String, Completer<Object?>> _activeRequests = {};
Future<Object?> runTask(FCTask task) async {
final completer = Completer<Object?>.sync();
_activeRequests[task.id] = completer;
_commands.send(task);
return await completer.future;
}
static Future<_FiroCacheWorker> spawn() async {
final sqliteDir = await StackFileSystem.applicationSQLiteDirectory();
final dbFilePath = "${sqliteDir.path}/${_FiroCache.sqliteDbFileName}";
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (dynamic initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete(
(
ReceivePort.fromRawReceivePort(initPort),
commandPort,
),
);
};
try {
await Isolate.spawn(
_startWorkerIsolate,
(initPort.sendPort, dbFilePath),
);
} catch (_) {
initPort.close();
rethrow;
}
final (receivePort, sendPort) = await connection.future;
return _FiroCacheWorker._(receivePort, sendPort);
}
_FiroCacheWorker._(this._responses, this._commands) {
_responses.listen(_handleResponsesFromIsolate);
}
void _handleResponsesFromIsolate(dynamic message) {
final (id, error) = message as (String, Object?);
final completer = _activeRequests.remove(id)!;
if (error != null) {
completer.completeError(error);
} else {
completer.complete(id);
}
}
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
Database db,
Mutex mutex,
) {
receivePort.listen((message) {
final task = message as FCTask;
mutex.protect(() async {
try {
final FCResult result;
switch (task.func) {
case FCFuncName._updateSparkAnonSetCoinsWith:
final data = task.data as (int, Map<String, dynamic>);
result = _updateSparkAnonSetCoinsWith(db, data.$2, data.$1);
break;
case FCFuncName._updateSparkUsedTagsWith:
result = _updateSparkUsedTagsWith(db, task.data as List<String>);
break;
}
if (result.success) {
sendPort.send((task.id, null));
} else {
sendPort.send((task.id, result.error!));
}
} catch (e) {
sendPort.send((task.id, e));
}
});
});
}
static void _startWorkerIsolate((SendPort, String) args) {
final receivePort = ReceivePort();
args.$1.send(receivePort.sendPort);
final mutex = Mutex();
final db = sqlite3.open(
args.$2,
mode: OpenMode.readWrite,
);
_handleCommandsToIsolate(receivePort, args.$1, db, mutex);
}
}

View file

@ -0,0 +1,169 @@
part of 'firo_cache.dart';
class FCResult {
final bool success;
final Object? error;
FCResult({required this.success, this.error});
}
// ===========================================================================
// ================== write to spark used tags cache =========================
/// update the sqlite cache
/// Expected json format:
/// returns true if successful, otherwise some exception
FCResult _updateSparkUsedTagsWith(
Database db,
List<String> tags,
) {
// hash the tags here since this function is called in a background isolate
final hashedTags = LibSpark.hashTags(base64Tags: tags);
if (hashedTags.isEmpty) {
// nothing to add, return early
return FCResult(success: true);
}
db.execute("BEGIN;");
try {
for (final tag in hashedTags) {
db.execute(
"""
INSERT OR IGNORE INTO SparkUsedCoinTags (tag)
VALUES (?);
""",
[tag],
);
}
db.execute("COMMIT;");
return FCResult(success: true);
} catch (e) {
db.execute("ROLLBACK;");
return FCResult(success: false, error: e);
}
}
// ===========================================================================
// ================== write to spark anon set cache ==========================
/// update the sqlite cache
/// Expected json format:
/// {
/// "blockHash": "someBlockHash",
/// "setHash": "someSetHash",
/// "coins": [
/// ["serliazed1", "hash1", "context1"],
/// ["serliazed2", "hash2", "context2"],
/// ...
/// ["serliazed3", "hash3", "context3"],
/// ["serliazed4", "hash4", "context4"],
/// ],
/// }
///
/// returns true if successful, otherwise false
FCResult _updateSparkAnonSetCoinsWith(
Database db,
Map<String, dynamic> json,
int groupId,
) {
final blockHash = json["blockHash"] as String;
final setHash = json["setHash"] as String;
final coinsRaw = json["coins"] as List;
if (coinsRaw.isEmpty) {
// no coins to actually insert
return FCResult(success: true);
}
final checkResult = db.select(
"""
SELECT *
FROM SparkSet
WHERE blockHash = ? AND setHash = ? AND groupId = ?;
""",
[
blockHash,
setHash,
groupId,
],
);
if (checkResult.isNotEmpty) {
// already up to date
return FCResult(success: true);
}
final coins = coinsRaw
.map(
(e) => [
e[0] as String,
e[1] as String,
e[2] as String,
],
)
.toList();
final timestamp = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
db.execute("BEGIN;");
try {
db.execute(
"""
INSERT INTO SparkSet (blockHash, setHash, groupId, timestampUTC)
VALUES (?, ?, ?, ?);
""",
[blockHash, setHash, groupId, timestamp],
);
final setId = db.lastInsertRowId;
for (final coin in coins) {
int coinId;
try {
// try to insert and get row id
db.execute(
"""
INSERT INTO SparkCoin (serialized, txHash, context)
VALUES (?, ?, ?);
""",
coin,
);
coinId = db.lastInsertRowId;
} on SqliteException catch (e) {
// if there already is a matching coin in the db
// just grab its row id
if (e.extendedResultCode == 2067) {
final result = db.select(
"""
SELECT id
FROM SparkCoin
WHERE serialized = ? AND txHash = ? AND context = ?;
""",
coin,
);
coinId = result.first["id"] as int;
} else {
rethrow;
}
}
// finally add the row id to the newly added set
db.execute(
"""
INSERT INTO SparkSetCoins (setId, coinId)
VALUES (?, ?);
""",
[setId, coinId],
);
}
db.execute("COMMIT;");
return FCResult(success: true);
} catch (e) {
db.execute("ROLLBACK;");
return FCResult(success: false, error: e);
}
}

View file

@ -116,70 +116,6 @@ class CachedElectrumXClient {
}
}
Future<Map<String, dynamic>> getSparkAnonymitySet({
required String groupId,
String blockhash = "",
required CryptoCurrency cryptoCurrency,
required bool useOnlyCacheIfNotEmpty,
}) async {
try {
final box = await DB.instance.getSparkAnonymitySetCacheBox(
currency: cryptoCurrency,
);
final cachedSet = box.get(groupId) as Map?;
Map<String, dynamic> set;
// null check to see if there is a cached set
if (cachedSet == null) {
set = {
"coinGroupID": int.parse(groupId),
"blockHash": blockhash,
"setHash": "",
"coins": <dynamic>[],
};
} else {
set = Map<String, dynamic>.from(cachedSet);
if (useOnlyCacheIfNotEmpty) {
return set;
}
}
final newSet = await electrumXClient.getSparkAnonymitySet(
coinGroupId: groupId,
startBlockHash: set["blockHash"] as String,
);
// update set with new data
if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) {
set["setHash"] = newSet["setHash"];
set["blockHash"] = newSet["blockHash"];
for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) {
// TODO verify this is correct (or append?)
if ((set["coins"] as List)
.where((e) => e[0] == newSet["coins"][i][0])
.isEmpty) {
set["coins"].insert(0, newSet["coins"][i]);
}
}
// save set to db
await box.put(groupId, set);
Logging.instance.log(
"Updated current anonymity set for ${cryptoCurrency.identifier} with group ID $groupId",
level: LogLevel.Info,
);
}
return set;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getSparkAnonymitySet(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
String base64ToHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
@ -284,53 +220,6 @@ class CachedElectrumXClient {
}
}
Future<Set<String>> getSparkUsedCoinsTags({
required CryptoCurrency cryptoCurrency,
}) async {
try {
final box = await DB.instance.getSparkUsedCoinsTagsCacheBox(
currency: cryptoCurrency,
);
final _list = box.get("tags") as List?;
final Set<String> cachedTags =
_list == null ? {} : List<String>.from(_list).toSet();
final startNumber = max(
0,
cachedTags.length - 100, // 100 being some arbitrary buffer
);
final newTags = await electrumXClient.getSparkUsedCoinsTags(
startNumber: startNumber,
);
// ensure we are getting some overlap so we know we are not missing any
if (cachedTags.isNotEmpty && newTags.isNotEmpty) {
assert(cachedTags.intersection(newTags).isNotEmpty);
}
// Make newTags an Iterable<String>.
final Iterable<String> iterableTags = newTags.map((e) => e.toString());
cachedTags.addAll(iterableTags);
await box.put(
"tags",
cachedTags.toList(),
);
return cachedTags;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getSparkUsedCoinsTags(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
/// Clear all cached transactions for the specified coin
Future<void> clearSharedTransactionCache({
required CryptoCurrency cryptoCurrency,

View file

@ -17,10 +17,9 @@ import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:electrum_adapter/methods/specific/firo.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:mutex/mutex.dart';
import 'client_manager.dart';
import 'package:stream_channel/stream_channel.dart';
import '../exceptions/electrumx/no_such_transaction.dart';
import '../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../services/event_bus/events/global/tor_status_changed_event.dart';
@ -29,7 +28,7 @@ import '../services/tor_service.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'package:stream_channel/stream_channel.dart';
import 'client_manager.dart';
class WifiOnlyException implements Exception {}
@ -910,10 +909,7 @@ class ElectrumXClient {
String? requestID,
}) async {
try {
Logging.instance.log(
"attempting to fetch spark.getsparkanonymityset...",
level: LogLevel.Info,
);
final start = DateTime.now();
await _checkElectrumAdapter();
final Map<String, dynamic> response =
await (getElectrumAdapter() as FiroElectrumClient)
@ -922,7 +918,10 @@ class ElectrumXClient {
startBlockHash: startBlockHash,
);
Logging.instance.log(
"Fetching spark.getsparkanonymityset finished",
"Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId"
"=$coinGroupId, startBlockHash=$startBlockHash). "
"coins.length: ${(response["coins"] as List?)?.length}"
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return response;
@ -933,16 +932,12 @@ class ElectrumXClient {
/// Takes [startNumber], if it is 0, we get the full set,
/// otherwise the used tags after that number
Future<Set<String>> getSparkUsedCoinsTags({
Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int startNumber,
}) async {
try {
// Use electrum_adapter package's getSparkUsedCoinsTags method.
Logging.instance.log(
"attempting to fetch spark.getusedcoinstags...",
level: LogLevel.Info,
);
final start = DateTime.now();
await _checkElectrumAdapter();
final Map<String, dynamic> response =
await (getElectrumAdapter() as FiroElectrumClient)
@ -954,8 +949,16 @@ class ElectrumXClient {
level: LogLevel.Info,
);
final map = Map<String, dynamic>.from(response);
final set = Set<String>.from(map["tags"] as List);
return await compute(_ffiHashTagsComputeWrapper, set);
final tags = List<String>.from(map["tags"] as List);
Logging.instance.log(
"Finished ElectrumXClient.getSparkUnhashedUsedCoinsTags(startNumber"
"=$startNumber). "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return tags;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
@ -1092,7 +1095,3 @@ class ElectrumXClient {
}
}
}
Set<String> _ffiHashTagsComputeWrapper(Set<String> base64Tags) {
return LibSpark.hashTags(base64Tags: base64Tags);
}

View file

@ -35,6 +35,7 @@ import 'app_config.dart';
import 'db/db_version_migration.dart';
import 'db/hive/db.dart';
import 'db/isar/main_db.dart';
import 'db/sqlite/firo_cache.dart';
import 'models/exchange/change_now/exchange_transaction.dart';
import 'models/exchange/change_now/exchange_transaction_status.dart';
import 'models/exchange/response_objects/trade.dart';
@ -200,6 +201,7 @@ void main(List<String> args) async {
}
await StackFileSystem.initThemesDir();
await FiroCacheCoordinator.init();
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
if (!Util.isDesktop) {

View file

@ -124,12 +124,14 @@ const _LoglogLevelEnumValueMap = {
r'Warning': r'Warning',
r'Error': r'Error',
r'Fatal': r'Fatal',
r'Debug': r'Debug',
};
const _LoglogLevelValueEnumMap = {
r'Info': LogLevel.Info,
r'Warning': LogLevel.Warning,
r'Error': LogLevel.Error,
r'Fatal': LogLevel.Fatal,
r'Debug': LogLevel.Debug,
};
Id _logGetId(Log object) {

View file

@ -20,6 +20,7 @@ import '../../../frost_route_generator.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../providers/frost_wallet/frost_wallet_providers.dart';
import '../../../providers/providers.dart';
import '../../../providers/wallet/public_private_balance_state_provider.dart';
import '../../../themes/coin_icon_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/amount/amount.dart';
@ -234,7 +235,10 @@ class _FrostSendViewState extends ConsumerState<FrostSendView> {
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
);
) &&
(coin is Firo
? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public
: true);
return ConditionalParent(
condition: !Util.isDesktop,

View file

@ -998,10 +998,15 @@ class _SendViewState extends ConsumerState<SendView> {
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
);
) &&
(coin is Firo
? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public
: true);
if (isFiro) {
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
selectedUTXOs = {};
if (ref.read(pSendAmount) == null) {
setState(() {
_calculateFeesFuture = calculateFees(

View file

@ -13,6 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../notifications/show_flush_bar.dart';
import '../../../providers/global/debug_service_provider.dart';
import '../../../providers/providers.dart';
@ -284,28 +285,33 @@ class HiddenSettings extends StatelessWidget {
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
//
},
child: RoundedWhiteContainer(
child: Text(
"Do nothing",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
);
},
),
// const SizedBox(
// height: 12,
// ),
// Consumer(
// builder: (_, ref, __) {
// return GestureDetector(
// onTap: () async {
// await showLoading(
// whileFuture: FiroCache.init(),
// context: context,
// rootNavigator: true,
// message: "initializing firo cache",
// );
// },
// child: RoundedWhiteContainer(
// child: Text(
// "init firo_cache",
// style: STextStyles.button(context).copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .accentColorDark,
// ),
// ),
// ),
// );
// },
// ),
],
),
),

View file

@ -16,6 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tuple/tuple.dart';
import '../../../db/hive/db.dart';
import '../../../db/sqlite/firo_cache.dart';
import '../../../models/epicbox_config_model.dart';
import '../../../notifications/show_flush_bar.dart';
import '../../../providers/global/wallets_provider.dart';
@ -413,7 +414,8 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
),
);
if (result == "OK" && mounted) {
if (result == "OK" &&
context.mounted) {
await showLoading(
whileFuture: Future.wait<void>(
[
@ -426,6 +428,9 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
.clearSharedTransactionCache(
currency: coin,
),
if (coin is Firo)
FiroCacheCoordinator
.clearSharedCache(),
],
),
context: context,

View file

@ -8,18 +8,14 @@
*
*/
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../providers/db/main_db_provider.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/isar/models/wallet_info.dart';
import '../../../../wallets/wallet/wallet.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
@ -40,46 +36,26 @@ class LelantusSettingsView extends ConsumerStatefulWidget {
}
class _LelantusSettingsViewState extends ConsumerState<LelantusSettingsView> {
late final TextEditingController _controller;
late final String walletId;
final _focusNode = FocusNode();
bool _isInitialized = false;
Wallet<CryptoCurrency>? wallet;
bool _enableLelantusScanning = false;
bool _isUpdatingLelantusScanning = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInitialized) {
// Get the wallet.
wallet = ref.watch(
pWallets.select(
(value) => value.getWallet(widget.walletId),
),
Future<void> _switchToggled(bool newValue) async {
if (_isUpdatingLelantusScanning) return;
_isUpdatingLelantusScanning = true; // Lock mutex.
try {
// Toggle enableLelantusScanning in wallet info.
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.enableLelantusScanning: newValue,
},
isar: ref.read(mainDBProvider).isar,
);
// Parse otherDataJsonString to get the enableLelantusScanning value.
if (wallet?.info.otherDataJsonString != null) {
final otherDataJson = json.decode(wallet!.info.otherDataJsonString!);
_enableLelantusScanning =
otherDataJson[WalletInfoKeys.enableLelantusScanning] as bool? ??
false;
}
_isInitialized = true; // Ensure this logic runs only once
} finally {
// ensure _isUpdatingLelantusScanning is set to false no matter what
_isUpdatingLelantusScanning = false;
}
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Background(
@ -107,25 +83,12 @@ class _LelantusSettingsViewState extends ConsumerState<LelantusSettingsView> {
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: _enableLelantusScanning,
onValueChanged: (newValue) async {
if (_isUpdatingLelantusScanning) return;
_isUpdatingLelantusScanning = true; // Lock mutex.
// Toggle enableLelantusScanning in wallet info.
await wallet?.info.updateOtherData(
newEntries: {
WalletInfoKeys.enableLelantusScanning:
!_enableLelantusScanning,
},
isar: ref.read(mainDBProvider).isar,
);
setState(() {
_enableLelantusScanning = !_enableLelantusScanning;
_isUpdatingLelantusScanning = false; // Free mutex.
});
},
isOn: ref.watch(
pWalletInfo(widget.walletId)
.select((value) => value.otherData),
)[WalletInfoKeys.enableLelantusScanning] as bool? ??
false,
onValueChanged: _switchToggled,
),
),
const SizedBox(

View file

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../db/sqlite/firo_cache.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/detail_item.dart';
class SparkInfoView extends ConsumerWidget {
const SparkInfoView({
super.key,
});
static const String routeName = "/sparkInfo";
@override
Widget build(BuildContext context, WidgetRef ref) {
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Spark Info",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FutureBuilder(
future: FiroCacheCoordinator.getSparkCacheSize(),
builder: (_, snapshot) {
String detail = "Loading...";
if (snapshot.connectionState == ConnectionState.done) {
detail = snapshot.data ?? detail;
}
return DetailItem(
title: "Spark electrumx cache size",
detail: detail,
);
},
),
],
),
),
),
);
}
}

View file

@ -24,6 +24,7 @@ import '../../../pinpad_views/lock_screen_view.dart';
import 'delete_wallet_warning_view.dart';
import 'lelantus_settings_view.dart';
import 'rename_wallet_view.dart';
import 'spark_info.dart';
class WalletSettingsWalletSettingsView extends ConsumerWidget {
const WalletSettingsWalletSettingsView({
@ -216,6 +217,39 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
),
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
SparkInfoView.routeName,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Spark info",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
],
),
),

View file

@ -16,13 +16,17 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
import '../../../pages/token_view/my_tokens_view.dart';
import '../../../pages/wallet_view/sub_widgets/transactions_list.dart';
import '../../../pages/wallet_view/transaction_views/all_transactions_view.dart';
import '../../../pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart';
import '../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart';
import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/active_wallet_provider.dart';
import '../../../providers/global/auto_swb_service_provider.dart';
import '../../../providers/providers.dart';
@ -35,14 +39,17 @@ import '../../../utilities/assets.dart';
import '../../../utilities/enums/backup_frequency_type.dart';
import '../../../utilities/enums/sync_type_enum.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/wallet_tools.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/banano_wallet.dart';
import '../../../wallets/wallet/impl/firo_wallet.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/custom_buttons/blue_text_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart';
import '../../../widgets/desktop/desktop_scaffold.dart';
import '../../../widgets/hover_text_field.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../coin_control/desktop_coin_control_use_dialog.dart';
import 'sub_widgets/desktop_wallet_features.dart';
import 'sub_widgets/desktop_wallet_summary.dart';
import 'sub_widgets/my_wallet.dart';
@ -129,7 +136,10 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance;
WidgetsBinding.instance.addPostFrameCallback(
(_) => ref.read(currentWalletIdProvider.notifier).state = wallet.walletId,
(_) {
ref.read(currentWalletIdProvider.notifier).state = wallet.walletId;
ref.read(desktopUseUTXOs.notifier).state = {};
},
);
if (!wallet.shouldAutoSync) {
@ -207,17 +217,71 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
),
if (kDebugMode) const Spacer(),
if (kDebugMode)
Row(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Debug Height:",
Row(
children: [
const Text(
"dbgHeight: ",
),
const SizedBox(
width: 2,
),
Text(
ref
.watch(pWalletChainHeight(widget.walletId))
.toString(),
),
],
),
const SizedBox(
width: 2,
),
Text(
ref.watch(pWalletChainHeight(widget.walletId)).toString(),
Row(
children: [
const Text(
"dbgTxCount: ",
),
const SizedBox(
width: 2,
),
Text(
wallet.isarTransactionVersion == 2
? ref
.watch(mainDBProvider)
.isar
.transactionV2s
.where()
.walletIdEqualTo(widget.walletId)
.countSync()
.toString()
: ref
.watch(mainDBProvider)
.isar
.transactions
.where()
.walletIdEqualTo(widget.walletId)
.countSync()
.toString(),
),
],
),
if (wallet.isarTransactionVersion == 2 &&
wallet is FiroWallet)
Row(
children: [
const Text(
"dbgBal: ",
),
const SizedBox(
width: 2,
),
Text(
WalletDevTools.checkFiroTransactionTally(
widget.walletId,
),
),
],
),
],
),
const Spacer(),

View file

@ -17,16 +17,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../models/isar/models/contact_entry.dart';
import '../../../../models/paynym/paynym_account_lite.dart';
import '../../../../models/send_view_auto_fill_data.dart';
import '../../../../pages/send_view/confirm_transaction_view.dart';
import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart';
import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart';
import '../../../coin_control/desktop_coin_control_use_dialog.dart';
import '../../../desktop_home_view.dart';
import 'address_book_address_chooser/address_book_address_chooser.dart';
import 'desktop_fee_dropdown.dart';
import '../../../../providers/providers.dart';
import '../../../../providers/ui/fee_rate_type_state_provider.dart';
import '../../../../providers/ui/preview_tx_button_state_provider.dart';
@ -70,6 +67,10 @@ import '../../../../widgets/icon_widgets/x_icon.dart';
import '../../../../widgets/rounded_container.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.dart';
import '../../../coin_control/desktop_coin_control_use_dialog.dart';
import '../../../desktop_home_view.dart';
import 'address_book_address_chooser/address_book_address_chooser.dart';
import 'desktop_fee_dropdown.dart';
class DesktopSend extends ConsumerStatefulWidget {
const DesktopSend({
@ -947,7 +948,10 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
(value) => value.enableCoinControl,
),
) &&
ref.watch(pWallets).getWallet(walletId) is CoinControlInterface;
ref.watch(pWallets).getWallet(walletId) is CoinControlInterface &&
(coin is Firo
? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public
: true);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -1042,6 +1046,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
],
onChanged: (value) {
if (value is FiroType) {
if (value != FiroType.public) {
ref.read(desktopUseUTXOs.state).state = {};
}
setState(() {
ref.read(publicPrivateBalanceStateProvider.state).state =
value;

View file

@ -23,6 +23,7 @@ import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/flutter_secure_storage_interface.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_app_bar.dart';
@ -68,12 +69,7 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
bool _nextLock = false;
void onNextPressed() async {
if (_nextLock) {
return;
}
_nextLock = true;
Future<void> _onNextPressed() async {
final String passphrase = passwordController.text;
final String repeatPassphrase = passwordRepeatController.text;
@ -85,7 +81,6 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
context: context,
),
);
_nextLock = false;
return;
}
if (passphrase != repeatPassphrase) {
@ -96,19 +91,31 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
context: context,
),
);
_nextLock = false;
return;
}
try {
if (await ref.read(storageCryptoHandlerProvider).hasPassword()) {
throw Exception(
"Tried creating a new password and attempted to overwrite an existing entry!",
);
whileFuture() async {
if (await ref.read(storageCryptoHandlerProvider).hasPassword()) {
throw Exception(
"Tried creating a new password and attempted to overwrite an existing entry!",
);
}
await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase);
await (ref.read(secureStoreProvider).store as DesktopSecureStore)
.init();
}
await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase);
await (ref.read(secureStoreProvider).store as DesktopSecureStore).init();
await showLoading(
whileFuture: whileFuture(),
context: context,
message: "Initializing...",
rootNavigator: true,
onException: (e) {
throw e;
},
);
// load default nodes now as node service requires storage handler to exist
@ -116,14 +123,15 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
await ref.read(nodeServiceChangeNotifierProvider).updateDefaults();
}
} catch (e) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Error: $e",
context: context,
),
);
_nextLock = false;
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Error: $e",
context: context,
),
);
}
return;
}
@ -152,7 +160,19 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
),
);
}
_nextLock = false;
}
void _onNextPressedWrapper() async {
if (_nextLock) {
return;
}
_nextLock = true;
try {
await _onNextPressed();
} finally {
_nextLock = false;
}
}
@override
@ -464,7 +484,7 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
onPressed: nextEnabled ? onNextPressed : null,
onPressed: nextEnabled ? _onNextPressedWrapper : null,
child: Text(
"Next",
style: nextEnabled

View file

@ -21,6 +21,7 @@ import '../../../providers/desktop/storage_crypto_handler_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/text_styles.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/progress_bar.dart';
@ -62,7 +63,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> {
String passwordFeedback =
"Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters.";
Future<bool> attemptChangePW() async {
bool _changePWLock = false;
Future<(bool, FlushBarType, String)> _attemptChangePW() async {
final String pw = passwordCurrentController.text;
final String pwNew = passwordController.text;
final String pwNewRepeat = passwordRepeatController.text;
@ -74,14 +76,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> {
if (pwNew != pwNewRepeat) {
await Future<void>.delayed(const Duration(seconds: 1));
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "New passphrase does not match!",
context: context,
),
);
return false;
return (false, FlushBarType.warning, "New passphrase does not match!");
} else {
final success =
await ref.read(storageCryptoHandlerProvider).changePassphrase(
@ -92,38 +87,21 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> {
if (success) {
await Future<void>.delayed(const Duration(seconds: 1));
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Passphrase successfully changed",
context: context,
),
return (
true,
FlushBarType.success,
"Passphrase successfully changed"
);
return true;
} else {
await Future<void>.delayed(const Duration(seconds: 1));
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase change failed",
context: context,
),
);
return false;
return (false, FlushBarType.warning, "Passphrase change failed");
}
}
} else {
await Future<void>.delayed(const Duration(seconds: 1));
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Current passphrase is not valid!",
context: context,
),
);
return false;
return (false, FlushBarType.warning, "Current passphrase is not valid!");
}
}
@ -522,12 +500,37 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> {
enabled: shouldEnableSave,
label: "Save changes",
onPressed: () async {
final didChangePW =
await attemptChangePW();
if (didChangePW) {
setState(() {
changePassword = false;
});
if (_changePWLock) {
return;
}
_changePWLock = true;
try {
final (didChangePW, type, message) =
(await showLoading(
whileFuture: _attemptChangePW(),
context: context,
message: "Updating...",
rootNavigator: true,
))!;
if (mounted) {
unawaited(
showFloatingFlushBar(
type: type,
message: message,
context: context,
),
);
}
if (didChangePW == true) {
setState(() {
changePassword = false;
});
}
} finally {
_changePWLock = false;
}
},
),

View file

@ -131,6 +131,7 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'pages/special/firo_rescan_recovery_error_dialog.dart';
@ -1966,6 +1967,15 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case SparkInfoView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const SparkInfoView(),
settings: RouteSettings(
name: settings.name,
),
);
// == Desktop specific routes ============================================
case CreatePasswordView.routeName:
if (args is bool) {

View file

@ -40,7 +40,7 @@ abstract class Constants {
// Enable Logger.print statements
static const bool disableLogger = false;
static const int currentDataVersion = 12;
static const int currentDataVersion = 13;
static const int rescanV1 = 1;

View file

@ -14,5 +14,6 @@ enum LogLevel {
Info,
Warning,
Error,
Fatal;
Fatal,
Debug;
}

View file

@ -13,5 +13,6 @@ export 'impl/box_shadow.dart';
export 'impl/cl_transaction.dart';
export 'impl/contract_abi.dart';
export 'impl/gradient.dart';
export 'impl/list.dart';
export 'impl/string.dart';
export 'impl/uint8_list.dart';

View file

@ -0,0 +1,19 @@
extension ListExt<T> on List<T> {
List<List<T>> chunked({required int chunkSize}) {
final remainder = length % chunkSize;
final count = length ~/ chunkSize;
final List<List<T>> result = [];
int i = 0;
while (i < count) {
result.add(sublist(i, i + chunkSize));
i++;
}
if (remainder > 0) {
result.add(sublist(i, i + remainder));
}
return result;
}
}

View file

@ -14,6 +14,7 @@ import 'dart:typed_data';
import 'package:dart_bs58/dart_bs58.dart';
import 'package:dart_bs58check/dart_bs58check.dart';
import 'package:hex/hex.dart';
import '../extensions.dart';
extension StringExtensions on String {
@ -27,4 +28,14 @@ extension StringExtensions on String {
Uint8List get toUint8ListFromBase58CheckEncoded => bs58check.decode(this);
BigInt get toBigIntFromHex => toUint8ListFromHex.toBigInt;
String get toHexFromBase64 => base64Decode(LineSplitter.split(this).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
String get toHexReversedFromBase64 =>
base64Decode(LineSplitter.split(this).join())
.reversed
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
}

View file

@ -91,6 +91,19 @@ abstract class StackFileSystem {
}
}
static Future<Directory> applicationSQLiteDirectory() async {
final root = await applicationRootDirectory();
if (Util.isDesktop) {
final dir = Directory("${root.path}/sqlite");
if (!dir.existsSync()) {
await dir.create();
}
return dir;
} else {
return root;
}
}
static Future<Directory> applicationTorDirectory() async {
final root = await applicationRootDirectory();
if (Util.isDesktop) {

View file

@ -0,0 +1,55 @@
import 'package:isar/isar.dart';
import '../db/isar/main_db.dart';
import '../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'amount/amount.dart';
import 'amount/amount_formatter.dart';
import 'amount/amount_unit.dart';
abstract class WalletDevTools {
static String checkFiroTransactionTally(String walletId) {
final amtFmt = AmountFormatter(
unit: AmountUnit.normal,
locale: "en_US",
coin: Firo(CryptoCurrencyNetwork.main),
maxDecimals: 8,
);
final all = MainDB.instance.isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.findAllSync();
final totalCount = all.length;
BigInt runningBalance = BigInt.zero;
for (final tx in all) {
final ownIns = tx.inputs
.where((e) => e.walletOwns)
.map((e) => e.value)
.fold(BigInt.zero, (p, e) => p + e);
runningBalance -= ownIns;
final ownOuts = tx.outputs
.where((e) => e.walletOwns)
.map((e) => e.value)
.fold(BigInt.zero, (p, e) => p + e);
runningBalance += ownOuts;
}
final balanceAccordingToTxHistory = Amount(
rawValue: runningBalance,
fractionDigits: 8,
);
print("======== $walletId =============");
print("totalTxCount: $totalCount");
print(
"balanceAccordingToTxns: ${amtFmt.format(balanceAccordingToTxHistory)}",
);
print("==================================================");
return amtFmt.format(balanceAccordingToTxHistory);
}
}

View file

@ -282,10 +282,8 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
switch (network) {
case CryptoCurrencyNetwork.main:
return NodeModel(
// host: "ecash.stackwallet.com",
// port: 59002,
host: "electrum.bitcoinabc.org",
port: 50002,
host: "ecash.stackwallet.com",
port: 59002,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: true,

View file

@ -508,4 +508,6 @@ abstract class WalletInfoKeys {
static const String lelantusCoinIsarRescanRequired =
"lelantusCoinIsarRescanRequired";
static const String enableLelantusScanning = "enableLelantusScanningKey";
static const String firoSparkCacheSetTimestampCache =
"firoSparkCacheSetTimestampCacheKey";
}

View file

@ -6,6 +6,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:isar/isar.dart';
import '../../../db/sqlite/firo_cache.dart';
import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
@ -20,6 +21,7 @@ import '../../isar/models/spark_coin.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/lelantus_interface.dart';
import '../wallet_mixin_interfaces/spark_interface.dart';
@ -27,7 +29,11 @@ import '../wallet_mixin_interfaces/spark_interface.dart';
const sparkStartBlock = 819300; // (approx 18 Jan 2024)
class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
with ElectrumXInterface<T>, LelantusInterface<T>, SparkInterface<T> {
with
ElectrumXInterface<T>,
LelantusInterface<T>,
SparkInterface<T>,
CoinControlInterface<T> {
// IMPORTANT: The order of the above mixins matters.
// SparkInterface MUST come after LelantusInterface.
@ -587,6 +593,15 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
@override
Future<void> recover({required bool isRescan}) async {
// reset last checked values
await info.updateOtherData(
newEntries: {
WalletInfoKeys.firoSparkCacheSetTimestampCache: <String, int>{},
},
isar: mainDB.isar,
);
final start = DateTime.now();
final root = await getRootHDNode();
final List<Future<({int index, List<Address> addresses})>> receiveFutures =
@ -610,37 +625,36 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
await mainDB.deleteWalletBlockchainData(walletId);
}
// Parse otherDataJsonString to get the enableLelantusScanning value.
bool? enableLelantusScanning = false;
if (info.otherDataJsonString != null) {
final otherDataJson = json.decode(info.otherDataJsonString!);
enableLelantusScanning =
otherDataJson[WalletInfoKeys.enableLelantusScanning] as bool? ??
false;
}
// lelantus
int? latestSetId;
Future<Map<int, dynamic>>? setDataMapFuture;
Future<List<String>>? usedSerialNumbersFuture;
final List<Future<dynamic>> lelantusFutures = [];
final enableLelantusScanning =
info.otherData[WalletInfoKeys.enableLelantusScanning] as bool? ??
false;
if (enableLelantusScanning) {
latestSetId = await electrumXClient.getLelantusLatestCoinId();
setDataMapFuture = getSetDataMap(latestSetId);
usedSerialNumbersFuture = electrumXCachedClient.getUsedCoinSerials(
cryptoCurrency: info.coin,
lelantusFutures.add(
electrumXCachedClient.getUsedCoinSerials(
cryptoCurrency: info.coin,
),
);
lelantusFutures.add(getSetDataMap(latestSetId));
}
// spark
final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId();
final sparkAnonSetFuture = electrumXCachedClient.getSparkAnonymitySet(
groupId: latestSparkCoinId.toString(),
cryptoCurrency: info.coin,
useOnlyCacheIfNotEmpty: false,
);
final List<Future<void>> sparkAnonSetFutures = [];
for (int i = 1; i <= latestSparkCoinId; i++) {
sparkAnonSetFutures.add(
FiroCacheCoordinator.runFetchAndUpdateSparkAnonSetCacheForGroupId(
i,
electrumXClient,
),
);
}
final sparkUsedCoinTagsFuture =
electrumXCachedClient.getSparkUsedCoinsTags(
cryptoCurrency: info.coin,
FiroCacheCoordinator.runFetchAndUpdateSparkUsedCoinTags(
electrumXClient,
);
// receiving addresses
@ -749,14 +763,13 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
updateUTXOs(),
]);
List<Future<dynamic>> futures = [];
futures.add(sparkAnonSetFuture);
futures.add(sparkUsedCoinTagsFuture);
final List<Future<dynamic>> futures = [];
if (enableLelantusScanning) {
futures.add(usedSerialNumbersFuture!);
futures.add(setDataMapFuture!);
futures.add(lelantusFutures[0]);
futures.add(lelantusFutures[1]);
}
futures.add(sparkUsedCoinTagsFuture);
futures.addAll(sparkAnonSetFutures);
final futureResults = await Future.wait(futures);
@ -764,29 +777,22 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
Set<String>? usedSerialsSet;
Map<dynamic, dynamic>? setDataMap;
if (enableLelantusScanning) {
usedSerialsSet = (futureResults[2] as List<String>).toSet();
setDataMap = futureResults[3] as Map<dynamic, dynamic>;
usedSerialsSet = (futureResults[0] as List<String>).toSet();
setDataMap = futureResults[1] as Map<dynamic, dynamic>;
}
// spark
final sparkAnonymitySet = futureResults[0] as Map<String, dynamic>;
final sparkSpentCoinTags = futureResults[1] as Set<String>;
if (Util.isDesktop) {
List<Future<dynamic>> futures = [];
if (enableLelantusScanning) {
futures.add(recoverLelantusWallet(
latestSetId: latestSetId!,
usedSerialNumbers: usedSerialsSet!,
setDataMap: setDataMap!,
));
}
futures.add(recoverSparkWallet(
anonymitySet: sparkAnonymitySet,
spentCoinTags: sparkSpentCoinTags,
));
await Future.wait(futures);
await Future.wait([
if (enableLelantusScanning)
recoverLelantusWallet(
latestSetId: latestSetId!,
usedSerialNumbers: usedSerialsSet!,
setDataMap: setDataMap!,
),
recoverSparkWallet(
latestSparkCoinId: latestSparkCoinId,
),
]);
} else {
if (enableLelantusScanning) {
await recoverLelantusWallet(
@ -796,13 +802,17 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
);
}
await recoverSparkWallet(
anonymitySet: sparkAnonymitySet,
spentCoinTags: sparkSpentCoinTags,
latestSparkCoinId: latestSparkCoinId,
);
}
});
unawaited(refresh());
Logging.instance.log(
"Firo recover for "
"${info.name}: ${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from electrumx_mixin recover(): $e\n$s",

View file

@ -474,6 +474,7 @@ abstract class Wallet<T extends CryptoCurrency> {
if (refreshMutex.isLocked) {
return;
}
final start = DateTime.now();
try {
// this acquire should be almost instant due to above check.
@ -619,6 +620,12 @@ abstract class Wallet<T extends CryptoCurrency> {
);
} finally {
refreshMutex.release();
Logging.instance.log(
"Refresh for "
"${info.name}: ${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
}
}

View file

@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:isar/isar.dart';
import '../../../db/sqlite/firo_cache.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
@ -20,6 +21,7 @@ import '../../../utilities/logger.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';
import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
import 'electrumx_interface.dart';
@ -259,17 +261,39 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
final List<Map<String, dynamic>> setMaps = [];
final List<({int groupId, String blockHash})> idAndBlockHashes = [];
for (int i = 1; i <= currentId; i++) {
final set = await electrumXCachedClient.getSparkAnonymitySet(
groupId: i.toString(),
cryptoCurrency: info.coin,
useOnlyCacheIfNotEmpty: true,
final resultSet = await FiroCacheCoordinator.getSetCoinsForGroupId(i);
if (resultSet.isEmpty) {
continue;
}
final info = await FiroCacheCoordinator.getLatestSetInfoForGroupId(
i,
);
set["coinGroupID"] = i;
setMaps.add(set);
if (info == null) {
throw Exception("The `info` should never be null here");
}
final Map<String, dynamic> setData = {
"blockHash": info.blockHash,
"setHash": info.setHash,
"coinGroupID": i,
"coins": resultSet
.map(
(row) => [
row["serialized"] as String,
row["txHash"] as String,
row["context"] as String,
],
)
.toList(),
};
setData["coinGroupID"] = i;
setMaps.add(setData);
idAndBlockHashes.add(
(
groupId: i,
blockHash: set["blockHash"] as String,
blockHash: setData["blockHash"] as String,
),
);
}
@ -607,79 +631,42 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
}
Future<void> refreshSparkData() async {
final sparkAddresses = await mainDB.isar.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.typeEqualTo(AddressType.spark)
.findAll();
final Set<String> paths =
sparkAddresses.map((e) => e.derivationPath!.value).toSet();
try {
final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId();
final anonymitySetFuture = electrumXCachedClient.getSparkAnonymitySet(
groupId: latestSparkCoinId.toString(),
cryptoCurrency: info.coin,
useOnlyCacheIfNotEmpty: false,
);
final spentCoinTagsFuture = electrumXCachedClient.getSparkUsedCoinsTags(
cryptoCurrency: info.coin,
);
final futureResults = await Future.wait([
anonymitySetFuture,
spentCoinTagsFuture,
]);
final anonymitySet = futureResults[0] as Map<String, dynamic>;
final spentCoinTags = futureResults[1] as Set<String>;
final List<SparkCoin> myCoins = [];
if (anonymitySet["coins"] is List &&
(anonymitySet["coins"] as List).isNotEmpty) {
final root = await getRootHDNode();
final privateKeyHexSet = paths
.map(
(e) => root.derivePath(e).privateKey.data.toHex,
)
.toSet();
final identifiedCoins = await compute(
_identifyCoins,
(
anonymitySetCoins: anonymitySet["coins"] as List,
groupId: latestSparkCoinId,
spentCoinTags: spentCoinTags,
privateKeyHexSet: privateKeyHexSet,
walletId: walletId,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
),
);
myCoins.addAll(identifiedCoins);
}
// check current coins
final currentCoins = await mainDB.isar.sparkCoins
.where()
.walletIdEqualToAnyLTagHash(walletId)
.filter()
.isUsedEqualTo(false)
.findAll();
for (final coin in currentCoins) {
if (spentCoinTags.contains(coin.lTagHash)) {
myCoins.add(coin.copyWith(isUsed: true));
// start by checking if any previous sets are missing from db and add the
// missing groupIds to the list if sets to check and update
final latestGroupId = await electrumXClient.getSparkLatestCoinId();
final List<int> groupIds = [];
if (latestGroupId > 1) {
for (int id = 1; id < latestGroupId; id++) {
final setExists =
await FiroCacheCoordinator.checkSetInfoForGroupIdExists(
id,
);
if (!setExists) {
groupIds.add(id);
}
}
}
groupIds.add(latestGroupId);
// update wallet spark coins in isar
await _addOrUpdateSparkCoins(myCoins);
// start fetch and update process for each set groupId as required
final possibleFutures = groupIds.map(
(e) =>
FiroCacheCoordinator.runFetchAndUpdateSparkAnonSetCacheForGroupId(
e,
electrumXClient,
),
);
// wait for each fetch and update to complete
await Future.wait([
...possibleFutures,
FiroCacheCoordinator.runFetchAndUpdateSparkUsedCoinTags(
electrumXClient,
),
]);
await _checkAndUpdateCoins();
// refresh spark balance
await refreshSparkBalance();
} catch (e, s) {
@ -737,8 +724,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
/// Should only be called within the standard wallet [recover] function due to
/// mutex locking. Otherwise behaviour MAY be undefined.
Future<void> recoverSparkWallet({
required Map<dynamic, dynamic> anonymitySet,
required Set<String> spentCoinTags,
required int latestSparkCoinId,
}) async {
// generate spark addresses if non existing
if (await getCurrentReceivingSparkAddress() == null) {
@ -746,35 +732,8 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
await mainDB.putAddress(address);
}
final sparkAddresses = await mainDB.isar.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.typeEqualTo(AddressType.spark)
.findAll();
final Set<String> paths =
sparkAddresses.map((e) => e.derivationPath!.value).toSet();
try {
final root = await getRootHDNode();
final privateKeyHexSet =
paths.map((e) => root.derivePath(e).privateKey.data.toHex).toSet();
final myCoins = await compute(
_identifyCoins,
(
anonymitySetCoins: anonymitySet["coins"] as List,
groupId: anonymitySet["coinGroupID"] as int,
spentCoinTags: spentCoinTags,
privateKeyHexSet: privateKeyHexSet,
walletId: walletId,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
),
);
// update wallet spark coins in isar
await _addOrUpdateSparkCoins(myCoins);
await _checkAndUpdateCoins();
// refresh spark balance
await refreshSparkBalance();
@ -787,6 +746,115 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
}
}
Future<void> _checkAndUpdateCoins() async {
final sparkAddresses = await mainDB.isar.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.typeEqualTo(AddressType.spark)
.findAll();
final root = await getRootHDNode();
final Set<String> privateKeyHexSet = sparkAddresses
.map(
(e) => root.derivePath(e.derivationPath!.value).privateKey.data.toHex,
)
.toSet();
final Map<int, List<List<String>>> rawCoinsBySetId = {};
final groupIdTimestampUTCMap =
info.otherData[WalletInfoKeys.firoSparkCacheSetTimestampCache]
as Map? ??
{};
final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId();
for (int i = 1; i <= latestSparkCoinId; i++) {
final lastCheckedTimeStampUTC =
groupIdTimestampUTCMap[i.toString()] as int? ?? 0;
final info = await FiroCacheCoordinator.getLatestSetInfoForGroupId(
i,
);
final anonymitySetResult =
await FiroCacheCoordinator.getSetCoinsForGroupId(
i,
newerThanTimeStamp: lastCheckedTimeStampUTC,
);
final coinsRaw = anonymitySetResult
.map(
(row) => [
row["serialized"] as String,
row["txHash"] as String,
row["context"] as String,
],
)
.toList();
if (coinsRaw.isNotEmpty) {
rawCoinsBySetId[i] = coinsRaw;
}
groupIdTimestampUTCMap[i.toString()] = max(
lastCheckedTimeStampUTC,
info?.timestampUTC ?? lastCheckedTimeStampUTC,
);
}
await info.updateOtherData(
newEntries: {
WalletInfoKeys.firoSparkCacheSetTimestampCache: groupIdTimestampUTCMap,
},
isar: mainDB.isar,
);
final List<SparkCoin> newlyIdCoins = [];
for (final groupId in rawCoinsBySetId.keys) {
final myCoins = await compute(
_identifyCoins,
(
anonymitySetCoins: rawCoinsBySetId[groupId]!,
groupId: groupId,
privateKeyHexSet: privateKeyHexSet,
walletId: walletId,
isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test,
),
);
newlyIdCoins.addAll(myCoins);
}
await _checkAndMarkCoinsUsedInDB(coinsNotInDbYet: newlyIdCoins);
}
Future<void> _checkAndMarkCoinsUsedInDB({
List<SparkCoin> coinsNotInDbYet = const [],
}) async {
final List<SparkCoin> coins = await mainDB.isar.sparkCoins
.where()
.walletIdEqualToAnyLTagHash(walletId)
.filter()
.isUsedEqualTo(false)
.findAll();
final List<SparkCoin> coinsToWrite = [];
final spentCoinTags = await FiroCacheCoordinator.getUsedCoinTags(0);
for (final coin in coins) {
if (spentCoinTags.contains(coin.lTagHash)) {
coinsToWrite.add(coin.copyWith(isUsed: true));
}
}
for (final coin in coinsNotInDbYet) {
if (spentCoinTags.contains(coin.lTagHash)) {
coinsToWrite.add(coin.copyWith(isUsed: true));
} else {
coinsToWrite.add(coin);
}
}
// update wallet spark coins in isar
await _addOrUpdateSparkCoins(coinsToWrite);
}
// modelled on CSparkWallet::CreateSparkMintTransactions https://github.com/firoorg/firo/blob/39c41e5e7ec634ced3700fe3f4f5509dc2e480d0/src/spark/sparkwallet.cpp#L752
Future<List<TxData>> _createSparkMintTransactions({
required List<UTXO> availableUtxos,
@ -1630,12 +1698,6 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
);
}
String base64ToReverseHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.reversed
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
/// Top level function which should be called wrapped in [compute]
Future<
({
@ -1701,7 +1763,6 @@ Future<List<SparkCoin>> _identifyCoins(
({
List<dynamic> anonymitySetCoins,
int groupId,
Set<String> spentCoinTags,
Set<String> privateKeyHexSet,
String walletId,
bool isTestNet,
@ -1718,7 +1779,7 @@ Future<List<SparkCoin>> _identifyCoins(
}
final serializedCoinB64 = data[0];
final txHash = base64ToReverseHex(data[1]);
final txHash = data[1].toHexReversedFromBase64;
final contextB64 = data[2];
final coin = LibSpark.identifyAndRecoverCoin(
@ -1744,7 +1805,7 @@ Future<List<SparkCoin>> _identifyCoins(
SparkCoin(
walletId: args.walletId,
type: coinType,
isUsed: args.spentCoinTags.contains(coin.lTagHash!),
isUsed: false,
groupId: args.groupId,
nonce: coin.nonceHex?.toUint8ListFromHex,
address: coin.address!,

View file

@ -12,6 +12,7 @@
#include <flutter_libmonero/flutter_libmonero_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <stack_wallet_backup/stack_wallet_backup_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_size/window_size_plugin.h>
@ -35,6 +36,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) stack_wallet_backup_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "StackWalletBackupPlugin");
stack_wallet_backup_plugin_register_with_registrar(stack_wallet_backup_registrar);

View file

@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_libmonero
flutter_secure_storage_linux
isar_flutter_libs
sqlite3_flutter_libs
stack_wallet_backup
url_launcher_linux
window_size

View file

@ -17,6 +17,7 @@ import lelantus
import package_info_plus
import path_provider_foundation
import share_plus
import sqlite3_flutter_libs
import stack_wallet_backup
import url_launcher_macos
import wakelock_macos
@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
StackWalletBackupPlugin.register(with: registry.registrar(forPlugin: "StackWalletBackupPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))

View file

@ -34,6 +34,21 @@ PODS:
- ReachabilitySwift (5.0.0)
- share_plus (0.0.1):
- FlutterMacOS
- sqlite3 (3.46.0):
- sqlite3/common (= 3.46.0)
- sqlite3/common (3.46.0)
- sqlite3/fts5 (3.46.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.46.0):
- sqlite3/common
- sqlite3/rtree (3.46.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- FlutterMacOS
- sqlite3 (~> 3.46.0)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- stack_wallet_backup (0.0.1):
- FlutterMacOS
- tor_ffi_plugin (0.0.1)
@ -61,6 +76,7 @@ DEPENDENCIES:
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- stack_wallet_backup (from `Flutter/ephemeral/.symlinks/plugins/stack_wallet_backup/macos`)
- tor_ffi_plugin (from `Flutter/ephemeral/.symlinks/plugins/tor_ffi_plugin/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@ -70,6 +86,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- ReachabilitySwift
- sqlite3
EXTERNAL SOURCES:
coinlib_flutter:
@ -104,6 +121,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
stack_wallet_backup:
:path: Flutter/ephemeral/.symlinks/plugins/stack_wallet_backup/macos
tor_ffi_plugin:
@ -133,6 +152,8 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d
sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83
stack_wallet_backup: 6ebc60b1bdcf11cf1f1cbad9aa78332e1e15778c
tor_ffi_plugin: 2566c1ed174688cca560fa0c64b7a799c66f07cb
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95

View file

@ -689,8 +689,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "439727b278250c61a291f5335c298c0f2d952517"
resolved-ref: "439727b278250c61a291f5335c298c0f2d952517"
ref: "7a11d0cadf8c7a6a5d5144dab18cef9536aa5943"
resolved-ref: "7a11d0cadf8c7a6a5d5144dab18cef9536aa5943"
url: "https://github.com/cypherstack/flutter_libsparkmobile.git"
source: git
version: "0.0.1"
@ -1194,12 +1194,12 @@ packages:
source: hosted
version: "0.2.0"
monero:
dependency: transitive
dependency: "direct main"
description:
path: "."
ref: "6a17a405a1a260fa228b2f4fc94044088a4335ac"
resolved-ref: "6a17a405a1a260fa228b2f4fc94044088a4335ac"
url: "https://git.mrcyjanek.net/mrcyjanek/monero.dart"
url: "https://www.github.com/mrcyjanek/monero.dart"
source: git
version: "0.0.0"
mutex:
@ -1633,6 +1633,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sqlite3:
dependency: "direct main"
description:
name: sqlite3
sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
url: "https://pub.dev"
source: hosted
version: "2.4.3"
sqlite3_flutter_libs:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "1e62698dc1ab396152ccaf3b3990d826244e9f3c8c39b51805f209adcd6dbea3"
url: "https://pub.dev"
source: hosted
version: "0.5.22"
stack_trace:
dependency: transitive
description:

View file

@ -14,7 +14,7 @@ PLUGINS_DIR=../../crypto_plugins
(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh )
(cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh )
(cd "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/ && ./build_all.sh )
(cd "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/ && ./build_all.sh )
set_rust_to_1720
(cd "${PLUGINS_DIR}"/frostdart/scripts/android && ./build_all.sh )

View file

@ -14,11 +14,11 @@ mkdir -p build
PLUGINS_DIR=../../crypto_plugins
(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh ) &
(cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh ) &
(cd "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/ && ./build_all.sh ) &&
set_rust_to_1720 &&
(cd "${PLUGINS_DIR}"/frostdart/scripts/android && ./build_all.sh ) &
(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh )
(cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh )
(cd "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/ && ./build_all.sh )
set_rust_to_1720
(cd "${PLUGINS_DIR}"/frostdart/scripts/android && ./build_all.sh )
wait
echo "Done building"

View file

@ -44,24 +44,24 @@ const ({String light, String dark})? _appIconAsset = null;
final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Bitcoin(CryptoCurrencyNetwork.main),
BitcoinFrost(CryptoCurrencyNetwork.main),
Litecoin(CryptoCurrencyNetwork.main),
Monero(CryptoCurrencyNetwork.main),
Banano(CryptoCurrencyNetwork.main),
Bitcoincash(CryptoCurrencyNetwork.main),
BitcoinFrost(CryptoCurrencyNetwork.main),
Dogecoin(CryptoCurrencyNetwork.main),
Epiccash(CryptoCurrencyNetwork.main),
Ecash(CryptoCurrencyNetwork.main),
Epiccash(CryptoCurrencyNetwork.main),
Ethereum(CryptoCurrencyNetwork.main),
Firo(CryptoCurrencyNetwork.main),
Monero(CryptoCurrencyNetwork.main),
Litecoin(CryptoCurrencyNetwork.main),
Nano(CryptoCurrencyNetwork.main),
Namecoin(CryptoCurrencyNetwork.main),
Particl(CryptoCurrencyNetwork.main),
Peercoin(CryptoCurrencyNetwork.main),
Solana(CryptoCurrencyNetwork.main),
Stellar(CryptoCurrencyNetwork.main),
Tezos(CryptoCurrencyNetwork.main),
Wownero(CryptoCurrencyNetwork.main),
Namecoin(CryptoCurrencyNetwork.main),
Nano(CryptoCurrencyNetwork.main),
Banano(CryptoCurrencyNetwork.main),
Bitcoin(CryptoCurrencyNetwork.test),
BitcoinFrost(CryptoCurrencyNetwork.test),
Litecoin(CryptoCurrencyNetwork.test),

View file

@ -33,7 +33,7 @@ dependencies:
flutter_libsparkmobile:
git:
url: https://github.com/cypherstack/flutter_libsparkmobile.git
ref: 439727b278250c61a291f5335c298c0f2d952517
ref: 7a11d0cadf8c7a6a5d5144dab18cef9536aa5943
flutter_libmonero:
path: ./crypto_plugins/flutter_libmonero
@ -49,7 +49,7 @@ dependencies:
monero:
git:
url: https://git.mrcyjanek.net/mrcyjanek/monero.dart
url: https://www.github.com/mrcyjanek/monero.dart
ref: 6a17a405a1a260fa228b2f4fc94044088a4335ac
flutter_libepiccash:
@ -183,6 +183,8 @@ dependencies:
ref: a83e375678eb22fe544dc125d29bbec0fb833882
path: packages/solana
calendar_date_picker2: ^1.0.2
sqlite3: ^2.4.3
sqlite3_flutter_libs: ^0.5.22
dev_dependencies:
flutter_test:
@ -209,6 +211,11 @@ flutter_native_splash:
dependency_overrides:
monero:
git:
url: https://www.github.com/mrcyjanek/monero.dart
ref: 6a17a405a1a260fa228b2f4fc94044088a4335ac
bip47:
git:
url: https://github.com/cypherstack/bip47.git

View file

@ -14,11 +14,11 @@ rustup target add x86_64-apple-ios
rustup target add aarch64-apple-ios
rustup target add x86_64-apple-ios
(cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) &&
set_rust_to_1720 &&
(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh )
set_rust_to_1720
(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh )
wait
echo "Done building"

View file

@ -16,11 +16,11 @@ rustup target add x86_64-apple-ios
rustup target add aarch64-apple-ios
rustup target add x86_64-apple-ios
(cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) &&
set_rust_to_1720 &&
(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh )
set_rust_to_1720
(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh )
wait
echo "Done building"

View file

@ -14,11 +14,11 @@ set_rust_to_1671
# flutter-elinux build linux --dart-define="IS_ARM=true"
mkdir -p build
./build_secure_storage_deps.sh &
(cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh )
(cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_all.sh )
set_rust_to_1720
(cd ../../crypto_plugins/frostdart/scripts/linux && ./build_all.sh ) &
(cd ../../crypto_plugins/frostdart/scripts/linux && ./build_all.sh )
./build_secp256k1.sh

View file

@ -8,7 +8,7 @@ set_rust_to_1671
(cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh )
set_rust_to_1720
(cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh )

View file

@ -8,11 +8,11 @@ set -x -e
source ../rust_version.sh
set_rust_to_1671
(cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) &&
set_rust_to_1720 &&
(cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh )
set_rust_to_1720
(cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh )
wait
echo "Done building"

View file

@ -9,11 +9,11 @@ source ../rust_version.sh
set_rust_to_1671
mkdir -p build
(cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh )
(cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh )
(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh)
set_rust_to_1720
(cd ../../crypto_plugins/frostdart/scripts/windows && ./build_all.sh ) &
(cd ../../crypto_plugins/frostdart/scripts/windows && ./build_all.sh )
./build_secp256k1_wsl.sh

View file

@ -428,21 +428,21 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient {
_i7.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i7.Future<Map<String, dynamic>>);
@override
_i7.Future<Set<String>> getSparkUsedCoinsTags({
_i7.Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int? startNumber,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
#getSparkUnhashedUsedCoinsTags,
[],
{
#requestID: requestID,
#startNumber: startNumber,
},
),
returnValue: _i7.Future<Set<String>>.value(<String>{}),
) as _i7.Future<Set<String>>);
returnValue: _i7.Future<List<String>>.value(<String>[]),
) as _i7.Future<List<String>>);
@override
_i7.Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,

View file

@ -78,27 +78,6 @@ class MockCachedElectrumXClient extends _i1.Mock
_i4.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i4.Future<Map<String, dynamic>>);
@override
_i4.Future<Map<String, dynamic>> getSparkAnonymitySet({
required String? groupId,
String? blockhash = r'',
required _i5.CryptoCurrency? cryptoCurrency,
required bool? useOnlyCacheIfNotEmpty,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkAnonymitySet,
[],
{
#groupId: groupId,
#blockhash: blockhash,
#cryptoCurrency: cryptoCurrency,
#useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty,
},
),
returnValue:
_i4.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i4.Future<Map<String, dynamic>>);
@override
String base64ToHex(String? source) => (super.noSuchMethod(
Invocation.method(
#base64ToHex,
@ -162,17 +141,6 @@ class MockCachedElectrumXClient extends _i1.Mock
returnValue: _i4.Future<List<String>>.value(<String>[]),
) as _i4.Future<List<String>>);
@override
_i4.Future<Set<String>> getSparkUsedCoinsTags(
{required _i5.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
[],
{#cryptoCurrency: cryptoCurrency},
),
returnValue: _i4.Future<Set<String>>.value(<String>{}),
) as _i4.Future<Set<String>>);
@override
_i4.Future<void> clearSharedTransactionCache(
{required _i5.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(

View file

@ -425,21 +425,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags({
_i6.Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int? startNumber,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
#getSparkUnhashedUsedCoinsTags,
[],
{
#requestID: requestID,
#startNumber: startNumber,
},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
@ -559,27 +559,6 @@ class MockCachedElectrumXClient extends _i1.Mock
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Map<String, dynamic>> getSparkAnonymitySet({
required String? groupId,
String? blockhash = r'',
required _i2.CryptoCurrency? cryptoCurrency,
required bool? useOnlyCacheIfNotEmpty,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkAnonymitySet,
[],
{
#groupId: groupId,
#blockhash: blockhash,
#cryptoCurrency: cryptoCurrency,
#useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty,
},
),
returnValue:
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
String base64ToHex(String? source) => (super.noSuchMethod(
Invocation.method(
#base64ToHex,
@ -643,17 +622,6 @@ class MockCachedElectrumXClient extends _i1.Mock
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
[],
{#cryptoCurrency: cryptoCurrency},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
@override
_i6.Future<void> clearSharedTransactionCache(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(

View file

@ -425,21 +425,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags({
_i6.Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int? startNumber,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
#getSparkUnhashedUsedCoinsTags,
[],
{
#requestID: requestID,
#startNumber: startNumber,
},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
@ -559,27 +559,6 @@ class MockCachedElectrumXClient extends _i1.Mock
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Map<String, dynamic>> getSparkAnonymitySet({
required String? groupId,
String? blockhash = r'',
required _i2.CryptoCurrency? cryptoCurrency,
required bool? useOnlyCacheIfNotEmpty,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkAnonymitySet,
[],
{
#groupId: groupId,
#blockhash: blockhash,
#cryptoCurrency: cryptoCurrency,
#useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty,
},
),
returnValue:
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
String base64ToHex(String? source) => (super.noSuchMethod(
Invocation.method(
#base64ToHex,
@ -643,17 +622,6 @@ class MockCachedElectrumXClient extends _i1.Mock
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
[],
{#cryptoCurrency: cryptoCurrency},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
@override
_i6.Future<void> clearSharedTransactionCache(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(

View file

@ -425,21 +425,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags({
_i6.Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int? startNumber,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
#getSparkUnhashedUsedCoinsTags,
[],
{
#requestID: requestID,
#startNumber: startNumber,
},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
@ -559,27 +559,6 @@ class MockCachedElectrumXClient extends _i1.Mock
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Map<String, dynamic>> getSparkAnonymitySet({
required String? groupId,
String? blockhash = r'',
required _i2.CryptoCurrency? cryptoCurrency,
required bool? useOnlyCacheIfNotEmpty,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkAnonymitySet,
[],
{
#groupId: groupId,
#blockhash: blockhash,
#cryptoCurrency: cryptoCurrency,
#useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty,
},
),
returnValue:
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
String base64ToHex(String? source) => (super.noSuchMethod(
Invocation.method(
#base64ToHex,
@ -643,17 +622,6 @@ class MockCachedElectrumXClient extends _i1.Mock
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
[],
{#cryptoCurrency: cryptoCurrency},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
@override
_i6.Future<void> clearSharedTransactionCache(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(

View file

@ -425,21 +425,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags({
_i6.Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int? startNumber,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
#getSparkUnhashedUsedCoinsTags,
[],
{
#requestID: requestID,
#startNumber: startNumber,
},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
@ -559,27 +559,6 @@ class MockCachedElectrumXClient extends _i1.Mock
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Map<String, dynamic>> getSparkAnonymitySet({
required String? groupId,
String? blockhash = r'',
required _i2.CryptoCurrency? cryptoCurrency,
required bool? useOnlyCacheIfNotEmpty,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkAnonymitySet,
[],
{
#groupId: groupId,
#blockhash: blockhash,
#cryptoCurrency: cryptoCurrency,
#useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty,
},
),
returnValue:
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
String base64ToHex(String? source) => (super.noSuchMethod(
Invocation.method(
#base64ToHex,
@ -643,17 +622,6 @@ class MockCachedElectrumXClient extends _i1.Mock
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
[],
{#cryptoCurrency: cryptoCurrency},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
@override
_i6.Future<void> clearSharedTransactionCache(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(

View file

@ -425,21 +425,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags({
_i6.Future<List<String>> getSparkUnhashedUsedCoinsTags({
String? requestID,
required int? startNumber,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
#getSparkUnhashedUsedCoinsTags,
[],
{
#requestID: requestID,
#startNumber: startNumber,
},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
@ -559,27 +559,6 @@ class MockCachedElectrumXClient extends _i1.Mock
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
_i6.Future<Map<String, dynamic>> getSparkAnonymitySet({
required String? groupId,
String? blockhash = r'',
required _i2.CryptoCurrency? cryptoCurrency,
required bool? useOnlyCacheIfNotEmpty,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkAnonymitySet,
[],
{
#groupId: groupId,
#blockhash: blockhash,
#cryptoCurrency: cryptoCurrency,
#useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty,
},
),
returnValue:
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i6.Future<Map<String, dynamic>>);
@override
String base64ToHex(String? source) => (super.noSuchMethod(
Invocation.method(
#base64ToHex,
@ -643,17 +622,6 @@ class MockCachedElectrumXClient extends _i1.Mock
returnValue: _i6.Future<List<String>>.value(<String>[]),
) as _i6.Future<List<String>>);
@override
_i6.Future<Set<String>> getSparkUsedCoinsTags(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(
Invocation.method(
#getSparkUsedCoinsTags,
[],
{#cryptoCurrency: cryptoCurrency},
),
returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>);
@override
_i6.Future<void> clearSharedTransactionCache(
{required _i2.CryptoCurrency? cryptoCurrency}) =>
(super.noSuchMethod(

View file

@ -0,0 +1,33 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
void main() {
test("Empty list", () {
final List<int> list = [];
expect(
list.chunked(chunkSize: 3).isEmpty,
true,
);
});
test("No remainder", () {
final List<int> list = [0, 1, 2, 3, 4, 5, 6, 7, 8];
final chunked = list.chunked(chunkSize: 3);
expect(chunked.length == 3, true);
expect(
chunked.map((e) => e.length == 3).reduce((v, e) => v && e),
true,
);
});
test("Some remainder", () {
final List<int> list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
final chunked = list.chunked(chunkSize: 3);
expect(chunked.length == 4, true);
expect(chunked.last.length == 1, true);
expect(
chunked.map((e) => e.length == 3).reduce((v, e) => v && e),
false,
);
});
}

View file

@ -13,6 +13,7 @@
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <stack_wallet_backup/stack_wallet_backup_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_size/window_size_plugin.h>
@ -32,6 +33,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
StackWalletBackupPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("StackWalletBackupPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
isar_flutter_libs
permission_handler_windows
share_plus
sqlite3_flutter_libs
stack_wallet_backup
url_launcher_windows
window_size