@ostium/builder-sdk 0.1.0 → 0.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.
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) {
@@ -3199,10 +3220,11 @@ async function fetchLivePrices(builderApiUrl) {
3199
3220
  }
3200
3221
  return result;
3201
3222
  }
3202
- function rawTickToPublic(item) {
3223
+ function rawTickToPublic(item, pairId) {
3203
3224
  const from = normalizePairName(item.from);
3204
3225
  const to = normalizePairName(item.to);
3205
3226
  return {
3227
+ pairId,
3206
3228
  feedId: item.feed_id,
3207
3229
  pair: `${from}-${to}`,
3208
3230
  from,
@@ -3249,9 +3271,12 @@ var OstiumPriceStream = class _OstiumPriceStream {
3249
3271
  snapshotHandlers = /* @__PURE__ */ new Set();
3250
3272
  /** pairId (string) → raw "FROM-TO" name for the WS API. */
3251
3273
  pairRawNameCache;
3252
- constructor(ws, pairRawNameCache, initialSubscribeRawPairs) {
3274
+ /** raw "FROM-TO" name → pairId (string) for mapping incoming ticks back to SDK ids. */
3275
+ rawNamePairIdCache;
3276
+ constructor(ws, pairRawNameCache, rawNamePairIdCache, initialSubscribeRawPairs) {
3253
3277
  this.ws = ws;
3254
3278
  this.pairRawNameCache = pairRawNameCache;
3279
+ this.rawNamePairIdCache = rawNamePairIdCache;
3255
3280
  if (initialSubscribeRawPairs?.length) {
3256
3281
  ws.once("open", () => {
3257
3282
  ws.send(JSON.stringify({ type: "subscribe", pairs: initialSubscribeRawPairs }));
@@ -3265,10 +3290,13 @@ var OstiumPriceStream = class _OstiumPriceStream {
3265
3290
  return;
3266
3291
  }
3267
3292
  if (msg.type === "snapshot" && Array.isArray(msg.data)) {
3268
- const ticks = msg.data.map(rawTickToPublic);
3293
+ const ticks = msg.data.map(
3294
+ (item) => rawTickToPublic(item, this.rawNamePairIdCache.get(`${item.from}-${item.to}`))
3295
+ );
3269
3296
  for (const h of this.snapshotHandlers) h(ticks);
3270
3297
  } else if (msg.type === "tick" && msg.data) {
3271
- const tick = rawTickToPublic(msg.data);
3298
+ const item = msg.data;
3299
+ const tick = rawTickToPublic(item, this.rawNamePairIdCache.get(`${item.from}-${item.to}`));
3272
3300
  for (const h of this.tickHandlers) h(tick);
3273
3301
  }
3274
3302
  });
@@ -3294,7 +3322,11 @@ var OstiumPriceStream = class _OstiumPriceStream {
3294
3322
  const ws = new WS(url, {
3295
3323
  headers: { "User-Agent": "ostium-sdk", "Accept": "*/*" }
3296
3324
  });
3297
- return new _OstiumPriceStream(ws, pairRawNameCache, initial);
3325
+ const rawNamePairIdCache = /* @__PURE__ */ new Map();
3326
+ for (const [pairId, rawName] of pairRawNameCache.entries()) {
3327
+ rawNamePairIdCache.set(rawName, pairId);
3328
+ }
3329
+ return new _OstiumPriceStream(ws, pairRawNameCache, rawNamePairIdCache, initial);
3298
3330
  }
3299
3331
  /**
3300
3332
  * Register a callback that fires on every incoming price tick.
@@ -3370,6 +3402,37 @@ var OstiumPriceStream = class _OstiumPriceStream {
3370
3402
  }
3371
3403
  };
3372
3404
 
3405
+ // src/data/internal/aggregations.ts
3406
+ function aggregateMarginSummary(pairPositions) {
3407
+ let accountValue = 0;
3408
+ let totalCollateralUsed = 0;
3409
+ let totalNtlPos = 0;
3410
+ let totalRawPnlUsd = 0;
3411
+ let totalCumRollover = 0;
3412
+ let totalWithdrawable = 0;
3413
+ for (const { position } of pairPositions) {
3414
+ const collateral = parseFloat(position.collateralUsed) || 0;
3415
+ const ntl = parseFloat(position.ntl) || 0;
3416
+ const pnl = parseFloat(position.unrealizedPnl) || 0;
3417
+ const rollover = parseFloat(position.cumRollover) || 0;
3418
+ const withdrawable = parseFloat(position.maxWithdrawable) || 0;
3419
+ accountValue += collateral + pnl;
3420
+ totalCollateralUsed += collateral;
3421
+ totalNtlPos += ntl;
3422
+ totalRawPnlUsd += pnl;
3423
+ totalCumRollover += rollover;
3424
+ totalWithdrawable += withdrawable;
3425
+ }
3426
+ return {
3427
+ accountValue: accountValue.toString(),
3428
+ totalCollateralUsed: totalCollateralUsed.toString(),
3429
+ totalNtlPos: totalNtlPos.toString(),
3430
+ totalRawPnlUsd: totalRawPnlUsd.toString(),
3431
+ totalCumRollover: totalCumRollover.toString(),
3432
+ totalWithdrawable: totalWithdrawable.toString()
3433
+ };
3434
+ }
3435
+
3373
3436
  // src/data/internal/formatters.ts
3374
3437
  var MIN_NOTIONAL = "5.0";
3375
3438
  var MIN_NOTIONAL_NUM = 5;
@@ -3471,14 +3534,14 @@ function formatPosition(raw, price, pnl, maxLeverage) {
3471
3534
  returnOnEquity: roe.toString(),
3472
3535
  liquidationPx: pnl.liquidationPrice.toString(),
3473
3536
  collateralUsed: collateral.toString(),
3474
- cumRollover: pnl.rollover.toString(),
3537
+ cumRollover: (pnl.rollover * -1).toString(),
3475
3538
  ...raw.takeProfitPrice && raw.takeProfitPrice !== "0" ? { tpPx: formatTokens(raw.takeProfitPrice).toString() } : {},
3476
3539
  ...raw.stopLossPrice && raw.stopLossPrice !== "0" ? { slPx: formatTokens(raw.stopLossPrice).toString() } : {},
3477
3540
  openTimestamp: parseInt(raw.timestamp || "0") * 1e3,
3478
3541
  isDayTrade: raw.isDayTrade ?? false,
3479
- maxLeverage: maxLeverage.toString()
3480
- },
3481
- maxWithdrawable: maxWithdrawForPosition(collateral, leverage, maxLeverage).toString()
3542
+ maxLeverage: maxLeverage.toString(),
3543
+ maxWithdrawable: maxWithdrawForPosition(collateral, leverage, maxLeverage).toString()
3544
+ }
3482
3545
  };
3483
3546
  }
3484
3547
  function formatFill(raw) {
@@ -3555,6 +3618,131 @@ function formatOpenOrder(raw) {
3555
3618
  timestamp: parseInt(raw.initiatedAt || "0") * 1e3
3556
3619
  };
3557
3620
  }
3621
+
3622
+ // src/data/internal/position-updates.ts
3623
+ function cloneResponse(response) {
3624
+ return {
3625
+ pairPositions: response.pairPositions.map((pairPos) => ({
3626
+ position: { ...pairPos.position }
3627
+ })),
3628
+ marginSummary: { ...response.marginSummary },
3629
+ time: response.time
3630
+ };
3631
+ }
3632
+ function applyTickToPosition(position, tick) {
3633
+ const size = parseFloat(position.szi) || 0;
3634
+ const entry = parseFloat(position.entryPx) || 0;
3635
+ const collateral = parseFloat(position.collateralUsed) || 0;
3636
+ const maxLeverage = parseFloat(position.maxLeverage) || 0;
3637
+ const rollover = parseFloat(position.cumRollover) || 0;
3638
+ const isLong = position.side === "B";
3639
+ const ntl = size * tick.mid;
3640
+ const rawPnl = isLong ? (tick.mid - entry) * size : (entry - tick.mid) * size;
3641
+ const netPnl = rawPnl + rollover;
3642
+ const roe = collateral > 0 ? netPnl / collateral : 0;
3643
+ const currentLeverage = collateral > 0 ? ntl / collateral : 0;
3644
+ const maxWithdrawable = maxWithdrawForPosition(collateral, currentLeverage, maxLeverage);
3645
+ return {
3646
+ ...position,
3647
+ ntl: ntl.toString(),
3648
+ unrealizedPnl: netPnl.toString(),
3649
+ returnOnEquity: roe.toString(),
3650
+ maxWithdrawable: maxWithdrawable.toString()
3651
+ };
3652
+ }
3653
+ function updateResponseWithTick(current, tick, timestampMs) {
3654
+ if (!tick.pairId) return current;
3655
+ let changed = false;
3656
+ const pairPositions = current.pairPositions.map((pairPos) => {
3657
+ if (String(pairPos.position.pairId) !== String(tick.pairId)) {
3658
+ return pairPos;
3659
+ }
3660
+ changed = true;
3661
+ return {
3662
+ ...pairPos,
3663
+ position: applyTickToPosition(pairPos.position, tick)
3664
+ };
3665
+ });
3666
+ if (!changed) return current;
3667
+ return {
3668
+ pairPositions,
3669
+ marginSummary: aggregateMarginSummary(pairPositions),
3670
+ time: timestampMs
3671
+ };
3672
+ }
3673
+ var OstiumPositionUpdatesStream = class {
3674
+ priceStream;
3675
+ updateHandlers = /* @__PURE__ */ new Set();
3676
+ current;
3677
+ trackedPairIds;
3678
+ constructor(initial, trackedPairIds, priceStream) {
3679
+ this.current = cloneResponse(initial);
3680
+ this.priceStream = priceStream;
3681
+ this.trackedPairIds = new Set(trackedPairIds);
3682
+ if (!priceStream) return;
3683
+ priceStream.onSnapshot((ticks) => {
3684
+ this.ingestSnapshot(ticks);
3685
+ });
3686
+ priceStream.onTick((tick) => {
3687
+ this.ingestTick(tick);
3688
+ });
3689
+ }
3690
+ onUpdate(handler) {
3691
+ this.updateHandlers.add(handler);
3692
+ return () => {
3693
+ this.updateHandlers.delete(handler);
3694
+ };
3695
+ }
3696
+ onOpen(handler) {
3697
+ this.priceStream?.onOpen(handler);
3698
+ return this;
3699
+ }
3700
+ onError(handler) {
3701
+ this.priceStream?.onError(handler);
3702
+ return this;
3703
+ }
3704
+ onClose(handler) {
3705
+ this.priceStream?.onClose(handler);
3706
+ return this;
3707
+ }
3708
+ getCurrent() {
3709
+ return cloneResponse(this.current);
3710
+ }
3711
+ /**
3712
+ * Apply a single externally sourced price tick to the tracked positions.
3713
+ *
3714
+ * Use this when your app already has its own websocket connection and you want
3715
+ * the SDK to handle only the position recalculation logic.
3716
+ */
3717
+ ingestTick(tick) {
3718
+ if (!tick.pairId || !this.trackedPairIds.has(String(tick.pairId))) return;
3719
+ const next = updateResponseWithTick(this.current, tick, tick.timestampSeconds * 1e3);
3720
+ if (next !== this.current) {
3721
+ this.current = next;
3722
+ this.emit(next);
3723
+ }
3724
+ }
3725
+ /**
3726
+ * Apply a batch of externally sourced ticks, such as a websocket snapshot.
3727
+ */
3728
+ ingestSnapshot(ticks) {
3729
+ let next = this.current;
3730
+ for (const tick of ticks) {
3731
+ if (!tick.pairId || !this.trackedPairIds.has(String(tick.pairId))) continue;
3732
+ next = updateResponseWithTick(next, tick, tick.timestampSeconds * 1e3);
3733
+ }
3734
+ if (next !== this.current) {
3735
+ this.current = next;
3736
+ this.emit(next);
3737
+ }
3738
+ }
3739
+ close() {
3740
+ this.priceStream?.close();
3741
+ }
3742
+ emit(positions) {
3743
+ for (const handler of this.updateHandlers) handler(cloneResponse(positions));
3744
+ }
3745
+ };
3558
3746
  var EMPTY_RESULT = { long: [], short: [] };
3559
3747
  var ORDERBOOK_MAX_LEVELS = 20;
3560
3748
  function generateLogSpacedNotionals(levels, capacity) {
@@ -3750,20 +3938,16 @@ var OstiumSubgraphClient = class _OstiumSubgraphClient {
3750
3938
  }),
3751
3939
  this.fetchLivePricesSafe()
3752
3940
  ]);
3753
- let totalWithdrawable = 0;
3754
3941
  const pairPositions = trades.map((trade) => {
3755
3942
  const price = prices[`${trade.pair.from}/${trade.pair.to}`];
3756
3943
  const pnl = price && blockNumber ? getTradePnL(trade, price, blockNumber) : EMPTY_PNL;
3757
3944
  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;
3945
+ return formatPosition(trade, price, pnl, maxLev);
3761
3946
  });
3762
3947
  const marginSummary = aggregateMarginSummary(pairPositions);
3763
3948
  return {
3764
3949
  pairPositions,
3765
3950
  marginSummary,
3766
- withdrawable: totalWithdrawable.toString(),
3767
3951
  time: Date.now()
3768
3952
  };
3769
3953
  }
@@ -4014,6 +4198,24 @@ var OstiumSubgraphClient = class _OstiumSubgraphClient {
4014
4198
  });
4015
4199
  return OstiumPriceStream.connect(this.builderApiUrl, rawNames, cache);
4016
4200
  }
4201
+ /**
4202
+ * Stream price-driven updates for an existing `getOpenPositions()` response.
4203
+ *
4204
+ * The SDK subscribes only to the unique pairs present in `initial.pairPositions`,
4205
+ * recalculates price-sensitive fields for affected positions on each tick, and
4206
+ * emits the full updated response.
4207
+ *
4208
+ * Pass an existing `priceStream` to reuse a websocket connection your app has
4209
+ * already opened. Omit it to let the SDK open and manage a dedicated
4210
+ * connection for the tracked pair ids.
4211
+ */
4212
+ streamPositionUpdates(initial, priceStream) {
4213
+ const pairIds = [...new Set(initial.pairPositions.map(({ position }) => String(position.pairId)))];
4214
+ if (pairIds.length === 0) {
4215
+ return new OstiumPositionUpdatesStream(initial, []);
4216
+ }
4217
+ return new OstiumPositionUpdatesStream(initial, pairIds, priceStream ?? this.streamPrices(pairIds));
4218
+ }
4017
4219
  // ─────────────────────────────────────────────────────────────────────────
4018
4220
  // Internal — fetchers, cache, query wrapper
4019
4221
  // ─────────────────────────────────────────────────────────────────────────
@@ -4072,43 +4274,28 @@ var OstiumSubgraphClient = class _OstiumSubgraphClient {
4072
4274
  }
4073
4275
  }
4074
4276
  };
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
4277
 
4097
4278
  // src/client.ts
4098
4279
  var HEX64_RE = /^0x[0-9a-fA-F]{64}$/;
4099
4280
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
4100
4281
  function validateConfig(config) {
4101
- if (!HEX64_RE.test(config.privateKey)) {
4282
+ if (config.privateKey && !HEX64_RE.test(config.privateKey)) {
4102
4283
  throw new OstiumError(
4103
4284
  "privateKey must be a 64-character hex string prefixed with 0x",
4104
4285
  "INVALID_CONFIG" /* INVALID_CONFIG */
4105
4286
  );
4106
4287
  }
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
- );
4288
+ for (const [label, value] of [
4289
+ ["traderAddress", config.traderAddress],
4290
+ ["delegateAddress", config.delegateAddress],
4291
+ ["safeAddress", config.safeAddress]
4292
+ ]) {
4293
+ if (value && !ADDRESS_RE.test(value)) {
4294
+ throw new OstiumError(
4295
+ `${label} must be a valid 42-character Ethereum address (0x + 40 hex chars)`,
4296
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4297
+ );
4298
+ }
4112
4299
  }
4113
4300
  if (config.slippageBps !== void 0 && (config.slippageBps < 0 || config.slippageBps > 500)) {
4114
4301
  throw new OstiumError(
@@ -4122,10 +4309,45 @@ function validateConfig(config) {
4122
4309
  "INVALID_CONFIG" /* INVALID_CONFIG */
4123
4310
  );
4124
4311
  }
4312
+ let mode;
4313
+ try {
4314
+ mode = resolveMode(config);
4315
+ } catch {
4316
+ throw new OstiumError(
4317
+ "Could not infer client mode from config. Provide the required addresses for the selected factory.",
4318
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4319
+ );
4320
+ }
4321
+ if (mode === "self-self" && !config.privateKey && !config.traderAddress) {
4322
+ throw new OstiumError(
4323
+ "Self + Self mode requires traderPrivateKey for submission or traderAddress for build-only usage.",
4324
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4325
+ );
4326
+ }
4327
+ if (mode === "self-gasless" && !config.privateKey && (!config.traderAddress || !config.safeAddress)) {
4328
+ throw new OstiumError(
4329
+ "Self + Gasless build-only mode requires traderAddress and safeAddress.",
4330
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4331
+ );
4332
+ }
4333
+ if (mode === "delegated-self" && (!config.traderAddress || !config.privateKey && !config.delegateAddress)) {
4334
+ throw new OstiumError(
4335
+ "Delegated + Self mode requires traderAddress and either delegatePrivateKey or delegateAddress.",
4336
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4337
+ );
4338
+ }
4339
+ if (mode === "delegated-gasless" && (!config.traderAddress || !config.privateKey && (!config.delegateAddress || !config.safeAddress))) {
4340
+ throw new OstiumError(
4341
+ "Delegated + Gasless mode requires traderAddress and either delegatePrivateKey or both delegateAddress and safeAddress.",
4342
+ "INVALID_CONFIG" /* INVALID_CONFIG */
4343
+ );
4344
+ }
4125
4345
  }
4126
4346
  var OstiumClient = class _OstiumClient {
4127
4347
  signer;
4128
4348
  submitter;
4349
+ traderAddress;
4350
+ effectiveSender;
4129
4351
  contracts;
4130
4352
  publicClient;
4131
4353
  defaultSlippageBps;
@@ -4140,6 +4362,8 @@ var OstiumClient = class _OstiumClient {
4140
4362
  constructor(internals) {
4141
4363
  this.signer = internals.signer;
4142
4364
  this.submitter = internals.submitter;
4365
+ this.traderAddress = internals.traderAddress;
4366
+ this.effectiveSender = internals.effectiveSender;
4143
4367
  this.contracts = internals.contracts;
4144
4368
  this.publicClient = internals.publicClient;
4145
4369
  this.defaultSlippageBps = internals.defaultSlippageBps;
@@ -4164,17 +4388,20 @@ var OstiumClient = class _OstiumClient {
4164
4388
  * });
4165
4389
  * await client.openTrade({ ... });
4166
4390
  * ```
4167
- */
4391
+ */
4168
4392
  static async createSelfAndSelf(params) {
4169
- return _OstiumClient._fromConfig({
4170
- privateKey: params.traderPrivateKey,
4171
- rpcUrl: params.rpcUrl,
4393
+ const base = {
4394
+ mode: "self-self",
4172
4395
  testnet: params.testnet,
4173
4396
  slippageBps: params.slippageBps,
4174
4397
  builder: params.builder,
4175
4398
  subgraphUrl: params.subgraphUrl,
4176
- builderApiUrl: params.builderApiUrl
4177
- });
4399
+ builderApiUrl: params.builderApiUrl,
4400
+ rpcUrl: params.rpcUrl
4401
+ };
4402
+ return _OstiumClient._fromConfig(
4403
+ "traderPrivateKey" in params ? { ...base, privateKey: params.traderPrivateKey } : { ...base, traderAddress: params.traderAddress }
4404
+ );
4178
4405
  }
4179
4406
  /**
4180
4407
  * **Self + Gasless** — your EOA owns everything; a Safe relays trades for free.
@@ -4198,17 +4425,27 @@ var OstiumClient = class _OstiumClient {
4198
4425
  */
4199
4426
  static async createSelfAndGasless(params) {
4200
4427
  const defaultUrl = params.testnet ? DEFAULT_PIMLICO_URL_TESTNET : DEFAULT_PIMLICO_URL;
4201
- return _OstiumClient._fromConfig({
4202
- privateKey: params.traderPrivateKey,
4203
- pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4428
+ const base = {
4429
+ mode: "self-gasless",
4204
4430
  rpcUrl: params.rpcUrl,
4205
- sponsorshipPolicyId: params.sponsorshipPolicyId,
4206
4431
  testnet: params.testnet,
4207
4432
  slippageBps: params.slippageBps,
4208
4433
  builder: params.builder,
4209
4434
  subgraphUrl: params.subgraphUrl,
4210
4435
  builderApiUrl: params.builderApiUrl
4211
- });
4436
+ };
4437
+ return _OstiumClient._fromConfig(
4438
+ "traderPrivateKey" in params ? {
4439
+ ...base,
4440
+ privateKey: params.traderPrivateKey,
4441
+ pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4442
+ sponsorshipPolicyId: params.sponsorshipPolicyId
4443
+ } : {
4444
+ ...base,
4445
+ traderAddress: params.traderAddress,
4446
+ safeAddress: params.safeAddress
4447
+ }
4448
+ );
4212
4449
  }
4213
4450
  /**
4214
4451
  * **Delegated + Self** — a delegate EOA signs and pays gas on behalf of a trader address.
@@ -4228,8 +4465,8 @@ var OstiumClient = class _OstiumClient {
4228
4465
  * ```
4229
4466
  */
4230
4467
  static async createDelegatedAndSelf(params) {
4231
- return _OstiumClient._fromConfig({
4232
- privateKey: params.delegatePrivateKey,
4468
+ const base = {
4469
+ mode: "delegated-self",
4233
4470
  traderAddress: params.traderAddress,
4234
4471
  rpcUrl: params.rpcUrl,
4235
4472
  testnet: params.testnet,
@@ -4237,7 +4474,10 @@ var OstiumClient = class _OstiumClient {
4237
4474
  builder: params.builder,
4238
4475
  subgraphUrl: params.subgraphUrl,
4239
4476
  builderApiUrl: params.builderApiUrl
4240
- });
4477
+ };
4478
+ return _OstiumClient._fromConfig(
4479
+ "delegatePrivateKey" in params ? { ...base, privateKey: params.delegatePrivateKey } : { ...base, delegateAddress: params.delegateAddress }
4480
+ );
4241
4481
  }
4242
4482
  /**
4243
4483
  * **Delegated + Gasless** — a Safe derived from the delegate key submits sponsored
@@ -4258,18 +4498,28 @@ var OstiumClient = class _OstiumClient {
4258
4498
  */
4259
4499
  static async createDelegatedAndGasless(params) {
4260
4500
  const defaultUrl = params.testnet ? DEFAULT_PIMLICO_URL_TESTNET : DEFAULT_PIMLICO_URL;
4261
- return _OstiumClient._fromConfig({
4262
- privateKey: params.delegatePrivateKey,
4501
+ const base = {
4502
+ mode: "delegated-gasless",
4263
4503
  traderAddress: params.traderAddress,
4264
- pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4265
4504
  rpcUrl: params.rpcUrl,
4266
- sponsorshipPolicyId: params.sponsorshipPolicyId,
4267
4505
  testnet: params.testnet,
4268
4506
  slippageBps: params.slippageBps,
4269
4507
  builder: params.builder,
4270
4508
  subgraphUrl: params.subgraphUrl,
4271
4509
  builderApiUrl: params.builderApiUrl
4272
- });
4510
+ };
4511
+ return _OstiumClient._fromConfig(
4512
+ "delegatePrivateKey" in params ? {
4513
+ ...base,
4514
+ privateKey: params.delegatePrivateKey,
4515
+ pimlicoUrl: params.pimlicoUrl ?? defaultUrl,
4516
+ sponsorshipPolicyId: params.sponsorshipPolicyId
4517
+ } : {
4518
+ ...base,
4519
+ delegateAddress: params.delegateAddress,
4520
+ safeAddress: params.safeAddress
4521
+ }
4522
+ );
4273
4523
  }
4274
4524
  /**
4275
4525
  * **Read-only client** — no signer, no submitter, no privateKey required.
@@ -4314,6 +4564,7 @@ var OstiumClient = class _OstiumClient {
4314
4564
  /** Internal factory shared by all public constructors. */
4315
4565
  static async _fromConfig(config) {
4316
4566
  validateConfig(config);
4567
+ const mode = resolveMode(config);
4317
4568
  const testnet = config.testnet ?? false;
4318
4569
  const networkConfig = getNetworkConfig(testnet);
4319
4570
  const chain = testnet ? arbitrumSepolia : arbitrum;
@@ -4323,18 +4574,29 @@ var OstiumClient = class _OstiumClient {
4323
4574
  tradingStorage: networkConfig.contracts.tradingStorage,
4324
4575
  usdc: networkConfig.contracts.usdc
4325
4576
  };
4326
- const selfGasless = isGasless(config) && !isDelegated(config);
4327
- const submitter = await createSubmitter(config, testnet);
4577
+ const selfGasless = mode === "self-gasless";
4328
4578
  let traderAddress;
4329
- if (isDelegated(config)) {
4579
+ if (mode === "self-self" || mode === "self-gasless") {
4580
+ traderAddress = config.traderAddress ?? privateKeyToAccount(config.privateKey).address;
4581
+ } else {
4330
4582
  traderAddress = config.traderAddress;
4583
+ }
4584
+ const signer = createSigner(mode, traderAddress);
4585
+ let submitter;
4586
+ let effectiveSender;
4587
+ if (hasPrivateKey(config)) {
4588
+ submitter = await createSubmitter(config, testnet, mode);
4589
+ effectiveSender = submitter.effectiveSender;
4590
+ } else if (mode === "self-self") {
4591
+ effectiveSender = traderAddress;
4592
+ } else if (mode === "delegated-self") {
4593
+ effectiveSender = config.delegateAddress;
4331
4594
  } else {
4332
- traderAddress = privateKeyToAccount(config.privateKey).address;
4595
+ effectiveSender = config.safeAddress;
4333
4596
  }
4334
- const signer = createSigner(config, traderAddress, selfGasless);
4335
4597
  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) {
4598
+ const rpcUrl = config.rpcUrl ?? publicRpc;
4599
+ if (hasPrivateKey(config) && mode !== "self-gasless" && mode !== "delegated-gasless" && !config.rpcUrl) {
4338
4600
  throw new OstiumError(
4339
4601
  "rpcUrl is required for self-submission mode",
4340
4602
  "INVALID_CONFIG" /* INVALID_CONFIG */
@@ -4342,7 +4604,7 @@ var OstiumClient = class _OstiumClient {
4342
4604
  }
4343
4605
  const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
4344
4606
  let eoaSubmit;
4345
- if (selfGasless) {
4607
+ if (selfGasless && hasPrivateKey(config)) {
4346
4608
  const eoaAccount = privateKeyToAccount(config.privateKey);
4347
4609
  const eoaRpcUrl = config.rpcUrl ?? publicRpc;
4348
4610
  const eoaWalletClient = createWalletClient({ account: eoaAccount, chain, transport: http(eoaRpcUrl) });
@@ -4364,11 +4626,13 @@ var OstiumClient = class _OstiumClient {
4364
4626
  return new _OstiumClient({
4365
4627
  signer,
4366
4628
  submitter,
4629
+ traderAddress,
4630
+ effectiveSender,
4367
4631
  contracts,
4368
4632
  publicClient,
4369
4633
  defaultSlippageBps,
4370
- delegated: isDelegated(config),
4371
- gasless: isGasless(config),
4634
+ delegated: mode === "delegated-self" || mode === "delegated-gasless",
4635
+ gasless: mode === "self-gasless" || mode === "delegated-gasless",
4372
4636
  selfGasless,
4373
4637
  eoaSubmit,
4374
4638
  builderAddress: config.builder?.address,
@@ -4386,17 +4650,25 @@ var OstiumClient = class _OstiumClient {
4386
4650
  * - Self + Gasless: EOA derived from privateKey (NOT the Safe — USDC lives here).
4387
4651
  */
4388
4652
  getTraderAddress() {
4389
- if (!this.signer) {
4653
+ if (!this.traderAddress) {
4390
4654
  throw new OstiumError(
4391
4655
  "No connected trader \u2014 pass `user` explicitly or use one of the createSelfAnd*/createDelegatedAnd* factories.",
4392
4656
  "INVALID_CONFIG" /* INVALID_CONFIG */
4393
4657
  );
4394
4658
  }
4395
- return this.signer.traderAddress;
4659
+ return this.traderAddress;
4396
4660
  }
4397
- /** True when the client was created via `createReadOnly` and cannot submit transactions. */
4661
+ /** True when the client cannot submit transactions directly. */
4398
4662
  isReadOnly() {
4399
- return !this.signer || !this.submitter;
4663
+ return !this.submitter;
4664
+ }
4665
+ /** True when the client can build mode-correct unsigned transaction requests. */
4666
+ canBuildTransactions() {
4667
+ return !!this.signer && !!this.effectiveSender;
4668
+ }
4669
+ /** True when the client has the credentials needed for SDK-managed submission. */
4670
+ canSubmitTransactions() {
4671
+ return !!this.submitter;
4400
4672
  }
4401
4673
  /**
4402
4674
  * Returns the Safe smart-account address used for gasless submission.
@@ -4408,7 +4680,7 @@ var OstiumClient = class _OstiumClient {
4408
4680
  * - Self + Self / Delegated + Self: returns undefined.
4409
4681
  */
4410
4682
  getSmartAccountAddress() {
4411
- return this.gasless && this.submitter ? this.submitter.effectiveSender : void 0;
4683
+ return this.gasless ? this.effectiveSender : void 0;
4412
4684
  }
4413
4685
  // ─────────────────────────────────────────────────────────────────────────
4414
4686
  // Self + Gasless setup
@@ -4432,15 +4704,10 @@ var OstiumClient = class _OstiumClient {
4432
4704
  * ```
4433
4705
  */
4434
4706
  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);
4707
+ return this.submitDirectEoa(this.getSetupGaslessDelegationEncoded());
4708
+ }
4709
+ getSetupGaslessDelegationTx() {
4710
+ return this.buildDirectEoaTx(this.getSetupGaslessDelegationEncoded());
4444
4711
  }
4445
4712
  // ─────────────────────────────────────────────────────────────────────────
4446
4713
  // USDC helpers
@@ -4502,18 +4769,14 @@ var OstiumClient = class _OstiumClient {
4502
4769
  * @param amount USD amount as decimal string (e.g. "1000"), or "max" for MaxUint256.
4503
4770
  */
4504
4771
  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);
4772
+ const encoded = this.getApproveUsdcEncoded(amount);
4513
4773
  if (this.selfGasless) {
4514
- return this.submitDirectFromEoa(encoded);
4774
+ return this.submitDirectEoa(encoded);
4515
4775
  }
4516
- return this.requireWriter().submitter.submit(encoded);
4776
+ return this.submitPrepared(encoded);
4777
+ }
4778
+ getApproveUsdcTx(amount) {
4779
+ return this.buildDirectEoaTx(this.getApproveUsdcEncoded(amount));
4517
4780
  }
4518
4781
  // ─────────────────────────────────────────────────────────────────────────
4519
4782
  // Delegation helpers
@@ -4527,16 +4790,20 @@ var OstiumClient = class _OstiumClient {
4527
4790
  * update the trader's delegation on their behalf.
4528
4791
  */
4529
4792
  async setDelegate(delegateAddress) {
4530
- const encoded = encodeSetDelegate(delegateAddress, this.contracts.trading);
4531
- return this.execute(encoded);
4793
+ return this.submitPrepared(this.getSetDelegateEncoded(delegateAddress));
4794
+ }
4795
+ getSetDelegateTx(delegateAddress) {
4796
+ return this.buildPreparedTx(this.getSetDelegateEncoded(delegateAddress));
4532
4797
  }
4533
4798
  /**
4534
4799
  * Remove the current delegate on the trading contract.
4535
4800
  * Follows the same delegation-wrapping rules as setDelegate().
4536
4801
  */
4537
4802
  async removeDelegate() {
4538
- const encoded = encodeRemoveDelegate(this.contracts.trading);
4539
- return this.execute(encoded);
4803
+ return this.submitPrepared(this.getRemoveDelegateEncoded());
4804
+ }
4805
+ getRemoveDelegateTx() {
4806
+ return this.buildPreparedTx(this.getRemoveDelegateEncoded());
4540
4807
  }
4541
4808
  // ─────────────────────────────────────────────────────────────────────────
4542
4809
  // Core trading methods
@@ -4549,6 +4816,12 @@ var OstiumClient = class _OstiumClient {
4549
4816
  * the current allowance.
4550
4817
  */
4551
4818
  async openTrade(params) {
4819
+ return this.submitPrepared(this.getOpenTradeEncoded(params));
4820
+ }
4821
+ getOpenTradeTx(params) {
4822
+ return this.buildPreparedTx(this.getOpenTradeEncoded(params));
4823
+ }
4824
+ getOpenTradeEncoded(params) {
4552
4825
  const collateralNum = parseFloat(params.collateral);
4553
4826
  if (collateralNum > MAX_COLLATERAL_USD) {
4554
4827
  throw new OstiumError(
@@ -4557,7 +4830,7 @@ var OstiumClient = class _OstiumClient {
4557
4830
  );
4558
4831
  }
4559
4832
  const apiOpen = {
4560
- a: params.pairIndex,
4833
+ a: Number(params.pairId),
4561
4834
  b: params.buy,
4562
4835
  p: params.price,
4563
4836
  s: params.collateral,
@@ -4580,8 +4853,7 @@ var OstiumClient = class _OstiumClient {
4580
4853
  }
4581
4854
  const builderFee = buildBuilderFee(apiBuilder);
4582
4855
  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);
4856
+ return encodeOpenTrade(trade, builderFee, orderType, slippageP, this.contracts.trading);
4585
4857
  }
4586
4858
  /**
4587
4859
  * Close an open position (full or partial).
@@ -4596,6 +4868,12 @@ var OstiumClient = class _OstiumClient {
4596
4868
  * ```
4597
4869
  */
4598
4870
  async closeTrade(params) {
4871
+ return this.submitPrepared(this.getCloseTradeEncoded(params));
4872
+ }
4873
+ getCloseTradeTx(params) {
4874
+ return this.buildPreparedTx(this.getCloseTradeEncoded(params));
4875
+ }
4876
+ getCloseTradeEncoded(params) {
4599
4877
  try {
4600
4878
  validateCloseParams({
4601
4879
  a: Number(params.pairId),
@@ -4615,7 +4893,7 @@ var OstiumClient = class _OstiumClient {
4615
4893
  const closePercentage = BigInt(Math.round(params.closePercent * 100));
4616
4894
  const marketPrice = parsePrice(params.price);
4617
4895
  const slippageP = BigInt(this.resolveSlippage(params.slippage));
4618
- const encoded = encodeCloseTradeMarket(
4896
+ return encodeCloseTradeMarket(
4619
4897
  pairIndex,
4620
4898
  index,
4621
4899
  closePercentage,
@@ -4623,7 +4901,6 @@ var OstiumClient = class _OstiumClient {
4623
4901
  slippageP,
4624
4902
  this.contracts.trading
4625
4903
  );
4626
- return this.execute(encoded);
4627
4904
  }
4628
4905
  /**
4629
4906
  * Cancel a pending order.
@@ -4646,6 +4923,12 @@ var OstiumClient = class _OstiumClient {
4646
4923
  * ```
4647
4924
  */
4648
4925
  async cancelOrder(params) {
4926
+ return this.submitPrepared(this.getCancelOrderEncoded(params));
4927
+ }
4928
+ getCancelOrderTx(params) {
4929
+ return this.buildPreparedTx(this.getCancelOrderEncoded(params));
4930
+ }
4931
+ getCancelOrderEncoded(params) {
4649
4932
  let encoded;
4650
4933
  if (params.type === "limit" /* Limit */) {
4651
4934
  const pairIdNum = Number(params.pairId);
@@ -4682,7 +4965,7 @@ var OstiumClient = class _OstiumClient {
4682
4965
  }
4683
4966
  encoded = encodeOpenTradeMarketTimeout(BigInt(params.orderId), this.contracts.trading);
4684
4967
  }
4685
- return this.execute(encoded);
4968
+ return encoded;
4686
4969
  }
4687
4970
  /**
4688
4971
  * Modify an open trade or a pending limit order.
@@ -4707,6 +4990,12 @@ var OstiumClient = class _OstiumClient {
4707
4990
  * - Both `takeProfit` and `stopLoss` without `price` → throws (send two calls).
4708
4991
  */
4709
4992
  async modifyOrder(params) {
4993
+ return this.submitPrepared(this.getModifyOrderEncoded(params));
4994
+ }
4995
+ getModifyOrderTx(params) {
4996
+ return this.buildPreparedTx(this.getModifyOrderEncoded(params));
4997
+ }
4998
+ getModifyOrderEncoded(params) {
4710
4999
  try {
4711
5000
  validateModifyParams({
4712
5001
  a: Number(params.pairId),
@@ -4746,7 +5035,7 @@ var OstiumClient = class _OstiumClient {
4746
5035
  "VALIDATION_FAILED" /* VALIDATION_FAILED */
4747
5036
  );
4748
5037
  }
4749
- return this.execute(encoded);
5038
+ return encoded;
4750
5039
  }
4751
5040
  /**
4752
5041
  * Update collateral on an open position (isolated margin).
@@ -4766,6 +5055,12 @@ var OstiumClient = class _OstiumClient {
4766
5055
  * Checks USDC allowance before top-up operations.
4767
5056
  */
4768
5057
  async updateCollateral(params) {
5058
+ return this.submitPrepared(await this.getUpdateCollateralEncoded(params));
5059
+ }
5060
+ async getUpdateCollateralTx(params) {
5061
+ return this.buildPreparedTx(await this.getUpdateCollateralEncoded(params));
5062
+ }
5063
+ async getUpdateCollateralEncoded(params) {
4769
5064
  const amount = parseFloat(params.amount);
4770
5065
  if (Number.isNaN(amount) || amount === 0) {
4771
5066
  throw new OstiumError(
@@ -4789,7 +5084,7 @@ var OstiumClient = class _OstiumClient {
4789
5084
  } else {
4790
5085
  encoded = encodeRemoveCollateral(pairIndex, index, absAmount, this.contracts.trading);
4791
5086
  }
4792
- return this.execute(encoded);
5087
+ return encoded;
4793
5088
  }
4794
5089
  // ─────────────────────────────────────────────────────────────────────────
4795
5090
  // Subgraph reads (Hyperliquid-style API)
@@ -4917,31 +5212,104 @@ var OstiumClient = class _OstiumClient {
4917
5212
  streamPrices(pairIds) {
4918
5213
  return this.subgraph.streamPrices(pairIds);
4919
5214
  }
5215
+ /**
5216
+ * Stream price-driven updates for an existing `getOpenPositions()` response.
5217
+ *
5218
+ * Subscribes only to the unique pairs referenced by the response and emits the
5219
+ * full updated positions payload on each relevant price update. Pass an
5220
+ * existing `priceStream` to reuse a websocket your app already owns, or omit
5221
+ * it to let the SDK create a dedicated connection.
5222
+ */
5223
+ streamPositionUpdates(initial, priceStream) {
5224
+ return this.subgraph.streamPositionUpdates(initial, priceStream);
5225
+ }
4920
5226
  // ─────────────────────────────────────────────────────────────────────────
4921
5227
  // Internal helpers
4922
5228
  // ─────────────────────────────────────────────────────────────────────────
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) {
5229
+ getSetupGaslessDelegationEncoded() {
5230
+ if (!this.selfGasless || !this.effectiveSender) {
4936
5231
  throw new OstiumError(
4937
- "This client is read-only. Use one of the createSelfAnd*/createDelegatedAnd* factories to enable writes.",
5232
+ "setupGaslessDelegation() is only available in Self + Gasless mode.",
4938
5233
  "INVALID_CONFIG" /* INVALID_CONFIG */
4939
5234
  );
4940
5235
  }
4941
- return { signer: this.signer, submitter: this.submitter };
5236
+ return encodeSetDelegate(this.effectiveSender, this.contracts.trading);
5237
+ }
5238
+ getApproveUsdcEncoded(amount) {
5239
+ if (this.delegated) {
5240
+ throw new OstiumError(
5241
+ "approveUsdc is not available in delegated mode. The trader must approve USDC directly from their own account.",
5242
+ "DELEGATION_FAILED" /* DELEGATION_FAILED */
5243
+ );
5244
+ }
5245
+ const rawAmount = amount === "max" ? maxUint256 : parseUsdc(amount);
5246
+ return encodeUsdcApprove(this.contracts.tradingStorage, rawAmount, this.contracts.usdc);
5247
+ }
5248
+ getSetDelegateEncoded(delegateAddress) {
5249
+ return encodeSetDelegate(delegateAddress, this.contracts.trading);
5250
+ }
5251
+ getRemoveDelegateEncoded() {
5252
+ return encodeRemoveDelegate(this.contracts.trading);
5253
+ }
5254
+ prepareEncoded(encoded) {
5255
+ const signer = this.requireBuildCapability();
5256
+ return signer.prepare(encoded);
5257
+ }
5258
+ buildPreparedTx(encoded) {
5259
+ return this.toBuiltTxRequest(this.prepareEncoded(encoded));
5260
+ }
5261
+ buildDirectEoaTx(encoded) {
5262
+ if (!this.canBuildTransactions() || !this.traderAddress) {
5263
+ throw new OstiumError(
5264
+ "This client cannot build transactions. Use one of the createSelfAnd*/createDelegatedAnd* factories.",
5265
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5266
+ );
5267
+ }
5268
+ return {
5269
+ kind: "eoa",
5270
+ to: encoded.to,
5271
+ data: encoded.data,
5272
+ value: encoded.value,
5273
+ from: this.traderAddress,
5274
+ traderAddress: this.traderAddress
5275
+ };
5276
+ }
5277
+ toBuiltTxRequest(encoded) {
5278
+ if (!this.canBuildTransactions() || !this.traderAddress || !this.effectiveSender) {
5279
+ throw new OstiumError(
5280
+ "This client cannot build transactions. Use one of the createSelfAnd*/createDelegatedAnd* factories.",
5281
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5282
+ );
5283
+ }
5284
+ if (this.gasless) {
5285
+ return {
5286
+ kind: "safe",
5287
+ safeAddress: this.effectiveSender,
5288
+ traderAddress: this.traderAddress,
5289
+ calls: [encoded]
5290
+ };
5291
+ }
5292
+ return {
5293
+ kind: "eoa",
5294
+ to: encoded.to,
5295
+ data: encoded.data,
5296
+ value: encoded.value,
5297
+ from: this.effectiveSender,
5298
+ traderAddress: this.traderAddress
5299
+ };
5300
+ }
5301
+ async submitPrepared(encoded) {
5302
+ const submitter = this.requireSubmitCapability();
5303
+ return submitter.submit(this.prepareEncoded(encoded));
4942
5304
  }
4943
5305
  /** Direct EOA submission — used only in Self + Gasless for approveUsdc and setupGaslessDelegation. */
4944
- async submitDirectFromEoa(encoded) {
5306
+ async submitDirectEoa(encoded) {
5307
+ if (!this.eoaSubmit) {
5308
+ throw new OstiumError(
5309
+ "This client cannot submit direct EOA transactions.",
5310
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5311
+ );
5312
+ }
4945
5313
  try {
4946
5314
  return await this.eoaSubmit(encoded);
4947
5315
  } catch (err) {
@@ -4957,6 +5325,24 @@ var OstiumClient = class _OstiumClient {
4957
5325
  throw new OstiumError(`Transaction failed: ${msg}`, "SUBMISSION_FAILED" /* SUBMISSION_FAILED */, err);
4958
5326
  }
4959
5327
  }
5328
+ requireBuildCapability() {
5329
+ if (!this.signer) {
5330
+ throw new OstiumError(
5331
+ "This client cannot build transactions. Use one of the createSelfAnd*/createDelegatedAnd* factories.",
5332
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5333
+ );
5334
+ }
5335
+ return this.signer;
5336
+ }
5337
+ requireSubmitCapability() {
5338
+ if (!this.submitter) {
5339
+ throw new OstiumError(
5340
+ "This client does not have submission credentials. Use the corresponding get*Tx() method or initialize with a private key.",
5341
+ "INVALID_CONFIG" /* INVALID_CONFIG */
5342
+ );
5343
+ }
5344
+ return this.submitter;
5345
+ }
4960
5346
  resolveSlippage(override) {
4961
5347
  return override ?? this.defaultSlippageBps;
4962
5348
  }
@@ -5839,7 +6225,7 @@ async function actionOpenTrade(client2) {
5839
6225
  const result = await spin(
5840
6226
  "Submitting openTrade\u2026",
5841
6227
  () => client2.openTrade({
5842
- pairIndex: pairId,
6228
+ pairId,
5843
6229
  buy: buy === "true",
5844
6230
  price,
5845
6231
  collateral,
@@ -5929,7 +6315,7 @@ async function actionCloseTrade(client2) {
5929
6315
  }
5930
6316
  async function actionCancelOrder(client2) {
5931
6317
  let cancelType = "limit" /* Limit */;
5932
- let pairIndex = "0";
6318
+ let pairIdInput = "0";
5933
6319
  let orderIndex = "0";
5934
6320
  let orderId = "";
5935
6321
  let retry = false;
@@ -5948,12 +6334,12 @@ async function actionCancelOrder(client2) {
5948
6334
  }
5949
6335
  case 1: {
5950
6336
  if (cancelType === "limit" /* Limit */) {
5951
- const v = await wtext({ message: "Pair index", initialValue: pairIndex });
6337
+ const v = await wtext({ message: "Pair ID", initialValue: pairIdInput });
5952
6338
  if (v === BACK) {
5953
6339
  step--;
5954
6340
  break;
5955
6341
  }
5956
- pairIndex = v;
6342
+ pairIdInput = v;
5957
6343
  step++;
5958
6344
  break;
5959
6345
  } else {
@@ -6000,7 +6386,7 @@ async function actionCancelOrder(client2) {
6000
6386
  }
6001
6387
  }
6002
6388
  }
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) };
6389
+ 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
6390
  const result = await spin(
6005
6391
  "Submitting cancelOrder\u2026",
6006
6392
  () => client2.cancelOrder(cancelParams)
@@ -6010,7 +6396,7 @@ async function actionCancelOrder(client2) {
6010
6396
  async function actionModifyOrder(client2) {
6011
6397
  const fields = await p.group(
6012
6398
  {
6013
- pairIndex: () => p.text({ message: "Pair index", initialValue: "0" }),
6399
+ pairIndex: () => p.text({ message: "Pair ID", initialValue: "0" }),
6014
6400
  index: () => p.text({ message: "Trade / order index", initialValue: "0" }),
6015
6401
  price: () => p.text({ message: "New limit price (optional)", placeholder: "leave blank to skip" }),
6016
6402
  tp: () => p.text({ message: "New take profit (optional)", placeholder: "leave blank to skip" }),
@@ -6033,7 +6419,7 @@ async function actionModifyOrder(client2) {
6033
6419
  async function actionUpdateCollateral(client2) {
6034
6420
  const fields = await p.group(
6035
6421
  {
6036
- pairIndex: () => p.text({ message: "Pair index", initialValue: "0" }),
6422
+ pairIndex: () => p.text({ message: "Pair ID", initialValue: "0" }),
6037
6423
  index: () => p.text({ message: "Trade index", initialValue: "0" }),
6038
6424
  amount: () => p.text({
6039
6425
  message: "Amount (positive = add, negative = remove)",
@@ -6172,7 +6558,7 @@ async function actionViewPrices(client2) {
6172
6558
  }
6173
6559
  }
6174
6560
  async function actionViewPositions(client2) {
6175
- const { pairPositions, marginSummary, withdrawable } = await spin(
6561
+ const { pairPositions, marginSummary } = await spin(
6176
6562
  "Fetching open positions\u2026",
6177
6563
  () => client2.getOpenPositions()
6178
6564
  );
@@ -6209,7 +6595,8 @@ async function actionViewPositions(client2) {
6209
6595
  p.log.info(`Total collateral: $${parseFloat(marginSummary.totalCollateralUsed).toFixed(4)}`);
6210
6596
  p.log.info(`Total notional: $${parseFloat(marginSummary.totalNtlPos).toFixed(4)}`);
6211
6597
  p.log.info(`Unrealized PnL: $${parseFloat(marginSummary.totalRawPnlUsd).toFixed(4)}`);
6212
- p.log.info(`Max withdrawable: $${parseFloat(withdrawable).toFixed(4)}`);
6598
+ p.log.info(`Cum rollover: $${parseFloat(marginSummary.totalCumRollover).toFixed(4)}`);
6599
+ p.log.info(`Max withdrawable: $${parseFloat(marginSummary.totalWithdrawable).toFixed(4)}`);
6213
6600
  }
6214
6601
  async function actionViewFills(client2) {
6215
6602
  const limitRaw = await p.text({