@ostium/builder-sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -243,11 +243,20 @@ function getNetworkConfig(isTestnet = false) {
243
243
  // src/config.ts
244
244
  var DEFAULT_PIMLICO_URL = "https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161";
245
245
  var DEFAULT_PIMLICO_URL_TESTNET = "https://builder.ostium.io/v1/pimlico/sponsor?chainId=421614";
246
- function isGasless(config) {
247
- return !!config.pimlicoUrl;
246
+ function hasPrivateKey(config) {
247
+ return typeof config.privateKey === "string" && config.privateKey.length > 0;
248
248
  }
249
- function isDelegated(config) {
250
- return !!config.traderAddress;
249
+ function resolveMode(config) {
250
+ if (config.mode) return config.mode;
251
+ if (config.privateKey) {
252
+ if (config.pimlicoUrl) return config.traderAddress ? "delegated-gasless" : "self-gasless";
253
+ return config.traderAddress ? "delegated-self" : "self-self";
254
+ }
255
+ if (config.safeAddress && config.delegateAddress && config.traderAddress) return "delegated-gasless";
256
+ if (config.safeAddress && config.traderAddress) return "self-gasless";
257
+ if (config.delegateAddress && config.traderAddress) return "delegated-self";
258
+ if (config.traderAddress) return "self-self";
259
+ throw new Error("Unable to infer client mode from config");
251
260
  }
252
261
 
253
262
  // src/errors.ts
@@ -2648,8 +2657,8 @@ var DelegatedSignerStrategy = class {
2648
2657
  };
2649
2658
 
2650
2659
  // src/signer/index.ts
2651
- function createSigner(config, traderAddress, selfGasless = false) {
2652
- if (isDelegated(config) || selfGasless) {
2660
+ function createSigner(mode, traderAddress) {
2661
+ if (mode !== "self-self") {
2653
2662
  return new DelegatedSignerStrategy(traderAddress);
2654
2663
  }
2655
2664
  return new SelfSignerStrategy(traderAddress);
@@ -2673,6 +2682,12 @@ var SelfSubmissionStrategy = class {
2673
2682
  effectiveSender;
2674
2683
  send;
2675
2684
  constructor(config, testnet) {
2685
+ if (!config.privateKey) {
2686
+ throw new OstiumError(
2687
+ "privateKey is required for self-submission mode",
2688
+ "INVALID_CONFIG" /* INVALID_CONFIG */
2689
+ );
2690
+ }
2676
2691
  if (!config.rpcUrl) {
2677
2692
  throw new OstiumError(
2678
2693
  "rpcUrl is required for self-submission mode",
@@ -2738,6 +2753,12 @@ var GaslessSubmissionStrategy = class _GaslessSubmissionStrategy {
2738
2753
  this.effectiveSender = safeAccount.address;
2739
2754
  }
2740
2755
  static async create(config, testnet) {
2756
+ if (!config.privateKey) {
2757
+ throw new OstiumError(
2758
+ "privateKey is required for gasless submission mode",
2759
+ "INVALID_CONFIG" /* INVALID_CONFIG */
2760
+ );
2761
+ }
2741
2762
  if (!config.pimlicoUrl) {
2742
2763
  throw new OstiumError(
2743
2764
  "pimlicoUrl is required for gasless mode",
@@ -2806,8 +2827,8 @@ var GaslessSubmissionStrategy = class _GaslessSubmissionStrategy {
2806
2827
  };
2807
2828
 
2808
2829
  // src/submitter/index.ts
2809
- async function createSubmitter(config, testnet) {
2810
- if (isGasless(config)) {
2830
+ async function createSubmitter(config, testnet, mode) {
2831
+ if (mode === "self-gasless" || mode === "delegated-gasless") {
2811
2832
  return GaslessSubmissionStrategy.create(config, testnet);
2812
2833
  }
2813
2834
  if (!config.rpcUrl) {
@@ -2896,6 +2917,17 @@ query GetTraderOpenTrades($trader: String!, $skip: Int!, $first: Int!) {
2896
2917
  ${PAIR_FULL}
2897
2918
  }
2898
2919
  }`;
2920
+ var GetAllOpenTradesQuery = `
2921
+ query GetAllOpenTrades($skip: Int!, $first: Int!) {
2922
+ trades(
2923
+ where: { isOpen: true }
2924
+ skip: $skip first: $first
2925
+ orderBy: timestamp orderDirection: desc
2926
+ ) {
2927
+ ${TRADE_CORE}
2928
+ ${PAIR_FULL}
2929
+ }
2930
+ }`;
2899
2931
  var ORDER_FIELDS = `
2900
2932
  id tradeID limitID trader
2901
2933
  pair { id from to group { id name } }
@@ -2975,9 +3007,9 @@ query GetTraderActiveLimits($trader: String!, $skip: Int!, $first: Int!) {
2975
3007
  }`;
2976
3008
 
2977
3009
  // src/data/internal/pagination.ts
2978
- async function paginateAll(fetcher, batchSize = 1e3, max = Infinity) {
3010
+ async function paginateAll(fetcher, batchSize = 1e3, max = Infinity, initialSkip = 0) {
2979
3011
  const results = [];
2980
- let skip = 0;
3012
+ let skip = initialSkip;
2981
3013
  while (results.length < max) {
2982
3014
  const remaining = max - results.length;
2983
3015
  const fetchSize = Math.min(batchSize, remaining);
@@ -3199,10 +3231,11 @@ async function fetchLivePrices(builderApiUrl) {
3199
3231
  }
3200
3232
  return result;
3201
3233
  }
3202
- function rawTickToPublic(item) {
3234
+ function rawTickToPublic(item, pairId) {
3203
3235
  const from = normalizePairName(item.from);
3204
3236
  const to = normalizePairName(item.to);
3205
3237
  return {
3238
+ pairId,
3206
3239
  feedId: item.feed_id,
3207
3240
  pair: `${from}-${to}`,
3208
3241
  from,
@@ -3249,9 +3282,12 @@ var OstiumPriceStream = class _OstiumPriceStream {
3249
3282
  snapshotHandlers = /* @__PURE__ */ new Set();
3250
3283
  /** pairId (string) → raw "FROM-TO" name for the WS API. */
3251
3284
  pairRawNameCache;
3252
- constructor(ws, pairRawNameCache, initialSubscribeRawPairs) {
3285
+ /** raw "FROM-TO" name → pairId (string) for mapping incoming ticks back to SDK ids. */
3286
+ rawNamePairIdCache;
3287
+ constructor(ws, pairRawNameCache, rawNamePairIdCache, initialSubscribeRawPairs) {
3253
3288
  this.ws = ws;
3254
3289
  this.pairRawNameCache = pairRawNameCache;
3290
+ this.rawNamePairIdCache = rawNamePairIdCache;
3255
3291
  if (initialSubscribeRawPairs?.length) {
3256
3292
  ws.once("open", () => {
3257
3293
  ws.send(JSON.stringify({ type: "subscribe", pairs: initialSubscribeRawPairs }));
@@ -3265,10 +3301,13 @@ var OstiumPriceStream = class _OstiumPriceStream {
3265
3301
  return;
3266
3302
  }
3267
3303
  if (msg.type === "snapshot" && Array.isArray(msg.data)) {
3268
- const ticks = msg.data.map(rawTickToPublic);
3304
+ const ticks = msg.data.map(
3305
+ (item) => rawTickToPublic(item, this.rawNamePairIdCache.get(`${item.from}-${item.to}`))
3306
+ );
3269
3307
  for (const h of this.snapshotHandlers) h(ticks);
3270
3308
  } else if (msg.type === "tick" && msg.data) {
3271
- const tick = rawTickToPublic(msg.data);
3309
+ const item = msg.data;
3310
+ const tick = rawTickToPublic(item, this.rawNamePairIdCache.get(`${item.from}-${item.to}`));
3272
3311
  for (const h of this.tickHandlers) h(tick);
3273
3312
  }
3274
3313
  });
@@ -3294,7 +3333,11 @@ var OstiumPriceStream = class _OstiumPriceStream {
3294
3333
  const ws = new WS(url, {
3295
3334
  headers: { "User-Agent": "ostium-sdk", "Accept": "*/*" }
3296
3335
  });
3297
- return new _OstiumPriceStream(ws, pairRawNameCache, initial);
3336
+ const rawNamePairIdCache = /* @__PURE__ */ new Map();
3337
+ for (const [pairId, rawName] of pairRawNameCache.entries()) {
3338
+ rawNamePairIdCache.set(rawName, pairId);
3339
+ }
3340
+ return new _OstiumPriceStream(ws, pairRawNameCache, rawNamePairIdCache, initial);
3298
3341
  }
3299
3342
  /**
3300
3343
  * Register a callback that fires on every incoming price tick.
@@ -3370,6 +3413,37 @@ var OstiumPriceStream = class _OstiumPriceStream {
3370
3413
  }
3371
3414
  };
3372
3415
 
3416
+ // src/data/internal/aggregations.ts
3417
+ function aggregateMarginSummary(pairPositions) {
3418
+ let accountValue = 0;
3419
+ let totalCollateralUsed = 0;
3420
+ let totalNtlPos = 0;
3421
+ let totalRawPnlUsd = 0;
3422
+ let totalCumRollover = 0;
3423
+ let totalWithdrawable = 0;
3424
+ for (const { position } of pairPositions) {
3425
+ const collateral = parseFloat(position.collateralUsed) || 0;
3426
+ const ntl = parseFloat(position.ntl) || 0;
3427
+ const pnl = parseFloat(position.unrealizedPnl) || 0;
3428
+ const rollover = parseFloat(position.cumRollover) || 0;
3429
+ const withdrawable = parseFloat(position.maxWithdrawable) || 0;
3430
+ accountValue += collateral + pnl;
3431
+ totalCollateralUsed += collateral;
3432
+ totalNtlPos += ntl;
3433
+ totalRawPnlUsd += pnl;
3434
+ totalCumRollover += rollover;
3435
+ totalWithdrawable += withdrawable;
3436
+ }
3437
+ return {
3438
+ accountValue: accountValue.toString(),
3439
+ totalCollateralUsed: totalCollateralUsed.toString(),
3440
+ totalNtlPos: totalNtlPos.toString(),
3441
+ totalRawPnlUsd: totalRawPnlUsd.toString(),
3442
+ totalCumRollover: totalCumRollover.toString(),
3443
+ totalWithdrawable: totalWithdrawable.toString()
3444
+ };
3445
+ }
3446
+
3373
3447
  // src/data/internal/formatters.ts
3374
3448
  var MIN_NOTIONAL = "5.0";
3375
3449
  var MIN_NOTIONAL_NUM = 5;
@@ -3471,14 +3545,14 @@ function formatPosition(raw, price, pnl, maxLeverage) {
3471
3545
  returnOnEquity: roe.toString(),
3472
3546
  liquidationPx: pnl.liquidationPrice.toString(),
3473
3547
  collateralUsed: collateral.toString(),
3474
- cumRollover: pnl.rollover.toString(),
3548
+ cumRollover: (pnl.rollover * -1).toString(),
3475
3549
  ...raw.takeProfitPrice && raw.takeProfitPrice !== "0" ? { tpPx: formatTokens(raw.takeProfitPrice).toString() } : {},
3476
3550
  ...raw.stopLossPrice && raw.stopLossPrice !== "0" ? { slPx: formatTokens(raw.stopLossPrice).toString() } : {},
3477
3551
  openTimestamp: parseInt(raw.timestamp || "0") * 1e3,
3478
3552
  isDayTrade: raw.isDayTrade ?? false,
3479
- maxLeverage: maxLeverage.toString()
3480
- },
3481
- maxWithdrawable: maxWithdrawForPosition(collateral, leverage, maxLeverage).toString()
3553
+ maxLeverage: maxLeverage.toString(),
3554
+ maxWithdrawable: maxWithdrawForPosition(collateral, leverage, maxLeverage).toString()
3555
+ }
3482
3556
  };
3483
3557
  }
3484
3558
  function formatFill(raw) {
@@ -3555,6 +3629,131 @@ function formatOpenOrder(raw) {
3555
3629
  timestamp: parseInt(raw.initiatedAt || "0") * 1e3
3556
3630
  };
3557
3631
  }
3632
+
3633
+ // src/data/internal/position-updates.ts
3634
+ function cloneResponse(response) {
3635
+ return {
3636
+ pairPositions: response.pairPositions.map((pairPos) => ({
3637
+ position: { ...pairPos.position }
3638
+ })),
3639
+ marginSummary: { ...response.marginSummary },
3640
+ time: response.time
3641
+ };
3642
+ }
3643
+ function applyTickToPosition(position, tick) {
3644
+ const size = parseFloat(position.szi) || 0;
3645
+ const entry = parseFloat(position.entryPx) || 0;
3646
+ const collateral = parseFloat(position.collateralUsed) || 0;
3647
+ const maxLeverage = parseFloat(position.maxLeverage) || 0;
3648
+ const rollover = parseFloat(position.cumRollover) || 0;
3649
+ const isLong = position.side === "B";
3650
+ const ntl = size * tick.mid;
3651
+ const rawPnl = isLong ? (tick.mid - entry) * size : (entry - tick.mid) * size;
3652
+ const netPnl = rawPnl + rollover;
3653
+ const roe = collateral > 0 ? netPnl / collateral : 0;
3654
+ const currentLeverage = collateral > 0 ? ntl / collateral : 0;
3655
+ const maxWithdrawable = maxWithdrawForPosition(collateral, currentLeverage, maxLeverage);
3656
+ return {
3657
+ ...position,
3658
+ ntl: ntl.toString(),
3659
+ unrealizedPnl: netPnl.toString(),
3660
+ returnOnEquity: roe.toString(),
3661
+ maxWithdrawable: maxWithdrawable.toString()
3662
+ };
3663
+ }
3664
+ function updateResponseWithTick(current, tick, timestampMs) {
3665
+ if (!tick.pairId) return current;
3666
+ let changed = false;
3667
+ const pairPositions = current.pairPositions.map((pairPos) => {
3668
+ if (String(pairPos.position.pairId) !== String(tick.pairId)) {
3669
+ return pairPos;
3670
+ }
3671
+ changed = true;
3672
+ return {
3673
+ ...pairPos,
3674
+ position: applyTickToPosition(pairPos.position, tick)
3675
+ };
3676
+ });
3677
+ if (!changed) return current;
3678
+ return {
3679
+ pairPositions,
3680
+ marginSummary: aggregateMarginSummary(pairPositions),
3681
+ time: timestampMs
3682
+ };
3683
+ }
3684
+ var OstiumPositionUpdatesStream = class {
3685
+ priceStream;
3686
+ updateHandlers = /* @__PURE__ */ new Set();
3687
+ current;
3688
+ trackedPairIds;
3689
+ constructor(initial, trackedPairIds, priceStream) {
3690
+ this.current = cloneResponse(initial);
3691
+ this.priceStream = priceStream;
3692
+ this.trackedPairIds = new Set(trackedPairIds);
3693
+ if (!priceStream) return;
3694
+ priceStream.onSnapshot((ticks) => {
3695
+ this.ingestSnapshot(ticks);
3696
+ });
3697
+ priceStream.onTick((tick) => {
3698
+ this.ingestTick(tick);
3699
+ });
3700
+ }
3701
+ onUpdate(handler) {
3702
+ this.updateHandlers.add(handler);
3703
+ return () => {
3704
+ this.updateHandlers.delete(handler);
3705
+ };
3706
+ }
3707
+ onOpen(handler) {
3708
+ this.priceStream?.onOpen(handler);
3709
+ return this;
3710
+ }
3711
+ onError(handler) {
3712
+ this.priceStream?.onError(handler);
3713
+ return this;
3714
+ }
3715
+ onClose(handler) {
3716
+ this.priceStream?.onClose(handler);
3717
+ return this;
3718
+ }
3719
+ getCurrent() {
3720
+ return cloneResponse(this.current);
3721
+ }
3722
+ /**
3723
+ * Apply a single externally sourced price tick to the tracked positions.
3724
+ *
3725
+ * Use this when your app already has its own websocket connection and you want
3726
+ * the SDK to handle only the position recalculation logic.
3727
+ */
3728
+ ingestTick(tick) {
3729
+ if (!tick.pairId || !this.trackedPairIds.has(String(tick.pairId))) return;
3730
+ const next = updateResponseWithTick(this.current, tick, tick.timestampSeconds * 1e3);
3731
+ if (next !== this.current) {
3732
+ this.current = next;
3733
+ this.emit(next);
3734
+ }
3735
+ }
3736
+ /**
3737
+ * Apply a batch of externally sourced ticks, such as a websocket snapshot.
3738
+ */
3739
+ ingestSnapshot(ticks) {
3740
+ let next = this.current;
3741
+ for (const tick of ticks) {
3742
+ if (!tick.pairId || !this.trackedPairIds.has(String(tick.pairId))) continue;
3743
+ next = updateResponseWithTick(next, tick, tick.timestampSeconds * 1e3);
3744
+ }
3745
+ if (next !== this.current) {
3746
+ this.current = next;
3747
+ this.emit(next);
3748
+ }
3749
+ }
3750
+ close() {
3751
+ this.priceStream?.close();
3752
+ }
3753
+ emit(positions) {
3754
+ for (const handler of this.updateHandlers) handler(cloneResponse(positions));
3755
+ }
3756
+ };
3558
3757
  var EMPTY_RESULT = { long: [], short: [] };
3559
3758
  var ORDERBOOK_MAX_LEVELS = 20;
3560
3759
  function generateLogSpacedNotionals(levels, capacity) {
@@ -3739,31 +3938,40 @@ var OstiumSubgraphClient = class _OstiumSubgraphClient {
3739
3938
  * PnL fields are zeroed.
3740
3939
  */
3741
3940
  async getOpenPositions(params) {
3742
- const { user, blockNumber } = params;
3941
+ const { user, blockNumber, limit = Infinity, skip: initialSkip = 0 } = params;
3942
+ const isGlobal = user === "ALL";
3743
3943
  const [trades, prices] = await Promise.all([
3744
- paginateAll(async (skip, first) => {
3745
- const data = await this.query(
3746
- GetTraderOpenTradesQuery,
3747
- { trader: user.toLowerCase(), skip, first }
3748
- );
3749
- return data.trades;
3750
- }),
3944
+ paginateAll(
3945
+ async (skip, first) => {
3946
+ if (isGlobal) {
3947
+ const data2 = await this.query(
3948
+ GetAllOpenTradesQuery,
3949
+ { skip, first }
3950
+ );
3951
+ return data2.trades;
3952
+ }
3953
+ const data = await this.query(
3954
+ GetTraderOpenTradesQuery,
3955
+ { trader: user.toLowerCase(), skip, first }
3956
+ );
3957
+ return data.trades;
3958
+ },
3959
+ 1e3,
3960
+ limit,
3961
+ initialSkip
3962
+ ),
3751
3963
  this.fetchLivePricesSafe()
3752
3964
  ]);
3753
- let totalWithdrawable = 0;
3754
3965
  const pairPositions = trades.map((trade) => {
3755
3966
  const price = prices[`${trade.pair.from}/${trade.pair.to}`];
3756
3967
  const pnl = price && blockNumber ? getTradePnL(trade, price, blockNumber) : EMPTY_PNL;
3757
3968
  const maxLev = trade.isDayTrade ? formatLeverage(trade.pair.overnightMaxLeverage) : pairMaxLeverage(trade.pair);
3758
- const pairPos = formatPosition(trade, price, pnl, maxLev);
3759
- totalWithdrawable += parseFloat(pairPos.maxWithdrawable);
3760
- return pairPos;
3969
+ return formatPosition(trade, price, pnl, maxLev);
3761
3970
  });
3762
3971
  const marginSummary = aggregateMarginSummary(pairPositions);
3763
3972
  return {
3764
3973
  pairPositions,
3765
3974
  marginSummary,
3766
- withdrawable: totalWithdrawable.toString(),
3767
3975
  time: Date.now()
3768
3976
  };
3769
3977
  }
@@ -4014,6 +4222,24 @@ var OstiumSubgraphClient = class _OstiumSubgraphClient {
4014
4222
  });
4015
4223
  return OstiumPriceStream.connect(this.builderApiUrl, rawNames, cache);
4016
4224
  }
4225
+ /**
4226
+ * Stream price-driven updates for an existing `getOpenPositions()` response.
4227
+ *
4228
+ * The SDK subscribes only to the unique pairs present in `initial.pairPositions`,
4229
+ * recalculates price-sensitive fields for affected positions on each tick, and
4230
+ * emits the full updated response.
4231
+ *
4232
+ * Pass an existing `priceStream` to reuse a websocket connection your app has
4233
+ * already opened. Omit it to let the SDK open and manage a dedicated
4234
+ * connection for the tracked pair ids.
4235
+ */
4236
+ streamPositionUpdates(initial, priceStream) {
4237
+ const pairIds = [...new Set(initial.pairPositions.map(({ position }) => String(position.pairId)))];
4238
+ if (pairIds.length === 0) {
4239
+ return new OstiumPositionUpdatesStream(initial, []);
4240
+ }
4241
+ return new OstiumPositionUpdatesStream(initial, pairIds, priceStream ?? this.streamPrices(pairIds));
4242
+ }
4017
4243
  // ─────────────────────────────────────────────────────────────────────────
4018
4244
  // Internal — fetchers, cache, query wrapper
4019
4245
  // ─────────────────────────────────────────────────────────────────────────
@@ -4072,43 +4298,28 @@ var OstiumSubgraphClient = class _OstiumSubgraphClient {
4072
4298
  }
4073
4299
  }
4074
4300
  };
4075
- function aggregateMarginSummary(pairPositions) {
4076
- let accountValue = 0;
4077
- let totalCollateralUsed = 0;
4078
- let totalNtlPos = 0;
4079
- let totalRawPnlUsd = 0;
4080
- for (const { position } of pairPositions) {
4081
- const collateral = parseFloat(position.collateralUsed) || 0;
4082
- const ntl = parseFloat(position.ntl) || 0;
4083
- const pnl = parseFloat(position.unrealizedPnl) || 0;
4084
- accountValue += collateral + pnl;
4085
- totalCollateralUsed += collateral;
4086
- totalNtlPos += ntl;
4087
- totalRawPnlUsd += pnl;
4088
- }
4089
- return {
4090
- accountValue: accountValue.toString(),
4091
- totalCollateralUsed: totalCollateralUsed.toString(),
4092
- totalNtlPos: totalNtlPos.toString(),
4093
- totalRawPnlUsd: totalRawPnlUsd.toString()
4094
- };
4095
- }
4096
4301
 
4097
4302
  // src/client.ts
4098
4303
  var HEX64_RE = /^0x[0-9a-fA-F]{64}$/;
4099
4304
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
4100
4305
  function validateConfig(config) {
4101
- if (!HEX64_RE.test(config.privateKey)) {
4306
+ if (config.privateKey && !HEX64_RE.test(config.privateKey)) {
4102
4307
  throw new OstiumError(
4103
4308
  "privateKey must be a 64-character hex string prefixed with 0x",
4104
4309
  "INVALID_CONFIG" /* INVALID_CONFIG */
4105
4310
  );
4106
4311
  }
4107
- if (config.traderAddress && !ADDRESS_RE.test(config.traderAddress)) {
4108
- throw new OstiumError(
4109
- "traderAddress must be a valid 42-character Ethereum address (0x + 40 hex chars)",
4110
- "INVALID_CONFIG" /* INVALID_CONFIG */
4111
- );
4312
+ for (const [label, value] of [
4313
+ ["traderAddress", config.traderAddress],
4314
+ ["delegateAddress", config.delegateAddress],
4315
+ ["safeAddress", config.safeAddress]
4316
+ ]) {
4317
+ if (value && !ADDRESS_RE.test(value)) {
4318
+ throw new OstiumError(
4319
+ `${label} must be a valid 42-character Ethereum address (0x + 40 hex chars)`,
4320
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4321
+ );
4322
+ }
4112
4323
  }
4113
4324
  if (config.slippageBps !== void 0 && (config.slippageBps < 0 || config.slippageBps > 500)) {
4114
4325
  throw new OstiumError(
@@ -4122,10 +4333,45 @@ function validateConfig(config) {
4122
4333
  "INVALID_CONFIG" /* INVALID_CONFIG */
4123
4334
  );
4124
4335
  }
4336
+ let mode;
4337
+ try {
4338
+ mode = resolveMode(config);
4339
+ } catch {
4340
+ throw new OstiumError(
4341
+ "Could not infer client mode from config. Provide the required addresses for the selected factory.",
4342
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4343
+ );
4344
+ }
4345
+ if (mode === "self-self" && !config.privateKey && !config.traderAddress) {
4346
+ throw new OstiumError(
4347
+ "Self + Self mode requires traderPrivateKey for submission or traderAddress for build-only usage.",
4348
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4349
+ );
4350
+ }
4351
+ if (mode === "self-gasless" && !config.privateKey && (!config.traderAddress || !config.safeAddress)) {
4352
+ throw new OstiumError(
4353
+ "Self + Gasless build-only mode requires traderAddress and safeAddress.",
4354
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4355
+ );
4356
+ }
4357
+ if (mode === "delegated-self" && (!config.traderAddress || !config.privateKey && !config.delegateAddress)) {
4358
+ throw new OstiumError(
4359
+ "Delegated + Self mode requires traderAddress and either delegatePrivateKey or delegateAddress.",
4360
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4361
+ );
4362
+ }
4363
+ if (mode === "delegated-gasless" && (!config.traderAddress || !config.privateKey && (!config.delegateAddress || !config.safeAddress))) {
4364
+ throw new OstiumError(
4365
+ "Delegated + Gasless mode requires traderAddress and either delegatePrivateKey or both delegateAddress and safeAddress.",
4366
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4367
+ );
4368
+ }
4125
4369
  }
4126
4370
  var OstiumClient = class _OstiumClient {
4127
4371
  signer;
4128
4372
  submitter;
4373
+ traderAddress;
4374
+ effectiveSender;
4129
4375
  contracts;
4130
4376
  publicClient;
4131
4377
  defaultSlippageBps;
@@ -4140,6 +4386,8 @@ var OstiumClient = class _OstiumClient {
4140
4386
  constructor(internals) {
4141
4387
  this.signer = internals.signer;
4142
4388
  this.submitter = internals.submitter;
4389
+ this.traderAddress = internals.traderAddress;
4390
+ this.effectiveSender = internals.effectiveSender;
4143
4391
  this.contracts = internals.contracts;
4144
4392
  this.publicClient = internals.publicClient;
4145
4393
  this.defaultSlippageBps = internals.defaultSlippageBps;
@@ -4164,17 +4412,20 @@ var OstiumClient = class _OstiumClient {
4164
4412
  * });
4165
4413
  * await client.openTrade({ ... });
4166
4414
  * ```
4167
- */
4415
+ */
4168
4416
  static async createSelfAndSelf(params) {
4169
- return _OstiumClient._fromConfig({
4170
- privateKey: params.traderPrivateKey,
4171
- rpcUrl: params.rpcUrl,
4417
+ const base = {
4418
+ mode: "self-self",
4172
4419
  testnet: params.testnet,
4173
4420
  slippageBps: params.slippageBps,
4174
4421
  builder: params.builder,
4175
4422
  subgraphUrl: params.subgraphUrl,
4176
- builderApiUrl: params.builderApiUrl
4177
- });
4423
+ builderApiUrl: params.builderApiUrl,
4424
+ rpcUrl: params.rpcUrl
4425
+ };
4426
+ return _OstiumClient._fromConfig(
4427
+ "traderPrivateKey" in params ? { ...base, privateKey: params.traderPrivateKey } : { ...base, traderAddress: params.traderAddress }
4428
+ );
4178
4429
  }
4179
4430
  /**
4180
4431
  * **Self + Gasless** — your EOA owns everything; a Safe relays trades for free.
@@ -4198,17 +4449,27 @@ var OstiumClient = class _OstiumClient {
4198
4449
  */
4199
4450
  static async createSelfAndGasless(params) {
4200
4451
  const defaultUrl = params.testnet ? DEFAULT_PIMLICO_URL_TESTNET : DEFAULT_PIMLICO_URL;
4201
- return _OstiumClient._fromConfig({
4202
- privateKey: params.traderPrivateKey,
4203
- pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4452
+ const base = {
4453
+ mode: "self-gasless",
4204
4454
  rpcUrl: params.rpcUrl,
4205
- sponsorshipPolicyId: params.sponsorshipPolicyId,
4206
4455
  testnet: params.testnet,
4207
4456
  slippageBps: params.slippageBps,
4208
4457
  builder: params.builder,
4209
4458
  subgraphUrl: params.subgraphUrl,
4210
4459
  builderApiUrl: params.builderApiUrl
4211
- });
4460
+ };
4461
+ return _OstiumClient._fromConfig(
4462
+ "traderPrivateKey" in params ? {
4463
+ ...base,
4464
+ privateKey: params.traderPrivateKey,
4465
+ pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4466
+ sponsorshipPolicyId: params.sponsorshipPolicyId
4467
+ } : {
4468
+ ...base,
4469
+ traderAddress: params.traderAddress,
4470
+ safeAddress: params.safeAddress
4471
+ }
4472
+ );
4212
4473
  }
4213
4474
  /**
4214
4475
  * **Delegated + Self** — a delegate EOA signs and pays gas on behalf of a trader address.
@@ -4228,8 +4489,8 @@ var OstiumClient = class _OstiumClient {
4228
4489
  * ```
4229
4490
  */
4230
4491
  static async createDelegatedAndSelf(params) {
4231
- return _OstiumClient._fromConfig({
4232
- privateKey: params.delegatePrivateKey,
4492
+ const base = {
4493
+ mode: "delegated-self",
4233
4494
  traderAddress: params.traderAddress,
4234
4495
  rpcUrl: params.rpcUrl,
4235
4496
  testnet: params.testnet,
@@ -4237,7 +4498,10 @@ var OstiumClient = class _OstiumClient {
4237
4498
  builder: params.builder,
4238
4499
  subgraphUrl: params.subgraphUrl,
4239
4500
  builderApiUrl: params.builderApiUrl
4240
- });
4501
+ };
4502
+ return _OstiumClient._fromConfig(
4503
+ "delegatePrivateKey" in params ? { ...base, privateKey: params.delegatePrivateKey } : { ...base, delegateAddress: params.delegateAddress }
4504
+ );
4241
4505
  }
4242
4506
  /**
4243
4507
  * **Delegated + Gasless** — a Safe derived from the delegate key submits sponsored
@@ -4258,18 +4522,28 @@ var OstiumClient = class _OstiumClient {
4258
4522
  */
4259
4523
  static async createDelegatedAndGasless(params) {
4260
4524
  const defaultUrl = params.testnet ? DEFAULT_PIMLICO_URL_TESTNET : DEFAULT_PIMLICO_URL;
4261
- return _OstiumClient._fromConfig({
4262
- privateKey: params.delegatePrivateKey,
4525
+ const base = {
4526
+ mode: "delegated-gasless",
4263
4527
  traderAddress: params.traderAddress,
4264
- pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4265
4528
  rpcUrl: params.rpcUrl,
4266
- sponsorshipPolicyId: params.sponsorshipPolicyId,
4267
4529
  testnet: params.testnet,
4268
4530
  slippageBps: params.slippageBps,
4269
4531
  builder: params.builder,
4270
4532
  subgraphUrl: params.subgraphUrl,
4271
4533
  builderApiUrl: params.builderApiUrl
4272
- });
4534
+ };
4535
+ return _OstiumClient._fromConfig(
4536
+ "delegatePrivateKey" in params ? {
4537
+ ...base,
4538
+ privateKey: params.delegatePrivateKey,
4539
+ pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4540
+ sponsorshipPolicyId: params.sponsorshipPolicyId
4541
+ } : {
4542
+ ...base,
4543
+ delegateAddress: params.delegateAddress,
4544
+ safeAddress: params.safeAddress
4545
+ }
4546
+ );
4273
4547
  }
4274
4548
  /**
4275
4549
  * **Read-only client** — no signer, no submitter, no privateKey required.
@@ -4314,6 +4588,7 @@ var OstiumClient = class _OstiumClient {
4314
4588
  /** Internal factory shared by all public constructors. */
4315
4589
  static async _fromConfig(config) {
4316
4590
  validateConfig(config);
4591
+ const mode = resolveMode(config);
4317
4592
  const testnet = config.testnet ?? false;
4318
4593
  const networkConfig = getNetworkConfig(testnet);
4319
4594
  const chain = testnet ? arbitrumSepolia : arbitrum;
@@ -4323,18 +4598,29 @@ var OstiumClient = class _OstiumClient {
4323
4598
  tradingStorage: networkConfig.contracts.tradingStorage,
4324
4599
  usdc: networkConfig.contracts.usdc
4325
4600
  };
4326
- const selfGasless = isGasless(config) && !isDelegated(config);
4327
- const submitter = await createSubmitter(config, testnet);
4601
+ const selfGasless = mode === "self-gasless";
4328
4602
  let traderAddress;
4329
- if (isDelegated(config)) {
4603
+ if (mode === "self-self" || mode === "self-gasless") {
4604
+ traderAddress = config.traderAddress ?? privateKeyToAccount(config.privateKey).address;
4605
+ } else {
4330
4606
  traderAddress = config.traderAddress;
4607
+ }
4608
+ const signer = createSigner(mode, traderAddress);
4609
+ let submitter;
4610
+ let effectiveSender;
4611
+ if (hasPrivateKey(config)) {
4612
+ submitter = await createSubmitter(config, testnet, mode);
4613
+ effectiveSender = submitter.effectiveSender;
4614
+ } else if (mode === "self-self") {
4615
+ effectiveSender = traderAddress;
4616
+ } else if (mode === "delegated-self") {
4617
+ effectiveSender = config.delegateAddress;
4331
4618
  } else {
4332
- traderAddress = privateKeyToAccount(config.privateKey).address;
4619
+ effectiveSender = config.safeAddress;
4333
4620
  }
4334
- const signer = createSigner(config, traderAddress, selfGasless);
4335
4621
  const publicRpc = testnet ? "https://sepolia-rollup.arbitrum.io/rpc" : "https://arb1.arbitrum.io/rpc";
4336
- const rpcUrl = config.rpcUrl ?? (isGasless(config) ? publicRpc : void 0);
4337
- if (!isGasless(config) && !config.rpcUrl) {
4622
+ const rpcUrl = config.rpcUrl ?? publicRpc;
4623
+ if (hasPrivateKey(config) && mode !== "self-gasless" && mode !== "delegated-gasless" && !config.rpcUrl) {
4338
4624
  throw new OstiumError(
4339
4625
  "rpcUrl is required for self-submission mode",
4340
4626
  "INVALID_CONFIG" /* INVALID_CONFIG */
@@ -4342,7 +4628,7 @@ var OstiumClient = class _OstiumClient {
4342
4628
  }
4343
4629
  const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
4344
4630
  let eoaSubmit;
4345
- if (selfGasless) {
4631
+ if (selfGasless && hasPrivateKey(config)) {
4346
4632
  const eoaAccount = privateKeyToAccount(config.privateKey);
4347
4633
  const eoaRpcUrl = config.rpcUrl ?? publicRpc;
4348
4634
  const eoaWalletClient = createWalletClient({ account: eoaAccount, chain, transport: http(eoaRpcUrl) });
@@ -4364,11 +4650,13 @@ var OstiumClient = class _OstiumClient {
4364
4650
  return new _OstiumClient({
4365
4651
  signer,
4366
4652
  submitter,
4653
+ traderAddress,
4654
+ effectiveSender,
4367
4655
  contracts,
4368
4656
  publicClient,
4369
4657
  defaultSlippageBps,
4370
- delegated: isDelegated(config),
4371
- gasless: isGasless(config),
4658
+ delegated: mode === "delegated-self" || mode === "delegated-gasless",
4659
+ gasless: mode === "self-gasless" || mode === "delegated-gasless",
4372
4660
  selfGasless,
4373
4661
  eoaSubmit,
4374
4662
  builderAddress: config.builder?.address,
@@ -4386,17 +4674,25 @@ var OstiumClient = class _OstiumClient {
4386
4674
  * - Self + Gasless: EOA derived from privateKey (NOT the Safe — USDC lives here).
4387
4675
  */
4388
4676
  getTraderAddress() {
4389
- if (!this.signer) {
4677
+ if (!this.traderAddress) {
4390
4678
  throw new OstiumError(
4391
4679
  "No connected trader \u2014 pass `user` explicitly or use one of the createSelfAnd*/createDelegatedAnd* factories.",
4392
4680
  "INVALID_CONFIG" /* INVALID_CONFIG */
4393
4681
  );
4394
4682
  }
4395
- return this.signer.traderAddress;
4683
+ return this.traderAddress;
4396
4684
  }
4397
- /** True when the client was created via `createReadOnly` and cannot submit transactions. */
4685
+ /** True when the client cannot submit transactions directly. */
4398
4686
  isReadOnly() {
4399
- return !this.signer || !this.submitter;
4687
+ return !this.submitter;
4688
+ }
4689
+ /** True when the client can build mode-correct unsigned transaction requests. */
4690
+ canBuildTransactions() {
4691
+ return !!this.signer && !!this.effectiveSender;
4692
+ }
4693
+ /** True when the client has the credentials needed for SDK-managed submission. */
4694
+ canSubmitTransactions() {
4695
+ return !!this.submitter;
4400
4696
  }
4401
4697
  /**
4402
4698
  * Returns the Safe smart-account address used for gasless submission.
@@ -4408,7 +4704,7 @@ var OstiumClient = class _OstiumClient {
4408
4704
  * - Self + Self / Delegated + Self: returns undefined.
4409
4705
  */
4410
4706
  getSmartAccountAddress() {
4411
- return this.gasless && this.submitter ? this.submitter.effectiveSender : void 0;
4707
+ return this.gasless ? this.effectiveSender : void 0;
4412
4708
  }
4413
4709
  // ─────────────────────────────────────────────────────────────────────────
4414
4710
  // Self + Gasless setup
@@ -4432,15 +4728,10 @@ var OstiumClient = class _OstiumClient {
4432
4728
  * ```
4433
4729
  */
4434
4730
  async setupGaslessDelegation() {
4435
- if (!this.selfGasless || !this.eoaSubmit || !this.submitter) {
4436
- throw new OstiumError(
4437
- "setupGaslessDelegation() is only available in Self + Gasless mode. Create the client with pimlicoUrl but without traderAddress.",
4438
- "INVALID_CONFIG" /* INVALID_CONFIG */
4439
- );
4440
- }
4441
- const safeAddress = this.submitter.effectiveSender;
4442
- const encoded = encodeSetDelegate(safeAddress, this.contracts.trading);
4443
- return this.submitDirectFromEoa(encoded);
4731
+ return this.submitDirectEoa(this.getSetupGaslessDelegationEncoded());
4732
+ }
4733
+ getSetupGaslessDelegationTx() {
4734
+ return this.buildDirectEoaTx(this.getSetupGaslessDelegationEncoded());
4444
4735
  }
4445
4736
  // ─────────────────────────────────────────────────────────────────────────
4446
4737
  // USDC helpers
@@ -4502,18 +4793,14 @@ var OstiumClient = class _OstiumClient {
4502
4793
  * @param amount USD amount as decimal string (e.g. "1000"), or "max" for MaxUint256.
4503
4794
  */
4504
4795
  async approveUsdc(amount) {
4505
- if (this.delegated) {
4506
- throw new OstiumError(
4507
- "approveUsdc is not available in delegated mode. The trader must approve USDC directly from their own account.",
4508
- "DELEGATION_FAILED" /* DELEGATION_FAILED */
4509
- );
4510
- }
4511
- const rawAmount = amount === "max" ? maxUint256 : parseUsdc(amount);
4512
- const encoded = encodeUsdcApprove(this.contracts.tradingStorage, rawAmount, this.contracts.usdc);
4796
+ const encoded = this.getApproveUsdcEncoded(amount);
4513
4797
  if (this.selfGasless) {
4514
- return this.submitDirectFromEoa(encoded);
4798
+ return this.submitDirectEoa(encoded);
4515
4799
  }
4516
- return this.requireWriter().submitter.submit(encoded);
4800
+ return this.submitPrepared(encoded);
4801
+ }
4802
+ getApproveUsdcTx(amount) {
4803
+ return this.buildDirectEoaTx(this.getApproveUsdcEncoded(amount));
4517
4804
  }
4518
4805
  // ─────────────────────────────────────────────────────────────────────────
4519
4806
  // Delegation helpers
@@ -4527,16 +4814,20 @@ var OstiumClient = class _OstiumClient {
4527
4814
  * update the trader's delegation on their behalf.
4528
4815
  */
4529
4816
  async setDelegate(delegateAddress) {
4530
- const encoded = encodeSetDelegate(delegateAddress, this.contracts.trading);
4531
- return this.execute(encoded);
4817
+ return this.submitPrepared(this.getSetDelegateEncoded(delegateAddress));
4818
+ }
4819
+ getSetDelegateTx(delegateAddress) {
4820
+ return this.buildPreparedTx(this.getSetDelegateEncoded(delegateAddress));
4532
4821
  }
4533
4822
  /**
4534
4823
  * Remove the current delegate on the trading contract.
4535
4824
  * Follows the same delegation-wrapping rules as setDelegate().
4536
4825
  */
4537
4826
  async removeDelegate() {
4538
- const encoded = encodeRemoveDelegate(this.contracts.trading);
4539
- return this.execute(encoded);
4827
+ return this.submitPrepared(this.getRemoveDelegateEncoded());
4828
+ }
4829
+ getRemoveDelegateTx() {
4830
+ return this.buildPreparedTx(this.getRemoveDelegateEncoded());
4540
4831
  }
4541
4832
  // ─────────────────────────────────────────────────────────────────────────
4542
4833
  // Core trading methods
@@ -4549,6 +4840,12 @@ var OstiumClient = class _OstiumClient {
4549
4840
  * the current allowance.
4550
4841
  */
4551
4842
  async openTrade(params) {
4843
+ return this.submitPrepared(this.getOpenTradeEncoded(params));
4844
+ }
4845
+ getOpenTradeTx(params) {
4846
+ return this.buildPreparedTx(this.getOpenTradeEncoded(params));
4847
+ }
4848
+ getOpenTradeEncoded(params) {
4552
4849
  const collateralNum = parseFloat(params.collateral);
4553
4850
  if (collateralNum > MAX_COLLATERAL_USD) {
4554
4851
  throw new OstiumError(
@@ -4557,7 +4854,7 @@ var OstiumClient = class _OstiumClient {
4557
4854
  );
4558
4855
  }
4559
4856
  const apiOpen = {
4560
- a: params.pairIndex,
4857
+ a: Number(params.pairId),
4561
4858
  b: params.buy,
4562
4859
  p: params.price,
4563
4860
  s: params.collateral,
@@ -4580,8 +4877,7 @@ var OstiumClient = class _OstiumClient {
4580
4877
  }
4581
4878
  const builderFee = buildBuilderFee(apiBuilder);
4582
4879
  const slippageP = params.type === "market" /* Market */ ? BigInt(this.resolveSlippage(params.slippage)) : 0n;
4583
- const encoded = encodeOpenTrade(trade, builderFee, orderType, slippageP, this.contracts.trading);
4584
- return this.execute(encoded);
4880
+ return encodeOpenTrade(trade, builderFee, orderType, slippageP, this.contracts.trading);
4585
4881
  }
4586
4882
  /**
4587
4883
  * Close an open position (full or partial).
@@ -4596,6 +4892,12 @@ var OstiumClient = class _OstiumClient {
4596
4892
  * ```
4597
4893
  */
4598
4894
  async closeTrade(params) {
4895
+ return this.submitPrepared(this.getCloseTradeEncoded(params));
4896
+ }
4897
+ getCloseTradeTx(params) {
4898
+ return this.buildPreparedTx(this.getCloseTradeEncoded(params));
4899
+ }
4900
+ getCloseTradeEncoded(params) {
4599
4901
  try {
4600
4902
  validateCloseParams({
4601
4903
  a: Number(params.pairId),
@@ -4615,7 +4917,7 @@ var OstiumClient = class _OstiumClient {
4615
4917
  const closePercentage = BigInt(Math.round(params.closePercent * 100));
4616
4918
  const marketPrice = parsePrice(params.price);
4617
4919
  const slippageP = BigInt(this.resolveSlippage(params.slippage));
4618
- const encoded = encodeCloseTradeMarket(
4920
+ return encodeCloseTradeMarket(
4619
4921
  pairIndex,
4620
4922
  index,
4621
4923
  closePercentage,
@@ -4623,7 +4925,6 @@ var OstiumClient = class _OstiumClient {
4623
4925
  slippageP,
4624
4926
  this.contracts.trading
4625
4927
  );
4626
- return this.execute(encoded);
4627
4928
  }
4628
4929
  /**
4629
4930
  * Cancel a pending order.
@@ -4646,6 +4947,12 @@ var OstiumClient = class _OstiumClient {
4646
4947
  * ```
4647
4948
  */
4648
4949
  async cancelOrder(params) {
4950
+ return this.submitPrepared(this.getCancelOrderEncoded(params));
4951
+ }
4952
+ getCancelOrderTx(params) {
4953
+ return this.buildPreparedTx(this.getCancelOrderEncoded(params));
4954
+ }
4955
+ getCancelOrderEncoded(params) {
4649
4956
  let encoded;
4650
4957
  if (params.type === "limit" /* Limit */) {
4651
4958
  const pairIdNum = Number(params.pairId);
@@ -4682,7 +4989,7 @@ var OstiumClient = class _OstiumClient {
4682
4989
  }
4683
4990
  encoded = encodeOpenTradeMarketTimeout(BigInt(params.orderId), this.contracts.trading);
4684
4991
  }
4685
- return this.execute(encoded);
4992
+ return encoded;
4686
4993
  }
4687
4994
  /**
4688
4995
  * Modify an open trade or a pending limit order.
@@ -4707,6 +5014,12 @@ var OstiumClient = class _OstiumClient {
4707
5014
  * - Both `takeProfit` and `stopLoss` without `price` → throws (send two calls).
4708
5015
  */
4709
5016
  async modifyOrder(params) {
5017
+ return this.submitPrepared(this.getModifyOrderEncoded(params));
5018
+ }
5019
+ getModifyOrderTx(params) {
5020
+ return this.buildPreparedTx(this.getModifyOrderEncoded(params));
5021
+ }
5022
+ getModifyOrderEncoded(params) {
4710
5023
  try {
4711
5024
  validateModifyParams({
4712
5025
  a: Number(params.pairId),
@@ -4746,7 +5059,7 @@ var OstiumClient = class _OstiumClient {
4746
5059
  "VALIDATION_FAILED" /* VALIDATION_FAILED */
4747
5060
  );
4748
5061
  }
4749
- return this.execute(encoded);
5062
+ return encoded;
4750
5063
  }
4751
5064
  /**
4752
5065
  * Update collateral on an open position (isolated margin).
@@ -4766,6 +5079,12 @@ var OstiumClient = class _OstiumClient {
4766
5079
  * Checks USDC allowance before top-up operations.
4767
5080
  */
4768
5081
  async updateCollateral(params) {
5082
+ return this.submitPrepared(await this.getUpdateCollateralEncoded(params));
5083
+ }
5084
+ async getUpdateCollateralTx(params) {
5085
+ return this.buildPreparedTx(await this.getUpdateCollateralEncoded(params));
5086
+ }
5087
+ async getUpdateCollateralEncoded(params) {
4769
5088
  const amount = parseFloat(params.amount);
4770
5089
  if (Number.isNaN(amount) || amount === 0) {
4771
5090
  throw new OstiumError(
@@ -4789,7 +5108,7 @@ var OstiumClient = class _OstiumClient {
4789
5108
  } else {
4790
5109
  encoded = encodeRemoveCollateral(pairIndex, index, absAmount, this.contracts.trading);
4791
5110
  }
4792
- return this.execute(encoded);
5111
+ return encoded;
4793
5112
  }
4794
5113
  // ─────────────────────────────────────────────────────────────────────────
4795
5114
  // Subgraph reads (Hyperliquid-style API)
@@ -4808,11 +5127,18 @@ var OstiumClient = class _OstiumClient {
4808
5127
  /**
4809
5128
  * Open positions + margin summary for a user. Defaults to the connected
4810
5129
  * trader. Live prices and the current block number are fetched automatically.
5130
+ *
5131
+ * - `user`: pass an address to scope to a single trader, or `'ALL'` to fetch
5132
+ * positions across every trader (no trader filter).
5133
+ * - `limit`: cap the number of positions returned (default: all).
5134
+ * - `skip`: offset into the result set for pagination (default: `0`).
4811
5135
  */
4812
5136
  async getOpenPositions(params) {
4813
5137
  return this.subgraph.getOpenPositions({
4814
5138
  user: params?.user ?? this.getTraderAddress(),
4815
- blockNumber: params?.blockNumber ?? await this.publicClient.getBlockNumber()
5139
+ blockNumber: params?.blockNumber ?? await this.publicClient.getBlockNumber(),
5140
+ limit: params?.limit,
5141
+ skip: params?.skip
4816
5142
  });
4817
5143
  }
4818
5144
  /**
@@ -4917,31 +5243,104 @@ var OstiumClient = class _OstiumClient {
4917
5243
  streamPrices(pairIds) {
4918
5244
  return this.subgraph.streamPrices(pairIds);
4919
5245
  }
5246
+ /**
5247
+ * Stream price-driven updates for an existing `getOpenPositions()` response.
5248
+ *
5249
+ * Subscribes only to the unique pairs referenced by the response and emits the
5250
+ * full updated positions payload on each relevant price update. Pass an
5251
+ * existing `priceStream` to reuse a websocket your app already owns, or omit
5252
+ * it to let the SDK create a dedicated connection.
5253
+ */
5254
+ streamPositionUpdates(initial, priceStream) {
5255
+ return this.subgraph.streamPositionUpdates(initial, priceStream);
5256
+ }
4920
5257
  // ─────────────────────────────────────────────────────────────────────────
4921
5258
  // Internal helpers
4922
5259
  // ─────────────────────────────────────────────────────────────────────────
4923
- /**
4924
- * Core execution pipeline:
4925
- * 1. signer.prepare() — optionally wraps in delegatedAction.
4926
- * 2. submitter.submit() — sends via EOA walletClient or Pimlico UserOp.
4927
- */
4928
- async execute(encoded) {
4929
- const { signer, submitter } = this.requireWriter();
4930
- const prepared = signer.prepare(encoded);
4931
- return submitter.submit(prepared);
4932
- }
4933
- /** Throws if the client is read-only; otherwise returns the signer + submitter. */
4934
- requireWriter() {
4935
- if (!this.signer || !this.submitter) {
5260
+ getSetupGaslessDelegationEncoded() {
5261
+ if (!this.selfGasless || !this.effectiveSender) {
4936
5262
  throw new OstiumError(
4937
- "This client is read-only. Use one of the createSelfAnd*/createDelegatedAnd* factories to enable writes.",
5263
+ "setupGaslessDelegation() is only available in Self + Gasless mode.",
4938
5264
  "INVALID_CONFIG" /* INVALID_CONFIG */
4939
5265
  );
4940
5266
  }
4941
- return { signer: this.signer, submitter: this.submitter };
5267
+ return encodeSetDelegate(this.effectiveSender, this.contracts.trading);
5268
+ }
5269
+ getApproveUsdcEncoded(amount) {
5270
+ if (this.delegated) {
5271
+ throw new OstiumError(
5272
+ "approveUsdc is not available in delegated mode. The trader must approve USDC directly from their own account.",
5273
+ "DELEGATION_FAILED" /* DELEGATION_FAILED */
5274
+ );
5275
+ }
5276
+ const rawAmount = amount === "max" ? maxUint256 : parseUsdc(amount);
5277
+ return encodeUsdcApprove(this.contracts.tradingStorage, rawAmount, this.contracts.usdc);
5278
+ }
5279
+ getSetDelegateEncoded(delegateAddress) {
5280
+ return encodeSetDelegate(delegateAddress, this.contracts.trading);
5281
+ }
5282
+ getRemoveDelegateEncoded() {
5283
+ return encodeRemoveDelegate(this.contracts.trading);
5284
+ }
5285
+ prepareEncoded(encoded) {
5286
+ const signer = this.requireBuildCapability();
5287
+ return signer.prepare(encoded);
5288
+ }
5289
+ buildPreparedTx(encoded) {
5290
+ return this.toBuiltTxRequest(this.prepareEncoded(encoded));
5291
+ }
5292
+ buildDirectEoaTx(encoded) {
5293
+ if (!this.canBuildTransactions() || !this.traderAddress) {
5294
+ throw new OstiumError(
5295
+ "This client cannot build transactions. Use one of the createSelfAnd*/createDelegatedAnd* factories.",
5296
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5297
+ );
5298
+ }
5299
+ return {
5300
+ kind: "eoa",
5301
+ to: encoded.to,
5302
+ data: encoded.data,
5303
+ value: encoded.value,
5304
+ from: this.traderAddress,
5305
+ traderAddress: this.traderAddress
5306
+ };
5307
+ }
5308
+ toBuiltTxRequest(encoded) {
5309
+ if (!this.canBuildTransactions() || !this.traderAddress || !this.effectiveSender) {
5310
+ throw new OstiumError(
5311
+ "This client cannot build transactions. Use one of the createSelfAnd*/createDelegatedAnd* factories.",
5312
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5313
+ );
5314
+ }
5315
+ if (this.gasless) {
5316
+ return {
5317
+ kind: "safe",
5318
+ safeAddress: this.effectiveSender,
5319
+ traderAddress: this.traderAddress,
5320
+ calls: [encoded]
5321
+ };
5322
+ }
5323
+ return {
5324
+ kind: "eoa",
5325
+ to: encoded.to,
5326
+ data: encoded.data,
5327
+ value: encoded.value,
5328
+ from: this.effectiveSender,
5329
+ traderAddress: this.traderAddress
5330
+ };
5331
+ }
5332
+ async submitPrepared(encoded) {
5333
+ const submitter = this.requireSubmitCapability();
5334
+ return submitter.submit(this.prepareEncoded(encoded));
4942
5335
  }
4943
5336
  /** Direct EOA submission — used only in Self + Gasless for approveUsdc and setupGaslessDelegation. */
4944
- async submitDirectFromEoa(encoded) {
5337
+ async submitDirectEoa(encoded) {
5338
+ if (!this.eoaSubmit) {
5339
+ throw new OstiumError(
5340
+ "This client cannot submit direct EOA transactions.",
5341
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5342
+ );
5343
+ }
4945
5344
  try {
4946
5345
  return await this.eoaSubmit(encoded);
4947
5346
  } catch (err) {
@@ -4957,6 +5356,24 @@ var OstiumClient = class _OstiumClient {
4957
5356
  throw new OstiumError(`Transaction failed: ${msg}`, "SUBMISSION_FAILED" /* SUBMISSION_FAILED */, err);
4958
5357
  }
4959
5358
  }
5359
+ requireBuildCapability() {
5360
+ if (!this.signer) {
5361
+ throw new OstiumError(
5362
+ "This client cannot build transactions. Use one of the createSelfAnd*/createDelegatedAnd* factories.",
5363
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5364
+ );
5365
+ }
5366
+ return this.signer;
5367
+ }
5368
+ requireSubmitCapability() {
5369
+ if (!this.submitter) {
5370
+ throw new OstiumError(
5371
+ "This client does not have submission credentials. Use the corresponding get*Tx() method or initialize with a private key.",
5372
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5373
+ );
5374
+ }
5375
+ return this.submitter;
5376
+ }
4960
5377
  resolveSlippage(override) {
4961
5378
  return override ?? this.defaultSlippageBps;
4962
5379
  }
@@ -5839,7 +6256,7 @@ async function actionOpenTrade(client2) {
5839
6256
  const result = await spin(
5840
6257
  "Submitting openTrade\u2026",
5841
6258
  () => client2.openTrade({
5842
- pairIndex: pairId,
6259
+ pairId,
5843
6260
  buy: buy === "true",
5844
6261
  price,
5845
6262
  collateral,
@@ -5929,7 +6346,7 @@ async function actionCloseTrade(client2) {
5929
6346
  }
5930
6347
  async function actionCancelOrder(client2) {
5931
6348
  let cancelType = "limit" /* Limit */;
5932
- let pairIndex = "0";
6349
+ let pairIdInput = "0";
5933
6350
  let orderIndex = "0";
5934
6351
  let orderId = "";
5935
6352
  let retry = false;
@@ -5948,12 +6365,12 @@ async function actionCancelOrder(client2) {
5948
6365
  }
5949
6366
  case 1: {
5950
6367
  if (cancelType === "limit" /* Limit */) {
5951
- const v = await wtext({ message: "Pair index", initialValue: pairIndex });
6368
+ const v = await wtext({ message: "Pair ID", initialValue: pairIdInput });
5952
6369
  if (v === BACK) {
5953
6370
  step--;
5954
6371
  break;
5955
6372
  }
5956
- pairIndex = v;
6373
+ pairIdInput = v;
5957
6374
  step++;
5958
6375
  break;
5959
6376
  } else {
@@ -6000,7 +6417,7 @@ async function actionCancelOrder(client2) {
6000
6417
  }
6001
6418
  }
6002
6419
  }
6003
- const cancelParams = cancelType === "limit" /* Limit */ ? { type: "limit" /* Limit */, pairId: parseInt(pairIndex), idx: parseInt(orderIndex) } : cancelType === "pendingClose" /* PendingClose */ ? { type: "pendingClose" /* PendingClose */, orderId: parseInt(orderId), retry } : { type: "pendingOpen" /* PendingOpen */, orderId: parseInt(orderId) };
6420
+ const cancelParams = cancelType === "limit" /* Limit */ ? { type: "limit" /* Limit */, pairId: parseInt(pairIdInput), idx: parseInt(orderIndex) } : cancelType === "pendingClose" /* PendingClose */ ? { type: "pendingClose" /* PendingClose */, orderId: parseInt(orderId), retry } : { type: "pendingOpen" /* PendingOpen */, orderId: parseInt(orderId) };
6004
6421
  const result = await spin(
6005
6422
  "Submitting cancelOrder\u2026",
6006
6423
  () => client2.cancelOrder(cancelParams)
@@ -6010,7 +6427,7 @@ async function actionCancelOrder(client2) {
6010
6427
  async function actionModifyOrder(client2) {
6011
6428
  const fields = await p.group(
6012
6429
  {
6013
- pairIndex: () => p.text({ message: "Pair index", initialValue: "0" }),
6430
+ pairIndex: () => p.text({ message: "Pair ID", initialValue: "0" }),
6014
6431
  index: () => p.text({ message: "Trade / order index", initialValue: "0" }),
6015
6432
  price: () => p.text({ message: "New limit price (optional)", placeholder: "leave blank to skip" }),
6016
6433
  tp: () => p.text({ message: "New take profit (optional)", placeholder: "leave blank to skip" }),
@@ -6033,7 +6450,7 @@ async function actionModifyOrder(client2) {
6033
6450
  async function actionUpdateCollateral(client2) {
6034
6451
  const fields = await p.group(
6035
6452
  {
6036
- pairIndex: () => p.text({ message: "Pair index", initialValue: "0" }),
6453
+ pairIndex: () => p.text({ message: "Pair ID", initialValue: "0" }),
6037
6454
  index: () => p.text({ message: "Trade index", initialValue: "0" }),
6038
6455
  amount: () => p.text({
6039
6456
  message: "Amount (positive = add, negative = remove)",
@@ -6172,7 +6589,7 @@ async function actionViewPrices(client2) {
6172
6589
  }
6173
6590
  }
6174
6591
  async function actionViewPositions(client2) {
6175
- const { pairPositions, marginSummary, withdrawable } = await spin(
6592
+ const { pairPositions, marginSummary } = await spin(
6176
6593
  "Fetching open positions\u2026",
6177
6594
  () => client2.getOpenPositions()
6178
6595
  );
@@ -6209,7 +6626,8 @@ async function actionViewPositions(client2) {
6209
6626
  p.log.info(`Total collateral: $${parseFloat(marginSummary.totalCollateralUsed).toFixed(4)}`);
6210
6627
  p.log.info(`Total notional: $${parseFloat(marginSummary.totalNtlPos).toFixed(4)}`);
6211
6628
  p.log.info(`Unrealized PnL: $${parseFloat(marginSummary.totalRawPnlUsd).toFixed(4)}`);
6212
- p.log.info(`Max withdrawable: $${parseFloat(withdrawable).toFixed(4)}`);
6629
+ p.log.info(`Cum rollover: $${parseFloat(marginSummary.totalCumRollover).toFixed(4)}`);
6630
+ p.log.info(`Max withdrawable: $${parseFloat(marginSummary.totalWithdrawable).toFixed(4)}`);
6213
6631
  }
6214
6632
  async function actionViewFills(client2) {
6215
6633
  const limitRaw = await p.text({