@0xinsider/sdk 0.14.0-bootstrap.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.
- package/LICENSE +21 -0
- package/README.md +674 -0
- package/dist/client.d.ts +1652 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2127 -0
- package/dist/client.js.map +1 -0
- package/dist/data-quality.d.ts +74 -0
- package/dist/data-quality.d.ts.map +1 -0
- package/dist/data-quality.js +68 -0
- package/dist/data-quality.js.map +1 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +700 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/pagination.d.ts +196 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +208 -0
- package/dist/pagination.js.map +1 -0
- package/dist/provenance.d.ts +8 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +15 -0
- package/dist/provenance.js.map +1 -0
- package/dist/retry.d.ts +68 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +125 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts +4910 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/schema.js.map +1 -0
- package/dist/stream.d.ts +509 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +932 -0
- package/dist/stream.js.map +1 -0
- package/dist/webhooks.d.ts +314 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +153 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +64 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,2127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone 0xinsider API V1 client.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the app's original repo-owned client (`web/src/lib/api-client`,
|
|
5
|
+
* removed in #9060; this package is now the sole client implementation). The
|
|
6
|
+
* `API_CLIENT_OPERATIONS` table below IS the contract surface:
|
|
7
|
+
* `scripts/check-sdk-openapi-drift.mjs` asserts it equals the set of operations
|
|
8
|
+
* with a `200` response in `web/public/api/v1/openapi.json`, so this package
|
|
9
|
+
* cannot silently diverge from the spec. That enforcement used to be a vitest
|
|
10
|
+
* suite, which the repository does not run -- the table shipped three
|
|
11
|
+
* operations short before #9687 replaced it with the runnable script, and
|
|
12
|
+
* #11894 deleted the suite.
|
|
13
|
+
*
|
|
14
|
+
* Differences from the retired app client (intentional):
|
|
15
|
+
* - No `@/lib/api-contracts` import; the operation table is inlined so the
|
|
16
|
+
* package has zero workspace coupling.
|
|
17
|
+
* - Errors throw the typed subclass hierarchy in `errors.ts` (the app client
|
|
18
|
+
* threw a single `OxinsiderApiError`).
|
|
19
|
+
* - Extra typed convenience methods for list/candle/webhook-event/stream
|
|
20
|
+
* surfaces and a typed `Grade` enum.
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { errorFromResponse, ExportIntegrityError, OxinsiderApiError, InvalidResponseError, RequestTimeoutError, ResponseBodyReadError, } from "./errors.js";
|
|
24
|
+
import { RETRY_AFTER_CEILING_MS, retryAfterSeconds, sleepUnlessAborted, } from "./retry.js";
|
|
25
|
+
/**
|
|
26
|
+
* The full V1 operation table. One row per OpenAPI operation with a
|
|
27
|
+
* documented 2xx, which since #16137 includes the `201`-only
|
|
28
|
+
* `registerAgent`; `kind` says which method reads its body. Kept in lockstep
|
|
29
|
+
* with `web/public/api/v1/openapi.json` by
|
|
30
|
+
* `scripts/check-sdk-openapi-drift.mjs`, which also checks every row's kind
|
|
31
|
+
* against the spec. Do not add a row without a matching spec operation, and
|
|
32
|
+
* do not remove a spec operation without removing its row. A redirect-only
|
|
33
|
+
* operation belongs in `REDIRECT_OPERATIONS` below, not here.
|
|
34
|
+
*/
|
|
35
|
+
export const API_CLIENT_OPERATIONS = [
|
|
36
|
+
{ method: "POST", path: "/api/v1/datasets/whale-trades", operationId: "submitWhaleDataset", auth: "bearer" },
|
|
37
|
+
{ method: "GET", path: "/api/v1/datasets/whale-trades/{job_id}", operationId: "getWhaleDatasetStatus", auth: "bearer" },
|
|
38
|
+
{ method: "POST", path: "/api/v1/datasets/whale-trades/{job_id}/cancel", operationId: "cancelWhaleDataset", auth: "bearer" },
|
|
39
|
+
{ method: "GET", path: "/api/v1/me", operationId: "getAccountIdentity", auth: "bearer" },
|
|
40
|
+
{
|
|
41
|
+
method: "GET",
|
|
42
|
+
path: "/api/v1",
|
|
43
|
+
operationId: "getApiDiscovery",
|
|
44
|
+
auth: "none",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
method: "GET",
|
|
48
|
+
path: "/api/v1/trader/{address}",
|
|
49
|
+
operationId: "getTrader",
|
|
50
|
+
auth: "bearer",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
method: "POST",
|
|
54
|
+
path: "/api/v1/traders/batch",
|
|
55
|
+
operationId: "batchGetTraders",
|
|
56
|
+
auth: "bearer",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
method: "GET",
|
|
60
|
+
path: "/api/v1/trader/{address}/position-timeline",
|
|
61
|
+
operationId: "getPositionTimeline",
|
|
62
|
+
auth: "bearer",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
method: "GET",
|
|
66
|
+
path: "/api/v1/traders/{trader}/position-timeline",
|
|
67
|
+
operationId: "getPositionTimelineById",
|
|
68
|
+
auth: "bearer",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
method: "GET",
|
|
72
|
+
path: "/api/v1/positions",
|
|
73
|
+
operationId: "listPositions",
|
|
74
|
+
auth: "bearer",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
method: "GET",
|
|
78
|
+
path: "/api/v1/large-positions",
|
|
79
|
+
operationId: "listLargePositions",
|
|
80
|
+
auth: "bearer",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
method: "GET",
|
|
84
|
+
path: "/api/v1/trader/{address}/pnl",
|
|
85
|
+
operationId: "getTraderPnl",
|
|
86
|
+
auth: "bearer",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
method: "GET",
|
|
90
|
+
path: "/api/v1/trader/{address}/categories",
|
|
91
|
+
operationId: "getTraderCategoryRecords",
|
|
92
|
+
auth: "bearer",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
method: "GET",
|
|
96
|
+
path: "/api/v1/trader/{address}/grade-at",
|
|
97
|
+
operationId: "getTraderGradeAt",
|
|
98
|
+
auth: "bearer",
|
|
99
|
+
},
|
|
100
|
+
// Pre-existing op-table drift (surfaced by test/drift.test.ts, #6915): these
|
|
101
|
+
// 200-returning openapi operations were never mirrored into the client table.
|
|
102
|
+
// Added so the table stays contract-complete against the checked-in spec. All
|
|
103
|
+
// inherit the global `bearerAuth` security (no per-op `security: []` override,
|
|
104
|
+
// and none are in the backend public-path allowlist), so auth is "bearer".
|
|
105
|
+
// The drift check intentionally excludes the redirect-only routes (openapi-spec
|
|
106
|
+
// redirect + export/download, no 200 response), so they are NOT added here.
|
|
107
|
+
{
|
|
108
|
+
method: "GET",
|
|
109
|
+
path: "/api/v1/trader/{address}/context",
|
|
110
|
+
operationId: "getTraderContext",
|
|
111
|
+
auth: "bearer",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
method: "GET",
|
|
115
|
+
path: "/api/v1/trader/{address}/context.md",
|
|
116
|
+
operationId: "getTraderContextMarkdown",
|
|
117
|
+
auth: "bearer",
|
|
118
|
+
kind: "text",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
method: "POST",
|
|
122
|
+
path: "/api/v1/trader/{address}/export",
|
|
123
|
+
operationId: "submitTraderExport",
|
|
124
|
+
auth: "bearer",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
method: "GET",
|
|
128
|
+
path: "/api/v1/trader/{address}/export/status",
|
|
129
|
+
operationId: "getTraderExportStatus",
|
|
130
|
+
auth: "bearer",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
method: "POST",
|
|
134
|
+
path: "/api/v1/trader/{address}/export/cancel",
|
|
135
|
+
operationId: "cancelTraderExport",
|
|
136
|
+
auth: "bearer",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
method: "GET",
|
|
140
|
+
path: "/api/v1/leaderboard/trending",
|
|
141
|
+
operationId: "listTrendingWallets",
|
|
142
|
+
auth: "bearer",
|
|
143
|
+
},
|
|
144
|
+
// Pre-existing gap, fixed here (#7209): the route has shipped in openapi.json,
|
|
145
|
+
// llms-full.txt, agents.md, and the discovery doc, but never in the SDK table --
|
|
146
|
+
// so `drift.test.ts` was RED on main and SDK users had no typed method for it.
|
|
147
|
+
{
|
|
148
|
+
method: "GET",
|
|
149
|
+
path: "/api/v1/sports/pre-game-sides",
|
|
150
|
+
operationId: "listPreGameSides",
|
|
151
|
+
auth: "bearer",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
method: "GET",
|
|
155
|
+
path: "/api/v1/sports/pre-game-side-observations",
|
|
156
|
+
operationId: "listPreGameSideObservations",
|
|
157
|
+
auth: "bearer",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
method: "GET",
|
|
161
|
+
path: "/api/v1/sports-edge-signals",
|
|
162
|
+
operationId: "listSportsEdgeSignals",
|
|
163
|
+
auth: "bearer",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
method: "GET",
|
|
167
|
+
path: "/api/v1/sports-edge-observations",
|
|
168
|
+
operationId: "listSportsEdgeObservations",
|
|
169
|
+
auth: "bearer",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
method: "GET",
|
|
173
|
+
path: "/api/v1/games",
|
|
174
|
+
operationId: "listGames",
|
|
175
|
+
auth: "bearer",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
method: "GET",
|
|
179
|
+
path: "/api/v1/games/{event_slug}",
|
|
180
|
+
operationId: "getGame",
|
|
181
|
+
auth: "bearer",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
method: "GET",
|
|
185
|
+
path: "/api/v1/large-trades",
|
|
186
|
+
operationId: "listLargeTrades",
|
|
187
|
+
auth: "bearer",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
method: "GET",
|
|
191
|
+
path: "/api/v1/large-trades/history",
|
|
192
|
+
operationId: "listLargeTradeHistory",
|
|
193
|
+
auth: "bearer",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
method: "GET",
|
|
197
|
+
path: "/api/v1/large-trades/{id}/counterparties/executions",
|
|
198
|
+
operationId: "listLargeTradeCounterpartyExecutions",
|
|
199
|
+
auth: "bearer",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
method: "GET",
|
|
203
|
+
path: "/api/v1/large-trades/{id}/counterparties/executions/{execution_id}/makers",
|
|
204
|
+
operationId: "listLargeTradeCounterpartyMakers",
|
|
205
|
+
auth: "bearer",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
method: "GET",
|
|
209
|
+
path: "/api/v1/large-trades/{id}",
|
|
210
|
+
operationId: "getLargeTrade",
|
|
211
|
+
auth: "bearer",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
method: "GET",
|
|
215
|
+
path: "/api/v1/whale-trades",
|
|
216
|
+
operationId: "listWhaleTrades",
|
|
217
|
+
auth: "bearer",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
method: "GET",
|
|
221
|
+
path: "/api/v1/whale-trades/history",
|
|
222
|
+
operationId: "listWhaleTradeHistory",
|
|
223
|
+
auth: "bearer",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
method: "GET",
|
|
227
|
+
path: "/api/v1/whale-trades/{id}/counterparties/executions",
|
|
228
|
+
operationId: "listWhaleTradeCounterpartyExecutions",
|
|
229
|
+
auth: "bearer",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
method: "GET",
|
|
233
|
+
path: "/api/v1/whale-trades/{id}/counterparties/executions/{execution_id}/makers",
|
|
234
|
+
operationId: "listWhaleTradeCounterpartyMakers",
|
|
235
|
+
auth: "bearer",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
method: "GET",
|
|
239
|
+
path: "/api/v1/content/search",
|
|
240
|
+
operationId: "searchContent",
|
|
241
|
+
auth: "bearer",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
method: "GET",
|
|
245
|
+
path: "/api/v1/whale-trades/{id}",
|
|
246
|
+
operationId: "getWhaleTrade",
|
|
247
|
+
auth: "bearer",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
method: "GET",
|
|
251
|
+
path: "/api/v1/leaderboard",
|
|
252
|
+
operationId: "listLeaderboard",
|
|
253
|
+
auth: "bearer",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
method: "GET",
|
|
257
|
+
path: "/api/v1/markets/search",
|
|
258
|
+
operationId: "searchMarkets",
|
|
259
|
+
auth: "bearer",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
method: "GET",
|
|
263
|
+
path: "/api/v1/markets/explore",
|
|
264
|
+
operationId: "exploreMarkets",
|
|
265
|
+
auth: "bearer",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
method: "GET",
|
|
269
|
+
path: "/api/v1/markets/smart-money-flows",
|
|
270
|
+
operationId: "listSmartMoneyFlows",
|
|
271
|
+
auth: "bearer",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
method: "GET",
|
|
275
|
+
path: "/api/v1/markets/sharp-money-flows",
|
|
276
|
+
operationId: "listSharpMoneyFlows",
|
|
277
|
+
auth: "bearer",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
method: "GET",
|
|
281
|
+
path: "/api/v1/coverage",
|
|
282
|
+
operationId: "getCoverage",
|
|
283
|
+
auth: "none",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
method: "GET",
|
|
287
|
+
path: "/api/v1/platforms",
|
|
288
|
+
operationId: "getPlatforms",
|
|
289
|
+
auth: "none",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
method: "GET",
|
|
293
|
+
path: "/api/v1/market/{condition_id}/holders",
|
|
294
|
+
operationId: "getMarketHolders",
|
|
295
|
+
auth: "bearer",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
method: "GET",
|
|
299
|
+
path: "/api/v1/market/{condition_id}/flow",
|
|
300
|
+
operationId: "getMarketFlow",
|
|
301
|
+
auth: "bearer",
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
method: "GET",
|
|
305
|
+
path: "/api/v1/market/{condition_id}/intel",
|
|
306
|
+
operationId: "getMarketIntel",
|
|
307
|
+
auth: "bearer",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
method: "POST",
|
|
311
|
+
path: "/api/v1/markets/flow/batch",
|
|
312
|
+
operationId: "batchGetMarketFlow",
|
|
313
|
+
auth: "bearer",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
method: "POST",
|
|
317
|
+
path: "/api/v1/markets/intel/batch",
|
|
318
|
+
operationId: "batchGetMarketIntel",
|
|
319
|
+
auth: "bearer",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
method: "GET",
|
|
323
|
+
path: "/api/v1/market/{condition_id}/snapshot",
|
|
324
|
+
operationId: "getMarketSnapshot",
|
|
325
|
+
auth: "bearer",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
method: "GET",
|
|
329
|
+
path: "/api/v1/market/{condition_id}/context.md",
|
|
330
|
+
operationId: "getMarketContextMarkdown",
|
|
331
|
+
auth: "bearer",
|
|
332
|
+
kind: "text",
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
method: "GET",
|
|
336
|
+
path: "/api/v1/market/{condition_id}/candles",
|
|
337
|
+
operationId: "getMarketCandles",
|
|
338
|
+
auth: "bearer",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
method: "GET",
|
|
342
|
+
path: "/api/v1/suspicious-trades",
|
|
343
|
+
operationId: "listSuspiciousTrades",
|
|
344
|
+
auth: "bearer",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
method: "GET",
|
|
348
|
+
path: "/api/v1/suspicious-trades/{id}",
|
|
349
|
+
operationId: "getSuspiciousTrade",
|
|
350
|
+
auth: "bearer",
|
|
351
|
+
},
|
|
352
|
+
// Deprecated aliases (#16301). Both paths stay live with no retirement
|
|
353
|
+
// date; responses carry Deprecation and a Link rel="successor-version".
|
|
354
|
+
// `/api/v1/insider-radar/{id}` also keeps answering `object: "radar_flag"`,
|
|
355
|
+
// so these entries are what an existing integration must keep exercising.
|
|
356
|
+
{
|
|
357
|
+
method: "GET",
|
|
358
|
+
path: "/api/v1/insider-radar",
|
|
359
|
+
operationId: "listInsiderRadar",
|
|
360
|
+
auth: "bearer",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
method: "GET",
|
|
364
|
+
path: "/api/v1/insider-radar/{id}",
|
|
365
|
+
operationId: "getInsiderRadarFlag",
|
|
366
|
+
auth: "bearer",
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
method: "GET",
|
|
370
|
+
path: "/api/v1/events/feed/since",
|
|
371
|
+
operationId: "getEventReplaySince",
|
|
372
|
+
auth: "bearer",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
method: "GET",
|
|
376
|
+
path: "/api/v1/stream",
|
|
377
|
+
operationId: "getStream",
|
|
378
|
+
auth: "bearer",
|
|
379
|
+
kind: "sse",
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
method: "GET",
|
|
383
|
+
path: "/api/v1/webhooks",
|
|
384
|
+
operationId: "listWebhooks",
|
|
385
|
+
auth: "bearer",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
method: "POST",
|
|
389
|
+
path: "/api/v1/webhooks",
|
|
390
|
+
operationId: "createWebhook",
|
|
391
|
+
auth: "bearer",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
method: "GET",
|
|
395
|
+
path: "/api/v1/webhooks/{id}",
|
|
396
|
+
operationId: "getWebhook",
|
|
397
|
+
auth: "bearer",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
method: "PATCH",
|
|
401
|
+
path: "/api/v1/webhooks/{id}",
|
|
402
|
+
operationId: "updateWebhook",
|
|
403
|
+
auth: "bearer",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
method: "DELETE",
|
|
407
|
+
path: "/api/v1/webhooks/{id}",
|
|
408
|
+
operationId: "deleteWebhook",
|
|
409
|
+
auth: "bearer",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
method: "POST",
|
|
413
|
+
path: "/api/v1/webhooks/{id}/verify",
|
|
414
|
+
operationId: "verifyWebhook",
|
|
415
|
+
auth: "bearer",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
method: "POST",
|
|
419
|
+
path: "/api/v1/webhooks/{id}/rotate-secret",
|
|
420
|
+
operationId: "rotateWebhookSecret",
|
|
421
|
+
auth: "bearer",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
method: "POST",
|
|
425
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/prepare",
|
|
426
|
+
operationId: "prepareWebhookSecret",
|
|
427
|
+
auth: "bearer",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
method: "POST",
|
|
431
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/activate",
|
|
432
|
+
operationId: "activateWebhookSecret",
|
|
433
|
+
auth: "bearer",
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
method: "POST",
|
|
437
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/retire",
|
|
438
|
+
operationId: "retireWebhookSecret",
|
|
439
|
+
auth: "bearer",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
method: "GET",
|
|
443
|
+
path: "/api/v1/webhooks/events",
|
|
444
|
+
operationId: "listWebhookEvents",
|
|
445
|
+
auth: "bearer",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
method: "GET",
|
|
449
|
+
path: "/api/v1/webhooks/{id}/deliveries",
|
|
450
|
+
operationId: "listWebhookDeliveries",
|
|
451
|
+
auth: "bearer",
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
method: "POST",
|
|
455
|
+
path: "/api/v1/webhooks/{id}/deliveries/{delivery_id}/redeliver",
|
|
456
|
+
operationId: "redeliverWebhookDelivery",
|
|
457
|
+
auth: "bearer",
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
method: "GET",
|
|
461
|
+
path: "/api/v1/health",
|
|
462
|
+
operationId: "getHealth",
|
|
463
|
+
auth: "none",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
method: "POST",
|
|
467
|
+
path: "/api/v1/mcp",
|
|
468
|
+
operationId: "createMcpJsonRpcResponse",
|
|
469
|
+
auth: "bearer",
|
|
470
|
+
kind: "jsonrpc",
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
method: "GET",
|
|
474
|
+
path: "/api/v1/reports",
|
|
475
|
+
operationId: "getReports",
|
|
476
|
+
auth: "bearer",
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
method: "GET",
|
|
480
|
+
path: "/api/v1/reports/daily",
|
|
481
|
+
operationId: "getDailyReportSnapshot",
|
|
482
|
+
auth: "bearer",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
method: "GET",
|
|
486
|
+
path: "/api/v1/reports/weekly",
|
|
487
|
+
operationId: "getWeeklyReportSnapshot",
|
|
488
|
+
auth: "bearer",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
method: "GET",
|
|
492
|
+
path: "/api/v1/reports/monthly",
|
|
493
|
+
operationId: "getMonthlyReportSnapshot",
|
|
494
|
+
auth: "bearer",
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
method: "GET",
|
|
498
|
+
path: "/api/v1/trader/{address}/export",
|
|
499
|
+
operationId: "getTraderExportSnapshot",
|
|
500
|
+
auth: "bearer",
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
method: "GET",
|
|
504
|
+
path: "/api/v1/usage",
|
|
505
|
+
operationId: "getUsage",
|
|
506
|
+
auth: "bearer",
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
method: "GET",
|
|
510
|
+
path: "/api/v1/pick-of-the-day",
|
|
511
|
+
operationId: "getPickOfTheDay",
|
|
512
|
+
auth: "bearer",
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
method: "GET",
|
|
516
|
+
path: "/api/v1/pick-of-the-day/archive",
|
|
517
|
+
operationId: "getPickOfTheDayArchive",
|
|
518
|
+
auth: "bearer",
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
method: "GET",
|
|
522
|
+
path: "/api/v1/pick-of-the-day/ledger",
|
|
523
|
+
operationId: "getPickOfTheDayLedger",
|
|
524
|
+
// Keyless since #16459: the commitment ledger is published to be
|
|
525
|
+
// republished, so a client can read it with no key configured.
|
|
526
|
+
auth: "none",
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
method: "POST",
|
|
530
|
+
path: "/api/v1/agents/register",
|
|
531
|
+
operationId: "registerAgent",
|
|
532
|
+
// `security: []` in the document: minting a sandbox key is the one write
|
|
533
|
+
// that must work before a caller has any credential.
|
|
534
|
+
auth: "none",
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
/**
|
|
538
|
+
* Published operations whose success is a redirect, with the method that
|
|
539
|
+
* follows it (#16137). These have no 2xx body, so they are absent from
|
|
540
|
+
* `API_CLIENT_OPERATIONS` and from the generated `schema.ts`;
|
|
541
|
+
* `scripts/check-sdk-openapi-drift.mjs` requires every redirect-only spec
|
|
542
|
+
* operation to be declared here, so one cannot go missing in silence again.
|
|
543
|
+
*/
|
|
544
|
+
export const REDIRECT_OPERATIONS = [
|
|
545
|
+
{
|
|
546
|
+
method: "GET",
|
|
547
|
+
path: "/api/v1/trader/{address}/export/download",
|
|
548
|
+
operationId: "downloadTraderExport",
|
|
549
|
+
auth: "bearer",
|
|
550
|
+
status: 302,
|
|
551
|
+
handledBy: "getTraderExportDownloadUrl() resolves the Location; downloadTraderExport() then fetches the object with no Authorization header.",
|
|
552
|
+
},
|
|
553
|
+
{ method: "GET", path: "/api/v1/datasets/whale-trades/{job_id}/download", operationId: "downloadWhaleDataset", auth: "bearer", status: 302, handledBy: "getWhaleDatasetDownloadUrl() resolves Location; fetch it without Authorization and verify the manifest hashes." },
|
|
554
|
+
{
|
|
555
|
+
method: "GET",
|
|
556
|
+
path: "/api/v1/openapi.json",
|
|
557
|
+
operationId: "redirectApiOpenapiSpec",
|
|
558
|
+
auth: "none",
|
|
559
|
+
status: 307,
|
|
560
|
+
handledBy: "No method: the document is public and static, so fetch the URL directly and let your runtime follow the redirect. This package ships the same contract as generated types in schema.ts.",
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
/**
|
|
564
|
+
* Published operations this client deliberately does not wrap, with the
|
|
565
|
+
* reason. The drift check requires every remaining spec operation to be
|
|
566
|
+
* accounted for here, so "unsupported" is always a stated decision.
|
|
567
|
+
*/
|
|
568
|
+
export const UNSUPPORTED_OPERATIONS = [
|
|
569
|
+
{
|
|
570
|
+
method: "GET",
|
|
571
|
+
path: "/api/v1/mcp",
|
|
572
|
+
operationId: "openMcpEventStream",
|
|
573
|
+
reason: "The MCP endpoint offers no server-to-client stream: GET answers 405 by design, so a client that reconnects to it would loop (#16354). Post JSON-RPC with mcp() instead.",
|
|
574
|
+
},
|
|
575
|
+
];
|
|
576
|
+
/**
|
|
577
|
+
* The writes the API replays under `Idempotency-Key`: a second request with
|
|
578
|
+
* the same key and the same body returns the first result instead of
|
|
579
|
+
* repeating the write. Source of truth: the operations declaring the
|
|
580
|
+
* `Idempotency-Key` header parameter in `web/public/api/v1/openapi.json`,
|
|
581
|
+
* pinned by `scripts/check-sdk-openapi-drift.mjs` (#16182). Only these are
|
|
582
|
+
* retried on a transport or 5xx failure, and only when a key is set; a key on
|
|
583
|
+
* any other operation is refused before the request, since the server would
|
|
584
|
+
* ignore it and a retry could repeat the side effect (`verifyWebhook` sends
|
|
585
|
+
* a challenge to your URL each time; `submitTraderExport` starts a job).
|
|
586
|
+
*/
|
|
587
|
+
export const IDEMPOTENT_WRITE_OPERATIONS = [
|
|
588
|
+
"createWebhook",
|
|
589
|
+
"updateWebhook",
|
|
590
|
+
"deleteWebhook",
|
|
591
|
+
"rotateWebhookSecret",
|
|
592
|
+
"prepareWebhookSecret",
|
|
593
|
+
"activateWebhookSecret",
|
|
594
|
+
"retireWebhookSecret",
|
|
595
|
+
"redeliverWebhookDelivery",
|
|
596
|
+
];
|
|
597
|
+
/**
|
|
598
|
+
* POST operations that read and never write (#16182): the batch lookups.
|
|
599
|
+
* `POST /api/v1/traders/batch` and `POST /api/v1/markets/flow/batch` (with its
|
|
600
|
+
* deprecated alias `POST /api/v1/markets/intel/batch`) resolve
|
|
601
|
+
* their inputs and store nothing (`backend/src/api_v1/handlers/batch.rs`), so
|
|
602
|
+
* a repeat cannot duplicate a side effect. Each attempt is one request
|
|
603
|
+
* against the account's quota and one reservation of batch item units, which
|
|
604
|
+
* is exactly what a retried GET costs, so they are retried like a GET. Every
|
|
605
|
+
* other POST, PATCH or DELETE is retried only as a keyed write above.
|
|
606
|
+
*/
|
|
607
|
+
export const READ_ONLY_POST_OPERATIONS = [
|
|
608
|
+
"batchGetTraders",
|
|
609
|
+
"batchGetMarketFlow",
|
|
610
|
+
"batchGetMarketIntel",
|
|
611
|
+
];
|
|
612
|
+
/**
|
|
613
|
+
* Writes whose repeat converges on the state the first one reached, so a
|
|
614
|
+
* retry cannot repeat a side effect and needs no `Idempotency-Key` (#16254).
|
|
615
|
+
* `POST /api/v1/trader/{address}/export/cancel` moves a job at most once and
|
|
616
|
+
* answers its current state every time: a second cancel of a cancelled job
|
|
617
|
+
* returns it unchanged, and a cancel that lost the race to a finished file
|
|
618
|
+
* returns the ready job. They are retried like a read; the API does not read
|
|
619
|
+
* an `Idempotency-Key` on them, so one is refused like on any other write.
|
|
620
|
+
*/
|
|
621
|
+
export const CONVERGENT_WRITE_OPERATIONS = [
|
|
622
|
+
"cancelWhaleDataset",
|
|
623
|
+
"cancelTraderExport",
|
|
624
|
+
];
|
|
625
|
+
const idempotentWrites = new Set(IDEMPOTENT_WRITE_OPERATIONS);
|
|
626
|
+
const readOnlyPosts = new Set(READ_ONLY_POST_OPERATIONS);
|
|
627
|
+
const convergentWrites = new Set(CONVERGENT_WRITE_OPERATIONS);
|
|
628
|
+
/** The retry class of an operation; see `RetryEligibility`. */
|
|
629
|
+
export function retryEligibility(operation) {
|
|
630
|
+
if (operation.method === "GET" ||
|
|
631
|
+
readOnlyPosts.has(operation.operationId) ||
|
|
632
|
+
convergentWrites.has(operation.operationId)) {
|
|
633
|
+
return "read";
|
|
634
|
+
}
|
|
635
|
+
return idempotentWrites.has(operation.operationId) ? "keyed" : "never";
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Trader skill grade. Source of truth:
|
|
639
|
+
* `components.schemas.Trader.properties.grade.enum` (S highest, F lowest).
|
|
640
|
+
*/
|
|
641
|
+
export const GRADES = ["S", "A", "B", "C", "D", "F"];
|
|
642
|
+
/** Default production API base used when `baseUrl` is omitted. */
|
|
643
|
+
export const DEFAULT_BASE_URL = "https://api.0xinsider.com";
|
|
644
|
+
/**
|
|
645
|
+
* The sandbox server, the second `servers` entry of the OpenAPI document: no
|
|
646
|
+
* credential, no production data, every documented operation answered with
|
|
647
|
+
* its example or a deterministic sample, `?sandbox_status=<code>` for a
|
|
648
|
+
* documented error, and `X-Oxi-Sandbox: true` on every response. The two
|
|
649
|
+
* Markdown documents answer `200 text/markdown` there and the export download
|
|
650
|
+
* answers its `302` to a sample file the sandbox serves itself. `GET
|
|
651
|
+
* /api/v1/stream` is the one operation it does not simulate and answers 400,
|
|
652
|
+
* because an SSE stream is a live connection rather than a body.
|
|
653
|
+
*/
|
|
654
|
+
export const SANDBOX_BASE_URL = "https://0xinsider.com/sandbox";
|
|
655
|
+
/** A live secret key starts with this; the sandbox never needs one. */
|
|
656
|
+
const LIVE_KEY_PREFIX = "oxi_sk_live_";
|
|
657
|
+
/** Default per-request deadline; see `ApiClientOptions.timeoutMs`. */
|
|
658
|
+
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
659
|
+
/** Default retry budget; see `ApiClientOptions.maxRetries`. */
|
|
660
|
+
export const DEFAULT_MAX_RETRIES = 2;
|
|
661
|
+
/** Statuses a retry can fix: rate limited, or a gateway/availability failure. */
|
|
662
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
663
|
+
const RETRY_BASE_DELAY_MS = 500;
|
|
664
|
+
const RETRY_MAX_BACKOFF_MS = 8_000;
|
|
665
|
+
const RETRY_JITTER_MS = 250;
|
|
666
|
+
function assertRetryCount(value, source) {
|
|
667
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
668
|
+
throw new Error(`${source} must be a non-negative integer, got ${String(value)}`);
|
|
669
|
+
}
|
|
670
|
+
return value;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Milliseconds to wait before retry `attempt` (0-based), or `null` when the
|
|
674
|
+
* server asked for a wait a retry loop should not hold. The ceiling check
|
|
675
|
+
* runs on the server's number before jitter is added, so the value handed to
|
|
676
|
+
* the timer is at most `RETRY_AFTER_CEILING_MS + RETRY_JITTER_MS`, far inside
|
|
677
|
+
* the timer range (`MAX_TIMER_DELAY_MS`, `retry.ts`).
|
|
678
|
+
*/
|
|
679
|
+
function retryDelayMs(attempt, retryAfter) {
|
|
680
|
+
if (retryAfter !== null) {
|
|
681
|
+
const requested = retryAfter * 1000;
|
|
682
|
+
if (requested > RETRY_AFTER_CEILING_MS)
|
|
683
|
+
return null;
|
|
684
|
+
return requested + Math.random() * RETRY_JITTER_MS;
|
|
685
|
+
}
|
|
686
|
+
const ceiling = Math.min(RETRY_MAX_BACKOFF_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
687
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
688
|
+
}
|
|
689
|
+
function isLoopbackHost(hostname) {
|
|
690
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
691
|
+
return (host === "localhost" ||
|
|
692
|
+
host === "::1" ||
|
|
693
|
+
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host));
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* The API key travels as a bearer header on every request, so the base URL
|
|
697
|
+
* decides who receives it. Only `https:` is accepted, with `http:` allowed for
|
|
698
|
+
* a loopback host (a local backend on `localhost` or `127.0.0.1`). Anything
|
|
699
|
+
* else throws from the constructor, before any request is sent (#11115).
|
|
700
|
+
*/
|
|
701
|
+
export function assertTrustedBaseUrl(baseUrl) {
|
|
702
|
+
let url;
|
|
703
|
+
try {
|
|
704
|
+
url = new URL(baseUrl);
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
throw new Error(`0xinsider API baseUrl is not a valid URL: ${baseUrl}`);
|
|
708
|
+
}
|
|
709
|
+
if (url.protocol === "https:")
|
|
710
|
+
return url;
|
|
711
|
+
if (url.protocol === "http:" && isLoopbackHost(url.hostname))
|
|
712
|
+
return url;
|
|
713
|
+
throw new Error(`Refusing to send the API key to ${url.origin}: baseUrl must use https: (http: is accepted only for a loopback host such as localhost or 127.0.0.1).`);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* The server URL as this client stores it: origin plus path, trailing slashes
|
|
717
|
+
* trimmed, and a trailing `/api/v1` removed once. Every operation path starts
|
|
718
|
+
* with `/api/v1/`, so a base that already ends in it would double the prefix;
|
|
719
|
+
* before #16138 such a base worked only because the path was discarded, and
|
|
720
|
+
* this keeps it working.
|
|
721
|
+
*/
|
|
722
|
+
function normalizeBaseUrl(base) {
|
|
723
|
+
const trimmed = base.toString().replace(/\/+$/, "");
|
|
724
|
+
return trimmed.replace(/\/api\/v1$/, "");
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Join an `/api/v1/...` path onto the server URL, keeping the server's own
|
|
728
|
+
* path (`/sandbox`) exactly once. REST (`buildUrl`) and the SSE stream
|
|
729
|
+
* (`stream.ts`) both resolve through this, so the two cannot disagree about
|
|
730
|
+
* where a path-bearing base points (#16138: `new URL("/api/v1/...", base)`
|
|
731
|
+
* resolved against the origin and dropped `/sandbox`).
|
|
732
|
+
*/
|
|
733
|
+
export function resolveApiUrl(baseUrl, path) {
|
|
734
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
735
|
+
return new URL(`${baseUrl}${suffix}`);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* One signal that aborts on the deadline OR when the caller's signal aborts.
|
|
739
|
+
* Uses native composition where available. The manual fallback removes both
|
|
740
|
+
* source listeners when either source aborts.
|
|
741
|
+
*/
|
|
742
|
+
export function composeSignals(timeoutMs, caller) {
|
|
743
|
+
return composeRequestSignal(timeoutMs, caller).signal;
|
|
744
|
+
}
|
|
745
|
+
/** Keep cancellation live through body consumption, then release its listeners. */
|
|
746
|
+
function composeRequestSignal(timeoutMs, caller) {
|
|
747
|
+
const timeout = timeoutMs === null ? undefined : AbortSignal.timeout(timeoutMs);
|
|
748
|
+
if (!timeout)
|
|
749
|
+
return { signal: caller };
|
|
750
|
+
if (!caller)
|
|
751
|
+
return { signal: timeout, timeout };
|
|
752
|
+
if (typeof AbortSignal.any === "function") {
|
|
753
|
+
return { signal: AbortSignal.any([caller, timeout]), timeout };
|
|
754
|
+
}
|
|
755
|
+
const controller = new AbortController();
|
|
756
|
+
const dispose = () => {
|
|
757
|
+
caller.removeEventListener("abort", onCallerAbort);
|
|
758
|
+
timeout.removeEventListener("abort", onTimeoutAbort);
|
|
759
|
+
};
|
|
760
|
+
const onCallerAbort = () => {
|
|
761
|
+
dispose();
|
|
762
|
+
controller.abort(caller.reason);
|
|
763
|
+
};
|
|
764
|
+
const onTimeoutAbort = () => {
|
|
765
|
+
dispose();
|
|
766
|
+
controller.abort(timeout.reason);
|
|
767
|
+
};
|
|
768
|
+
if (caller.aborted) {
|
|
769
|
+
onCallerAbort();
|
|
770
|
+
}
|
|
771
|
+
else if (timeout.aborted) {
|
|
772
|
+
onTimeoutAbort();
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
caller.addEventListener("abort", onCallerAbort, { once: true });
|
|
776
|
+
timeout.addEventListener("abort", onTimeoutAbort, { once: true });
|
|
777
|
+
}
|
|
778
|
+
return { signal: controller.signal, timeout, dispose };
|
|
779
|
+
}
|
|
780
|
+
const EXPORT_BODY_CLEANUP_TIMEOUT_MS = 2_000;
|
|
781
|
+
/** A failed transfer keeps its primary error and an observable cleanup cause. */
|
|
782
|
+
function exportCleanupCause(primary, cleanup) {
|
|
783
|
+
if ((typeof primary === "object" && primary !== null) || typeof primary === "function") {
|
|
784
|
+
try {
|
|
785
|
+
const previousCause = primary.cause;
|
|
786
|
+
Object.defineProperty(primary, "cause", {
|
|
787
|
+
configurable: true,
|
|
788
|
+
writable: true,
|
|
789
|
+
value: previousCause === undefined ? cleanup : new AggregateError([previousCause, cleanup], "The original cause and export body cleanup failure"),
|
|
790
|
+
});
|
|
791
|
+
return primary;
|
|
792
|
+
}
|
|
793
|
+
catch (annotationError) {
|
|
794
|
+
return new AggregateError([primary, cleanup, annotationError], "Export failed and its body cleanup diagnostic could not be attached", { cause: primary });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return new AggregateError([primary, cleanup], "Export failed and its body cleanup also failed", { cause: primary });
|
|
798
|
+
}
|
|
799
|
+
/** Observe cancellation without buffering an unbounded object-store error body. */
|
|
800
|
+
async function releaseExportBody(cancel, primary) {
|
|
801
|
+
if (!cancel)
|
|
802
|
+
return primary;
|
|
803
|
+
let timer;
|
|
804
|
+
const cancelled = Promise.resolve().then(() => cancel(primary)).then(() => undefined, (cause) => new Error("Export body cancellation failed", { cause }));
|
|
805
|
+
const deadline = new Promise((resolve) => {
|
|
806
|
+
timer = setTimeout(() => resolve(new Error(`Export body cleanup did not settle within ${String(EXPORT_BODY_CLEANUP_TIMEOUT_MS)} ms; completion is unknown`)), EXPORT_BODY_CLEANUP_TIMEOUT_MS);
|
|
807
|
+
});
|
|
808
|
+
try {
|
|
809
|
+
const failure = await Promise.race([cancelled, deadline]);
|
|
810
|
+
return failure ? exportCleanupCause(primary, failure) : primary;
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
clearTimeout(timer);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/** Own only the latest body/reader until the response is handed to the caller. */
|
|
817
|
+
class ExportBodyOwner {
|
|
818
|
+
cancel;
|
|
819
|
+
response(response) {
|
|
820
|
+
this.cancel = (reason) => response.body?.cancel(reason) ?? Promise.resolve();
|
|
821
|
+
}
|
|
822
|
+
reader(reader, dispose) {
|
|
823
|
+
let finished = false;
|
|
824
|
+
let cancellation;
|
|
825
|
+
const finish = () => {
|
|
826
|
+
if (finished)
|
|
827
|
+
return;
|
|
828
|
+
finished = true;
|
|
829
|
+
dispose();
|
|
830
|
+
reader.releaseLock();
|
|
831
|
+
};
|
|
832
|
+
const cancel = (reason) => {
|
|
833
|
+
if (cancellation)
|
|
834
|
+
return cancellation;
|
|
835
|
+
if (finished)
|
|
836
|
+
return Promise.resolve();
|
|
837
|
+
finished = true;
|
|
838
|
+
dispose();
|
|
839
|
+
cancellation = Promise.resolve().then(() => reader.cancel(reason)).finally(() => reader.releaseLock());
|
|
840
|
+
return cancellation;
|
|
841
|
+
};
|
|
842
|
+
// Record the reader before hash or Response construction can throw.
|
|
843
|
+
this.cancel = cancel;
|
|
844
|
+
return { reader, finish, cancel, active: () => !finished };
|
|
845
|
+
}
|
|
846
|
+
transfer() {
|
|
847
|
+
this.cancel = undefined;
|
|
848
|
+
}
|
|
849
|
+
release(primary) {
|
|
850
|
+
const cancel = this.cancel;
|
|
851
|
+
this.cancel = undefined;
|
|
852
|
+
return releaseExportBody(cancel, primary);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/** Hash decoded bytes with backpressure and finish the reader/listener owner. */
|
|
856
|
+
function managedExportBody(response, owner, dispose, needsDisposal, verification) {
|
|
857
|
+
if (!verification && !needsDisposal)
|
|
858
|
+
return response;
|
|
859
|
+
if (!response.body) {
|
|
860
|
+
if (verification)
|
|
861
|
+
throw new ExportIntegrityError(verification.jobId, verification.expectedSha256, null, verification.expectedSizeBytes, 0);
|
|
862
|
+
dispose();
|
|
863
|
+
return response;
|
|
864
|
+
}
|
|
865
|
+
const lease = owner.reader(response.body.getReader(), dispose);
|
|
866
|
+
const hasher = verification ? createHash("sha256") : undefined;
|
|
867
|
+
let actualSizeBytes = 0;
|
|
868
|
+
let consumerCancelled = false;
|
|
869
|
+
const integrityFailure = (cause) => verification
|
|
870
|
+
? new ExportIntegrityError(verification.jobId, verification.expectedSha256, null, verification.expectedSizeBytes, actualSizeBytes, cause) : cause;
|
|
871
|
+
const body = new ReadableStream({
|
|
872
|
+
async pull(controller) {
|
|
873
|
+
let chunk;
|
|
874
|
+
try {
|
|
875
|
+
chunk = await lease.reader.read();
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
if (consumerCancelled)
|
|
879
|
+
return;
|
|
880
|
+
// A rejected read is terminal; release the lock rather than cancel
|
|
881
|
+
// an already errored stream and relabel its original failure.
|
|
882
|
+
lease.finish();
|
|
883
|
+
controller.error(integrityFailure(error));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (consumerCancelled || !lease.active())
|
|
887
|
+
return;
|
|
888
|
+
try {
|
|
889
|
+
if (chunk.done) {
|
|
890
|
+
lease.finish();
|
|
891
|
+
const actualSha256 = hasher?.digest("hex");
|
|
892
|
+
if (verification && (actualSizeBytes !== verification.expectedSizeBytes ||
|
|
893
|
+
actualSha256 !== verification.expectedSha256)) {
|
|
894
|
+
controller.error(new ExportIntegrityError(verification.jobId, verification.expectedSha256, actualSha256 ?? null, verification.expectedSizeBytes, actualSizeBytes));
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
controller.close();
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
hasher?.update(chunk.value);
|
|
902
|
+
actualSizeBytes += chunk.value.byteLength;
|
|
903
|
+
controller.enqueue(chunk.value);
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
const primary = await releaseExportBody(lease.cancel, error);
|
|
907
|
+
if (!consumerCancelled)
|
|
908
|
+
controller.error(integrityFailure(primary));
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
cancel(reason) {
|
|
912
|
+
consumerCancelled = true;
|
|
913
|
+
return lease.cancel(reason);
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
const result = new Response(body, {
|
|
917
|
+
status: response.status,
|
|
918
|
+
statusText: response.statusText,
|
|
919
|
+
headers: response.headers,
|
|
920
|
+
});
|
|
921
|
+
owner.response(result);
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
/** JSON-RPC methods `POST /api/v1/mcp` answers without a credential. */
|
|
925
|
+
function isKeylessMcpMethod(method) {
|
|
926
|
+
return (method === "initialize" ||
|
|
927
|
+
method === "ping" ||
|
|
928
|
+
method === "tools/list" ||
|
|
929
|
+
method.startsWith("notifications/"));
|
|
930
|
+
}
|
|
931
|
+
function isTimeoutAbort(error) {
|
|
932
|
+
return (typeof error === "object" &&
|
|
933
|
+
error !== null &&
|
|
934
|
+
"name" in error &&
|
|
935
|
+
error.name === "TimeoutError");
|
|
936
|
+
}
|
|
937
|
+
/** Which method reads each kind of success body, named in the error that refuses the wrong one. */
|
|
938
|
+
const READER_FOR_KIND = {
|
|
939
|
+
envelope: "call() or list()",
|
|
940
|
+
text: "text()",
|
|
941
|
+
jsonrpc: "mcp()",
|
|
942
|
+
sse: "streamFeed(), streamFeedResilient() or consumeStreamCheckpointed()",
|
|
943
|
+
};
|
|
944
|
+
// --- MCP JSON-RPC (#16137) ---
|
|
945
|
+
/** The MCP methods `POST /api/v1/mcp` accepts, from the operation's request body. */
|
|
946
|
+
export const MCP_METHODS = [
|
|
947
|
+
"initialize",
|
|
948
|
+
"notifications/initialized",
|
|
949
|
+
"notifications/cancelled",
|
|
950
|
+
"ping",
|
|
951
|
+
"tools/list",
|
|
952
|
+
"tools/call",
|
|
953
|
+
];
|
|
954
|
+
function isMcpJsonRpcResponse(body) {
|
|
955
|
+
if (typeof body !== "object" || body === null)
|
|
956
|
+
return false;
|
|
957
|
+
const candidate = body;
|
|
958
|
+
return (candidate.jsonrpc === "2.0" &&
|
|
959
|
+
(typeof candidate.id === "string" ||
|
|
960
|
+
typeof candidate.id === "number" ||
|
|
961
|
+
candidate.id === null));
|
|
962
|
+
}
|
|
963
|
+
// --- Export download (#16137) ---
|
|
964
|
+
/** The `downloadTraderExport` row of `REDIRECT_OPERATIONS`, as a request target. */
|
|
965
|
+
const exportDownloadOperation = {
|
|
966
|
+
method: REDIRECT_OPERATIONS[0].method,
|
|
967
|
+
path: REDIRECT_OPERATIONS[0].path,
|
|
968
|
+
operationId: REDIRECT_OPERATIONS[0].operationId,
|
|
969
|
+
auth: REDIRECT_OPERATIONS[0].auth,
|
|
970
|
+
};
|
|
971
|
+
/**
|
|
972
|
+
* Read a presigned URL's own expiry, and refuse a destination that would
|
|
973
|
+
* downgrade the transport. The backend already validates the redirect
|
|
974
|
+
* against its expected R2 origin; this is the client-side half, so a
|
|
975
|
+
* redirect can never move a download onto plain `http:`.
|
|
976
|
+
*/
|
|
977
|
+
function presignedTarget(location) {
|
|
978
|
+
let url;
|
|
979
|
+
try {
|
|
980
|
+
url = new URL(location);
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
throw new InvalidResponseError(302, `The export download redirect is not an absolute URL: ${location}`, null);
|
|
984
|
+
}
|
|
985
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
986
|
+
throw new InvalidResponseError(302, `Refusing to follow the export download redirect to ${url.origin}: it must use https: (http: is accepted only for a loopback host).`, null);
|
|
987
|
+
}
|
|
988
|
+
const signedAt = url.searchParams.get("X-Amz-Date");
|
|
989
|
+
const lifetime = url.searchParams.get("X-Amz-Expires");
|
|
990
|
+
const expiresAt = presignExpiry(signedAt, lifetime);
|
|
991
|
+
return { url: url.toString(), ...(expiresAt ? { expiresAt } : {}) };
|
|
992
|
+
}
|
|
993
|
+
/** `20260922T101500Z` plus `3600` seconds, as an ISO instant; `null` if either is unreadable. */
|
|
994
|
+
function presignExpiry(signedAt, lifetimeSeconds) {
|
|
995
|
+
if (!signedAt || !lifetimeSeconds)
|
|
996
|
+
return null;
|
|
997
|
+
const parsed = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(signedAt);
|
|
998
|
+
const seconds = Number(lifetimeSeconds);
|
|
999
|
+
if (!parsed || !Number.isFinite(seconds))
|
|
1000
|
+
return null;
|
|
1001
|
+
const [, year, month, day, hour, minute, second] = parsed;
|
|
1002
|
+
const signed = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
|
1003
|
+
return new Date(signed + seconds * 1000).toISOString();
|
|
1004
|
+
}
|
|
1005
|
+
/** The `filename="..."` of a `Content-Disposition`, or `null`. */
|
|
1006
|
+
function filenameFromDisposition(disposition) {
|
|
1007
|
+
if (!disposition)
|
|
1008
|
+
return null;
|
|
1009
|
+
const quoted = /filename\*?=(?:UTF-8'')?"([^"]+)"/i.exec(disposition);
|
|
1010
|
+
if (quoted)
|
|
1011
|
+
return decodeURIComponent(quoted[1]);
|
|
1012
|
+
const bare = /filename\*?=(?:UTF-8'')?([^;]+)/i.exec(disposition);
|
|
1013
|
+
return bare ? decodeURIComponent(bare[1].trim()) : null;
|
|
1014
|
+
}
|
|
1015
|
+
/** `GET /api/v1/leaderboard` -> parameters -> strategy (`web/public/api/v1/openapi.json`). */
|
|
1016
|
+
export const LEADERBOARD_STRATEGIES = [
|
|
1017
|
+
"accumulator",
|
|
1018
|
+
"algo_trader",
|
|
1019
|
+
"arbitrageur",
|
|
1020
|
+
"directional",
|
|
1021
|
+
"event_driven",
|
|
1022
|
+
"market_maker",
|
|
1023
|
+
"momentum",
|
|
1024
|
+
"scalper",
|
|
1025
|
+
"speculator",
|
|
1026
|
+
"swing_trader",
|
|
1027
|
+
];
|
|
1028
|
+
/**
|
|
1029
|
+
* Body-level `expand` values of `POST /api/v1/traders/batch`. The type is the
|
|
1030
|
+
* operation's own body (`OperationBody["batchGetTraders"]["expand"]`); the
|
|
1031
|
+
* const is the same list as a runtime value for a caller that iterates it.
|
|
1032
|
+
*/
|
|
1033
|
+
export const BATCH_TRADER_EXPANSIONS = [
|
|
1034
|
+
"strategy",
|
|
1035
|
+
"categories",
|
|
1036
|
+
"quant_metrics",
|
|
1037
|
+
"trust",
|
|
1038
|
+
];
|
|
1039
|
+
const operationsById = new Map(API_CLIENT_OPERATIONS.map((operation) => [operation.operationId, operation]));
|
|
1040
|
+
export class OxinsiderApiClient {
|
|
1041
|
+
baseUrl;
|
|
1042
|
+
apiKey;
|
|
1043
|
+
sandbox;
|
|
1044
|
+
fetchImpl;
|
|
1045
|
+
timeoutMs;
|
|
1046
|
+
maxRetries;
|
|
1047
|
+
/**
|
|
1048
|
+
* A client for the sandbox server (#16138): `SANDBOX_BASE_URL`, no
|
|
1049
|
+
* credential needed, example data only, never production data. The same
|
|
1050
|
+
* as `new OxinsiderApiClient({ ...options, sandbox: true })`; pass a
|
|
1051
|
+
* sandbox key (`oxi_sk_test_*`) as `apiKey` to have it checked.
|
|
1052
|
+
*
|
|
1053
|
+
* @example
|
|
1054
|
+
* const sandbox = OxinsiderApiClient.sandbox();
|
|
1055
|
+
* const board = await sandbox.listLeaderboard({ limit: 5 }); // no key
|
|
1056
|
+
* board.meta?.sandbox; // true
|
|
1057
|
+
*/
|
|
1058
|
+
static sandbox(options = {}) {
|
|
1059
|
+
return new OxinsiderApiClient({ ...options, sandbox: true });
|
|
1060
|
+
}
|
|
1061
|
+
constructor(options = {}) {
|
|
1062
|
+
this.sandbox = options.sandbox === true;
|
|
1063
|
+
const base = assertTrustedBaseUrl(options.baseUrl ?? (this.sandbox ? SANDBOX_BASE_URL : DEFAULT_BASE_URL));
|
|
1064
|
+
this.baseUrl = normalizeBaseUrl(base);
|
|
1065
|
+
if (this.sandbox && options.apiKey?.startsWith(LIVE_KEY_PREFIX)) {
|
|
1066
|
+
// The sandbox needs no credential and ignores a live key, so the only
|
|
1067
|
+
// effect of sending one is a secret on the wire for nothing.
|
|
1068
|
+
throw new Error("Refusing to send a live API key (oxi_sk_live_*) to the sandbox: omit apiKey, or pass a sandbox key (oxi_sk_test_*) from POST /api/v1/agents/register.");
|
|
1069
|
+
}
|
|
1070
|
+
this.apiKey = options.apiKey;
|
|
1071
|
+
this.timeoutMs =
|
|
1072
|
+
options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
1073
|
+
this.maxRetries = assertRetryCount(options.maxRetries ?? DEFAULT_MAX_RETRIES, "maxRetries");
|
|
1074
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1075
|
+
if (typeof fetchImpl !== "function") {
|
|
1076
|
+
throw new Error("No fetch implementation available. Pass `fetch` in OxinsiderApiClient options or run on Node 18+ / a fetch-capable runtime.");
|
|
1077
|
+
}
|
|
1078
|
+
// Bind so a passed-through `globalThis.fetch` keeps its receiver.
|
|
1079
|
+
this.fetchImpl = fetchImpl.bind(globalThis);
|
|
1080
|
+
}
|
|
1081
|
+
// The implementation signature is what both overloads narrow; `unknown`
|
|
1082
|
+
// here is the widest return both can refine, not a shape a caller sees.
|
|
1083
|
+
async call(operationId, options = {}) {
|
|
1084
|
+
const operation = this.resolveOperation(operationId, "envelope");
|
|
1085
|
+
return this.request(operation, options, async (response) => {
|
|
1086
|
+
if (response.status === 304) {
|
|
1087
|
+
return notModifiedResponse(response);
|
|
1088
|
+
}
|
|
1089
|
+
const envelope = await parseJson(response);
|
|
1090
|
+
if (isApiEnvelope(envelope)) {
|
|
1091
|
+
const etag = response.headers.get("etag");
|
|
1092
|
+
const sandbox = response.headers.get("x-oxi-sandbox") === "true";
|
|
1093
|
+
const queryIgnored = response.headers.get("x-query-ignored");
|
|
1094
|
+
const effectiveQuery = response.headers.get("x-effective-query");
|
|
1095
|
+
// Only ADD to a meta the server actually sent. The contract makes
|
|
1096
|
+
// `meta` required on every envelope, so synthesizing one from `?? {}`
|
|
1097
|
+
// would hand a caller a `ResponseMeta` missing its required fields --
|
|
1098
|
+
// which is what the hand-written type hid before #14278. `status` is
|
|
1099
|
+
// always lifted (#16137): `201` and `202` are load-bearing on
|
|
1100
|
+
// `registerAgent` and `submitTraderExport`, and a caller cannot read
|
|
1101
|
+
// them off a body that looks identical to a 200.
|
|
1102
|
+
if (envelope.meta) {
|
|
1103
|
+
envelope.meta = {
|
|
1104
|
+
...envelope.meta,
|
|
1105
|
+
status: response.status,
|
|
1106
|
+
...(etag ? { etag } : {}),
|
|
1107
|
+
...(sandbox ? { sandbox: true } : {}),
|
|
1108
|
+
...(queryIgnored ? { queryIgnored } : {}),
|
|
1109
|
+
...(effectiveQuery ? { effectiveQuery } : {}),
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return envelope;
|
|
1113
|
+
}
|
|
1114
|
+
throw new OxinsiderApiError(response.status, envelope);
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
async text(operationId, options = {}) {
|
|
1118
|
+
const operation = this.resolveOperation(operationId, "text");
|
|
1119
|
+
return this.request(operation, options, async (response) => readResponseText(response), { accept: "text/markdown, text/plain;q=0.9, */*;q=0.1" });
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Post one MCP JSON-RPC 2.0 message to `POST /api/v1/mcp` (#16137).
|
|
1123
|
+
*
|
|
1124
|
+
* The endpoint answers a JSON-RPC envelope, not the `{ object, data, meta }`
|
|
1125
|
+
* envelope every other operation uses, so `call()` refuses it. A request
|
|
1126
|
+
* (one carrying `id`) comes back as `status: 200` with `response` set; a
|
|
1127
|
+
* supported notification (no `id`) comes back as `status: 202` with
|
|
1128
|
+
* `response: null`, which is the empty body the server sends. A JSON-RPC
|
|
1129
|
+
* `error` member is a protocol-level failure and is RETURNED, not thrown:
|
|
1130
|
+
* only a non-2xx HTTP status throws, because an unknown tool is an answer
|
|
1131
|
+
* and a revoked credential is not.
|
|
1132
|
+
*
|
|
1133
|
+
* The request is never retried: `tools/call` can have an effect, and the
|
|
1134
|
+
* endpoint honours no `Idempotency-Key`.
|
|
1135
|
+
*
|
|
1136
|
+
* @example
|
|
1137
|
+
* const listed = await client.mcp({ method: "tools/list", id: 1 });
|
|
1138
|
+
* listed.response?.result;
|
|
1139
|
+
*/
|
|
1140
|
+
async mcp(request, options = {}) {
|
|
1141
|
+
// `initialize`, `ping`, `tools/list` and the notifications need no
|
|
1142
|
+
// credential (`backend/src/mcp/mod.rs`); only `tools/call` does, so the
|
|
1143
|
+
// local credential check is per method (#16686). A configured key is
|
|
1144
|
+
// still sent on every method.
|
|
1145
|
+
const operation = this.resolveOperation("createMcpJsonRpcResponse", "jsonrpc", !isKeylessMcpMethod(request.method));
|
|
1146
|
+
const { sessionId, protocolVersion, ...transportOptions } = options;
|
|
1147
|
+
const headers = { ...options.headers };
|
|
1148
|
+
if (sessionId !== undefined) {
|
|
1149
|
+
headers["mcp-session-id"] = sessionId;
|
|
1150
|
+
}
|
|
1151
|
+
if (protocolVersion !== undefined) {
|
|
1152
|
+
headers["mcp-protocol-version"] = protocolVersion;
|
|
1153
|
+
}
|
|
1154
|
+
// Set after the spread: an explicit `jsonrpc: undefined` would otherwise
|
|
1155
|
+
// drop the field and the server would answer 400 (#16686).
|
|
1156
|
+
const body = { ...request, jsonrpc: "2.0" };
|
|
1157
|
+
return this.request(operation, { ...transportOptions, headers, body, maxRetries: 0 }, async (response) => {
|
|
1158
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1159
|
+
const result = {
|
|
1160
|
+
status: response.status === 202 ? 202 : 200,
|
|
1161
|
+
response: null,
|
|
1162
|
+
...(sessionId ? { sessionId } : {}),
|
|
1163
|
+
};
|
|
1164
|
+
if (response.status === 202)
|
|
1165
|
+
return result;
|
|
1166
|
+
const parsed = await parseJson(response);
|
|
1167
|
+
if (!isMcpJsonRpcResponse(parsed)) {
|
|
1168
|
+
throw new InvalidResponseError(response.status, "POST /api/v1/mcp returned a body that is not a JSON-RPC 2.0 response: expected an object with jsonrpc \"2.0\" and an id.", parsed);
|
|
1169
|
+
}
|
|
1170
|
+
result.response = parsed;
|
|
1171
|
+
return result;
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Read the owner-authorized lifecycle and artifact manifest for one export
|
|
1176
|
+
* job. The manifest is immutable for the artifact; the download URL remains
|
|
1177
|
+
* temporary and is resolved separately.
|
|
1178
|
+
*/
|
|
1179
|
+
/** Submit a bounded cross-market snapshot; repeated identical live requests reuse it. */
|
|
1180
|
+
submitWhaleDataset(body, options = {}) {
|
|
1181
|
+
return this.call("submitWhaleDataset", { ...options, body });
|
|
1182
|
+
}
|
|
1183
|
+
getWhaleDatasetStatus(jobId, options = {}) {
|
|
1184
|
+
return this.call("getWhaleDatasetStatus", { ...options, path: { job_id: jobId } });
|
|
1185
|
+
}
|
|
1186
|
+
cancelWhaleDataset(jobId, options = {}) {
|
|
1187
|
+
return this.call("cancelWhaleDataset", { ...options, path: { job_id: jobId } });
|
|
1188
|
+
}
|
|
1189
|
+
/** Resolve only the signed URL. Never log it or forward API credentials to storage. */
|
|
1190
|
+
async getWhaleDatasetDownloadUrl(jobId, options = {}) {
|
|
1191
|
+
return this.request({ method: "GET", path: "/api/v1/datasets/whale-trades/{job_id}/download", operationId: "downloadWhaleDataset", auth: "bearer" }, { ...options, path: { job_id: jobId } }, async (response) => {
|
|
1192
|
+
const location = response.headers.get("location");
|
|
1193
|
+
if (!location)
|
|
1194
|
+
throw new InvalidResponseError(response.status, "Dataset redirect has no readable Location; use a server runtime.", null);
|
|
1195
|
+
return presignedTarget(location);
|
|
1196
|
+
}, { isSuccess: (response) => response.status === 302, redirect: "manual" });
|
|
1197
|
+
}
|
|
1198
|
+
/** Stream decoded NDJSON, validating its manifest hash when the body finishes. */
|
|
1199
|
+
async downloadWhaleDataset(jobId, options = {}) {
|
|
1200
|
+
const { downloadTimeoutMs, verifyChecksum = true, ...requestOptions } = options;
|
|
1201
|
+
const status = await this.getWhaleDatasetStatus(jobId, requestOptions);
|
|
1202
|
+
if (isApiNotModifiedResponse(status) || status.data.status !== "ready" || !status.data.artifact?.manifest) {
|
|
1203
|
+
throw new InvalidResponseError(200, "Dataset has no ready manifest; follow next_action on the status resource.", status);
|
|
1204
|
+
}
|
|
1205
|
+
const manifest = status.data.artifact.manifest;
|
|
1206
|
+
const target = await this.getWhaleDatasetDownloadUrl(jobId, requestOptions);
|
|
1207
|
+
return this.downloadExportObject(target, { downloadTimeoutMs, signal: options.signal }, verifyChecksum ? {
|
|
1208
|
+
jobId, expectedSha256: manifest.content_sha256, expectedSizeBytes: manifest.content_size_bytes,
|
|
1209
|
+
} : undefined, (status) => new InvalidResponseError(status, "Dataset signed URL failed; request a fresh download URL.", null));
|
|
1210
|
+
}
|
|
1211
|
+
getTraderExportStatus(address, jobId, options = {}) {
|
|
1212
|
+
return this.call("getTraderExportStatus", {
|
|
1213
|
+
...options,
|
|
1214
|
+
path: { address },
|
|
1215
|
+
query: { job_id: jobId },
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Resolve the presigned object URL behind `GET /api/v1/trader/{address}/export/download`
|
|
1220
|
+
* without downloading anything (#16137).
|
|
1221
|
+
*
|
|
1222
|
+
* The route answers `302` with a `Location`, which `call()` treats as an
|
|
1223
|
+
* error. This reads the redirect target and stops there, so a caller can
|
|
1224
|
+
* hand the URL to a download manager or a browser. The returned URL is a
|
|
1225
|
+
* bearer credential in itself: anyone holding it can read the file until it
|
|
1226
|
+
* expires, so do not log it.
|
|
1227
|
+
*
|
|
1228
|
+
* Browser caveat: `fetch` with `redirect: "manual"` yields an opaque
|
|
1229
|
+
* redirect whose `Location` no script can read. The method says so rather
|
|
1230
|
+
* than guessing a URL; run the download from a server runtime.
|
|
1231
|
+
*/
|
|
1232
|
+
async getTraderExportDownloadUrl(address, jobId, options = {}) {
|
|
1233
|
+
const operation = exportDownloadOperation;
|
|
1234
|
+
if (!this.apiKey && !this.sandbox) {
|
|
1235
|
+
throw new Error("downloadTraderExport requires an API key (oxi_sk_*)");
|
|
1236
|
+
}
|
|
1237
|
+
return this.request(operation, { ...options, path: { address }, query: { job_id: jobId } }, async (response) => {
|
|
1238
|
+
if (response.type === "opaqueredirect" || response.status === 0) {
|
|
1239
|
+
throw new InvalidResponseError(302, "This runtime hides redirect targets: fetch with redirect: \"manual\" returned an opaque redirect, so the presigned download URL cannot be read. Run the export download from a server runtime (Node 18+, Deno, Bun, a Worker).", null);
|
|
1240
|
+
}
|
|
1241
|
+
const location = response.headers.get("location");
|
|
1242
|
+
if (!location) {
|
|
1243
|
+
throw new InvalidResponseError(response.status, "GET /api/v1/trader/{address}/export/download answered a redirect with no Location header.", null);
|
|
1244
|
+
}
|
|
1245
|
+
return presignedTarget(location);
|
|
1246
|
+
}, {
|
|
1247
|
+
// The 302 IS the success here, so it must not enter the error path.
|
|
1248
|
+
isSuccess: (response) => response.status === 302 || response.type === "opaqueredirect",
|
|
1249
|
+
redirect: "manual",
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Download a finished export (#16137): resolve the `302`, then fetch the
|
|
1254
|
+
* object store directly.
|
|
1255
|
+
*
|
|
1256
|
+
* The second request carries NO headers at all, so the live API key never
|
|
1257
|
+
* reaches the object store; the presigned URL is its own credential. The
|
|
1258
|
+
* body is not buffered -- read `response.body` as a stream and write it
|
|
1259
|
+
* where it belongs.
|
|
1260
|
+
*
|
|
1261
|
+
* There is no default deadline on the object fetch, the way `streamFeed`
|
|
1262
|
+
* has none: a multi-gigabyte export would fail the 15-second REST default
|
|
1263
|
+
* halfway through. Pass `signal` to cancel it, or `downloadTimeoutMs` for
|
|
1264
|
+
* a deadline of your own. `timeoutMs` still bounds the redirect request.
|
|
1265
|
+
*
|
|
1266
|
+
* @example
|
|
1267
|
+
* const { response, filename } = await client.downloadTraderExport(address, jobId);
|
|
1268
|
+
* await pipeline(Readable.fromWeb(response.body), createWriteStream(filename ?? "export.json"));
|
|
1269
|
+
*/
|
|
1270
|
+
async downloadTraderExport(address, jobId, options = {}) {
|
|
1271
|
+
const { verifyChecksum = false, downloadTimeoutMs, ...requestOptions } = options;
|
|
1272
|
+
let manifest = null;
|
|
1273
|
+
if (verifyChecksum) {
|
|
1274
|
+
const status = await this.getTraderExportStatus(address, jobId, requestOptions);
|
|
1275
|
+
if (isApiNotModifiedResponse(status)) {
|
|
1276
|
+
throw new InvalidResponseError(304, `Export job ${String(jobId)} status was not modified, so its manifest could not be verified. Omit If-None-Match when verifyChecksum is enabled.`, status);
|
|
1277
|
+
}
|
|
1278
|
+
if (status.data.status !== "ready" || !status.data.artifact?.manifest) {
|
|
1279
|
+
throw new InvalidResponseError(200, `Export job ${String(jobId)} did not return a ready artifact manifest, so verifyChecksum cannot verify the download. Wait for status=ready or request a fresh export.`, status);
|
|
1280
|
+
}
|
|
1281
|
+
manifest = status.data.artifact.manifest;
|
|
1282
|
+
}
|
|
1283
|
+
const target = await this.getTraderExportDownloadUrl(address, jobId, requestOptions);
|
|
1284
|
+
return this.downloadExportObject(target, { downloadTimeoutMs, signal: options.signal }, manifest ? {
|
|
1285
|
+
jobId, expectedSha256: manifest.content_sha256, expectedSizeBytes: manifest.content_size_bytes,
|
|
1286
|
+
} : undefined, (status) => new InvalidResponseError(status, `The presigned export URL answered ${status}. Links last at most one hour and cannot outlive artifact retention. Request a fresh link with getTraderExportDownloadUrl; if the job is expired, submit a new export.`, null));
|
|
1287
|
+
}
|
|
1288
|
+
async downloadExportObject(target, options, verification, statusError) {
|
|
1289
|
+
const signal = composeRequestSignal(options.downloadTimeoutMs ?? null, options.signal);
|
|
1290
|
+
let disposed = false;
|
|
1291
|
+
const dispose = () => {
|
|
1292
|
+
if (disposed)
|
|
1293
|
+
return;
|
|
1294
|
+
disposed = true;
|
|
1295
|
+
signal.dispose?.();
|
|
1296
|
+
};
|
|
1297
|
+
const owner = new ExportBodyOwner();
|
|
1298
|
+
try {
|
|
1299
|
+
// A signed object URL authorizes itself; never forward API headers.
|
|
1300
|
+
let response = await this.fetchImpl(target.url, { method: "GET", signal: signal.signal });
|
|
1301
|
+
owner.response(response);
|
|
1302
|
+
if (!response.ok)
|
|
1303
|
+
throw statusError(response.status);
|
|
1304
|
+
response = managedExportBody(response, owner, dispose, signal.dispose !== undefined, verification);
|
|
1305
|
+
const contentLength = response.headers.get("content-length");
|
|
1306
|
+
const result = {
|
|
1307
|
+
...target,
|
|
1308
|
+
response,
|
|
1309
|
+
contentLength: contentLength === null ? null : Number.parseInt(contentLength, 10),
|
|
1310
|
+
contentType: response.headers.get("content-type"),
|
|
1311
|
+
filename: filenameFromDisposition(response.headers.get("content-disposition")),
|
|
1312
|
+
integrity: verification ? {
|
|
1313
|
+
algorithm: "sha256", expectedSha256: verification.expectedSha256,
|
|
1314
|
+
expectedSizeBytes: verification.expectedSizeBytes,
|
|
1315
|
+
} : null,
|
|
1316
|
+
};
|
|
1317
|
+
owner.transfer();
|
|
1318
|
+
return result;
|
|
1319
|
+
}
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
try {
|
|
1322
|
+
throw await owner.release(error);
|
|
1323
|
+
}
|
|
1324
|
+
finally {
|
|
1325
|
+
dispose();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Look up an operation and refuse it when the caller reached for the wrong
|
|
1331
|
+
* reader (#16137): `call()` on a Markdown route used to fail as a bad 200.
|
|
1332
|
+
*/
|
|
1333
|
+
resolveOperation(operationId, expected, requireCredential = true) {
|
|
1334
|
+
const operation = operationsById.get(operationId);
|
|
1335
|
+
if (!operation) {
|
|
1336
|
+
throw new Error(`Unknown 0xinsider API operation: ${operationId}`);
|
|
1337
|
+
}
|
|
1338
|
+
const kind = operation.kind ?? "envelope";
|
|
1339
|
+
if (kind !== expected) {
|
|
1340
|
+
throw new Error(`${operationId} answers a "${kind}" body, not "${expected}": use ${READER_FOR_KIND[kind]}.`);
|
|
1341
|
+
}
|
|
1342
|
+
// The sandbox answers every operation without a credential; production
|
|
1343
|
+
// does not, and the local check saves a round trip that can only be 401.
|
|
1344
|
+
if (requireCredential && operation.auth === "bearer" && !this.apiKey && !this.sandbox) {
|
|
1345
|
+
throw new Error(`${operationId} requires an API key (oxi_sk_*)`);
|
|
1346
|
+
}
|
|
1347
|
+
return operation;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* One request pipeline for every transport: URL and headers, the retry
|
|
1351
|
+
* policy, one deadline shared by every attempt, the typed error hierarchy,
|
|
1352
|
+
* and cancellation held live through body consumption. `consume` runs
|
|
1353
|
+
* inside that window, so a slow body is still covered by the deadline and
|
|
1354
|
+
* by the caller's `signal`.
|
|
1355
|
+
*/
|
|
1356
|
+
async request(operation, options, consume, transport = {}) {
|
|
1357
|
+
const operationId = operation.operationId;
|
|
1358
|
+
const timeoutMs = options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs;
|
|
1359
|
+
const maxRetries = options.maxRetries === undefined
|
|
1360
|
+
? this.maxRetries
|
|
1361
|
+
: assertRetryCount(options.maxRetries, "maxRetries");
|
|
1362
|
+
const url = this.buildUrl(operation, options);
|
|
1363
|
+
const headers = this.buildHeaders(operation, options, transport.accept);
|
|
1364
|
+
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
1365
|
+
const isSuccess = transport.isSuccess ??
|
|
1366
|
+
((response) => response.ok || response.status === 304);
|
|
1367
|
+
// A key the server does not read is a false promise of safety: the
|
|
1368
|
+
// request would be retried as though replayable while the route repeats
|
|
1369
|
+
// its effect (#16182). Refuse it here, before anything is sent.
|
|
1370
|
+
const eligibility = retryEligibility(operation);
|
|
1371
|
+
const keyed = headers.get("idempotency-key") !== null;
|
|
1372
|
+
if (keyed && eligibility !== "keyed") {
|
|
1373
|
+
throw new Error(`${operationId} does not honour Idempotency-Key; the API replays only ${IDEMPOTENT_WRITE_OPERATIONS.join(", ")}. Remove idempotencyKey: a retry of this request could repeat its side effect.`);
|
|
1374
|
+
}
|
|
1375
|
+
// Replaying a write without a key could repeat it (a second webhook
|
|
1376
|
+
// endpoint, say), so only a read or a keyed write is retried (#14281).
|
|
1377
|
+
// `headers` and `body` are built once, above, so every attempt carries
|
|
1378
|
+
// the same key and the same bytes.
|
|
1379
|
+
const retryable = eligibility === "read" || (eligibility === "keyed" && keyed);
|
|
1380
|
+
// One deadline for every attempt: a retry that cannot finish before it is
|
|
1381
|
+
// not started, so the caller sees the real failure, not a timeout.
|
|
1382
|
+
const deadlineAt = timeoutMs === null ? null : Date.now() + timeoutMs;
|
|
1383
|
+
const retryDelayWithinBudget = (attempt, retryAfter) => {
|
|
1384
|
+
if (!retryable || attempt >= maxRetries)
|
|
1385
|
+
return null;
|
|
1386
|
+
const delay = retryDelayMs(attempt, retryAfter);
|
|
1387
|
+
if (delay === null)
|
|
1388
|
+
return null;
|
|
1389
|
+
if (deadlineAt !== null && Date.now() + delay >= deadlineAt)
|
|
1390
|
+
return null;
|
|
1391
|
+
return delay;
|
|
1392
|
+
};
|
|
1393
|
+
const requestSignal = composeRequestSignal(timeoutMs, options.signal);
|
|
1394
|
+
try {
|
|
1395
|
+
let response;
|
|
1396
|
+
for (let attempt = 0;; attempt += 1) {
|
|
1397
|
+
try {
|
|
1398
|
+
requestSignal.signal?.throwIfAborted();
|
|
1399
|
+
response = await this.fetchImpl(url, {
|
|
1400
|
+
method: operation.method,
|
|
1401
|
+
headers,
|
|
1402
|
+
body,
|
|
1403
|
+
signal: requestSignal.signal,
|
|
1404
|
+
...(transport.redirect ? { redirect: transport.redirect } : {}),
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
catch (error) {
|
|
1408
|
+
// A timeout or caller abort is final; the handler below maps it.
|
|
1409
|
+
if (requestSignal.signal?.aborted || isTimeoutAbort(error))
|
|
1410
|
+
throw error;
|
|
1411
|
+
const delay = retryDelayWithinBudget(attempt, null);
|
|
1412
|
+
if (delay === null)
|
|
1413
|
+
throw error;
|
|
1414
|
+
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (isSuccess(response))
|
|
1418
|
+
break;
|
|
1419
|
+
const errorBody = await parseJson(response);
|
|
1420
|
+
const retryAfter = retryAfterSeconds(response);
|
|
1421
|
+
const delay = RETRYABLE_STATUSES.has(response.status)
|
|
1422
|
+
? retryDelayWithinBudget(attempt, retryAfter)
|
|
1423
|
+
: null;
|
|
1424
|
+
if (delay === null) {
|
|
1425
|
+
throw errorFromResponse(response.status, errorBody, retryAfter, {
|
|
1426
|
+
requestId: response.headers.get("x-request-id"),
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
1430
|
+
}
|
|
1431
|
+
return await consume(response);
|
|
1432
|
+
}
|
|
1433
|
+
catch (error) {
|
|
1434
|
+
if (timeoutMs !== null &&
|
|
1435
|
+
requestSignal.timeout?.aborted &&
|
|
1436
|
+
requestSignal.signal?.aborted &&
|
|
1437
|
+
requestSignal.signal.reason === requestSignal.timeout.reason) {
|
|
1438
|
+
// Composition preserves the first abort reason. Identity records which
|
|
1439
|
+
// source won even when both have fired before this catch runs (#19749).
|
|
1440
|
+
throw new RequestTimeoutError(operationId, timeoutMs);
|
|
1441
|
+
}
|
|
1442
|
+
if (requestSignal.signal?.aborted) {
|
|
1443
|
+
throw requestSignal.signal.reason;
|
|
1444
|
+
}
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
finally {
|
|
1448
|
+
requestSignal.dispose?.();
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
async list(operationId, options = {}) {
|
|
1452
|
+
const result = await this.call(operationId, options);
|
|
1453
|
+
if (isNotModified(result)) {
|
|
1454
|
+
throw new OxinsiderApiError(304, "list() received a 304 not_modified; use call() if you send If-None-Match on a list endpoint");
|
|
1455
|
+
}
|
|
1456
|
+
if (!isListEnvelope(result)) {
|
|
1457
|
+
throw new InvalidResponseError(200, `Operation ${operationId} did not return a list envelope: expected object "list", an array data, a boolean has_more and a string or absent next_cursor`, result);
|
|
1458
|
+
}
|
|
1459
|
+
return result;
|
|
1460
|
+
}
|
|
1461
|
+
// --- Typed convenience methods (key surfaces) ---
|
|
1462
|
+
//
|
|
1463
|
+
// Each takes the route's path parameters positionally, its documented
|
|
1464
|
+
// query as `params` (list reads) or `options.query` (single reads), and
|
|
1465
|
+
// the shared transport options: `signal`, `timeoutMs`, `maxRetries`,
|
|
1466
|
+
// `headers` (`If-None-Match` for a conditional read) and `idempotencyKey`
|
|
1467
|
+
// on a keyed write. The result is the operation's own envelope; no method
|
|
1468
|
+
// takes a type argument any more (#16136).
|
|
1469
|
+
getTrader(address, options = {}) {
|
|
1470
|
+
return this.call("getTrader", { ...options, path: { address } });
|
|
1471
|
+
}
|
|
1472
|
+
getTraderPnl(address, options = {}) {
|
|
1473
|
+
return this.call("getTraderPnl", { ...options, path: { address } });
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Fetch one wallet's win record per canonical category.
|
|
1477
|
+
*
|
|
1478
|
+
* Counts every settled market at any position size, so it can differ from
|
|
1479
|
+
* `category_strengths` on `getTrader`, which reads the floored calibration
|
|
1480
|
+
* sample. A category under `min_decided_for_win_rate` keeps its `wins` and
|
|
1481
|
+
* `decided` with `win_rate: null` and `status: "not_enough_data"`; a category
|
|
1482
|
+
* the wallet has no settled market in is absent, which means no record rather
|
|
1483
|
+
* than a 0% record. The `Esports` record carries `games`: the wallet's record
|
|
1484
|
+
* per esports title (`LoL`, `CS2`, `Dota 2`, ...) under the same rule, named
|
|
1485
|
+
* as the holder chips name them in `category_win_rate_game`. Pass
|
|
1486
|
+
* `params.category` to filter to one bucket; a filter that reaches Esports
|
|
1487
|
+
* returns its games too.
|
|
1488
|
+
*/
|
|
1489
|
+
getTraderCategoryRecords(address, params = {}, options = {}) {
|
|
1490
|
+
return this.call("getTraderCategoryRecords", {
|
|
1491
|
+
...options,
|
|
1492
|
+
path: { address },
|
|
1493
|
+
query: params,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
/** Read the grade proven visible at one past decision time. */
|
|
1497
|
+
getTraderGradeAt(address, params, options = {}) {
|
|
1498
|
+
return this.call("getTraderGradeAt", {
|
|
1499
|
+
...options,
|
|
1500
|
+
path: { address },
|
|
1501
|
+
query: params,
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Resolve 1 to 25 traders in one request (`POST /api/v1/traders/batch`).
|
|
1506
|
+
*
|
|
1507
|
+
* `traders` is sent as the request body's `traders` array, the field the
|
|
1508
|
+
* route requires: wallet addresses, usernames, `trd_` ids or integer trader
|
|
1509
|
+
* ids, resolved in input order. Until #16135 this method sent the array as
|
|
1510
|
+
* `identifiers`, a field the route does not declare, so every call was
|
|
1511
|
+
* refused for a missing `traders`. `options.expand` is sent as the body's
|
|
1512
|
+
* `expand` array and applies to every item.
|
|
1513
|
+
*
|
|
1514
|
+
* The response `data` keeps request order and one row per input, duplicates
|
|
1515
|
+
* included; a row is `status: "ok"` with `data`, or `status: "error"` with
|
|
1516
|
+
* the item's own `error`. An identity that resolves to nothing is a per-item
|
|
1517
|
+
* error, never a request failure, and since #18135 the route answers the
|
|
1518
|
+
* `not_found` this comment already promised: a username, `trd_` id or
|
|
1519
|
+
* numeric trader id that names no trader is `not_found` with `error.param`
|
|
1520
|
+
* `"traders"`, where it used to be an `ok` row with the input echoed into
|
|
1521
|
+
* `address`. A wallet address the API does not track yet stays `ok` with
|
|
1522
|
+
* `sync_status: "unknown"`, because that address is real and may still be
|
|
1523
|
+
* graded. `meta` is the batch's `BatchResponseMeta`: `request_cost` and
|
|
1524
|
+
* `rate_limit` follow the batch item quota, not the per-request one.
|
|
1525
|
+
*/
|
|
1526
|
+
batchGetTraders(traders, options = {}) {
|
|
1527
|
+
const { expand, ...request } = options;
|
|
1528
|
+
return this.call("batchGetTraders", {
|
|
1529
|
+
...request,
|
|
1530
|
+
body: expand === undefined ? { traders } : { traders, expand },
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Resolve up to 25 markets' flow and top positions in one request
|
|
1535
|
+
* (`POST /api/v1/markets/flow/batch`); `meta` is the batch's own.
|
|
1536
|
+
*/
|
|
1537
|
+
batchGetMarketFlow(body, options = {}) {
|
|
1538
|
+
return this.call("batchGetMarketFlow", { ...options, body });
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* @deprecated Use {@link batchGetMarketFlow} (#16312); calls the deprecated
|
|
1542
|
+
* `POST /api/v1/markets/intel/batch`, whose envelope keeps
|
|
1543
|
+
* `object: "market_intel_batch"`.
|
|
1544
|
+
*/
|
|
1545
|
+
batchGetMarketIntel(body, options = {}) {
|
|
1546
|
+
return this.call("batchGetMarketIntel", { ...options, body });
|
|
1547
|
+
}
|
|
1548
|
+
listLeaderboard(params = {}, options = {}) {
|
|
1549
|
+
return this.list("listLeaderboard", { ...options, query: params });
|
|
1550
|
+
}
|
|
1551
|
+
listTrendingWallets(params = {}, options = {}) {
|
|
1552
|
+
return this.list("listTrendingWallets", { ...options, query: params });
|
|
1553
|
+
}
|
|
1554
|
+
listLargeTrades(params = {}, options = {}) {
|
|
1555
|
+
return this.list("listLargeTrades", { ...options, query: params });
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Typed conditional-read variant of {@link listLargeTrades}, for polling
|
|
1559
|
+
* (#18507). Returns the list envelope on 200 or the typed `not_modified`
|
|
1560
|
+
* result on 304, where {@link listLargeTrades} throws on a 304. Pass
|
|
1561
|
+
* `since` (the id of the first trade of your last answer that had trades)
|
|
1562
|
+
* with `If-None-Match`: a poll that finds no new trade is a 304 with no body.
|
|
1563
|
+
*/
|
|
1564
|
+
listLargeTradesConditional(params = {}, options = {}) {
|
|
1565
|
+
return this.call("listLargeTrades", { ...options, query: params });
|
|
1566
|
+
}
|
|
1567
|
+
listLargeTradeHistory(params = {}, options = {}) {
|
|
1568
|
+
return this.list("listLargeTradeHistory", { ...options, query: params });
|
|
1569
|
+
}
|
|
1570
|
+
/** @deprecated Use {@link listLargeTrades} (#16304); calls the deprecated `/api/v1/whale-trades`. */
|
|
1571
|
+
listWhaleTrades(params = {}, options = {}) {
|
|
1572
|
+
return this.list("listWhaleTrades", { ...options, query: params });
|
|
1573
|
+
}
|
|
1574
|
+
/** @deprecated Use {@link listLargeTradeHistory} (#16304); calls the deprecated `/api/v1/whale-trades/history`. */
|
|
1575
|
+
listWhaleTradeHistory(params = {}, options = {}) {
|
|
1576
|
+
return this.list("listWhaleTradeHistory", { ...options, query: params });
|
|
1577
|
+
}
|
|
1578
|
+
listPositions(params = {}, options = {}) {
|
|
1579
|
+
return this.list("listPositions", { ...options, query: params });
|
|
1580
|
+
}
|
|
1581
|
+
listLargePositions(params = {}, options = {}) {
|
|
1582
|
+
return this.list("listLargePositions", { ...options, query: params });
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* List ranked sharp-money flows. Canonical since #16308 ("sharp money" is
|
|
1586
|
+
* the pinned product term); it calls `/api/v1/markets/sharp-money-flows`.
|
|
1587
|
+
*/
|
|
1588
|
+
listSharpMoneyFlows(params = {}, options = {}) {
|
|
1589
|
+
return this.list("listSharpMoneyFlows", { ...options, query: params });
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* @deprecated Use {@link listSharpMoneyFlows} (#16308). Kept live; it calls
|
|
1593
|
+
* the deprecated `/api/v1/markets/smart-money-flows`, whose responses carry
|
|
1594
|
+
* `Deprecation` and a `Link rel="successor-version"`.
|
|
1595
|
+
*/
|
|
1596
|
+
listSmartMoneyFlows(params = {}, options = {}) {
|
|
1597
|
+
return this.list("listSmartMoneyFlows", { ...options, query: params });
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* List covered games: both sides with their provider ids and live scores, the
|
|
1601
|
+
* UTC kickoff, the provider's own status, and every linked Polymarket market
|
|
1602
|
+
* with its condition id and outcome token ids. Ordered by kickoff, then by
|
|
1603
|
+
* `event_slug`, with unscheduled games last. An unknown `sport` or `status`
|
|
1604
|
+
* returns an empty page rather than an error, and the response's `coverage`
|
|
1605
|
+
* names what this deployment serves.
|
|
1606
|
+
*/
|
|
1607
|
+
listGames(params = {}, options = {}) {
|
|
1608
|
+
return this.list("listGames", { ...options, query: params });
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Read one game by its `event_slug`, the identity the `live_sports_updated`
|
|
1612
|
+
* webhook pulse carries. A slug outside the published coverage returns 404.
|
|
1613
|
+
*/
|
|
1614
|
+
getGame(eventSlug, options = {}) {
|
|
1615
|
+
return this.call("getGame", { ...options, path: { event_slug: eventSlug } });
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* List upcoming games ranked by the side profitable wallets hold, with
|
|
1619
|
+
* additive, shadow-only category evidence. `category_skill` never changes
|
|
1620
|
+
* membership, ordering, or sizing. Rows carry `side`, `ranked_at`,
|
|
1621
|
+
* `backing_score` and `side_share`; the older `piled_side`,
|
|
1622
|
+
* `signal_created_at`, `conviction_score` and `smart_score` keys carry the
|
|
1623
|
+
* same values and stay on the wire.
|
|
1624
|
+
*/
|
|
1625
|
+
listPreGameSides(params = {}, options = {}) {
|
|
1626
|
+
return this.list("listPreGameSides", { ...options, query: params });
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* @deprecated Use {@link listPreGameSides} (#16310). Kept live; it calls the
|
|
1630
|
+
* deprecated `/api/v1/sports-edge-signals` path, which answers with
|
|
1631
|
+
* `Deprecation` and successor `Link` headers and the same body.
|
|
1632
|
+
*/
|
|
1633
|
+
listSportsEdgeSignals(params = {}, options = {}) {
|
|
1634
|
+
return this.list("listSportsEdgeSignals", { ...options, query: params });
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Read an explicitly observation-only sports cohort and its accountable
|
|
1638
|
+
* per-sport funnel. This surface is isolated from the funded signals route.
|
|
1639
|
+
* Healthy wider-holder and emerging-pile snapshots may be served for about
|
|
1640
|
+
* 180 seconds; in-play cached snapshots are capped at about 30 seconds and
|
|
1641
|
+
* stale provider live-board evidence fails closed. emerging-pile is an
|
|
1642
|
+
* additive wider-holder projection, not an arrival-history or independent
|
|
1643
|
+
* denominator view.
|
|
1644
|
+
*/
|
|
1645
|
+
async listPreGameSideObservations(params, options = {}) {
|
|
1646
|
+
const response = await this.list("listPreGameSideObservations", {
|
|
1647
|
+
...options,
|
|
1648
|
+
query: params,
|
|
1649
|
+
});
|
|
1650
|
+
return sportsEdgeObservationsResponse(response);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* @deprecated Use {@link listPreGameSideObservations} (#16310). Kept live; it
|
|
1654
|
+
* calls the deprecated `/api/v1/sports-edge-observations` path, which answers
|
|
1655
|
+
* with `Deprecation` and successor `Link` headers and the same body.
|
|
1656
|
+
*/
|
|
1657
|
+
async listSportsEdgeObservations(params, options = {}) {
|
|
1658
|
+
const response = await this.list("listSportsEdgeObservations", {
|
|
1659
|
+
...options,
|
|
1660
|
+
query: params,
|
|
1661
|
+
});
|
|
1662
|
+
return sportsEdgeObservationsResponse(response);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Typed conditional-read variant of {@link listSportsEdgeObservations}.
|
|
1666
|
+
* Returns the full observation response on 200 or a typed `not_modified`
|
|
1667
|
+
* envelope on 304 instead of routing the latter through generic `call()`.
|
|
1668
|
+
* The endpoint emits a weak semantic ETag over the stable response payload;
|
|
1669
|
+
* request-specific `meta` is excluded. For emerging-pile, the opaque
|
|
1670
|
+
* projection cutoff inside `next_cursor` is excluded while its stable page
|
|
1671
|
+
* position remains covered.
|
|
1672
|
+
*/
|
|
1673
|
+
async listPreGameSideObservationsConditional(params, options = {}) {
|
|
1674
|
+
const response = await this.call("listPreGameSideObservations", {
|
|
1675
|
+
...options,
|
|
1676
|
+
query: params,
|
|
1677
|
+
});
|
|
1678
|
+
if (isNotModified(response)) {
|
|
1679
|
+
return response;
|
|
1680
|
+
}
|
|
1681
|
+
return sportsEdgeObservationsResponse(response);
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* @deprecated Use {@link listPreGameSideObservationsConditional} (#16310).
|
|
1685
|
+
* Kept live on the deprecated `/api/v1/sports-edge-observations` path.
|
|
1686
|
+
*/
|
|
1687
|
+
async listSportsEdgeObservationsConditional(params, options = {}) {
|
|
1688
|
+
const response = await this.call("listSportsEdgeObservations", {
|
|
1689
|
+
...options,
|
|
1690
|
+
query: params,
|
|
1691
|
+
});
|
|
1692
|
+
if (isNotModified(response)) {
|
|
1693
|
+
return response;
|
|
1694
|
+
}
|
|
1695
|
+
return sportsEdgeObservationsResponse(response);
|
|
1696
|
+
}
|
|
1697
|
+
searchMarkets(q, options = {}) {
|
|
1698
|
+
const { query, ...request } = options;
|
|
1699
|
+
return this.list("searchMarkets", { ...request, query: { ...query, q } });
|
|
1700
|
+
}
|
|
1701
|
+
searchContent(q, options = {}) {
|
|
1702
|
+
const { query, ...request } = options;
|
|
1703
|
+
return this.list("searchContent", { ...request, query: { ...query, q } });
|
|
1704
|
+
}
|
|
1705
|
+
exploreMarkets(params = {}, options = {}) {
|
|
1706
|
+
return this.list("exploreMarkets", { ...options, query: params });
|
|
1707
|
+
}
|
|
1708
|
+
/** One market's flow and top positions (`GET /api/v1/market/{condition_id}/flow`). */
|
|
1709
|
+
getMarketFlow(conditionId, options = {}) {
|
|
1710
|
+
return this.call("getMarketFlow", {
|
|
1711
|
+
...options,
|
|
1712
|
+
path: { condition_id: conditionId },
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* @deprecated Use {@link getMarketFlow} (#16312); calls the deprecated
|
|
1717
|
+
* `GET /api/v1/market/{condition_id}/intel`, whose envelope keeps
|
|
1718
|
+
* `object: "market_intel"`.
|
|
1719
|
+
*/
|
|
1720
|
+
getMarketIntel(conditionId, options = {}) {
|
|
1721
|
+
return this.call("getMarketIntel", {
|
|
1722
|
+
...options,
|
|
1723
|
+
path: { condition_id: conditionId },
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
getMarketSnapshot(conditionId, options = {}) {
|
|
1727
|
+
return this.call("getMarketSnapshot", {
|
|
1728
|
+
...options,
|
|
1729
|
+
path: { condition_id: conditionId },
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* One page of a market's graded (S/A/B) holder roster from a complete
|
|
1734
|
+
* provider holder scan: the list a Pick of the Day shows, for any market.
|
|
1735
|
+
* `params.outcome` (`yes` | `no` | `all`), `params.min_grade` (`S` | `A` |
|
|
1736
|
+
* `B`), `params.limit` and `params.cursor` (`mh_` prefix). The page carries
|
|
1737
|
+
* the `market`, `scan` and roster `totals` beside `data`; `total` is the
|
|
1738
|
+
* count matching the filters across every page.
|
|
1739
|
+
*/
|
|
1740
|
+
getMarketHolders(conditionId, params = {}, options = {}) {
|
|
1741
|
+
return this.list("getMarketHolders", {
|
|
1742
|
+
...options,
|
|
1743
|
+
path: { condition_id: conditionId },
|
|
1744
|
+
query: params,
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Fetch bucketed OHLC candles for a market's outcome tokens.
|
|
1749
|
+
* Resolution defaults to the server default; pass `params.resolution` as
|
|
1750
|
+
* `"1d"` or `"1w"` to override. `params.from` is exclusive and `params.to`
|
|
1751
|
+
* is inclusive; when both are present, `from` must be less than or equal to
|
|
1752
|
+
* `to`.
|
|
1753
|
+
*/
|
|
1754
|
+
getMarketCandles(conditionId, params = {}, options = {}) {
|
|
1755
|
+
return this.call("getMarketCandles", {
|
|
1756
|
+
...options,
|
|
1757
|
+
path: { condition_id: conditionId },
|
|
1758
|
+
query: params,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Fetch one suspicious trade by raw `whale_alerts.id` or the `rf_`-prefixed
|
|
1763
|
+
* id list responses emit. The envelope's `object` is `"suspicious_trade"`.
|
|
1764
|
+
*/
|
|
1765
|
+
getSuspiciousTrade(id, options = {}) {
|
|
1766
|
+
return this.call("getSuspiciousTrade", { ...options, path: { id } });
|
|
1767
|
+
}
|
|
1768
|
+
/** List stored trades whose recorded suspicion score meets the flag threshold. */
|
|
1769
|
+
listSuspiciousTrades(params = {}, options = {}) {
|
|
1770
|
+
return this.list("listSuspiciousTrades", { ...options, query: params });
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* @deprecated Use `getSuspiciousTrade()`; renamed in #16301. This method
|
|
1774
|
+
* keeps calling the deprecated `GET /api/v1/insider-radar/{id}`, which stays
|
|
1775
|
+
* live with no retirement date and still answers `object: "radar_flag"`, so
|
|
1776
|
+
* an integration branching on that envelope keeps working.
|
|
1777
|
+
*/
|
|
1778
|
+
getInsiderRadarFlag(id, options = {}) {
|
|
1779
|
+
return this.call("getInsiderRadarFlag", { ...options, path: { id } });
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* @deprecated Use `listSuspiciousTrades()`; renamed in #16301. This method
|
|
1783
|
+
* keeps calling the deprecated `GET /api/v1/insider-radar`, which stays live
|
|
1784
|
+
* with no retirement date and answers with `Deprecation` plus a `Link
|
|
1785
|
+
* rel="successor-version"` header.
|
|
1786
|
+
*/
|
|
1787
|
+
listInsiderRadar(params = {}, options = {}) {
|
|
1788
|
+
return this.list("listInsiderRadar", { ...options, query: params });
|
|
1789
|
+
}
|
|
1790
|
+
getLargeTrade(id, options = {}) {
|
|
1791
|
+
return this.call("getLargeTrade", { ...options, path: { id } });
|
|
1792
|
+
}
|
|
1793
|
+
/** @deprecated Use {@link getLargeTrade} (#16304); calls the deprecated `/api/v1/whale-trades/{id}`. */
|
|
1794
|
+
getWhaleTrade(id, options = {}) {
|
|
1795
|
+
return this.call("getWhaleTrade", { ...options, path: { id } });
|
|
1796
|
+
}
|
|
1797
|
+
// --- Exports ---
|
|
1798
|
+
/**
|
|
1799
|
+
* Cancel a submitted export (`POST /api/v1/trader/{address}/export/cancel`, #16254).
|
|
1800
|
+
*
|
|
1801
|
+
* Resolves to the job resource after the cancel, the same shape
|
|
1802
|
+
* `getTraderExportStatus` returns: `cancelled` for a job no worker had
|
|
1803
|
+
* started, `cancel_requested` for a running one (poll the status route at
|
|
1804
|
+
* `poll_after_s` until it reads `cancelled`), or the job unchanged when a
|
|
1805
|
+
* cancel can no longer reach it (its file is being published, or it is
|
|
1806
|
+
* already terminal). Compare `status` rather than assuming success. A
|
|
1807
|
+
* cancel never deletes a ready file, never returns quota, and is safe to
|
|
1808
|
+
* repeat, so it is retried on a transport or 5xx failure like a read.
|
|
1809
|
+
*/
|
|
1810
|
+
cancelTraderExport(address, jobId, options = {}) {
|
|
1811
|
+
return this.call("cancelTraderExport", {
|
|
1812
|
+
...options,
|
|
1813
|
+
path: { address },
|
|
1814
|
+
query: { job_id: jobId },
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
// --- Webhooks ---
|
|
1818
|
+
listWebhooks(options = {}) {
|
|
1819
|
+
return this.list("listWebhooks", options);
|
|
1820
|
+
}
|
|
1821
|
+
getWebhook(id, options = {}) {
|
|
1822
|
+
return this.call("getWebhook", { ...options, path: { id } });
|
|
1823
|
+
}
|
|
1824
|
+
createWebhook(body, options = {}) {
|
|
1825
|
+
return this.call("createWebhook", { ...options, body });
|
|
1826
|
+
}
|
|
1827
|
+
updateWebhook(id, body, options = {}) {
|
|
1828
|
+
return this.call("updateWebhook", { ...options, path: { id }, body });
|
|
1829
|
+
}
|
|
1830
|
+
deleteWebhook(id, options = {}) {
|
|
1831
|
+
return this.call("deleteWebhook", { ...options, path: { id } });
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Fetch the self-describing webhook event catalog (each entry's `id`,
|
|
1835
|
+
* description, payload shape, and active/dormant status).
|
|
1836
|
+
*/
|
|
1837
|
+
listWebhookEvents(options = {}) {
|
|
1838
|
+
return this.list("listWebhookEvents", options);
|
|
1839
|
+
}
|
|
1840
|
+
/** List delivery attempts for one webhook endpoint (Stripe-style list). */
|
|
1841
|
+
listWebhookDeliveries(webhookId, params = {}, options = {}) {
|
|
1842
|
+
return this.list("listWebhookDeliveries", {
|
|
1843
|
+
...options,
|
|
1844
|
+
path: { id: webhookId },
|
|
1845
|
+
query: params,
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Requeue one `dead_letter` delivery with a fresh attempt budget and return
|
|
1850
|
+
* the delivery row. Re-enabling a disabled endpoint resends nothing, so this
|
|
1851
|
+
* is how its dead-lettered deliveries are recovered. The API answers 409 when
|
|
1852
|
+
* the delivery is already delivered or still queued, or when the endpoint is
|
|
1853
|
+
* disabled, unverified, or no longer subscribed to the delivery's event type;
|
|
1854
|
+
* the error message names the fix. Pass `idempotencyKey` to make a retry safe.
|
|
1855
|
+
*/
|
|
1856
|
+
redeliverWebhookDelivery(webhookId, deliveryId, options = {}) {
|
|
1857
|
+
return this.call("redeliverWebhookDelivery", {
|
|
1858
|
+
...options,
|
|
1859
|
+
path: { id: webhookId, delivery_id: deliveryId },
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
// --- System ---
|
|
1863
|
+
getHealth(options = {}) {
|
|
1864
|
+
return this.call("getHealth", options);
|
|
1865
|
+
}
|
|
1866
|
+
getApiDiscovery(options = {}) {
|
|
1867
|
+
return this.call("getApiDiscovery", options);
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Which V1 reads the API serves for Polymarket (`GET /api/v1/coverage`).
|
|
1871
|
+
* Public: needs no API key.
|
|
1872
|
+
*/
|
|
1873
|
+
getCoverage(options = {}) {
|
|
1874
|
+
return this.call("getCoverage", options);
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* @deprecated Use {@link getCoverage} (#16315); calls the deprecated
|
|
1878
|
+
* `GET /api/v1/platforms`, which serves the same body.
|
|
1879
|
+
*/
|
|
1880
|
+
getPlatforms(options = {}) {
|
|
1881
|
+
return this.call("getPlatforms", options);
|
|
1882
|
+
}
|
|
1883
|
+
getAccountIdentity(options = {}) {
|
|
1884
|
+
return this.call("getAccountIdentity", options);
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* One wallet's context as Markdown, ready to paste into a model prompt
|
|
1888
|
+
* (`GET /api/v1/trader/{address}/context.md`). The JSON form of the same
|
|
1889
|
+
* read is `getTraderContext`.
|
|
1890
|
+
*/
|
|
1891
|
+
getTraderContextMarkdown(address, options = {}) {
|
|
1892
|
+
return this.text("getTraderContextMarkdown", { ...options, path: { address } });
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* One market's context as Markdown
|
|
1896
|
+
* (`GET /api/v1/market/{condition_id}/context.md`). The JSON form is
|
|
1897
|
+
* `getMarketSnapshot`.
|
|
1898
|
+
*/
|
|
1899
|
+
getMarketContextMarkdown(conditionId, options = {}) {
|
|
1900
|
+
return this.text("getMarketContextMarkdown", {
|
|
1901
|
+
...options,
|
|
1902
|
+
path: { condition_id: conditionId },
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Mint a sandbox key (`POST /api/v1/agents/register`), the one write that
|
|
1907
|
+
* needs no credential. Answers `201`, which is why the SDK had no method
|
|
1908
|
+
* for it until #16137: the drift gate counted only operations with a
|
|
1909
|
+
* documented `200`. `meta.status` is `201`; `data.api_key` is the
|
|
1910
|
+
* `oxi_sk_test_*` key to pass to `OxinsiderApiClient.sandbox()`.
|
|
1911
|
+
*
|
|
1912
|
+
* Nothing is stored: the key cannot be listed or revoked and does not
|
|
1913
|
+
* expire. Register again for another one.
|
|
1914
|
+
*/
|
|
1915
|
+
registerAgent(options = {}) {
|
|
1916
|
+
return this.call("registerAgent", options);
|
|
1917
|
+
}
|
|
1918
|
+
getUsage(options = {}) {
|
|
1919
|
+
return this.call("getUsage", options);
|
|
1920
|
+
}
|
|
1921
|
+
/** Fetch today's editorial Pick of the Day (single-object envelope). */
|
|
1922
|
+
getPickOfTheDay(options = {}) {
|
|
1923
|
+
return this.call("getPickOfTheDay", options);
|
|
1924
|
+
}
|
|
1925
|
+
/** Fetch the Pick of the Day archive with hit-rate (single-object envelope). */
|
|
1926
|
+
getPickOfTheDayArchive(options = {}) {
|
|
1927
|
+
return this.call("getPickOfTheDayArchive", options);
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Fetch the Pick of the Day commitment ledger (single-object envelope).
|
|
1931
|
+
*
|
|
1932
|
+
* Every entry is `sealed` (a live pick: the hash, no side and no price),
|
|
1933
|
+
* `opened` (a settled pick: the nonce and the exact hashed payload) or
|
|
1934
|
+
* `uncommitted` (no commitment; once settled, its unhashed side and price
|
|
1935
|
+
* under `payload`). Verify an opened entry by
|
|
1936
|
+
* appending the hex-decoded `commitment_nonce` to the `payload` bytes as
|
|
1937
|
+
* received and hashing with sha256; do not reserialize the payload, since it
|
|
1938
|
+
* is served byte for byte as it was hashed.
|
|
1939
|
+
*/
|
|
1940
|
+
getPickOfTheDayLedger(options = {}) {
|
|
1941
|
+
return this.call("getPickOfTheDayLedger", options);
|
|
1942
|
+
}
|
|
1943
|
+
// --- Internals ---
|
|
1944
|
+
/** Resolve the base URL (used by the SSE stream consumer). */
|
|
1945
|
+
getBaseUrl() {
|
|
1946
|
+
return this.baseUrl;
|
|
1947
|
+
}
|
|
1948
|
+
/** The configured API key, if any (used by the SSE stream consumer). */
|
|
1949
|
+
getApiKey() {
|
|
1950
|
+
return this.apiKey;
|
|
1951
|
+
}
|
|
1952
|
+
/** Whether this client was built in sandbox mode (#16138). */
|
|
1953
|
+
isSandbox() {
|
|
1954
|
+
return this.sandbox;
|
|
1955
|
+
}
|
|
1956
|
+
/** The resolved fetch implementation (used by the SSE stream consumer). */
|
|
1957
|
+
getFetch() {
|
|
1958
|
+
return this.fetchImpl;
|
|
1959
|
+
}
|
|
1960
|
+
buildUrl(operation, options) {
|
|
1961
|
+
const url = resolveApiUrl(this.baseUrl, interpolatePath(operation.path, options.path ?? {}));
|
|
1962
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
1963
|
+
if (value === null || value === undefined) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (Array.isArray(value)) {
|
|
1967
|
+
for (const item of value) {
|
|
1968
|
+
url.searchParams.append(key, String(item));
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
url.searchParams.set(key, String(value));
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return url.toString();
|
|
1976
|
+
}
|
|
1977
|
+
buildHeaders(operation, options, accept = "application/json") {
|
|
1978
|
+
const headers = new Headers(options.headers);
|
|
1979
|
+
headers.set("accept", accept);
|
|
1980
|
+
if (options.body !== undefined && !headers.has("content-type")) {
|
|
1981
|
+
headers.set("content-type", "application/json");
|
|
1982
|
+
}
|
|
1983
|
+
if (operation.auth === "bearer" && this.apiKey) {
|
|
1984
|
+
headers.set("authorization", `Bearer ${this.apiKey}`);
|
|
1985
|
+
}
|
|
1986
|
+
if (options.idempotencyKey !== undefined) {
|
|
1987
|
+
headers.set("idempotency-key", options.idempotencyKey);
|
|
1988
|
+
}
|
|
1989
|
+
// Validate the effective value after the explicit option has overridden
|
|
1990
|
+
// custom headers. The backend trims it before checking its byte limit;
|
|
1991
|
+
// a blank header is treated as no durable replay key (#19748).
|
|
1992
|
+
const idempotencyKey = headers.get("idempotency-key");
|
|
1993
|
+
if (idempotencyKey !== null) {
|
|
1994
|
+
const key = idempotencyKey.trim();
|
|
1995
|
+
if (key.length === 0 || new TextEncoder().encode(key).byteLength > 255) {
|
|
1996
|
+
throw new Error("Idempotency-Key must be nonempty after trimming and at most 255 UTF-8 bytes. Supply a stable nonempty key or omit it to send the mutation once.");
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (options.strictQuery) {
|
|
2000
|
+
headers.set("x-query-validation", "strict");
|
|
2001
|
+
}
|
|
2002
|
+
return headers;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Narrow a `call()` result to the typed 304. Takes any envelope-shaped
|
|
2007
|
+
* value, so it works on an `OperationResult<K>` (whose `meta` is the
|
|
2008
|
+
* operation's own type) as well as the loose `ApiClientResponse<T>`
|
|
2009
|
+
* (#16136).
|
|
2010
|
+
*/
|
|
2011
|
+
export function isApiNotModifiedResponse(response) {
|
|
2012
|
+
return response.object === "not_modified";
|
|
2013
|
+
}
|
|
2014
|
+
function isNotModified(response) {
|
|
2015
|
+
return response.object === "not_modified";
|
|
2016
|
+
}
|
|
2017
|
+
export function interpolatePath(path, params) {
|
|
2018
|
+
return path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
2019
|
+
const value = params[name];
|
|
2020
|
+
if (value === undefined) {
|
|
2021
|
+
throw new Error(`Missing path parameter: ${name}`);
|
|
2022
|
+
}
|
|
2023
|
+
return encodeURIComponent(String(value));
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
async function readResponseText(response) {
|
|
2027
|
+
const unusable = response.bodyUsed || response.body?.locked === true;
|
|
2028
|
+
try {
|
|
2029
|
+
return await response.text();
|
|
2030
|
+
}
|
|
2031
|
+
catch (error) {
|
|
2032
|
+
// A consumed/locked custom-fetch response or failed allocation is a local
|
|
2033
|
+
// contract failure, rather than evidence of a transport interruption.
|
|
2034
|
+
if (unusable || error instanceof RangeError)
|
|
2035
|
+
throw error;
|
|
2036
|
+
throw new ResponseBodyReadError(response.status, error);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
async function parseJson(response) {
|
|
2040
|
+
const text = await readResponseText(response);
|
|
2041
|
+
if (text === "") {
|
|
2042
|
+
return null;
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
return JSON.parse(text);
|
|
2046
|
+
}
|
|
2047
|
+
catch {
|
|
2048
|
+
return text;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function notModifiedResponse(response) {
|
|
2052
|
+
const meta = { status: 304 };
|
|
2053
|
+
const etag = response.headers.get("etag");
|
|
2054
|
+
if (etag) {
|
|
2055
|
+
meta.etag = etag;
|
|
2056
|
+
}
|
|
2057
|
+
const requestId = response.headers.get("x-request-id");
|
|
2058
|
+
if (requestId) {
|
|
2059
|
+
meta.request_id = requestId;
|
|
2060
|
+
}
|
|
2061
|
+
const queryIgnored = response.headers.get("x-query-ignored");
|
|
2062
|
+
if (queryIgnored) {
|
|
2063
|
+
meta.queryIgnored = queryIgnored;
|
|
2064
|
+
}
|
|
2065
|
+
const effectiveQuery = response.headers.get("x-effective-query");
|
|
2066
|
+
if (effectiveQuery) {
|
|
2067
|
+
meta.effectiveQuery = effectiveQuery;
|
|
2068
|
+
}
|
|
2069
|
+
return { object: "not_modified", data: null, meta };
|
|
2070
|
+
}
|
|
2071
|
+
function isApiEnvelope(body) {
|
|
2072
|
+
return (typeof body === "object" &&
|
|
2073
|
+
body !== null &&
|
|
2074
|
+
"object" in body &&
|
|
2075
|
+
"data" in body);
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Whether `body` is a list envelope whose control fields the walker can act
|
|
2079
|
+
* on: `object: "list"`, an array `data`, a BOOLEAN `has_more`, and a
|
|
2080
|
+
* `next_cursor` that is a string, `null` or absent (#16246: the check used to
|
|
2081
|
+
* accept any `has_more` that was merely present, so a malformed value could
|
|
2082
|
+
* be coerced into "no more pages" or "more pages" by truthiness).
|
|
2083
|
+
*/
|
|
2084
|
+
export function isListEnvelope(body) {
|
|
2085
|
+
if (typeof body !== "object" || body === null)
|
|
2086
|
+
return false;
|
|
2087
|
+
const candidate = body;
|
|
2088
|
+
return (candidate.object === "list" &&
|
|
2089
|
+
Array.isArray(candidate.data) &&
|
|
2090
|
+
typeof candidate.has_more === "boolean" &&
|
|
2091
|
+
(candidate.next_cursor === undefined ||
|
|
2092
|
+
candidate.next_cursor === null ||
|
|
2093
|
+
typeof candidate.next_cursor === "string"));
|
|
2094
|
+
}
|
|
2095
|
+
function sportsEdgeObservationsResponse(response) {
|
|
2096
|
+
const candidate = response;
|
|
2097
|
+
const funnel = candidate.funnel;
|
|
2098
|
+
const meta = candidate.meta;
|
|
2099
|
+
if (candidate.object !== "list" ||
|
|
2100
|
+
!Array.isArray(candidate.data) ||
|
|
2101
|
+
typeof candidate.has_more !== "boolean" ||
|
|
2102
|
+
!(candidate.next_cursor === null ||
|
|
2103
|
+
typeof candidate.next_cursor === "string") ||
|
|
2104
|
+
typeof candidate.snapshot_as_of !== "string" ||
|
|
2105
|
+
typeof candidate.degraded !== "boolean" ||
|
|
2106
|
+
typeof funnel !== "object" ||
|
|
2107
|
+
funnel === null ||
|
|
2108
|
+
Array.isArray(funnel) ||
|
|
2109
|
+
!Array.isArray(funnel.sports) ||
|
|
2110
|
+
typeof meta !== "object" ||
|
|
2111
|
+
meta === null ||
|
|
2112
|
+
Array.isArray(meta) ||
|
|
2113
|
+
typeof meta.request_id !== "string" ||
|
|
2114
|
+
typeof meta.cached !== "boolean" ||
|
|
2115
|
+
typeof meta.cost !== "number" ||
|
|
2116
|
+
!Number.isInteger(meta.cost)) {
|
|
2117
|
+
throw new OxinsiderApiError(200, {
|
|
2118
|
+
object: "error",
|
|
2119
|
+
error: {
|
|
2120
|
+
code: "invalid_response",
|
|
2121
|
+
message: "listSportsEdgeObservations returned an invalid required response envelope",
|
|
2122
|
+
},
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
return response;
|
|
2126
|
+
}
|
|
2127
|
+
//# sourceMappingURL=client.js.map
|