fix(realunit): complete all quotes of a batch settlement tx - #4454
Conversation
The issuer may settle multiple purchases in a single on-chain tx with one transfer event each. Quote completion deduplicated consumed settlements per tx hash, so only the first quote of a batch was ever completed and the remaining quotes were stuck in WaitingForPayment although the shares had arrived. Consumption is now tracked per transfer event, identified by its (tx hash, share amount) pairing and counted, which also reconstructs the consumed event of already completed requests and thereby heals stuck quotes on the next cron run.
|
Reducing the settlement key to the plain tx hash — the very semantics this branch replaces — left all existing specs green, so the amount component of the pairing was never exercised. Add the case a tx-hash-only match gets wrong: the already consumed transfer is not the first event of the batch tx. Also use Util.sort instead of a hand-rolled comparator and move the private helpers behind the HELPER METHODS section marker, as in the sibling job services.
|
Completed the mandatory pre-review process: 2 review passes (conformity and logic, each against the full diff; the second pass ran against the final commit) until zero open findings. What the passes changed:
One reported point was declined with reasoning: Also documented during review: the design premise that the on-chain history carries no per-event id does not hold — the indexer assigns every transfer a stable Gates: |
Problem
The issuer may settle multiple purchases in a single on-chain tx containing one transfer event per purchase (batch settlement).
completeSettledQuotesdeduplicated consumed settlements per tx hash (assumption in the entity comment: "each settlement tx may complete at most one request per user"), so only the first quote of a batch was ever completed — all remaining quotes of the same tx stayed inWaitingForPaymentforever and the app kept showing a pending payment, although the shares had long arrived.Real case: userData 412710 — two purchases (300 CHF → 219 shares, 30'000 CHF → 22'047 shares) were settled by the issuer in one tx (
0xec82fdb0…f940, two transfer events). The 300 CHF quote (2222184) consumed the tx hash; the 30'000 CHF quote (2222199) could never match afterwards.Fix
Consumption is now tracked per transfer event instead of per tx. A consumed event is identified by its (tx hash, share amount) pairing and counted (multiset), which also covers batches containing several same-amount settlements.
getUsedSettlementTxIdsbecomesgetUsedSettlementsand additionally returns the request'sestimatedAmount, from which the consumed event of already completed requests is reconstructed.settlementTxIdsemantics unchanged (plain tx hash, still links to the explorer), no migration neededtxRequestWaitingExpiryCheck,EVERY_DAY_AT_3AM) resets it toCreated(after that it would need manual completion)estimatedAmountis never updated for buy quotes (updateEstimatedAmountis only called for sell/swap payment infos), so the reconstruction is stableKnown limitation — deliberate, tracked in #4459
The amount-based reconstruction is a stopgap, not the target design. It was chosen on the assumption that the on-chain history carries no per-event id — that assumption is wrong: the indexer already assigns every transfer a stable identity (
transferId = ${blockNumber}-${logIndex}, exposed onaccountHistoryasid/transferId, seeDFXswiss/RealUnit-ponder). It is simply not selected inaccountHistoryQuery, so it never reachesHistoryEventDto.Matching on the amount therefore carries three dependencies that exact id matching would not have: rounding via
Math.floor, the unpaginated 100-event history window, and the implicit coupling toupdateEstimatedAmountnever touching buy quotes. None of them break the case above, which is why this ships now — the stuck quote heals only until 31.07. Selecting the event id, persisting it and dropping the heuristic is tracked as #4459 and is a net simplification.Tests
format:check,lint,type-checkclean;jestpayment + realunit suites: 485 passedIncidental changes
Util.sortreplaces the hand-rolled timestamp comparator (identical semantics: same default ASC direction,Number(Date)equals.getTime(), still operating on the fresh array fromhistory.filter(...), so the address-keyed history cache stays untouched)// --- HELPER METHODS --- //marker with the public cron methods first, matching the sibling job services