Background
During the work on #3738 a side-by-side audit of src/integration/exchange/dto/scrypt.dto.ts (+ scrypt.service.ts, scrypt-websocket-connection.ts, exchange-tx.mapper.ts) against the freshly checked-in vendor AsyncAPI spec at src/integration/exchange/docs/scrypt-asyncapi.yaml (see #3737) surfaced a handful of drifts. None of them are regressions introduced by #3738 — most predate it — but several have non-cosmetic risk and should be tracked.
For each item below: verify against the live server payload before changing, because the spec may be incomplete or out of date in a few places (vendor confirmed this is a recurring issue with their docs).
Must verify, then fix
1. ScryptTransactionStatus likely has wrong values
- Code (
scrypt.dto.ts:8-12): Completed | Failed | Rejected
- Spec (
scrypt-asyncapi.yaml:3542-3544, example :1303): PendingApproval | Approved | Rejected
- Impact if spec is correct:
mapScryptStatus (exchange-tx.mapper.ts:111-121) returns 'pending' for every successful withdrawal because Approved falls through to default. Also affects scrypt.service.ts:161 success detection.
- Verify first: tap incoming
BalanceTransaction events on PRD and log the actual Status values seen.
2. ScryptOrderStatus missing Replaced
- Spec (
:2927) lists Replaced (and possibly Removed, Cancelled with double-l) on OrderEvent.OrdStatus. Our enum (scrypt.dto.ts:92-101) doesn't include it.
- Impact: The exhaustive
switch in ScryptService.checkTrade (scrypt.service.ts:341-415) silently returns undefined for unmodeled statuses → the caller treats that as "not done" → order ticks indefinitely.
- Fix: Add
REPLACED = 'Replaced' and a handler (probably: refresh the in-memory report, then re-check).
3. ExecType.CancelRejected / ReplaceRejected not surfaced
- Spec (
:3024) enumerates 18 ExecType values. We don't model an enum and don't branch on ExecType.
- Impact: After a failed cancel,
cancelOrder (scrypt.service.ts:475-494) returns false and editOrder (:522-527) only checks OrdStatus === REJECTED. A CancelRejected ExecType with a still-New OrdStatus is misread as "cancel succeeded, order remains active."
- Fix: Model
ScryptExecType enum and explicitly handle the reject variants.
4. OrderCancelRequest / OrderCancelReplaceRequest missing required TransactTime
- Spec (
:3184, :3210) marks TransactTime as required.
- Code (
scrypt.service.ts:478-483 and :506-512) doesn't include it.
- Impact: Server may reject (verify on DEV first; could be that the spec is stricter than the live server).
- Fix: Add
TransactTime: new Date().toISOString() to both payloads.
Should fix (smaller correctness or hygiene)
5. ScryptTimeInForce enum missing inbound-only values
- Spec (
:2950) has Day, ImmediateOrCancel, GoodTillDate on OrderEvent.TimeInForce. We only model GoodTillCancel | FillAndKill | FillOrKill and only ever send the first.
- Impact: Cosmetic unless we ever subscribe to
Order events.
6. OrdType.RFQ not modeled
- Spec (
:3035, :3145) lists RFQ. We only send Market | Limit.
- Impact: If we ever receive an
ExecutionReport with OrdType: RFQ it deserializes as a raw string we never branch on. Today: theoretical; only matters once RFQ trading is wired.
7. ScryptBalanceTransaction schema diverges
- Code (
scrypt.dto.ts:25-38) declares Fee, TxHash, RejectReason, RejectText, Timestamp, TransactTime.
- Spec
BalanceTransactionEvent (:3512-3548, additionalProperties: false) defines only Timestamp, ClReqID, TransactionID, Revision, TransactionType, MarketAccount, Quantity, Currency, Status, LastUpdateTime.
- Impact: We read phantom fields (
TransactTime in scrypt.service.ts:212, Fee, TxHash, RejectReason, RejectText in the mapper and getWithdrawalStatus). Either the spec is incomplete (likely) or these reads silently produce undefined.
- Verify: log a raw
BalanceTransaction frame on PRD.
8. Revision is required by spec and ignored by us
- Spec (
:3527) requires Revision: integer on every BalanceTransactionEvent.
- Code overwrites the in-memory map by
ClReqID (scrypt.service.ts:86, 94) without comparing revisions. Older messages arriving after newer ones (e.g. after a reconnect + replay) can clobber state.
9. MarketDataSnapshot.Status is ignored
- Spec (
:2433, enum :2445): Online | Offline. Required.
- Code (
fetchOrderBook, scrypt.service.ts:572-586) doesn't check Status before returning prices → a stale Offline snapshot is treated as live.
10. SecurityEvent.UpdateAction ignored
- Spec (
:2757-2760): UpdateAction: Update | Remove. Our ScryptSecurity DTO doesn't model it; getSecurity/getTradePair (scrypt.service.ts:551-578) treats every cached entry as live → a Remove push would never evict a stale security.
11. BalanceTransactionEvent.LastUpdateTime required, unread
- Spec (
:3514): required. We expose externalUpdated via optional Timestamp (exchange-tx.mapper.ts:90). Switch to LastUpdateTime.
Nice to have (future surface)
ExecutionReport.QuoteID, RFQID + TradeEvent.QuoteID, RFQID (:3085-3102, :3334, :3340) — needed for RFQ correlation when we eventually use that path.
QuoteEvent.TradedPx (:3404, "all-in price that includes fees") — would let us bypass the embedded-commission cap logic for RFQ orders.
MarketDataSnapshotSubscription.Depth, Throttle, PriceIncrement (:2402-2421) — we always pull full top-of-book; could subscribe with Depth: "1" to reduce traffic.
HelloEvent (:2374-2389) — log the session ID for support tickets.
What looks correct
- HMAC payload + base64-url translation + headers (
scrypt-websocket-connection.ts:244-254) match spec :99-113 exactly.
- ISO-8601 timestamp with microsecond zero-pad (
.000000Z) matches the spec example precisely.
placeOrder request shape covers all spec-required NewOrderSingleData fields.
NewDepositRequest TxHashes array shape matches spec.
- All numeric quantities sent as strings.
subscribe / unsubscribe / page operation names, pagination via next token.
Acceptance criteria
Background
During the work on #3738 a side-by-side audit of
src/integration/exchange/dto/scrypt.dto.ts(+scrypt.service.ts,scrypt-websocket-connection.ts,exchange-tx.mapper.ts) against the freshly checked-in vendor AsyncAPI spec atsrc/integration/exchange/docs/scrypt-asyncapi.yaml(see #3737) surfaced a handful of drifts. None of them are regressions introduced by #3738 — most predate it — but several have non-cosmetic risk and should be tracked.For each item below: verify against the live server payload before changing, because the spec may be incomplete or out of date in a few places (vendor confirmed this is a recurring issue with their docs).
Must verify, then fix
1.
ScryptTransactionStatuslikely has wrong valuesscrypt.dto.ts:8-12):Completed | Failed | Rejectedscrypt-asyncapi.yaml:3542-3544, example:1303):PendingApproval | Approved | RejectedmapScryptStatus(exchange-tx.mapper.ts:111-121) returns'pending'for every successful withdrawal becauseApprovedfalls through todefault. Also affectsscrypt.service.ts:161success detection.BalanceTransactionevents on PRD and log the actualStatusvalues seen.2.
ScryptOrderStatusmissingReplaced:2927) listsReplaced(and possiblyRemoved,Cancelledwith double-l) onOrderEvent.OrdStatus. Our enum (scrypt.dto.ts:92-101) doesn't include it.switchinScryptService.checkTrade(scrypt.service.ts:341-415) silently returnsundefinedfor unmodeled statuses → the caller treats that as "not done" → order ticks indefinitely.REPLACED = 'Replaced'and a handler (probably: refresh the in-memory report, then re-check).3.
ExecType.CancelRejected/ReplaceRejectednot surfaced:3024) enumerates 18 ExecType values. We don't model an enum and don't branch onExecType.cancelOrder(scrypt.service.ts:475-494) returnsfalseandeditOrder(:522-527) only checksOrdStatus === REJECTED. ACancelRejectedExecType with a still-NewOrdStatus is misread as "cancel succeeded, order remains active."ScryptExecTypeenum and explicitly handle the reject variants.4.
OrderCancelRequest/OrderCancelReplaceRequestmissing requiredTransactTime:3184,:3210) marksTransactTimeas required.scrypt.service.ts:478-483and:506-512) doesn't include it.TransactTime: new Date().toISOString()to both payloads.Should fix (smaller correctness or hygiene)
5.
ScryptTimeInForceenum missing inbound-only values:2950) hasDay, ImmediateOrCancel, GoodTillDateonOrderEvent.TimeInForce. We only modelGoodTillCancel | FillAndKill | FillOrKilland only ever send the first.Orderevents.6.
OrdType.RFQnot modeled:3035,:3145) listsRFQ. We only sendMarket | Limit.ExecutionReportwithOrdType: RFQit deserializes as a raw string we never branch on. Today: theoretical; only matters once RFQ trading is wired.7.
ScryptBalanceTransactionschema divergesscrypt.dto.ts:25-38) declaresFee, TxHash, RejectReason, RejectText, Timestamp, TransactTime.BalanceTransactionEvent(:3512-3548,additionalProperties: false) defines onlyTimestamp, ClReqID, TransactionID, Revision, TransactionType, MarketAccount, Quantity, Currency, Status, LastUpdateTime.TransactTimeinscrypt.service.ts:212,Fee, TxHash, RejectReason, RejectTextin the mapper andgetWithdrawalStatus). Either the spec is incomplete (likely) or these reads silently produceundefined.BalanceTransactionframe on PRD.8.
Revisionis required by spec and ignored by us:3527) requiresRevision: integeron everyBalanceTransactionEvent.ClReqID(scrypt.service.ts:86, 94) without comparing revisions. Older messages arriving after newer ones (e.g. after a reconnect + replay) can clobber state.9.
MarketDataSnapshot.Statusis ignored:2433, enum:2445):Online | Offline. Required.fetchOrderBook,scrypt.service.ts:572-586) doesn't checkStatusbefore returning prices → a staleOfflinesnapshot is treated as live.10.
SecurityEvent.UpdateActionignored:2757-2760):UpdateAction: Update | Remove. OurScryptSecurityDTO doesn't model it;getSecurity/getTradePair(scrypt.service.ts:551-578) treats every cached entry as live → aRemovepush would never evict a stale security.11.
BalanceTransactionEvent.LastUpdateTimerequired, unread:3514): required. We exposeexternalUpdatedvia optionalTimestamp(exchange-tx.mapper.ts:90). Switch toLastUpdateTime.Nice to have (future surface)
ExecutionReport.QuoteID, RFQID+TradeEvent.QuoteID, RFQID(:3085-3102,:3334,:3340) — needed for RFQ correlation when we eventually use that path.QuoteEvent.TradedPx(:3404, "all-in price that includes fees") — would let us bypass the embedded-commission cap logic for RFQ orders.MarketDataSnapshotSubscription.Depth, Throttle, PriceIncrement(:2402-2421) — we always pull full top-of-book; could subscribe withDepth: "1"to reduce traffic.HelloEvent(:2374-2389) — log the session ID for support tickets.What looks correct
scrypt-websocket-connection.ts:244-254) match spec:99-113exactly..000000Z) matches the spec example precisely.placeOrderrequest shape covers all spec-requiredNewOrderSingleDatafields.NewDepositRequestTxHashesarray shape matches spec.subscribe / unsubscribe / pageoperation names, pagination vianexttoken.Acceptance criteria
Must verify, then fixitem: a quick PRD log capture to confirm the actual server payload, then DTO + handler fixShould fixitems: fixed unless verification shows the spec is wrong (then leave a code comment with the discrepancy)