Skip to content

a0b32b4e - Order the Safe account history by the timestamp it shows - #4461

Merged
TaprootFreak merged 3 commits into
developfrom
fix/custody-history-order-by-valuta
Jul 29, 2026
Merged

a0b32b4e - Order the Safe account history by the timestamp it shows#4461
TaprootFreak merged 3 commits into
developfrom
fix/custody-history-order-by-valuta

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #4448 and DFXswiss/services#1207, both merged.

The problem

The activity list now shows a timestamp per entry: the valuta timestamp for a completed order, the creation date for every other state. The query behind it still ordered by created alone.

An order recorded later but settled faster therefore appears below an older row that carries a younger visible date. Before the timestamps were displayed this was invisible; now the ordering openly contradicts what the reader can see. Fiat and crypto rails settle at different speeds, so this is ordinary data, not a corner case.

The cap makes it worse than cosmetic: the hundred rows kept were selected against a different key than the one on display.

The change

getOrdersByUserData moves from find() to a QueryBuilder, because TypeORM's declarative order option cannot express COALESCE. Filtering and loaded relations are unchanged; the ordering is:

ORDER BY COALESCE("custodyOrder"."completedAt", "custodyOrder"."created") DESC, "custodyOrder"."id" DESC
LIMIT 100

Three details, each of which cost a review pass:

  • The COALESCE expression quotes its own columns. In a raw orderBy string TypeORM does not translate alias.property, so unquoted camelCase columns would be folded to lower case and fail at runtime.
  • limit, not take. This is the one that mattered. take splits the query into an id pass and an entity pass and parses the raw orderBy at every dot — the expression above became a lookup for an alias named COALESCE("custodyOrder", and the method threw on every call. The list would have been dead, not mis-sorted. limit is safe here because every relation joined is to-one, so no row can be duplicated.
  • A tiebreaker on id. Two orders can share a timestamp. Without it their relative order is whatever the database returns, so rows could swap places between calls or cross the cap and vanish. Both criteria sit before the LIMIT, so the selection itself is deterministic, not just the display.

Verification

Gates green. Beyond that, checked end to end against a running local instance with real data:

  • Before the fix the list threw; after it, it loads.
  • An order recorded 28.05. and settled 14.06. now appears first, where the previous ordering put it last.
  • The rendered list reads strictly descending by the dates it shows.
  • The generated SQL was inspected directly to confirm the alias translation and that ordering precedes the cap.

The list displays completedAt for a completed order and created for every
other, but was ordered by created alone. An order recorded later and
settled faster therefore sat below an older row carrying a younger date.
Sorting by the displayed timestamp also fixes which 100 rows survive the
cap - previously the cut was made against a different key than the one
the reader sees.
take() splits the query into an id pass and an entity pass, and parses the
raw orderBy string at every dot on the way - the COALESCE expression turns
into a lookup for an alias that does not exist, and the call throws before
any SQL reaches the database. Every relation joined here is to-one, so no
row can be duplicated and limiting rows equals limiting entities.
@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

Two review passes so far. The first found a blocking defect in the original version of this change and it is worth recording, because it would not have surfaced in any unit test.

.take(100) makes TypeORM split the query into an id pass and an entity pass, and on the way it parses the raw orderBy string at every dot. The COALESCE("custodyOrder"."completedAt", ...) expression therefore became a lookup for an alias literally named COALESCE("custodyOrder", and the method threw before any SQL reached the database — the activity list would have been dead, not merely mis-sorted. .limit(100) avoids that path entirely, and is safe here because every relation joined is to-one, so no row can be duplicated.

Verified end to end against a running local instance with real data, not only by reading:

  • Before the fix the list threw; after it the list loads.
  • An order recorded on 28.05. and settled on 14.06. now appears first. Under the previous created ordering it sat last, which is exactly the defect this PR is about.
  • The rendered list reads strictly descending by the dates it shows: 14.06., 12.06., 11.06., 10.06., 09.06., 05.06., 03.06.

format:check, lint and type-check green.

Two orders can carry the same valuta. Without a tiebreaker their relative
order is whatever the database happens to return, so rows could swap places
between calls, or cross the cap and disappear from the list entirely.
@TaprootFreak

Copy link
Copy Markdown
Collaborator Author

Three review passes. Two real defects came out of them, neither of which a unit test would have caught:

  1. blockingtake(100) made TypeORM split the query and parse the raw orderBy at every dot, turning the COALESCE expression into a lookup for an alias that does not exist. The method threw on every call: the activity list would have been dead, not mis-sorted. Replaced with limit(100), which is safe here because every relation joined is to-one.
  2. minor — no tiebreaker on equal timestamps, so rows could swap places between calls or cross the cap and disappear. Added id DESC; both criteria now sit before the LIMIT, so the selection is deterministic and not just the display.

Verified against a running instance after each change, not only by reading — the list loads, an order recorded 28.05. and settled 14.06. sorts first where it previously sorted last, and the generated SQL was inspected to confirm the alias translation and that ordering precedes the cap.

@TaprootFreak
TaprootFreak marked this pull request as ready for review July 29, 2026 16:35
@TaprootFreak
TaprootFreak merged commit e6139b8 into develop Jul 29, 2026
12 checks passed
@TaprootFreak
TaprootFreak deleted the fix/custody-history-order-by-valuta branch July 29, 2026 17:30
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.

1 participant