@depthhq/maker-sdk 0.1.11 → 0.1.13

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/bot/bot.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare class MakerBot {
15
15
  readonly hedge?: HedgeEngine | undefined;
16
16
  private state?;
17
17
  private position?;
18
+ private verifiedPosition?;
18
19
  private dirty;
19
20
  private busy;
20
21
  private stopping;
@@ -24,6 +25,7 @@ export declare class MakerBot {
24
25
  private lastCleanup;
25
26
  private lastFills;
26
27
  private inventoryVersion?;
28
+ private reservationView?;
27
29
  private constructor();
28
30
  static create(options: {
29
31
  maker: MakerClient;
@@ -41,6 +43,9 @@ export declare class MakerBot {
41
43
  private cancel;
42
44
  private replace;
43
45
  tick(): Promise<BotSnapshot>;
46
+ private renew;
47
+ private quotePolicy;
48
+ private quoteRisk;
44
49
  stop(): void;
45
50
  close(): Promise<void>;
46
51
  }
@@ -36,6 +36,10 @@ export type BotMarket = MarketState & {
36
36
  fundingIntervalSeconds: number;
37
37
  };
38
38
  };
39
+ export declare function executionCosts(policy: BotPolicy, pricing?: Pick<BotPricing, "takerFeeBps" | "hourlyFundingBps">): {
40
+ bid: number;
41
+ ask: number;
42
+ };
39
43
  export declare function validateBotPolicy(policy: BotPolicy): void;
40
44
  export declare function quoteHealth(market: BotMarket, now: number): string | undefined;
41
- export declare function botQuote(policy: BotPolicy, market: BotMarket, position: PositionSnapshot, nonce: bigint, now: number, pricing?: BotPricing): QuoteInput | undefined;
45
+ export declare function botQuote(policy: BotPolicy, market: BotMarket, position: PositionSnapshot, nonce: bigint, now: number, pricing?: BotPricing, reservationCredit?: bigint): QuoteInput | undefined;
@@ -1,5 +1,14 @@
1
1
  import type { QuoteInput } from "../client/types.js";
2
2
  import type { HedgeState } from "../hedge/types.js";
3
+ export type QuoteRisk = {
4
+ quote: string;
5
+ notional: bigint;
6
+ price: bigint;
7
+ version: bigint;
8
+ balanceNonce?: string;
9
+ externalPosition: bigint;
10
+ hedgeNonce: number;
11
+ };
3
12
  export type BotState = {
4
13
  genesis: string;
5
14
  maker: string;
@@ -13,11 +22,14 @@ export type BotState = {
13
22
  version: bigint;
14
23
  release: boolean;
15
24
  };
25
+ inventoryPendingSince?: number;
26
+ quoteRisk?: QuoteRisk;
16
27
  replacement?: {
17
28
  prior: string;
18
29
  nonce: bigint;
19
30
  request?: QuoteInput;
20
31
  referencePrice?: bigint;
32
+ risk?: QuoteRisk;
21
33
  };
22
34
  fills: Record<string, {
23
35
  signature: string;
@@ -16,6 +16,7 @@ import type { GrantState, BackstopInput, MakerOptions, ResolvedMakerOptions, Quo
16
16
  export declare function retryQuote<T>(action: () => Promise<T>, deadline: number): Promise<T>;
17
17
  export declare class MakerClient {
18
18
  readonly options: ResolvedMakerOptions;
19
+ private marketView?;
19
20
  readonly hedges: MakerAccess;
20
21
  readonly automation: MakerAutomation;
21
22
  readonly provider: anchor.AnchorProvider;
@@ -49,6 +49,7 @@ export type QuoteState = {
49
49
  expiry: anchor.BN;
50
50
  attachedOrders: number;
51
51
  pendingComputation?: PublicKey;
52
+ balanceNonce: number[];
52
53
  marketRevision: anchor.BN;
53
54
  oracleTimestamp: anchor.BN;
54
55
  oracleReferenceE8: anchor.BN;
package/dist/index.js CHANGED
@@ -26396,7 +26396,8 @@ import {
26396
26396
  quoteQueueAddress,
26397
26397
  protocolAddress,
26398
26398
  SOL_USD_MARKET_ID as SOL_USD_MARKET_ID2,
26399
- quoteBounds as quoteBounds3
26399
+ quoteBounds as quoteBounds4,
26400
+ quoteSizes as quoteSizes2
26400
26401
  } from "@depthhq/bindings";
26401
26402
  import { DepthView } from "@depthhq/sdk/views";
26402
26403
  import { DepthRecovery } from "@depthhq/sdk/recovery";
@@ -26603,7 +26604,7 @@ import { PublicKey as PublicKey2 } from "@solana/web3.js";
26603
26604
 
26604
26605
  // src/bot/policy.ts
26605
26606
  import { positionPnl } from "@depthhq/sdk";
26606
- import { quoteBounds } from "@depthhq/bindings";
26607
+ import { quoteBounds, quoteSizes } from "@depthhq/bindings";
26607
26608
 
26608
26609
  // src/hedge/policy.ts
26609
26610
  function validateHedgePolicy(policy) {
@@ -27298,6 +27299,7 @@ class QuoteHandle {
27298
27299
  client;
27299
27300
  reserveSignature;
27300
27301
  releaseSignature;
27302
+ priorId;
27301
27303
  balanceView;
27302
27304
  constructor(id, prepareSignature, client) {
27303
27305
  this.id = id;
@@ -27311,9 +27313,36 @@ class QuoteHandle {
27311
27313
  status: name(state.status),
27312
27314
  maxCapacity: BigInt(state.capacity.toString()),
27313
27315
  attachedOrders: state.attachedOrders,
27316
+ expiresAt: state.expiry.toNumber(),
27314
27317
  ...state.replacementNonce == null ? {} : { replacementNonce: BigInt(state.replacementNonce.toString()) }
27315
27318
  };
27316
27319
  }
27320
+ async renew(input) {
27321
+ const market = await this.client.read("Market", this.client.marketAddress);
27322
+ if (!Number.isSafeInteger(input.validForSeconds) || input.validForSeconds < 1 || input.validForSeconds > market.config.maxQuoteLifetimeSeconds)
27323
+ throw new RangeError("Quote duration is outside market policy");
27324
+ const expiry = Math.floor(Date.now() / 1000) + input.validForSeconds;
27325
+ const before = await this.status();
27326
+ if (before.status !== "active")
27327
+ throw new Error("Only active quotes can be renewed");
27328
+ if (before.expiresAt >= expiry)
27329
+ return before;
27330
+ const actor = this.client.options.wallet.publicKey;
27331
+ try {
27332
+ await this.client.program.methods.renewQuote(new anchor4.BN(expiry)).accountsPartial({
27333
+ actor,
27334
+ market: this.client.marketAddress,
27335
+ quote: this.id,
27336
+ grant: actor.equals(this.client.options.maker) ? this.client.program.programId : this.client.grantAddress()
27337
+ }).rpc({ commitment: "confirmed" });
27338
+ } catch (error) {
27339
+ const receipt = await this.status();
27340
+ if (receipt.status !== "active" || receipt.expiresAt < expiry)
27341
+ throw error;
27342
+ return receipt;
27343
+ }
27344
+ return this.status();
27345
+ }
27317
27346
  async capacity() {
27318
27347
  for (let attempt = 0;attempt < 3; attempt++) {
27319
27348
  const view = this.balanceView ?? await this.client.requestQuoteView(this.id);
@@ -27328,6 +27357,17 @@ class QuoteHandle {
27328
27357
  }
27329
27358
  throw new Error("Quote balance changed while the private view was running");
27330
27359
  }
27360
+ async reservation() {
27361
+ const view = this.balanceView ?? await this.client.requestQuoteView(this.id);
27362
+ this.balanceView = view;
27363
+ const snapshot = await view.wait({ timeoutMs: 0 });
27364
+ if (snapshot.status === "pending")
27365
+ return;
27366
+ await view.deferClose();
27367
+ this.balanceView = undefined;
27368
+ if (snapshot.status === "ready" && "remainingMargin" in snapshot)
27369
+ return snapshot.remainingMargin;
27370
+ }
27331
27371
  async confirm(options = {}) {
27332
27372
  const timeoutMs = options.timeoutMs ?? 60000;
27333
27373
  const started = Date.now();
@@ -27376,8 +27416,22 @@ class QuoteHandle {
27376
27416
  return snapshot;
27377
27417
  }
27378
27418
  if (snapshot.status === "validated" || snapshot.status === "waiting") {
27419
+ if (this.priorId && snapshot.status === "validated") {
27420
+ const prior = await this.client.options.connection.getAccountInfo(this.priorId, "confirmed");
27421
+ if (prior) {
27422
+ const previous = await this.client.quote.get(this.priorId).status();
27423
+ if (previous.status === "active") {
27424
+ const quote2 = await this.client.quoteState(this.id);
27425
+ return this.client.quote.replacement(this.priorId, BigInt(quote2.nonce.toString())).rotate(this);
27426
+ }
27427
+ }
27428
+ }
27379
27429
  const market = await this.client.read("Market", this.client.marketAddress);
27380
27430
  const quote = await this.client.quoteState(this.id);
27431
+ const now = Math.floor(Date.now() / 1000);
27432
+ const age = now - market.oracleTimestamp.toNumber();
27433
+ if (age > market.config.maxOracleAgeSeconds - Math.min(Math.floor(market.config.maxOracleAgeSeconds / 2), 60))
27434
+ return snapshot;
27381
27435
  const reference = BigInt(quote.oracleReferenceE8.toString());
27382
27436
  const price = BigInt(market.oraclePriceE8.toString());
27383
27437
  const drift = price > reference ? price - reference : reference - price;
@@ -27447,6 +27501,15 @@ class QuoteHandle {
27447
27501
  return snapshot;
27448
27502
  const actor = this.client.options.wallet.publicKey;
27449
27503
  const grant = actor.equals(this.client.options.maker) ? undefined : this.client.grantAddress();
27504
+ if (snapshot.status === "reserving") {
27505
+ await this.cancelStep(() => this.client.program.methods.cancelQuoteReserve().accountsPartial({
27506
+ actor,
27507
+ grant: grant ?? this.client.program.programId,
27508
+ quote: this.id,
27509
+ marketAccount: this.client.marketAccount()
27510
+ }).rpc({ commitment: "confirmed" }), ["validated", "active", "cancelled"]);
27511
+ return this.cancel();
27512
+ }
27450
27513
  if (snapshot.status === "validating" || snapshot.status === "validated") {
27451
27514
  return this.cancelStep(() => this.client.program.methods.cancelPreparedQuote().accountsPartial({
27452
27515
  actor,
@@ -27520,6 +27583,8 @@ class QuoteHandle {
27520
27583
  import * as anchor5 from "@anchor-lang/core";
27521
27584
  import { DEPTH_PROGRAM_ID as DEPTH_PROGRAM_ID3 } from "@depthhq/bindings";
27522
27585
  import { PublicKey as PublicKey6 } from "@solana/web3.js";
27586
+ import { quoteBounds as quoteBounds3 } from "@depthhq/bindings";
27587
+
27523
27588
  class QuoteReplacement {
27524
27589
  client;
27525
27590
  priorId;
@@ -27565,10 +27630,12 @@ class QuoteReplacement {
27565
27630
  if (input.clientNonce !== undefined && input.clientNonce !== this.nonce)
27566
27631
  throw new Error("Replacement terms use a different client nonce");
27567
27632
  await this.begin();
27568
- return this.client.quote.publish({
27633
+ const handle = await this.client.quote.publish({
27569
27634
  ...input,
27570
27635
  clientNonce: this.nonce
27571
27636
  });
27637
+ handle.priorId = this.priorId;
27638
+ return handle;
27572
27639
  }
27573
27640
  async begin() {
27574
27641
  if (!await this.client.options.connection.getAccountInfo(this.priorId, "confirmed")) {
@@ -27590,6 +27657,74 @@ class QuoteReplacement {
27590
27657
  }).rpc({ commitment: "confirmed" });
27591
27658
  }
27592
27659
  }
27660
+ async rotate(handle = this.client.quote.get(this.nextId)) {
27661
+ const snapshot = await handle.status();
27662
+ if (["reserving", "active", "filled", "cancelled", "rejected"].includes(snapshot.status))
27663
+ return snapshot;
27664
+ if (snapshot.status !== "validated")
27665
+ return snapshot;
27666
+ const [prior, quote, market, account] = await Promise.all([
27667
+ this.client.quote.get(this.priorId).status(),
27668
+ this.client.quoteState(this.nextId),
27669
+ this.client.read("Market", this.client.marketAddress),
27670
+ this.client.read("MarketAccount", this.client.marketAccount())
27671
+ ]);
27672
+ if (prior.status !== "active")
27673
+ return handle.activate({ timeoutMs: 0 });
27674
+ const reference = BigInt(quote.oracleReferenceE8.toString());
27675
+ const price = BigInt(market.oraclePriceE8.toString());
27676
+ const drift = price > reference ? price - reference : reference - price;
27677
+ if (!quote.marketRevision.eq(market.revision) || market.oracleTimestamp.lt(quote.oracleTimestamp) || reference <= 0n || drift * 10000n > reference * BigInt(quoteBounds3(market.config.maxQuoteDeviationBps).driftBps) || quote.expiry.toNumber() <= Math.floor(Date.now() / 1000))
27678
+ return handle.cancel();
27679
+ if (market.pendingOrders > 0 || !("ready" in account.status) || prior.attachedOrders > 0)
27680
+ return snapshot;
27681
+ const age = Math.floor(Date.now() / 1000) - market.oracleTimestamp.toNumber();
27682
+ if (age > market.config.maxOracleAgeSeconds - Math.min(60, Math.floor(market.config.maxOracleAgeSeconds / 2)))
27683
+ return snapshot;
27684
+ const offset = new anchor5.BN(randomBytes3(8), "le");
27685
+ const actor = this.client.options.wallet.publicKey;
27686
+ try {
27687
+ handle.reserveSignature = await this.client.program.methods.rotateQuote(offset).accountsPartial({
27688
+ ...await this.client.queue("rotate_quote", offset),
27689
+ payer: actor,
27690
+ market: this.client.marketAddress,
27691
+ marketAccount: this.client.marketAccount(),
27692
+ quote: this.nextId,
27693
+ prior: this.priorId,
27694
+ grant: actor.equals(this.client.options.maker) ? this.client.program.programId : this.client.grantAddress()
27695
+ }).rpc({ commitment: "confirmed" });
27696
+ } catch (error) {
27697
+ const receipt = await handle.status();
27698
+ if (["reserving", "active"].includes(receipt.status))
27699
+ return receipt;
27700
+ const code2 = error.error?.errorCode?.code;
27701
+ if (["WrongMarketAccountStatus", "WrongOrderStatus"].includes(code2 ?? ""))
27702
+ return receipt;
27703
+ throw error;
27704
+ }
27705
+ return handle.status();
27706
+ }
27707
+ async discard() {
27708
+ const next = this.client.quote.get(this.nextId);
27709
+ const snapshot = await next.cancel();
27710
+ if (!["cancelled", "rejected"].includes(snapshot.status))
27711
+ throw new Error("Successor cancellation is still pending");
27712
+ const prior = await this.client.quoteState(this.priorId);
27713
+ if (prior.replacementNonce == null)
27714
+ return;
27715
+ const actor = this.client.options.wallet.publicKey;
27716
+ try {
27717
+ await this.client.program.methods.resetQuoteReplacement().accountsPartial({
27718
+ actor,
27719
+ quote: this.priorId,
27720
+ successor: this.nextId,
27721
+ grant: actor.equals(this.client.options.maker) ? this.client.program.programId : this.client.grantAddress()
27722
+ }).rpc({ commitment: "confirmed" });
27723
+ } catch (error) {
27724
+ if ((await this.client.quoteState(this.priorId)).replacementNonce != null)
27725
+ throw error;
27726
+ }
27727
+ }
27593
27728
  async resume(input, options = {}) {
27594
27729
  if (input.clientNonce !== undefined && input.clientNonce !== this.nonce)
27595
27730
  throw new Error("Replacement terms use a different client nonce");
@@ -27600,6 +27735,7 @@ class QuoteReplacement {
27600
27735
  const deadline = Date.now() + timeoutMs;
27601
27736
  const previous = this.client.quote.get(this.priorId);
27602
27737
  const next = await this.prepare(input);
27738
+ next.priorId = this.priorId;
27603
27739
  for (;; ) {
27604
27740
  const snapshot = await next.status();
27605
27741
  if (["validated", "waiting", "reserving", "active", "filled"].includes(snapshot.status))
@@ -27616,6 +27752,10 @@ class QuoteReplacement {
27616
27752
  const snapshot = await previous.status();
27617
27753
  if (["cancelled", "filled", "rejected"].includes(snapshot.status))
27618
27754
  break;
27755
+ if (snapshot.status === "active") {
27756
+ await this.rotate(next);
27757
+ return next;
27758
+ }
27619
27759
  if (["active", "closing"].includes(snapshot.status)) {
27620
27760
  if (snapshot.status === "active" || snapshot.attachedOrders === 0)
27621
27761
  await previous.cancel();
@@ -27707,6 +27847,7 @@ function stateName(value) {
27707
27847
 
27708
27848
  class MakerClient {
27709
27849
  options;
27850
+ marketView;
27710
27851
  hedges;
27711
27852
  automation;
27712
27853
  provider;
@@ -28072,7 +28213,7 @@ class MakerClient {
28072
28213
  throw new Error("Only SOL-USD is supported");
28073
28214
  if (!Number.isSafeInteger(input.validForSeconds) || input.validForSeconds < 1 || input.validForSeconds > market.config.maxQuoteLifetimeSeconds)
28074
28215
  throw new RangeError("Quote duration is outside market policy");
28075
- const tiers = market.config.quoteTiers.map((size) => BigInt(size.toString())).filter((size) => size > 0n && size <= input.maxCapacity).map((size) => ({
28216
+ const tiers = quoteSizes2(market.config.quoteTiers.map((size) => BigInt(size.toString())), input.maxCapacity).map((size) => ({
28076
28217
  size,
28077
28218
  bidE8: input.bidE8,
28078
28219
  askE8: input.askE8
@@ -28184,10 +28325,13 @@ class MakerClient {
28184
28325
  summary: async (options = { timeoutMs: 30000 }) => {
28185
28326
  if (this.options.wallet.publicKey.equals(this.options.maker))
28186
28327
  return (await this.owned()).position.summary(options);
28187
- const view = await this.requestMarketView();
28328
+ const view = this.marketView ?? await this.requestMarketView();
28329
+ this.marketView = view;
28188
28330
  const result = await view.wait(options);
28189
- if (result.status !== "pending")
28331
+ if (result.status !== "pending") {
28332
+ this.marketView = undefined;
28190
28333
  await view.deferClose();
28334
+ }
28191
28335
  return result;
28192
28336
  }
28193
28337
  };
@@ -28224,7 +28368,7 @@ class MakerClient {
28224
28368
  validateQuote(input, market) {
28225
28369
  const now = Math.floor(Date.now() / 1000);
28226
28370
  const reference = BigInt(market.oraclePriceE8.toString());
28227
- const { preparationBps } = quoteBounds3(market.config.maxQuoteDeviationBps);
28371
+ const { preparationBps } = quoteBounds4(market.config.maxQuoteDeviationBps);
28228
28372
  if (!["draft", "active"].includes(stateName(market.status)))
28229
28373
  throw new Error("Market does not accept quotes");
28230
28374
  if (input.tiers.length === 0 || input.tiers.length > 4) {
@@ -28244,7 +28388,7 @@ class MakerClient {
28244
28388
  throw new Error("Quote prices exceed the preparation deviation limit");
28245
28389
  });
28246
28390
  u642(input.maxCapacity, "maxCapacity");
28247
- if (input.maxCapacity < input.tiers.at(-1).size || input.maxCapacity > BigInt(market.config.maxAccountNotional.toString()) || input.maxCapacity > BigInt(market.config.maxMarketNotional.toString())) {
28391
+ if (input.maxCapacity < input.tiers[0].size || input.maxCapacity > BigInt(market.config.maxAccountNotional.toString()) || input.maxCapacity > BigInt(market.config.maxMarketNotional.toString())) {
28248
28392
  throw new Error("Quote exceeds market capacity");
28249
28393
  }
28250
28394
  if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= now || input.expiresAt - now > market.config.maxQuoteLifetimeSeconds) {
@@ -24907,7 +24907,8 @@ import {
24907
24907
  quoteQueueAddress,
24908
24908
  protocolAddress,
24909
24909
  SOL_USD_MARKET_ID as SOL_USD_MARKET_ID2,
24910
- quoteBounds as quoteBounds3
24910
+ quoteBounds as quoteBounds4,
24911
+ quoteSizes as quoteSizes2
24911
24912
  } from "@depthhq/bindings";
24912
24913
  import { DepthView } from "@depthhq/sdk/views";
24913
24914
  import { DepthRecovery } from "@depthhq/sdk/recovery";
@@ -25118,7 +25119,7 @@ import { PublicKey as PublicKey2 } from "@solana/web3.js";
25118
25119
 
25119
25120
  // src/bot/policy.ts
25120
25121
  import { positionPnl } from "@depthhq/sdk";
25121
- import { quoteBounds } from "@depthhq/bindings";
25122
+ import { quoteBounds, quoteSizes } from "@depthhq/bindings";
25122
25123
 
25123
25124
  // src/hedge/policy.ts
25124
25125
  function validateHedgePolicy(policy) {
@@ -25817,6 +25818,7 @@ class QuoteHandle {
25817
25818
  client;
25818
25819
  reserveSignature;
25819
25820
  releaseSignature;
25821
+ priorId;
25820
25822
  balanceView;
25821
25823
  constructor(id, prepareSignature, client) {
25822
25824
  this.id = id;
@@ -25830,9 +25832,36 @@ class QuoteHandle {
25830
25832
  status: name(state.status),
25831
25833
  maxCapacity: BigInt(state.capacity.toString()),
25832
25834
  attachedOrders: state.attachedOrders,
25835
+ expiresAt: state.expiry.toNumber(),
25833
25836
  ...state.replacementNonce == null ? {} : { replacementNonce: BigInt(state.replacementNonce.toString()) }
25834
25837
  };
25835
25838
  }
25839
+ async renew(input) {
25840
+ const market = await this.client.read("Market", this.client.marketAddress);
25841
+ if (!Number.isSafeInteger(input.validForSeconds) || input.validForSeconds < 1 || input.validForSeconds > market.config.maxQuoteLifetimeSeconds)
25842
+ throw new RangeError("Quote duration is outside market policy");
25843
+ const expiry = Math.floor(Date.now() / 1000) + input.validForSeconds;
25844
+ const before = await this.status();
25845
+ if (before.status !== "active")
25846
+ throw new Error("Only active quotes can be renewed");
25847
+ if (before.expiresAt >= expiry)
25848
+ return before;
25849
+ const actor = this.client.options.wallet.publicKey;
25850
+ try {
25851
+ await this.client.program.methods.renewQuote(new anchor4.BN(expiry)).accountsPartial({
25852
+ actor,
25853
+ market: this.client.marketAddress,
25854
+ quote: this.id,
25855
+ grant: actor.equals(this.client.options.maker) ? this.client.program.programId : this.client.grantAddress()
25856
+ }).rpc({ commitment: "confirmed" });
25857
+ } catch (error) {
25858
+ const receipt = await this.status();
25859
+ if (receipt.status !== "active" || receipt.expiresAt < expiry)
25860
+ throw error;
25861
+ return receipt;
25862
+ }
25863
+ return this.status();
25864
+ }
25836
25865
  async capacity() {
25837
25866
  for (let attempt = 0;attempt < 3; attempt++) {
25838
25867
  const view = this.balanceView ?? await this.client.requestQuoteView(this.id);
@@ -25847,6 +25876,17 @@ class QuoteHandle {
25847
25876
  }
25848
25877
  throw new Error("Quote balance changed while the private view was running");
25849
25878
  }
25879
+ async reservation() {
25880
+ const view = this.balanceView ?? await this.client.requestQuoteView(this.id);
25881
+ this.balanceView = view;
25882
+ const snapshot = await view.wait({ timeoutMs: 0 });
25883
+ if (snapshot.status === "pending")
25884
+ return;
25885
+ await view.deferClose();
25886
+ this.balanceView = undefined;
25887
+ if (snapshot.status === "ready" && "remainingMargin" in snapshot)
25888
+ return snapshot.remainingMargin;
25889
+ }
25850
25890
  async confirm(options = {}) {
25851
25891
  const timeoutMs = options.timeoutMs ?? 60000;
25852
25892
  const started = Date.now();
@@ -25895,8 +25935,22 @@ class QuoteHandle {
25895
25935
  return snapshot;
25896
25936
  }
25897
25937
  if (snapshot.status === "validated" || snapshot.status === "waiting") {
25938
+ if (this.priorId && snapshot.status === "validated") {
25939
+ const prior = await this.client.options.connection.getAccountInfo(this.priorId, "confirmed");
25940
+ if (prior) {
25941
+ const previous = await this.client.quote.get(this.priorId).status();
25942
+ if (previous.status === "active") {
25943
+ const quote2 = await this.client.quoteState(this.id);
25944
+ return this.client.quote.replacement(this.priorId, BigInt(quote2.nonce.toString())).rotate(this);
25945
+ }
25946
+ }
25947
+ }
25898
25948
  const market = await this.client.read("Market", this.client.marketAddress);
25899
25949
  const quote = await this.client.quoteState(this.id);
25950
+ const now = Math.floor(Date.now() / 1000);
25951
+ const age = now - market.oracleTimestamp.toNumber();
25952
+ if (age > market.config.maxOracleAgeSeconds - Math.min(Math.floor(market.config.maxOracleAgeSeconds / 2), 60))
25953
+ return snapshot;
25900
25954
  const reference = BigInt(quote.oracleReferenceE8.toString());
25901
25955
  const price = BigInt(market.oraclePriceE8.toString());
25902
25956
  const drift = price > reference ? price - reference : reference - price;
@@ -25966,6 +26020,15 @@ class QuoteHandle {
25966
26020
  return snapshot;
25967
26021
  const actor = this.client.options.wallet.publicKey;
25968
26022
  const grant = actor.equals(this.client.options.maker) ? undefined : this.client.grantAddress();
26023
+ if (snapshot.status === "reserving") {
26024
+ await this.cancelStep(() => this.client.program.methods.cancelQuoteReserve().accountsPartial({
26025
+ actor,
26026
+ grant: grant ?? this.client.program.programId,
26027
+ quote: this.id,
26028
+ marketAccount: this.client.marketAccount()
26029
+ }).rpc({ commitment: "confirmed" }), ["validated", "active", "cancelled"]);
26030
+ return this.cancel();
26031
+ }
25969
26032
  if (snapshot.status === "validating" || snapshot.status === "validated") {
25970
26033
  return this.cancelStep(() => this.client.program.methods.cancelPreparedQuote().accountsPartial({
25971
26034
  actor,
@@ -26040,6 +26103,7 @@ import * as anchor5 from "@anchor-lang/core";
26040
26103
  import { DEPTH_PROGRAM_ID as DEPTH_PROGRAM_ID3 } from "@depthhq/bindings";
26041
26104
  import { PublicKey as PublicKey6 } from "@solana/web3.js";
26042
26105
  import { Buffer as Buffer7 } from "buffer";
26106
+ import { quoteBounds as quoteBounds3 } from "@depthhq/bindings";
26043
26107
 
26044
26108
  class QuoteReplacement {
26045
26109
  client;
@@ -26086,10 +26150,12 @@ class QuoteReplacement {
26086
26150
  if (input.clientNonce !== undefined && input.clientNonce !== this.nonce)
26087
26151
  throw new Error("Replacement terms use a different client nonce");
26088
26152
  await this.begin();
26089
- return this.client.quote.publish({
26153
+ const handle = await this.client.quote.publish({
26090
26154
  ...input,
26091
26155
  clientNonce: this.nonce
26092
26156
  });
26157
+ handle.priorId = this.priorId;
26158
+ return handle;
26093
26159
  }
26094
26160
  async begin() {
26095
26161
  if (!await this.client.options.connection.getAccountInfo(this.priorId, "confirmed")) {
@@ -26111,6 +26177,74 @@ class QuoteReplacement {
26111
26177
  }).rpc({ commitment: "confirmed" });
26112
26178
  }
26113
26179
  }
26180
+ async rotate(handle = this.client.quote.get(this.nextId)) {
26181
+ const snapshot = await handle.status();
26182
+ if (["reserving", "active", "filled", "cancelled", "rejected"].includes(snapshot.status))
26183
+ return snapshot;
26184
+ if (snapshot.status !== "validated")
26185
+ return snapshot;
26186
+ const [prior, quote, market, account] = await Promise.all([
26187
+ this.client.quote.get(this.priorId).status(),
26188
+ this.client.quoteState(this.nextId),
26189
+ this.client.read("Market", this.client.marketAddress),
26190
+ this.client.read("MarketAccount", this.client.marketAccount())
26191
+ ]);
26192
+ if (prior.status !== "active")
26193
+ return handle.activate({ timeoutMs: 0 });
26194
+ const reference = BigInt(quote.oracleReferenceE8.toString());
26195
+ const price = BigInt(market.oraclePriceE8.toString());
26196
+ const drift = price > reference ? price - reference : reference - price;
26197
+ if (!quote.marketRevision.eq(market.revision) || market.oracleTimestamp.lt(quote.oracleTimestamp) || reference <= 0n || drift * 10000n > reference * BigInt(quoteBounds3(market.config.maxQuoteDeviationBps).driftBps) || quote.expiry.toNumber() <= Math.floor(Date.now() / 1000))
26198
+ return handle.cancel();
26199
+ if (market.pendingOrders > 0 || !("ready" in account.status) || prior.attachedOrders > 0)
26200
+ return snapshot;
26201
+ const age = Math.floor(Date.now() / 1000) - market.oracleTimestamp.toNumber();
26202
+ if (age > market.config.maxOracleAgeSeconds - Math.min(60, Math.floor(market.config.maxOracleAgeSeconds / 2)))
26203
+ return snapshot;
26204
+ const offset = new anchor5.BN(randomBytes3(8), "le");
26205
+ const actor = this.client.options.wallet.publicKey;
26206
+ try {
26207
+ handle.reserveSignature = await this.client.program.methods.rotateQuote(offset).accountsPartial({
26208
+ ...await this.client.queue("rotate_quote", offset),
26209
+ payer: actor,
26210
+ market: this.client.marketAddress,
26211
+ marketAccount: this.client.marketAccount(),
26212
+ quote: this.nextId,
26213
+ prior: this.priorId,
26214
+ grant: actor.equals(this.client.options.maker) ? this.client.program.programId : this.client.grantAddress()
26215
+ }).rpc({ commitment: "confirmed" });
26216
+ } catch (error) {
26217
+ const receipt = await handle.status();
26218
+ if (["reserving", "active"].includes(receipt.status))
26219
+ return receipt;
26220
+ const code = error.error?.errorCode?.code;
26221
+ if (["WrongMarketAccountStatus", "WrongOrderStatus"].includes(code ?? ""))
26222
+ return receipt;
26223
+ throw error;
26224
+ }
26225
+ return handle.status();
26226
+ }
26227
+ async discard() {
26228
+ const next = this.client.quote.get(this.nextId);
26229
+ const snapshot = await next.cancel();
26230
+ if (!["cancelled", "rejected"].includes(snapshot.status))
26231
+ throw new Error("Successor cancellation is still pending");
26232
+ const prior = await this.client.quoteState(this.priorId);
26233
+ if (prior.replacementNonce == null)
26234
+ return;
26235
+ const actor = this.client.options.wallet.publicKey;
26236
+ try {
26237
+ await this.client.program.methods.resetQuoteReplacement().accountsPartial({
26238
+ actor,
26239
+ quote: this.priorId,
26240
+ successor: this.nextId,
26241
+ grant: actor.equals(this.client.options.maker) ? this.client.program.programId : this.client.grantAddress()
26242
+ }).rpc({ commitment: "confirmed" });
26243
+ } catch (error) {
26244
+ if ((await this.client.quoteState(this.priorId)).replacementNonce != null)
26245
+ throw error;
26246
+ }
26247
+ }
26114
26248
  async resume(input, options = {}) {
26115
26249
  if (input.clientNonce !== undefined && input.clientNonce !== this.nonce)
26116
26250
  throw new Error("Replacement terms use a different client nonce");
@@ -26121,6 +26255,7 @@ class QuoteReplacement {
26121
26255
  const deadline = Date.now() + timeoutMs;
26122
26256
  const previous = this.client.quote.get(this.priorId);
26123
26257
  const next = await this.prepare(input);
26258
+ next.priorId = this.priorId;
26124
26259
  for (;; ) {
26125
26260
  const snapshot = await next.status();
26126
26261
  if (["validated", "waiting", "reserving", "active", "filled"].includes(snapshot.status))
@@ -26137,6 +26272,10 @@ class QuoteReplacement {
26137
26272
  const snapshot = await previous.status();
26138
26273
  if (["cancelled", "filled", "rejected"].includes(snapshot.status))
26139
26274
  break;
26275
+ if (snapshot.status === "active") {
26276
+ await this.rotate(next);
26277
+ return next;
26278
+ }
26140
26279
  if (["active", "closing"].includes(snapshot.status)) {
26141
26280
  if (snapshot.status === "active" || snapshot.attachedOrders === 0)
26142
26281
  await previous.cancel();
@@ -26228,6 +26367,7 @@ function stateName(value) {
26228
26367
 
26229
26368
  class MakerClient {
26230
26369
  options;
26370
+ marketView;
26231
26371
  hedges;
26232
26372
  automation;
26233
26373
  provider;
@@ -26593,7 +26733,7 @@ class MakerClient {
26593
26733
  throw new Error("Only SOL-USD is supported");
26594
26734
  if (!Number.isSafeInteger(input.validForSeconds) || input.validForSeconds < 1 || input.validForSeconds > market.config.maxQuoteLifetimeSeconds)
26595
26735
  throw new RangeError("Quote duration is outside market policy");
26596
- const tiers = market.config.quoteTiers.map((size) => BigInt(size.toString())).filter((size) => size > 0n && size <= input.maxCapacity).map((size) => ({
26736
+ const tiers = quoteSizes2(market.config.quoteTiers.map((size) => BigInt(size.toString())), input.maxCapacity).map((size) => ({
26597
26737
  size,
26598
26738
  bidE8: input.bidE8,
26599
26739
  askE8: input.askE8
@@ -26705,10 +26845,13 @@ class MakerClient {
26705
26845
  summary: async (options = { timeoutMs: 30000 }) => {
26706
26846
  if (this.options.wallet.publicKey.equals(this.options.maker))
26707
26847
  return (await this.owned()).position.summary(options);
26708
- const view = await this.requestMarketView();
26848
+ const view = this.marketView ?? await this.requestMarketView();
26849
+ this.marketView = view;
26709
26850
  const result = await view.wait(options);
26710
- if (result.status !== "pending")
26851
+ if (result.status !== "pending") {
26852
+ this.marketView = undefined;
26711
26853
  await view.deferClose();
26854
+ }
26712
26855
  return result;
26713
26856
  }
26714
26857
  };
@@ -26745,7 +26888,7 @@ class MakerClient {
26745
26888
  validateQuote(input, market) {
26746
26889
  const now = Math.floor(Date.now() / 1000);
26747
26890
  const reference = BigInt(market.oraclePriceE8.toString());
26748
- const { preparationBps } = quoteBounds3(market.config.maxQuoteDeviationBps);
26891
+ const { preparationBps } = quoteBounds4(market.config.maxQuoteDeviationBps);
26749
26892
  if (!["draft", "active"].includes(stateName(market.status)))
26750
26893
  throw new Error("Market does not accept quotes");
26751
26894
  if (input.tiers.length === 0 || input.tiers.length > 4) {
@@ -26765,7 +26908,7 @@ class MakerClient {
26765
26908
  throw new Error("Quote prices exceed the preparation deviation limit");
26766
26909
  });
26767
26910
  u642(input.maxCapacity, "maxCapacity");
26768
- if (input.maxCapacity < input.tiers.at(-1).size || input.maxCapacity > BigInt(market.config.maxAccountNotional.toString()) || input.maxCapacity > BigInt(market.config.maxMarketNotional.toString())) {
26911
+ if (input.maxCapacity < input.tiers[0].size || input.maxCapacity > BigInt(market.config.maxAccountNotional.toString()) || input.maxCapacity > BigInt(market.config.maxMarketNotional.toString())) {
26769
26912
  throw new Error("Quote exceeds market capacity");
26770
26913
  }
26771
26914
  if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= now || input.expiresAt - now > market.config.maxQuoteLifetimeSeconds) {
package/dist/node.js CHANGED
@@ -154,6 +154,11 @@ class BotStore {
154
154
  for (const value of [state.quote, state.replacement?.prior])
155
155
  if (value !== undefined && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
156
156
  throw new Error("Maker bot checkpoint quote is invalid");
157
+ if (state.inventoryPendingSince !== undefined && (!Number.isSafeInteger(state.inventoryPendingSince) || state.inventoryPendingSince <= 0))
158
+ throw new Error("Inventory checkpoint time is invalid");
159
+ for (const risk of [state.quoteRisk, state.replacement?.risk])
160
+ if (risk && (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(risk.quote) || typeof risk.notional !== "bigint" || risk.notional < 0n || typeof risk.price !== "bigint" || risk.price <= 0n || typeof risk.version !== "bigint" || risk.version < 0n || typeof risk.externalPosition !== "bigint" || !Number.isSafeInteger(risk.hedgeNonce) || risk.hedgeNonce < 0 || risk.balanceNonce !== undefined && !/^[0-9a-f]{32}$/.test(risk.balanceNonce)))
161
+ throw new Error("Quote risk checkpoint is invalid");
157
162
  if (state.quoteWrite && (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(state.quoteWrite.quote) || typeof state.quoteWrite.version !== "bigint" || typeof state.quoteWrite.release !== "boolean" || state.quoteWrite.version < 0n))
158
163
  throw new Error("Quote write checkpoint is invalid");
159
164
  for (const request of [state.request, state.replacement?.request])
@@ -206,7 +211,7 @@ class BotStore {
206
211
 
207
212
  // src/bot/policy.ts
208
213
  import { positionPnl } from "@depthhq/sdk";
209
- import { quoteBounds } from "@depthhq/bindings";
214
+ import { quoteBounds, quoteSizes } from "@depthhq/bindings";
210
215
 
211
216
  // src/hedge/policy.ts
212
217
  function validateHedgePolicy(policy) {
@@ -267,6 +272,13 @@ var abs = (value) => value < 0n ? -value : value;
267
272
  var notional = (size, priceE8) => (abs(size) * priceE8 + 99999999999n) / 100000000000n;
268
273
 
269
274
  // src/bot/policy.ts
275
+ function executionCosts(policy, pricing) {
276
+ const horizon = policy.fundingHours ?? 1;
277
+ return {
278
+ bid: Math.ceil((pricing?.takerFeeBps ?? 0) + Math.max(0, -(pricing?.hourlyFundingBps ?? 0)) * horizon),
279
+ ask: Math.ceil((pricing?.takerFeeBps ?? 0) + Math.max(0, pricing?.hourlyFundingBps ?? 0) * horizon)
280
+ };
281
+ }
270
282
  function validateBotPolicy(policy) {
271
283
  if (policy.maxCapacity <= 0n || policy.maxInventoryNotional <= 0n || policy.maxCapacity > policy.maxInventoryNotional)
272
284
  throw new RangeError("Maker capacity must fit a positive inventory limit");
@@ -296,7 +308,7 @@ function quoteHealth(market, now) {
296
308
  if (BigInt(market.oraclePriceE8.toString()) < 100000000n)
297
309
  return "oracle price invalid";
298
310
  }
299
- function botQuote(policy, market, position, nonce, now, pricing) {
311
+ function botQuote(policy, market, position, nonce, now, pricing, reservationCredit = 0n) {
300
312
  validateBotPolicy(policy);
301
313
  const reason = quoteHealth(market, now);
302
314
  if (reason)
@@ -304,12 +316,14 @@ function botQuote(policy, market, position, nonce, now, pricing) {
304
316
  if (position.status !== "ready" || position.position === undefined || position.collateral === undefined || position.reserved === undefined)
305
317
  throw new Error("Maker private position is not ready");
306
318
  const price = BigInt(market.oraclePriceE8.toString());
307
- const exposure = (position.position < 0n ? -position.position : position.position) * price / 100000000000n;
319
+ const exposure = ((position.position < 0n ? -position.position : position.position) * price + 99999999999n) / 100000000000n;
308
320
  const marginBps = BigInt(market.config.initialMarginBps);
321
+ if (reservationCredit < 0n || reservationCredit > position.reserved)
322
+ throw new Error("Replacement margin credit exceeds the private reservation");
309
323
  if (marginBps <= 0n)
310
324
  throw new Error("Maker margin policy is invalid");
311
325
  const pnl = position.position === 0n ? 0n : positionPnl(position, price, BigInt(market.fundingIndex.toString())).net;
312
- const free = position.collateral + pnl - position.reserved - (exposure * marginBps + 9999n) / 10000n;
326
+ const free = position.collateral + pnl - position.reserved - (exposure * marginBps + 9999n) / 10000n + reservationCredit;
313
327
  const limits = [
314
328
  policy.maxCapacity,
315
329
  policy.maxInventoryNotional - exposure,
@@ -318,7 +332,7 @@ function botQuote(policy, market, position, nonce, now, pricing) {
318
332
  BigInt(market.config.maxMarketNotional.toString())
319
333
  ];
320
334
  const capacity = limits.reduce((minimum, value) => value < minimum ? value : minimum);
321
- const tiers = market.config.quoteTiers.map((value) => BigInt(value.toString())).filter((size) => size > 0n && size <= capacity);
335
+ const tiers = quoteSizes(market.config.quoteTiers.map((value) => BigInt(value.toString())), capacity);
322
336
  if (!tiers.length)
323
337
  return;
324
338
  const half = Math.ceil(policy.spreadBps / 2);
@@ -335,9 +349,9 @@ function botQuote(policy, market, position, nonce, now, pricing) {
335
349
  const boundedSkew = skewBps < BigInt(policy.inventorySkewBps ?? 0) ? skewBps : BigInt(policy.inventorySkewBps ?? 0);
336
350
  const skew = price * boundedSkew * (netPosition > 0n ? -1n : 1n) / 10000n;
337
351
  const costs = BigInt(half + (policy.executionCostBps ?? 0));
338
- const horizon = policy.fundingHours ?? 1;
339
- const bidCosts = costs + BigInt(Math.ceil((pricing?.takerFeeBps ?? 0) + Math.max(0, -(pricing?.hourlyFundingBps ?? 0)) * horizon));
340
- const askCosts = costs + BigInt(Math.ceil((pricing?.takerFeeBps ?? 0) + Math.max(0, pricing?.hourlyFundingBps ?? 0) * horizon));
352
+ const execution = executionCosts(policy, pricing);
353
+ const bidCosts = costs + BigInt(execution.bid);
354
+ const askCosts = costs + BigInt(execution.ask);
341
355
  const quotes = tiers.map((size) => {
342
356
  const units = (size * 100000000000n + price - 1n) / price;
343
357
  const sell = pricing ? executionPrice(pricing.book.bids, units) : price;
@@ -369,6 +383,7 @@ class MakerBot {
369
383
  hedge;
370
384
  state;
371
385
  position;
386
+ verifiedPosition;
372
387
  dirty = true;
373
388
  busy = false;
374
389
  stopping = false;
@@ -378,6 +393,7 @@ class MakerBot {
378
393
  lastCleanup = 0;
379
394
  lastFills = 0;
380
395
  inventoryVersion;
396
+ reservationView;
381
397
  constructor(maker, policy, store, hedge) {
382
398
  this.maker = maker;
383
399
  this.policy = policy;
@@ -456,8 +472,18 @@ class MakerBot {
456
472
  let ready = "ready" in account.status && "ready" in confirmed.status && account.version.toString() === confirmed.version.toString();
457
473
  const currentVersion = BigInt(confirmed.version.toString());
458
474
  const candidate = write && this.position && "ready" in confirmed.status && currentVersion === write.version + 1n && (this.inventoryVersion ?? this.position.version) >= write.version && (this.inventoryVersion ?? this.position.version) <= currentVersion;
459
- const completedWrite = candidate && (await this.maker.quoteState(new PublicKey(write.quote))).status;
460
- const ownCommit = completedWrite && (write.release ? "cancelled" in completedWrite || "filled" in completedWrite : ("active" in completedWrite));
475
+ const completedQuote = write && "ready" in confirmed.status && currentVersion === write.version + 1n && await this.maker.quoteState(new PublicKey(write.quote));
476
+ const completedWrite = candidate && completedQuote ? completedQuote.status : undefined;
477
+ const ownCommit = write && completedWrite && (write.release ? "cancelled" in completedWrite || "filled" in completedWrite : ("active" in completedWrite));
478
+ const riskWrite = [
479
+ this.state.quoteRisk,
480
+ this.state.replacement?.risk
481
+ ].find((risk) => risk?.quote === write?.quote);
482
+ if (completedQuote && "active" in completedQuote.status && riskWrite) {
483
+ riskWrite.version = currentVersion;
484
+ riskWrite.balanceNonce = Buffer.from(completedQuote.balanceNonce).toString("hex");
485
+ await this.store.save(this.state);
486
+ }
461
487
  if (ownCommit) {
462
488
  this.inventoryVersion = currentVersion;
463
489
  ready = true;
@@ -466,9 +492,9 @@ class MakerBot {
466
492
  await this.store.save(this.state);
467
493
  }
468
494
  }
469
- if (ready && (!this.position || (this.inventoryVersion ?? this.position.version) !== currentVersion)) {
495
+ if ("ready" in confirmed.status && (!this.position || this.position.status !== "ready" || this.position.position === undefined || (this.inventoryVersion ?? this.position.version) !== currentVersion && this.position.version !== currentVersion)) {
470
496
  const prior = this.position;
471
- this.position = await this.maker.position.summary({ timeoutMs: 180000 });
497
+ this.position = await this.maker.position.summary({ timeoutMs: 0 });
472
498
  const fresh = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
473
499
  const latest = await this.maker.read("MarketAccount", this.maker.marketAccount());
474
500
  ready = "ready" in fresh.status && "ready" in latest.status && fresh.version.toString() === latest.version.toString() && this.position.status === "ready" && this.position.version === BigInt(fresh.version.toString());
@@ -482,13 +508,50 @@ class MakerBot {
482
508
  this.dirty = false;
483
509
  }
484
510
  }
485
- if (ready)
511
+ if (ready && this.position?.status === "ready") {
512
+ this.pricingDirty ||= this.verifiedPosition !== undefined && (this.verifiedPosition.position !== this.position.position || this.verifiedPosition.collateral !== this.position.collateral);
513
+ this.verifiedPosition = this.position;
486
514
  this.dirty = false;
515
+ }
487
516
  const snapshot = await this.hedge.tick(this.state.hedge, () => this.saveHedge(), ready ? this.position?.position : undefined);
488
517
  if (snapshot.phase === "pending" || snapshot.phase === "blocked") {
518
+ const risk = this.state.quoteRisk;
519
+ const inventoryWait = snapshot.phase === "pending" && !this.state.hedge?.pending && (snapshot.reason === "Waiting for finalized Depth inventory" || snapshot.reason === "Waiting for the external account to include the confirmed hedge" || snapshot.reason === "Hedge submitted; reconciling confirmed exposure");
520
+ if (inventoryWait && risk && risk.quote === this.state.quote && this.hedge.policy.ratioBps === 1e4) {
521
+ const [book2, external] = await Promise.all([
522
+ this.hedge.adapter.book(),
523
+ this.hedge.adapter.account()
524
+ ]);
525
+ validateBook(book2, Date.now(), this.hedge.policy.maxPriceAgeMs);
526
+ if (!Number.isSafeInteger(external.timestamp) || external.timestamp > Date.now() + 1000 || Date.now() - external.timestamp > this.hedge.policy.maxPriceAgeMs || external.openOrders.length || !Number.isInteger(external.leverage) || external.leverage < 1 || external.availableMargin < 0n || external.leverage > this.hedge.policy.maxLeverage)
527
+ throw new Error("External account is unavailable for quote continuity");
528
+ const market = await this.maker.read("Market", this.maker.marketAddress);
529
+ const price = BigInt(market.oraclePriceE8.toString());
530
+ const expected = risk.externalPosition + this.state.hedge.history.filter((fill) => fill.nonce > risk.hedgeNonce).reduce((total, fill) => total + (fill.buy ? fill.filledSize : -fill.filledSize), 0n);
531
+ const quote = await this.maker.quoteState(new PublicKey(risk.quote));
532
+ const balanceNonce = Buffer.from(quote.balanceNonce).toString("hex");
533
+ const unchanged = currentVersion === risk.version;
534
+ const ownFill = "ready" in confirmed.status && currentVersion === risk.version + 1n && risk.balanceNonce !== undefined && balanceNonce !== risk.balanceNonce;
535
+ if (ownFill) {
536
+ risk.version = currentVersion;
537
+ risk.balanceNonce = balanceNonce;
538
+ this.state.inventoryPendingSince ??= Date.now();
539
+ await this.store.save(this.state);
540
+ }
541
+ const bounded = (unchanged || ownFill) && external.position === expected && risk.notional * price <= this.hedge.policy.maxUnhedgedNotional * risk.price;
542
+ if (market.pendingOrders > 0 && bounded)
543
+ return snapshot.reason;
544
+ if (bounded && unchanged && this.state.inventoryPendingSince === undefined && this.state.hedge.unhedgedSince === undefined)
545
+ return snapshot.reason;
546
+ this.state.inventoryPendingSince ??= Date.now();
547
+ await this.store.save(this.state);
548
+ if (bounded && Date.now() - this.state.inventoryPendingSince < this.hedge.policy.maxUnhedgedMs)
549
+ return snapshot.reason;
550
+ }
489
551
  await this.cancel(true);
490
552
  return snapshot.reason ?? "hedge reconciliation pending";
491
553
  }
554
+ this.state.inventoryPendingSince = undefined;
492
555
  const [book, costs] = await Promise.all([
493
556
  this.hedge.adapter.book(),
494
557
  this.hedge.adapter.costs()
@@ -497,7 +560,9 @@ class MakerBot {
497
560
  const before = this.pricing.book.bids[0].priceE8 + this.pricing.book.asks[0].priceE8;
498
561
  const after = book.bids[0].priceE8 + book.asks[0].priceE8;
499
562
  const change = after > before ? after - before : before - after;
500
- this.pricingDirty ||= change * 10000n >= before * BigInt(this.policy.repriceBps) || costs.takerFeeBps !== this.pricing.takerFeeBps || costs.hourlyFundingBps !== this.pricing.hourlyFundingBps;
563
+ const priorCosts = executionCosts(this.policy, this.pricing);
564
+ const nextCosts = executionCosts(this.policy, costs);
565
+ this.pricingDirty ||= change * 10000n >= before * BigInt(this.policy.repriceBps) || priorCosts.bid !== nextCosts.bid || priorCosts.ask !== nextCosts.ask;
501
566
  }
502
567
  this.pricing = {
503
568
  book,
@@ -561,6 +626,7 @@ class MakerBot {
561
626
  state.quote = flow.nextId.toBase58();
562
627
  state.request = state.replacement.request;
563
628
  state.referencePrice = state.replacement.referencePrice;
629
+ state.quoteRisk = state.replacement.risk;
564
630
  state.replacement = undefined;
565
631
  await this.store.save(state);
566
632
  } else if (!prior && !next && (!state.replacement.request || state.replacement.request.expiresAt + 180 < Math.floor(Date.now() / 1000))) {
@@ -636,7 +702,8 @@ class MakerBot {
636
702
  "waiting",
637
703
  "validated",
638
704
  "validating",
639
- "closing"
705
+ "closing",
706
+ "reserving"
640
707
  ].includes(snapshot.status) ? await this.quoteWrite(handle2, true) : snapshot;
641
708
  successorPending = !terminal.has(result.status);
642
709
  }
@@ -669,7 +736,14 @@ class MakerBot {
669
736
  await this.store.save(state);
670
737
  return true;
671
738
  }
672
- if (["active", "waiting", "validated", "validating", "closing"].includes(quote.status))
739
+ if ([
740
+ "active",
741
+ "waiting",
742
+ "validated",
743
+ "validating",
744
+ "closing",
745
+ "reserving"
746
+ ].includes(quote.status))
673
747
  await this.quoteWrite(handle, true);
674
748
  return false;
675
749
  }
@@ -677,23 +751,41 @@ class MakerBot {
677
751
  const state = this.state;
678
752
  const replacement = state.replacement;
679
753
  const flow = this.maker.quote.replacement(new PublicKey(replacement.prior), replacement.nonce);
754
+ if (now - market.oracleTimestamp.toNumber() > market.config.maxOracleAgeSeconds - Math.min(Math.floor(market.config.maxOracleAgeSeconds / 2), this.policy.lifetimeSeconds))
755
+ return this.snapshot("pending", "waiting for a fresh oracle update");
680
756
  if (!replacement.request) {
681
757
  const account = await this.maker.read("MarketAccount", this.maker.marketAccount());
682
758
  if (!("ready" in account.status) || market.pendingOrders)
683
759
  return this.snapshot("pending", "settlement in progress");
684
- if (!this.position || this.position.version !== BigInt(account.version.toString())) {
760
+ if (!this.position || this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString())) {
685
761
  this.position = await this.maker.position.summary({
686
- timeoutMs: 180000
762
+ timeoutMs: 0
687
763
  });
688
764
  if (this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString()))
689
765
  return this.snapshot("pending", "maker view changed");
766
+ this.inventoryVersion = this.position.version;
767
+ this.dirty = false;
768
+ return this.snapshot("pending", "maker view refreshed");
690
769
  }
691
770
  this.inventoryVersion = this.position.version;
692
771
  this.dirty = false;
693
- replacement.request = botQuote(this.policy, market, this.position, replacement.nonce, now, this.pricing);
772
+ const previous2 = await this.maker.quote.get(new PublicKey(replacement.prior)).status();
773
+ const policy = this.quotePolicy(market);
774
+ const planned = botQuote(policy, market, this.position, replacement.nonce, now, this.pricing);
775
+ let credit = 0n;
776
+ if (previous2.status === "active" && (!planned || planned.maxCapacity < policy.maxCapacity)) {
777
+ this.reservationView ??= this.maker.quote.get(new PublicKey(replacement.prior));
778
+ const margin = await this.reservationView.reservation();
779
+ if (margin === undefined)
780
+ return this.snapshot("pending", "reading remaining quote margin");
781
+ credit = margin;
782
+ this.reservationView = undefined;
783
+ }
784
+ replacement.request = botQuote(policy, market, this.position, replacement.nonce, now, this.pricing, credit);
694
785
  if (!replacement.request)
695
786
  return this.snapshot("pending", "inventory or collateral limit");
696
787
  replacement.referencePrice = BigInt(market.oraclePriceE8.toString());
788
+ replacement.risk = this.quoteRisk(flow.nextId, replacement.request, market);
697
789
  await this.store.save(state);
698
790
  }
699
791
  await flow.begin();
@@ -703,16 +795,39 @@ class MakerBot {
703
795
  if (snapshot.status === "validating" && replacement.request.expiresAt > now)
704
796
  return this.snapshot("pending", "validating successor quote");
705
797
  if (["rejected", "cancelled"].includes(snapshot.status) || replacement.request.expiresAt <= now) {
706
- await this.cancel();
707
- return this.snapshot("pending", "releasing expired successor");
798
+ await flow.discard();
799
+ state.replacement = undefined;
800
+ this.reservationView = undefined;
801
+ await this.store.save(state);
802
+ return this.snapshot("pending", "retrying expired successor");
708
803
  }
709
804
  const prior = this.maker.quote.get(new PublicKey(replacement.prior));
710
805
  const previous = await prior.status();
711
- const size = this.position.position;
712
- const exposure = ((size < 0n ? -size : size) * BigInt(market.oraclePriceE8.toString()) + 99999999999n) / 100000000000n;
713
- if (!terminal.has(previous.status) && ["validated", "waiting", "reserving"].includes(snapshot.status) && market.activeQuoteCount < 4 && exposure + previous.maxCapacity + snapshot.maxCapacity <= this.policy.maxInventoryNotional && BigInt(market.quotedNotional.toString()) + snapshot.maxCapacity <= BigInt(market.config.maxMarketNotional.toString())) {
714
- await this.quoteWrite(next);
715
- return this.snapshot("pending", "activating successor quote");
806
+ if (previous.status === "active" && ["validated", "reserving"].includes(snapshot.status)) {
807
+ const price = BigInt(market.oraclePriceE8.toString());
808
+ const size = this.position.position;
809
+ const exposure = ((size < 0n ? -size : size) * price + 99999999999n) / 100000000000n;
810
+ const risk = this.quoteRisk(next.id, replacement.request, market);
811
+ if (snapshot.status === "validated" && (exposure + snapshot.maxCapacity > this.policy.maxInventoryNotional || risk && risk.notional > this.hedge.policy.maxUnhedgedNotional)) {
812
+ await flow.discard();
813
+ state.replacement = undefined;
814
+ await this.store.save(state);
815
+ return this.snapshot("pending", "repricing changed inventory");
816
+ }
817
+ if (snapshot.status === "validated")
818
+ replacement.risk = risk;
819
+ await this.store.save(state);
820
+ const account = await this.maker.read("MarketAccount", this.maker.marketAccount());
821
+ await flow.rotate(next);
822
+ if (next.reserveSignature) {
823
+ state.quoteWrite = {
824
+ quote: next.id.toBase58(),
825
+ version: BigInt(account.version.toString()),
826
+ release: false
827
+ };
828
+ await this.store.save(state);
829
+ }
830
+ return this.snapshot("pending", "rotating quote reservation");
716
831
  }
717
832
  if (!terminal.has(previous.status)) {
718
833
  if (["active", "waiting", "validated", "validating", "closing"].includes(previous.status))
@@ -722,6 +837,7 @@ class MakerBot {
722
837
  state.quote = flow.nextId.toBase58();
723
838
  state.request = replacement.request;
724
839
  state.referencePrice = replacement.referencePrice;
840
+ state.quoteRisk = replacement.risk;
725
841
  state.replacement = undefined;
726
842
  this.pricingDirty = false;
727
843
  await this.store.save(state);
@@ -762,6 +878,8 @@ class MakerBot {
762
878
  await this.hedge.tick(state.hedge, () => this.saveHedge(), undefined);
763
879
  return this.snapshot(stopped && !state.hedge?.pending ? "stopped" : "pending", reason);
764
880
  }
881
+ if (state.quote)
882
+ await this.renew(market, now);
765
883
  await this.fills();
766
884
  const hedgeReason = await this.hedgeTick();
767
885
  if (hedgeReason)
@@ -799,12 +917,29 @@ class MakerBot {
799
917
  await this.cancel();
800
918
  return this.snapshot("pending", "releasing quote");
801
919
  } else if (quote.status === "active") {
920
+ if (this.dirty && !this.hedge) {
921
+ const account2 = await this.maker.read("MarketAccount", this.maker.marketAccount());
922
+ if ("ready" in account2.status) {
923
+ const before = this.position;
924
+ this.position = await this.maker.position.summary({
925
+ timeoutMs: 0
926
+ });
927
+ if (this.position.status === "ready" && this.position.version === BigInt(account2.version.toString())) {
928
+ this.dirty = false;
929
+ this.pricingDirty ||= before !== undefined && before.position !== this.position.position;
930
+ this.inventoryVersion = this.position.version;
931
+ }
932
+ }
933
+ if (this.dirty)
934
+ return this.snapshot("pending", "reading maker inventory");
935
+ }
802
936
  const price = BigInt(market.oraclePriceE8.toString());
803
937
  const difference = state.referencePrice === undefined ? undefined : (price > state.referencePrice ? price - state.referencePrice : state.referencePrice - price) * 10000n;
804
938
  const current = await this.maker.quoteState(handle2.id);
805
- const duration = Math.min(this.policy.lifetimeSeconds, market.config.maxQuoteLifetimeSeconds);
806
- const refresh = current.expiry.toNumber() - now <= duration - Math.min(this.policy.refreshSeconds, duration - 1);
807
- if (!this.dirty && !this.pricingDirty && !refresh && difference !== undefined && difference < BigInt(this.policy.repriceBps) * state.referencePrice)
939
+ const reference = BigInt(current.oracleReferenceE8.toString());
940
+ const drift = price > reference ? price - reference : reference - price;
941
+ const compatible = current.marketRevision.eq(market.revision) && drift * 10000n <= reference * BigInt(Math.min(Math.floor(market.config.maxQuoteDeviationBps / 4), 25));
942
+ if (!this.dirty && !this.pricingDirty && compatible && current.expiry.toNumber() > now && difference !== undefined && difference < BigInt(this.policy.repriceBps) * state.referencePrice)
808
943
  return this.snapshot("active");
809
944
  state.replacement = { prior: state.quote, nonce: nonce() };
810
945
  this.pricingDirty = false;
@@ -821,7 +956,7 @@ class MakerBot {
821
956
  return this.snapshot("idle", "maker account busy");
822
957
  if (this.dirty || !this.position || this.position.version !== BigInt(account.version.toString())) {
823
958
  this.position = await this.maker.position.summary({
824
- timeoutMs: 180000
959
+ timeoutMs: 0
825
960
  });
826
961
  if (this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString()))
827
962
  return this.snapshot("idle", "maker view changed");
@@ -829,12 +964,13 @@ class MakerBot {
829
964
  return this.snapshot("idle", "maker view refreshed");
830
965
  }
831
966
  if (!state.request) {
832
- const request2 = botQuote(this.policy, market, this.position, nonce(), now, this.pricing);
967
+ const request2 = botQuote(this.quotePolicy(market), market, this.position, nonce(), now, this.pricing);
833
968
  if (!request2)
834
969
  return this.snapshot("idle", "inventory or collateral limit");
835
970
  state.request = request2;
836
971
  this.pricingDirty = false;
837
972
  state.referencePrice = BigInt(market.oraclePriceE8.toString());
973
+ state.quoteRisk = this.quoteRisk(this.requestId(), request2, market);
838
974
  await this.store.save(state);
839
975
  }
840
976
  const request = state.request;
@@ -860,6 +996,55 @@ class MakerBot {
860
996
  this.busy = false;
861
997
  }
862
998
  }
999
+ async renew(market, now) {
1000
+ const state = this.state;
1001
+ if (this.hedge && (state.hedge?.pending || state.inventoryPendingSince !== undefined && Date.now() - state.inventoryPendingSince >= this.hedge.policy.maxUnhedgedMs))
1002
+ return;
1003
+ const handle = this.maker.quote.get(new PublicKey(state.quote));
1004
+ const quote = await this.maker.quoteState(handle.id);
1005
+ if (!("active" in quote.status) || quote.expiry.toNumber() <= now)
1006
+ return;
1007
+ const duration = Math.min(this.policy.lifetimeSeconds, market.config.maxQuoteLifetimeSeconds);
1008
+ if (quote.expiry.toNumber() - now > Math.max(this.policy.refreshSeconds, duration - this.policy.refreshSeconds))
1009
+ return;
1010
+ const price = BigInt(market.oraclePriceE8.toString());
1011
+ const reference = BigInt(quote.oracleReferenceE8.toString());
1012
+ const drift = price > reference ? price - reference : reference - price;
1013
+ if (!quote.marketRevision.eq(market.revision) || reference <= 0n || drift * 10000n > reference * BigInt(Math.min(Math.floor(market.config.maxQuoteDeviationBps / 4), 25)))
1014
+ return;
1015
+ const renewed = await handle.renew({ validForSeconds: duration });
1016
+ if (state.request)
1017
+ state.request.expiresAt = renewed.expiresAt;
1018
+ await this.store.save(state);
1019
+ }
1020
+ quotePolicy(market) {
1021
+ if (!this.hedge || !this.pricing || this.position?.position === undefined)
1022
+ return this.policy;
1023
+ const price = BigInt(market.oraclePriceE8.toString());
1024
+ const size = this.position.position + this.pricing.externalPosition;
1025
+ const risk = ((size < 0n ? -size : size) * price + 99999999999n) / 100000000000n;
1026
+ const remaining = this.hedge.policy.maxUnhedgedNotional * 9n / 10n - risk;
1027
+ const capacity = remaining * 10000n / BigInt(1e4 + market.config.maxQuoteDeviationBps);
1028
+ return {
1029
+ ...this.policy,
1030
+ maxCapacity: capacity < this.policy.maxCapacity ? capacity > 0n ? capacity : 1n : this.policy.maxCapacity
1031
+ };
1032
+ }
1033
+ quoteRisk(id, request, market) {
1034
+ if (!this.hedge || !this.pricing || this.position?.position === undefined)
1035
+ return;
1036
+ const price = BigInt(market.oraclePriceE8.toString());
1037
+ const size = this.position.position + this.pricing.externalPosition;
1038
+ const notional2 = ((size < 0n ? -size : size) * price + 99999999999n) / 100000000000n + (request.maxCapacity * BigInt(1e4 + market.config.maxQuoteDeviationBps) + 9999n) / 10000n;
1039
+ return {
1040
+ quote: id.toBase58(),
1041
+ notional: notional2,
1042
+ price,
1043
+ version: this.position.version,
1044
+ externalPosition: this.pricing.externalPosition,
1045
+ hedgeNonce: this.state.hedge.nonce
1046
+ };
1047
+ }
863
1048
  stop() {
864
1049
  this.stopping = true;
865
1050
  }
@@ -5,6 +5,7 @@ export type QuoteSnapshot = {
5
5
  status: string;
6
6
  maxCapacity: bigint;
7
7
  attachedOrders: number;
8
+ expiresAt: number;
8
9
  replacementNonce?: bigint;
9
10
  };
10
11
  export declare class QuoteHandle {
@@ -13,10 +14,15 @@ export declare class QuoteHandle {
13
14
  private readonly client;
14
15
  reserveSignature?: string;
15
16
  releaseSignature?: string;
17
+ priorId?: PublicKey;
16
18
  private balanceView?;
17
19
  constructor(id: PublicKey, prepareSignature: string | undefined, client: MakerClient);
18
20
  status(): Promise<QuoteSnapshot>;
21
+ renew(input: {
22
+ validForSeconds: number;
23
+ }): Promise<QuoteSnapshot>;
19
24
  capacity(): Promise<bigint>;
25
+ reservation(): Promise<bigint | undefined>;
20
26
  confirm(options?: {
21
27
  timeoutMs?: number;
22
28
  pollMs?: number;
@@ -17,6 +17,8 @@ export declare class QuoteReplacement {
17
17
  status(): Promise<ReplacementSnapshot>;
18
18
  prepare(input: QuoteRequest): Promise<QuoteHandle>;
19
19
  begin(): Promise<void>;
20
+ rotate(handle?: QuoteHandle): Promise<QuoteSnapshot>;
21
+ discard(): Promise<void>;
20
22
  resume(input: QuoteRequest, options?: {
21
23
  timeoutMs?: number;
22
24
  pollMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@depthhq/maker-sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,8 +29,8 @@
29
29
  "@solana/web3.js": "1.99.0",
30
30
  "buffer": "6.0.3",
31
31
  "viem": "2.38.0",
32
- "@depthhq/bindings": "0.1.3",
33
- "@depthhq/sdk": "0.1.7"
32
+ "@depthhq/bindings": "0.1.4",
33
+ "@depthhq/sdk": "0.1.8"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "node ../../scripts/build-package.mjs maker-sdk",