Skip to content

Release: develop -> main - #4576

Merged
TaprootFreak merged 1 commit into
mainfrom
develop
Aug 1, 2026
Merged

Release: develop -> main#4576
TaprootFreak merged 1 commit into
mainfrom
develop

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 1, 2026

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

…4568)

* feat(payout): log amount, asset and chain per escalated payout order

`logFailedOrders` wrote a single collecting line for the whole batch, carrying only order id,
context and correlation id. Two consequences: judging an escalation always required a DB lookup
for what was actually at stake, and log-based monitoring could only ever extract one order per
line - a batch of seven escalations (as happened on 2026-07-24) surfaced exactly one of them,
because a regexp matches a line once.

Write one line per escalated order in addition to the collecting line, carrying the payout
amount, its asset and the chain the payout was going out on. The collecting line is unchanged:
it is also the body of the escalation mail.

The wording is a parsing contract - every field is fenced by a literal on both sides so a value
containing a space or a comma cannot swallow the next one - and a new spec pins the shape,
including the nullable asset relation degrading to a placeholder rather than to an unparsable
line.

* docs(payout): name the mail path precisely in the escalation log comment

* fix(payout): quote the asset name in the escalation line and cover the fence cases

Review follow-up on the parsing contract, two gaps:

The asset name is the only free-form value in the line. Fenced only by ` of ` and ` on chain `, a name that
happened to contain " on chain " would have ended the asset field early and handed the parser a wrong chain -
with no parse error at all. A silently wrong value in a critical alert is worse than a loud failure, so the
name is now quoted; the only name that can still break parsing is one containing an apostrophe, and that
breaks visibly.

`?? 'unknown'` also did not cover an empty name: it only catches null/undefined, so `name: ''` would have
produced an unparsable line - exactly what the fallback exists to prevent. Now `||`.

Two tests added for the cases that were missing: an empty asset name, and a name carrying the fence wording.

* test(payout): pin the apostrophe case as the accepted limit of the quoting

The service comment claims that an apostrophe in the asset name breaks the line visibly
rather than mis-parsing it. That was prose only: nothing failed if a future change to the
escaping turned it back into a partial match with a wrong chain. Now it is a test.

* test(payout): give the batch case distinct chains and assets

The multi-order test used the same asset and chain for both orders and only checked the id per
line, so it could not have caught fields being mixed up between orders in a batch - which is the
one place where that could happen. Now the two orders differ in amount, asset and chain, and every
field is matched against the line of its own order.

* fix(payout): read the asset name greedily - quoting alone can be forged

Review follow-up. The claim that an apostrophe in the asset name always breaks parsing
visibly was wrong. Read lazily, the field ends at the first quote INSIDE the name, so a name
like `Foo' on chain Ethereum` closes its own field and then imitates the next fence: the
pattern matches happily and yields a WRONG chain, with no error at all - precisely the silent
failure the quoting was meant to prevent.

The fix belongs on the reading side. The pinned contract now reads the name greedily, up to the
LAST `' on chain ` before `, context`. That fence is always the one this service wrote, so no
value inside the name can forge it.

Two consequences for the tests: the apostrophe case is no longer an accepted casualty but parses
correctly, and the adversarial name that imitates the fence is pinned as its own case.

* fix(payout): JSON-encode the free-form fields instead of fencing them with a quote

Third round on the same defect, so this time the approach goes rather than the symptom.

A plain quote around a free-form value is forgeable in BOTH reading directions, and the two
previous attempts each closed one and opened the other:

- read up to the FIRST quote, a name like `Foo" on chain Ethereum` closes its own field and
  imitates the next fence -> wrong chain, no error;
- read up to the LAST one, a later free-form field offers a competing fence. `correlationId` is
  a plain string column, so `129680" on chain FAKE, context FAKE, correlation "x` does exactly
  that -> wrong chain, no error, and this one is reachable through the admin manual-payout path,
  where the id is only `@IsString()`.

Both were measured, not argued. Neither reading is safe, because the ambiguity is in the format,
not in the quantifier: the closing quote is not identifiable as long as the value may contain one.

The free-form values are now JSON-encoded and read as JSON strings, with `(?:[^"\\]|\\.)*`
skipping escape pairs. A quote inside a value arrives escaped, so the closing quote is
unambiguous whatever the value contains. Verified for: apostrophe, embedded quote plus a forged
fence, a correlation id carrying a full forged tail, backslash, empty name.

* style(payout): apply Prettier formatting to the escalation spec

The Format check step in CI is stricter than eslint, which is what I had run locally.

* test(payout): pin the backslash case and fix three comments left from the quote-fence version

Two review follow-ups.

The commit that introduced JSON encoding claimed the backslash case as verified, but nothing
pinned it. A trailing backslash is exactly what lets a forged quote slip past a reader that does
not track escape pairs, because `\"` then looks like an escaped quote when it is really an escaped
backslash followed by the real closing one. Both free-form fields now carry one in the tests.

Three explanatory comments still illustrated the forgery with an apostrophe. The fence is a double
quote since the switch to JSON encoding, so the examples described an escape character that no
longer plays any role.

* test(payout): pin backslash parity, not just the presence of a backslash

The single backslash case was not enough to hold the escape mechanism. An encoder that doubles
only the FIRST backslash of a value - a `replace` without the global flag, an entirely ordinary
mistake - passes all twelve existing tests while leaving the line forgeable: with two backslashes
ahead of an embedded quote it emits an odd number of them, the quote then reads as unescaped, and
the chain comes back as `Ethereum" on chain Tron` instead of `Tron`.

Verified by mutation rather than by argument: with that encoder patched in, the new test is the
only one of the thirteen that fails; the service file was restored afterwards and is unchanged.

* test(payout): pin that a newline in a value cannot split the record

The escaping has to cover control characters, not just quote and backslash - and this is where
getting it wrong stops being a parsing problem. A newline inside a value splits the record into
two physical lines, and because the payload can spell out a complete second escalation, the log
would carry a fully invented order with a freely chosen chain and amount. That is a forged record,
not a mis-read field.

`JSON.stringify` already prevents it, but nothing pinned it: an encoder that escapes quote and
backslash correctly and globally, yet leaves control characters alone, passed all thirteen tests.
Verified by mutation - with that encoder patched in, the new test is the only one of fourteen that
fails; the service file was restored afterwards and is unchanged.
@TaprootFreak
TaprootFreak merged commit eff6dd8 into main Aug 1, 2026
7 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