@pear-protocol/utils 1.0.0 → 1.2.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.
@@ -8,5 +8,9 @@ import type { Fill } from './fill.types';
8
8
  * - Reduce position : entry price unchanged
9
9
  * - Close to zero : keep last entry price (for tracking purpose)
10
10
  * - Flip sign : entry price resets to flip fill's price
11
+ *
12
+ * A 0-price (unpriceable synthetic) fill that opens or adds to the held size taints the
13
+ * basis until the size flattens or flips: the entry is then unknown and reported as zero,
14
+ * the same "no entry" answer callers already get for an empty ledger.
11
15
  */
12
16
  export declare function computeEntryPriceForAsset(fills: Fill[]): BigNumber;
package/dist/index.d.ts CHANGED
@@ -10,4 +10,5 @@ export * from './proxy';
10
10
  export * from './sync/index';
11
11
  export * from './validate-leverage';
12
12
  export * from './validate-quantity';
13
+ export * from './vaults/index';
13
14
  export * from './xauth/index';
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import BigNumber7 from 'bignumber.js';
2
2
  import { sha256 } from '@noble/hashes/sha2';
3
- import { bytesToHex, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
3
+ import { bytesToHex, utf8ToBytes, randomBytes } from '@noble/hashes/utils';
4
4
  import { WATERMARK_FUTURE_SKEW_MS } from '@pear-protocol/types';
5
+ import { keccak_256 } from '@noble/hashes/sha3';
5
6
  import { base64urlnopad } from '@scure/base';
6
7
 
7
8
  var __defProp = Object.defineProperty;
@@ -124,6 +125,7 @@ __name(compareValue, "compareValue");
124
125
  function computeEntryPriceForAsset(fills) {
125
126
  let entryPrice = ZERO;
126
127
  let positionSize = ZERO;
128
+ let unpriced = false;
127
129
  for (const fill of fills) {
128
130
  const price = new BigNumber7(fill.price);
129
131
  const quantity = new BigNumber7(fill.quantity);
@@ -133,11 +135,16 @@ function computeEntryPriceForAsset(fills) {
133
135
  const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
134
136
  if (positionSize.isZero() || isFlip) {
135
137
  entryPrice = price;
138
+ unpriced = price.isZero();
136
139
  } else if (isSameDirection) {
137
140
  entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
141
+ unpriced = unpriced || price.isZero();
138
142
  }
139
143
  positionSize = newPositionSize;
140
144
  }
145
+ if (unpriced) {
146
+ return ZERO;
147
+ }
141
148
  return entryPrice;
142
149
  }
143
150
  __name(computeEntryPriceForAsset, "computeEntryPriceForAsset");
@@ -381,6 +388,9 @@ function computeAssetUnrealizedPnLs(exposureMap, priceMap, fills) {
381
388
  const currentPrice = new BigNumber7(priceString);
382
389
  const assetFills = fills.filter((fill) => fill.symbol === id);
383
390
  const entryPrice = computeEntryPriceForAsset(assetFills);
391
+ if (entryPrice.isZero()) {
392
+ continue;
393
+ }
384
394
  const upnl = signedQuantity.multipliedBy(currentPrice.minus(entryPrice));
385
395
  result.push({
386
396
  id,
@@ -1133,6 +1143,7 @@ function precisePrice(connector, raw, precision, side = "BUY", slippageTolerance
1133
1143
  case "bybit":
1134
1144
  case "okx":
1135
1145
  case "lighter":
1146
+ case "lighter_rhc":
1136
1147
  return preciseTickGridPrice(effectiveRaw, precision, side);
1137
1148
  }
1138
1149
  }
@@ -1197,6 +1208,7 @@ function preciseQuantity(connector, raw, precision, side) {
1197
1208
  case "bybit":
1198
1209
  case "okx":
1199
1210
  case "lighter":
1211
+ case "lighter_rhc":
1200
1212
  return preciseStepGridQuantity(raw, precision, side);
1201
1213
  }
1202
1214
  }
@@ -1266,6 +1278,7 @@ __name(proxyFetch, "proxyFetch");
1266
1278
 
1267
1279
  // src/sync/applicables/constants.ts
1268
1280
  var DUST = parse("0.00000001");
1281
+ var LATE_FILL_GRACE_MS = 5 * 6e4;
1269
1282
  var MISSED_HISTORY_FILL_PREFIX = "opening-synthetic-";
1270
1283
  var BASKET_RULES = [
1271
1284
  // V2 Basket Hyperliquid — legs of one Pear basket order share the order's cloid.
@@ -1278,13 +1291,22 @@ function isPearCloid(connector, cloid) {
1278
1291
  if (!cloid) {
1279
1292
  return false;
1280
1293
  }
1281
- return PEAR_CLOID_CHECK[connector]?.(cloid) ?? false;
1294
+ return PEAR_CLOID_CHECK[connector](cloid);
1282
1295
  }
1283
1296
  __name(isPearCloid, "isPearCloid");
1284
1297
  var PEAR_CLOID_CHECK = {
1285
1298
  hyperliquid: /* @__PURE__ */ __name((cloid) => cloid.startsWith("0x50454152"), "hyperliquid"),
1286
- lighter: /* @__PURE__ */ __name((cloid) => cloid.length === 15 && cloid.startsWith("10"), "lighter")
1299
+ lighter: isLighterPearCloid,
1300
+ lighter_rhc: isLighterPearCloid,
1301
+ // No Pear brand is minted into a CEX client order id, so provenance cannot be read from it.
1302
+ binance: /* @__PURE__ */ __name(() => false, "binance"),
1303
+ bybit: /* @__PURE__ */ __name(() => false, "bybit"),
1304
+ okx: /* @__PURE__ */ __name(() => false, "okx")
1287
1305
  };
1306
+ function isLighterPearCloid(cloid) {
1307
+ return cloid.length === 15 && cloid.startsWith("10");
1308
+ }
1309
+ __name(isLighterPearCloid, "isLighterPearCloid");
1288
1310
 
1289
1311
  // src/sync/applicables/basket.ts
1290
1312
  var Basket = class _Basket {
@@ -1292,12 +1314,14 @@ var Basket = class _Basket {
1292
1314
  __name(this, "Basket");
1293
1315
  }
1294
1316
  legs;
1295
- constructor(legs) {
1317
+ fills;
1318
+ constructor(legs, fills) {
1296
1319
  this.legs = legs;
1320
+ this.fills = fills;
1297
1321
  }
1298
1322
  /** Detect baskets from a fill stream, returned in ascending time order. */
1299
1323
  static detect(fills) {
1300
- return groupFillsIntoBaskets(fills).map((group) => new _Basket(combineFillsBySymbolAndSide(group)));
1324
+ return groupFillsIntoBaskets(fills).map((group) => new _Basket(combineFillsBySymbolAndSide(group), group));
1301
1325
  }
1302
1326
  /** Active long symbol set the matcher compares against open positions. */
1303
1327
  get longs() {
@@ -1504,189 +1528,6 @@ var Exposure = class _Exposure {
1504
1528
  }
1505
1529
  };
1506
1530
 
1507
- // src/sync/applicables/matching.ts
1508
- function matchPositions(args) {
1509
- const { tradeAccountId, orphanFills, pear } = args;
1510
- const book = WorkingBook.load({
1511
- tradeAccountId,
1512
- openPositions: pear.positions()
1513
- });
1514
- for (const basket of Basket.detect(orphanFills)) {
1515
- const exactCloseTarget = findExactCloseTarget(book.open(), basket.longs, basket.shorts);
1516
- const leftover = applyCloses({
1517
- legs: basket.legs,
1518
- book,
1519
- exactCloseTarget
1520
- });
1521
- if (leftover.length === 0) {
1522
- continue;
1523
- }
1524
- const target = resolveLeftoverTarget({
1525
- book,
1526
- leftover
1527
- });
1528
- applyLeftover(target, leftover);
1529
- }
1530
- return book.toMatched();
1531
- }
1532
- __name(matchPositions, "matchPositions");
1533
- var WorkingBook = class WorkingBook2 {
1534
- static {
1535
- __name(this, "WorkingBook");
1536
- }
1537
- tradeAccountId;
1538
- all = [];
1539
- openByKey = /* @__PURE__ */ new Map();
1540
- constructor(tradeAccountId) {
1541
- this.tradeAccountId = tradeAccountId;
1542
- }
1543
- /** Seed the book from existing open DB positions (they arrive pre-filtered by connector). */
1544
- static load(args) {
1545
- const { tradeAccountId, openPositions } = args;
1546
- const book = new WorkingBook2(tradeAccountId);
1547
- for (const p of openPositions) {
1548
- const position = {
1549
- // Trust the position's own stored key — never recompute it from exposure.
1550
- // A partial fill on the original order leaves exposure short of the
1551
- // symbols the key was hashed from, so a recomputed key would never match.
1552
- positionKey: p.positionKey,
1553
- exposure: Exposure.fromRecord(p.exposure),
1554
- parts: [],
1555
- closed: false,
1556
- existingOrigin: p.origin
1557
- };
1558
- book.all.push(position);
1559
- book.openByKey.set(position.positionKey, position);
1560
- }
1561
- return book;
1562
- }
1563
- /** Deterministic position key for a symbol set under this book's trade account. */
1564
- keyFor(symbols) {
1565
- return generateDeterministicPositionKey({
1566
- tradeAccountId: this.tradeAccountId,
1567
- symbols
1568
- });
1569
- }
1570
- /** Currently open working positions, in insertion order. */
1571
- open() {
1572
- return this.all.filter((position) => !position.closed);
1573
- }
1574
- /** Look up an open working position by its positionKey (for dedup on merge). */
1575
- findOpenByKey(key) {
1576
- return this.openByKey.get(key);
1577
- }
1578
- /** Register a fresh empty working position under `key` and return it. */
1579
- create(key) {
1580
- const position = {
1581
- positionKey: key,
1582
- exposure: Exposure.empty(),
1583
- parts: [],
1584
- closed: false
1585
- };
1586
- this.all.push(position);
1587
- this.openByKey.set(key, position);
1588
- return position;
1589
- }
1590
- /** Flip a position to closed once every leg is within DUST of zero. */
1591
- markClosedIfFlat(position) {
1592
- if (position.closed || !position.exposure.isFlat()) {
1593
- return;
1594
- }
1595
- position.exposure.snapToZero();
1596
- position.closed = true;
1597
- this.openByKey.delete(position.positionKey);
1598
- }
1599
- /** Working positions that received fills become MatchedPosition records; untouched ones are dropped. */
1600
- toMatched() {
1601
- return this.all.filter((position) => position.parts.length > 0).map((position) => ({
1602
- positionKey: position.positionKey,
1603
- parts: position.parts,
1604
- closed: position.closed,
1605
- exposure: position.exposure,
1606
- existingOrigin: position.existingOrigin
1607
- }));
1608
- }
1609
- };
1610
- function findExactCloseTarget(openList, basketLongs, basketShorts) {
1611
- return openList.find((position) => {
1612
- const { longs, shorts } = position.exposure.activeLegs();
1613
- return setEq(longs, basketShorts) && setEq(shorts, basketLongs);
1614
- });
1615
- }
1616
- __name(findExactCloseTarget, "findExactCloseTarget");
1617
- var setEq = /* @__PURE__ */ __name((a, b) => a.size === b.size && [
1618
- ...a
1619
- ].every((x) => b.has(x)), "setEq");
1620
- function applyCloses(args) {
1621
- const { legs, book, exactCloseTarget } = args;
1622
- const openList = book.open();
1623
- const leftover = [];
1624
- for (const leg of legs) {
1625
- let remaining = leg.totalSize;
1626
- const closeTargetIsLong = sideSign(leg.side) === -1;
1627
- const holders = openList.filter((position) => {
1628
- const qty = position.exposure.get(leg.symbol);
1629
- if (qty.isZero()) {
1630
- return false;
1631
- }
1632
- return closeTargetIsLong ? qty.isPositive() : qty.isNegative();
1633
- });
1634
- if (exactCloseTarget) {
1635
- holders.sort((a, b) => a === exactCloseTarget ? -1 : b === exactCloseTarget ? 1 : 0);
1636
- }
1637
- for (const position of holders) {
1638
- if (isLessThanOrEqual(remaining, ZERO)) {
1639
- break;
1640
- }
1641
- const have = position.exposure.get(leg.symbol).abs();
1642
- const reduce = min(have, remaining);
1643
- position.parts.push(...leg.take(reduce));
1644
- position.exposure.addDelta(leg.symbol, signedBySide(reduce, leg.side));
1645
- remaining = remaining.minus(reduce);
1646
- book.markClosedIfFlat(position);
1647
- }
1648
- if (isGreaterThanOrEqual(remaining, DUST)) {
1649
- leftover.push({
1650
- side: leg.side,
1651
- qty: remaining,
1652
- leg
1653
- });
1654
- }
1655
- }
1656
- return leftover;
1657
- }
1658
- __name(applyCloses, "applyCloses");
1659
- function resolveLeftoverTarget(args) {
1660
- const { book, leftover } = args;
1661
- const leftoverLongs = new Set(leftover.filter((entry) => entry.side === "BUY").map((entry) => entry.leg.symbol));
1662
- const leftoverShorts = new Set(leftover.filter((entry) => entry.side === "SELL").map((entry) => entry.leg.symbol));
1663
- const target = book.open().find((position) => {
1664
- const { longs, shorts } = position.exposure.activeLegs();
1665
- const sameActiveShape = setEq(leftoverLongs, longs) && setEq(leftoverShorts, shorts);
1666
- const noDormantLegs = position.exposure.symbols().length === longs.size + shorts.size;
1667
- return sameActiveShape && noDormantLegs;
1668
- });
1669
- if (target) {
1670
- return target;
1671
- }
1672
- const symbols = [
1673
- .../* @__PURE__ */ new Set([
1674
- ...leftoverLongs,
1675
- ...leftoverShorts
1676
- ])
1677
- ];
1678
- const positionKey = book.keyFor(symbols);
1679
- return book.findOpenByKey(positionKey) ?? book.create(positionKey);
1680
- }
1681
- __name(resolveLeftoverTarget, "resolveLeftoverTarget");
1682
- function applyLeftover(target, leftover) {
1683
- for (const entry of leftover) {
1684
- target.parts.push(...entry.leg.take(entry.qty));
1685
- target.exposure.addSigned(entry.leg.symbol, entry.qty.toFixed(), entry.side);
1686
- }
1687
- }
1688
- __name(applyLeftover, "applyLeftover");
1689
-
1690
1531
  // src/sync/applicables/synthetics.ts
1691
1532
  function isMissedHistoryFill(fill) {
1692
1533
  return fill.exchangeFillId.startsWith(MISSED_HISTORY_FILL_PREFIX);
@@ -1881,7 +1722,8 @@ function assignSyntheticsToPositions(args) {
1881
1722
  const positionKey = existing ? existing.positionKey : keyOf([
1882
1723
  symbol
1883
1724
  ]);
1884
- target = positions.find((pos) => pos.positionKey === positionKey) ?? created.find((pos) => pos.positionKey === positionKey);
1725
+ const isKeyTarget = /* @__PURE__ */ __name((pos) => pos.positionKey === positionKey && (!existing || pos.existingOrigin !== void 0), "isKeyTarget");
1726
+ target = positions.find(isKeyTarget) ?? created.find(isKeyTarget);
1885
1727
  if (!target) {
1886
1728
  target = existing ? (
1887
1729
  // seed its exposure and origin so folding the synthetic yields the true
@@ -1958,6 +1800,223 @@ function findGapToTarget(args) {
1958
1800
  }
1959
1801
  __name(findGapToTarget, "findGapToTarget");
1960
1802
 
1803
+ // src/sync/applicables/matching.ts
1804
+ function matchPositions(args) {
1805
+ const { tradeAccountId, orphanFills, pear } = args;
1806
+ const book = WorkingBook.load({
1807
+ tradeAccountId,
1808
+ openPositions: pear.positions()
1809
+ });
1810
+ for (const basket of Basket.detect(orphanFills)) {
1811
+ const at = basketTime(basket);
1812
+ book.foldIntoLive(at);
1813
+ const exactCloseTarget = findExactCloseTarget(book.live(at), basket.longs, basket.shorts);
1814
+ const leftover = applyCloses({
1815
+ legs: basket.legs,
1816
+ book,
1817
+ exactCloseTarget,
1818
+ at
1819
+ });
1820
+ if (leftover.length === 0) {
1821
+ continue;
1822
+ }
1823
+ const target = resolveLeftoverTarget({
1824
+ book,
1825
+ leftover,
1826
+ at
1827
+ });
1828
+ applyLeftover(target, leftover);
1829
+ }
1830
+ book.foldIntoLive(Number.POSITIVE_INFINITY);
1831
+ return book.toMatched();
1832
+ }
1833
+ __name(matchPositions, "matchPositions");
1834
+ function basketTime(basket) {
1835
+ const realTimestamps = basket.fills.filter((fill) => !isMissedHistoryFill(fill)).map((fill) => fill.timestamp);
1836
+ if (realTimestamps.length === 0) {
1837
+ return Number.POSITIVE_INFINITY;
1838
+ }
1839
+ return Math.min(...realTimestamps);
1840
+ }
1841
+ __name(basketTime, "basketTime");
1842
+ var WorkingBook = class WorkingBook2 {
1843
+ static {
1844
+ __name(this, "WorkingBook");
1845
+ }
1846
+ tradeAccountId;
1847
+ all = [];
1848
+ constructor(tradeAccountId) {
1849
+ this.tradeAccountId = tradeAccountId;
1850
+ }
1851
+ /** Seed the book from existing open DB positions (they arrive pre-filtered by connector). */
1852
+ static load(args) {
1853
+ const { tradeAccountId, openPositions } = args;
1854
+ const book = new WorkingBook2(tradeAccountId);
1855
+ for (const p of openPositions) {
1856
+ const position = {
1857
+ // Trust the position's own stored key — never recompute it from exposure.
1858
+ // A partial fill on the original order leaves exposure short of the
1859
+ // symbols the key was hashed from, so a recomputed key would never match.
1860
+ positionKey: p.positionKey,
1861
+ exposure: Exposure.fromRecord(p.exposure),
1862
+ parts: [],
1863
+ closed: false,
1864
+ existingOrigin: p.origin,
1865
+ // A seeded position takes fills from its creation, less the late-arrival grace.
1866
+ liveFrom: Date.parse(p.createdAt) - LATE_FILL_GRACE_MS
1867
+ };
1868
+ book.all.push(position);
1869
+ }
1870
+ return book;
1871
+ }
1872
+ /** Deterministic position key for a symbol set under this book's trade account. */
1873
+ keyFor(symbols) {
1874
+ return generateDeterministicPositionKey({
1875
+ tradeAccountId: this.tradeAccountId,
1876
+ symbols
1877
+ });
1878
+ }
1879
+ /** Working positions open and already existing at basket time `at`, in insertion order. */
1880
+ live(at) {
1881
+ return this.all.filter((position) => !position.closed && position.liveFrom <= at);
1882
+ }
1883
+ /** Look up a position open and existing at `at` by its positionKey (for dedup on merge). */
1884
+ findLiveByKey(key, at) {
1885
+ return this.live(at).find((position) => position.positionKey === key);
1886
+ }
1887
+ /**
1888
+ * Fold every open position opened mid-replay into the open seeded position sharing its
1889
+ * key once that seeded position is live at `at`. The earlier open was still holding
1890
+ * exposure when the seeded position was created, and the server keeps at most one open
1891
+ * row per key. Ownership is ambiguous here — exposure and timestamps cannot prove the
1892
+ * seeded position traded on top of that history — so the fold conservatively keeps
1893
+ * the attribution the matcher gave before #1067.
1894
+ */
1895
+ foldIntoLive(at) {
1896
+ for (const seeded of this.live(at).filter((position) => position.existingOrigin !== void 0)) {
1897
+ const earlier = this.all.filter((position) => position.existingOrigin === void 0 && !position.closed && position.positionKey === seeded.positionKey);
1898
+ for (const position of earlier) {
1899
+ seeded.parts.push(...position.parts);
1900
+ for (const symbol of position.exposure.symbols()) {
1901
+ seeded.exposure.addDelta(symbol, position.exposure.get(symbol));
1902
+ }
1903
+ this.all = this.all.filter((candidate) => candidate !== position);
1904
+ }
1905
+ this.markClosedIfFlat(seeded);
1906
+ }
1907
+ }
1908
+ /** Register a fresh empty working position under `key` and return it. */
1909
+ create(key) {
1910
+ const position = {
1911
+ positionKey: key,
1912
+ exposure: Exposure.empty(),
1913
+ parts: [],
1914
+ closed: false,
1915
+ // Opened by this replay's own fills, so it takes fills from the start.
1916
+ liveFrom: Number.NEGATIVE_INFINITY
1917
+ };
1918
+ this.all.push(position);
1919
+ return position;
1920
+ }
1921
+ /** Flip a position to closed once every leg is within DUST of zero. */
1922
+ markClosedIfFlat(position) {
1923
+ if (position.closed || !position.exposure.isFlat()) {
1924
+ return;
1925
+ }
1926
+ position.exposure.snapToZero();
1927
+ position.closed = true;
1928
+ }
1929
+ /** Working positions that received fills become MatchedPosition records; untouched ones are dropped. */
1930
+ toMatched() {
1931
+ return this.all.filter((position) => position.parts.length > 0).map((position) => ({
1932
+ positionKey: position.positionKey,
1933
+ parts: position.parts,
1934
+ closed: position.closed,
1935
+ exposure: position.exposure,
1936
+ existingOrigin: position.existingOrigin
1937
+ }));
1938
+ }
1939
+ };
1940
+ function findExactCloseTarget(openList, basketLongs, basketShorts) {
1941
+ return openList.find((position) => {
1942
+ const { longs, shorts } = position.exposure.activeLegs();
1943
+ return setEq(longs, basketShorts) && setEq(shorts, basketLongs);
1944
+ });
1945
+ }
1946
+ __name(findExactCloseTarget, "findExactCloseTarget");
1947
+ var setEq = /* @__PURE__ */ __name((a, b) => a.size === b.size && [
1948
+ ...a
1949
+ ].every((x) => b.has(x)), "setEq");
1950
+ function applyCloses(args) {
1951
+ const { legs, book, exactCloseTarget, at } = args;
1952
+ const openList = book.live(at);
1953
+ const leftover = [];
1954
+ for (const leg of legs) {
1955
+ let remaining = leg.totalSize;
1956
+ const closeTargetIsLong = sideSign(leg.side) === -1;
1957
+ const holders = openList.filter((position) => {
1958
+ const qty = position.exposure.get(leg.symbol);
1959
+ if (qty.isZero()) {
1960
+ return false;
1961
+ }
1962
+ return closeTargetIsLong ? qty.isPositive() : qty.isNegative();
1963
+ });
1964
+ if (exactCloseTarget) {
1965
+ holders.sort((a, b) => a === exactCloseTarget ? -1 : b === exactCloseTarget ? 1 : 0);
1966
+ }
1967
+ for (const position of holders) {
1968
+ if (isLessThanOrEqual(remaining, ZERO)) {
1969
+ break;
1970
+ }
1971
+ const have = position.exposure.get(leg.symbol).abs();
1972
+ const reduce = min(have, remaining);
1973
+ position.parts.push(...leg.take(reduce));
1974
+ position.exposure.addDelta(leg.symbol, signedBySide(reduce, leg.side));
1975
+ remaining = remaining.minus(reduce);
1976
+ book.markClosedIfFlat(position);
1977
+ }
1978
+ if (isGreaterThanOrEqual(remaining, DUST)) {
1979
+ leftover.push({
1980
+ side: leg.side,
1981
+ qty: remaining,
1982
+ leg
1983
+ });
1984
+ }
1985
+ }
1986
+ return leftover;
1987
+ }
1988
+ __name(applyCloses, "applyCloses");
1989
+ function resolveLeftoverTarget(args) {
1990
+ const { book, leftover, at } = args;
1991
+ const leftoverLongs = new Set(leftover.filter((entry) => entry.side === "BUY").map((entry) => entry.leg.symbol));
1992
+ const leftoverShorts = new Set(leftover.filter((entry) => entry.side === "SELL").map((entry) => entry.leg.symbol));
1993
+ const target = book.live(at).find((position) => {
1994
+ const { longs, shorts } = position.exposure.activeLegs();
1995
+ const sameActiveShape = setEq(leftoverLongs, longs) && setEq(leftoverShorts, shorts);
1996
+ const noDormantLegs = position.exposure.symbols().length === longs.size + shorts.size;
1997
+ return sameActiveShape && noDormantLegs;
1998
+ });
1999
+ if (target) {
2000
+ return target;
2001
+ }
2002
+ const symbols = [
2003
+ .../* @__PURE__ */ new Set([
2004
+ ...leftoverLongs,
2005
+ ...leftoverShorts
2006
+ ])
2007
+ ];
2008
+ const positionKey = book.keyFor(symbols);
2009
+ return book.findLiveByKey(positionKey, at) ?? book.create(positionKey);
2010
+ }
2011
+ __name(resolveLeftoverTarget, "resolveLeftoverTarget");
2012
+ function applyLeftover(target, leftover) {
2013
+ for (const entry of leftover) {
2014
+ target.parts.push(...entry.leg.take(entry.qty));
2015
+ target.exposure.addSigned(entry.leg.symbol, entry.qty.toFixed(), entry.side);
2016
+ }
2017
+ }
2018
+ __name(applyLeftover, "applyLeftover");
2019
+
1961
2020
  // src/sync/applicables/fills.ts
1962
2021
  function assignPartIds(positions) {
1963
2022
  const bySource = /* @__PURE__ */ new Map();
@@ -2618,6 +2677,246 @@ function validateQuantity(quantity, price, precision) {
2618
2677
  return true;
2619
2678
  }
2620
2679
  __name(validateQuantity, "validateQuantity");
2680
+ var VAULT_ACTION_DOMAIN = {
2681
+ name: "PearVaultService",
2682
+ version: "1",
2683
+ chainId: 999
2684
+ };
2685
+ var VAULT_ACTION_TYPES = {
2686
+ "withdraw-to-evm": {
2687
+ WithdrawToEvm: [
2688
+ {
2689
+ name: "vault",
2690
+ type: "address"
2691
+ },
2692
+ {
2693
+ name: "amount",
2694
+ type: "string"
2695
+ },
2696
+ {
2697
+ name: "token",
2698
+ type: "string"
2699
+ },
2700
+ {
2701
+ name: "nonce",
2702
+ type: "string"
2703
+ },
2704
+ {
2705
+ name: "deadline",
2706
+ type: "uint256"
2707
+ }
2708
+ ]
2709
+ },
2710
+ "set-trader": {
2711
+ SetTrader: [
2712
+ {
2713
+ name: "vault",
2714
+ type: "address"
2715
+ },
2716
+ {
2717
+ name: "trader",
2718
+ type: "address"
2719
+ },
2720
+ {
2721
+ name: "nonce",
2722
+ type: "string"
2723
+ },
2724
+ {
2725
+ name: "deadline",
2726
+ type: "uint256"
2727
+ }
2728
+ ]
2729
+ },
2730
+ "agent-approve": {
2731
+ AgentApprove: [
2732
+ {
2733
+ name: "vault",
2734
+ type: "address"
2735
+ },
2736
+ {
2737
+ name: "nonce",
2738
+ type: "string"
2739
+ },
2740
+ {
2741
+ name: "deadline",
2742
+ type: "uint256"
2743
+ }
2744
+ ]
2745
+ },
2746
+ "set-unified-account": {
2747
+ SetUnifiedAccount: [
2748
+ {
2749
+ name: "vault",
2750
+ type: "address"
2751
+ },
2752
+ {
2753
+ name: "nonce",
2754
+ type: "string"
2755
+ },
2756
+ {
2757
+ name: "deadline",
2758
+ type: "uint256"
2759
+ }
2760
+ ]
2761
+ },
2762
+ "update-metadata": {
2763
+ UpdateVaultMetadata: [
2764
+ {
2765
+ name: "vault",
2766
+ type: "address"
2767
+ },
2768
+ {
2769
+ name: "name",
2770
+ type: "string"
2771
+ },
2772
+ {
2773
+ name: "about",
2774
+ type: "string"
2775
+ },
2776
+ {
2777
+ name: "imageUri",
2778
+ type: "string"
2779
+ },
2780
+ {
2781
+ name: "nonce",
2782
+ type: "string"
2783
+ },
2784
+ {
2785
+ name: "deadline",
2786
+ type: "uint256"
2787
+ }
2788
+ ]
2789
+ }
2790
+ };
2791
+ var CREATE_VAULT_TYPES = {
2792
+ CreateVault: [
2793
+ {
2794
+ name: "leader",
2795
+ type: "address"
2796
+ },
2797
+ {
2798
+ name: "name",
2799
+ type: "string"
2800
+ },
2801
+ {
2802
+ name: "description",
2803
+ type: "string"
2804
+ },
2805
+ {
2806
+ name: "imageUri",
2807
+ type: "string"
2808
+ },
2809
+ {
2810
+ name: "configId",
2811
+ type: "uint256"
2812
+ },
2813
+ {
2814
+ name: "maxWithdrawalPeriodDays",
2815
+ type: "uint256"
2816
+ },
2817
+ {
2818
+ name: "nonce",
2819
+ type: "string"
2820
+ },
2821
+ {
2822
+ name: "deadline",
2823
+ type: "uint256"
2824
+ }
2825
+ ]
2826
+ };
2827
+ function buildLeaderActionTypedData(action, vaultAddress, body) {
2828
+ const vault = checksumAddress(vaultAddress);
2829
+ const nonce = String(body.nonce ?? "");
2830
+ const deadline = BigInt(body.deadline ?? 0);
2831
+ switch (action) {
2832
+ case "withdraw-to-evm":
2833
+ return {
2834
+ types: VAULT_ACTION_TYPES[action],
2835
+ primaryType: "WithdrawToEvm",
2836
+ message: {
2837
+ vault,
2838
+ amount: String(body.amount ?? ""),
2839
+ token: String(body.tokenId ?? "0"),
2840
+ nonce,
2841
+ deadline
2842
+ }
2843
+ };
2844
+ case "set-trader":
2845
+ return {
2846
+ types: VAULT_ACTION_TYPES[action],
2847
+ primaryType: "SetTrader",
2848
+ message: {
2849
+ vault,
2850
+ trader: checksumAddress(String(body.traderAddress ?? "")),
2851
+ nonce,
2852
+ deadline
2853
+ }
2854
+ };
2855
+ case "agent-approve":
2856
+ return {
2857
+ types: VAULT_ACTION_TYPES[action],
2858
+ primaryType: "AgentApprove",
2859
+ message: {
2860
+ vault,
2861
+ nonce,
2862
+ deadline
2863
+ }
2864
+ };
2865
+ case "set-unified-account":
2866
+ return {
2867
+ types: VAULT_ACTION_TYPES[action],
2868
+ primaryType: "SetUnifiedAccount",
2869
+ message: {
2870
+ vault,
2871
+ nonce,
2872
+ deadline
2873
+ }
2874
+ };
2875
+ case "update-metadata":
2876
+ return {
2877
+ types: VAULT_ACTION_TYPES[action],
2878
+ primaryType: "UpdateVaultMetadata",
2879
+ message: {
2880
+ vault,
2881
+ name: String(body.name ?? ""),
2882
+ about: String(body.about ?? ""),
2883
+ imageUri: String(body.imageUri ?? ""),
2884
+ nonce,
2885
+ deadline
2886
+ }
2887
+ };
2888
+ }
2889
+ }
2890
+ __name(buildLeaderActionTypedData, "buildLeaderActionTypedData");
2891
+ function buildCreateVaultMessage(body) {
2892
+ return {
2893
+ leader: checksumAddress(String(body.leader ?? "")),
2894
+ name: String(body.name ?? ""),
2895
+ description: String(body.description ?? ""),
2896
+ imageUri: String(body.imageUri ?? ""),
2897
+ configId: BigInt(String(body.configId ?? 0)),
2898
+ maxWithdrawalPeriodDays: BigInt(String(body.maxWithdrawalPeriodDays ?? 0)),
2899
+ nonce: String(body.nonce ?? ""),
2900
+ deadline: BigInt(body.deadline ?? 0)
2901
+ };
2902
+ }
2903
+ __name(buildCreateVaultMessage, "buildCreateVaultMessage");
2904
+ function checksumAddress(address) {
2905
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
2906
+ throw new Error(`invalid address: ${address}`);
2907
+ }
2908
+ const lower = address.slice(2).toLowerCase();
2909
+ const hash = keccak_256(utf8ToBytes(lower));
2910
+ let out = "";
2911
+ for (const [index, byte] of hash.subarray(0, 20).entries()) {
2912
+ const high = lower.charAt(index * 2);
2913
+ const low = lower.charAt(index * 2 + 1);
2914
+ out += byte >> 4 >= 8 ? high.toUpperCase() : high;
2915
+ out += (byte & 15) >= 8 ? low.toUpperCase() : low;
2916
+ }
2917
+ return `0x${out}`;
2918
+ }
2919
+ __name(checksumAddress, "checksumAddress");
2621
2920
  var STORAGE_PREFIX = "pear.x-handoff.";
2622
2921
  function startXHandoff(storage) {
2623
2922
  const handoffId = base64urlnopad.encode(randomBytes(16));
@@ -2689,4 +2988,4 @@ function takeVerifier(handoffId, storage) {
2689
2988
  }
2690
2989
  __name(takeVerifier, "takeVerifier");
2691
2990
 
2692
- export { BIPS, ZERO, abs, add, aggregateRealizedPositionState, baseToContracts, buildApplicables, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeDeployedCapital, computeDeployedCapitalState, computeEntryPriceForAsset, computeFillTransitions, computeMaxBasketSize, computeMultiQuoteBasketSize, computePositionEntryPrices, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, computeUnrealizedPositionStateFromEntryPrices, configureProxy, contractsToBase, countDecimals, deriveBasketKey, divide, exponentiate, finishXHandoff, generateDeterministicPositionKey, groupFillsByAsset, groupFillsByPosition, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, proxyFetch, sideSign, sign, signed, signedBySide, sizeBasket, sizeBasketByLeverageMode, sizeBasketPerAssetLeverage, startXHandoff, stripXReturn, subtract, sumAttributedFunding, toDecimalString, validateLeverage, validateQuantity, walkPositionLedger, walkPositionTransitions };
2991
+ export { BIPS, CREATE_VAULT_TYPES, VAULT_ACTION_DOMAIN, VAULT_ACTION_TYPES, ZERO, abs, add, aggregateRealizedPositionState, baseToContracts, buildApplicables, buildCreateVaultMessage, buildLeaderActionTypedData, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeDeployedCapital, computeDeployedCapitalState, computeEntryPriceForAsset, computeFillTransitions, computeMaxBasketSize, computeMultiQuoteBasketSize, computePositionEntryPrices, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, computeUnrealizedPositionStateFromEntryPrices, configureProxy, contractsToBase, countDecimals, deriveBasketKey, divide, exponentiate, finishXHandoff, generateDeterministicPositionKey, groupFillsByAsset, groupFillsByPosition, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, proxyFetch, sideSign, sign, signed, signedBySide, sizeBasket, sizeBasketByLeverageMode, sizeBasketPerAssetLeverage, startXHandoff, stripXReturn, subtract, sumAttributedFunding, toDecimalString, validateLeverage, validateQuantity, walkPositionLedger, walkPositionTransitions };
@@ -12,6 +12,7 @@ import type { FillPart } from './types';
12
12
  */
13
13
  export declare class Basket {
14
14
  readonly legs: CombinedLeg[];
15
+ readonly fills: HttpTypes.UnknownExternalFill[];
15
16
  private constructor();
16
17
  /** Detect baskets from a fill stream, returned in ascending time order. */
17
18
  static detect(fills: HttpTypes.UnknownExternalFill[]): Basket[];
@@ -9,6 +9,13 @@ import type { BasketRule } from './types';
9
9
  * tolerance: a genuine partial close (>= 1e-8) stays open, as it should.
10
10
  */
11
11
  export declare const DUST: BigNumber;
12
+ /**
13
+ * How long before an open position's creation a fill may still belong to it. Fills can
14
+ * reach sync out of order by seconds across cycles (observed up to ~30s), so a strict
15
+ * cutoff would split one position in two; anything older predates the position and must
16
+ * not be replayed into it (see pear-protocol/backend#1067).
17
+ */
18
+ export declare const LATE_FILL_GRACE_MS: number;
12
19
  export declare const MISSED_HISTORY_FILL_PREFIX = "opening-synthetic-";
13
20
  export declare const BASKET_RULES: BasketRule[];
14
21
  /**
@@ -9,6 +9,11 @@ import type { MatchedPosition } from './types';
9
9
  * - resolveLeftoverTarget merges into a position whose FULL leg set (active and
10
10
  * dormant) is exactly the leftover's, else opens new (same-shape ADDs consolidate here).
11
11
  * The asset SET of a position never grows, so positionKey == key(opening symbols).
12
+ *
13
+ * Every phase only sees positions that already existed when the basket happened: a
14
+ * basket older than an open position's creation (beyond LATE_FILL_GRACE_MS) cannot
15
+ * close, reduce or merge into it, so a late-fetched earlier lifecycle is not bound to a
16
+ * position opened after it (pear-protocol/backend#1067).
12
17
  */
13
18
  export declare function matchPositions(args: {
14
19
  tradeAccountId: string;
@@ -31,6 +31,11 @@ export type WorkingPosition = {
31
31
  closed: boolean;
32
32
  /** Origin of the seeded existing position; undefined for positions opened mid-replay. */
33
33
  existingOrigin?: HttpTypes.PositionOrigin;
34
+ /**
35
+ * Earliest basket time (ms) this position may take fills from: its creation minus the
36
+ * late-fill grace for a seeded position, -Infinity for one opened mid-replay.
37
+ */
38
+ liveFrom: number;
34
39
  };
35
40
  /** A matched position that received fill slices, ready to become an applicable. */
36
41
  export type MatchedPosition = {
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Every action a leader-signed route may carry. A new route must add its member
3
+ * here, which forces the typed-data switch below to name it at build time.
4
+ */
5
+ export type LeaderActionName = 'withdraw-to-evm' | 'set-trader' | 'agent-approve' | 'set-unified-account' | 'update-metadata';
6
+ /**
7
+ * EIP-712 domain for off-chain vault-service auth. `chainId` is pinned to 999
8
+ * (HyperEVM) by the shared contract — signing is offline, so there is no chain
9
+ * switch and this must NOT be read from env. `verifyingContract` is omitted on
10
+ * purpose: this is service auth, not an on-chain contract call.
11
+ */
12
+ export declare const VAULT_ACTION_DOMAIN: {
13
+ readonly name: "PearVaultService";
14
+ readonly version: "1";
15
+ readonly chainId: 999;
16
+ };
17
+ /**
18
+ * EIP-712 types per action — the one copy both signer and verifier import.
19
+ * One struct per action: every signed parameter is a readable field in the
20
+ * wallet prompt, and the primary-type name itself discriminates the action,
21
+ * so structurally identical actions can never replay into each other.
22
+ */
23
+ export declare const VAULT_ACTION_TYPES: {
24
+ readonly 'withdraw-to-evm': {
25
+ readonly WithdrawToEvm: readonly [{
26
+ readonly name: "vault";
27
+ readonly type: "address";
28
+ }, {
29
+ readonly name: "amount";
30
+ readonly type: "string";
31
+ }, {
32
+ readonly name: "token";
33
+ readonly type: "string";
34
+ }, {
35
+ readonly name: "nonce";
36
+ readonly type: "string";
37
+ }, {
38
+ readonly name: "deadline";
39
+ readonly type: "uint256";
40
+ }];
41
+ };
42
+ readonly 'set-trader': {
43
+ readonly SetTrader: readonly [{
44
+ readonly name: "vault";
45
+ readonly type: "address";
46
+ }, {
47
+ readonly name: "trader";
48
+ readonly type: "address";
49
+ }, {
50
+ readonly name: "nonce";
51
+ readonly type: "string";
52
+ }, {
53
+ readonly name: "deadline";
54
+ readonly type: "uint256";
55
+ }];
56
+ };
57
+ readonly 'agent-approve': {
58
+ readonly AgentApprove: readonly [{
59
+ readonly name: "vault";
60
+ readonly type: "address";
61
+ }, {
62
+ readonly name: "nonce";
63
+ readonly type: "string";
64
+ }, {
65
+ readonly name: "deadline";
66
+ readonly type: "uint256";
67
+ }];
68
+ };
69
+ readonly 'set-unified-account': {
70
+ readonly SetUnifiedAccount: readonly [{
71
+ readonly name: "vault";
72
+ readonly type: "address";
73
+ }, {
74
+ readonly name: "nonce";
75
+ readonly type: "string";
76
+ }, {
77
+ readonly name: "deadline";
78
+ readonly type: "uint256";
79
+ }];
80
+ };
81
+ readonly 'update-metadata': {
82
+ readonly UpdateVaultMetadata: readonly [{
83
+ readonly name: "vault";
84
+ readonly type: "address";
85
+ }, {
86
+ readonly name: "name";
87
+ readonly type: "string";
88
+ }, {
89
+ readonly name: "about";
90
+ readonly type: "string";
91
+ }, {
92
+ readonly name: "imageUri";
93
+ readonly type: "string";
94
+ }, {
95
+ readonly name: "nonce";
96
+ readonly type: "string";
97
+ }, {
98
+ readonly name: "deadline";
99
+ readonly type: "uint256";
100
+ }];
101
+ };
102
+ };
103
+ /**
104
+ * EIP-712 type for a create — the one copy both signer and verifier import.
105
+ * Every signed parameter is a readable field in the wallet prompt.
106
+ */
107
+ export declare const CREATE_VAULT_TYPES: {
108
+ readonly CreateVault: readonly [{
109
+ readonly name: "leader";
110
+ readonly type: "address";
111
+ }, {
112
+ readonly name: "name";
113
+ readonly type: "string";
114
+ }, {
115
+ readonly name: "description";
116
+ readonly type: "string";
117
+ }, {
118
+ readonly name: "imageUri";
119
+ readonly type: "string";
120
+ }, {
121
+ readonly name: "configId";
122
+ readonly type: "uint256";
123
+ }, {
124
+ readonly name: "maxWithdrawalPeriodDays";
125
+ readonly type: "uint256";
126
+ }, {
127
+ readonly name: "nonce";
128
+ readonly type: "string";
129
+ }, {
130
+ readonly name: "deadline";
131
+ readonly type: "uint256";
132
+ }];
133
+ };
134
+ /**
135
+ * One action's complete signable typed data. Widened (non-generic) on purpose:
136
+ * viem's generic overloads cannot take a union of per-action shapes at one call site.
137
+ */
138
+ export type LeaderActionTypedData = {
139
+ types: Record<string, readonly {
140
+ name: string;
141
+ type: string;
142
+ }[]>;
143
+ primaryType: string;
144
+ message: {
145
+ vault: `0x${string}`;
146
+ amount?: string;
147
+ token?: string;
148
+ trader?: `0x${string}`;
149
+ name?: string;
150
+ about?: string;
151
+ imageUri?: string;
152
+ nonce: string;
153
+ deadline: bigint;
154
+ };
155
+ };
156
+ /**
157
+ * Canonical typed-data reconstruction from the request path + body. Signer and
158
+ * verifier MUST call this same builder with these same normalized values:
159
+ * - vault = the :vaultAddress path segment (checksummed inside the signed EIP-712 message; storage stays lowercase)
160
+ * - amount = body.amount, verbatim decimal string (withdraw only)
161
+ * - token = body.tokenId ?? '0', verbatim string (withdraw only)
162
+ * - trader = body.traderAddress, checksummed (set-trader only)
163
+ * - name / about / imageUri = the full display set, verbatim strings; '' means cleared (update-metadata only)
164
+ * - nonce = body.nonce (client-generated; salts the signature for idempotency)
165
+ * - deadline = body.deadline (unix seconds) as uint256
166
+ */
167
+ export declare function buildLeaderActionTypedData(action: LeaderActionName, vaultAddress: string, body: Record<string, unknown>): LeaderActionTypedData;
168
+ /**
169
+ * Canonical CreateVault reconstruction from the request body. Signer and verifier
170
+ * MUST call this same builder; absent optionals sign as '' and 0.
171
+ */
172
+ export declare function buildCreateVaultMessage(body: Record<string, unknown>): {
173
+ leader: `0x${string}`;
174
+ name: string;
175
+ description: string;
176
+ imageUri: string;
177
+ configId: bigint;
178
+ maxWithdrawalPeriodDays: bigint;
179
+ nonce: string;
180
+ deadline: bigint;
181
+ };
@@ -0,0 +1 @@
1
+ export * from './action-typed-data';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/utils",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Pear Protocol Utility functions",
5
5
  "private": false,
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@noble/hashes": "^1.3.2",
30
- "@pear-protocol/types": "^2.0.0",
30
+ "@pear-protocol/types": "^2.10.0",
31
31
  "@scure/base": "^2.3.0",
32
32
  "bignumber.js": "9.3.1"
33
33
  },