@agg-build/hooks 4.26.1 → 4.27.3
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.
- package/dist/deposit.d.mts +142 -138
- package/dist/deposit.d.mts.map +1 -0
- package/dist/deposit.d.ts +142 -138
- package/dist/deposit.d.ts.map +1 -0
- package/dist/deposit.js +751 -2414
- package/dist/deposit.mjs +744 -727
- package/dist/deposit.mjs.map +1 -0
- package/dist/index-CbNR3EIA.d.mts +341 -0
- package/dist/index-CbNR3EIA.d.mts.map +1 -0
- package/dist/index-CsP1jRMD.d.ts +341 -0
- package/dist/index-CsP1jRMD.d.ts.map +1 -0
- package/dist/index-D0fXU85a.d.mts +743 -0
- package/dist/index-D0fXU85a.d.mts.map +1 -0
- package/dist/index-D0fXU85a.d.ts +743 -0
- package/dist/index-D0fXU85a.d.ts.map +1 -0
- package/dist/index.d.mts +6458 -7515
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6458 -7515
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7451 -11417
- package/dist/index.mjs +7154 -5221
- package/dist/index.mjs.map +1 -0
- package/dist/use-ramp-session-7RAG9tvY.js +30 -0
- package/dist/use-ramp-session-BFWHCtuc.mjs +21 -0
- package/dist/use-ramp-session-BFWHCtuc.mjs.map +1 -0
- package/dist/use-sync-balances-BkcqInJh.mjs +1779 -0
- package/dist/use-sync-balances-BkcqInJh.mjs.map +1 -0
- package/dist/{use-sync-balances-CeD8qZWP.d.ts → use-sync-balances-CVEg__2V.d.mts} +7 -8
- package/dist/use-sync-balances-CVEg__2V.d.mts.map +1 -0
- package/dist/{use-sync-balances-CeD8qZWP.d.mts → use-sync-balances-CVEg__2V.d.ts} +7 -8
- package/dist/use-sync-balances-CVEg__2V.d.ts.map +1 -0
- package/dist/use-sync-balances-HryB8Q7F.js +1974 -0
- package/dist/withdraw--f5kYKJA.mjs +1474 -0
- package/dist/withdraw--f5kYKJA.mjs.map +1 -0
- package/dist/withdraw-BXWZYmJa.js +1591 -0
- package/dist/withdraw.d.mts +3 -351
- package/dist/withdraw.d.ts +3 -351
- package/dist/withdraw.js +10 -2229
- package/dist/withdraw.mjs +3 -20
- package/package.json +7 -6
- package/dist/chunk-JFJNW2PD.mjs +0 -5649
- package/dist/chunk-LNIAMJTZ.mjs +0 -505
- package/dist/chunk-OZQ3K47U.mjs +0 -31
- package/dist/use-deposit-addresses-B9ICS-3U.d.mts +0 -236
- package/dist/use-deposit-addresses-B9ICS-3U.d.ts +0 -236
|
@@ -0,0 +1,1474 @@
|
|
|
1
|
+
import { S as shouldInvalidateBalancesForOrderEvent, _ as invalidateBalanceQueries, c as useAggBalanceState, d as useAggUiConfig, l as useAggClient, r as useDepositAddresses, t as useSyncBalances, v as invalidatePositionQueries, w as executionKeys, x as invalidateUserMoneyState, y as invalidateUserActivityQueries } from "./use-sync-balances-BkcqInJh.mjs";
|
|
2
|
+
import { CandleBuilder } from "@agg-build/sdk";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/candle-store.ts
|
|
7
|
+
const MAX_BUILDERS = 60;
|
|
8
|
+
const builders = /* @__PURE__ */ new Map();
|
|
9
|
+
/**
|
|
10
|
+
* LRU-bounded store for per-market CandleBuilders.
|
|
11
|
+
* Fed from WS provider (onDelta/onSnapshot/onTrade).
|
|
12
|
+
* Read from useLiveCandles and SDK-only partners.
|
|
13
|
+
*/
|
|
14
|
+
function getOrCreateBuilder(marketId) {
|
|
15
|
+
const existing = builders.get(marketId);
|
|
16
|
+
if (existing) {
|
|
17
|
+
builders.delete(marketId);
|
|
18
|
+
builders.set(marketId, existing);
|
|
19
|
+
return existing;
|
|
20
|
+
}
|
|
21
|
+
evict();
|
|
22
|
+
const builder = new CandleBuilder();
|
|
23
|
+
builders.set(marketId, builder);
|
|
24
|
+
return builder;
|
|
25
|
+
}
|
|
26
|
+
function getBuilder(marketId) {
|
|
27
|
+
return builders.get(marketId);
|
|
28
|
+
}
|
|
29
|
+
function resetAllBuilders() {
|
|
30
|
+
for (const builder of builders.values()) builder.reset();
|
|
31
|
+
}
|
|
32
|
+
function evict() {
|
|
33
|
+
if (builders.size < MAX_BUILDERS) return;
|
|
34
|
+
const oldest = builders.keys().next().value;
|
|
35
|
+
if (oldest) {
|
|
36
|
+
builders.get(oldest)?.dispose();
|
|
37
|
+
builders.delete(oldest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** On-demand counts only; no candle arrays escape through diagnostics. */
|
|
41
|
+
function getCandleStoreDiagnostics() {
|
|
42
|
+
return {
|
|
43
|
+
builders: builders.size,
|
|
44
|
+
retainedCandles: [...builders.values()].reduce((total, builder) => total + [
|
|
45
|
+
"1m",
|
|
46
|
+
"5m",
|
|
47
|
+
"1h",
|
|
48
|
+
"1d"
|
|
49
|
+
].reduce((count, interval) => count + builder.getClosed(interval).length + (builder.getForming(interval) ? 1 : 0), 0), 0),
|
|
50
|
+
maxBuilders: MAX_BUILDERS
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/market-data/live-cache.ts
|
|
55
|
+
const MAX_TRADES = 50;
|
|
56
|
+
const createMarketLiveState = (marketId, overrides) => ({
|
|
57
|
+
marketId,
|
|
58
|
+
orderbook: null,
|
|
59
|
+
orderbookSnapshotVersion: 0,
|
|
60
|
+
orderbookError: null,
|
|
61
|
+
trades: [],
|
|
62
|
+
isConnected: false,
|
|
63
|
+
integrity: "ok",
|
|
64
|
+
...overrides
|
|
65
|
+
});
|
|
66
|
+
const withLiveOrderbook = (data, orderbook, source) => {
|
|
67
|
+
const current = data ?? createMarketLiveState(orderbook.marketId);
|
|
68
|
+
return {
|
|
69
|
+
...current,
|
|
70
|
+
orderbook,
|
|
71
|
+
orderbookSnapshotVersion: source === "snapshot" ? current.orderbookSnapshotVersion + 1 : current.orderbookSnapshotVersion,
|
|
72
|
+
orderbookError: null,
|
|
73
|
+
integrity: "ok"
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const withLiveOrderbookError = (data, marketId, orderbookError) => {
|
|
77
|
+
const current = data ?? createMarketLiveState(marketId);
|
|
78
|
+
if (current.orderbookError === orderbookError && current.marketId === marketId) return current;
|
|
79
|
+
return {
|
|
80
|
+
...current,
|
|
81
|
+
marketId,
|
|
82
|
+
orderbookError
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const withLiveIntegrity = (data, integrity) => {
|
|
86
|
+
if (!data) return data;
|
|
87
|
+
if (data.integrity === integrity) return data;
|
|
88
|
+
return {
|
|
89
|
+
...data,
|
|
90
|
+
integrity
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const withLiveTrade = (data, trade) => {
|
|
94
|
+
const current = data ?? createMarketLiveState(trade.outcomeId);
|
|
95
|
+
const trades = [trade, ...current.trades].slice(0, MAX_TRADES);
|
|
96
|
+
return {
|
|
97
|
+
...current,
|
|
98
|
+
trades
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/market-data/query-keys.ts
|
|
103
|
+
const marketDataKeys = {
|
|
104
|
+
all: () => ["market-data"],
|
|
105
|
+
live: (marketId) => [
|
|
106
|
+
"market-data",
|
|
107
|
+
"live",
|
|
108
|
+
marketId
|
|
109
|
+
],
|
|
110
|
+
livePrefix: () => ["market-data", "live"],
|
|
111
|
+
orderbook: (marketId, selectionKey, depth) => [
|
|
112
|
+
"market-data",
|
|
113
|
+
"orderbook",
|
|
114
|
+
marketId,
|
|
115
|
+
selectionKey ?? null,
|
|
116
|
+
depth ?? null
|
|
117
|
+
],
|
|
118
|
+
orderbookPrefix: (marketId) => marketId == null ? ["market-data", "orderbook"] : [
|
|
119
|
+
"market-data",
|
|
120
|
+
"orderbook",
|
|
121
|
+
marketId
|
|
122
|
+
],
|
|
123
|
+
orderbookQuote: (marketId, side, size) => [
|
|
124
|
+
"market-data",
|
|
125
|
+
"orderbook-quote",
|
|
126
|
+
marketId,
|
|
127
|
+
side,
|
|
128
|
+
size
|
|
129
|
+
],
|
|
130
|
+
/**
|
|
131
|
+
* Stable per `{marketId, interval, rangeKey}`. Deliberately excludes the
|
|
132
|
+
* concrete `startTs`/`endTs` of the rolling window — those advance every
|
|
133
|
+
* candle bucket, and including them would mint a fresh key on every
|
|
134
|
+
* rollover, defeating React Query's cache on tab/page revisits. The
|
|
135
|
+
* current window is passed to `queryFn` via closure instead, and
|
|
136
|
+
* `refetchInterval` advances the data under the stable key.
|
|
137
|
+
*/
|
|
138
|
+
chart: (marketId, interval, rangeKey, countBack) => [
|
|
139
|
+
"market-data",
|
|
140
|
+
"chart",
|
|
141
|
+
marketId,
|
|
142
|
+
interval,
|
|
143
|
+
rangeKey,
|
|
144
|
+
countBack ?? null
|
|
145
|
+
],
|
|
146
|
+
chartPrefix: (marketId, interval) => {
|
|
147
|
+
if (!marketId) return ["market-data", "chart"];
|
|
148
|
+
if (!interval) return [
|
|
149
|
+
"market-data",
|
|
150
|
+
"chart",
|
|
151
|
+
marketId
|
|
152
|
+
];
|
|
153
|
+
return [
|
|
154
|
+
"market-data",
|
|
155
|
+
"chart",
|
|
156
|
+
marketId,
|
|
157
|
+
interval
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/market-data/live-store.ts
|
|
163
|
+
const LIVE_RETENTION = {
|
|
164
|
+
maxInactiveSnapshots: 32,
|
|
165
|
+
maxInactiveLevels: 16384,
|
|
166
|
+
inactiveTtlMs: 5e3,
|
|
167
|
+
maxEntries: 300
|
|
168
|
+
};
|
|
169
|
+
const levelEntries = (state) => {
|
|
170
|
+
const book = state.orderbook;
|
|
171
|
+
return book ? (book.bids?.length ?? 0) + (book.asks?.length ?? 0) + Object.values(book.venueOrderbooks ?? {}).reduce((n, venue) => n + venue.bids.length + venue.asks.length, 0) : 0;
|
|
172
|
+
};
|
|
173
|
+
const hasSnapshot = (state) => !!state.orderbook || !!state.candles || state.trades.length > 0;
|
|
174
|
+
/** Stable per-outcome snapshots; inactive depth is bounded separately from active subscribers. */
|
|
175
|
+
var MarketLiveStore = class {
|
|
176
|
+
constructor(seed) {
|
|
177
|
+
this.seed = seed;
|
|
178
|
+
this.connectionState = false;
|
|
179
|
+
this.connectionGeneration = 0;
|
|
180
|
+
this.states = /* @__PURE__ */ new Map();
|
|
181
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
182
|
+
this.touched = /* @__PURE__ */ new Map();
|
|
183
|
+
this.needsPrune = false;
|
|
184
|
+
}
|
|
185
|
+
ensure(id) {
|
|
186
|
+
let state = this.states.get(id);
|
|
187
|
+
if (state && !this.listeners.has(id) && Date.now() - (this.touched.get(id) ?? 0) >= LIVE_RETENTION.inactiveTtlMs) {
|
|
188
|
+
this.drop(id);
|
|
189
|
+
state = void 0;
|
|
190
|
+
}
|
|
191
|
+
if (!state) {
|
|
192
|
+
const seed = this.seed?.(id);
|
|
193
|
+
state = {
|
|
194
|
+
...createMarketLiveState(id, {
|
|
195
|
+
isConnected: this.connectionState,
|
|
196
|
+
...seed
|
|
197
|
+
}),
|
|
198
|
+
connectionGeneration: this.connectionGeneration,
|
|
199
|
+
orderbookGeneration: seed?.orderbook ? -1 : this.connectionGeneration
|
|
200
|
+
};
|
|
201
|
+
this.states.set(id, state);
|
|
202
|
+
this.touched.set(id, Date.now());
|
|
203
|
+
this.needsPrune = true;
|
|
204
|
+
}
|
|
205
|
+
return state;
|
|
206
|
+
}
|
|
207
|
+
get(id) {
|
|
208
|
+
const state = this.ensure(id);
|
|
209
|
+
this.touched.set(id, Date.now());
|
|
210
|
+
if (this.needsPrune) this.prune(/* @__PURE__ */ new Set([id]));
|
|
211
|
+
return state;
|
|
212
|
+
}
|
|
213
|
+
/** Read retained data without creating a state, keeping it alive, or observing updates. */
|
|
214
|
+
peek(id) {
|
|
215
|
+
if (!this.listeners.has(id) && Date.now() - (this.touched.get(id) ?? 0) >= LIVE_RETENTION.inactiveTtlMs) return void 0;
|
|
216
|
+
return this.states.get(id);
|
|
217
|
+
}
|
|
218
|
+
/** Protect a render's whole selection before useSyncExternalStore subscribes. */
|
|
219
|
+
getMany(ids) {
|
|
220
|
+
const result = ids.map((id) => {
|
|
221
|
+
const state = this.ensure(id);
|
|
222
|
+
this.touched.set(id, Date.now());
|
|
223
|
+
return state;
|
|
224
|
+
});
|
|
225
|
+
if (this.needsPrune) this.prune(new Set(ids));
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
update(id, updater) {
|
|
229
|
+
const previous = this.ensure(id);
|
|
230
|
+
const next = updater(previous);
|
|
231
|
+
if (!next || next === previous) return;
|
|
232
|
+
const bookChanged = next.orderbook && (!previous.orderbook || next.orderbook.seq !== previous.orderbook.seq || next.orderbook.checksum !== previous.orderbook.checksum || next.orderbook.timestamp !== previous.orderbook.timestamp || next.orderbookSnapshotVersion !== previous.orderbookSnapshotVersion);
|
|
233
|
+
this.states.set(id, bookChanged ? {
|
|
234
|
+
...next,
|
|
235
|
+
orderbookGeneration: this.connectionGeneration
|
|
236
|
+
} : next);
|
|
237
|
+
if (bookChanged || next.candles !== previous.candles || next.trades !== previous.trades) this.touched.set(id, Date.now());
|
|
238
|
+
for (const listener of this.listeners.get(id) ?? []) listener();
|
|
239
|
+
if (!this.listeners.has(id) || this.needsPrune) this.prune(/* @__PURE__ */ new Set([id]));
|
|
240
|
+
}
|
|
241
|
+
connected(connected) {
|
|
242
|
+
if (connected && !this.connectionState) this.connectionGeneration++;
|
|
243
|
+
this.connectionState = connected;
|
|
244
|
+
for (const id of [...this.states.keys()]) this.update(id, (state) => state.isConnected === connected && state.connectionGeneration === this.connectionGeneration ? state : {
|
|
245
|
+
...state,
|
|
246
|
+
isConnected: connected,
|
|
247
|
+
connectionGeneration: this.connectionGeneration,
|
|
248
|
+
...connected ? {} : { candles: void 0 }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
subscribe(ids, listener) {
|
|
252
|
+
for (const id of ids) {
|
|
253
|
+
const set = this.listeners.get(id) ?? /* @__PURE__ */ new Set();
|
|
254
|
+
set.add(listener);
|
|
255
|
+
this.listeners.set(id, set);
|
|
256
|
+
this.touched.set(id, Date.now());
|
|
257
|
+
}
|
|
258
|
+
return () => {
|
|
259
|
+
for (const id of ids) {
|
|
260
|
+
const set = this.listeners.get(id);
|
|
261
|
+
set?.delete(listener);
|
|
262
|
+
if (!set?.size) {
|
|
263
|
+
this.listeners.delete(id);
|
|
264
|
+
this.touched.set(id, Date.now());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.prune();
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
getDiagnostics() {
|
|
271
|
+
const inactive = [...this.states].filter(([id]) => !this.listeners.has(id));
|
|
272
|
+
return {
|
|
273
|
+
entries: this.states.size,
|
|
274
|
+
activeEntries: this.listeners.size,
|
|
275
|
+
cachedBooks: [...this.states.values()].filter((state) => state.orderbook).length,
|
|
276
|
+
cachedLevelEntries: [...this.states.values()].reduce((n, state) => n + levelEntries(state), 0),
|
|
277
|
+
inactiveSnapshots: inactive.filter(([, state]) => hasSnapshot(state)).length,
|
|
278
|
+
inactiveLevelEntries: inactive.reduce((n, [, state]) => n + levelEntries(state), 0),
|
|
279
|
+
limits: LIVE_RETENTION
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
drop(id) {
|
|
283
|
+
this.states.delete(id);
|
|
284
|
+
this.touched.delete(id);
|
|
285
|
+
}
|
|
286
|
+
prune(protectedIds = /* @__PURE__ */ new Set()) {
|
|
287
|
+
this.needsPrune = false;
|
|
288
|
+
if (this.timer) clearTimeout(this.timer);
|
|
289
|
+
this.timer = void 0;
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const inactive = [...this.states].filter(([id]) => !this.listeners.has(id) && !protectedIds.has(id)).sort(([a], [b]) => (this.touched.get(a) ?? 0) - (this.touched.get(b) ?? 0));
|
|
292
|
+
let count = inactive.filter(([, state]) => hasSnapshot(state)).length;
|
|
293
|
+
let levels = inactive.reduce((n, [, state]) => n + levelEntries(state), 0);
|
|
294
|
+
for (const [id, state] of inactive) if (now - (this.touched.get(id) ?? 0) >= LIVE_RETENTION.inactiveTtlMs || this.states.size > LIVE_RETENTION.maxEntries || count > LIVE_RETENTION.maxInactiveSnapshots || levels > LIVE_RETENTION.maxInactiveLevels) {
|
|
295
|
+
if (hasSnapshot(state)) count--;
|
|
296
|
+
levels -= levelEntries(state);
|
|
297
|
+
this.drop(id);
|
|
298
|
+
}
|
|
299
|
+
let nextExpiry = Infinity;
|
|
300
|
+
for (const id of this.states.keys()) if (!this.listeners.has(id)) nextExpiry = Math.min(nextExpiry, (this.touched.get(id) ?? now) + LIVE_RETENTION.inactiveTtlMs);
|
|
301
|
+
if (Number.isFinite(nextExpiry)) this.timer = setTimeout(() => {
|
|
302
|
+
this.timer = void 0;
|
|
303
|
+
this.prune();
|
|
304
|
+
}, Math.max(1, nextExpiry - now));
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const stores = /* @__PURE__ */ new WeakMap();
|
|
308
|
+
function getMarketLiveStore(client) {
|
|
309
|
+
let store = stores.get(client);
|
|
310
|
+
if (!store) {
|
|
311
|
+
store = new MarketLiveStore((id) => client.getQueryData?.(marketDataKeys.live(id)));
|
|
312
|
+
stores.set(client, store);
|
|
313
|
+
}
|
|
314
|
+
return store;
|
|
315
|
+
}
|
|
316
|
+
function useLiveStates(ids) {
|
|
317
|
+
const store = getMarketLiveStore(useQueryClient());
|
|
318
|
+
const key = JSON.stringify(ids);
|
|
319
|
+
const selector = useMemo(() => {
|
|
320
|
+
const selectedIds = JSON.parse(key);
|
|
321
|
+
let previous = [];
|
|
322
|
+
return {
|
|
323
|
+
subscribe: (listener) => store.subscribe(selectedIds, listener),
|
|
324
|
+
get: () => {
|
|
325
|
+
const next = store.getMany(selectedIds).map((data) => ({ data }));
|
|
326
|
+
if (next.length === previous.length && next.every((entry, index) => entry.data === previous[index]?.data)) return previous;
|
|
327
|
+
previous = next;
|
|
328
|
+
return next;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}, [key, store]);
|
|
332
|
+
return useSyncExternalStore(selector.subscribe, selector.get, selector.get);
|
|
333
|
+
}
|
|
334
|
+
/** Keep acknowledged closed tails until REST history catches up; empty batches never erase them. */
|
|
335
|
+
function mergeCandleTails(previous, next) {
|
|
336
|
+
return Object.fromEntries([
|
|
337
|
+
"1m",
|
|
338
|
+
"5m",
|
|
339
|
+
"1h",
|
|
340
|
+
"1d"
|
|
341
|
+
].map((interval) => {
|
|
342
|
+
const oldClosed = previous?.[interval].closed ?? [];
|
|
343
|
+
const incoming = next[interval].closed;
|
|
344
|
+
const closed = incoming.length ? [...new Map([...oldClosed, ...incoming].map((candle) => [candle.openTime, candle])).values()].sort((a, b) => a.openTime - b.openTime).slice(-500) : oldClosed;
|
|
345
|
+
return [interval, {
|
|
346
|
+
forming: next[interval].forming,
|
|
347
|
+
closed
|
|
348
|
+
}];
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/market-data/market-resolution-cache.ts
|
|
353
|
+
const isRecord = (value) => {
|
|
354
|
+
return typeof value === "object" && value !== null;
|
|
355
|
+
};
|
|
356
|
+
const NESTED_COLLECTION_FIELDS = [
|
|
357
|
+
"pages",
|
|
358
|
+
"data",
|
|
359
|
+
"markets",
|
|
360
|
+
"events",
|
|
361
|
+
"venueMarkets"
|
|
362
|
+
];
|
|
363
|
+
const marketContainsOutcomeId = (market, outcomeId) => {
|
|
364
|
+
if (!isRecord(market)) return false;
|
|
365
|
+
const outcomes = market.venueMarketOutcomes;
|
|
366
|
+
if (!Array.isArray(outcomes)) return false;
|
|
367
|
+
return outcomes.some((outcome) => isRecord(outcome) && outcome.id === outcomeId);
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Deep-search arbitrary query data for a venue market that holds `outcomeId`.
|
|
371
|
+
* Used both to decide which cached queries to optimistically patch and as the
|
|
372
|
+
* `invalidateQueries` predicate.
|
|
373
|
+
*/
|
|
374
|
+
const cachedDataContainsOutcomeId = (data, outcomeId) => {
|
|
375
|
+
if (Array.isArray(data)) return data.some((entry) => cachedDataContainsOutcomeId(entry, outcomeId));
|
|
376
|
+
if (!isRecord(data)) return false;
|
|
377
|
+
if (marketContainsOutcomeId(data, outcomeId)) return true;
|
|
378
|
+
return NESTED_COLLECTION_FIELDS.some((field) => {
|
|
379
|
+
const value = data[field];
|
|
380
|
+
return Array.isArray(value) && value.some((entry) => cachedDataContainsOutcomeId(entry, outcomeId));
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
const normalizeLabel = (label) => {
|
|
384
|
+
return typeof label === "string" ? label.trim().toLowerCase() : "";
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Match a local outcome to the resolution payload by external identifier first
|
|
388
|
+
* (authoritative on-chain token id) then by label — the WS event carries
|
|
389
|
+
* `externalIdentifier`/`label`, never the internal `id` per outcome.
|
|
390
|
+
*/
|
|
391
|
+
const findResolvedOutcome = (resolvedOutcomes, outcome) => {
|
|
392
|
+
const externalIdentifier = outcome.externalIdentifier;
|
|
393
|
+
if (typeof externalIdentifier === "string" && externalIdentifier.length > 0) {
|
|
394
|
+
const matchedByExternalId = resolvedOutcomes.find((resolved) => resolved.externalIdentifier === externalIdentifier);
|
|
395
|
+
if (matchedByExternalId) return matchedByExternalId;
|
|
396
|
+
}
|
|
397
|
+
const label = normalizeLabel(outcome.label);
|
|
398
|
+
if (label) return resolvedOutcomes.find((resolved) => normalizeLabel(resolved.label) === label) ?? null;
|
|
399
|
+
return null;
|
|
400
|
+
};
|
|
401
|
+
const applyWinnersToOutcomes = (outcomes, resolution) => {
|
|
402
|
+
let changed = false;
|
|
403
|
+
const next = outcomes.map((outcome) => {
|
|
404
|
+
if (!isRecord(outcome)) return outcome;
|
|
405
|
+
const resolved = findResolvedOutcome(resolution.outcomes, outcome);
|
|
406
|
+
if (!resolved) return outcome;
|
|
407
|
+
const nextWinner = resolved.winner === true;
|
|
408
|
+
if (outcome.winner === nextWinner) return outcome;
|
|
409
|
+
changed = true;
|
|
410
|
+
return {
|
|
411
|
+
...outcome,
|
|
412
|
+
winner: nextWinner
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
return changed ? next : outcomes;
|
|
416
|
+
};
|
|
417
|
+
const applyResolutionToMarket = (market, resolution) => {
|
|
418
|
+
const outcomes = market.venueMarketOutcomes;
|
|
419
|
+
if (!Array.isArray(outcomes)) return market;
|
|
420
|
+
if (!outcomes.some((outcome) => isRecord(outcome) && outcome.id === resolution.outcomeId)) return market;
|
|
421
|
+
const nextOutcomes = applyWinnersToOutcomes(outcomes, resolution);
|
|
422
|
+
if (!(market.status !== resolution.status) && nextOutcomes === outcomes) return market;
|
|
423
|
+
return {
|
|
424
|
+
...market,
|
|
425
|
+
status: resolution.status,
|
|
426
|
+
venueMarketOutcomes: nextOutcomes
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Return a structurally-shared copy of `data` with the resolved market marked
|
|
431
|
+
* `status: resolved|closed` and its outcomes' `winner` flags stamped from the
|
|
432
|
+
* WS payload. Untouched branches keep their original reference (no spurious
|
|
433
|
+
* re-renders). Returns the original `data` when nothing matched.
|
|
434
|
+
*/
|
|
435
|
+
const applyResolutionToCachedData = (data, resolution) => {
|
|
436
|
+
if (Array.isArray(data)) {
|
|
437
|
+
let changed = false;
|
|
438
|
+
const next = data.map((entry) => {
|
|
439
|
+
const updated = applyResolutionToCachedData(entry, resolution);
|
|
440
|
+
if (updated !== entry) changed = true;
|
|
441
|
+
return updated;
|
|
442
|
+
});
|
|
443
|
+
return changed ? next : data;
|
|
444
|
+
}
|
|
445
|
+
if (!isRecord(data)) return data;
|
|
446
|
+
if (Array.isArray(data.venueMarketOutcomes)) return applyResolutionToMarket(data, resolution);
|
|
447
|
+
let changed = false;
|
|
448
|
+
const next = { ...data };
|
|
449
|
+
for (const field of NESTED_COLLECTION_FIELDS) {
|
|
450
|
+
const value = data[field];
|
|
451
|
+
if (!Array.isArray(value)) continue;
|
|
452
|
+
const updated = applyResolutionToCachedData(value, resolution);
|
|
453
|
+
if (updated !== value) {
|
|
454
|
+
next[field] = updated;
|
|
455
|
+
changed = true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return changed ? next : data;
|
|
459
|
+
};
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/market-data/history-retention.ts
|
|
462
|
+
const DEFAULT_HISTORY_RETENTION = {
|
|
463
|
+
maxInactiveQueries: 16,
|
|
464
|
+
maxInactivePoints: 2e4
|
|
465
|
+
};
|
|
466
|
+
const owners = /* @__PURE__ */ new WeakMap();
|
|
467
|
+
const isHistory = (key) => key[0] === "market-data" && key[1] === "chart";
|
|
468
|
+
const points = (data) => {
|
|
469
|
+
const chart = data;
|
|
470
|
+
return chart?.venues ? Object.values(chart.venues).reduce((total, venue) => total + venue.candles.length + (venue.liveCandle ? 1 : 0), 0) : 0;
|
|
471
|
+
};
|
|
472
|
+
/** Read-only diagnostics for chart policy only; ordinary REST and user caches are excluded. */
|
|
473
|
+
function getChartHistoryRetentionStats(client) {
|
|
474
|
+
const queries = client.getQueryCache().findAll({ queryKey: marketDataKeys.chartPrefix() });
|
|
475
|
+
const active = queries.filter((query) => query.getObserversCount() > 0);
|
|
476
|
+
const inactive = queries.filter((query) => query.getObserversCount() === 0);
|
|
477
|
+
const owner = owners.get(client);
|
|
478
|
+
return {
|
|
479
|
+
activeQueries: active.length,
|
|
480
|
+
inactiveQueries: inactive.length,
|
|
481
|
+
activePoints: active.reduce((sum, query) => sum + points(query.state.data), 0),
|
|
482
|
+
inactivePoints: inactive.reduce((sum, query) => sum + points(query.state.data), 0),
|
|
483
|
+
inFlightQueries: queries.filter((query) => query.state.fetchStatus === "fetching").length,
|
|
484
|
+
trackedQueries: owner?.touched.size ?? 0,
|
|
485
|
+
evictedQueries: owner?.evicted ?? 0,
|
|
486
|
+
limits: owner?.limits ?? DEFAULT_HISTORY_RETENTION
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** One reusable policy owner per QueryClient. Called by the hooks provider, never the demo app. */
|
|
490
|
+
function installChartHistoryRetention(client, limits = DEFAULT_HISTORY_RETENTION) {
|
|
491
|
+
let owner = owners.get(client);
|
|
492
|
+
if (!owner) {
|
|
493
|
+
owner = {
|
|
494
|
+
refs: 0,
|
|
495
|
+
clock: Date.now(),
|
|
496
|
+
touched: /* @__PURE__ */ new Map(),
|
|
497
|
+
evicted: 0,
|
|
498
|
+
queued: false,
|
|
499
|
+
pruning: false,
|
|
500
|
+
limits,
|
|
501
|
+
unsubscribe: () => {},
|
|
502
|
+
prune: () => {}
|
|
503
|
+
};
|
|
504
|
+
owners.set(client, owner);
|
|
505
|
+
const current = owner;
|
|
506
|
+
const prune = () => {
|
|
507
|
+
if (current.pruning) return;
|
|
508
|
+
current.pruning = true;
|
|
509
|
+
try {
|
|
510
|
+
const inactive = client.getQueryCache().findAll({ queryKey: marketDataKeys.chartPrefix() }).filter((query) => query.getObserversCount() === 0).sort((a, b) => (current.touched.get(a.queryHash) ?? a.state.dataUpdatedAt) - (current.touched.get(b.queryHash) ?? b.state.dataUpdatedAt));
|
|
511
|
+
let count = inactive.length;
|
|
512
|
+
let totalPoints = inactive.reduce((sum, query) => sum + points(query.state.data), 0);
|
|
513
|
+
for (const query of inactive) {
|
|
514
|
+
if (count <= current.limits.maxInactiveQueries && totalPoints <= current.limits.maxInactivePoints) break;
|
|
515
|
+
if (query.getObserversCount() > 0) continue;
|
|
516
|
+
count--;
|
|
517
|
+
totalPoints -= points(query.state.data);
|
|
518
|
+
current.evicted++;
|
|
519
|
+
current.touched.delete(query.queryHash);
|
|
520
|
+
client.removeQueries({
|
|
521
|
+
queryKey: query.queryKey,
|
|
522
|
+
exact: true
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
} finally {
|
|
526
|
+
current.pruning = false;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
current.prune = prune;
|
|
530
|
+
const schedule = () => {
|
|
531
|
+
if (current.queued) return;
|
|
532
|
+
current.queued = true;
|
|
533
|
+
queueMicrotask(() => {
|
|
534
|
+
current.queued = false;
|
|
535
|
+
prune();
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
current.unsubscribe = client.getQueryCache().subscribe((event) => {
|
|
539
|
+
if (!isHistory(event.query.queryKey)) return;
|
|
540
|
+
if (event.type === "removed") current.touched.delete(event.query.queryHash);
|
|
541
|
+
else if (event.type === "observerAdded" || event.type === "updated" || event.type === "added") {
|
|
542
|
+
current.clock = Math.max(current.clock + 1, Date.now());
|
|
543
|
+
current.touched.set(event.query.queryHash, current.clock);
|
|
544
|
+
}
|
|
545
|
+
if (!current.pruning) schedule();
|
|
546
|
+
});
|
|
547
|
+
schedule();
|
|
548
|
+
}
|
|
549
|
+
owner.refs++;
|
|
550
|
+
const current = owner;
|
|
551
|
+
let released = false;
|
|
552
|
+
return () => {
|
|
553
|
+
if (released) return;
|
|
554
|
+
released = true;
|
|
555
|
+
if (--current.refs > 0) return;
|
|
556
|
+
queueMicrotask(() => {
|
|
557
|
+
if (current.refs > 0) return;
|
|
558
|
+
current.prune();
|
|
559
|
+
current.unsubscribe();
|
|
560
|
+
owners.delete(client);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/core/providers/ws-provider.tsx
|
|
566
|
+
const AggWebSocketContext = createContext(null);
|
|
567
|
+
const AggWebSocketConnectionContext = createContext(false);
|
|
568
|
+
const AggWsUserEventContext = createContext(null);
|
|
569
|
+
const snapshotUnavailableOutcomeIdPattern = /snapshot unavailable for\s+([^\s]+)/i;
|
|
570
|
+
const resolveSnapshotUnavailableOutcomeId = (errorMessage) => {
|
|
571
|
+
const matchedOutcomeId = snapshotUnavailableOutcomeIdPattern.exec(errorMessage)?.[1];
|
|
572
|
+
if (!matchedOutcomeId) return null;
|
|
573
|
+
return matchedOutcomeId.replace(/[.,;:!?]+$/, "");
|
|
574
|
+
};
|
|
575
|
+
function useAggWebSocket() {
|
|
576
|
+
return useContext(AggWebSocketContext);
|
|
577
|
+
}
|
|
578
|
+
function useAggWebSocketConnectionState() {
|
|
579
|
+
return useContext(AggWebSocketConnectionContext);
|
|
580
|
+
}
|
|
581
|
+
function useOnOrderSubmitted(callback) {
|
|
582
|
+
const listeners = useContext(AggWsUserEventContext);
|
|
583
|
+
const callbackRef = useRef(callback);
|
|
584
|
+
callbackRef.current = callback;
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
if (!listeners || !callbackRef.current) return;
|
|
587
|
+
const handler = (msg) => {
|
|
588
|
+
callbackRef.current?.(msg);
|
|
589
|
+
};
|
|
590
|
+
return listeners.addOrderSubmittedListener(handler);
|
|
591
|
+
}, [listeners, callback !== null]);
|
|
592
|
+
}
|
|
593
|
+
function useOnBalanceUpdate(callback) {
|
|
594
|
+
const listeners = useContext(AggWsUserEventContext);
|
|
595
|
+
const callbackRef = useRef(callback);
|
|
596
|
+
callbackRef.current = callback;
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!listeners || !callbackRef.current) return;
|
|
599
|
+
const handler = (msg) => {
|
|
600
|
+
callbackRef.current?.(msg);
|
|
601
|
+
};
|
|
602
|
+
return listeners.addBalanceUpdateListener(handler);
|
|
603
|
+
}, [listeners, callback !== null]);
|
|
604
|
+
}
|
|
605
|
+
function useOnOrderEvent(callback) {
|
|
606
|
+
const listeners = useContext(AggWsUserEventContext);
|
|
607
|
+
const callbackRef = useRef(callback);
|
|
608
|
+
callbackRef.current = callback;
|
|
609
|
+
useEffect(() => {
|
|
610
|
+
if (!listeners || !callbackRef.current) return;
|
|
611
|
+
const handler = (msg) => {
|
|
612
|
+
callbackRef.current?.(msg);
|
|
613
|
+
};
|
|
614
|
+
return listeners.addOrderEventListener(handler);
|
|
615
|
+
}, [listeners, callback !== null]);
|
|
616
|
+
}
|
|
617
|
+
function useOnRedeemEvent(callback) {
|
|
618
|
+
const listeners = useContext(AggWsUserEventContext);
|
|
619
|
+
const callbackRef = useRef(callback);
|
|
620
|
+
callbackRef.current = callback;
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
if (!listeners || !callbackRef.current) return;
|
|
623
|
+
const handler = (msg) => {
|
|
624
|
+
callbackRef.current?.(msg);
|
|
625
|
+
};
|
|
626
|
+
return listeners.addRedeemEventListener(handler);
|
|
627
|
+
}, [listeners, callback !== null]);
|
|
628
|
+
}
|
|
629
|
+
function useOnWithdrawalLifecycle(callback) {
|
|
630
|
+
const listeners = useContext(AggWsUserEventContext);
|
|
631
|
+
const callbackRef = useRef(callback);
|
|
632
|
+
callbackRef.current = callback;
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
if (!listeners || !callbackRef.current) return;
|
|
635
|
+
const handler = (msg) => {
|
|
636
|
+
callbackRef.current?.(msg);
|
|
637
|
+
};
|
|
638
|
+
return listeners.addWithdrawalLifecycleListener(handler);
|
|
639
|
+
}, [listeners, callback !== null]);
|
|
640
|
+
}
|
|
641
|
+
function AggWebSocketProvider({ children }) {
|
|
642
|
+
const client = useAggClient();
|
|
643
|
+
const { enableWebsocketsLogs } = useAggUiConfig();
|
|
644
|
+
const queryClient = useQueryClient();
|
|
645
|
+
const liveStore = getMarketLiveStore(queryClient);
|
|
646
|
+
useEffect(() => installChartHistoryRetention(queryClient), [queryClient]);
|
|
647
|
+
const [ws, setWs] = useState(null);
|
|
648
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
649
|
+
const [isClientAuthenticated, setIsClientAuthenticated] = useState(client.isAuthenticated);
|
|
650
|
+
const wsRef = useRef(null);
|
|
651
|
+
const resyncDeadlineByMarketRef = useRef(/* @__PURE__ */ new Map());
|
|
652
|
+
const lastBalanceInvalidationAtRef = useRef(0);
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
setIsClientAuthenticated(client.isAuthenticated);
|
|
655
|
+
return client.onAuthStateChange((authenticated) => {
|
|
656
|
+
setIsClientAuthenticated(authenticated);
|
|
657
|
+
});
|
|
658
|
+
}, [client]);
|
|
659
|
+
const orderSubmittedListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
660
|
+
const balanceUpdateListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
661
|
+
const orderEventListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
662
|
+
const redeemEventListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
663
|
+
const withdrawalLifecycleListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
664
|
+
const userEventListeners = useMemo(() => ({
|
|
665
|
+
addOrderSubmittedListener: (cb) => {
|
|
666
|
+
orderSubmittedListenersRef.current.add(cb);
|
|
667
|
+
return () => {
|
|
668
|
+
orderSubmittedListenersRef.current.delete(cb);
|
|
669
|
+
};
|
|
670
|
+
},
|
|
671
|
+
addBalanceUpdateListener: (cb) => {
|
|
672
|
+
balanceUpdateListenersRef.current.add(cb);
|
|
673
|
+
return () => {
|
|
674
|
+
balanceUpdateListenersRef.current.delete(cb);
|
|
675
|
+
};
|
|
676
|
+
},
|
|
677
|
+
addOrderEventListener: (cb) => {
|
|
678
|
+
orderEventListenersRef.current.add(cb);
|
|
679
|
+
return () => {
|
|
680
|
+
orderEventListenersRef.current.delete(cb);
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
addRedeemEventListener: (cb) => {
|
|
684
|
+
redeemEventListenersRef.current.add(cb);
|
|
685
|
+
return () => {
|
|
686
|
+
redeemEventListenersRef.current.delete(cb);
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
addWithdrawalLifecycleListener: (cb) => {
|
|
690
|
+
withdrawalLifecycleListenersRef.current.add(cb);
|
|
691
|
+
return () => {
|
|
692
|
+
withdrawalLifecycleListenersRef.current.delete(cb);
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}), []);
|
|
696
|
+
const syncLiveQuery = useCallback((marketId, updater) => {
|
|
697
|
+
liveStore.update(marketId, (previous) => updater(previous));
|
|
698
|
+
}, [liveStore]);
|
|
699
|
+
const invalidateBalanceCaches = useCallback(() => {
|
|
700
|
+
const now = Date.now();
|
|
701
|
+
if (now - lastBalanceInvalidationAtRef.current < 750) return;
|
|
702
|
+
lastBalanceInvalidationAtRef.current = now;
|
|
703
|
+
invalidateBalanceQueries(queryClient);
|
|
704
|
+
}, [queryClient]);
|
|
705
|
+
const invalidatePositionCaches = useCallback(() => {
|
|
706
|
+
invalidatePositionQueries(queryClient);
|
|
707
|
+
}, [queryClient]);
|
|
708
|
+
const invalidateActivityCaches = useCallback(() => {
|
|
709
|
+
invalidateUserActivityQueries(queryClient);
|
|
710
|
+
}, [queryClient]);
|
|
711
|
+
const invalidateResolutionCaches = useCallback((resolution) => {
|
|
712
|
+
const { outcomeId } = resolution;
|
|
713
|
+
syncLiveQuery(outcomeId, (previous) => {
|
|
714
|
+
if (!previous) return previous;
|
|
715
|
+
return {
|
|
716
|
+
...previous,
|
|
717
|
+
orderbook: null,
|
|
718
|
+
orderbookSnapshotVersion: previous.orderbookSnapshotVersion + 1,
|
|
719
|
+
orderbookError: null,
|
|
720
|
+
integrity: "ok"
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
const affectedQueries = queryClient.getQueryCache().findAll({ predicate: (query) => cachedDataContainsOutcomeId(query.state.data, outcomeId) });
|
|
724
|
+
for (const query of affectedQueries) queryClient.setQueryData(query.queryKey, (previous) => applyResolutionToCachedData(previous, resolution));
|
|
725
|
+
queryClient.invalidateQueries({
|
|
726
|
+
queryKey: ["midpoints"],
|
|
727
|
+
refetchType: "all"
|
|
728
|
+
});
|
|
729
|
+
queryClient.invalidateQueries({
|
|
730
|
+
queryKey: ["venue-market-midpoints"],
|
|
731
|
+
refetchType: "all"
|
|
732
|
+
});
|
|
733
|
+
queryClient.invalidateQueries({
|
|
734
|
+
queryKey: ["venue-event"],
|
|
735
|
+
refetchType: "all"
|
|
736
|
+
});
|
|
737
|
+
queryClient.invalidateQueries({
|
|
738
|
+
queryKey: ["venue-markets"],
|
|
739
|
+
refetchType: "all"
|
|
740
|
+
});
|
|
741
|
+
queryClient.invalidateQueries({
|
|
742
|
+
queryKey: ["events"],
|
|
743
|
+
refetchType: "all"
|
|
744
|
+
});
|
|
745
|
+
queryClient.invalidateQueries({
|
|
746
|
+
queryKey: ["event-list"],
|
|
747
|
+
refetchType: "all"
|
|
748
|
+
});
|
|
749
|
+
queryClient.invalidateQueries({
|
|
750
|
+
queryKey: ["search"],
|
|
751
|
+
refetchType: "all"
|
|
752
|
+
});
|
|
753
|
+
queryClient.invalidateQueries({
|
|
754
|
+
predicate: (query) => cachedDataContainsOutcomeId(query.state.data, outcomeId),
|
|
755
|
+
refetchType: "all"
|
|
756
|
+
});
|
|
757
|
+
}, [queryClient, syncLiveQuery]);
|
|
758
|
+
const publishLocalCandles = useCallback((id) => {
|
|
759
|
+
const builder = getOrCreateBuilder(id);
|
|
760
|
+
const candles = Object.fromEntries([
|
|
761
|
+
"1m",
|
|
762
|
+
"5m",
|
|
763
|
+
"1h",
|
|
764
|
+
"1d"
|
|
765
|
+
].map((interval) => [interval, {
|
|
766
|
+
forming: builder.getForming(interval) ? { ...builder.getForming(interval) } : null,
|
|
767
|
+
closed: builder.getClosed(interval).slice(-2)
|
|
768
|
+
}]));
|
|
769
|
+
liveStore.update(id, (previous) => ({
|
|
770
|
+
...previous,
|
|
771
|
+
candles
|
|
772
|
+
}));
|
|
773
|
+
}, [liveStore]);
|
|
774
|
+
const callbacks = useMemo(() => ({
|
|
775
|
+
onMarketView: (view) => {
|
|
776
|
+
if (view.book && view.integrity === "ok") resyncDeadlineByMarketRef.current.delete(view.outcomeId);
|
|
777
|
+
liveStore.update(view.outcomeId, (previous) => ({
|
|
778
|
+
...view.book ? withLiveOrderbook(previous, view.book, view.snapshot ? "snapshot" : "delta") : previous,
|
|
779
|
+
integrity: view.integrity,
|
|
780
|
+
candles: mergeCandleTails(previous.candles, view.candles),
|
|
781
|
+
receivedAtMs: view.receivedAtMs,
|
|
782
|
+
trades: [...view.trades, ...previous.trades].slice(0, 50)
|
|
783
|
+
}));
|
|
784
|
+
getOrCreateBuilder(view.outcomeId).replaceTail(view.candles);
|
|
785
|
+
},
|
|
786
|
+
onSnapshot: (marketId, book) => {
|
|
787
|
+
if (wsRef.current?.processesCandles) return;
|
|
788
|
+
resyncDeadlineByMarketRef.current.delete(marketId);
|
|
789
|
+
syncLiveQuery(marketId, (previous) => withLiveOrderbook(previous, book, "snapshot"));
|
|
790
|
+
if (!wsRef.current?.processesCandles && book.midpoint != null) {
|
|
791
|
+
getOrCreateBuilder(marketId).addMidpoint(book.midpoint, book.timestamp);
|
|
792
|
+
publishLocalCandles(marketId);
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
onDelta: (marketId, book) => {
|
|
796
|
+
if (wsRef.current?.processesCandles) return;
|
|
797
|
+
resyncDeadlineByMarketRef.current.delete(marketId);
|
|
798
|
+
syncLiveQuery(marketId, (previous) => withLiveOrderbook(previous, book, "delta"));
|
|
799
|
+
if (!wsRef.current?.processesCandles && book.midpoint != null) {
|
|
800
|
+
getOrCreateBuilder(marketId).addMidpoint(book.midpoint, book.timestamp);
|
|
801
|
+
publishLocalCandles(marketId);
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
onTrade: (trade) => {
|
|
805
|
+
if (wsRef.current?.processesCandles) return;
|
|
806
|
+
syncLiveQuery(trade.outcomeId, (previous) => withLiveTrade(previous, trade));
|
|
807
|
+
if (!wsRef.current?.processesCandles) {
|
|
808
|
+
getOrCreateBuilder(trade.outcomeId).addTrade(trade.price, trade.size, trade.timestamp);
|
|
809
|
+
publishLocalCandles(trade.outcomeId);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
onHeartbeat: () => {
|
|
813
|
+
const now = Date.now();
|
|
814
|
+
resyncDeadlineByMarketRef.current.forEach((deadline, marketId) => {
|
|
815
|
+
if (deadline > now) return;
|
|
816
|
+
wsRef.current?.resnapshot(marketId);
|
|
817
|
+
resyncDeadlineByMarketRef.current.set(marketId, now + 35e3);
|
|
818
|
+
});
|
|
819
|
+
},
|
|
820
|
+
onDiagnostics: (event) => {
|
|
821
|
+
if (event.type !== "seq_gap" && event.type !== "checksum_mismatch") return;
|
|
822
|
+
const id = event.outcomeId ?? event.marketId;
|
|
823
|
+
resyncDeadlineByMarketRef.current.set(id, Date.now() + 35e3);
|
|
824
|
+
syncLiveQuery(id, (previous) => withLiveIntegrity(previous, "resyncing"));
|
|
825
|
+
},
|
|
826
|
+
onOrderSubmitted: (msg) => {
|
|
827
|
+
for (const listener of orderSubmittedListenersRef.current) listener(msg);
|
|
828
|
+
},
|
|
829
|
+
onBalanceUpdate: (msg) => {
|
|
830
|
+
if (isClientAuthenticated) {
|
|
831
|
+
invalidateBalanceCaches();
|
|
832
|
+
invalidateActivityCaches();
|
|
833
|
+
}
|
|
834
|
+
for (const listener of balanceUpdateListenersRef.current) listener(msg);
|
|
835
|
+
},
|
|
836
|
+
onOrderEvent: (msg) => {
|
|
837
|
+
if (isClientAuthenticated) {
|
|
838
|
+
const currentUserId = client.user?.id ?? null;
|
|
839
|
+
if (shouldInvalidateBalancesForOrderEvent(msg, currentUserId)) {
|
|
840
|
+
invalidateBalanceCaches();
|
|
841
|
+
invalidatePositionCaches();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
for (const listener of orderEventListenersRef.current) listener(msg);
|
|
845
|
+
},
|
|
846
|
+
onRedeemEvent: (msg) => {
|
|
847
|
+
for (const listener of redeemEventListenersRef.current) listener(msg);
|
|
848
|
+
},
|
|
849
|
+
onWithdrawalLifecycle: (msg) => {
|
|
850
|
+
if (isClientAuthenticated) invalidateBalanceCaches();
|
|
851
|
+
for (const listener of withdrawalLifecycleListenersRef.current) listener(msg);
|
|
852
|
+
},
|
|
853
|
+
onMarketResolved: (msg) => {
|
|
854
|
+
invalidateResolutionCaches(msg);
|
|
855
|
+
if (isClientAuthenticated) {
|
|
856
|
+
invalidateBalanceCaches();
|
|
857
|
+
invalidatePositionCaches();
|
|
858
|
+
invalidateActivityCaches();
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
onError: (msg) => {
|
|
862
|
+
const outcomeId = resolveSnapshotUnavailableOutcomeId(msg.message);
|
|
863
|
+
if (!outcomeId) return;
|
|
864
|
+
syncLiveQuery(outcomeId, (previous) => withLiveOrderbookError(previous, outcomeId, msg.message));
|
|
865
|
+
},
|
|
866
|
+
onConnectionStateChange: (connected) => {
|
|
867
|
+
setIsConnected(connected);
|
|
868
|
+
if (connected) queryClient.invalidateQueries({
|
|
869
|
+
queryKey: marketDataKeys.chartPrefix(),
|
|
870
|
+
refetchType: "active"
|
|
871
|
+
});
|
|
872
|
+
else resetAllBuilders();
|
|
873
|
+
liveStore.connected(connected);
|
|
874
|
+
}
|
|
875
|
+
}), [
|
|
876
|
+
client,
|
|
877
|
+
invalidateActivityCaches,
|
|
878
|
+
invalidateBalanceCaches,
|
|
879
|
+
invalidatePositionCaches,
|
|
880
|
+
invalidateResolutionCaches,
|
|
881
|
+
isClientAuthenticated,
|
|
882
|
+
queryClient,
|
|
883
|
+
syncLiveQuery,
|
|
884
|
+
liveStore,
|
|
885
|
+
publishLocalCandles
|
|
886
|
+
]);
|
|
887
|
+
const callbacksRef = useRef(callbacks);
|
|
888
|
+
callbacksRef.current = callbacks;
|
|
889
|
+
useEffect(() => {
|
|
890
|
+
wsRef.current?.setCallbacks(callbacks);
|
|
891
|
+
}, [callbacks]);
|
|
892
|
+
useEffect(() => {
|
|
893
|
+
let instance = null;
|
|
894
|
+
const resyncDeadlineByMarket = resyncDeadlineByMarketRef.current;
|
|
895
|
+
try {
|
|
896
|
+
instance = client.createWebSocket(callbacksRef.current, { enableLogs: enableWebsocketsLogs });
|
|
897
|
+
} catch {
|
|
898
|
+
console.warn("[AggUI] WebSocket connection could not be established. Ensure wsUrl is configured in createAggClient(). Real-time features (live prices, order updates) are disabled.");
|
|
899
|
+
setWs(null);
|
|
900
|
+
wsRef.current = null;
|
|
901
|
+
setIsConnected(false);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
instance.setHostDiagnostics?.(() => ({
|
|
905
|
+
live: liveStore.getDiagnostics(),
|
|
906
|
+
history: getChartHistoryRetentionStats(queryClient),
|
|
907
|
+
candles: getCandleStoreDiagnostics()
|
|
908
|
+
}));
|
|
909
|
+
setWs(instance);
|
|
910
|
+
wsRef.current = instance;
|
|
911
|
+
return () => {
|
|
912
|
+
instance?.destroy();
|
|
913
|
+
if (wsRef.current === instance) wsRef.current = null;
|
|
914
|
+
resyncDeadlineByMarket.clear();
|
|
915
|
+
setWs(null);
|
|
916
|
+
setIsConnected(false);
|
|
917
|
+
};
|
|
918
|
+
}, [
|
|
919
|
+
client,
|
|
920
|
+
enableWebsocketsLogs,
|
|
921
|
+
isClientAuthenticated,
|
|
922
|
+
liveStore,
|
|
923
|
+
queryClient
|
|
924
|
+
]);
|
|
925
|
+
return /* @__PURE__ */ jsx(AggWebSocketConnectionContext.Provider, {
|
|
926
|
+
value: isConnected,
|
|
927
|
+
children: /* @__PURE__ */ jsx(AggWsUserEventContext.Provider, {
|
|
928
|
+
value: userEventListeners,
|
|
929
|
+
children: /* @__PURE__ */ jsx(AggWebSocketContext.Provider, {
|
|
930
|
+
value: ws,
|
|
931
|
+
children
|
|
932
|
+
})
|
|
933
|
+
})
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/execution/use-withdraw-managed.ts
|
|
938
|
+
/**
|
|
939
|
+
* Mutation hook for withdrawing funds from managed wallets. On submission
|
|
940
|
+
* success the backend has already debited the managed-side balance and
|
|
941
|
+
* recorded the activity row, so we invalidate the full money-state cache
|
|
942
|
+
* surface (balances, positions, activity). Terminal lifecycle invalidation
|
|
943
|
+
* is handled separately by `useWithdrawalLifecycle`.
|
|
944
|
+
*/
|
|
945
|
+
function useWithdrawManaged(options) {
|
|
946
|
+
const client = useAggClient();
|
|
947
|
+
const queryClient = useQueryClient();
|
|
948
|
+
return useMutation({
|
|
949
|
+
mutationFn: (params) => client.withdrawManaged(params),
|
|
950
|
+
onSuccess: (data) => {
|
|
951
|
+
invalidateUserMoneyState(queryClient);
|
|
952
|
+
options?.onSuccess?.(data);
|
|
953
|
+
},
|
|
954
|
+
onError: options?.onError
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/execution/use-managed-balances.ts
|
|
959
|
+
/**
|
|
960
|
+
* Query hook for fetching unified managed wallet balances across all chains.
|
|
961
|
+
*/
|
|
962
|
+
function useManagedBalances(options) {
|
|
963
|
+
const client = useAggClient();
|
|
964
|
+
const { enabled = true, mode, custody } = options ?? {};
|
|
965
|
+
const params = {
|
|
966
|
+
...mode ? { mode } : {},
|
|
967
|
+
...custody ? { custody } : {}
|
|
968
|
+
};
|
|
969
|
+
const query = useQuery({
|
|
970
|
+
queryKey: executionKeys.balances(mode, custody),
|
|
971
|
+
queryFn: () => client.getManagedBalances(Object.keys(params).length ? params : void 0),
|
|
972
|
+
enabled,
|
|
973
|
+
staleTime: 1e4,
|
|
974
|
+
refetchInterval: mode === "paper" ? false : 5e3,
|
|
975
|
+
refetchIntervalInBackground: false,
|
|
976
|
+
gcTime: 3e5,
|
|
977
|
+
retry: 1
|
|
978
|
+
});
|
|
979
|
+
return {
|
|
980
|
+
...query,
|
|
981
|
+
balances: query.data
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
//#endregion
|
|
985
|
+
//#region src/withdraw/use-withdraw-flow.ts
|
|
986
|
+
const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
|
|
987
|
+
const SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
988
|
+
/** Solana mainnet chain id (mirrors `CHAIN_IDS.SOLANA` in `@app/balance`). */
|
|
989
|
+
const SOLANA_CHAIN_ID = 792703809;
|
|
990
|
+
/**
|
|
991
|
+
* Chain IDs the withdrawal pipeline can settle on. Mirrors
|
|
992
|
+
* `WITHDRAWAL_SUPPORTED_CHAIN_IDS` in `@app/balance` (api). Solana is
|
|
993
|
+
* supported as a destination — the API resolves the SPL USDC mint and
|
|
994
|
+
* Relay's cross-chain quotes accept base58 recipients (no on-chain
|
|
995
|
+
* forwarder for Solana, so the lifecycle path skips that step). Keep this
|
|
996
|
+
* set in lock-step with the api-side constant.
|
|
997
|
+
*/
|
|
998
|
+
const WITHDRAWAL_SUPPORTED_CHAIN_IDS = /* @__PURE__ */ new Set([
|
|
999
|
+
1,
|
|
1000
|
+
137,
|
|
1001
|
+
42161,
|
|
1002
|
+
8453,
|
|
1003
|
+
56,
|
|
1004
|
+
SOLANA_CHAIN_ID,
|
|
1005
|
+
1337
|
|
1006
|
+
]);
|
|
1007
|
+
const isValidDestinationAddress = (address, chainId) => {
|
|
1008
|
+
if (chainId === SOLANA_CHAIN_ID) return SOLANA_ADDRESS_REGEX.test(address);
|
|
1009
|
+
return EVM_ADDRESS_REGEX.test(address);
|
|
1010
|
+
};
|
|
1011
|
+
/**
|
|
1012
|
+
* Classify a recipient address by chain family so the network picker can
|
|
1013
|
+
* auto-select the matching chain and disable the others. EVM addresses are
|
|
1014
|
+
* unambiguous (`0x…` — `0` isn't in the base58 alphabet, so they can never
|
|
1015
|
+
* match the Solana regex), so we test EVM first. Returns null until the input
|
|
1016
|
+
* is a structurally complete address of either family — partial/garbage input
|
|
1017
|
+
* leaves every network enabled.
|
|
1018
|
+
*/
|
|
1019
|
+
const detectDestinationAddressKind = (address) => {
|
|
1020
|
+
const trimmed = address.trim();
|
|
1021
|
+
if (!trimmed) return null;
|
|
1022
|
+
if (EVM_ADDRESS_REGEX.test(trimmed)) return "evm";
|
|
1023
|
+
if (SOLANA_ADDRESS_REGEX.test(trimmed)) return "solana";
|
|
1024
|
+
return null;
|
|
1025
|
+
};
|
|
1026
|
+
const DEFAULT_WITHDRAW_SUMMARY = {
|
|
1027
|
+
amountReceived: "",
|
|
1028
|
+
network: "",
|
|
1029
|
+
toWallet: "",
|
|
1030
|
+
fees: "—"
|
|
1031
|
+
};
|
|
1032
|
+
const formatRawTokenAmount = (rawAmount, decimals) => {
|
|
1033
|
+
if (!rawAmount) return "0";
|
|
1034
|
+
const raw = BigInt(rawAmount);
|
|
1035
|
+
const divisor = BigInt(`1${"0".repeat(decimals)}`);
|
|
1036
|
+
const whole = raw / divisor;
|
|
1037
|
+
const remainder = raw % divisor;
|
|
1038
|
+
if (remainder === BigInt(0)) return whole.toString();
|
|
1039
|
+
return `${whole.toString()}.${remainder.toString().padStart(decimals, "0").replace(/0+$/, "")}`;
|
|
1040
|
+
};
|
|
1041
|
+
const formatRawTokenAmountForDisplay = (rawAmount, decimals, fractionDigits = 2) => {
|
|
1042
|
+
const [wholePart, fractionalPart = ""] = formatRawTokenAmount(rawAmount, decimals).split(".");
|
|
1043
|
+
return `${wholePart}.${fractionalPart.padEnd(fractionDigits, "0").slice(0, fractionDigits)}`;
|
|
1044
|
+
};
|
|
1045
|
+
const parseTokenAmountToRaw = (amount, decimals) => {
|
|
1046
|
+
const normalizedAmount = amount.trim();
|
|
1047
|
+
if (!normalizedAmount || !/^\d*(?:\.\d*)?$/.test(normalizedAmount)) return null;
|
|
1048
|
+
const [wholePart = "0", fractionalPart = ""] = normalizedAmount.split(".");
|
|
1049
|
+
if (fractionalPart.length > decimals) return null;
|
|
1050
|
+
const normalizedWhole = wholePart === "" ? "0" : wholePart;
|
|
1051
|
+
const normalizedFraction = fractionalPart.padEnd(decimals, "0");
|
|
1052
|
+
try {
|
|
1053
|
+
return `${BigInt(normalizedWhole)}${normalizedFraction}`.replace(/^0+(?=\d)/, "");
|
|
1054
|
+
} catch {
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
const shortenAddress = (address) => address.length > 12 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;
|
|
1059
|
+
function useWithdrawFlow(options) {
|
|
1060
|
+
const { open, onOpenChange } = options;
|
|
1061
|
+
const { totalBalance } = useAggBalanceState();
|
|
1062
|
+
const { balances } = useManagedBalances({ enabled: open });
|
|
1063
|
+
const { supportedChains } = useDepositAddresses({ enabled: open });
|
|
1064
|
+
const withdrawMutation = useWithdrawManaged();
|
|
1065
|
+
const syncBalances = useSyncBalances();
|
|
1066
|
+
const ws = useAggWebSocket();
|
|
1067
|
+
useEffect(() => {
|
|
1068
|
+
if (!open) return;
|
|
1069
|
+
syncBalances.mutate(void 0, { onError: () => {} });
|
|
1070
|
+
}, [open]);
|
|
1071
|
+
const [withdrawDestination, setWithdrawDestination] = useState("");
|
|
1072
|
+
const [withdrawAmount, setWithdrawAmount] = useState("");
|
|
1073
|
+
const [withdrawToken, setWithdrawToken] = useState("USDC");
|
|
1074
|
+
const [withdrawNetwork, setWithdrawNetwork] = useState("");
|
|
1075
|
+
const [withdrawSummary, setWithdrawSummary] = useState(DEFAULT_WITHDRAW_SUMMARY);
|
|
1076
|
+
const [withdrawalId, setWithdrawalId] = useState(null);
|
|
1077
|
+
const [isMax, setIsMax] = useState(false);
|
|
1078
|
+
const detectedAddressKind = useMemo(() => detectDestinationAddressKind(withdrawDestination), [withdrawDestination]);
|
|
1079
|
+
const networkOptions = useMemo(() => (supportedChains ?? []).filter((chain) => WITHDRAWAL_SUPPORTED_CHAIN_IDS.has(chain.chainId)).map((chain) => {
|
|
1080
|
+
const value = String(chain.chainId);
|
|
1081
|
+
const isSolana = chain.chainId === SOLANA_CHAIN_ID;
|
|
1082
|
+
const disabled = detectedAddressKind === "solana" ? !isSolana : detectedAddressKind === "evm" ? isSolana : false;
|
|
1083
|
+
return {
|
|
1084
|
+
value,
|
|
1085
|
+
label: chain.name,
|
|
1086
|
+
disabled
|
|
1087
|
+
};
|
|
1088
|
+
}), [supportedChains, detectedAddressKind]);
|
|
1089
|
+
const firstEnabledNetwork = networkOptions.find((option) => !option.disabled)?.value ?? networkOptions[0]?.value ?? "";
|
|
1090
|
+
const isCurrentNetworkSelectable = networkOptions.some((option) => option.value === withdrawNetwork && !option.disabled);
|
|
1091
|
+
const resolvedWithdrawNetwork = isCurrentNetworkSelectable ? withdrawNetwork : firstEnabledNetwork;
|
|
1092
|
+
useEffect(() => {
|
|
1093
|
+
if (!networkOptions.length) return;
|
|
1094
|
+
if (!isCurrentNetworkSelectable && firstEnabledNetwork) setWithdrawNetwork(firstEnabledNetwork);
|
|
1095
|
+
}, [
|
|
1096
|
+
networkOptions,
|
|
1097
|
+
isCurrentNetworkSelectable,
|
|
1098
|
+
firstEnabledNetwork
|
|
1099
|
+
]);
|
|
1100
|
+
const tokenOptions = useMemo(() => {
|
|
1101
|
+
const selectedChainId = Number(resolvedWithdrawNetwork);
|
|
1102
|
+
const supportedTokensForNetwork = supportedChains?.find((chain) => chain.chainId === selectedChainId)?.tokens ?? [];
|
|
1103
|
+
const withdrawableSymbols = new Set(supportedTokensForNetwork.map((token) => token.symbol));
|
|
1104
|
+
return (balances?.cash ?? []).filter((cashEntry) => withdrawableSymbols.has(cashEntry.tokenSymbol)).map((cashEntry) => ({
|
|
1105
|
+
value: cashEntry.tokenSymbol,
|
|
1106
|
+
label: cashEntry.tokenSymbol
|
|
1107
|
+
}));
|
|
1108
|
+
}, [
|
|
1109
|
+
balances,
|
|
1110
|
+
resolvedWithdrawNetwork,
|
|
1111
|
+
supportedChains
|
|
1112
|
+
]);
|
|
1113
|
+
const resolvedWithdrawToken = tokenOptions.some((option) => option.value === withdrawToken) ? withdrawToken : tokenOptions[0]?.value || "";
|
|
1114
|
+
useEffect(() => {
|
|
1115
|
+
if (!tokenOptions.length) return;
|
|
1116
|
+
if (!tokenOptions.some((option) => option.value === withdrawToken)) setWithdrawToken(tokenOptions[0].value);
|
|
1117
|
+
}, [tokenOptions, withdrawToken]);
|
|
1118
|
+
const selectedCashEntry = useMemo(() => balances?.cash.find((cashEntry) => cashEntry.tokenSymbol === resolvedWithdrawToken), [balances, resolvedWithdrawToken]);
|
|
1119
|
+
const selectedTokenDecimals = useMemo(() => {
|
|
1120
|
+
const selectedChainId = Number(resolvedWithdrawNetwork);
|
|
1121
|
+
return supportedChains?.find((chain) => chain.chainId === selectedChainId)?.tokens.find((token) => token.symbol === resolvedWithdrawToken)?.decimals ?? selectedCashEntry?.decimals ?? 6;
|
|
1122
|
+
}, [
|
|
1123
|
+
resolvedWithdrawNetwork,
|
|
1124
|
+
resolvedWithdrawToken,
|
|
1125
|
+
selectedCashEntry?.decimals,
|
|
1126
|
+
supportedChains
|
|
1127
|
+
]);
|
|
1128
|
+
const exactBalance = useMemo(() => {
|
|
1129
|
+
if (!selectedCashEntry) return "0";
|
|
1130
|
+
return formatRawTokenAmount(selectedCashEntry.totalRaw, selectedCashEntry.decimals);
|
|
1131
|
+
}, [selectedCashEntry]);
|
|
1132
|
+
const balanceDisplay = useMemo(() => {
|
|
1133
|
+
if (!selectedCashEntry) return `0.00 ${resolvedWithdrawToken || withdrawToken}`;
|
|
1134
|
+
return `${formatRawTokenAmountForDisplay(selectedCashEntry.totalRaw, selectedCashEntry.decimals)} ${resolvedWithdrawToken}`;
|
|
1135
|
+
}, [
|
|
1136
|
+
resolvedWithdrawToken,
|
|
1137
|
+
selectedCashEntry,
|
|
1138
|
+
withdrawToken
|
|
1139
|
+
]);
|
|
1140
|
+
const resetFlowState = useCallback(() => {
|
|
1141
|
+
setWithdrawDestination("");
|
|
1142
|
+
setWithdrawAmount("");
|
|
1143
|
+
setWithdrawToken("USDC");
|
|
1144
|
+
setWithdrawNetwork("");
|
|
1145
|
+
setWithdrawSummary(DEFAULT_WITHDRAW_SUMMARY);
|
|
1146
|
+
setWithdrawalId(null);
|
|
1147
|
+
setIsMax(false);
|
|
1148
|
+
}, []);
|
|
1149
|
+
const handleWithdrawOpenChange = useCallback((isOpen) => {
|
|
1150
|
+
if (!isOpen) resetFlowState();
|
|
1151
|
+
onOpenChange(isOpen);
|
|
1152
|
+
}, [onOpenChange, resetFlowState]);
|
|
1153
|
+
useEffect(() => {
|
|
1154
|
+
if (!open) resetFlowState();
|
|
1155
|
+
}, [open, resetFlowState]);
|
|
1156
|
+
const handleWithdrawProvider = useCallback(async () => {
|
|
1157
|
+
const destinationChainId = Number(resolvedWithdrawNetwork);
|
|
1158
|
+
const trimmedDestination = withdrawDestination.trim();
|
|
1159
|
+
const amountRaw = parseTokenAmountToRaw(withdrawAmount, selectedTokenDecimals);
|
|
1160
|
+
if (!amountRaw || BigInt(amountRaw) <= BigInt(0)) throw new Error("Enter a withdrawal amount greater than zero.");
|
|
1161
|
+
if (!Number.isFinite(destinationChainId) || destinationChainId <= 0) throw new Error("Select a destination network.");
|
|
1162
|
+
if (!isValidDestinationAddress(trimmedDestination, destinationChainId)) throw new Error(`Enter ${destinationChainId === SOLANA_CHAIN_ID ? "a Solana wallet address (base58, 32–44 chars)" : "an EVM destination address (0x… 40 hex chars)"}.`);
|
|
1163
|
+
if (selectedCashEntry) {
|
|
1164
|
+
const scaleBy = (n, exp) => {
|
|
1165
|
+
if (exp <= 0) return n;
|
|
1166
|
+
return n * BigInt(`1${"0".repeat(exp)}`);
|
|
1167
|
+
};
|
|
1168
|
+
const balanceInDestFrame = (() => {
|
|
1169
|
+
const native = BigInt(selectedCashEntry.totalRaw);
|
|
1170
|
+
if (selectedCashEntry.decimals === selectedTokenDecimals) return native;
|
|
1171
|
+
if (selectedCashEntry.decimals > selectedTokenDecimals) return native / scaleBy(BigInt(1), selectedCashEntry.decimals - selectedTokenDecimals);
|
|
1172
|
+
return scaleBy(native, selectedTokenDecimals - selectedCashEntry.decimals);
|
|
1173
|
+
})();
|
|
1174
|
+
if (!isMax && BigInt(amountRaw) > balanceInDestFrame) throw new Error("Withdrawal amount exceeds your available balance.");
|
|
1175
|
+
}
|
|
1176
|
+
ws?.connect();
|
|
1177
|
+
await new Promise((resolve, reject) => {
|
|
1178
|
+
withdrawMutation.mutate({
|
|
1179
|
+
amountRaw,
|
|
1180
|
+
tokenSymbol: resolvedWithdrawToken,
|
|
1181
|
+
destinationAddress: trimmedDestination,
|
|
1182
|
+
destinationChainId,
|
|
1183
|
+
...isMax ? { max: true } : {}
|
|
1184
|
+
}, {
|
|
1185
|
+
onSuccess: (data) => {
|
|
1186
|
+
setWithdrawalId(data.withdrawalId);
|
|
1187
|
+
resolve();
|
|
1188
|
+
},
|
|
1189
|
+
onError: reject
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
setWithdrawSummary({
|
|
1193
|
+
amountReceived: `${formatRawTokenAmountForDisplay(amountRaw, selectedTokenDecimals)} ${resolvedWithdrawToken}`,
|
|
1194
|
+
network: supportedChains?.find((chain) => chain.chainId === destinationChainId)?.name ?? resolvedWithdrawNetwork,
|
|
1195
|
+
toWallet: shortenAddress(trimmedDestination),
|
|
1196
|
+
fees: "—"
|
|
1197
|
+
});
|
|
1198
|
+
}, [
|
|
1199
|
+
isMax,
|
|
1200
|
+
resolvedWithdrawNetwork,
|
|
1201
|
+
resolvedWithdrawToken,
|
|
1202
|
+
selectedCashEntry,
|
|
1203
|
+
selectedTokenDecimals,
|
|
1204
|
+
supportedChains,
|
|
1205
|
+
withdrawAmount,
|
|
1206
|
+
withdrawDestination,
|
|
1207
|
+
withdrawMutation,
|
|
1208
|
+
ws
|
|
1209
|
+
]);
|
|
1210
|
+
return {
|
|
1211
|
+
open,
|
|
1212
|
+
onOpenChange: handleWithdrawOpenChange,
|
|
1213
|
+
withdrawFlow: {
|
|
1214
|
+
balance: totalBalance,
|
|
1215
|
+
balanceDisplay,
|
|
1216
|
+
amount: withdrawAmount,
|
|
1217
|
+
destinationWallet: withdrawDestination,
|
|
1218
|
+
tokenOptions,
|
|
1219
|
+
networkOptions,
|
|
1220
|
+
selectedToken: resolvedWithdrawToken,
|
|
1221
|
+
selectedNetwork: resolvedWithdrawNetwork,
|
|
1222
|
+
purchaseSummary: withdrawSummary,
|
|
1223
|
+
withdrawalId,
|
|
1224
|
+
isMax
|
|
1225
|
+
},
|
|
1226
|
+
onWithdrawDestinationChange: setWithdrawDestination,
|
|
1227
|
+
onWithdrawAmountChange: useCallback((v) => {
|
|
1228
|
+
setIsMax(false);
|
|
1229
|
+
setWithdrawAmount(v);
|
|
1230
|
+
}, []),
|
|
1231
|
+
onWithdrawTokenChange: setWithdrawToken,
|
|
1232
|
+
onWithdrawNetworkChange: setWithdrawNetwork,
|
|
1233
|
+
onMaxClick: useCallback(() => {
|
|
1234
|
+
setIsMax(true);
|
|
1235
|
+
setWithdrawAmount(exactBalance === "0" ? "0" : exactBalance);
|
|
1236
|
+
}, [exactBalance]),
|
|
1237
|
+
onSelectWithdrawProvider: useCallback(async (_providerId) => handleWithdrawProvider(), [handleWithdrawProvider]),
|
|
1238
|
+
onDoneWithdraw: useCallback(() => handleWithdrawOpenChange(false), [handleWithdrawOpenChange])
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/withdraw/use-withdraw-estimate.ts
|
|
1243
|
+
/**
|
|
1244
|
+
* Natively estimates withdrawal fees, network reserves, and the expected net
|
|
1245
|
+
* amount to be received based on the selected token, destination network,
|
|
1246
|
+
* and transfer amount.
|
|
1247
|
+
*/
|
|
1248
|
+
function useWithdrawEstimate({ amount, selectedToken, selectedNetwork }) {
|
|
1249
|
+
return useMemo(() => {
|
|
1250
|
+
const numAmount = Number(amount);
|
|
1251
|
+
if (!amount || isNaN(numAmount) || numAmount <= 0 || !selectedToken || !selectedNetwork) return null;
|
|
1252
|
+
const network = selectedNetwork.toLowerCase();
|
|
1253
|
+
let feeVal = .1;
|
|
1254
|
+
let reserveVal = .2;
|
|
1255
|
+
if (network === "1" || network === "mainnet" || network === "ethereum") {
|
|
1256
|
+
feeVal = 1.22;
|
|
1257
|
+
reserveVal = .3;
|
|
1258
|
+
} else if (network === "137" || network === "polygon") {
|
|
1259
|
+
feeVal = .05;
|
|
1260
|
+
reserveVal = .15;
|
|
1261
|
+
} else if (network === "42161" || network === "arbitrum") {
|
|
1262
|
+
feeVal = .05;
|
|
1263
|
+
reserveVal = .15;
|
|
1264
|
+
} else if (network === "8453" || network === "base") {
|
|
1265
|
+
feeVal = .05;
|
|
1266
|
+
reserveVal = .15;
|
|
1267
|
+
} else if (network === "56" || network === "bnb" || network === "bsc") {
|
|
1268
|
+
feeVal = .1;
|
|
1269
|
+
reserveVal = .2;
|
|
1270
|
+
} else if (network === "792703809" || network === "solana") {
|
|
1271
|
+
feeVal = .01;
|
|
1272
|
+
reserveVal = .04;
|
|
1273
|
+
} else if (network === "1337" || network === "hyperliquid") {
|
|
1274
|
+
feeVal = .02;
|
|
1275
|
+
reserveVal = .08;
|
|
1276
|
+
}
|
|
1277
|
+
const youReceiveVal = Math.max(0, numAmount - feeVal);
|
|
1278
|
+
return {
|
|
1279
|
+
estimatedFees: `~$${feeVal.toFixed(2)}`,
|
|
1280
|
+
networkReserve: `~$${reserveVal.toFixed(2)}`,
|
|
1281
|
+
youReceive: `~${youReceiveVal.toFixed(2)} ${selectedToken}`
|
|
1282
|
+
};
|
|
1283
|
+
}, [
|
|
1284
|
+
amount,
|
|
1285
|
+
selectedToken,
|
|
1286
|
+
selectedNetwork
|
|
1287
|
+
]);
|
|
1288
|
+
}
|
|
1289
|
+
//#endregion
|
|
1290
|
+
//#region src/withdraw/use-withdrawal-lifecycle.ts
|
|
1291
|
+
const INITIAL_STATE = {
|
|
1292
|
+
pending: true,
|
|
1293
|
+
status: null,
|
|
1294
|
+
requestedAmountRaw: null,
|
|
1295
|
+
completedAmountRaw: null,
|
|
1296
|
+
feeRaw: null,
|
|
1297
|
+
terminal: false,
|
|
1298
|
+
lastLeg: null,
|
|
1299
|
+
legs: [],
|
|
1300
|
+
errorMessage: null,
|
|
1301
|
+
timestamp: null
|
|
1302
|
+
};
|
|
1303
|
+
/**
|
|
1304
|
+
* Convert a leg row from the REST status response into the WS lifecycle leg
|
|
1305
|
+
* shape so a single state mapping handles both. The REST leg includes the
|
|
1306
|
+
* type/status fields the WS event omits in the legs[] array — they line up
|
|
1307
|
+
* cleanly because the WS leg type is a superset.
|
|
1308
|
+
*/
|
|
1309
|
+
const restLegToWsLeg = (leg) => ({
|
|
1310
|
+
sourceChainId: leg.sourceChainId,
|
|
1311
|
+
destChainId: leg.destChainId,
|
|
1312
|
+
type: leg.type,
|
|
1313
|
+
status: leg.status,
|
|
1314
|
+
amountRaw: leg.amountRaw,
|
|
1315
|
+
txHash: leg.txHash,
|
|
1316
|
+
bridgeOperationId: leg.bridgeOperationId
|
|
1317
|
+
});
|
|
1318
|
+
/**
|
|
1319
|
+
* Merge the new event's leg-state into the existing `legs[]`.
|
|
1320
|
+
*
|
|
1321
|
+
* - If the event carries a full `legs[]` snapshot (pending / terminal
|
|
1322
|
+
* rollup), it replaces — the server's snapshot is authoritative.
|
|
1323
|
+
* - If it carries only a `leg` delta, find the matching row by
|
|
1324
|
+
* (sourceChainId, destChainId, type) and replace it; if no row matches
|
|
1325
|
+
* (the delta arrived before any snapshot listed the leg), append it.
|
|
1326
|
+
* This preserves the timeline UI between snapshots — without this,
|
|
1327
|
+
* intermediate `submitted`/`confirmed` deltas would wipe `legs[]` to
|
|
1328
|
+
* `[]` and the per-leg checklist would collapse mid-flow.
|
|
1329
|
+
* - If neither is present, leave the existing `legs[]` untouched.
|
|
1330
|
+
*/
|
|
1331
|
+
const mergeLegs = (prev, snapshot, delta) => {
|
|
1332
|
+
if (snapshot) return snapshot;
|
|
1333
|
+
if (!delta) return prev;
|
|
1334
|
+
const idx = prev.findIndex((l) => l.sourceChainId === delta.sourceChainId && l.destChainId === delta.destChainId && l.type === delta.type);
|
|
1335
|
+
if (idx === -1) return [...prev, delta];
|
|
1336
|
+
const next = prev.slice();
|
|
1337
|
+
next[idx] = delta;
|
|
1338
|
+
return next;
|
|
1339
|
+
};
|
|
1340
|
+
const restToLifecycleState = (response) => {
|
|
1341
|
+
return {
|
|
1342
|
+
pending: false,
|
|
1343
|
+
status: response.status,
|
|
1344
|
+
requestedAmountRaw: response.requested.amountRaw,
|
|
1345
|
+
completedAmountRaw: response.completedAmountRaw ?? null,
|
|
1346
|
+
feeRaw: response.expected.feeRaw ?? null,
|
|
1347
|
+
terminal: response.status === "completed" || response.status === "partial" || response.status === "failed",
|
|
1348
|
+
lastLeg: null,
|
|
1349
|
+
legs: response.legs.map(restLegToWsLeg),
|
|
1350
|
+
errorMessage: response.errorMessage ?? null,
|
|
1351
|
+
timestamp: 0
|
|
1352
|
+
};
|
|
1353
|
+
};
|
|
1354
|
+
/**
|
|
1355
|
+
* Subscribes to `withdrawal_lifecycle` WS events for a single `withdrawalId`
|
|
1356
|
+
* and exposes the latest snapshot. Returns a stable `pending` initial state
|
|
1357
|
+
* until the first matching event arrives.
|
|
1358
|
+
*
|
|
1359
|
+
* Pass `null` for `withdrawalId` to disable the subscription (e.g. before
|
|
1360
|
+
* the user has submitted a withdrawal). Switching the `withdrawalId` resets
|
|
1361
|
+
* the state so a stale snapshot from a prior withdrawal can never bleed
|
|
1362
|
+
* across.
|
|
1363
|
+
*
|
|
1364
|
+
* **Backfill behavior**: on mount with a non-null `withdrawalId`, and again
|
|
1365
|
+
* whenever the WS connection re-opens, the hook calls
|
|
1366
|
+
* `GET /execution/withdrawals/:id` to recover any lifecycle events that fired
|
|
1367
|
+
* before the listener was attached or while the socket was disconnected. WS
|
|
1368
|
+
* events always win over REST snapshots (REST stamps as `timestamp: 0`), so a
|
|
1369
|
+
* race between the two never regresses to a stale state.
|
|
1370
|
+
*/
|
|
1371
|
+
function useWithdrawalLifecycle(withdrawalId) {
|
|
1372
|
+
const client = useAggClient();
|
|
1373
|
+
const wsConnected = useAggWebSocketConnectionState();
|
|
1374
|
+
const queryClient = useQueryClient();
|
|
1375
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
1376
|
+
const stateRef = useRef(state);
|
|
1377
|
+
stateRef.current = state;
|
|
1378
|
+
const balanceRefetchedForRef = useRef(null);
|
|
1379
|
+
useEffect(() => {
|
|
1380
|
+
setState(INITIAL_STATE);
|
|
1381
|
+
}, [withdrawalId]);
|
|
1382
|
+
useEffect(() => {
|
|
1383
|
+
if (!withdrawalId) return;
|
|
1384
|
+
let cancelled = false;
|
|
1385
|
+
(async () => {
|
|
1386
|
+
try {
|
|
1387
|
+
const response = await client.getWithdrawalStatus(withdrawalId);
|
|
1388
|
+
if (cancelled) return;
|
|
1389
|
+
if (response.withdrawalId !== withdrawalId) return;
|
|
1390
|
+
const current = stateRef.current;
|
|
1391
|
+
if (current.timestamp != null && current.timestamp > 0) return;
|
|
1392
|
+
setState(restToLifecycleState(response));
|
|
1393
|
+
} catch {}
|
|
1394
|
+
})();
|
|
1395
|
+
return () => {
|
|
1396
|
+
cancelled = true;
|
|
1397
|
+
};
|
|
1398
|
+
}, [
|
|
1399
|
+
client,
|
|
1400
|
+
withdrawalId,
|
|
1401
|
+
wsConnected
|
|
1402
|
+
]);
|
|
1403
|
+
useOnWithdrawalLifecycle(useMemo(() => {
|
|
1404
|
+
if (!withdrawalId) return null;
|
|
1405
|
+
return (msg) => {
|
|
1406
|
+
if (msg.withdrawalId !== withdrawalId) return;
|
|
1407
|
+
setState((prev) => ({
|
|
1408
|
+
pending: false,
|
|
1409
|
+
status: msg.status,
|
|
1410
|
+
requestedAmountRaw: msg.requestedAmountRaw ?? prev.requestedAmountRaw,
|
|
1411
|
+
completedAmountRaw: msg.completedAmountRaw ?? prev.completedAmountRaw,
|
|
1412
|
+
feeRaw: msg.feeRaw ?? null,
|
|
1413
|
+
terminal: msg.terminal,
|
|
1414
|
+
lastLeg: msg.leg ?? null,
|
|
1415
|
+
legs: mergeLegs(prev.legs, msg.legs, msg.leg),
|
|
1416
|
+
errorMessage: msg.errorMessage ?? null,
|
|
1417
|
+
timestamp: msg.timestamp
|
|
1418
|
+
}));
|
|
1419
|
+
};
|
|
1420
|
+
}, [withdrawalId]));
|
|
1421
|
+
useEffect(() => {
|
|
1422
|
+
if (!withdrawalId) return;
|
|
1423
|
+
if (!state.terminal) return;
|
|
1424
|
+
if (balanceRefetchedForRef.current === withdrawalId) return;
|
|
1425
|
+
balanceRefetchedForRef.current = withdrawalId;
|
|
1426
|
+
invalidateUserMoneyState(queryClient);
|
|
1427
|
+
client.syncManagedBalances().catch(() => {});
|
|
1428
|
+
}, [
|
|
1429
|
+
client,
|
|
1430
|
+
queryClient,
|
|
1431
|
+
state.terminal,
|
|
1432
|
+
withdrawalId
|
|
1433
|
+
]);
|
|
1434
|
+
return {
|
|
1435
|
+
state,
|
|
1436
|
+
reset: useCallback(() => setState(INITIAL_STATE), [])
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
//#endregion
|
|
1440
|
+
//#region src/withdraw/use-withdraw-preview.ts
|
|
1441
|
+
/**
|
|
1442
|
+
* Live withdrawal preview backed by the API (`POST /execution/withdraw/preview`).
|
|
1443
|
+
* Disabled until amount/token/chain/address are present and amount is non-zero.
|
|
1444
|
+
* The backend caches identical previews for 15s; FE debounce keeps Relay calls
|
|
1445
|
+
* bounded. Returns the real fee + receive amount, replacing the hardcoded
|
|
1446
|
+
* `useWithdrawEstimate`.
|
|
1447
|
+
*/
|
|
1448
|
+
function useWithdrawPreview({ amountRaw, tokenSymbol, destinationChainId, destinationAddress, max }) {
|
|
1449
|
+
const client = useAggClient();
|
|
1450
|
+
const enabled = !!amountRaw && BigInt(amountRaw || "0") > BigInt(0) && !!tokenSymbol && !!destinationChainId && !!destinationAddress;
|
|
1451
|
+
return useQuery({
|
|
1452
|
+
queryKey: [
|
|
1453
|
+
"withdraw-preview",
|
|
1454
|
+
tokenSymbol,
|
|
1455
|
+
amountRaw,
|
|
1456
|
+
destinationChainId,
|
|
1457
|
+
destinationAddress,
|
|
1458
|
+
max ?? false
|
|
1459
|
+
],
|
|
1460
|
+
enabled,
|
|
1461
|
+
staleTime: 15e3,
|
|
1462
|
+
queryFn: () => client.withdrawPreview({
|
|
1463
|
+
amountRaw,
|
|
1464
|
+
tokenSymbol,
|
|
1465
|
+
destinationChainId,
|
|
1466
|
+
destinationAddress,
|
|
1467
|
+
...max ? { max: true } : {}
|
|
1468
|
+
})
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
//#endregion
|
|
1472
|
+
export { marketDataKeys as _, useManagedBalances as a, getOrCreateBuilder as b, useAggWebSocket as c, useOnOrderEvent as d, useOnOrderSubmitted as f, useLiveStates as g, getMarketLiveStore as h, useWithdrawFlow as i, useAggWebSocketConnectionState as l, useOnWithdrawalLifecycle as m, useWithdrawalLifecycle as n, useWithdrawManaged as o, useOnRedeemEvent as p, useWithdrawEstimate as r, AggWebSocketProvider as s, useWithdrawPreview as t, useOnBalanceUpdate as u, createMarketLiveState as v, getBuilder as y };
|
|
1473
|
+
|
|
1474
|
+
//# sourceMappingURL=withdraw--f5kYKJA.mjs.map
|