What changed, and why it matters
This commit is a routine code cleanup. It removes several unused or dead utility files, widgets, and tests, and moves one small status icon definition into the screen that uses it. There is no change to how the app handles money, keys, network connections, or user data, and nothing in the commit suggests a security fix.
No security action needed. Treat as normal maintenance; verify that the deleted code is truly unused by downstream consumers before release.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff deletes 666 lines across 11 files, mostly removing unused Dart helpers and UI widgets (bip39 derivation, blockchain height lookup, transaction notification logic, wallet path helper, fiat/Monero amount widgets, loading button, status icon, and their tests). The only functional edit is in wallet_home.dart, where the StatusIconStatus enum is relocated from the deleted status_icon.dart into the screen file, and the corresponding import is removed. No cryptographic, networking, storage, or authentication behavior is modified.
Changed components
lib/screens/wallet_home.dartlib/util/bip39.dartlib/util/height.dartlib/util/tx_notifications.dartlib/util/wallet.dartlib/widgets/fiat_amount.dartlib/widgets/loading_button.dartlib/widgets/monero_amount.dartlib/widgets/status_icon.darttest/tx_notifications_test.dartInspect captured patch +3 / −666
diff --git a/lib/screens/terms_of_service.dart b/lib/screens/terms_of_service.dart
index 5af6cd7..4cb6808 100644
--- a/lib/screens/terms_of_service.dart
+++ b/lib/screens/terms_of_service.dart
@@ -78,14 +78,7 @@ class TermsOfService extends StatelessWidget {
Container(
width: double.infinity,
- // padding: EdgeInsets.symmetric(vertical: 12),
margin: EdgeInsets.only(bottom: 10.0),
- decoration: BoxDecoration(
- // color: Theme.of(
- // context,
- // ).colorScheme.surfaceVariant.withOpacity(0.08),
- borderRadius: BorderRadius.circular(6.0),
- ),
child: Text(
'THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.',
style: monospaceStyle,
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index bac77c2..9fa81d0 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -10,7 +10,6 @@ import 'package:skylight_wallet/models/app_wallet.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
-import 'package:skylight_wallet/widgets/status_icon.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
import 'package:skylight_wallet/widgets/wallet_navigation_bar.dart';
import 'package:wallet_domain/wallet_domain.dart' show TxDetails;
@@ -32,6 +31,9 @@ TextStyle get _balanceSubStyle =>
/// Monero is decimal-12; cap the displayed coin amount for legibility.
String _amountText(double amount) => amount.toStringAsFixed(5);
+/// The resolved LWS connection state, driving the disconnected indicator.
+enum StatusIconStatus { loading, complete, fail }
+
class WalletHomeScreen extends StatefulWidget {
const WalletHomeScreen({super.key});
diff --git a/lib/util/bip39.dart b/lib/util/bip39.dart
deleted file mode 100644
index f8e9019..0000000
--- a/lib/util/bip39.dart
+++ /dev/null
@@ -1,49 +0,0 @@
-// From https://github.com/cake-tech/cake_wallet/blob/main/cw_monero/lib/bip39_seed.dart
-
-import 'dart:typed_data';
-import 'package:bip32/bip32.dart' as bip32;
-import 'package:bip39/bip39.dart' as bip39;
-import 'package:polyseed/polyseed.dart';
-
-String getLegacySeedFromBip39(String mnemonic, {int accountIndex = 0, String passphrase = ""}) {
- final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase);
- final bip32KeyPair = bip32.BIP32.fromSeed(seed).derivePath("m/44'/128'/$accountIndex'/0/0");
- final spendKey = _reduceECKey(bip32KeyPair.privateKey!);
-
- return LegacySeedLang.getByEnglishName("English").encodePhrase(spendKey.toHexString());
-}
-
-const _ed25519CurveOrder = "1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED";
-
-Uint8List _reduceECKey(Uint8List buffer) {
- final curveOrder = BigInt.parse(_ed25519CurveOrder, radix: 16);
- final bigNumber = _readBytes(buffer);
-
- var result = bigNumber % curveOrder;
-
- final resultBuffer = Uint8List(32);
- for (var i = 0; i < 32; i++) {
- resultBuffer[i] = (result & BigInt.from(0xff)).toInt();
- result = result >> 8;
- }
-
- return resultBuffer;
-}
-
-/// Read BigInt from a little-endian Uint8List
-/// From https://github.com/dart-lang/sdk/issues/32803#issuecomment-387405784
-BigInt _readBytes(Uint8List bytes) {
- BigInt read(int start, int end) {
- if (end - start <= 4) {
- var result = 0;
- for (int i = end - 1; i >= start; i--) {
- result = result * 256 + bytes[i];
- }
- return BigInt.from(result);
- }
- final mid = start + ((end - start) >> 1);
- return read(start, mid) + read(mid, end) * (BigInt.one << ((mid - start) * 8));
- }
-
- return read(0, bytes.length);
-}
diff --git a/lib/util/height.dart b/lib/util/height.dart
deleted file mode 100644
index b116a5e..0000000
--- a/lib/util/height.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-import 'dart:async';
-import 'dart:convert';
-import 'dart:math';
-
-import 'package:skylight_wallet/services/tor_settings_service.dart';
-import 'package:skylight_wallet/util/logging.dart';
-import 'package:skylight_wallet/util/socks_http.dart';
-
-Future<int> getCurrentBlockchainHeight() async {
- final urls = [
- 'http://rucknium757bokwv3ss35ftgc3gzb7hgbvvglbg3hisp7tsj2fkd2nyd.onion:18081/get_height', // Rucknium
- 'http://un4yrhwq4d53caoiaadeiur5e5wgkgp74zw3p3twqh3nxh6ztz347dad.onion:18081/get_height', // Triplebit
- 'http://fz2lbxvjob6ifeonngaep2xvf2ypxjjn23i3ncblcxjreovev56ubyyd.onion:18089/get_height', // Unredacted
- ];
-
- urls.shuffle(Random.secure());
-
- final proxyInfo = await TorSettingsService.sharedInstance.getProxy();
-
- if (proxyInfo == null) {
- // FIXME
- return 0;
- }
-
- for (String url in urls) {
- try {
- final response = await makeSocksHttpRequest('GET', url, proxyInfo);
-
- if (response.statusCode == 200) {
- final jsonResponse = jsonDecode(response.body);
-
- if (jsonResponse['height'] is int) {
- return jsonResponse['height'];
- }
- }
- } catch (error) {
- log(LogLevel.error, error.toString());
- }
- }
-
- throw Exception('failedToLoadHeight');
-}
diff --git a/lib/util/tx_notifications.dart b/lib/util/tx_notifications.dart
deleted file mode 100644
index 06aed28..0000000
--- a/lib/util/tx_notifications.dart
+++ /dev/null
@@ -1,89 +0,0 @@
-import 'package:skylight_wallet/models/wallet_types.dart';
-import 'package:skylight_wallet/consts.dart' as consts;
-
-/// How many announced transaction hashes are remembered. The cutoff covers
-/// everything older, so this only has to span transactions near the tip.
-const int maxRememberedTxHashes = 50;
-
-/// What to announce, and the state to persist afterwards.
-class TxNotificationDecision {
- /// Incoming transactions to announce, oldest first.
- final List<TxDetails> toAnnounce;
-
- /// New value for the cutoff (unix seconds).
- final int cutoff;
-
- /// New list of remembered hashes, oldest first.
- final List<String> announcedHashes;
-
- const TxNotificationDecision({
- required this.toAnnounce,
- required this.cutoff,
- required this.announcedHashes,
- });
-}
-
-/// Works out which incoming transactions the user hasn't been told about.
-///
-/// Two pieces of state, because neither is enough alone:
-///
-/// [cutoff] is a coarse "everything before this is old news" line. It stops a
-/// restored wallet — or one that had notifications switched off for a while —
-/// from announcing a backlog, and it bounds how much has to be remembered.
-///
-/// [announcedHashes] catches what a timestamp cannot. A transaction's timestamp
-/// is the time it was *seen* while it sits in the mempool and the *block's*
-/// timestamp once it is mined, so it moves — usually forward, sometimes
-/// backwards — and a payment announced from the mempool would be announced
-/// again on confirmation. The same applies to a transaction that is dropped or
-/// reorged out and later reappears.
-///
-/// Only confirmed transactions advance the cutoff. An unconfirmed one carries
-/// roughly the current time, which can sit well ahead of blocks the wallet is
-/// still scanning; moving the cutoff there would silence whatever those blocks
-/// turn up.
-TxNotificationDecision decideTxNotifications({
- required List<TxDetails> txHistory,
- required int cutoff,
- required List<String> announcedHashes,
- int maxHashes = maxRememberedTxHashes,
-}) {
- final seen = announcedHashes.toSet();
-
- final toAnnounce =
- txHistory
- .where(
- (tx) =>
- tx.direction == consts.txDirectionIncoming &&
- tx.timestamp > cutoff &&
- !seen.contains(tx.hash),
- )
- .toList()
- // Oldest first, so a burst is announced in the order it happened.
- ..sort((a, b) => a.timestamp.compareTo(b.timestamp));
-
- var newCutoff = cutoff;
-
- for (final tx in txHistory) {
- if (isConfirmedTx(tx) && tx.timestamp > newCutoff) {
- newCutoff = tx.timestamp;
- }
- }
-
- final hashes = [
- ...announcedHashes.where((hash) => !toAnnounce.any((tx) => tx.hash == hash)),
- ...toAnnounce.map((tx) => tx.hash),
- ];
-
- return TxNotificationDecision(
- toAnnounce: toAnnounce,
- cutoff: newCutoff,
- announcedHashes: hashes.length > maxHashes ? hashes.sublist(hashes.length - maxHashes) : hashes,
- );
-}
-
-/// True when a transaction is in a block. Deliberately strict: treating an
-/// unconfirmed transaction as confirmed would drag the cutoff forward and
-/// silence real notifications, while the reverse only costs a re-check that
-/// [decideTxNotifications] deduplicates by hash anyway.
-bool isConfirmedTx(TxDetails tx) => tx.height > 0;
diff --git a/lib/util/wallet.dart b/lib/util/wallet.dart
deleted file mode 100644
index 648a4e4..0000000
--- a/lib/util/wallet.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-import 'package:skylight_wallet/consts.dart' as consts;
-import 'package:skylight_wallet/util/dirs.dart';
-
-Future<String> getWalletPath([String? walletFileName]) async {
- var path = (await getAppDir()).path;
- String walletName = walletFileName ?? consts.walletFileName;
- path = '$path/$walletName';
- return path;
-}
diff --git a/lib/widgets/fiat_amount.dart b/lib/widgets/fiat_amount.dart
deleted file mode 100644
index 611ce81..0000000
--- a/lib/widgets/fiat_amount.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-import 'package:flutter/material.dart';
-
-class FiatAmount extends StatelessWidget {
- final String prefix;
- final double amount;
- final double maxFontSize;
-
- const FiatAmount({
- super.key,
- required this.prefix,
- required this.amount,
- required this.maxFontSize,
- });
-
- @override
- Widget build(BuildContext context) {
- return Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '$prefix${amount.toInt()}',
- style: TextStyle(fontSize: maxFontSize, fontWeight: FontWeight.w400),
- ),
- Container(
- margin: EdgeInsets.only(top: maxFontSize * 0.16),
- child: Text(
- (amount % 1).toStringAsFixed(2).substring(2),
- style: TextStyle(
- fontSize: maxFontSize / 2,
- fontWeight: FontWeight.w300,
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/lib/widgets/loading_button.dart b/lib/widgets/loading_button.dart
deleted file mode 100644
index e36bb25..0000000
--- a/lib/widgets/loading_button.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'package:flutter/material.dart';
-
-/// Filled action button with a consistent loading state: while [isLoading] a
-/// left-aligned spinner takes the leading-icon slot and the button is
-/// disabled; the [label] stays visible throughout.
-class LoadingButton extends StatelessWidget {
- const LoadingButton({
- super.key,
- required this.isLoading,
- required this.onPressed,
- required this.label,
- this.icon,
- });
-
- final bool isLoading;
- final VoidCallback? onPressed;
- final String label;
-
- /// Optional leading icon shown when not loading.
- final IconData? icon;
-
- @override
- Widget build(BuildContext context) {
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final spinnerColor = isDark ? Theme.of(context).colorScheme.onPrimary : Colors.white;
-
- return FilledButton(
- onPressed: isLoading ? null : onPressed,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- spacing: 8,
- children: [
- if (isLoading)
- SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(strokeWidth: 2, color: spinnerColor),
- )
- else if (icon != null)
- Icon(icon),
- Text(label),
- ],
- ),
- );
- }
-}
diff --git a/lib/widgets/monero_amount.dart b/lib/widgets/monero_amount.dart
deleted file mode 100644
index 4e342f9..0000000
--- a/lib/widgets/monero_amount.dart
+++ /dev/null
@@ -1,47 +0,0 @@
-import 'package:flutter/material.dart';
-
-class MoneroAmount extends StatelessWidget {
- final double amount;
- final double maxFontSize;
- final String? prefix;
-
- const MoneroAmount({
- super.key,
- required this.amount,
- required this.maxFontSize,
- this.prefix,
- });
-
- @override
- Widget build(BuildContext context) {
- final amountStr = amount.toStringAsFixed(12);
- final smallerSlice = amountStr.substring(amountStr.length - 9);
- final biggerSlice = amountStr.substring(0, amountStr.length - 9);
-
- return Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (prefix != null)
- Text(
- prefix!,
- style: TextStyle(fontSize: maxFontSize, fontWeight: FontWeight.w700),
- ),
- Text(
- biggerSlice,
- style: TextStyle(fontSize: maxFontSize, fontWeight: FontWeight.w700),
- ),
- Container(
- margin: EdgeInsetsGeometry.only(top: maxFontSize * 0.16),
- child: Text(
- smallerSlice,
- style: TextStyle(
- fontSize: maxFontSize / 2,
- fontWeight: FontWeight.w400,
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/lib/widgets/status_icon.dart b/lib/widgets/status_icon.dart
deleted file mode 100644
index 860e7a2..0000000
--- a/lib/widgets/status_icon.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-
-enum StatusIconStatus { loading, complete, fail }
-
-class StatusIcon extends StatelessWidget {
- const StatusIcon({super.key, required this.status, required this.torIsEnabled});
-
- final StatusIconStatus status;
- final bool torIsEnabled;
-
- @override
- Widget build(BuildContext context) {
- final brightness = Theme.of(context).brightness;
- final isDarkTheme = brightness == Brightness.dark;
-
- if (!torIsEnabled) {
- return status == StatusIconStatus.complete
- ? Icon(Icons.check_circle_rounded, size: 26, color: Colors.teal)
- : status == StatusIconStatus.fail
- ? Icon(Icons.cancel, size: 26, color: Colors.red)
- : SizedBox(
- width: 26,
- height: 26,
- child: CircularProgressIndicator(padding: EdgeInsets.all(3), strokeWidth: 2),
- );
- }
-
- return Stack(
- fit: StackFit.loose,
- children: [
- if (torIsEnabled) SvgPicture.asset('assets/icons/tor.svg', width: 22, height: 22),
- Positioned(
- bottom: 0,
- right: 0,
- child: Container(
- width: 12,
- height: 12,
- decoration: BoxDecoration(
- color: isDarkTheme ? Colors.grey[900] : Colors.white,
- shape: BoxShape.circle,
- ),
- child: status == StatusIconStatus.complete
- ? Icon(Icons.check_circle_rounded, size: 12, color: Colors.teal)
- : status == StatusIconStatus.fail
- ? Icon(Icons.cancel, size: 12, color: Colors.red)
- : CircularProgressIndicator(strokeWidth: 2, padding: EdgeInsets.all(2)),
- ),
- ),
- ],
- );
- }
-}
diff --git a/test/tx_notifications_test.dart b/test/tx_notifications_test.dart
deleted file mode 100644
index c5bb6ba..0000000
--- a/test/tx_notifications_test.dart
+++ /dev/null
@@ -1,285 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:skylight_wallet/consts.dart' as consts;
-import 'package:skylight_wallet/models/wallet_types.dart';
-import 'package:skylight_wallet/util/tx_notifications.dart';
-
-/// A transaction as the wallet reports it. [height] of -1 means unconfirmed.
-TxDetails tx({
- required String hash,
- required int timestamp,
- int direction = consts.txDirectionIncoming,
- int height = 100,
- double amount = 1.5,
-}) {
- return TxDetails(
- index: 0,
- direction: direction,
- hash: hash,
- amount: amount,
- fee: 0,
- recipients: const [],
- accountIndex: 0,
- subaddrIndexList: const [0],
- timestamp: timestamp,
- height: height,
- confirmations: height > 0 ? 10 : 0,
- key: '',
- );
-}
-
-/// The wallet hands history back newest first.
-List<TxDetails> newestFirst(List<TxDetails> txs) =>
- [...txs]..sort((a, b) => b.timestamp.compareTo(a.timestamp));
-
-List<String> hashesOf(List<TxDetails> txs) => txs.map((t) => t.hash).toList();
-
-void main() {
- group('decideTxNotifications', () {
- test('announces an incoming transaction newer than the cutoff', () {
- final decision = decideTxNotifications(
- txHistory: newestFirst([tx(hash: 'a', timestamp: 1000)]),
- cutoff: 500,
- announcedHashes: const [],
- );
-
- expect(hashesOf(decision.toAnnounce), ['a']);
- expect(decision.cutoff, 1000);
- expect(decision.announcedHashes, ['a']);
- });
-
- test('ignores outgoing transactions', () {
- final decision = decideTxNotifications(
- txHistory: newestFirst([
- tx(hash: 'out', timestamp: 1000, direction: consts.txDirectionOutgoing),
- ]),
- cutoff: 500,
- announcedHashes: const [],
- );
-
- expect(decision.toAnnounce, isEmpty);
- // The cutoff still moves: it tracks what has been seen, not what was said.
- expect(decision.cutoff, 1000);
- });
-
- test('ignores anything at or before the cutoff', () {
- final decision = decideTxNotifications(
- txHistory: newestFirst([
- tx(hash: 'old', timestamp: 400),
- tx(hash: 'exactly-at', timestamp: 500),
- ]),
- cutoff: 500,
- announcedHashes: const [],
- );
-
- expect(decision.toAnnounce, isEmpty);
- expect(decision.cutoff, 500);
- });
-
- test('announces a burst oldest first', () {
- final decision = decideTxNotifications(
- txHistory: newestFirst([
- tx(hash: 'c', timestamp: 3000),
- tx(hash: 'a', timestamp: 1000),
- tx(hash: 'b', timestamp: 2000),
- ]),
- cutoff: 500,
- announcedHashes: const [],
- );
-
- expect(hashesOf(decision.toAnnounce), ['a', 'b', 'c']);
- expect(decision.cutoff, 3000);
- });
-
- test('a second pass over the same history announces nothing', () {
- final history = newestFirst([tx(hash: 'a', timestamp: 1000)]);
-
- final first = decideTxNotifications(
- txHistory: history,
- cutoff: 500,
- announcedHashes: const [],
- );
- final second = decideTxNotifications(
- txHistory: history,
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- expect(second.toAnnounce, isEmpty);
- expect(second.cutoff, first.cutoff);
- });
-
- test('nothing is announced when the history is empty', () {
- final decision = decideTxNotifications(
- txHistory: const [],
- cutoff: 500,
- announcedHashes: const [],
- );
-
- expect(decision.toAnnounce, isEmpty);
- expect(decision.cutoff, 500);
- });
-
- test('the cutoff never moves backwards', () {
- final decision = decideTxNotifications(
- txHistory: newestFirst([tx(hash: 'old', timestamp: 100)]),
- cutoff: 9000,
- announcedHashes: const [],
- );
-
- expect(decision.cutoff, 9000);
- });
- });
-
- group('mempool and reorg edge cases', () {
- test('a mempool transaction is announced once, not again when mined', () {
- // Seen unconfirmed at t=1000...
- final unconfirmed = tx(hash: 'a', timestamp: 1000, height: -1);
- final first = decideTxNotifications(
- txHistory: [unconfirmed],
- cutoff: 500,
- announcedHashes: const [],
- );
-
- expect(hashesOf(first.toAnnounce), ['a']);
- // An unconfirmed transaction must not drag the cutoff forward.
- expect(first.cutoff, 500);
-
- // ...then mined into a block stamped later than it was seen.
- final mined = tx(hash: 'a', timestamp: 1600, height: 3000);
- final second = decideTxNotifications(
- txHistory: [mined],
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- expect(second.toAnnounce, isEmpty, reason: 'the hash is remembered');
- expect(second.cutoff, 1600);
- });
-
- test('a block timestamp earlier than when the tx was seen is still not repeated', () {
- final first = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 1000, height: -1)],
- cutoff: 500,
- announcedHashes: const [],
- );
- final second = decideTxNotifications(
- // Miner clocks drift; a block can be stamped before the tx was seen.
- txHistory: [tx(hash: 'a', timestamp: 900, height: 3000)],
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- expect(second.toAnnounce, isEmpty);
- });
-
- test('a dropped transaction that reappears is not announced twice', () {
- final first = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 1000, height: -1)],
- cutoff: 500,
- announcedHashes: const [],
- );
-
- // Dropped from the mempool: gone from history entirely.
- final whileGone = decideTxNotifications(
- txHistory: const [],
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- // Rebroadcast and mined later.
- final back = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 5000, height: 3100)],
- cutoff: whileGone.cutoff,
- announcedHashes: whileGone.announcedHashes,
- );
-
- expect(back.toAnnounce, isEmpty);
- });
-
- test('a reorged-out transaction is not announced again when it returns', () {
- final first = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 1000, height: 3000)],
- cutoff: 500,
- announcedHashes: const [],
- );
- expect(hashesOf(first.toAnnounce), ['a']);
-
- // Reorged out and re-mined in a different block, with a new timestamp.
- final after = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 1200, height: 3001)],
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- expect(after.toAnnounce, isEmpty);
- });
-
- test('an unconfirmed tx does not silence older blocks still being scanned', () {
- // A mempool payment is seen while the scanner is far behind the tip.
- final first = decideTxNotifications(
- txHistory: [tx(hash: 'mempool', timestamp: 9000, height: -1)],
- cutoff: 500,
- announcedHashes: const [],
- );
- expect(first.cutoff, 500, reason: 'unconfirmed must not move the cutoff');
-
- // The scan then reaches an older block holding another payment.
- final second = decideTxNotifications(
- txHistory: newestFirst([
- tx(hash: 'mempool', timestamp: 9000, height: -1),
- tx(hash: 'older-block', timestamp: 4000, height: 2900),
- ]),
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- expect(hashesOf(second.toAnnounce), ['older-block']);
- });
- });
-
- group('remembered hashes', () {
- test('are capped, keeping the most recent', () {
- var cutoff = 0;
- var hashes = <String>[];
-
- // 10 unconfirmed transactions, so the cutoff never advances and the cap
- // is the only thing keeping the list bounded.
- for (var i = 1; i <= 10; i++) {
- final decision = decideTxNotifications(
- txHistory: [tx(hash: 'tx$i', timestamp: i * 100, height: -1)],
- cutoff: cutoff,
- announcedHashes: hashes,
- maxHashes: 3,
- );
- cutoff = decision.cutoff;
- hashes = decision.announcedHashes;
- }
-
- expect(hashes, ['tx8', 'tx9', 'tx10']);
- });
-
- test('a hash is not duplicated when the same tx is re-seen', () {
- final first = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 1000, height: -1)],
- cutoff: 500,
- announcedHashes: const [],
- );
- final second = decideTxNotifications(
- txHistory: [tx(hash: 'a', timestamp: 1000, height: -1)],
- cutoff: first.cutoff,
- announcedHashes: first.announcedHashes,
- );
-
- expect(second.announcedHashes, ['a']);
- });
- });
-
- group('isConfirmedTx', () {
- test('treats -1 and 0 as unconfirmed', () {
- expect(isConfirmedTx(tx(hash: 'a', timestamp: 1, height: -1)), isFalse);
- expect(isConfirmedTx(tx(hash: 'a', timestamp: 1, height: 0)), isFalse);
- expect(isConfirmedTx(tx(hash: 'a', timestamp: 1, height: 1)), isTrue);
- });
- });
-}
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.