@arcaresearch/sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/arca.js ADDED
@@ -0,0 +1,780 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Arca = void 0;
4
+ const client_1 = require("./client");
5
+ const errors_1 = require("./errors");
6
+ const types_1 = require("./types");
7
+ const websocket_1 = require("./websocket");
8
+ const DEFAULT_BASE_URL = 'https://api.arcaos.io';
9
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const TYPEID_RE = /^[a-z]{2,63}_[0-9a-hjkmnp-tv-z]{26}$/i;
11
+ function isIdFormat(value) {
12
+ return UUID_RE.test(value) || TYPEID_RE.test(value);
13
+ }
14
+ function isApiKeyConfig(config) {
15
+ return 'apiKey' in config;
16
+ }
17
+ function isTokenConfig(config) {
18
+ return 'token' in config;
19
+ }
20
+ /**
21
+ * Decode a JWT payload without verifying the signature.
22
+ * Used client-side to extract claims like realmId from scoped tokens.
23
+ */
24
+ function decodeJwtPayload(token) {
25
+ const parts = token.split('.');
26
+ if (parts.length !== 3)
27
+ throw new Error('Invalid JWT format');
28
+ const payload = parts[1];
29
+ // atob works in browser; Buffer works in Node
30
+ const json = typeof atob === 'function'
31
+ ? atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
32
+ : Buffer.from(payload, 'base64url').toString('utf-8');
33
+ return JSON.parse(json);
34
+ }
35
+ /**
36
+ * The Arca SDK client.
37
+ *
38
+ * Usage with API key (backend):
39
+ * ```typescript
40
+ * const arca = new Arca({
41
+ * apiKey: 'arca_78ae7276_...',
42
+ * realm: 'development',
43
+ * });
44
+ * await arca.ready();
45
+ * ```
46
+ *
47
+ * Usage with scoped token (frontend):
48
+ * ```typescript
49
+ * const arca = Arca.fromToken(scopedJwt);
50
+ * await arca.ready();
51
+ * ```
52
+ */
53
+ class Arca {
54
+ client;
55
+ realmInput;
56
+ resolvedRealmId = null;
57
+ initPromise = null;
58
+ /** WebSocket manager for real-time events. */
59
+ ws;
60
+ /** @deprecated Use ws.legacySubscribe() or ws.on() / ws.subscribe() instead. */
61
+ events;
62
+ constructor(config) {
63
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
64
+ if (isApiKeyConfig(config)) {
65
+ this.client = new client_1.ArcaClient({
66
+ credential: config.apiKey,
67
+ credentialType: 'apiKey',
68
+ baseUrl: `${baseUrl}/api/v1`,
69
+ });
70
+ this.realmInput = config.realm;
71
+ }
72
+ else if (isTokenConfig(config)) {
73
+ this.client = new client_1.ArcaClient({
74
+ credential: config.token,
75
+ credentialType: 'token',
76
+ baseUrl: `${baseUrl}/api/v1`,
77
+ });
78
+ this.realmInput = config.realm ?? null;
79
+ // Try to extract realmId from the token claims
80
+ if (!this.realmInput) {
81
+ try {
82
+ const claims = decodeJwtPayload(config.token);
83
+ if (typeof claims.realmId === 'string') {
84
+ this.resolvedRealmId = claims.realmId;
85
+ }
86
+ }
87
+ catch {
88
+ // If we can't decode, we'll try to resolve later
89
+ }
90
+ }
91
+ }
92
+ else {
93
+ throw new Error('ArcaConfig must include either apiKey or token');
94
+ }
95
+ this.ws = new websocket_1.WebSocketManager({
96
+ baseUrl: `${baseUrl}/api/v1`,
97
+ credential: isApiKeyConfig(config) ? config.apiKey : config.token,
98
+ credentialType: isApiKeyConfig(config) ? 'apiKey' : 'token',
99
+ getRealmId: () => this.realmId(),
100
+ });
101
+ this.events = {
102
+ subscribe: (optsOrCallback, maybeCallback) => {
103
+ let opts = {};
104
+ let callback;
105
+ if (typeof optsOrCallback === 'function') {
106
+ callback = optsOrCallback;
107
+ }
108
+ else {
109
+ opts = optsOrCallback;
110
+ callback = maybeCallback;
111
+ }
112
+ return this.ws.legacySubscribe(opts, callback);
113
+ },
114
+ };
115
+ }
116
+ /**
117
+ * Create an Arca instance from a scoped JWT token.
118
+ * The realm is extracted from the token claims automatically.
119
+ *
120
+ * @param token - Scoped JWT issued by POST /auth/token
121
+ * @param opts - Optional overrides (baseUrl, realm)
122
+ */
123
+ static fromToken(token, opts) {
124
+ return new Arca({
125
+ token,
126
+ realm: opts?.realm,
127
+ baseUrl: opts?.baseUrl,
128
+ });
129
+ }
130
+ /**
131
+ * Initialize the SDK by resolving the realm slug to an ID.
132
+ * This is called automatically on the first API call, but can be
133
+ * called explicitly for eager initialization.
134
+ */
135
+ async ready() {
136
+ if (this.resolvedRealmId)
137
+ return;
138
+ if (!this.initPromise) {
139
+ this.initPromise = this.resolveRealm();
140
+ }
141
+ await this.initPromise;
142
+ }
143
+ // ---- Arca Objects ----
144
+ /**
145
+ * Create a denominated Arca object at the given path (idempotent).
146
+ * If one already exists with matching type/denomination, it is returned.
147
+ * Otherwise a new one is created.
148
+ *
149
+ * Pass `operationPath` (from the nonce API with separator ":") for
150
+ * explicit operation-level idempotency, especially when delete+recreate
151
+ * scenarios are expected.
152
+ */
153
+ async createDenominatedArca(opts) {
154
+ await this.ready();
155
+ return this.client.post('/objects', {
156
+ realmId: this.realmId(),
157
+ path: opts.ref,
158
+ type: 'denominated',
159
+ denomination: opts.denomination,
160
+ metadata: opts.metadata ?? null,
161
+ operationPath: opts.operationPath ?? null,
162
+ });
163
+ }
164
+ /**
165
+ * Create an Arca object of any type at the given path (idempotent).
166
+ *
167
+ * Pass `operationPath` (from the nonce API with separator ":") for
168
+ * explicit operation-level idempotency.
169
+ */
170
+ async createArca(opts) {
171
+ await this.ready();
172
+ return this.client.post('/objects', {
173
+ realmId: this.realmId(),
174
+ path: opts.ref,
175
+ type: opts.type,
176
+ denomination: opts.denomination ?? null,
177
+ metadata: opts.metadata ?? null,
178
+ operationPath: opts.operationPath ?? null,
179
+ });
180
+ }
181
+ /** @deprecated Use createDenominatedArca instead */
182
+ async ensureDenominatedArca(opts) {
183
+ return this.createDenominatedArca(opts);
184
+ }
185
+ /** @deprecated Use createArca instead */
186
+ async ensureArca(opts) {
187
+ return this.createArca(opts);
188
+ }
189
+ /**
190
+ * Delete an Arca object by path. If sweepTo is provided, remaining funds
191
+ * are transferred to that Arca before deletion.
192
+ *
193
+ * If the object is already deleted, returns the existing state.
194
+ */
195
+ async ensureDeleted(opts) {
196
+ await this.ready();
197
+ return this.client.post('/objects/delete', {
198
+ realmId: this.realmId(),
199
+ path: opts.ref,
200
+ sweepToPath: opts.sweepTo ?? null,
201
+ liquidatePositions: opts.liquidatePositions ?? false,
202
+ operationPath: opts.operationPath ?? null,
203
+ });
204
+ }
205
+ /**
206
+ * Get an Arca object by path.
207
+ */
208
+ async getObject(path) {
209
+ await this.ready();
210
+ return this.client.get('/objects/by-path', {
211
+ realmId: this.realmId(),
212
+ path,
213
+ });
214
+ }
215
+ /**
216
+ * Get an Arca object's full detail (operations, events, deltas, balances) by ID.
217
+ */
218
+ async getObjectDetail(objectId) {
219
+ await this.ready();
220
+ return this.client.get(`/objects/${objectId}`);
221
+ }
222
+ /**
223
+ * List Arca objects, optionally filtered by path prefix.
224
+ */
225
+ async listObjects(opts) {
226
+ await this.ready();
227
+ const params = { realmId: this.realmId() };
228
+ if (opts?.prefix)
229
+ params.prefix = opts.prefix;
230
+ if (opts?.includeDeleted)
231
+ params.includeDeleted = 'true';
232
+ return this.client.get('/objects', params);
233
+ }
234
+ /**
235
+ * Get balances for an Arca object by ID.
236
+ */
237
+ async getBalances(objectId) {
238
+ await this.ready();
239
+ const res = await this.client.get(`/objects/${objectId}/balances`);
240
+ return res.balances;
241
+ }
242
+ /**
243
+ * Get balances for an Arca object by path.
244
+ * Resolves the path to an object ID, then fetches balances.
245
+ */
246
+ async getBalancesByPath(path) {
247
+ const obj = await this.getObject(path);
248
+ return this.getBalances(obj.id);
249
+ }
250
+ /**
251
+ * Browse Arca objects in a folder-like structure.
252
+ * Returns folders (path prefixes) and objects at the given prefix.
253
+ */
254
+ async browseObjects(opts) {
255
+ await this.ready();
256
+ const params = {
257
+ realmId: this.realmId(),
258
+ prefix: opts?.prefix ?? '/',
259
+ };
260
+ if (opts?.includeDeleted)
261
+ params.includeDeleted = 'true';
262
+ return this.client.get('/objects/browse', params);
263
+ }
264
+ /**
265
+ * Get version history for an Arca object.
266
+ */
267
+ async getObjectVersions(objectId) {
268
+ await this.ready();
269
+ return this.client.get(`/objects/${objectId}/versions`);
270
+ }
271
+ /**
272
+ * Get snapshot balances for an Arca object at a specific point in time.
273
+ */
274
+ async getSnapshotBalances(objectId, asOf) {
275
+ await this.ready();
276
+ return this.client.get(`/objects/${objectId}/snapshot`, {
277
+ realmId: this.realmId(),
278
+ asOf,
279
+ });
280
+ }
281
+ // ---- Transfers ----
282
+ /**
283
+ * Execute a transfer between two Arca objects.
284
+ * Settlement is immediate for denominated targets, or async for
285
+ * targets that require a receiver workflow (e.g. exchange objects).
286
+ * The operation path serves as the idempotency key.
287
+ */
288
+ async transfer(opts) {
289
+ await this.ready();
290
+ return this.client.post('/transfer', {
291
+ realmId: this.realmId(),
292
+ path: opts.path,
293
+ sourceArcaPath: opts.from,
294
+ targetArcaPath: opts.to,
295
+ amount: opts.amount,
296
+ });
297
+ }
298
+ // ---- Deposits ----
299
+ /**
300
+ * Initiate a deposit to a denominated Arca object.
301
+ * In demo realms, deposits are simulated with a configurable delay.
302
+ */
303
+ async deposit(opts) {
304
+ await this.ready();
305
+ return this.client.post('/deposit', {
306
+ realmId: this.realmId(),
307
+ arcaPath: opts.arcaRef,
308
+ amount: opts.amount,
309
+ path: opts.path ?? null,
310
+ senderAddress: opts.senderAddress ?? null,
311
+ });
312
+ }
313
+ // ---- Withdrawals ----
314
+ /**
315
+ * Initiate a withdrawal from a denominated Arca object to an on-chain address.
316
+ * Requires custody service (on-chain mode) to be active.
317
+ */
318
+ async withdrawal(opts) {
319
+ await this.ready();
320
+ return this.client.post('/withdrawal', {
321
+ realmId: this.realmId(),
322
+ arcaPath: opts.arcaPath,
323
+ amount: opts.amount,
324
+ destinationAddress: opts.destinationAddress ?? '',
325
+ path: opts.path ?? null,
326
+ });
327
+ }
328
+ // ---- Operations ----
329
+ /**
330
+ * Get operation detail by ID (includes correlated events and deltas).
331
+ */
332
+ async getOperation(operationId) {
333
+ await this.ready();
334
+ return this.client.get(`/operations/${operationId}`);
335
+ }
336
+ /**
337
+ * List operations in the realm.
338
+ */
339
+ async listOperations(opts) {
340
+ await this.ready();
341
+ const params = { realmId: this.realmId() };
342
+ if (opts?.type)
343
+ params.type = opts.type;
344
+ if (opts?.arcaPath)
345
+ params.arcaPath = opts.arcaPath;
346
+ if (opts?.path)
347
+ params.path = opts.path;
348
+ return this.client.get('/operations', params);
349
+ }
350
+ /**
351
+ * List events in the realm.
352
+ */
353
+ async listEvents(opts) {
354
+ await this.ready();
355
+ const params = { realmId: this.realmId() };
356
+ if (opts?.arcaPath)
357
+ params.arcaPath = opts.arcaPath;
358
+ if (opts?.path)
359
+ params.path = opts.path;
360
+ return this.client.get('/events', params);
361
+ }
362
+ /**
363
+ * Get event detail by ID (includes the parent operation and correlated deltas).
364
+ */
365
+ async getEventDetail(eventId) {
366
+ await this.ready();
367
+ return this.client.get(`/events/${eventId}`);
368
+ }
369
+ // ---- State Deltas ----
370
+ /**
371
+ * List state deltas for a given Arca path.
372
+ */
373
+ async listDeltas(arcaPath) {
374
+ await this.ready();
375
+ return this.client.get('/deltas', {
376
+ realmId: this.realmId(),
377
+ arcaPath,
378
+ });
379
+ }
380
+ // ---- Nonces ----
381
+ /**
382
+ * Get the next unique nonce for a path prefix.
383
+ * Useful for constructing unique idempotent operation paths.
384
+ *
385
+ * **Important:** Reserve the nonce *before* the operation and store the
386
+ * resulting path. Reuse the stored path on retry — never call `nonce()`
387
+ * inline inside an operation call, as each invocation produces a new
388
+ * unique path and defeats idempotency.
389
+ *
390
+ * @param prefix - Path prefix (e.g. '/op/transfer/fund')
391
+ * @param separator - Override separator between prefix and nonce number.
392
+ * Default: '/' if prefix ends with '/', otherwise '-'.
393
+ * Use ':' for operation nonces (e.g. '/op/create/wallets/main:1').
394
+ */
395
+ async nonce(prefix, separator) {
396
+ await this.ready();
397
+ const body = {
398
+ realmId: this.realmId(),
399
+ prefix,
400
+ };
401
+ if (separator !== undefined) {
402
+ body.separator = separator;
403
+ }
404
+ return this.client.post('/nonce', body);
405
+ }
406
+ // ---- Summary ----
407
+ /**
408
+ * Get aggregate counts for the realm.
409
+ */
410
+ async summary() {
411
+ await this.ready();
412
+ return this.client.get('/summary', {
413
+ realmId: this.realmId(),
414
+ });
415
+ }
416
+ // ---- Aggregation & P&L ----
417
+ /**
418
+ * Get aggregated valuation for all objects under a path prefix.
419
+ * Pass `asOf` to get historical aggregation at a past timestamp.
420
+ */
421
+ async getPathAggregation(prefix, options) {
422
+ await this.ready();
423
+ const params = {
424
+ realmId: this.realmId(),
425
+ prefix,
426
+ };
427
+ if (options?.asOf)
428
+ params.asOf = options.asOf;
429
+ return this.client.get('/objects/aggregate', params);
430
+ }
431
+ /**
432
+ * Get P&L (profit and loss) for objects under a path prefix over a time range.
433
+ * Returns starting/ending equity, net inflows/outflows, and calculated P&L.
434
+ */
435
+ async getPnl(prefix, from, to) {
436
+ await this.ready();
437
+ return this.client.get('/objects/pnl', {
438
+ realmId: this.realmId(),
439
+ prefix,
440
+ from,
441
+ to,
442
+ });
443
+ }
444
+ /**
445
+ * Get equity history (time-series) for objects under a path prefix.
446
+ * Returns equity values sampled evenly over the time range.
447
+ * The `points` parameter controls how many samples (default 200, max 1000).
448
+ */
449
+ async getEquityHistory(prefix, from, to, points = 200) {
450
+ await this.ready();
451
+ return this.client.get('/objects/aggregate/history', {
452
+ realmId: this.realmId(),
453
+ prefix,
454
+ from,
455
+ to,
456
+ points: String(points),
457
+ });
458
+ }
459
+ /**
460
+ * Create an aggregation watch that tracks a set of sources.
461
+ * Returns the watch ID and the initial aggregation.
462
+ * When the underlying data changes, `aggregation.updated` events are
463
+ * pushed via WebSocket with the updated aggregation inline.
464
+ */
465
+ async createAggregationWatch(sources) {
466
+ await this.ready();
467
+ return this.client.post('/aggregations/watch', {
468
+ realmId: this.realmId(),
469
+ sources,
470
+ });
471
+ }
472
+ /**
473
+ * Get the current aggregation for an existing watch.
474
+ */
475
+ async getWatchAggregation(watchId) {
476
+ await this.ready();
477
+ return this.client.get(`/aggregations/watch/${watchId}`);
478
+ }
479
+ /**
480
+ * Destroy an aggregation watch. The server stops tracking and pushing updates.
481
+ */
482
+ async destroyAggregationWatch(watchId) {
483
+ await this.ready();
484
+ await this.client.delete(`/aggregations/watch/${watchId}`);
485
+ }
486
+ // ---- Reconciliation ----
487
+ /**
488
+ * List reconciliation state entries for the realm.
489
+ */
490
+ async listReconciliationState() {
491
+ await this.ready();
492
+ return this.client.get('/reconciliation', {
493
+ realmId: this.realmId(),
494
+ });
495
+ }
496
+ // ---- Exchange (Perps) ----
497
+ /**
498
+ * Create a Perps Exchange Arca object.
499
+ * Automatically sets type=exchange, denomination=USD, and exchangeType metadata.
500
+ */
501
+ async createPerpsExchange(opts) {
502
+ await this.ready();
503
+ return this.client.post('/objects', {
504
+ realmId: this.realmId(),
505
+ path: opts.ref,
506
+ type: 'exchange',
507
+ denomination: 'USD',
508
+ metadata: JSON.stringify({ exchangeType: opts.exchangeType || 'hyperliquid' }),
509
+ operationPath: opts.operationPath,
510
+ });
511
+ }
512
+ /**
513
+ * Get exchange account state (equity, margin, positions, orders).
514
+ */
515
+ async getExchangeState(objectId) {
516
+ await this.ready();
517
+ return this.client.get(`/objects/${objectId}/exchange/state`);
518
+ }
519
+ /**
520
+ * Get active asset trading data for an exchange object: max trade sizes,
521
+ * available margin, mark price, and fee rate. Accounts for leverage,
522
+ * existing positions, and fees (including builder fee when provided).
523
+ */
524
+ async getActiveAssetData(objectId, coin, builderFeeBps) {
525
+ await this.ready();
526
+ let url = `/objects/${objectId}/exchange/active-asset-data?coin=${encodeURIComponent(coin)}`;
527
+ if (builderFeeBps && builderFeeBps > 0)
528
+ url += `&builderFeeBps=${builderFeeBps}`;
529
+ return this.client.get(url);
530
+ }
531
+ /**
532
+ * Update leverage for a coin on an exchange Arca object.
533
+ * Leverage is a per-coin setting that applies to the entire position.
534
+ * Re-margins any existing position at the new leverage.
535
+ * Rejects if the account can't afford the increased margin when decreasing leverage.
536
+ */
537
+ async updateLeverage(opts) {
538
+ await this.ready();
539
+ return this.client.post(`/objects/${opts.objectId}/exchange/leverage`, {
540
+ coin: opts.coin,
541
+ leverage: opts.leverage,
542
+ });
543
+ }
544
+ /**
545
+ * Get leverage settings for a coin (or all coins) on an exchange Arca object.
546
+ */
547
+ async getLeverage(objectId, coin) {
548
+ await this.ready();
549
+ const params = {};
550
+ if (coin)
551
+ params.coin = coin;
552
+ return this.client.get(`/objects/${objectId}/exchange/leverage`, params);
553
+ }
554
+ /**
555
+ * Get all leverage settings for an exchange Arca object.
556
+ * Unlike getLeverage(), always returns an array.
557
+ */
558
+ async listLeverageSettings(objectId) {
559
+ const result = await this.getLeverage(objectId);
560
+ return Array.isArray(result) ? result : [result];
561
+ }
562
+ /**
563
+ * Place an order on an exchange Arca object.
564
+ * The operation path serves as the idempotency key.
565
+ */
566
+ async placeOrder(opts) {
567
+ await this.ready();
568
+ return this.client.post(`/objects/${opts.objectId}/exchange/orders`, {
569
+ realmId: this.realmId(),
570
+ path: opts.path,
571
+ coin: opts.coin,
572
+ side: opts.side,
573
+ orderType: opts.orderType,
574
+ size: opts.size,
575
+ szDenom: opts.szDenom ?? 'token',
576
+ price: opts.price,
577
+ leverage: opts.leverage ?? 1,
578
+ reduceOnly: opts.reduceOnly ?? false,
579
+ timeInForce: opts.timeInForce ?? 'GTC',
580
+ ...(opts.builderFeeBps != null && { builderFeeBps: opts.builderFeeBps }),
581
+ ...(opts.feeTargets != null && { feeTargets: opts.feeTargets }),
582
+ });
583
+ }
584
+ /**
585
+ * List orders for an exchange Arca object.
586
+ */
587
+ async listOrders(objectId, status) {
588
+ await this.ready();
589
+ const params = {};
590
+ if (status)
591
+ params.status = status;
592
+ return this.client.get(`/objects/${objectId}/exchange/orders`, params);
593
+ }
594
+ /**
595
+ * Get a specific order with its fills.
596
+ */
597
+ async getOrder(objectId, orderId) {
598
+ await this.ready();
599
+ return this.client.get(`/objects/${objectId}/exchange/orders/${orderId}`);
600
+ }
601
+ /**
602
+ * Cancel an order on an exchange Arca object.
603
+ * The operation path serves as the idempotency key.
604
+ */
605
+ async cancelOrder(opts) {
606
+ await this.ready();
607
+ const params = new URLSearchParams({
608
+ realmId: this.realmId(),
609
+ path: opts.path,
610
+ });
611
+ return this.client.delete(`/objects/${opts.objectId}/exchange/orders/${opts.orderId}?${params.toString()}`);
612
+ }
613
+ /**
614
+ * List positions for an exchange Arca object.
615
+ */
616
+ async listPositions(objectId) {
617
+ await this.ready();
618
+ return this.client.get(`/objects/${objectId}/exchange/positions`);
619
+ }
620
+ /**
621
+ * Get market metadata (supported assets).
622
+ */
623
+ async getMarketMeta() {
624
+ await this.ready();
625
+ return this.client.get('/exchange/market/meta');
626
+ }
627
+ /**
628
+ * Get current mid prices for all assets.
629
+ */
630
+ async getMarketMids() {
631
+ await this.ready();
632
+ return this.client.get('/exchange/market/mids');
633
+ }
634
+ /**
635
+ * Get L2 order book for a specific coin.
636
+ */
637
+ async getOrderBook(coin) {
638
+ await this.ready();
639
+ return this.client.get(`/exchange/market/book/${coin}`);
640
+ }
641
+ /**
642
+ * Get OHLCV candle data for a specific coin.
643
+ */
644
+ async getCandles(coin, interval, options) {
645
+ await this.ready();
646
+ const params = { interval };
647
+ if (options?.startTime !== undefined)
648
+ params.startTime = String(options.startTime);
649
+ if (options?.endTime !== undefined)
650
+ params.endTime = String(options.endTime);
651
+ return this.client.get(`/exchange/market/candles/${coin}`, params);
652
+ }
653
+ // ---- Invariants ----
654
+ /**
655
+ * Run all DB-only invariant checks (I1-I6) via the internal API.
656
+ * Returns structured results per invariant with pass/fail and violations.
657
+ */
658
+ async checkInvariants(limit) {
659
+ await this.ready();
660
+ const params = {};
661
+ if (limit !== undefined)
662
+ params.limit = String(limit);
663
+ return this.client.get('/internal/invariant-check', params);
664
+ }
665
+ /**
666
+ * Poll until all operations in the realm have reached a terminal state.
667
+ * Useful after a batch of async operations (deposits, exchange transfers)
668
+ * to wait for settlement before running invariant checks.
669
+ *
670
+ * @param opts.intervalMs - Polling interval in ms (default: 1000)
671
+ * @param opts.timeoutMs - Max wait time in ms (default: 120000)
672
+ * @param opts.onPoll - Optional callback with the current pending count
673
+ * @returns The number of polls performed
674
+ */
675
+ async waitForQuiescence(opts) {
676
+ await this.ready();
677
+ const interval = opts?.intervalMs ?? 1000;
678
+ const timeout = opts?.timeoutMs ?? 120_000;
679
+ const start = Date.now();
680
+ let polls = 0;
681
+ while (true) {
682
+ const res = await this.listOperations();
683
+ const pending = res.operations.filter(op => op.state === 'pending');
684
+ polls++;
685
+ opts?.onPoll?.(pending.length);
686
+ if (pending.length === 0)
687
+ return polls;
688
+ if (Date.now() - start > timeout) {
689
+ throw new Error(`Timed out waiting for quiescence after ${timeout}ms. ` +
690
+ `${pending.length} operations still pending.`);
691
+ }
692
+ await new Promise(r => setTimeout(r, interval));
693
+ }
694
+ }
695
+ /**
696
+ * Wait for a specific operation to reach a terminal state.
697
+ * Uses WebSocket events to resolve immediately when the operation updates.
698
+ * If the WS event arrives without embedded operation data (enrichment failure),
699
+ * fetches the operation via HTTP as a fallback.
700
+ */
701
+ async waitForOperation(operationId, timeoutMs = 30000) {
702
+ await this.ready();
703
+ return new Promise((resolve, reject) => {
704
+ let settled = false;
705
+ const timeout = setTimeout(() => {
706
+ cleanup();
707
+ reject(new Error(`Timed out waiting for operation ${operationId} after ${timeoutMs}ms`));
708
+ }, timeoutMs);
709
+ const cleanup = () => {
710
+ settled = true;
711
+ clearTimeout(timeout);
712
+ this.ws.off(types_1.EventType.OperationUpdated, handler);
713
+ };
714
+ const isTerminal = (s) => s !== 'pending';
715
+ const tryResolve = (op) => {
716
+ if (settled)
717
+ return;
718
+ if (isTerminal(op.state)) {
719
+ cleanup();
720
+ resolve(op);
721
+ }
722
+ };
723
+ const fetchAndResolve = () => {
724
+ if (settled)
725
+ return;
726
+ this.getOperation(operationId)
727
+ .then(detail => tryResolve(detail.operation))
728
+ .catch(() => { });
729
+ };
730
+ const handler = (event) => {
731
+ if (event.entityId !== operationId)
732
+ return;
733
+ if (event.operation) {
734
+ tryResolve(event.operation);
735
+ }
736
+ else {
737
+ fetchAndResolve();
738
+ }
739
+ };
740
+ this.ws.on(types_1.EventType.OperationUpdated, handler);
741
+ // Check immediately in case it already completed.
742
+ // If it 404s (Temporal race), retry once after a short delay.
743
+ this.getOperation(operationId)
744
+ .then(detail => tryResolve(detail.operation))
745
+ .catch(() => {
746
+ setTimeout(fetchAndResolve, 2000);
747
+ });
748
+ });
749
+ }
750
+ // ---- Internal ----
751
+ realmId() {
752
+ if (!this.resolvedRealmId) {
753
+ throw new Error('Arca SDK not initialized. Call await arca.ready() first.');
754
+ }
755
+ return this.resolvedRealmId;
756
+ }
757
+ async resolveRealm() {
758
+ // If already resolved (e.g., from token claims), we're done
759
+ if (this.resolvedRealmId)
760
+ return;
761
+ // If no realm input was provided (and token didn't contain one), error
762
+ if (!this.realmInput) {
763
+ throw new Error('No realm specified. Provide a realm in the config or use a scoped token that contains a realmId claim.');
764
+ }
765
+ // If it looks like a UUID or a TypeID (e.g. rlm_01h2xcejqtf2nbrexx3vqjhp41), use directly
766
+ if (isIdFormat(this.realmInput)) {
767
+ this.resolvedRealmId = this.realmInput;
768
+ return;
769
+ }
770
+ // Otherwise, resolve slug/name/id to ID by listing realms
771
+ const result = await this.client.get('/realms');
772
+ const realm = result.realms.find((r) => r.slug === this.realmInput || r.name === this.realmInput || r.id === this.realmInput);
773
+ if (!realm) {
774
+ throw new errors_1.NotFoundError(`Realm '${this.realmInput}' not found. Available realms: ${result.realms.map((r) => r.slug).join(', ')}`);
775
+ }
776
+ this.resolvedRealmId = realm.id;
777
+ }
778
+ }
779
+ exports.Arca = Arca;
780
+ //# sourceMappingURL=arca.js.map