2022-08-26 08:11:35 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
2023-01-27 18:49:25 +00:00
|
|
|
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
import 'package:stackwallet/hive/db.dart';
|
2022-10-03 00:54:35 +00:00
|
|
|
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
import 'package:stackwallet/models/notification_model.dart';
|
2022-10-03 00:54:35 +00:00
|
|
|
import 'package:stackwallet/services/exchange/exchange_response.dart';
|
2022-08-26 08:11:35 +00:00
|
|
|
import 'package:stackwallet/services/node_service.dart';
|
|
|
|
import 'package:stackwallet/services/notifications_api.dart';
|
|
|
|
import 'package:stackwallet/services/trade_service.dart';
|
|
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|
|
|
import 'package:stackwallet/utilities/logger.dart';
|
|
|
|
import 'package:stackwallet/utilities/prefs.dart';
|
|
|
|
|
2023-02-05 20:32:39 +00:00
|
|
|
import 'exchange/exchange.dart';
|
|
|
|
|
2022-08-26 08:11:35 +00:00
|
|
|
class NotificationsService extends ChangeNotifier {
|
|
|
|
late NodeService nodeService;
|
|
|
|
late TradesService tradesService;
|
|
|
|
late Prefs prefs;
|
|
|
|
|
|
|
|
NotificationsService._();
|
|
|
|
static final NotificationsService _instance = NotificationsService._();
|
|
|
|
static NotificationsService get instance => _instance;
|
|
|
|
|
|
|
|
Future<void> init({
|
|
|
|
required NodeService nodeService,
|
|
|
|
required TradesService tradesService,
|
|
|
|
required Prefs prefs,
|
|
|
|
}) async {
|
|
|
|
this.nodeService = nodeService;
|
|
|
|
this.tradesService = tradesService;
|
|
|
|
this.prefs = prefs;
|
|
|
|
}
|
|
|
|
|
|
|
|
// watched transactions
|
|
|
|
List<NotificationModel> get _watchedTransactionNotifications {
|
|
|
|
return DB.instance
|
|
|
|
.values<NotificationModel>(boxName: DB.boxNameWatchedTransactions);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _addWatchedTxNotification(NotificationModel notification) async {
|
|
|
|
await DB.instance.put<NotificationModel>(
|
|
|
|
boxName: DB.boxNameWatchedTransactions,
|
|
|
|
key: notification.id,
|
|
|
|
value: notification);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _deleteWatchedTxNotification(
|
|
|
|
NotificationModel notification) async {
|
|
|
|
await DB.instance.delete<NotificationModel>(
|
|
|
|
boxName: DB.boxNameWatchedTransactions, key: notification.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// watched trades
|
|
|
|
List<NotificationModel> get _watchedChangeNowTradeNotifications {
|
|
|
|
return DB.instance
|
|
|
|
.values<NotificationModel>(boxName: DB.boxNameWatchedTrades);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _addWatchedTradeNotification(
|
|
|
|
NotificationModel notification) async {
|
|
|
|
await DB.instance.put<NotificationModel>(
|
|
|
|
boxName: DB.boxNameWatchedTrades,
|
|
|
|
key: notification.id,
|
|
|
|
value: notification);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _deleteWatchedTradeNotification(
|
|
|
|
NotificationModel notification) async {
|
|
|
|
await DB.instance.delete<NotificationModel>(
|
|
|
|
boxName: DB.boxNameWatchedTrades, key: notification.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
static Timer? _timer;
|
|
|
|
|
|
|
|
// todo: change this number?
|
|
|
|
static Duration notificationRefreshInterval = const Duration(seconds: 60);
|
|
|
|
|
|
|
|
void startCheckingWatchedNotifications() {
|
|
|
|
stopCheckingWatchedTransactions();
|
|
|
|
|
|
|
|
_timer = Timer.periodic(notificationRefreshInterval, (_) {
|
|
|
|
Logging.instance
|
|
|
|
.log("Periodic notifications update check", level: LogLevel.Info);
|
2022-10-17 23:20:08 +00:00
|
|
|
if (prefs.externalCalls) {
|
|
|
|
_checkTrades();
|
|
|
|
}
|
2022-08-26 08:11:35 +00:00
|
|
|
_checkTransactions();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void stopCheckingWatchedTransactions() {
|
|
|
|
_timer?.cancel();
|
|
|
|
_timer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
stopCheckingWatchedTransactions();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _checkTransactions() async {
|
|
|
|
for (final notification in _watchedTransactionNotifications) {
|
2022-09-08 12:45:38 +00:00
|
|
|
try {
|
|
|
|
final Coin coin = coinFromPrettyName(notification.coinName);
|
|
|
|
final txid = notification.txid!;
|
|
|
|
|
|
|
|
final node = nodeService.getPrimaryNodeFor(coin: coin);
|
|
|
|
if (node != null) {
|
|
|
|
if (coin.isElectrumXCoin) {
|
|
|
|
final eNode = ElectrumXNode(
|
|
|
|
address: node.host,
|
|
|
|
port: node.port,
|
|
|
|
name: node.name,
|
|
|
|
id: node.id,
|
|
|
|
useSSL: node.useSSL,
|
2022-08-26 08:11:35 +00:00
|
|
|
);
|
2022-09-08 12:45:38 +00:00
|
|
|
final failovers = nodeService
|
|
|
|
.failoverNodesFor(coin: coin)
|
|
|
|
.map((e) => ElectrumXNode(
|
|
|
|
address: e.host,
|
|
|
|
port: e.port,
|
|
|
|
name: e.name,
|
|
|
|
id: e.id,
|
|
|
|
useSSL: e.useSSL,
|
|
|
|
))
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
final client = ElectrumX.from(
|
|
|
|
node: eNode,
|
|
|
|
failovers: failovers,
|
|
|
|
prefs: prefs,
|
|
|
|
);
|
|
|
|
final tx = await client.getTransaction(txHash: txid);
|
|
|
|
|
|
|
|
int confirmations = tx["confirmations"] as int? ?? 0;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2022-09-08 12:45:38 +00:00
|
|
|
bool shouldWatchForUpdates = true;
|
|
|
|
// check if the number of confirmations is greater than the number
|
|
|
|
// required by the wallet to count the tx as confirmed and update the
|
|
|
|
// flag on whether this notification should still be monitored
|
|
|
|
if (confirmations >= coin.requiredConfirmations) {
|
|
|
|
shouldWatchForUpdates = false;
|
|
|
|
confirmations = coin.requiredConfirmations;
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
|
2022-09-08 12:45:38 +00:00
|
|
|
// grab confirms string to compare
|
|
|
|
final String newConfirms =
|
|
|
|
"($confirmations/${coin.requiredConfirmations})";
|
|
|
|
final String oldConfirms = notification.title
|
|
|
|
.substring(notification.title.lastIndexOf("("));
|
|
|
|
|
|
|
|
// only update if they don't match
|
|
|
|
if (oldConfirms != newConfirms) {
|
|
|
|
final String newTitle =
|
|
|
|
notification.title.replaceFirst(oldConfirms, newConfirms);
|
|
|
|
|
|
|
|
final updatedNotification = notification.copyWith(
|
|
|
|
title: newTitle,
|
|
|
|
shouldWatchForUpdates: shouldWatchForUpdates,
|
|
|
|
);
|
|
|
|
|
|
|
|
// remove from watch list if shouldWatchForUpdates was changed
|
|
|
|
if (!shouldWatchForUpdates) {
|
|
|
|
await _deleteWatchedTxNotification(notification);
|
|
|
|
}
|
|
|
|
|
|
|
|
// replaces the current notification with the updated one
|
2023-01-27 18:49:25 +00:00
|
|
|
await add(updatedNotification, true);
|
2022-09-08 12:45:38 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// TODO: check non electrumx coins
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-27 18:49:25 +00:00
|
|
|
} on NoSuchTransactionException catch (e, s) {
|
|
|
|
await _deleteWatchedTxNotification(notification);
|
2022-09-08 12:45:38 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("$e $s", level: LogLevel.Error);
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _checkTrades() async {
|
|
|
|
for (final notification in _watchedChangeNowTradeNotifications) {
|
|
|
|
final id = notification.changeNowId!;
|
|
|
|
|
2022-10-03 00:54:35 +00:00
|
|
|
final trades =
|
|
|
|
tradesService.trades.where((element) => element.tradeId == id);
|
2022-08-26 08:11:35 +00:00
|
|
|
|
2022-10-03 00:54:35 +00:00
|
|
|
if (trades.isEmpty) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final oldTrade = trades.first;
|
|
|
|
late final ExchangeResponse<Trade> response;
|
2023-02-05 20:32:39 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
final exchange = Exchange.fromName(oldTrade.exchangeName);
|
|
|
|
response = await exchange.updateTrade(oldTrade);
|
|
|
|
} catch (_) {
|
|
|
|
return;
|
2022-10-03 00:54:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (response.value == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final trade = response.value!;
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
// only update if status has changed
|
2022-10-03 00:54:35 +00:00
|
|
|
if (trade.status != notification.title) {
|
2022-08-26 08:11:35 +00:00
|
|
|
bool shouldWatchForUpdates = true;
|
|
|
|
// TODO: make sure we set shouldWatchForUpdates to correct value here
|
2022-10-03 00:54:35 +00:00
|
|
|
switch (trade.status) {
|
|
|
|
case "Refunded":
|
|
|
|
case "refunded":
|
|
|
|
case "Failed":
|
|
|
|
case "failed":
|
|
|
|
case "closed":
|
|
|
|
case "expired":
|
|
|
|
case "Finished":
|
|
|
|
case "finished":
|
2023-03-07 20:13:12 +00:00
|
|
|
case "Completed":
|
|
|
|
case "completed":
|
|
|
|
case "Not found":
|
2022-08-26 08:11:35 +00:00
|
|
|
shouldWatchForUpdates = false;
|
|
|
|
break;
|
2022-10-03 00:54:35 +00:00
|
|
|
default:
|
|
|
|
shouldWatchForUpdates = true;
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
final updatedNotification = notification.copyWith(
|
2022-10-03 00:54:35 +00:00
|
|
|
title: trade.status,
|
2022-08-26 08:11:35 +00:00
|
|
|
shouldWatchForUpdates: shouldWatchForUpdates,
|
|
|
|
);
|
|
|
|
|
|
|
|
// remove from watch list if shouldWatchForUpdates was changed
|
|
|
|
if (!shouldWatchForUpdates) {
|
|
|
|
await _deleteWatchedTradeNotification(notification);
|
|
|
|
}
|
|
|
|
|
|
|
|
// replaces the current notification with the updated one
|
2022-10-03 00:54:35 +00:00
|
|
|
unawaited(add(updatedNotification, true));
|
2022-08-26 08:11:35 +00:00
|
|
|
|
|
|
|
// update the trade in db
|
2022-10-03 00:54:35 +00:00
|
|
|
// over write trade stored in db with updated version
|
|
|
|
await tradesService.edit(trade: trade, shouldNotifyListeners: true);
|
2022-08-26 08:11:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool get hasUnreadNotifications {
|
|
|
|
// final count = (_unreadCountBox.get("count") ?? 0) > 0;
|
|
|
|
// debugPrint("NOTIF_COUNT: ${_unreadCountBox.get("count")}");
|
|
|
|
return DB.instance
|
|
|
|
.values<NotificationModel>(boxName: DB.boxNameNotifications)
|
|
|
|
.where((element) => element.read == false)
|
|
|
|
.isNotEmpty;
|
|
|
|
// return count;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool hasUnreadNotificationsFor(String walletId) {
|
|
|
|
return DB.instance
|
|
|
|
.values<NotificationModel>(boxName: DB.boxNameNotifications)
|
|
|
|
.where(
|
|
|
|
(element) => element.read == false && element.walletId == walletId)
|
|
|
|
.isNotEmpty;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<NotificationModel> get notifications {
|
|
|
|
final list = DB.instance
|
|
|
|
.values<NotificationModel>(boxName: DB.boxNameNotifications)
|
|
|
|
.toList(growable: false)
|
|
|
|
.reversed
|
|
|
|
.toList(growable: false);
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> add(
|
|
|
|
NotificationModel notification,
|
|
|
|
bool shouldNotifyListeners,
|
|
|
|
) async {
|
|
|
|
await DB.instance.put<NotificationModel>(
|
|
|
|
boxName: DB.boxNameNotifications,
|
|
|
|
key: notification.id,
|
|
|
|
value: notification,
|
|
|
|
);
|
|
|
|
if (notification.shouldWatchForUpdates) {
|
|
|
|
if (notification.txid != null) {
|
|
|
|
_addWatchedTxNotification(notification);
|
|
|
|
}
|
|
|
|
if (notification.changeNowId != null) {
|
|
|
|
_addWatchedTradeNotification(notification);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (shouldNotifyListeners) {
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> delete(
|
|
|
|
NotificationModel notification,
|
|
|
|
bool shouldNotifyListeners,
|
|
|
|
) async {
|
|
|
|
await DB.instance.delete<NotificationModel>(
|
|
|
|
boxName: DB.boxNameNotifications, key: notification.id);
|
|
|
|
|
|
|
|
await _deleteWatchedTradeNotification(notification);
|
|
|
|
await _deleteWatchedTxNotification(notification);
|
|
|
|
|
|
|
|
if (shouldNotifyListeners) {
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markAsRead(int id, bool shouldNotifyListeners) async {
|
|
|
|
final model = DB.instance
|
|
|
|
.get<NotificationModel>(boxName: DB.boxNameNotifications, key: id)!;
|
|
|
|
await DB.instance.put<NotificationModel>(
|
|
|
|
boxName: DB.boxNameNotifications,
|
|
|
|
key: model.id,
|
|
|
|
value: model.copyWith(read: true),
|
|
|
|
);
|
|
|
|
NotificationApi.clearNotification(id);
|
|
|
|
if (shouldNotifyListeners) {
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|