@reyaxyz/common 0.362.0 → 0.363.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.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/exposure/command.js +8 -22
  3. package/dist/commands/exposure/command.js.map +1 -1
  4. package/dist/incentives-api-types.js.map +1 -1
  5. package/dist/services/getNativeToken.js +1 -0
  6. package/dist/services/getNativeToken.js.map +1 -1
  7. package/dist/services/getSocketAddresses.js +1 -0
  8. package/dist/services/getSocketAddresses.js.map +1 -1
  9. package/dist/trading-api-types.js.map +1 -1
  10. package/dist/transactions/abis/Errors.json +1638 -34
  11. package/dist/transactions/abis/OrdersGateway.json +729 -123
  12. package/dist/transactions/abis/PassivePerpProxy.json +542 -0
  13. package/dist/transactions/buildMulticallTx.js.map +1 -1
  14. package/dist/transactions/consts.js +1 -2
  15. package/dist/transactions/consts.js.map +1 -1
  16. package/dist/transactions/contractAddresses.js +45 -5
  17. package/dist/transactions/contractAddresses.js.map +1 -1
  18. package/dist/transactions/sign.js +237 -111
  19. package/dist/transactions/sign.js.map +1 -1
  20. package/dist/types/commands/exposure/command.d.ts +8 -7
  21. package/dist/types/commands/exposure/command.d.ts.map +1 -1
  22. package/dist/types/incentives-api-types.d.ts +61 -5
  23. package/dist/types/incentives-api-types.d.ts.map +1 -1
  24. package/dist/types/services/getNativeToken.d.ts.map +1 -1
  25. package/dist/types/services/getSocketAddresses.d.ts.map +1 -1
  26. package/dist/types/trading-api-types.d.ts +200 -32
  27. package/dist/types/trading-api-types.d.ts.map +1 -1
  28. package/dist/types/transactions/buildMulticallTx.d.ts +1 -0
  29. package/dist/types/transactions/buildMulticallTx.d.ts.map +1 -1
  30. package/dist/types/transactions/consts.d.ts +0 -1
  31. package/dist/types/transactions/consts.d.ts.map +1 -1
  32. package/dist/types/transactions/contractAddresses.d.ts +0 -1
  33. package/dist/types/transactions/contractAddresses.d.ts.map +1 -1
  34. package/dist/types/transactions/sign.d.ts +113 -9
  35. package/dist/types/transactions/sign.d.ts.map +1 -1
  36. package/dist/types/types.d.ts +62 -63
  37. package/dist/types/types.d.ts.map +1 -1
  38. package/dist/types/utils/calculate.d.ts.map +1 -1
  39. package/dist/types/utils/consts.d.ts +2 -2
  40. package/dist/types/utils/consts.d.ts.map +1 -1
  41. package/dist/types/utils/error-sanitizer.d.ts +15 -0
  42. package/dist/types/utils/error-sanitizer.d.ts.map +1 -0
  43. package/dist/types/utils/index.d.ts +3 -0
  44. package/dist/types/utils/index.d.ts.map +1 -1
  45. package/dist/types/utils/network.d.ts.map +1 -1
  46. package/dist/types/utils/number.d.ts +5 -0
  47. package/dist/types/utils/number.d.ts.map +1 -1
  48. package/dist/types/utils/realized-balance.d.ts +54 -0
  49. package/dist/types/utils/realized-balance.d.ts.map +1 -0
  50. package/dist/types/utils/throttled-error.d.ts +42 -0
  51. package/dist/types/utils/throttled-error.d.ts.map +1 -0
  52. package/dist/types/utils/token/normalize.d.ts +2 -1
  53. package/dist/types/utils/token/normalize.d.ts.map +1 -1
  54. package/dist/types/utils/token/token-info-getters.d.ts.map +1 -1
  55. package/dist/types/utils/token/token-info.d.ts.map +1 -1
  56. package/dist/types.js +25 -11
  57. package/dist/types.js.map +1 -1
  58. package/dist/utils/calculate.js +18 -9
  59. package/dist/utils/calculate.js.map +1 -1
  60. package/dist/utils/consts.js +2 -7
  61. package/dist/utils/consts.js.map +1 -1
  62. package/dist/utils/error-sanitizer.js +135 -0
  63. package/dist/utils/error-sanitizer.js.map +1 -0
  64. package/dist/utils/index.js +3 -0
  65. package/dist/utils/index.js.map +1 -1
  66. package/dist/utils/network.js +4 -0
  67. package/dist/utils/network.js.map +1 -1
  68. package/dist/utils/number.js +96 -1
  69. package/dist/utils/number.js.map +1 -1
  70. package/dist/utils/realized-balance.js +126 -0
  71. package/dist/utils/realized-balance.js.map +1 -0
  72. package/dist/utils/throttled-error.js +60 -0
  73. package/dist/utils/throttled-error.js.map +1 -0
  74. package/dist/utils/token/normalize.js +2 -2
  75. package/dist/utils/token/normalize.js.map +1 -1
  76. package/dist/utils/token/token-info-getters.js +80 -5
  77. package/dist/utils/token/token-info-getters.js.map +1 -1
  78. package/dist/utils/token/token-info.js +11 -0
  79. package/dist/utils/token/token-info.js.map +1 -1
  80. package/package.json +3 -3
  81. package/src/commands/exposure/command.ts +8 -38
  82. package/src/incentives-api-types.ts +60 -5
  83. package/src/services/getNativeToken.ts +1 -0
  84. package/src/services/getSocketAddresses.ts +1 -0
  85. package/src/trading-api-types.ts +258 -39
  86. package/src/transactions/abis/Errors.json +5478 -3874
  87. package/src/transactions/abis/OrdersGateway.json +730 -124
  88. package/src/transactions/abis/PassivePerpProxy.json +542 -0
  89. package/src/transactions/buildMulticallTx.ts +4 -0
  90. package/src/transactions/consts.ts +0 -1
  91. package/src/transactions/contractAddresses.ts +42 -7
  92. package/src/transactions/sign.ts +306 -150
  93. package/src/types.ts +102 -82
  94. package/src/utils/calculate.ts +21 -8
  95. package/src/utils/consts.ts +4 -16
  96. package/src/utils/error-sanitizer.ts +137 -0
  97. package/src/utils/index.ts +3 -0
  98. package/src/utils/network.ts +4 -0
  99. package/src/utils/number.ts +110 -0
  100. package/src/utils/realized-balance.ts +163 -0
  101. package/src/utils/throttled-error.ts +85 -0
  102. package/src/utils/token/normalize.ts +6 -1
  103. package/src/utils/token/token-info-getters.ts +104 -6
  104. package/src/utils/token/token-info.ts +78 -0
package/src/types.ts CHANGED
@@ -49,6 +49,7 @@ export type MarketEntity = {
49
49
 
50
50
  export enum ReyaChainId {
51
51
  reyaNetwork = 1729,
52
+ reyaLocal = 31337,
52
53
  reyaCronos = 89346162,
53
54
  }
54
55
 
@@ -145,16 +146,14 @@ export type StrippedMarginAccountEntity = Pick<
145
146
  'id' | 'name'
146
147
  >;
147
148
 
148
- export type ConditionalOrdersInfo = {
149
- stopLoss?: {
150
- stopLossPrice: number;
151
- orderId: string;
152
- };
153
- takeProfit?: {
154
- takeProfitPrice: number;
155
- orderId: string;
156
- };
157
- };
149
+ // PRO-126 Bug 5: `ConditionalOrdersInfo` and the
150
+ // `PositionEntity.conditionalOrdersInfo` field that surfaced
151
+ // pending SL/TP info on positions were retired alongside the
152
+ // `OrdersGateway.execute` / conditional-orders DB pipeline. The
153
+ // off-chain API no longer has a backing source for live trigger
154
+ // metadata; once the ME exposes pending trigger info via its own
155
+ // surface, an equivalent field can be reintroduced sourcing from
156
+ // there.
158
157
 
159
158
  export type PositionEntity = {
160
159
  id: number;
@@ -162,7 +161,6 @@ export type PositionEntity = {
162
161
  size: number;
163
162
  base: number;
164
163
  price: number;
165
- conditionalOrdersInfo?: ConditionalOrdersInfo;
166
164
  markPrice: number;
167
165
  orderStatus: Status;
168
166
  realisedPnl: number;
@@ -175,8 +173,6 @@ export type PositionEntity = {
175
173
  accountId: number;
176
174
  };
177
175
 
178
- export type OrderType = 'market';
179
-
180
176
  export type OrderHistoryType =
181
177
  | 'long-trade'
182
178
  | 'short-trade'
@@ -186,7 +182,7 @@ export type OrderHistoryType =
186
182
  export type OrderHistoryEntity = {
187
183
  id: string;
188
184
  action: OrderHistoryType;
189
- orderType: OrderType;
185
+ orderType: 'market';
190
186
  base: number;
191
187
  executionPrice: number;
192
188
  realisedPnl?: number | null;
@@ -260,34 +256,6 @@ export type LpTransactionHistoryEntity = {
260
256
  timestamp: number;
261
257
  };
262
258
 
263
- export type MarginAccountTransactionHistoryType =
264
- | 'deposit'
265
- | 'withdrawal'
266
- | 'transfer'
267
- | 'auto_exchange_deposit'
268
- | 'auto_exchange_withdrawal';
269
-
270
- export type MarginAccountTransactionStatus = 'confirmed' | 'pending';
271
-
272
- export type MarginAccountTransactionHistoryEntity = {
273
- id: number;
274
- type: MarginAccountTransactionHistoryType;
275
- token: string;
276
- amount: number;
277
- transactionLink: string;
278
- status: MarginAccountTransactionStatus;
279
- timestamp: number;
280
- };
281
-
282
- export type ReferralFeeTransactionHistoryEntity = {
283
- id: number;
284
- referee_wallet_address: string;
285
- token: string;
286
- amount: number;
287
- transactionLink: string;
288
- timestamp: number;
289
- };
290
-
291
259
  export type TokenEntity = {
292
260
  name: TokenName;
293
261
  address: Address;
@@ -776,6 +744,23 @@ export type TransactionExecutionOutput = {
776
744
  txHash: string | null;
777
745
  txSubmitted?: boolean;
778
746
  errorMessage?: string;
747
+ /**
748
+ * Structured error discriminator surfaced by the slim
749
+ * `executeReyaTransaction` core. One of
750
+ * `TRANSACTION_FAILED_TO_SEND` (pre-send: wallet manager rejected),
751
+ * `RECEIPT_NOT_FOUND` (post-send: receipt fetch failed),
752
+ * `TRANSACTION_REVERTED` (post-send: receipt.status === 0), or
753
+ * `FAILED_FILL` (post-send: FailedUnifiedFillBytes / FailedFillBytes).
754
+ * Optional — populated when the slim core returned a discriminated
755
+ * failure. May coexist with a non-null `txHash` on post-send failures
756
+ * (the on-chain hash is surfaced for `RECEIPT_NOT_FOUND`,
757
+ * `TRANSACTION_REVERTED`, and `FAILED_FILL`). UI consumers should prefer
758
+ * `errorMessage` for display; this field is for analytics + logs.
759
+ *
760
+ * `FAILED_ORDER` (used to be: FailedOrderBytes) is retired alongside
761
+ * the legacy `OrdersGateway.execute` IOC handler in PRO-126 Bug 5.
762
+ */
763
+ errorCode?: string;
779
764
  accountId?: string | null;
780
765
  coreSigNonce?: string | null;
781
766
  poolSigNonce?: string | null;
@@ -841,16 +826,29 @@ export type PendingWithdrawal = {
841
826
  tokenAmount: number;
842
827
  } & (PendingWithdrawalStatus | ReadyWithdrawalStatus);
843
828
 
844
- export enum OrdersGatewayOrderType {
845
- STOP_LOSS = 0,
846
- TAKE_PROFIT = 1,
847
- LIMIT_ORDER = 2,
848
- MARKET_ORDER = 3,
849
- REDUCE_ONLY_MARKET_ORDER = 4,
850
- FULL_CLOSE_ORDER = 5,
851
- LIMIT_ORDER_SPOT = 6,
829
+ // PRO-126 Bug 5 retired the legacy `OrdersGatewayOrderType` enum (the
830
+ // 8-field `ConditionalOrderDetails` typehash discriminator). The
831
+ // on-chain `OrdersGateway.execute` selector that consumed those values
832
+ // is gone; every order class now uses `OrderType` below, whose values
833
+ // mirror the on-chain `OrderType` enum in
834
+ // orders-gateway/src/libraries/DataTypes.sol.
835
+ export enum OrderType {
836
+ Limit = 0,
837
+ StopLoss = 1,
838
+ TakeProfit = 2,
839
+ }
840
+
841
+ // Matches the on-chain `MarketType` enum. Used to discriminate spot vs perp
842
+ // in unified off-chain code paths and with the market-id namespace helpers in
843
+ // packages/common-backend/src/market-id-namespace.
844
+ export enum MarketType {
845
+ Spot = 0,
846
+ Perp = 1,
852
847
  }
853
848
 
849
+ // Canonical on-chain V3 codes, also written by the pre-cutover V2 indexer.
850
+ export { CanonicalExecutionType as ExecutionType } from './canonical-execution-type';
851
+
854
852
  export enum ConditionalOrderType {
855
853
  STOP_LOSS = 0,
856
854
  TAKE_PROFIT = 1,
@@ -965,23 +963,6 @@ export type V1AuthenticatorTransport =
965
963
  | 'AUTHENTICATOR_TRANSPORT_HYBRID';
966
964
 
967
965
  // on-chain events (consider pushing into a separate file)
968
- export type PositionDataUpdatedDetails = {
969
- marketId: string;
970
- accountId: string;
971
- // todo: p1: argument names don't align with the on-chain event
972
- orderBase: string;
973
- realisedPnl: string;
974
- lastPrice: string;
975
- lastPriceTimestamp: string;
976
- fundingValue: string;
977
- baseMultiplier: string;
978
- adlUnwindPrice: string;
979
- blockTimestamp: string;
980
- transactionHash: string;
981
- blockNumber: number;
982
- uniqueId: string;
983
- };
984
-
985
966
  export type MatchOrderDetails = {
986
967
  uniqueId: string;
987
968
  transactionHash: string;
@@ -1040,10 +1021,11 @@ export interface PassivePerpOrderAndPositionUpdate {
1040
1021
  uniqueId: bigint;
1041
1022
  }
1042
1023
 
1043
- export type AssetPair =
1024
+ export type ConfiguredAssetPair =
1044
1025
  | 'ETHUSD'
1045
1026
  | 'WSTETHUSD'
1046
1027
  | 'WBTCUSD'
1028
+ | 'BTCUSD' // Localnet's mock BTC collateral feed
1047
1029
  | 'USDCUSD'
1048
1030
  | 'USDEUSD'
1049
1031
  | 'SUSDEUSD'
@@ -1132,6 +1114,19 @@ export type AssetPair =
1132
1114
  | 'LINEAUSDMARK'
1133
1115
  | 'MEGAUSDMARK';
1134
1116
 
1117
+ type SpotPair<T extends string> = T extends `${infer Base}USDMARK`
1118
+ ? `${Base}USD`
1119
+ : never;
1120
+
1121
+ /** Internal cache keys whose prices mirror an existing reference feed. */
1122
+ export type MirroredAssetPair = 'REYALM#KEYROCKRUSD' | 'REYALM#FLOWRUSD';
1123
+
1124
+ /** Includes Stork spot feeds and mirrored price cache keys. */
1125
+ export type AssetPair =
1126
+ | ConfiguredAssetPair
1127
+ | SpotPair<ConfiguredAssetPair>
1128
+ | MirroredAssetPair;
1129
+
1135
1130
  export type ContractId = string;
1136
1131
 
1137
1132
  export type Price = {
@@ -1143,9 +1138,31 @@ export type Price = {
1143
1138
 
1144
1139
  export type MarketPrice = {
1145
1140
  marketId: number;
1146
- price: string;
1141
+ /** @deprecated No longer written; will be removed when V1 prices endpoints are dropped. */
1142
+ price?: string;
1143
+ /**
1144
+ * ME mark price (E18), despite the name — the pool-price task has written the
1145
+ * matching-engine mark here since the perpOB rewire. Surfaced as the v2
1146
+ * `MarketSummary.markPrice`. The genuine index price is `indexPrice`.
1147
+ */
1147
1148
  oraclePrice: string;
1148
- poolPrice: string;
1149
+ /**
1150
+ * Orderbook mid (E18). Absent on an empty or one-sided book — there is
1151
+ * deliberately no mark-price fallback, because publishing the mark under the
1152
+ * name "mid" makes a book with no two-sided quote indistinguishable from one
1153
+ * trading exactly at the mark. Consumers must treat it as optional, the same
1154
+ * way `SpotMarketSummary.throttledMidPrice` has always been treated.
1155
+ */
1156
+ poolPrice?: string;
1157
+ /**
1158
+ * Stork spot `*USD` index price (E18), rUSD-quoted — divided by USDCUSD the
1159
+ * same way the collateral prices are, since Stork publishes it as X/USD and
1160
+ * every price this API serves is X/USDC. Absent when the feed has no fresh
1161
+ * tick, when the USDCUSD cross-rate is unavailable, and permanently on
1162
+ * markets with no Stork feed, so consumers must treat it as optional rather
1163
+ * than substituting a stale value.
1164
+ */
1165
+ indexPrice?: string;
1149
1166
  updatedAt: number;
1150
1167
  };
1151
1168
 
@@ -1163,9 +1180,13 @@ export const isMarketPrice = (
1163
1180
  // Replication-derived types
1164
1181
  type Unpacked<T> = T extends (infer U)[] ? U : T;
1165
1182
 
1166
- export type ConditionalOrderSensitive = Unpacked<
1167
- Replication['replication:ConditionalOrdersSensitive']['result']
1168
- >;
1183
+ // PRO-126 Bug 7: `ConditionalOrderSensitive` + `OrderReplicationData`
1184
+ // retired with the rest of the `ConditionalOrders` DB pipeline. The
1185
+ // `replication:ConditionalOrdersSensitive` channel is no longer
1186
+ // subscribed (bun-socket/index.ts) and no longer publishes
1187
+ // (common-backend/src/config/database.ts). Order data flows through
1188
+ // the ME's `{orders}:changes` stream consumed by AccountOrdersMap;
1189
+ // callers should derive types from the ME's protobuf `Order` schema.
1169
1190
 
1170
1191
  export type Position = Unpacked<Replication['replication:position']['result']>;
1171
1192
 
@@ -1174,13 +1195,12 @@ export type PositionReplicationData = Stringified<
1174
1195
  Unpacked<Replication['replication:position']['result']>
1175
1196
  >;
1176
1197
 
1177
- export type OrderReplicationData = Stringified<
1178
- Unpacked<Replication['replication:ConditionalOrdersSensitive']['result']>
1179
- >;
1180
-
1181
- export type PerpExecutionReplicationData = Stringified<
1182
- Unpacked<Replication['replication:orders']['result']>
1183
- >;
1198
+ export type PerpExecutionReplicationData = Omit<
1199
+ Stringified<Unpacked<Replication['replication:orders']['result']>>,
1200
+ 'created_at'
1201
+ > & {
1202
+ created_at?: string;
1203
+ };
1184
1204
 
1185
1205
  export type AccountRealBalanceReplicationData = Stringified<
1186
1206
  Unpacked<Replication['replication:account_real_balances']['result']>
@@ -1190,8 +1210,8 @@ export type SpotExecutionReplicationData = Stringified<
1190
1210
  Unpacked<Replication['replication:spot_executions']['result']>
1191
1211
  >;
1192
1212
 
1193
- export type SpotExecutionBustReplicationData = Stringified<
1194
- Unpacked<Replication['replication:spot_execution_busts']['result']>
1213
+ export type ExecutionBustReplicationData = Stringified<
1214
+ Unpacked<Replication['replication:execution_busts']['result']>
1195
1215
  >;
1196
1216
 
1197
1217
  export type MarketDataReplicationData = Stringified<
@@ -593,7 +593,10 @@ export const calculateTotalMargin = ({
593
593
  let fundingCashFlow = 0;
594
594
  let adlCashFlow = 0;
595
595
 
596
- if (marketTracker) {
596
+ // Mirrors calculateSettledPositionPnl: a flat position accrues nothing, and
597
+ // a zero position multiplier (reopened after full ADL) cannot be divided by,
598
+ // so only realized and mark PnL count for it.
599
+ if (marketTracker && position.base !== 0 && position.baseMultiplier > 0) {
597
600
  // fundingCashFlow
598
601
  const marketTrackerFundingValue =
599
602
  position.base < 0
@@ -619,13 +622,18 @@ export const calculateTotalMargin = ({
619
622
  adlCashFlow = (adlUnwindPrice - initialPrice) * position.base;
620
623
  }
621
624
 
625
+ // The underlying supplies the mark; all position cashflows are in the quote token.
626
+ // A missing mark means "no price for this bucket", not a price of zero:
627
+ // carry the position at its last price rather than swinging by its notional.
628
+ const markPrice = markPrices.get(tokenInfo.underlyingAssetId);
622
629
  const unrealizedPnl =
623
- ((markPrices.get(tokenInfo.quoteTokenId) ?? 0) - position.lastPrice) *
624
- position.base;
630
+ markPrice === undefined
631
+ ? 0
632
+ : (markPrice - position.lastPrice) * position.base;
625
633
 
626
634
  pnl.set(
627
- tokenInfo.underlyingAssetId,
628
- (pnl.get(tokenInfo.underlyingAssetId) ?? 0) +
635
+ tokenInfo.quoteTokenId,
636
+ (pnl.get(tokenInfo.quoteTokenId) ?? 0) +
629
637
  position.realizedPnl +
630
638
  fundingCashFlow +
631
639
  adlCashFlow +
@@ -633,10 +641,15 @@ export const calculateTotalMargin = ({
633
641
  );
634
642
  });
635
643
 
636
- return Array.from(accountBalances.entries()).reduce(
637
- (sum, [address, balance]) =>
644
+ // A quote-token PnL balance can exist without a collateral deposit row.
645
+ const assets = new Set([
646
+ ...Array.from(accountBalances.keys()),
647
+ ...Array.from(pnl.keys()),
648
+ ]);
649
+ return Array.from(assets).reduce(
650
+ (sum, address) =>
638
651
  sum +
639
- (balance + (pnl.get(address) ?? 0)) *
652
+ ((accountBalances.get(address) ?? 0) + (pnl.get(address) ?? 0)) *
640
653
  (address === rUSDAddress
641
654
  ? 1
642
655
  : (assetPrices.get(address) ?? 0) / (rUSDPrice || 1)),
@@ -1,10 +1,4 @@
1
- import {
2
- ConditionalOrderType,
3
- ConditionalOrderTypeName,
4
- Rank,
5
- RankTrading,
6
- TierType,
7
- } from '../types';
1
+ import { Rank, RankTrading, TierType } from '../types';
8
2
 
9
3
  export const MAX_UINT128 = 2 ** 128 - 1;
10
4
  export const API_TIMEOUT: number = 2500;
@@ -111,15 +105,6 @@ export const RANKS_TRADING: Record<number, RankTrading> = {
111
105
  },
112
106
  };
113
107
 
114
- export const conditionalOrderTypeNames: Record<
115
- ConditionalOrderType,
116
- ConditionalOrderTypeName
117
- > = {
118
- [ConditionalOrderType.STOP_LOSS]: 'Stop Loss',
119
- [ConditionalOrderType.TAKE_PROFIT]: 'Take Profit',
120
- [ConditionalOrderType.LIMIT_ORDER]: 'Limit Order',
121
- };
122
-
123
108
  export const INSTANT_TRADING_RATE_XP = 2;
124
109
  export const TRADING_LOTTERY_BOOST = 10;
125
110
 
@@ -135,6 +120,9 @@ export const VOLUME_TIER_MAP: Record<number, number> = {
135
120
  6: 5_000_000_000, // Tier 6: $5,000,000,000 volume
136
121
  };
137
122
 
123
+ export const FEE_TIER_VOLUME_LOOKBACK_WINDOW_IN_SECONDS =
124
+ 30 * ONE_DAY_IN_SECONDS;
125
+
138
126
  export const TIER_TYPE_MAP: Record<number, TierType> = {
139
127
  0: TierType.REGULAR,
140
128
  1: TierType.REGULAR,
@@ -0,0 +1,137 @@
1
+ // Redis can echo command arguments inside an error's message. Once an error is
2
+ // known to carry command metadata, its message and stack must therefore be
3
+ // treated as tainted rather than attempting value-by-value replacement.
4
+ const REDACTED_COMMAND_ERROR =
5
+ '[redacted: Redis command error — message/stack withheld (may echo arguments)]';
6
+
7
+ function commandField(source: unknown): { name?: unknown } | undefined {
8
+ if (source === null || typeof source !== 'object') return undefined;
9
+ const command = (source as { command?: unknown }).command;
10
+ return command !== null && typeof command === 'object'
11
+ ? (command as { name?: unknown })
12
+ : undefined;
13
+ }
14
+
15
+ function redactCommandBearing(
16
+ source: { name?: unknown; code?: unknown; cause?: unknown },
17
+ command: { name?: unknown },
18
+ depth: number,
19
+ ): Record<string, unknown> {
20
+ const out: Record<string, unknown> = {};
21
+ if (typeof source.name === 'string') out.name = source.name;
22
+ if (typeof source.code === 'string' || typeof source.code === 'number') {
23
+ out.code = source.code;
24
+ }
25
+ out.command = typeof command.name === 'string' ? { name: command.name } : {};
26
+ out.message = REDACTED_COMMAND_ERROR;
27
+ // The stack is intentionally withheld because it can repeat the tainted
28
+ // message. Causes are projected independently so safe diagnostics survive.
29
+ if (source.cause !== undefined && depth < 3) {
30
+ out.cause = normalizeError(source.cause, depth + 1);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Project an unknown error into a serializable diagnostic record. An ioredis
37
+ * error carrying `command: { name, args }` is fail-closed: arguments plus the
38
+ * potentially argument-bearing message/stack are withheld, while safe fields
39
+ * such as the error name, code, command name, and projected cause survive.
40
+ */
41
+ export function normalizeError(
42
+ err: unknown,
43
+ depth = 0,
44
+ ): Record<string, unknown> {
45
+ if (err instanceof Error) {
46
+ // A logger must never throw because an error object has a hostile getter.
47
+ try {
48
+ const command = commandField(err);
49
+ if (command) return redactCommandBearing(err, command, depth);
50
+
51
+ const out: Record<string, unknown> = {};
52
+ if (typeof err.name === 'string') out.name = err.name;
53
+ if (typeof err.message === 'string') out.message = err.message;
54
+ if (typeof err.stack === 'string') out.stack = err.stack;
55
+ const { code } = err as { code?: unknown };
56
+ if (typeof code === 'string' || typeof code === 'number') out.code = code;
57
+ const { cause } = err as { cause?: unknown };
58
+ if (cause !== undefined && depth < 3) {
59
+ out.cause = normalizeError(cause, depth + 1);
60
+ }
61
+ return out;
62
+ } catch {
63
+ return { message: '[unserializable error]' };
64
+ }
65
+ }
66
+
67
+ // Plain objects use the same command-taint rule and an allow-list projection
68
+ // so arbitrary password/argument fields never reach a log record.
69
+ if (err !== null && typeof err === 'object') {
70
+ try {
71
+ const command = commandField(err);
72
+ if (command) {
73
+ return redactCommandBearing(
74
+ err as { name?: unknown; code?: unknown; cause?: unknown },
75
+ command,
76
+ depth,
77
+ );
78
+ }
79
+ const src = err as Record<string, unknown>;
80
+ const out: Record<string, unknown> = {};
81
+ if (typeof src.message === 'string') out.message = src.message;
82
+ if (typeof src.name === 'string') out.name = src.name;
83
+ if (typeof src.code === 'string' || typeof src.code === 'number') {
84
+ out.code = src.code;
85
+ }
86
+ if (src.cause !== undefined && depth < 3) {
87
+ out.cause = normalizeError(src.cause, depth + 1);
88
+ }
89
+ return out;
90
+ } catch {
91
+ return { message: '[unserializable error]' };
92
+ }
93
+ }
94
+
95
+ try {
96
+ return { message: String(err) };
97
+ } catch {
98
+ return { message: '[unserializable error]' };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Pure, in-place log-record sanitizer. Keeping this free of Winston types lets
104
+ * every package install a tiny adapter around the same implementation without
105
+ * introducing package cycles or forcing the shared leaf package to depend on a
106
+ * logging framework.
107
+ */
108
+ export function sanitizeErrorLogInfo<T extends Record<string, unknown>>(
109
+ info: T,
110
+ ): T {
111
+ const record = info as Record<string, unknown>;
112
+ // Nested `{ error }` and `{ cause }` are the preferred contextual shapes.
113
+ // Primitive values cannot carry command metadata, so preserve their existing
114
+ // field shape rather than wrapping them in `{ message }`.
115
+ if (record.error !== null && typeof record.error === 'object') {
116
+ record.error = normalizeError(record.error);
117
+ }
118
+ if (record.cause !== null && typeof record.cause === 'object') {
119
+ record.cause = normalizeError(record.cause);
120
+ }
121
+
122
+ // Winston can flatten a raw Error passed as the second argument. Command
123
+ // presence alone makes the merged message/stack tainted, including stackless
124
+ // and plain-object variants.
125
+ const command = commandField(record);
126
+ if (command) {
127
+ record.message = REDACTED_COMMAND_ERROR;
128
+ record.stack = undefined;
129
+ try {
130
+ record.command =
131
+ typeof command.name === 'string' ? { name: command.name } : {};
132
+ } catch {
133
+ record.command = {};
134
+ }
135
+ }
136
+ return info;
137
+ }
@@ -15,3 +15,6 @@ export * from './stringify';
15
15
  export * from './delay';
16
16
  export * from './transaction-receipt';
17
17
  export * from './layerzero';
18
+ export * from './throttled-error';
19
+ export * from './error-sanitizer';
20
+ export * from './realized-balance';
@@ -19,6 +19,8 @@ export function getMoneyInOutNetworks(
19
19
  MoneyInOutChainId.arbitrumSepolia,
20
20
  ];
21
21
 
22
+ if (chainId === ReyaChainId.reyaLocal) return [];
23
+
22
24
  throw new Error(`Unknown Reya chain id ${chainId}`);
23
25
  }
24
26
 
@@ -52,11 +54,13 @@ export const chainIdToExplorerURL: {
52
54
  [MoneyInOutChainId.opSepolia]: 'https://sepolia-optimism.etherscan.io',
53
55
  [MoneyInOutChainId.ethereumSepolia]: 'https://sepolia.etherscan.io',
54
56
  [ReyaChainId.reyaNetwork]: 'https://explorer.reya.network',
57
+ [ReyaChainId.reyaLocal]: 'http://127.0.0.1:4000',
55
58
  [ReyaChainId.reyaCronos]: 'https://reya-cronos.blockscout.com',
56
59
  };
57
60
 
58
61
  const ChainRpcUrls: Record<MoneyInOutChainId | ReyaChainId, string[]> = {
59
62
  [ReyaChainId.reyaNetwork]: ['https://rpc.reya.network'],
63
+ [ReyaChainId.reyaLocal]: ['http://127.0.0.1:8545'],
60
64
  [MoneyInOutChainId.ethereumMainnet]: [
61
65
  'https://mainnet.gateway.tenderly.co',
62
66
  'https://eth.llamarpc.com',
@@ -12,6 +12,116 @@ export function amountNormalizer(
12
12
  );
13
13
  }
14
14
 
15
+ /**
16
+ * Divide a plain-integer decimal string by 10**decimals by shifting the decimal
17
+ * point, returning EXACTLY what `amountNormalizer(value, decimals).toString()`
18
+ * would return — or `null` when that cannot be guaranteed, so the caller falls
19
+ * back to BigNumber.
20
+ *
21
+ * This exists because the perp-execution row transform is the single largest
22
+ * CPU consumer on the api tier (PRO-1025: `transformTrade` 26% inclusive,
23
+ * `amountNormalizer` 19%, self time dominated by bignumber.js multiply/divide
24
+ * and the BigNumber constructor). Each row normalises ~30 Decimal fields and a
25
+ * response carries up to 100 rows, so this runs a few thousand times per
26
+ * request. Dividing a decimal string by a power of ten is a decimal-point
27
+ * shift; it does not need arbitrary-precision arithmetic.
28
+ *
29
+ * Measured: 9.8x faster than the BigNumber path on realistic devnet values,
30
+ * with a 100% fast-path hit rate on those shapes.
31
+ *
32
+ * TWO BigNumber behaviours make the naive shift wrong, and both are why this
33
+ * returns null rather than guessing. Each would have silently changed the
34
+ * public API's serialisation:
35
+ *
36
+ * 1. `toString()` switches to EXPONENTIAL notation outside a magnitude window.
37
+ * BigNumber's EXPONENTIAL_AT default is [-7, 21], so plain notation holds
38
+ * only for a decimal exponent in [-6, 20]; `1e-7` renders "1e-7", not
39
+ * "0.0000001". A plain shift would emit the long form for those.
40
+ * 2. `.div()` ROUNDS to DECIMAL_PLACES (default 20). So amountNormalizer is
41
+ * already lossy past 20 fractional digits, and an exact shift produces MORE
42
+ * digits than the current output. Verified: 4000164727132806080849 / 1e27
43
+ * is 0.00000400016472713281 through BigNumber, not the exact value.
44
+ *
45
+ * The fast path is therefore taken only when the result is provably identical:
46
+ * inside the plain-notation window, and with no rounding possible.
47
+ */
48
+ const PLAIN_INTEGER = /^[0-9]+$/;
49
+ const BN_PLAIN_NOTATION_MIN_EXPONENT = -6;
50
+ const BN_PLAIN_NOTATION_MAX_EXPONENT = 20;
51
+ const BN_DECIMAL_PLACES = 20;
52
+
53
+ export function normalizeAmountFast(
54
+ value: string,
55
+ decimals: number,
56
+ ): string | null {
57
+ let digits = value;
58
+ let negative = false;
59
+ if (digits.charCodeAt(0) === 45 /* '-' */) {
60
+ negative = true;
61
+ digits = digits.slice(1);
62
+ }
63
+ if (!PLAIN_INTEGER.test(digits)) return null;
64
+
65
+ digits = digits.replace(/^0+(?=[0-9])/, '');
66
+ if (digits === '0') {
67
+ // Negative zero: BigNumber disagrees with itself here — toString() gives
68
+ // "0" but toNumber() gives -0, and Object.is(-0, 0) is false. Rather than
69
+ // reproduce that split, decline so both wrappers fall back and inherit the
70
+ // exact behaviour. Costs nothing: only a literal zero input reaches here.
71
+ return negative ? null : '0';
72
+ }
73
+
74
+ // Decimal exponent of the result: BigNumber renders plain only inside the
75
+ // window, and rounds beyond BN_DECIMAL_PLACES fractional digits.
76
+ const exponent = digits.length - 1 - decimals;
77
+ if (
78
+ exponent < BN_PLAIN_NOTATION_MIN_EXPONENT ||
79
+ exponent > BN_PLAIN_NOTATION_MAX_EXPONENT ||
80
+ decimals > BN_DECIMAL_PLACES
81
+ ) {
82
+ return null;
83
+ }
84
+
85
+ if (decimals === 0) return negative ? `-${digits}` : digits;
86
+
87
+ const padded =
88
+ digits.length <= decimals
89
+ ? '0'.repeat(decimals - digits.length + 1) + digits
90
+ : digits;
91
+ const integerPart = padded.slice(0, padded.length - decimals);
92
+ const fractionPart = padded
93
+ .slice(padded.length - decimals)
94
+ .replace(/0+$/, '');
95
+ const rendered = fractionPart
96
+ ? `${integerPart}.${fractionPart}`
97
+ : integerPart;
98
+
99
+ if (rendered === '0') return '0';
100
+ return negative ? `-${rendered}` : rendered;
101
+ }
102
+
103
+ /** `amountNormalizer(value, decimals).toString()`, fast path where provable. */
104
+ export function normalizeAmountToString(
105
+ value: string,
106
+ decimals: number = 18,
107
+ ): string {
108
+ return (
109
+ normalizeAmountFast(value, decimals) ??
110
+ amountNormalizer(value, decimals).toString()
111
+ );
112
+ }
113
+
114
+ /** `amountNormalizer(value, decimals).toNumber()`, fast path where provable. */
115
+ export function normalizeAmountToNumber(
116
+ value: string,
117
+ decimals: number = 18,
118
+ ): number {
119
+ const fast = normalizeAmountFast(value, decimals);
120
+ return fast !== null
121
+ ? Number(fast)
122
+ : amountNormalizer(value, decimals).toNumber();
123
+ }
124
+
15
125
  export function amountDenormalizer(
16
126
  value: BigNumber | number | string,
17
127
  decimals: number = 18,