mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 20:09:23 +00:00
207 lines
5.3 KiB
Dart
207 lines
5.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:cs_monero/cs_monero.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:mutex/mutex.dart';
|
|
|
|
import '../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
|
|
|
enum ChurnStatus {
|
|
waiting,
|
|
running,
|
|
failed,
|
|
success;
|
|
}
|
|
|
|
class ChurningService extends ChangeNotifier {
|
|
// stack only uses account 0 at this point in time
|
|
static const kAccount = 0;
|
|
|
|
ChurningService({required this.wallet});
|
|
|
|
final LibMoneroWallet wallet;
|
|
Wallet get csWallet => wallet.libMoneroWallet!;
|
|
|
|
int rounds = 1; // default
|
|
bool ignoreErrors = false; // default
|
|
|
|
bool _running = false;
|
|
|
|
ChurnStatus waitingForUnlockedBalance = ChurnStatus.waiting;
|
|
ChurnStatus makingChurnTransaction = ChurnStatus.waiting;
|
|
ChurnStatus completedStatus = ChurnStatus.waiting;
|
|
int roundsCompleted = 0;
|
|
bool done = false;
|
|
Object? lastSeenError;
|
|
|
|
bool _canChurn() {
|
|
if (csWallet.getUnlockedBalance(accountIndex: kAccount) > BigInt.zero) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
String? confirmsInfo;
|
|
Future<void> _updateConfirmsInfo() async {
|
|
final currentHeight = wallet.currentKnownChainHeight;
|
|
if (currentHeight < 1) {
|
|
return;
|
|
}
|
|
|
|
final outputs = await csWallet.getOutputs(refresh: true);
|
|
final required = wallet.cryptoCurrency.minConfirms;
|
|
|
|
int lowestNumberOfConfirms = required;
|
|
|
|
for (final output in outputs.where((e) => !e.isFrozen && !e.spent)) {
|
|
final confirms = currentHeight - output.height;
|
|
|
|
lowestNumberOfConfirms = min(lowestNumberOfConfirms, confirms);
|
|
}
|
|
|
|
final bool shouldNotify;
|
|
if (lowestNumberOfConfirms == required) {
|
|
shouldNotify = confirmsInfo != null;
|
|
confirmsInfo = null;
|
|
} else {
|
|
final prev = confirmsInfo;
|
|
confirmsInfo = "($lowestNumberOfConfirms/$required)";
|
|
shouldNotify = confirmsInfo != prev;
|
|
}
|
|
|
|
if (_running && _timerRunning && shouldNotify) {
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Timer? _confirmsTimer;
|
|
bool _timerRunning = false;
|
|
void _stopConfirmsTimer() {
|
|
_timerRunning = false;
|
|
_confirmsTimer?.cancel();
|
|
confirmsInfo = null;
|
|
_confirmsTimer = null;
|
|
}
|
|
|
|
void _startConfirmsTimer() {
|
|
_confirmsTimer?.cancel();
|
|
_confirmsTimer = Timer.periodic(
|
|
const Duration(seconds: 5),
|
|
(_) => _updateConfirmsInfo(),
|
|
);
|
|
}
|
|
|
|
final _pause = Mutex();
|
|
bool get isPaused => _pause.isLocked;
|
|
void unpause() {
|
|
if (_pause.isLocked) _pause.release();
|
|
}
|
|
|
|
Future<void> churn() async {
|
|
if (rounds < 0 || _running) {
|
|
// TODO: error?
|
|
return;
|
|
}
|
|
|
|
_running = true;
|
|
waitingForUnlockedBalance = ChurnStatus.running;
|
|
makingChurnTransaction = ChurnStatus.waiting;
|
|
completedStatus = ChurnStatus.waiting;
|
|
roundsCompleted = 0;
|
|
done = false;
|
|
lastSeenError = null;
|
|
notifyListeners();
|
|
|
|
final roundsToDo = rounds;
|
|
final continuous = rounds == 0;
|
|
|
|
bool complete() => !continuous && roundsCompleted >= roundsToDo;
|
|
|
|
while (!complete() && _running) {
|
|
if (_canChurn()) {
|
|
waitingForUnlockedBalance = ChurnStatus.success;
|
|
makingChurnTransaction = ChurnStatus.running;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_stopConfirmsTimer();
|
|
Logging.log?.i("Doing churn #${roundsCompleted + 1}");
|
|
await _churnTxSimple();
|
|
waitingForUnlockedBalance = ChurnStatus.success;
|
|
makingChurnTransaction = ChurnStatus.success;
|
|
roundsCompleted++;
|
|
notifyListeners();
|
|
} catch (e, s) {
|
|
Logging.log?.e(
|
|
"Churning round #${roundsCompleted + 1} failed",
|
|
error: e,
|
|
stackTrace: s,
|
|
);
|
|
lastSeenError = e;
|
|
makingChurnTransaction = ChurnStatus.failed;
|
|
notifyListeners();
|
|
if (!ignoreErrors) {
|
|
await _pause.acquire();
|
|
await _pause.protect(() async {});
|
|
|
|
if (!_running) {
|
|
completedStatus = ChurnStatus.failed;
|
|
// exit if stop option chosen on error
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Logging.log?.i("Can't churn yet, waiting...");
|
|
}
|
|
|
|
if (!complete() && _running) {
|
|
_startConfirmsTimer();
|
|
waitingForUnlockedBalance = ChurnStatus.running;
|
|
makingChurnTransaction = ChurnStatus.waiting;
|
|
completedStatus = ChurnStatus.waiting;
|
|
notifyListeners();
|
|
// sleep
|
|
await Future<void>.delayed(const Duration(seconds: 30));
|
|
}
|
|
}
|
|
|
|
waitingForUnlockedBalance = ChurnStatus.success;
|
|
makingChurnTransaction = ChurnStatus.success;
|
|
completedStatus = ChurnStatus.success;
|
|
done = true;
|
|
_running = false;
|
|
notifyListeners();
|
|
Logging.log?.i("Churning complete");
|
|
}
|
|
|
|
void stopChurning() {
|
|
done = true;
|
|
_running = false;
|
|
notifyListeners();
|
|
unpause();
|
|
}
|
|
|
|
Future<void> _churnTxSimple({
|
|
final TransactionPriority priority = TransactionPriority.normal,
|
|
}) async {
|
|
final address = csWallet.getAddress(
|
|
accountIndex: kAccount,
|
|
addressIndex: 0,
|
|
);
|
|
|
|
final pending = await csWallet.createTx(
|
|
output: Recipient(
|
|
address: address.value,
|
|
amount: BigInt.zero, // Doesn't matter if `sweep` is true
|
|
),
|
|
priority: priority,
|
|
accountIndex: kAccount,
|
|
sweep: true,
|
|
);
|
|
|
|
await csWallet.commitTx(pending);
|
|
}
|
|
}
|