@arcaresearch/sdk 0.1.61 → 0.1.63

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 CHANGED
@@ -26,6 +26,18 @@ const DEFAULT_BASE_URL = 'https://api.arcaos.io';
26
26
  const REFRESH_BUFFER_MS = 30_000; // refresh 30s before expiry
27
27
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
28
  const TYPEID_RE = /^[a-z]{2,63}_[0-9a-hjkmnp-tv-z]{26}$/i;
29
+ /**
30
+ * Validate that a path argument starts with '/'.
31
+ * Paths without a trailing slash target a single object.
32
+ * Paths with a trailing slash aggregate all objects under that prefix.
33
+ */
34
+ function validatePath(path) {
35
+ if (!path.startsWith('/')) {
36
+ throw new Error(`Path must start with '/'. Got: "${path}". ` +
37
+ `Use a trailing slash for aggregation (e.g., '/users/alice/') ` +
38
+ `or an exact path for a single object (e.g., '/users/alice/main').`);
39
+ }
40
+ }
29
41
  function isIdFormat(value) {
30
42
  return UUID_RE.test(value) || TYPEID_RE.test(value);
31
43
  }
@@ -310,13 +322,21 @@ class Arca {
310
322
  return this.client.get(`/objects/${objectId}`);
311
323
  }
312
324
  /**
313
- * List Arca objects, optionally filtered by path prefix.
325
+ * List Arca objects, optionally filtered by path.
326
+ *
327
+ * @param opts.path - Object path or path prefix to filter by.
328
+ * Exact path (no trailing slash): returns the single matching object.
329
+ * Path prefix (trailing slash): returns all objects under that prefix.
330
+ * Examples: '/users/alice/main' (single), '/users/alice/' (subtree)
314
331
  */
315
332
  async listObjects(opts) {
316
333
  await this.ready();
317
334
  const params = { realmId: this.realmId() };
318
- if (opts?.prefix)
319
- params.prefix = opts.prefix;
335
+ const p = opts?.path ?? opts?.prefix;
336
+ if (p) {
337
+ validatePath(p);
338
+ params.prefix = p;
339
+ }
320
340
  if (opts?.includeDeleted)
321
341
  params.includeDeleted = 'true';
322
342
  return this.client.get('/objects', params);
@@ -339,13 +359,17 @@ class Arca {
339
359
  }
340
360
  /**
341
361
  * Browse Arca objects in a folder-like structure.
342
- * Returns folders (path prefixes) and objects at the given prefix.
362
+ * Returns folders (path segments) and objects at the given path.
363
+ *
364
+ * @param opts.path - Path prefix to browse (default: '/'). Must end with '/'.
343
365
  */
344
366
  async browseObjects(opts) {
345
367
  await this.ready();
368
+ const p = opts?.path ?? opts?.prefix ?? '/';
369
+ validatePath(p);
346
370
  const params = {
347
371
  realmId: this.realmId(),
348
- prefix: opts?.prefix ?? '/',
372
+ prefix: p,
349
373
  };
350
374
  if (opts?.includeDeleted)
351
375
  params.includeDeleted = 'true';
@@ -543,16 +567,18 @@ class Arca {
543
567
  * inline inside an operation call, as each invocation produces a new
544
568
  * unique path and defeats idempotency.
545
569
  *
546
- * @param prefix - Path prefix (e.g. '/op/transfer/fund')
547
- * @param separator - Override separator between prefix and nonce number.
548
- * Default: '/' if prefix ends with '/', otherwise '-'.
570
+ * @param path - Path prefix for nonce generation (e.g. '/op/transfer/fund').
571
+ * Always used as a prefix the nonce number is appended.
572
+ * @param separator - Override separator between path and nonce number.
573
+ * Default: '/' if path ends with '/', otherwise '-'.
549
574
  * Use ':' for operation nonces (e.g. '/op/create/wallets/main:1').
550
575
  */
551
- async nonce(prefix, separator) {
576
+ async nonce(path, separator) {
577
+ validatePath(path);
552
578
  await this.ready();
553
579
  const body = {
554
580
  realmId: this.realmId(),
555
- prefix,
581
+ prefix: path,
556
582
  };
557
583
  if (separator !== undefined) {
558
584
  body.separator = separator;
@@ -571,47 +597,66 @@ class Arca {
571
597
  }
572
598
  // ---- Aggregation & P&L ----
573
599
  /**
574
- * Get aggregated valuation for all objects under a path prefix.
575
- * Pass `asOf` to get historical aggregation at a past timestamp.
576
- */
577
- async getPathAggregation(prefix, options) {
600
+ * Get aggregated valuation for objects at a path.
601
+ *
602
+ * @param path - Object path or path prefix.
603
+ * Exact path (no trailing slash): returns valuation for a single object.
604
+ * Path prefix (trailing slash): returns aggregated valuation for all objects under that prefix.
605
+ * Examples: '/users/alice/main' (single object), '/users/alice/' (all of alice's objects)
606
+ * @param options.asOf - ISO timestamp for historical aggregation.
607
+ */
608
+ async getPathAggregation(path, options) {
609
+ validatePath(path);
578
610
  await this.ready();
579
611
  const params = {
580
612
  realmId: this.realmId(),
581
- prefix,
613
+ prefix: path,
582
614
  };
583
615
  if (options?.asOf)
584
616
  params.asOf = options.asOf;
585
617
  return this.client.get('/objects/aggregate', params);
586
618
  }
587
619
  /**
588
- * Get P&L (profit and loss) for objects under a path prefix over a time range.
589
- * Returns starting/ending equity, net inflows/outflows, and calculated P&L.
590
- */
591
- async getPnl(prefix, from, to) {
620
+ * Get P&L (profit and loss) for objects at a path over a time range.
621
+ *
622
+ * @param path - Object path or path prefix.
623
+ * Exact path (no trailing slash): returns P&L for a single object.
624
+ * Path prefix (trailing slash): returns aggregated P&L for all objects under that prefix.
625
+ * Examples: '/users/alice/main' (single object), '/users/alice/' (all of alice's objects)
626
+ * @param from - ISO timestamp for the start of the range.
627
+ * @param to - ISO timestamp for the end of the range.
628
+ */
629
+ async getPnl(path, from, to) {
630
+ validatePath(path);
592
631
  await this.ready();
593
632
  return this.client.get('/objects/pnl', {
594
633
  realmId: this.realmId(),
595
- prefix,
634
+ prefix: path,
596
635
  from,
597
636
  to,
598
637
  });
599
638
  }
600
639
  /**
601
- * Get P&L history (time-series) for objects under a path prefix.
602
- * Returns P&L and equity values sampled evenly over the time range,
603
- * adjusted for external flows (deposits/withdrawals).
604
- * The `points` parameter controls how many samples (default 200, max 1000).
605
- */
606
- async getPnlHistory(prefix, from, to, points = 200) {
607
- const key = (0, cache_1.buildCacheKey)('pnlHistory', { prefix, from, to, points: String(points) });
640
+ * Get P&L history (time-series) for objects at a path.
641
+ *
642
+ * @param path - Object path or path prefix.
643
+ * Exact path (no trailing slash): returns P&L history for a single object.
644
+ * Path prefix (trailing slash): returns aggregated P&L history for all objects under that prefix.
645
+ * Examples: '/users/alice/main' (single object), '/users/alice/' (all of alice's objects)
646
+ * @param from - ISO timestamp for the start of the range.
647
+ * @param to - ISO timestamp for the end of the range.
648
+ * @param points - Number of samples (default 200, max 1000).
649
+ */
650
+ async getPnlHistory(path, from, to, points = 200) {
651
+ validatePath(path);
652
+ const key = (0, cache_1.buildCacheKey)('pnlHistory', { prefix: path, from, to, points: String(points) });
608
653
  const cached = this.historyCache.get(key);
609
654
  if (cached)
610
655
  return cached;
611
656
  await this.ready();
612
657
  const result = await this.client.get('/objects/pnl/history', {
613
658
  realmId: this.realmId(),
614
- prefix,
659
+ prefix: path,
615
660
  from,
616
661
  to,
617
662
  points: String(points),
@@ -620,19 +665,26 @@ class Arca {
620
665
  return result;
621
666
  }
622
667
  /**
623
- * Get equity history (time-series) for objects under a path prefix.
624
- * Returns equity values sampled evenly over the time range.
625
- * The `points` parameter controls how many samples (default 200, max 1000).
626
- */
627
- async getEquityHistory(prefix, from, to, points = 200) {
628
- const key = (0, cache_1.buildCacheKey)('equityHistory', { prefix, from, to, points: String(points) });
668
+ * Get equity history (time-series) for objects at a path.
669
+ *
670
+ * @param path - Object path or path prefix.
671
+ * Exact path (no trailing slash): returns equity history for a single object.
672
+ * Path prefix (trailing slash): returns aggregated equity history for all objects under that prefix.
673
+ * Examples: '/users/alice/main' (single object), '/users/alice/' (all of alice's objects)
674
+ * @param from - ISO timestamp for the start of the range.
675
+ * @param to - ISO timestamp for the end of the range.
676
+ * @param points - Number of samples (default 200, max 1000).
677
+ */
678
+ async getEquityHistory(path, from, to, points = 200) {
679
+ validatePath(path);
680
+ const key = (0, cache_1.buildCacheKey)('equityHistory', { prefix: path, from, to, points: String(points) });
629
681
  const cached = this.historyCache.get(key);
630
682
  if (cached)
631
683
  return cached;
632
684
  await this.ready();
633
685
  const result = await this.client.get('/objects/aggregate/history', {
634
686
  realmId: this.realmId(),
635
- prefix,
687
+ prefix: path,
636
688
  from,
637
689
  to,
638
690
  points: String(points),
@@ -645,18 +697,24 @@ class Arca {
645
697
  * aggregation updates. The last point reflects the current live equity;
646
698
  * when the interval boundary crosses, the live point is promoted to
647
699
  * historical and a new live point starts.
700
+ *
701
+ * @param path - Object path or path prefix.
702
+ * Exact path (no trailing slash): chart for a single object.
703
+ * Path prefix (trailing slash): chart aggregated across all objects under that prefix.
704
+ * Examples: '/users/alice/main' (single object), '/users/alice/' (all of alice's objects)
648
705
  */
649
- async watchEquityChart(prefix, from, to, points = 200, options) {
706
+ async watchEquityChart(path, from, to, points = 200, options) {
707
+ validatePath(path);
650
708
  await this.ready();
651
709
  const exchange = options?.exchange ?? 'sim';
652
710
  let history;
653
711
  try {
654
- history = await this.getEquityHistory(prefix, from, to, points);
712
+ history = await this.getEquityHistory(path, from, to, points);
655
713
  }
656
714
  catch {
657
715
  history = { equityPoints: [], resolution: undefined };
658
716
  }
659
- const aggStream = await this.watchAggregation([{ type: 'prefix', value: prefix }], { exchange });
717
+ const aggStream = await this.watchAggregation([{ type: 'prefix', value: path }], { exchange });
660
718
  return new streams_1.EquityChartStream(history.equityPoints, aggStream, history.resolution);
661
719
  }
662
720
  /**
@@ -664,13 +722,19 @@ class Arca {
664
722
  * aggregation updates and operation events. The rightmost point updates on
665
723
  * each aggregation event. Operation events (deposits/transfers) update
666
724
  * cumulative flows client-side — zero additional server reads.
725
+ *
726
+ * @param path - Object path or path prefix.
727
+ * Exact path (no trailing slash): P&L chart for a single object.
728
+ * Path prefix (trailing slash): P&L chart aggregated across all objects under that prefix.
729
+ * Examples: '/users/alice/main' (single object), '/users/alice/' (all of alice's objects)
667
730
  */
668
- async watchPnlChart(prefix, from, to, points = 200, options) {
731
+ async watchPnlChart(path, from, to, points = 200, options) {
732
+ validatePath(path);
669
733
  await this.ready();
670
734
  const exchange = options?.exchange ?? 'sim';
671
- const history = await this.getPnlHistory(prefix, from, to, points);
672
- const aggStream = await this.watchAggregation([{ type: 'prefix', value: prefix }], { exchange });
673
- return new streams_1.PnlChartStream(history.pnlPoints, history.externalFlows, history.startingEquityUsd, history.midPrices ?? {}, prefix, aggStream, this.ws, history.resolution);
735
+ const history = await this.getPnlHistory(path, from, to, points);
736
+ const aggStream = await this.watchAggregation([{ type: 'prefix', value: path }], { exchange });
737
+ return new streams_1.PnlChartStream(history.pnlPoints, history.externalFlows, history.startingEquityUsd, history.midPrices ?? {}, path, aggStream, this.ws, history.resolution);
674
738
  }
675
739
  /**
676
740
  * Create an aggregation watch that tracks a set of sources.
@@ -837,7 +901,6 @@ class Arca {
837
901
  getOrder: (objectId, orderId) => this.getOrder(objectId, orderId),
838
902
  onFillEvent: (handler) => {
839
903
  this.ws.ensureConnected();
840
- this.ws.subscribe(['exchange']);
841
904
  this.ws.on(types_1.EventType.ExchangeFill, handler);
842
905
  return () => this.ws.off(types_1.EventType.ExchangeFill, handler);
843
906
  },
@@ -933,7 +996,6 @@ class Arca {
933
996
  getOrder: (objectId, orderId) => this.getOrder(objectId, orderId),
934
997
  onFillEvent: (handler) => {
935
998
  this.ws.ensureConnected();
936
- this.ws.subscribe(['exchange']);
937
999
  this.ws.on(types_1.EventType.ExchangeFill, handler);
938
1000
  return () => this.ws.off(types_1.EventType.ExchangeFill, handler);
939
1001
  },
@@ -963,7 +1025,8 @@ class Arca {
963
1025
  }
964
1026
  /**
965
1027
  * Watch fills (trade history) for an exchange Arca object.
966
- * Returns a live-updating stream that combines an initial REST snapshot with
1028
+ * Resolves the object path from `objectId`, creates a path-scoped watch,
1029
+ * then combines an initial REST snapshot with live `exchange.fill` and
967
1030
  * `fill.recorded` WebSocket events. Each update contains a platform-level
968
1031
  * `Fill` with P&L, fee breakdown, direction, and resulting position.
969
1032
  *
@@ -976,20 +1039,17 @@ class Arca {
976
1039
  */
977
1040
  async watchFills(objectId, opts) {
978
1041
  await this.ready();
979
- let objectPath;
980
- try {
981
- const obj = await this.getObject(objectId);
982
- objectPath = obj.path;
983
- }
984
- catch { /* path matching is best-effort */ }
1042
+ const obj = await this.getObject(objectId);
1043
+ const objectPath = obj.path;
985
1044
  const stream = new streams_1.FillWatchStream(this.ws, () => this.listFills(objectId, opts), objectId, objectPath);
986
1045
  await stream.ready();
987
1046
  return stream;
988
1047
  }
989
1048
  /**
990
- * Subscribe to real-time funding payment events for an exchange Arca object.
991
- * Each update includes the ``FundingPayment`` and its ``EventEnvelope``
992
- * for correlation tracing.
1049
+ * Watch real-time funding payment events for an exchange Arca object.
1050
+ * Resolves the object path from `objectId`, creates a path-scoped watch,
1051
+ * then streams `exchange.funding` events. Each update includes the
1052
+ * `FundingPayment` and its `EventEnvelope` for correlation tracing.
993
1053
  *
994
1054
  * ```ts
995
1055
  * const stream = await arca.watchFunding('obj_...');
@@ -1001,12 +1061,8 @@ class Arca {
1001
1061
  */
1002
1062
  async watchFunding(objectId) {
1003
1063
  await this.ready();
1004
- let objectPath;
1005
- try {
1006
- const obj = await this.getObject(objectId);
1007
- objectPath = obj.path;
1008
- }
1009
- catch { /* path matching is best-effort */ }
1064
+ const obj = await this.getObject(objectId);
1065
+ const objectPath = obj.path;
1010
1066
  const stream = new streams_1.FundingWatchStream(this.ws, objectId, objectPath);
1011
1067
  await stream.ready();
1012
1068
  return stream;
@@ -1129,30 +1185,33 @@ class Arca {
1129
1185
  return stream;
1130
1186
  }
1131
1187
  /**
1132
- * Subscribe to real-time operation events.
1133
- * Server sends an initial snapshot of recent operations, then streams
1134
- * creates and updates. Returns an {@link OperationWatchStream}.
1135
- * Call `.close()` when done.
1188
+ * Watch real-time operation events under a path prefix.
1189
+ * Creates a path-scoped watch; server sends a `watch_snapshot` with
1190
+ * recent operations, then streams `operation.created` / `operation.updated`.
1191
+ * Returns an {@link OperationWatchStream}. Call `.close()` when done.
1192
+ *
1193
+ * @param path - Arca path prefix to watch (default: "/" for all operations)
1136
1194
  */
1137
- async watchOperations() {
1195
+ async watchOperations(path = '/') {
1138
1196
  await this.ready();
1139
- const stream = new streams_1.OperationWatchStream(this.ws, 'operations', async () => {
1140
- const res = await this.listOperations();
1197
+ const stream = new streams_1.OperationWatchStream(this.ws, path, async () => {
1198
+ const res = await this.listOperations(path !== '/' ? { path } : undefined);
1141
1199
  return res.operations;
1142
1200
  });
1143
1201
  await stream.ready();
1144
1202
  return stream;
1145
1203
  }
1146
1204
  /**
1147
- * Subscribe to real-time balance updates.
1148
- * Server sends an initial snapshot of all balances, then streams updates.
1149
- * Optionally filter by an Arca path prefix. Call `.close()` when done.
1205
+ * Watch real-time balance updates under a path prefix.
1206
+ * Creates a path-scoped watch; server sends a `watch_snapshot` with
1207
+ * current balances (four-bucket summary), then streams `balance.updated`.
1208
+ * Call `.close()` when done.
1150
1209
  *
1151
- * @param arcaRef - Optional path filter (e.g. "/wallets/main")
1210
+ * @param path - Arca path prefix to watch (default: "/" for all balances)
1152
1211
  */
1153
- async watchBalances(arcaRef) {
1212
+ async watchBalances(path = '/') {
1154
1213
  await this.ready();
1155
- const stream = new streams_1.BalanceWatchStream(this.ws, 'balances', arcaRef, (entityId) => this.getBalances(entityId));
1214
+ const stream = new streams_1.BalanceWatchStream(this.ws, path, (entityId) => this.getBalances(entityId));
1156
1215
  await stream.ready();
1157
1216
  return stream;
1158
1217
  }
@@ -1168,9 +1227,13 @@ class Arca {
1168
1227
  });
1169
1228
  }
1170
1229
  /**
1171
- * Subscribe to real-time valuation updates for a single Arca object.
1172
- * Uses the same computation path as aggregation watches (Axiom 10).
1173
- * Call `.close()` when done.
1230
+ * Watch real-time valuation updates for a single Arca object.
1231
+ * Creates a path-scoped watch with an aggregation watch for valuation;
1232
+ * server sends `watch_snapshot` with initial valuation, then streams
1233
+ * `object.valuation` events. Uses the same computation path as
1234
+ * aggregation watches (Axiom 10). Call `.close()` when done.
1235
+ *
1236
+ * @param path - Arca object path (e.g. "/users/alice/main")
1174
1237
  */
1175
1238
  async watchObject(path) {
1176
1239
  await this.ready();
@@ -1236,6 +1299,7 @@ class Arca {
1236
1299
  /**
1237
1300
  * Watch the exchange state for an exchange Arca object, including positions,
1238
1301
  * open orders, and pending intents (order operations in flight).
1302
+ * Resolves the object path from `objectId` and creates a path-scoped watch.
1239
1303
  *
1240
1304
  * Pending intents are the exchange equivalent of transfer holds: they signal
1241
1305
  * that an order has been submitted but the venue hasn't confirmed it yet.
@@ -1243,7 +1307,9 @@ class Arca {
1243
1307
  */
1244
1308
  async watchExchangeState(objectId) {
1245
1309
  await this.ready();
1246
- return new streams_1.ExchangeWatchStream(this.ws, objectId, (id) => this.getExchangeState(id));
1310
+ const obj = await this.getObject(objectId);
1311
+ const objectPath = obj.path;
1312
+ return new streams_1.ExchangeWatchStream(this.ws, objectPath, objectId, (id) => this.getExchangeState(id));
1247
1313
  }
1248
1314
  // ---- Auto-tracking ----
1249
1315
  _autoTrackOps;
@@ -1303,7 +1369,7 @@ class Arca {
1303
1369
  const start = Date.now();
1304
1370
  let polls = 0;
1305
1371
  this.ws.ensureConnected();
1306
- this.ws.subscribe(['operations']);
1372
+ this.ws.watchPath('/').catch(() => { });
1307
1373
  const checkPending = async () => {
1308
1374
  const res = await this.listOperations();
1309
1375
  polls++;
@@ -1338,6 +1404,7 @@ class Arca {
1338
1404
  clearTimeout(timer);
1339
1405
  clearInterval(safetyPoll);
1340
1406
  this.ws.off(types_1.EventType.OperationUpdated, handler);
1407
+ this.ws.unwatchPath('/');
1341
1408
  };
1342
1409
  const handler = async () => {
1343
1410
  if (settled)
@@ -1382,7 +1449,7 @@ class Arca {
1382
1449
  async waitForOperation(operationId, timeoutMs = 30000) {
1383
1450
  await this.ready();
1384
1451
  this.ws.ensureConnected();
1385
- this.ws.subscribe(['operations']);
1452
+ this.ws.watchPath('/').catch(() => { });
1386
1453
  const RECONCILIATION_INTERVAL = 5000;
1387
1454
  return new Promise((resolve, reject) => {
1388
1455
  let settled = false;
@@ -1402,6 +1469,7 @@ class Arca {
1402
1469
  clearTimeout(timeout);
1403
1470
  clearInterval(reconciliationPoll);
1404
1471
  this.ws.off(types_1.EventType.OperationUpdated, handler);
1472
+ this.ws.unwatchPath('/');
1405
1473
  };
1406
1474
  const isTerminal = (s) => s !== 'pending';
1407
1475
  const tryResolve = (op) => {