@openmarketsai/order-sheet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +183 -0
  4. package/dist/auth.d.ts +24 -0
  5. package/dist/auth.js +36 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/client.d.ts +158 -0
  8. package/dist/client.js +253 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/errors.d.ts +34 -0
  11. package/dist/errors.js +34 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/generated/contract.d.ts +28 -0
  14. package/dist/generated/contract.js +176 -0
  15. package/dist/generated/contract.js.map +1 -0
  16. package/dist/http.d.ts +19 -0
  17. package/dist/http.js +58 -0
  18. package/dist/http.js.map +1 -0
  19. package/dist/index.cjs +1742 -0
  20. package/dist/index.d.ts +41 -0
  21. package/dist/index.js +31 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/math.d.ts +22 -0
  24. package/dist/math.js +55 -0
  25. package/dist/math.js.map +1 -0
  26. package/dist/messages.d.ts +21 -0
  27. package/dist/messages.js +97 -0
  28. package/dist/messages.js.map +1 -0
  29. package/dist/odds.d.ts +12 -0
  30. package/dist/odds.js +33 -0
  31. package/dist/odds.js.map +1 -0
  32. package/dist/quote.d.ts +20 -0
  33. package/dist/quote.js +117 -0
  34. package/dist/quote.js.map +1 -0
  35. package/dist/react.cjs +2137 -0
  36. package/dist/react.d.ts +157 -0
  37. package/dist/react.js +297 -0
  38. package/dist/react.js.map +1 -0
  39. package/dist/sheet.d.ts +77 -0
  40. package/dist/sheet.js +648 -0
  41. package/dist/sheet.js.map +1 -0
  42. package/dist/stream.d.ts +71 -0
  43. package/dist/stream.js +160 -0
  44. package/dist/stream.js.map +1 -0
  45. package/dist/types.d.ts +287 -0
  46. package/dist/types.js +2 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/ui/styles.css +284 -0
  49. package/dist/ui/theme.d.ts +108 -0
  50. package/dist/ui/theme.js +180 -0
  51. package/dist/ui/theme.js.map +1 -0
  52. package/dist/ui.cjs +2606 -0
  53. package/dist/ui.d.ts +124 -0
  54. package/dist/ui.js +154 -0
  55. package/dist/ui.js.map +1 -0
  56. package/dist/validate.d.ts +20 -0
  57. package/dist/validate.js +126 -0
  58. package/dist/validate.js.map +1 -0
  59. package/dist/venues.d.ts +19 -0
  60. package/dist/venues.js +103 -0
  61. package/dist/venues.js.map +1 -0
  62. package/dist/wire.d.ts +382 -0
  63. package/dist/wire.js +2 -0
  64. package/dist/wire.js.map +1 -0
  65. package/package.json +78 -0
package/dist/sheet.js ADDED
@@ -0,0 +1,648 @@
1
+ /**
2
+ * One slip for one position. The user picks the venue; the sheet quotes,
3
+ * validates against the served constraints, refines through /preflight, places
4
+ * through /buy with a fresh idempotency key, and follows the order on the
5
+ * `orders` stream (or a poll) to fill and settlement.
6
+ *
7
+ * State is an immutable snapshot; `subscribe` gets the whole thing on every change.
8
+ */
9
+ import { credentialSupportsStream } from './auth.js';
10
+ import { OrderSheetError, isOrderSheetError } from './errors.js';
11
+ import { localIssue } from './messages.js';
12
+ import { formatMoney } from './odds.js';
13
+ import { buildQuote, resolveMaxPrice } from './quote.js';
14
+ import { validateSheet } from './validate.js';
15
+ import { buildVenueOptions, fromServerIssue } from './venues.js';
16
+ const TERMINAL_PHASES = new Set(['filled', 'failed', 'cancelled', 'settled']);
17
+ /** The snapshot a sheet starts from, before anything has loaded. Pure — no I/O. */
18
+ export function initialSheetState(o, ref, init = {}) {
19
+ const userMaxPrice = init.maxPrice ?? null;
20
+ return {
21
+ phase: 'loading',
22
+ position: 'position' in ref ? ref.position : null,
23
+ venues: [],
24
+ venue: null,
25
+ mode: init.mode ?? o.defaults.mode,
26
+ orderType: init.orderType ?? o.defaults.orderType,
27
+ stake: init.stake
28
+ ? ('amount' in init.stake ? { unit: 'amount', value: init.stake.amount } : { unit: 'shares', value: init.stake.shares })
29
+ : { unit: o.defaults.stakeUnit, value: null },
30
+ maxPrice: userMaxPrice,
31
+ maxPriceIsAuto: userMaxPrice == null,
32
+ slippage: init.slippage ?? o.defaults.slippage,
33
+ oddsFormat: init.oddsFormat ?? o.defaults.oddsFormat,
34
+ quote: null,
35
+ validation: { ok: false, blocking: [], warnings: [], info: [], all: [] },
36
+ preflight: null,
37
+ preflightStatus: 'idle',
38
+ result: null,
39
+ orders: [],
40
+ error: null,
41
+ account: null,
42
+ canPlace: false,
43
+ confirmLabel: '',
44
+ hint: null,
45
+ loading: true,
46
+ updatedAt: o.now(),
47
+ };
48
+ }
49
+ /**
50
+ * A sheet that does nothing: no fetch, no timer, no listener on the client.
51
+ * What `useOrderSheet` renders with on the server and before its effect has
52
+ * opened the real sheet. Every action is a no-op; placing answers `not_ready`.
53
+ */
54
+ export function createInertSheet(o, ref, init = {}) {
55
+ const state = initialSheetState(o, ref, init);
56
+ const notReady = async () => ({ status: 'blocked', issues: [localIssue('not_ready')] });
57
+ const noop = () => { };
58
+ return {
59
+ getState: () => state,
60
+ subscribe: () => noop,
61
+ select: noop,
62
+ set: noop,
63
+ refresh: async () => { },
64
+ preflight: async () => null,
65
+ place: notReady,
66
+ confirm: notReady,
67
+ cancelConfirm: noop,
68
+ cancel: async () => ({ cancelled: [], skipped: [] }),
69
+ reset: noop,
70
+ destroy: noop,
71
+ ingestOrderUpdate: noop,
72
+ ingestOrderSettled: noop,
73
+ };
74
+ }
75
+ export function createSheet(client, ref, init = {}) {
76
+ const o = client.options;
77
+ const listeners = new Set();
78
+ let destroyed = false;
79
+ let userMaxPrice = init.maxPrice ?? null;
80
+ let selectedPartnerId = init.partnerId ?? refPartner(ref);
81
+ let preflightTimer = null;
82
+ let preflightSeq = 0;
83
+ let pendingIdempotencyKey = null;
84
+ /** The exact inputs the user saw on the confirm step. Only these may be placed by the confirm click. */
85
+ let confirmation = null;
86
+ /** The placement in flight, if any: a second call gets the same promise, never a second buy. */
87
+ let inFlight = null;
88
+ let constraintsStale = false;
89
+ let quoteAt = null;
90
+ let pollTimer = null;
91
+ const trackedIds = new Set();
92
+ const offs = [];
93
+ let state = initialSheetState(o, ref, init);
94
+ const emit = () => { for (const l of listeners) {
95
+ try {
96
+ l(state);
97
+ }
98
+ catch (e) {
99
+ console.error('[order-sheet] listener threw', e);
100
+ }
101
+ } };
102
+ const patchState = (p) => { state = { ...state, ...p, updatedAt: o.now() }; emit(); };
103
+ const inputKey = () => JSON.stringify([
104
+ selectedPartnerId, state.mode, state.orderType, state.stake.unit, state.stake.value, effectiveMaxPrice(),
105
+ ]);
106
+ function effectiveMaxPrice() {
107
+ const venue = currentVenue();
108
+ if (!venue)
109
+ return userMaxPrice;
110
+ return resolveMaxPrice(venue, state.orderType, userMaxPrice, state.slippage);
111
+ }
112
+ function currentVenue() {
113
+ if (!state.position || !selectedPartnerId)
114
+ return null;
115
+ const venues = buildVenueOptions({
116
+ position: state.position, constraints: client.getCachedConstraints(), account: state.account,
117
+ mode: state.mode, orderType: state.orderType, selectedPartnerId, oddsFormat: state.oddsFormat,
118
+ });
119
+ return venues.find((v) => v.partnerId === selectedPartnerId) ?? null;
120
+ }
121
+ // ── the one derivation: venues → quote → validation → phase ──
122
+ function recompute(extra = {}) {
123
+ if (destroyed)
124
+ return;
125
+ const next = { ...extra };
126
+ const position = extra.position ?? state.position;
127
+ const account = extra.account ?? state.account;
128
+ const constraints = client.getCachedConstraints();
129
+ const mode = extra.mode ?? state.mode;
130
+ const orderType = extra.orderType ?? state.orderType;
131
+ const stake = extra.stake ?? state.stake;
132
+ const oddsFormat = extra.oddsFormat ?? state.oddsFormat;
133
+ const slippage = extra.slippage ?? state.slippage;
134
+ let venues = [];
135
+ let venue = null;
136
+ if (position) {
137
+ venues = buildVenueOptions({ position, constraints, account, mode, orderType, selectedPartnerId, oddsFormat });
138
+ venue = venues.find((v) => v.partnerId === selectedPartnerId) ?? null;
139
+ }
140
+ const maxPrice = venue ? resolveMaxPrice(venue, orderType, userMaxPrice, slippage) : userMaxPrice;
141
+ const currency = mode === 'paper' ? (account?.practiceWallet.currency ?? 'ATLAS') : (venue?.account.currency ?? 'USD');
142
+ // A preflight answers for ONE set of inputs; drop it the moment they change.
143
+ const key = JSON.stringify([selectedPartnerId, mode, orderType, stake.unit, stake.value, maxPrice]);
144
+ const preflight = state.preflight && state.preflight.inputKey === key ? state.preflight : null;
145
+ const quoteAgeMs = quoteAt != null ? o.now() - quoteAt : null;
146
+ let validation = validateSheet({
147
+ account, venue, venues, mode, orderType, stake, maxPrice,
148
+ realMoneyEnabled: o.realMoney.enabled,
149
+ paperBettable: position ? position.paper_bettable : null,
150
+ quoteAgeMs, staleAfterMs: o.defaults.staleAfterMs, constraintsStale, currency,
151
+ });
152
+ if (preflight)
153
+ validation = mergeServerVerdict(validation, preflight);
154
+ const quote = venue ? buildQuote({
155
+ venue, mode, orderType, stake, maxPrice, oddsFormat, preflight, currency,
156
+ stale: quoteAgeMs != null && quoteAgeMs > o.defaults.staleAfterMs,
157
+ }) : null;
158
+ // A confirmation is for ONE set of inputs. The moment they differ (an edit,
159
+ // a re-read that moved the price), it is gone and the user confirms afresh.
160
+ if (confirmation && confirmation.key !== key)
161
+ clearConfirmation();
162
+ const loading = extra.loading ?? state.loading;
163
+ let phase = state.phase;
164
+ if (loading)
165
+ phase = 'loading';
166
+ else if (state.phase === 'placing' && !extra.phase)
167
+ phase = state.phase;
168
+ else if (state.phase === 'confirming' && !extra.phase && confirmation)
169
+ phase = state.phase;
170
+ else if (state.result && TERMINAL_PHASES.has(state.phase) && !extra.phase)
171
+ phase = state.phase;
172
+ else if (state.result && ['resting', 'pending', 'partial'].includes(state.phase) && !extra.phase)
173
+ phase = state.phase;
174
+ else if (extra.phase)
175
+ phase = extra.phase;
176
+ else if (!venue || stake.value == null)
177
+ phase = 'idle';
178
+ else
179
+ phase = validation.ok && quote ? 'ready' : 'blocked';
180
+ const hintIssue = validation.info.find((i) => i.code === 'better_price_available');
181
+ const best = hintIssue ? venues.find((v) => v.best) ?? null : null;
182
+ next.position = position;
183
+ next.account = account;
184
+ next.venues = venues;
185
+ next.venue = venue;
186
+ next.mode = mode;
187
+ next.orderType = orderType;
188
+ next.stake = stake;
189
+ next.oddsFormat = oddsFormat;
190
+ next.slippage = slippage;
191
+ next.maxPrice = maxPrice;
192
+ next.maxPriceIsAuto = userMaxPrice == null;
193
+ next.quote = quote;
194
+ next.validation = validation;
195
+ next.preflight = preflight;
196
+ next.preflightStatus = preflight ? state.preflightStatus : (state.preflightStatus === 'pending' ? 'pending' : 'idle');
197
+ next.phase = phase;
198
+ next.canPlace = (phase === 'ready' || phase === 'confirming') && validation.ok && !!quote;
199
+ next.confirmLabel = confirmLabel(phase, mode, venue, quote?.display.cost ?? null, o);
200
+ next.hint = best && hintIssue ? { code: 'better_price_available', venue: best, message: hintIssue.message } : null;
201
+ next.loading = loading;
202
+ patchState(next);
203
+ }
204
+ function mergeServerVerdict(v, pf) {
205
+ const leg = pf.leg;
206
+ const all = [];
207
+ if (!leg.accepted && leg.rejected_code) {
208
+ all.push({
209
+ code: leg.rejected_code,
210
+ severity: 'block',
211
+ message: leg.message_user ?? localIssue(leg.rejected_code, { detail: leg.rejected_reason ?? undefined }).message,
212
+ detail: leg.rejected_reason,
213
+ fix: leg.fix ?? localIssue(leg.rejected_code).fix,
214
+ scope: 'leg',
215
+ source: 'server',
216
+ });
217
+ }
218
+ for (const w of leg.warnings ?? [])
219
+ all.push(fromServerIssue(w));
220
+ // The server has answered for these inputs: its verdict replaces the local
221
+ // one for the codes it decided; local issues it did not mention stay.
222
+ const serverCodes = new Set(all.map((i) => i.code));
223
+ for (const i of v.all)
224
+ if (!serverCodes.has(i.code) || i.source === 'server')
225
+ all.push(i);
226
+ const blocking = all.filter((i) => i.severity === 'block');
227
+ return { ok: blocking.length === 0, blocking, warnings: all.filter((i) => i.severity === 'warn'), info: all.filter((i) => i.severity === 'info'), all };
228
+ }
229
+ // ── loading ──
230
+ async function load() {
231
+ patchState({ loading: true, error: null });
232
+ try {
233
+ const [loaded, constraints] = await Promise.all([client.loadPosition(ref), client.constraints().catch(() => null)]);
234
+ constraintsStale = client.constraintsStale();
235
+ if (!selectedPartnerId && loaded.partnerId)
236
+ selectedPartnerId = loaded.partnerId;
237
+ quoteAt = o.now();
238
+ const account = constraints ? client.accountFromConstraints(constraints) : await client.account().catch(() => null);
239
+ recompute({ position: loaded.position, account, loading: false });
240
+ scheduleAutoPreflight();
241
+ }
242
+ catch (e) {
243
+ const err = isOrderSheetError(e) ? e : new OrderSheetError({ code: 'network_error', message: e?.message ?? String(e), cause: e });
244
+ patchState({ loading: false, phase: 'blocked', error: err });
245
+ }
246
+ }
247
+ // ── preflight ──
248
+ function scheduleAutoPreflight() {
249
+ if (!o.preflight.auto)
250
+ return;
251
+ if (preflightTimer) {
252
+ o.timers.clear(preflightTimer);
253
+ preflightTimer = null;
254
+ }
255
+ if (!state.validation.ok || !state.quote)
256
+ return;
257
+ if (state.preflight && state.preflight.inputKey === inputKey())
258
+ return;
259
+ preflightTimer = o.timers.set(() => { preflightTimer = null; void runPreflight({}); }, o.preflight.debounceMs);
260
+ }
261
+ async function runPreflight(opts) {
262
+ const venue = state.venue;
263
+ const leg = buildLeg();
264
+ if (!venue || !leg)
265
+ return null;
266
+ const key = inputKey();
267
+ const seq = ++preflightSeq;
268
+ patchState({ preflightStatus: 'pending' });
269
+ try {
270
+ const res = await client.preflight([leg], { verifyLiquidity: opts.verifyLiquidity });
271
+ if (seq !== preflightSeq || destroyed)
272
+ return null;
273
+ const snapshot = { inputKey: key, at: o.now(), leg: res.results[0], haltedAtIndex: res.halted_at_index ?? null };
274
+ state = { ...state, preflight: snapshot, preflightStatus: 'done', error: null };
275
+ recompute();
276
+ return snapshot;
277
+ }
278
+ catch (e) {
279
+ if (seq !== preflightSeq || destroyed)
280
+ return null;
281
+ const err = isOrderSheetError(e) ? e : new OrderSheetError({ code: 'network_error', message: String(e?.message ?? e), cause: e });
282
+ patchState({ preflightStatus: 'error', error: err });
283
+ recompute();
284
+ return null;
285
+ }
286
+ }
287
+ function buildLeg() {
288
+ const venue = state.venue;
289
+ const maxPrice = state.maxPrice;
290
+ if (!venue || state.stake.value == null || maxPrice == null)
291
+ return null;
292
+ const leg = {
293
+ max_price: maxPrice,
294
+ // ALWAYS explicit. The API defaults an omitted mode to REAL.
295
+ mode: state.mode,
296
+ order_type: state.orderType,
297
+ ...(state.stake.unit === 'amount' ? { amount: state.stake.value } : { shares: state.stake.value }),
298
+ };
299
+ if (venue.liquidityHash)
300
+ leg.liquidity_hash = venue.liquidityHash;
301
+ else if (state.position) {
302
+ leg.position_hash = state.position.position_hash;
303
+ leg.partner_id = venue.partnerId;
304
+ }
305
+ else
306
+ return null;
307
+ return leg;
308
+ }
309
+ // ── placing ──
310
+ function clearConfirmation() {
311
+ confirmation = null;
312
+ pendingIdempotencyKey = null;
313
+ }
314
+ /** `place()` — or the confirm click on the same button. */
315
+ function place(opts = {}) {
316
+ if (inFlight)
317
+ return inFlight;
318
+ return track(placeOrConfirm(opts, false));
319
+ }
320
+ /** `confirm()` — only ever the second step of a real-money placement. */
321
+ function confirm() {
322
+ if (inFlight)
323
+ return inFlight;
324
+ return track(placeOrConfirm({}, true));
325
+ }
326
+ function track(p) {
327
+ inFlight = p;
328
+ const done = () => { if (inFlight === p)
329
+ inFlight = null; };
330
+ p.then(done, done);
331
+ return p;
332
+ }
333
+ async function placeOrConfirm(opts, viaConfirm) {
334
+ if (preflightTimer) {
335
+ o.timers.clear(preflightTimer);
336
+ preflightTimer = null;
337
+ }
338
+ recompute();
339
+ const venue = state.venue;
340
+ const leg = buildLeg();
341
+ const key = inputKey();
342
+ // The confirm step is valid only for the inputs it was shown with: recompute()
343
+ // has already dropped a confirmation whose inputs changed.
344
+ const confirmedNow = state.phase === 'confirming' && confirmation !== null && confirmation.key === key;
345
+ if (viaConfirm && !confirmedNow) {
346
+ if (state.phase === 'confirming') {
347
+ clearConfirmation();
348
+ recompute();
349
+ }
350
+ return { status: 'blocked', issues: [localIssue('confirm_expired')] };
351
+ }
352
+ if (!state.validation.ok || !state.quote || !venue || !leg) {
353
+ return { status: 'blocked', issues: state.validation.blocking.length ? state.validation.blocking : [localIssue('no_stake')] };
354
+ }
355
+ if (state.mode === 'real' && o.realMoney.confirm && !confirmedNow) {
356
+ confirmation = { key };
357
+ pendingIdempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
358
+ const issue = localIssue('confirm_required', { venue: venue.name, amount: state.quote.cost, currency: state.quote.currency });
359
+ recompute({ phase: 'confirming' });
360
+ return { status: 'confirm_required', issues: [issue] };
361
+ }
362
+ const idempotencyKey = opts.idempotencyKey ?? pendingIdempotencyKey ?? newIdempotencyKey();
363
+ confirmation = null;
364
+ pendingIdempotencyKey = idempotencyKey;
365
+ recompute({ phase: 'placing' });
366
+ let raw;
367
+ try {
368
+ raw = await client.buy([leg], { idempotencyKey, verifyLiquidity: opts.verifyLiquidity });
369
+ }
370
+ catch (e) {
371
+ const err = isOrderSheetError(e) ? e : new OrderSheetError({ code: 'network_error', message: String(e?.message ?? e), cause: e });
372
+ // A request the server refused (4xx) was never placed: back to the slip.
373
+ // A transport failure leaves the outcome unknown: keep the key so a
374
+ // retry replays instead of doubling, and say so.
375
+ if (err.retryable)
376
+ patchState({ error: err, phase: 'failed' });
377
+ else {
378
+ pendingIdempotencyKey = null;
379
+ state = { ...state, error: err };
380
+ recompute({ phase: 'blocked' });
381
+ }
382
+ throw err;
383
+ }
384
+ pendingIdempotencyKey = null;
385
+ const result = toPlaceResult(raw, venue, state.account, idempotencyKey, o.now());
386
+ for (const l of result.legs)
387
+ for (const id of l.routerOrderIds)
388
+ trackedIds.add(id);
389
+ const orders = result.legs.flatMap((l) => l.routerOrderIds.map((id) => ({
390
+ routerOrderId: id, partnerId: l.routedPartnerIds[0] ?? l.partnerId ?? '', status: l.status === 'completed' ? 'closed' : 'open',
391
+ fillStatus: l.status === 'completed' ? (l.fill.unfilled > 0.005 ? 'partially_filled' : 'filled') : 'unfilled',
392
+ filledAmt: l.fill.notional, unfilledAmt: l.fill.unfilled, avgPrice: l.fill.avgPrice, settled: null, updatedAt: new Date(o.now()).toISOString(),
393
+ })));
394
+ state = { ...state, result, orders, error: null };
395
+ recompute({ phase: phaseFromSummary(result.summary) });
396
+ if (result.summary === 'resting' || result.summary === 'pending')
397
+ followOrders().catch(() => { });
398
+ return { status: 'placed', result };
399
+ }
400
+ // ── following the order ──
401
+ async function followOrders() {
402
+ if (credentialSupportsStream(o.auth) && o.stream.enabled) {
403
+ await client.ensureStream();
404
+ return;
405
+ }
406
+ // JWT sessions have no stream: poll the order list for the tracked ids.
407
+ if (pollTimer || !o.stream.pollIntervalMs)
408
+ return;
409
+ const tick = async () => {
410
+ pollTimer = null;
411
+ if (destroyed || !state.position || trackedIds.size === 0 || TERMINAL_PHASES.has(state.phase))
412
+ return;
413
+ try {
414
+ const rows = await client.listOrders({ contestId: state.position.contest_id });
415
+ for (const r of rows)
416
+ if (trackedIds.has(r.router_order_id))
417
+ applyOrderRow(r);
418
+ }
419
+ catch { /* next tick */ }
420
+ if (!destroyed && !TERMINAL_PHASES.has(state.phase))
421
+ pollTimer = o.timers.set(tick, o.stream.pollIntervalMs);
422
+ };
423
+ pollTimer = o.timers.set(tick, o.stream.pollIntervalMs);
424
+ }
425
+ function applyOrderRow(r) {
426
+ ingestOrderUpdate({
427
+ type: 'order.update', router_order_id: r.router_order_id, partner_id: r.partner_id, router_contest_id: r.contest_id,
428
+ status: r.status, fill_status: r.fill_status ?? 'unfilled', filled_amt: r.filled_amt ?? 0, unfilled_amt: r.unfilled_amt ?? 0,
429
+ avg_price: null, timestamp: new Date(o.now()).toISOString(),
430
+ });
431
+ if (r.result)
432
+ ingestOrderSettled({
433
+ type: 'order.settled', router_order_id: r.router_order_id, partner_id: r.partner_id, router_contest_id: r.contest_id,
434
+ result: r.result, winnings: r.winnings ?? 0, net_winnings: r.net_winnings ?? 0, timestamp: new Date(o.now()).toISOString(),
435
+ });
436
+ }
437
+ function ingestOrderUpdate(frame) {
438
+ if (!trackedIds.has(frame.router_order_id))
439
+ return;
440
+ const orders = state.orders.map((t) => t.routerOrderId === frame.router_order_id
441
+ ? { ...t, status: frame.status, fillStatus: frame.fill_status, filledAmt: frame.filled_amt, unfilledAmt: frame.unfilled_amt, avgPrice: frame.avg_price ?? t.avgPrice, updatedAt: frame.timestamp }
442
+ : t);
443
+ state = { ...state, orders };
444
+ recompute({ phase: phaseFromOrders(orders, state.phase) });
445
+ }
446
+ function ingestOrderSettled(frame) {
447
+ if (!trackedIds.has(frame.router_order_id))
448
+ return;
449
+ const orders = state.orders.map((t) => t.routerOrderId === frame.router_order_id
450
+ ? { ...t, settled: { result: frame.result, winnings: frame.winnings, netWinnings: frame.net_winnings }, updatedAt: frame.timestamp }
451
+ : t);
452
+ state = { ...state, orders };
453
+ recompute({ phase: orders.every((t) => t.settled) ? 'settled' : state.phase });
454
+ }
455
+ offs.push(client.events.on('order.update', ingestOrderUpdate));
456
+ offs.push(client.events.on('order.settled', ingestOrderSettled));
457
+ offs.push(client.events.on('constraints', () => { constraintsStale = client.constraintsStale(); recompute(); scheduleAutoPreflight(); }));
458
+ // The stream sends changes only, with no replay: after a (re)connect, read the
459
+ // tracked orders once so a fill that landed while the socket was down is seen.
460
+ offs.push(client.events.on('stream.connected', () => { void refetchTracked(); }));
461
+ async function refetchTracked() {
462
+ if (destroyed || !state.position || trackedIds.size === 0 || TERMINAL_PHASES.has(state.phase))
463
+ return;
464
+ try {
465
+ const rows = await client.listOrders({ contestId: state.position.contest_id });
466
+ if (destroyed)
467
+ return;
468
+ for (const r of rows)
469
+ if (trackedIds.has(r.router_order_id))
470
+ applyOrderRow(r);
471
+ }
472
+ catch { /* the next frame or reconnect tries again */ }
473
+ }
474
+ // ── boot ──
475
+ void load();
476
+ return {
477
+ getState: () => state,
478
+ subscribe(l) { listeners.add(l); return () => { listeners.delete(l); }; },
479
+ select(partnerId) {
480
+ if (state.phase === 'placing')
481
+ return;
482
+ clearConfirmation();
483
+ selectedPartnerId = partnerId;
484
+ if (state.maxPriceIsAuto)
485
+ userMaxPrice = null;
486
+ state = { ...state, preflight: null, preflightStatus: 'idle' };
487
+ recompute();
488
+ scheduleAutoPreflight();
489
+ },
490
+ set(patch) {
491
+ const next = {};
492
+ if (patch.stake !== undefined) {
493
+ next.stake = patch.stake === null
494
+ ? { unit: state.stake.unit, value: null }
495
+ : 'amount' in patch.stake ? { unit: 'amount', value: patch.stake.amount } : { unit: 'shares', value: patch.stake.shares };
496
+ }
497
+ if (patch.maxPrice !== undefined)
498
+ userMaxPrice = patch.maxPrice;
499
+ if (patch.orderType !== undefined) {
500
+ next.orderType = patch.orderType;
501
+ if (state.maxPriceIsAuto)
502
+ userMaxPrice = null;
503
+ }
504
+ if (patch.mode !== undefined)
505
+ next.mode = patch.mode;
506
+ if (patch.oddsFormat !== undefined)
507
+ next.oddsFormat = patch.oddsFormat;
508
+ if (patch.slippage !== undefined)
509
+ next.slippage = patch.slippage;
510
+ // Any edit after confirm_required voids the confirmation: the user must
511
+ // see — and confirm — the order that will actually be placed.
512
+ clearConfirmation();
513
+ state = { ...state, error: null };
514
+ recompute(next);
515
+ scheduleAutoPreflight();
516
+ },
517
+ refresh: load,
518
+ preflight: (opts = {}) => runPreflight(opts),
519
+ place,
520
+ confirm,
521
+ cancelConfirm() { if (state.phase === 'placing')
522
+ return; clearConfirmation(); recompute(); },
523
+ async cancel() {
524
+ const ids = state.orders.filter((t) => t.fillStatus !== 'filled' && !t.settled).map((t) => t.routerOrderId);
525
+ if (ids.length === 0)
526
+ return { cancelled: [], skipped: [] };
527
+ const res = await client.cancelOrders(ids);
528
+ const cancelled = res.cancelled.map((c) => c.router_order_id);
529
+ const orders = state.orders.map((t) => cancelled.includes(t.routerOrderId) ? { ...t, status: 'cancelled', updatedAt: new Date(o.now()).toISOString() } : t);
530
+ state = { ...state, orders };
531
+ recompute({ phase: cancelled.length > 0 && orders.every((t) => t.status === 'cancelled' || t.fillStatus === 'filled') ? 'cancelled' : state.phase });
532
+ return { cancelled, skipped: [...res.skipped.map((s) => s.router_order_id), ...res.errors.map((e) => e.router_order_id)] };
533
+ },
534
+ reset() {
535
+ if (state.phase === 'placing')
536
+ return;
537
+ clearConfirmation();
538
+ userMaxPrice = init.maxPrice ?? null;
539
+ trackedIds.clear();
540
+ state = { ...state, stake: { unit: state.stake.unit, value: null }, result: null, orders: [], preflight: null, preflightStatus: 'idle', error: null };
541
+ recompute({ phase: 'idle' });
542
+ },
543
+ destroy() {
544
+ if (destroyed)
545
+ return;
546
+ destroyed = true;
547
+ clearConfirmation();
548
+ listeners.clear();
549
+ for (const off of offs)
550
+ off();
551
+ if (preflightTimer)
552
+ o.timers.clear(preflightTimer);
553
+ if (pollTimer)
554
+ o.timers.clear(pollTimer);
555
+ },
556
+ ingestOrderUpdate,
557
+ ingestOrderSettled,
558
+ };
559
+ }
560
+ // ─── helpers ───────────────────────────────────────────────────────────────────
561
+ function refPartner(ref) {
562
+ if ('liquidityHash' in ref)
563
+ return ref.liquidityHash.split(':')[0] || null;
564
+ if ('partnerId' in ref && ref.partnerId)
565
+ return ref.partnerId;
566
+ return null;
567
+ }
568
+ export function newIdempotencyKey() {
569
+ const c = globalThis.crypto;
570
+ if (c?.randomUUID)
571
+ return `oms_${c.randomUUID()}`;
572
+ return `oms_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
573
+ }
574
+ function confirmLabel(phase, mode, venue, cost, o) {
575
+ const L = o.labels;
576
+ if (phase === 'placing')
577
+ return L.placing;
578
+ if (phase === 'confirming')
579
+ return `${L.confirmReal}${cost ? ` · ${cost}` : ''}${venue ? ` on ${venue.name}` : ''}`;
580
+ const verb = mode === 'real' ? L.placeReal : L.placePractice;
581
+ return cost && phase !== 'idle' ? `${verb} · ${cost}` : verb;
582
+ }
583
+ export function phaseFromSummary(summary) {
584
+ switch (summary) {
585
+ case 'filled': return 'filled';
586
+ case 'partial': return 'partial';
587
+ case 'resting': return 'resting';
588
+ case 'pending': return 'pending';
589
+ case 'failed': return 'failed';
590
+ }
591
+ }
592
+ /** Fold every tracked order's fill state into one phase. */
593
+ export function phaseFromOrders(orders, current) {
594
+ if (orders.length === 0)
595
+ return current;
596
+ if (orders.every((t) => t.settled))
597
+ return 'settled';
598
+ if (orders.every((t) => t.fillStatus === 'filled'))
599
+ return 'filled';
600
+ if (orders.every((t) => t.status === 'cancelled'))
601
+ return orders.some((t) => t.filledAmt > 0) ? 'partial' : 'cancelled';
602
+ if (orders.some((t) => t.status === 'cancelled' || t.status === 'closed')) {
603
+ return orders.some((t) => t.filledAmt > 0) ? 'partial' : 'failed';
604
+ }
605
+ if (orders.some((t) => t.fillStatus === 'partially_filled'))
606
+ return 'partial';
607
+ return current === 'pending' ? 'pending' : 'resting';
608
+ }
609
+ export function toPlaceResult(raw, venue, account, idempotencyKey, now) {
610
+ const practiceBook = account?.practiceWallet.partnerId ?? null;
611
+ const legs = raw.results.map((r) => {
612
+ const routed = r.venues.map((v) => v.partner_id);
613
+ const isPaperBook = routed.length > 0 && practiceBook != null && routed.every((p) => p === practiceBook);
614
+ const substituted = !!r.requested_partner_id && routed.length > 0 && routed.some((p) => p !== r.requested_partner_id) && !isPaperBook;
615
+ const issue = r.rejected_code
616
+ ? {
617
+ code: r.rejected_code, severity: 'block', scope: 'leg', source: 'server',
618
+ message: r.message_user ?? localIssue(r.rejected_code, { venue: venue.name, detail: r.rejected_reason ?? undefined }).message,
619
+ detail: r.rejected_reason, fix: r.fix ?? localIssue(r.rejected_code).fix,
620
+ }
621
+ : (r.status === 'failed' || r.status === 'rejected')
622
+ ? { code: 'unknown', severity: 'block', scope: 'leg', source: 'server', message: r.rejected_reason ?? 'The venue declined this order.', detail: r.rejected_reason, fix: 'retry' }
623
+ : null;
624
+ return {
625
+ index: r.index, status: r.status, partnerId: r.requested_partner_id, routedPartnerIds: routed, substituted,
626
+ routerOrderIds: r.venues.map((v) => v.router_order_id).filter((id) => !!id),
627
+ fill: { notional: r.filled_notional, shares: r.filled_shares, avgPrice: r.avg_price, unfilled: r.unfilled_notional },
628
+ issue, sorDecisionId: r.sor_decision_id,
629
+ };
630
+ });
631
+ return { summary: summarize(legs), legs, opportunityId: raw.opportunity_id, idempotencyKey, placedAt: now, raw };
632
+ }
633
+ export function summarize(legs) {
634
+ if (legs.length === 0)
635
+ return 'failed';
636
+ if (legs.some((l) => l.status === 'failed' || l.status === 'rejected')) {
637
+ return legs.some((l) => l.fill.notional > 0) ? 'partial' : 'failed';
638
+ }
639
+ if (legs.some((l) => l.status === 'resting'))
640
+ return 'resting';
641
+ if (legs.some((l) => l.status === 'pending'))
642
+ return 'pending';
643
+ return legs.some((l) => l.fill.unfilled > 0.005) ? 'partial' : 'filled';
644
+ }
645
+ export function describeCost(n, currency) {
646
+ return formatMoney(n, currency);
647
+ }
648
+ //# sourceMappingURL=sheet.js.map