Skip to content

feat: export txn history as csv#2210

Merged
i5hi merged 31 commits into
developfrom
1363-csv-export-transactions
May 29, 2026
Merged

feat: export txn history as csv#2210
i5hi merged 31 commits into
developfrom
1363-csv-export-transactions

Conversation

@basantagoswami
Copy link
Copy Markdown
Contributor

@basantagoswami basantagoswami commented May 28, 2026

Fixes #1363

@ethicnology
Copy link
Copy Markdown
Member

Hey @claude can you review this PR according to our AGENTS.md and ARCHITECTURE.md rules and the expected feature detailed in #1363

@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @ethicnology's task in 6m 16s —— View job


PR Review: CSV Transaction Export (v1)

Todos:

  • Read AGENTS.md and ARCHITECTURE.md for repo rules
  • Read issue CSV Export for All Wallet Transactions #1363 (could not fetch — gh and external HTTP are blocked in this sandbox, see note below)
  • Review changed files (use case, formatter, cubit, UI)
  • Verify architecture compliance (ports/adapters, layering)
  • Check tests
  • Compile review

Note on #1363: I couldn't access GitHub issues from this sandbox (auth-blocked gh and external curl/WebFetch). Review is anchored on AGENTS.md + ARCHITECTURE.md and the PR description's note that BIP-329 is out of scope for v1. If #1363 has explicit acceptance criteria, please point me at them and I'll re-check.


Overall, the slice is well-structured — a port + adapter + dedicated use case + cubit + screen, with a unit test for the use case. It mostly respects AGENTS.md. A handful of issues are worth addressing before merge.

🔴 Bugs / correctness

1. Date filter mixes local and UTC timeexport_transactions_csv_usecase.dart:25

final inclusiveEnd = end == null
    ? null
    : DateTime(end.year, end.month, end.day, 23, 59, 59);

DateTime(...) constructs a local time. tx.timestamp from wallet/swap/payjoin entities is normally UTC. Comparing a local "23:59:59 of day Y" against UTC timestamps shifts the inclusion boundary by the device's UTC offset (e.g. in UTC-5 the inclusive end becomes 04:59:59 UTC of the next day; in UTC+10 it cuts off transactions that happened earlier the same UTC day). Same risk applies to start — it's accepted as-is from showDatePicker, which returns local midnight.

The unit test exports_..._test.dart:131 passes only because CI runs in UTC; the bug will not manifest there.

Fix: normalise both ends to UTC explicitly. Either accept UTC dates from the picker (DateTime.utc(picked.year, picked.month, picked.day)) or convert in the use case (DateTime.utc(end.year, end.month, end.day, 23, 59, 59)). Add a test that exercises a non-UTC timestamp.

Fix this →

2. Status reports the wallet-tx status when a swap is in flightcsv_transaction_export_formatter.dart:103-144

_status checks walletTransaction first, then swap. For a chain swap whose send-side onchain tx is confirmed but the swap itself is still pending/claimable, the CSV will show confirmed — hiding the fact that the swap workflow isn't done. Same issue for LN-receive once the wallet tx for the claim is confirmed. Either prefer the swap/payjoin status when one is present, or expose two columns (tx_status, workflow_status). Worth flagging in #1363 since this affects accounting accuracy.

3. network column for Lightning rows shows bitcoin/liquid, not lightningcsv_transaction_export_formatter.dart:58

For a Lightning send/receive, isChainSwap is false, so the column falls through to tx.isBitcoin ? 'bitcoin' : 'liquid'. The type column already says lightning_send/lightning_receive, but a user filtering by network=lightning would never see those rows. Consider 'lightning' when isLightning, or document the convention.


🟠 Architecture / AGENTS.md compliance

4. NoTransactionsToExportError belongs in application/, not domain/transaction_error.dart:11-13

This error describes an application-layer condition ("the filtered query returned nothing"), not a domain invariant. ARCHITECTURE.md rule #11 / AGENTS.md errors-per-layer rule expects:

  • domain/domain_errors.dart (plural)
  • application/application_errors.dart

The existing transaction_error.dart is the feature-prefixed, singular legacy style explicitly flagged in AGENTS.md (the fund_exchange outlier — "don't replicate"). Adding a new application-only error to the legacy file replicates the violation. Suggest:

  • Move NoTransactionsToExportError to lib/features/transactions/application/application_errors.dart.
  • Leave the legacy transaction_error.dart alone (don't reshuffle in this PR — AGENTS.md says don't refactor unrelated legacy).

5. Cubit leaks raw exception strings to the UIexport_transactions_cubit.dart:42

} catch (e) {
  emit(ExportTransactionsState.error(message: e.toString()));
}

AGENTS.md rule #11: "Map foreign errors at every layer boundary; never leak another layer's error type." The UI renders e.toString() via transactionError(s.message). Catch known cases (e.g. FileSystemException, file_picker errors) and emit translated messages; let the rest become a single localised "Export failed" line.

6. Filename construction lives in the cubitexport_transactions_cubit.dart:27-28

Mild but worth noting per AGENTS.md rule #4 (BLoCs are thin). Pushing the filename + bytes encoding into the use case (returning a small ExportResult { filename, bytes }) keeps the cubit as pure state-translation. Also makes the filename testable.

Also: the filename embeds : from the ISO timestamp (2026-05-28T08:54:00Z). Fine on Android/iOS but rejected by Windows / some desktop file managers. Consider 2026-05-28_085400Z since the project's Linux devcontainer hints at desktop builds.

7. Registration nittransactions_locator.dart:51-53

TransactionExportFormatter is a port/adapter, not a use case, but it's registered inside registerUsecases(). Minor; move to a small registerAdapters() or rename the method.

8. Single-file foldersapplication/ports/ and application/usecases/ each contain exactly one file

AGENTS.md rule #14 says don't create the folder until a second file exists, unless it's "imminent in the same PR." The TODO at the top of export_transactions_csv_usecase.dart notes that GetTransactionsUsecase / GetTransactionsByTxIdUsecase should move from domain/usecases/ to application/usecases/. That's defensible, but consider doing the move in a follow-up commit on this branch so the structure isn't half-built when this lands.


🟡 UX / polish

9. Date display ignores localeexport_transactions_screen.dart:46-49

Hardcoded YYYY-MM-DD works but breaks the i18n convention used elsewhere. Use MaterialLocalizations.of(context).formatShortDate(date) for the user-facing string (keep ISO for the CSV and filename).

10. Sort order is oldest-first, ascending — export_transactions_csv_usecase.dart:38. Most accounting tools (and Bull's own list) show newest-first. Either is defensible — flag for #1363's spec.

11. Translations — every locale got the new English keys translated except the two *_Latn/regional files I didn't sample; app_es.arb has a real Spanish translation while many others appear to be auto-translated. Worth confirming with the team whether non-English strings should land empty/English-fallback until human-reviewed (existing pattern in the repo seems mixed).


✅ What looks good

  • Port/Adapter split (TransactionExportFormatter / CsvTransactionExportFormatter) is exactly the hex-arch shape ARCHITECTURE.md asks for.
  • Use case is thin, side-effect free, returns plain String — easy to test.
  • CSV escaping (_escape) correctly handles ,, ", \n, \r per RFC 4180.
  • isOrder filter excludes exchange orders — matches the // this covers self-custodial wallet activity only comment.
  • The new screen uses theme tokens, BBText, BBButton.big, AppLocalizations — no hardcoded colours or strings.
  • Test file lives at the right path (test/features/transactions/application/usecases/...).
  • TODO comment honestly flags the legacy domain/usecases/ location of the other use cases.

🧪 Test gaps

The use case test covers wallet-only transactions. Add cases for:

  • A chain-swap row (verifies _chainNetwork send/receive mapping and swap_fee_sats).
  • A LN-send and LN-receive row (verifies invoice/preimage columns and the network column issue above).
  • A payjoin row (payjoin_send vs payjoin_receive).
  • Multiple labels on one tx (verifies ; join).
  • Null timestamp filtered out when a range is set (currently only the empty-list case covers it implicitly).

Summary

Solid first cut. Top three things to address before merge:

  1. UTC normalisation in the date filter (Use Sync in isolate #1 above) — correctness bug.
  2. Move NoTransactionsToExportError to application/application_errors.dart (Add "bump fee" function in transaction page for RBF transactions #4).
  3. Stop leaking e.toString() to the UI (Background snapshots protection #5).

The status-precedence (#2) and lightning-network labelling (#3) are worth a quick chat with whoever scoped #1363 — they're product decisions, not bugs.
· Branch: 1363-csv-export-transactions

@ethicnology
Copy link
Copy Markdown
Member

@basantagoswami any answers regarding Claude review ?

@BullishNode
Copy link
Copy Markdown
Contributor

@ethicnology comparing Claude with my codex bb-review skill

  1. The CSV exports sensitive Lightning fields without explicit disclosure or
    approval. invoice and preimage are included in the export, including
    LnSendSwap.preimage, while the UI describes this as a generic transaction
    export. Please remove those fields by default or add an explicit opt-in/
    disclosure with tests.

  2. Chain-swap rows can report the wrong network. The generic network
    column always uses the swap send network, even when the row is the receive-
    side wallet transaction. Prefer walletTransaction.network when present, and
    keep send_network / receive_network as swap-leg metadata.

  3. Date filtering has bad edge cases: reversed ranges produce a misleading “no
    transactions” result, and the end date uses 23:59:59, excluding sub-second
    transactions in the final second. Validate picker ranges and use a half-open
    end bound: < endDate + 1 day.

  4. backup_settings imports transactions/ui/transactions_router.dart,
    bypassing the facade-only feature boundary, and FEATURES.md is not updated.
    Either remove the backup-settings entry and keep export discoverability in
    Transaction History, or expose a public transactions navigation contract and
    update the graph.

  5. ExportTransactionsCubit owns file saving (file_picker), filename
    construction, byte encoding, and raw e.toString() error display. Move save
    orchestration behind an application port/use case and map failures to
    localized presentation states.

  6. The export screen uses a fixed Column with long localized copy, date
    fields, Spacer, and a bottom CTA. This can overflow on small/large-text
    layouts. Make the body scrollable while keeping the CTA reachable.

  7. The formatter port/DI layer and single-file application/usecases/ folder
    are premature for one CSV-only export. Simplify the formatter path and place
    the single use case directly under application/ per AGENTS.md.

@ethicnology
Copy link
Copy Markdown
Member

@basantagoswami
Copy link
Copy Markdown
Contributor Author

@claude

  • utc bug: tx.timestamp is already local time, because it's built via DateTime.fromMillisecondsSinceEpoch without isUtc: true
  • swap status precedence: fix, swap status is checked before wallet tx status now
  • lightning network column: lightning now has 'lightning' for network and nothing for address
  • NoTransactionsToExportError: moved to application/transactions_application_error.dart along with InvalidDateRangeError
  • cubit e.toString(): now emits error with no message, ui shows localized error message
  • filename issue in cubit: TransactionExportSaver port + CsvTransactionExportSaver adapter, and filename now uses _ as separator
  • single file in application/usecases/ breaking the arch doc rule: fixed by moving GetTransactionsUsecase and GetTransactionsByTxIdUsecase also
  • formatter in registerUsecases() moved to registerAdapters()
  • screen overflow: fixed using scrollable body + pinned CTA
  • date locale: fixed using MaterialLocalizations.formatShortDate
  • sort order: fixed to be newest first
  • date range edge cases: InvalidDateRangeError on reversed range, half-open end bound (< endDate + 1 day)
  • test gaps: added new tests, updated to reflect code changes
  • bolt11 invoice: removed column completely, only keeping preimage

review again

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @basantagoswami's task in 6m 39s —— View job


Re-review of v2 — most prior items addressed, two AGENTS.md issues remain

Todos:

  • Re-read AGENTS.md / ARCHITECTURE.md / FEATURES.md
  • Inventory commits since previous review
  • Verify each prior review item was addressed
  • Read the new files (saver port + adapter, application errors, updated cubit/screen/formatter/usecase/tests)
  • Cross-check Codex bb-review points
  • Look for regressions / new issues
  • Post review

Solid pass on the previous feedback — every functional item from my v1 review and most of the Codex items are addressed (UTC/half-open boundary, swap-status precedence, LN network/address, saver port + filename, locale-aware date display, newest-first sort, reversed-range validation, screen overflow, scrollable body + pinned CTA, tests for LN/chain-swap/payjoin/null-timestamp). Two AGENTS.md compliance items still need attention before merge, plus a couple of smaller flags.

🔴 AGENTS.md compliance — please address

1. Error file name violates rule #11lib/features/transactions/application/transactions_application_error.dart

AGENTS.md rule #11 (and ARCHITECTURE.md §"Error handling") is explicit:

application/application_errors.dart (plural). The feature name is already implied by the path; don't repeat it. Caveat: the fund_exchange feature uses <feature>_<layer>_error.dartlegacy divergence; reconcile over time, don't replicate.

The new file is both feature-prefixed (transactions_) and singular (error) — exactly the fund_exchange outlier shape AGENTS.md flags as "don't replicate." Suggested:

  • Rename to lib/features/transactions/application/application_errors.dart.
  • (Leave the legacy domain/transaction_error.dart alone — that's an unrelated cleanup.)

Fix this →

2. New cross-feature edge backup_settings → transactions is undocumentedbackup_settings_screen.dart:11 + FEATURES.md

import 'package:bb_mobile/features/transactions/ui/transactions_router.dart';

This adds a previously-absent BACKUPS → TX_HISTORY dependency. Two issues:

a) Bypasses facade convention (rule #1). Compare to the labels button in the same file: it does import 'package:bb_mobile/features/labels/labels_facade.dart'; and uses LabelsRouter re-exported by the facade. Transactions has no facade today, so this PR is implicitly creating a new precedent of importing ui/<feature>_router.dart directly. Yes — wallet already does this, so the violation is legacy-tracking rather than novel, but AGENTS.md is explicit: "do not replicate violations." The clean fix is small: add lib/features/transactions/public/transactions_facade.dart that re-exports ui/transactions_router.dart (matches the labels pattern), and import that from backup_settings.

b) FEATURES.md graph not updated (rule #13). Rule #13 says any PR that changes which features another feature consumes must update the mermaid graph in the same commit. The new BACKUPS --> TX_HISTORY edge isn't in FEATURES.md (currently only BACKUPS --> BIP85 / TOR / WALLETS).

Fix this →


🟠 Worth a discussion (not blockers)

3. network column for chain-swap rows still shows the send legcsv_transaction_export_formatter.dart:186

After the new dedup (commit 897592ed1), the kept row for a self-swap (user holds both legs) is the receive wallet tx, but _resolveNetwork still returns _chainNetwork(swap, send: true). For a bitcoinToLiquid self-swap, the user sees network=bitcoin even though the kept wallet tx is on liquid. The dedicated send_network / receive_network columns disambiguate, but anyone filtering or pivoting on the main network column will misread the row.

Two clean options:

  • Use the wallet tx's network when present (tx.isBitcoin ? 'bitcoin' : 'liquid') and keep send/receive_network as the leg-specific source of truth. Aligns with address = receiveAddress which already prefers the receive side.
  • Or document that network for chain_swap is always the send leg — and update the existing test exports_..._test.dart:364 with a comment saying the column intentionally diverges from the row's wallet tx.

The current test (line 364) encodes today's behavior but doesn't argue why — and Codex's flag #2 still applies to it.

4. swap.amountSat is unwrapped inside a null-aware chaincsv_transaction_export_formatter.dart:116

swapFeeSats: swap?.fees?.totalFees(swap.amountSat).toString() ?? '',

swap is Swap?; the bare swap.amountSat works only because flow analysis promotes within the null-aware chain. It compiles today, but is fragile — refactors or analyzer-version bumps can break promotion. Cheap fix: swap?.fees?.totalFees(swap!.amountSat) or extract final fees = swap?.fees;/final amt = swap?.amountSat; and gate.

5. The use-case unit test instantiates the concrete CsvTransactionExportFormatter directlyexport_transactions_csv_usecase_test.dart:105

Couples the use-case test to the CSV format. Fine while CSV is the only format, but if a second formatter ever lands (e.g. BIP-329 JSON), these tests will start asserting CSV-specific column indices for behavior that should be format-agnostic. Worth a // TODO: fake formatter when a second format lands note now, or split into "use case orchestrates filter/sort/dedup" tests (with a fake formatter) and "CSV formatter renders rows correctly" tests (against the real adapter).


✅ What's resolved / good

  • Date filter — half-open < end + 1 day boundary is the right shape (and the reversed-range guard before it is a nice touch). Local-time semantics here are consistent with the user picking days from a local-time showDatePicker; the CSV's UTC-formatted date column is unambiguous for analysis tools. Worth a one-line comment that the picker semantics are local and the CSV is UTC, so future readers don't conflate them.
  • Swap-status precedence (csv_transaction_export_formatter.dart:149) — swap first, then wallet tx, then payjoin. Matches the test at line 222.
  • LN network/addresslightning for network, empty for address. Asserted in two tests.
  • Saver port + adapter (transaction_export_saver.dart, csv_transaction_export_saver.dart) — keeps file_picker out of the cubit and gives the filename construction a clear home. Filename now uses _ (Windows-friendly).
  • Cubit — clean: NoTransactionsToExportError and InvalidDateRangeError mapped to distinct states; everything else logged with stack and emitted as a parameterless .error(). UI picks the localized string. Matches rule Change address must inherint label of outbound transaction label #11's boundary-mapping principle.
  • Single-file folder fix — moving GetTransactionsUsecase and GetTransactionsByTxIdUsecase to application/usecases/ solves rule Trigger wallet sync after import or recovery #14 (now 3 files; folder is justified).
  • LocatorregisterAdapters runs before registerUsecases in locator.dart:82-83; resolution order is correct.
  • ScreenExpanded(SingleChildScrollView(...)) + pinned bottom Padding(BBButton.big) handles overflow on small viewports / large text. Locale-aware date string via MaterialLocalizations.formatShortDate.
  • Tests — good coverage on LN send/receive, chain-swap dedup, chain-swap-with-only-send-leg, payjoin send/receive, swap-status precedence, null-timestamp filtering, sort order, reversed-range error.
  • Removed invoice column — keeping just preimage is a reasonable v1 compromise on the Codex sensitivity point. (Preimage is still sensitive but it's the payment proof, which is what users actually want from an export.)

Summary

Two AGENTS.md items to address (error filename + facade/FEATURES.md edge); the chain-swap network column and the swap.amountSat unwrap are worth a quick fix or a comment.
· Branch: 1363-csv-export-transactions

@basantagoswami basantagoswami changed the title feat: export txn history as csv (v1) feat: export txn history as csv May 29, 2026
@basantagoswami
Copy link
Copy Markdown
Contributor Author

basantagoswami commented May 29, 2026

  • error file renamed to application_errors.dart
  • chain-swap network column now shows receive network
  • facade rejecte, every other cross-feature consumer of TransactionsRoute (wallet, buy, sell, send, pay, receive, withdraw) imports transactions_router.dart directly; the facade pattern is labels-only and not replicated elsewhere, so skipping it for now

i5hi and others added 2 commits May 29, 2026 15:09
Replace presence checks (socket file exists, daemon process running,
keyring file exists) with functional probes (bus accepts Peer.Ping,
secret-tool store/clear round-trips). Presence checks passed when a
crash left a stale dbus socket, or when the keyring file existed but
was locked with a forgotten password, causing the app to die at init
with "Failed to unlock the keyring".

Also runs the bootstrap store/clear unconditionally (cheap and
idempotent) and asserts usability at the end of postStartCommand so
failures surface here instead of mid-app-init.
ethicnology and others added 7 commits May 29, 2026 10:08
Presence checks (socket file exists, daemon process running, keyring
file present) passed when a crash left a stale dbus socket or when
the keyring file existed but was locked with a forgotten password,
causing the app to die at init with libsecret_error: Failed to
unlock the keyring.

Replace with functional probes: dbus Peer.Ping, and secret-tool
store/clear round-trip against the default collection. Bootstrap
runs unconditionally (cheap, idempotent), and a final assertion
fails postStartCommand here rather than mid-app-init.
Replace the inline QrImageView in ShowAnimatedQrWidget with the shared
QrDisplayWidget so the QR keeps a theme-independent ~26px white quiet
zone (16px surfaceFixed padding + qr_flutter's internal padding) and a
contain-fit 300x300 size. Large PSBT payloads previously filled the
parent Container — whose color is appColors.background (black in dark
mode) — leaving the camera's auto-exposure unable to resolve the QR
boundary on Jade. Fixes #1917.
@ethicnology
Copy link
Copy Markdown
Member

rebased on develop

@i5hi i5hi merged commit bab5c06 into develop May 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CSV Export for All Wallet Transactions

5 participants