Skip to content

Release: develop -> main - #4504

Merged
TaprootFreak merged 5 commits into
mainfrom
develop
Jul 30, 2026
Merged

Release: develop -> main#4504
TaprootFreak merged 5 commits into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown

Automatic Release PR

This PR was automatically created after changes were pushed to develop.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge when ready for production

…points (#4498)

* Document the `wait` naming convention for long-polling endpoints

An endpoint that blocks on purpose must carry `wait` as its own path
segment, and an endpoint expected to answer quickly must not use the
word anywhere in its path.

Monitoring depends on it: the "Slowest requests" panel of the
dfx-api-traces dashboard excludes routes matching `^.*/wait(/.*)?$` on
http.route, because a long poll's duration measures how long a customer
took to act rather than how long the API computed. Naming a blocking
endpoint without `wait` turns it into a permanent latency outlier that
masks real regressions; naming a fast endpoint with `wait` silently
removes it from the latency table.

* Tighten the wait-naming section after review

Four corrections:

- The rule said a fast endpoint must not use `wait` "anywhere in its
  path", while the same section states that only a complete `wait`
  segment matches. Reworded to "as a path segment" so the rule and its
  matching semantics agree.
- "cancelled" -> "canceled": CONTRIBUTING.md requires American English
  spelling, and this was the only occurrence in the file.
- Dropped the internal dashboard name, the concrete row cap and the
  reference to the infrastructure repository. This is a public repo; the
  rule stands on its own without naming internal monitoring assets.
- Added a cross-reference under Naming Conventions, where URL routes are
  already covered, so the rule is discoverable from there.

* List the blocking endpoints that carry no wait segment

Review found the endpoint table incomplete: three further routes block
on an external event without a `wait` segment.

- `GET /v1/lnurlp/:id` polls for a pending payment via Util.poll,
  default 10 s and extendable by the caller through `params.timeout`.
  It shows up in latency monitoring with peaks around 20 s.
- `GET /v1/node/:node/tx/:txId` and its `:mode` variant poll for a
  confirmation for up to 600 s.

None of them can reasonably be renamed: the LNURL pay-request path is
encoded into LNURLs already in circulation, and the node routes are
admin-only and excluded from the API docs. They are now listed as
explicit exemptions rather than left unmentioned, so the rule describes
the codebase as it is instead of claiming a clean slate.

Also corrects the resolution condition: for MULTIPLE-mode payment links
the waiter resolves once a quote reaches the completion threshold, while
the payment itself can stay Pending.

* Make the wait segment the default and exemptions explicit

The previous wording bound only new endpoints and closed the list of
exemptions outright. That is the wrong shape: the segment is the
standard, and an exemption stays possible — but only as an explicit,
justified entry in the table.

A blocking route that neither carries the segment nor appears in the
table is now stated to be a defect, with the two acceptable fixes named
(rename, or document). This keeps the rule enforceable without pretending
the three existing exemptions can be legislated away.

* Separate passive waiting from waiting on your own operation

Review showed the definition was too broad. Five further routes match
"response time governed by an external event" without carrying a wait
segment:

- GET /v1/lnurlp/tx/:id (waits for an on-chain confirmation)
- PUT /v1/sell/paymentInfos/:id/confirm, authorization branch
- PUT /v1/swap/paymentInfos/:id/confirm, authorization branch
- PUT /v1/realunit/sell/:id/confirm, eip7702 branch
- PUT /v1/realunit/transfer/:id/confirm

Listing them as exemptions would have been the wrong fix. They differ in
kind from a long poll: each starts an operation and then waits for that
operation to complete, so the duration measures work the API set in
motion. That is exactly the signal latency monitoring should surface —
PUT /v1/realunit/sell/:id/confirm has been observed at 47 s. Hiding it
would defeat the purpose.

The rule now says so explicitly: `wait` is for endpoints that do nothing
but wait for another actor, and endpoints awaiting their own operation
must not carry the segment.

* Scope the defect wording to passive waiting only

Review found an internal contradiction: the section first states that
endpoints awaiting their own operation must not be named `wait`, then
declares unconditionally that "a blocking route that is neither named
`wait` nor listed here is a defect". Read literally, that reclassified
the five routes just legitimized two paragraphs earlier as undocumented
defects.

Both the rule sentence and the monitoring bullet now say "passively
waiting" instead of "blocking", and the rule states explicitly that
routes awaiting their own operation need no table entry.

* Reclassify GET /v1/lnurlp/tx/:id as a passive waiter

Review caught a misclassification. The route was listed as "waits for
its own operation", but the payment request tells the payer to broadcast
the transaction themselves and send back only the hash
(payment-request.mapper.ts): "Broadcast the signed transaction to the
blockchain and send the transaction hash back via the endpoint". The API
then polls for a confirmation of a transaction it never sent — passive
waiting on an external actor, the same class as the other exemptions.

Its `hex` branch does broadcast, but returns right after txInMempool
without awaiting confirmation, so it is not a long poll at all.

The route therefore moves into the exemption table. Like the LNURL
pay-request path, it cannot be renamed: the URL is handed to the payer
inside the payment request.

* Correct the hex-branch note for the ICP case

The exemption row claimed the `hex` branch "broadcasts and returns
immediately — it does not wait". That holds for the EVM and Firo paths,
but the ICP sub-branch waits for the payer's allowance first
(payment-quote.service.ts:711, Util.retry with 3 attempts at 2 s) before
broadcasting. The classification of the route is unaffected — it stays a
passive waiter and an exemption — but the stated reason was inaccurate.
`Util.retry(action, 3, 2000)` runs three attempts in total, not three
retries after a first failure: the parameter is named `tryCount` and the
recursion guard is `tryCount > 1` (util.ts:685). The wait therefore spans
at most two delays of 2 s, not three.

Follow-up to #4498, where the wording said "3 retries at 2 s".
…4506)

* perf: skip balancesByFinancialType in financial log query on demand

The financial-overview screen calls GET /v1/dashboard/financial/log every
minute but never reads balancesByType; that sub-tree still made up 82% of
the response and roughly half the DB time (measured 2030ms vs 1078ms for
the same query). Add an opt-out query parameter that drops the jsonb
sub-tree from the SELECT list instead of just discarding it after loading.
Existing callers that don't pass the parameter keep getting the exact same
response as before.

* style: apply prettier line width to the new service signature

* test(dashboard): pin the byType query parameter at the controller

The endpoint had no controller test at all, so a transposed argument or an
inverted string check would have passed every existing service and repository
test. Six inputs are pinned; only the exact string false disables the subtree.

* test(dashboard): pin dailySample and the from edge cases too

dailySample is evaluated by the same rule as byType but was only ever tested
with true and false, so inverting its check would have passed. It now gets the
same six inputs, each holding byType at the opposite value so a transposition
of the two arguments cannot hide. Adds the two untested from branches: omitted
yields undefined, and an unparseable value throws without reaching the service.

* test(dashboard): cover the empty-string case for from

An empty from takes the same early-return branch as a missing one, but only
this case catches a mutation of that branch to an undefined check: an empty
string would then reach Date parsing and throw instead of yielding undefined.
…er aggregate (#4497)

* perf(trading): index trading_order by (tradingRuleId, id) and rewrite the per-rule max-order lookup

TradingRuleService.getCurrentTradingOrders ran a table-wide GROUP BY MAX(id)
aggregate over trading_order once per minute (LogJobService), scanning 5.4M
rows to compute 17 maxima (474 ms, ~680 MB read per call).

The obvious per-rule rewrite is not safe on its own: measured in production
against the current index set, a correlated MAX(id) subquery per rule takes
4,329 ms — 9x slower than today, because Postgres has no index to jump
straight to a rule's row range and PostgreSQL 17.10 has no index skip scan.
So this ships the composite index and the query rewrite together, never
apart: migration/1785480000000-AddTradingOrderRuleIdIndex.js adds
("tradingRuleId", "id"), and getCurrentTradingOrders now looks up the max
id per rule directly, letting Postgres use that index (17 cheap lookups
instead of one full scan). The existing single-column index on
tradingRuleId is left in place; the migration docstring notes the
resulting redundancy.

Added trading-rule.service.pg.spec.ts (pg-mem, real generated SQL) to pin
the rewrite: highest id per rule, rules without orders produce no entry,
an order with no matching rule is excluded, and the result is checked
against the pre-rewrite aggregate on the same seeded data.

* fix(trading): resolve migration ordering collision, document N+1 trade-off

AddTradingOrderRuleIdIndex1785480000000 sorted before the already-merged
ClearDevUserSignatures1785500000000, so it would run before an already-executed
migration. Renamed to 1785510000000 (class name and name property updated to
match); the generated index name IDX_710fd49e19d248643cb2afa70f is unaffected,
since TypeORM derives it from the table and column names, not the timestamp.

Also documents in getCurrentTradingOrders that the per-rule MAX(id) lookups are
an N+1 pattern that scales linearly with the rule count (currently 17 rules ->
19 queries), and why a single correlated-subquery alternative was skipped: it
would give up testability against the lightweight mirror entities used in
trading-rule.service.pg.spec.ts.

* revert(trading): keep the single-statement aggregate, ship the index alone

The per-rule rewrite is withdrawn. It traded one READ COMMITTED snapshot for N,
so two rules could report maxima from different points in time — not acceptable
where LogJobService writes the FinanceLog from the result. The coherent
single-statement alternative is a correlated subquery, which pg-mem cannot
execute at all, so it could not be covered by a test that runs.

The index stays: measured against a Postgres 17.10 rebuild with production
planner settings, the unchanged aggregate moves from a Parallel Seq Scan over
93,486 blocks to a Parallel Index Only Scan over 15,020 with zero heap fetches.
The docstring drops the claim that index and rewrite must ship together.

The test loses its comparison against a re-created copy of the same aggregate,
which could no longer fail, and gains an empty-rule-table case plus an explicit
assertion that no null reaches the In(...) list.

* docs(migration): drop an unmeasured table size from the docstring intro

The opening sentence claimed ~920 MB, which matches none of the measurements
this migration cites — production reports 698 MB for the heap and the rebuild
730 MB. The intro now carries no figures at all; the measured ones stay further
down, each with its source named.

* fix(test): give the pg-mem mirror entity the relation the join needs

The suite was written for a query shape that took no join, so the mirror
entity carried only the tradingRuleId column. Restoring the single-statement
aggregate brought back innerJoin on a relation path, which TypeORM resolves
through entity metadata — all three tests aborted with 'Relation with property
path tradingRule in entity was not found'. Foreign key creation stays off so
the deliberately orphaned fixture remains insertable.

* fix(test): narrow the findBy spy argument instead of casting to a made-up shape

The cast asserted a hand-written shape onto findBy's argument type, which
also allows an array — TypeScript rejected it as insufficiently overlapping.
The argument is now narrowed: an array condition throws, then instanceof
FindOperator establishes the real TypeORM type. The assertion itself is
unchanged, and every unexpected shape fails loudly rather than skipping.

* docs(test): say what the operator guard actually checks

The comment claimed a different operator would fail loudly, but the guard
tests the FindOperator base class — Any, Not and Between all pass it. The
guard's real job is rejecting a bare value that would slip past the array
check, and the operator choice is not what this test is about.
…endpoint (#4512)

* perf(statistic): load only status settings for the status endpoint

GET /v1/statistic/status is polled about six times a minute and returns
32 bytes. It went through SettingService.getAll(), which reads the whole
setting table and filtered for the Status suffix in JS afterwards.

Measured in production: the table holds 76 rows totalling 1586 kB of
values, of which the two Status keys are 14 bytes. A single unrelated
entry (ledgerCutoverBoundary.trading_order) accounts for 1534 kB and was
transferred and materialised into entities on every poll.

Filter in the database instead, per CONTRIBUTING ("filter in SQL not
JS"). getAll() stays as it is — the settings endpoint still needs every
row. The response is unchanged.

* refactor(setting): use find with Like and a deterministic order

Review follow-ups:
- Prefer the repository method over the query builder, per CONTRIBUTING;
  every other suffix filter in this codebase uses Like in find options.
- Order by id so the projected-name reduce in StatisticService has a
  defined winner. Two distinct keys can map to the same projected name
  (StatusfooStatus and fooStatusStatus both become fooStatus); the old
  find() path did not guarantee an order either, so this pins behaviour
  that was previously left to the query plan.
- Add a direct repository test asserting the exact find options, so a
  swapped pattern (Status% instead of %Status) or a dropped order fails.
@TaprootFreak
TaprootFreak merged commit 50e38b9 into main Jul 30, 2026
16 checks 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.

1 participant