@depthhq/maker-sdk 0.1.9 → 0.1.10

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.
@@ -24,6 +24,7 @@ export declare class BackstopHandle {
24
24
  snapshot: BackstopSnapshot;
25
25
  }>;
26
26
  release(): Promise<BackstopRelease>;
27
+ renew(validForSeconds?: number): Promise<string>;
27
28
  close(): Promise<string>;
28
29
  }
29
30
  export declare class BackstopRelease {
package/dist/bot/bot.d.ts CHANGED
@@ -21,6 +21,9 @@ export declare class MakerBot {
21
21
  private pricing?;
22
22
  private hedgeLease?;
23
23
  private pricingDirty;
24
+ private lastCleanup;
25
+ private lastFills;
26
+ private inventoryVersion?;
24
27
  private constructor();
25
28
  static create(options: {
26
29
  maker: MakerClient;
@@ -30,11 +33,13 @@ export declare class MakerBot {
30
33
  }): Promise<MakerBot>;
31
34
  private saveHedge;
32
35
  private hedgeTick;
36
+ private quoteWrite;
33
37
  private snapshot;
34
38
  private requestId;
35
39
  private reconcile;
36
40
  private fills;
37
41
  private cancel;
42
+ private replace;
38
43
  tick(): Promise<BotSnapshot>;
39
44
  stop(): void;
40
45
  close(): Promise<void>;
@@ -8,9 +8,16 @@ export type BotState = {
8
8
  quote?: string;
9
9
  request?: QuoteInput;
10
10
  referencePrice?: bigint;
11
+ quoteWrite?: {
12
+ quote: string;
13
+ version: bigint;
14
+ release: boolean;
15
+ };
11
16
  replacement?: {
12
17
  prior: string;
13
18
  nonce: bigint;
19
+ request?: QuoteInput;
20
+ referencePrice?: bigint;
14
21
  };
15
22
  fills: Record<string, {
16
23
  signature: string;
@@ -131,6 +131,14 @@ export declare class MakerClient {
131
131
  revoke: (operator: PublicKey) => Promise<string>;
132
132
  };
133
133
  readonly quote: {
134
+ cleanup: (options?: {
135
+ limit?: number;
136
+ exclude?: string[];
137
+ }) => Promise<{
138
+ closed: number;
139
+ reclaimedLamports: bigint;
140
+ signature?: string;
141
+ }>;
134
142
  replacement: (prior: PublicKey, clientNonce: bigint) => QuoteReplacement;
135
143
  publish: (request: QuoteRequest) => Promise<QuoteHandle>;
136
144
  get: (id: PublicKey) => QuoteHandle;
@@ -48,6 +48,10 @@ export type QuoteState = {
48
48
  capacity: anchor.BN;
49
49
  expiry: anchor.BN;
50
50
  attachedOrders: number;
51
+ pendingComputation?: PublicKey;
52
+ marketRevision: anchor.BN;
53
+ oracleTimestamp: anchor.BN;
54
+ oracleReferenceE8: anchor.BN;
51
55
  replacementNonce: anchor.BN | null;
52
56
  };
53
57
  export type MarketState = {
@@ -56,6 +60,7 @@ export type MarketState = {
56
60
  settlementMint: PublicKey;
57
61
  vault: PublicKey;
58
62
  oracleTimestamp: anchor.BN;
63
+ oraclePriceE8: anchor.BN;
59
64
  pendingOrders: number;
60
65
  backstopOffer: PublicKey;
61
66
  activeQuoteCount: number;
@@ -68,6 +73,7 @@ export type MarketState = {
68
73
  maxMarketNotional: anchor.BN;
69
74
  maxQuoteLifetimeSeconds: number;
70
75
  maxOracleAgeSeconds: number;
76
+ maxQuoteDeviationBps: number;
71
77
  backstopMinCollateral: anchor.BN;
72
78
  };
73
79
  };
package/dist/index.js CHANGED
@@ -26395,7 +26395,8 @@ import {
26395
26395
  marketAddress,
26396
26396
  quoteQueueAddress,
26397
26397
  protocolAddress,
26398
- SOL_USD_MARKET_ID as SOL_USD_MARKET_ID2
26398
+ SOL_USD_MARKET_ID as SOL_USD_MARKET_ID2,
26399
+ quoteBounds as quoteBounds3
26399
26400
  } from "@depthhq/bindings";
26400
26401
  import { DepthView } from "@depthhq/sdk/views";
26401
26402
  import { DepthRecovery } from "@depthhq/sdk/recovery";
@@ -26602,6 +26603,7 @@ import { PublicKey as PublicKey2 } from "@solana/web3.js";
26602
26603
 
26603
26604
  // src/bot/policy.ts
26604
26605
  import { positionPnl } from "@depthhq/sdk";
26606
+ import { quoteBounds } from "@depthhq/bindings";
26605
26607
 
26606
26608
  // src/hedge/policy.ts
26607
26609
  function validateHedgePolicy(policy) {
@@ -27068,6 +27070,21 @@ class BackstopHandle {
27068
27070
  });
27069
27071
  return new BackstopRelease(this, signature);
27070
27072
  }
27073
+ async renew(validForSeconds = 86400) {
27074
+ if (!this.client.options.wallet.publicKey.equals(this.client.options.maker))
27075
+ throw new Error("Only the maker owner can renew backstop capacity");
27076
+ if (!Number.isSafeInteger(validForSeconds) || validForSeconds < 1 || validForSeconds > 86400)
27077
+ throw new RangeError("Backstop duration must be from 1 to 86400 seconds");
27078
+ const current = await this.status();
27079
+ const now = Math.floor(Date.now() / 1000);
27080
+ if (current.status !== "active" || current.expiresAt <= now || now + validForSeconds <= current.expiresAt)
27081
+ throw new Error("Renew an active, unexpired offer with a later expiry");
27082
+ return this.client.program.methods.renewBackstop(new anchor2.BN(now + validForSeconds)).accountsPartial({
27083
+ maker: this.client.options.maker,
27084
+ market: this.client.marketAddress,
27085
+ offer: this.id
27086
+ }).rpc({ commitment: "confirmed" });
27087
+ }
27071
27088
  async close() {
27072
27089
  if (!this.client.options.wallet.publicKey.equals(this.client.options.maker))
27073
27090
  throw new Error("Only the maker owner can close its backstop record");
@@ -27270,6 +27287,7 @@ function createCursorStore(genesis, maker, _directory) {
27270
27287
  // src/quote/quote.ts
27271
27288
  import * as anchor4 from "@anchor-lang/core";
27272
27289
  import { DepthError as DepthError2 } from "@depthhq/sdk";
27290
+ import { quoteBounds as quoteBounds2 } from "@depthhq/bindings";
27273
27291
  function name(value) {
27274
27292
  return Object.keys(value)[0] ?? "unknown";
27275
27293
  }
@@ -27359,6 +27377,12 @@ class QuoteHandle {
27359
27377
  }
27360
27378
  if (snapshot.status === "validated" || snapshot.status === "waiting") {
27361
27379
  const market = await this.client.read("Market", this.client.marketAddress);
27380
+ const quote = await this.client.quoteState(this.id);
27381
+ const reference = BigInt(quote.oracleReferenceE8.toString());
27382
+ const price = BigInt(market.oraclePriceE8.toString());
27383
+ const drift = price > reference ? price - reference : reference - price;
27384
+ if (!quote.marketRevision.eq(market.revision) || market.oracleTimestamp.lt(quote.oracleTimestamp) || reference <= 0n || drift * 10000n > reference * BigInt(quoteBounds2(market.config.maxQuoteDeviationBps).driftBps) || quote.expiry.toNumber() <= Math.floor(Date.now() / 1000))
27385
+ return this.cancel();
27362
27386
  const queueState = await this.client.read("QuoteQueue", this.client.quoteQueueAddress);
27363
27387
  if (!queueState.market.equals(this.client.marketAddress)) {
27364
27388
  throw new Error("Quote queue belongs to another market");
@@ -27478,11 +27502,16 @@ class QuoteHandle {
27478
27502
  return this.status();
27479
27503
  }
27480
27504
  async close() {
27481
- if (!this.client.options.wallet.publicKey.equals(this.client.options.maker))
27482
- throw new Error("Only the maker owner can reclaim quote rent");
27483
27505
  const snapshot = await this.status();
27484
27506
  if (!["cancelled", "filled", "rejected"].includes(snapshot.status) || snapshot.attachedOrders > 0)
27485
27507
  throw new Error("Release the quote and settle its orders before closing");
27508
+ const actor = this.client.options.wallet.publicKey;
27509
+ if (!actor.equals(this.client.options.maker)) {
27510
+ const state = await this.client.quoteState(this.id);
27511
+ if (!state.operator.equals(actor))
27512
+ throw new Error("Only the maker or original payer can reclaim quote rent");
27513
+ return this.client.program.methods.closeOperatorQuote().accountsPartial({ operator: actor, quote: this.id }).rpc({ commitment: "confirmed" });
27514
+ }
27486
27515
  return this.client.program.methods.closeQuote().accountsPartial({ maker: this.client.options.maker, quote: this.id }).rpc({ commitment: "confirmed" });
27487
27516
  }
27488
27517
  }
@@ -27515,13 +27544,16 @@ class QuoteReplacement {
27515
27544
  }
27516
27545
  }
27517
27546
  async status() {
27518
- const prior = await this.client.quote.get(this.priorId).status();
27519
- if (prior.replacementNonce !== undefined && prior.replacementNonce !== this.nonce) {
27547
+ const priorAccount = await this.client.options.connection.getAccountInfo(this.priorId, "confirmed");
27548
+ const prior = priorAccount ? await this.client.quote.get(this.priorId).status() : undefined;
27549
+ if (prior?.replacementNonce !== undefined && prior.replacementNonce !== this.nonce) {
27520
27550
  throw new Error("Prior quote is linked to a different successor");
27521
27551
  }
27522
27552
  const account = await this.client.options.connection.getAccountInfo(this.nextId, "confirmed");
27523
27553
  const next = account ? await this.client.quote.get(this.nextId).status() : undefined;
27524
- const stopped = ["cancelled", "filled"].includes(prior.status);
27554
+ if (!prior && !next)
27555
+ throw new Error("Replacement quotes are unavailable");
27556
+ const stopped = !prior || ["cancelled", "filled", "rejected"].includes(prior.status);
27525
27557
  return {
27526
27558
  prior,
27527
27559
  next,
@@ -27529,7 +27561,21 @@ class QuoteReplacement {
27529
27561
  phase: next?.status === "active" || next?.status === "filled" ? "active" : next?.status === "rejected" ? "failed" : next ? "publishing" : stopped ? "ready" : "stopping"
27530
27562
  };
27531
27563
  }
27564
+ async prepare(input) {
27565
+ if (input.clientNonce !== undefined && input.clientNonce !== this.nonce)
27566
+ throw new Error("Replacement terms use a different client nonce");
27567
+ await this.begin();
27568
+ return this.client.quote.publish({
27569
+ ...input,
27570
+ clientNonce: this.nonce
27571
+ });
27572
+ }
27532
27573
  async begin() {
27574
+ if (!await this.client.options.connection.getAccountInfo(this.priorId, "confirmed")) {
27575
+ if (await this.client.options.connection.getAccountInfo(this.nextId, "confirmed"))
27576
+ return;
27577
+ throw new Error("Replacement quotes are unavailable");
27578
+ }
27533
27579
  const prior = await this.client.quoteState(this.priorId);
27534
27580
  const recorded = prior.replacementNonce == null ? undefined : BigInt(prior.replacementNonce.toString());
27535
27581
  if (recorded !== undefined && recorded !== this.nonce) {
@@ -27553,10 +27599,22 @@ class QuoteReplacement {
27553
27599
  throw new RangeError("Replacement timeout or polling interval is invalid");
27554
27600
  const deadline = Date.now() + timeoutMs;
27555
27601
  const previous = this.client.quote.get(this.priorId);
27556
- await this.begin();
27602
+ const next = await this.prepare(input);
27603
+ for (;; ) {
27604
+ const snapshot = await next.status();
27605
+ if (["validated", "waiting", "reserving", "active", "filled"].includes(snapshot.status))
27606
+ break;
27607
+ if (["rejected", "cancelled"].includes(snapshot.status))
27608
+ throw new Error("Successor quote was rejected; prior quote remains available");
27609
+ if (Date.now() >= deadline)
27610
+ throw new Error(`Successor quote ${this.nextId.toBase58()} is still validating`);
27611
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, deadline - Date.now())));
27612
+ }
27613
+ if (!await this.client.options.connection.getAccountInfo(this.priorId, "confirmed"))
27614
+ return next;
27557
27615
  for (;; ) {
27558
27616
  const snapshot = await previous.status();
27559
- if (["cancelled", "filled"].includes(snapshot.status))
27617
+ if (["cancelled", "filled", "rejected"].includes(snapshot.status))
27560
27618
  break;
27561
27619
  if (["active", "closing"].includes(snapshot.status)) {
27562
27620
  if (snapshot.status === "active" || snapshot.attachedOrders === 0)
@@ -27569,14 +27627,56 @@ class QuoteReplacement {
27569
27627
  }
27570
27628
  await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, deadline - Date.now())));
27571
27629
  }
27572
- const request = { ...input, clientNonce: this.nonce };
27573
- const next = await this.client.quote.publish(request);
27574
27630
  if (!next.id.equals(this.nextId))
27575
27631
  throw new Error("Replacement quote address changed");
27576
27632
  return next;
27577
27633
  }
27578
27634
  }
27579
27635
 
27636
+ // src/quote/cleanup.ts
27637
+ import { ComputeBudgetProgram, Transaction as Transaction2 } from "@solana/web3.js";
27638
+ async function cleanupQuotes(client, options = {}) {
27639
+ const limit = options.limit ?? 12;
27640
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 12)
27641
+ throw new RangeError("Cleanup limit must be between 1 and 12");
27642
+ const excluded = new Set(options.exclude);
27643
+ const actor = client.options.wallet.publicKey;
27644
+ const owner = actor.equals(client.options.maker);
27645
+ const transaction = new Transaction2().add(ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }));
27646
+ let closed = 0;
27647
+ let reclaimedLamports = 0n;
27648
+ let cursor;
27649
+ do {
27650
+ const page = await client.quote.list({ cursor, limit: 100 });
27651
+ for (const item of page.items) {
27652
+ if (excluded.has(item.id.toBase58()))
27653
+ continue;
27654
+ const state = await client.quoteState(item.id);
27655
+ if (!["cancelled", "filled", "rejected"].includes(Object.keys(state.status)[0]) || state.attachedOrders !== 0 || !owner && !state.operator.equals(actor))
27656
+ continue;
27657
+ if (state.replacementNonce != null) {
27658
+ const successor = client.quote.replacement(item.id, BigInt(state.replacementNonce.toString())).nextId;
27659
+ if (!await client.options.connection.getAccountInfo(successor, "confirmed"))
27660
+ continue;
27661
+ }
27662
+ const info = await client.options.connection.getAccountInfo(item.id, "confirmed");
27663
+ if (!info)
27664
+ continue;
27665
+ transaction.add(await (owner ? client.program.methods.closeQuote().accountsPartial({ maker: actor, quote: item.id }) : client.program.methods.closeOperatorQuote().accountsPartial({ operator: actor, quote: item.id })).instruction());
27666
+ reclaimedLamports += BigInt(info.lamports);
27667
+ if (++closed === limit)
27668
+ break;
27669
+ }
27670
+ cursor = closed < limit ? page.nextCursor ?? undefined : undefined;
27671
+ } while (cursor);
27672
+ if (!closed)
27673
+ return { closed, reclaimedLamports };
27674
+ const signature = await client.provider.sendAndConfirm(transaction, [], {
27675
+ commitment: "confirmed"
27676
+ });
27677
+ return { closed, reclaimedLamports, signature };
27678
+ }
27679
+
27580
27680
  // src/client/client.ts
27581
27681
  function pda(seeds) {
27582
27682
  return PublicKey7.findProgramAddressSync(seeds, DEPTH_PROGRAM_ID4)[0];
@@ -27595,7 +27695,7 @@ async function retryQuote(action, deadline) {
27595
27695
  try {
27596
27696
  return await action();
27597
27697
  } catch (error) {
27598
- if (!(error instanceof anchor6.AnchorError) || error.error.errorCode.code !== "WrongMarketAccountStatus" || Date.now() >= deadline)
27698
+ if (!(error instanceof anchor6.AnchorError) || !["WrongMarketAccountStatus", "StaleMarketSnapshot"].includes(error.error.errorCode.code) || Date.now() >= deadline)
27599
27699
  throw error;
27600
27700
  await new Promise((resolve) => setTimeout(resolve, 1000));
27601
27701
  }
@@ -27828,6 +27928,7 @@ class MakerClient {
27828
27928
  }
27829
27929
  };
27830
27930
  quote = {
27931
+ cleanup: (options) => cleanupQuotes(this, options),
27831
27932
  replacement: (prior, clientNonce) => new QuoteReplacement(this, prior, clientNonce),
27832
27933
  publish: async (request) => {
27833
27934
  const clientNonce = request.clientNonce ?? BigInt(new anchor6.BN(randomBytes3(8), "le").toString());
@@ -28122,6 +28223,8 @@ class MakerClient {
28122
28223
  };
28123
28224
  validateQuote(input, market) {
28124
28225
  const now = Math.floor(Date.now() / 1000);
28226
+ const reference = BigInt(market.oraclePriceE8.toString());
28227
+ const { preparationBps } = quoteBounds3(market.config.maxQuoteDeviationBps);
28125
28228
  if (!["draft", "active"].includes(stateName(market.status)))
28126
28229
  throw new Error("Market does not accept quotes");
28127
28230
  if (input.tiers.length === 0 || input.tiers.length > 4) {
@@ -28137,6 +28240,8 @@ class MakerClient {
28137
28240
  if (tier.bidE8 === 0n || tier.askE8 === 0n || tier.bidE8 >= tier.askE8) {
28138
28241
  throw new Error("Quote bid and ask are invalid");
28139
28242
  }
28243
+ if (reference <= 0n || [tier.bidE8, tier.askE8].some((price) => (price > reference ? price - reference : reference - price) * 10000n > reference * BigInt(preparationBps)))
28244
+ throw new Error("Quote prices exceed the preparation deviation limit");
28140
28245
  });
28141
28246
  u642(input.maxCapacity, "maxCapacity");
28142
28247
  if (input.maxCapacity < input.tiers.at(-1).size || input.maxCapacity > BigInt(market.config.maxAccountNotional.toString()) || input.maxCapacity > BigInt(market.config.maxMarketNotional.toString())) {
@@ -24906,7 +24906,8 @@ import {
24906
24906
  marketAddress,
24907
24907
  quoteQueueAddress,
24908
24908
  protocolAddress,
24909
- SOL_USD_MARKET_ID as SOL_USD_MARKET_ID2
24909
+ SOL_USD_MARKET_ID as SOL_USD_MARKET_ID2,
24910
+ quoteBounds as quoteBounds3
24910
24911
  } from "@depthhq/bindings";
24911
24912
  import { DepthView } from "@depthhq/sdk/views";
24912
24913
  import { DepthRecovery } from "@depthhq/sdk/recovery";
@@ -25117,6 +25118,7 @@ import { PublicKey as PublicKey2 } from "@solana/web3.js";
25117
25118
 
25118
25119
  // src/bot/policy.ts
25119
25120
  import { positionPnl } from "@depthhq/sdk";
25121
+ import { quoteBounds } from "@depthhq/bindings";
25120
25122
 
25121
25123
  // src/hedge/policy.ts
25122
25124
  function validateHedgePolicy(policy) {
@@ -25585,6 +25587,21 @@ class BackstopHandle {
25585
25587
  });
25586
25588
  return new BackstopRelease(this, signature);
25587
25589
  }
25590
+ async renew(validForSeconds = 86400) {
25591
+ if (!this.client.options.wallet.publicKey.equals(this.client.options.maker))
25592
+ throw new Error("Only the maker owner can renew backstop capacity");
25593
+ if (!Number.isSafeInteger(validForSeconds) || validForSeconds < 1 || validForSeconds > 86400)
25594
+ throw new RangeError("Backstop duration must be from 1 to 86400 seconds");
25595
+ const current = await this.status();
25596
+ const now = Math.floor(Date.now() / 1000);
25597
+ if (current.status !== "active" || current.expiresAt <= now || now + validForSeconds <= current.expiresAt)
25598
+ throw new Error("Renew an active, unexpired offer with a later expiry");
25599
+ return this.client.program.methods.renewBackstop(new anchor2.BN(now + validForSeconds)).accountsPartial({
25600
+ maker: this.client.options.maker,
25601
+ market: this.client.marketAddress,
25602
+ offer: this.id
25603
+ }).rpc({ commitment: "confirmed" });
25604
+ }
25588
25605
  async close() {
25589
25606
  if (!this.client.options.wallet.publicKey.equals(this.client.options.maker))
25590
25607
  throw new Error("Only the maker owner can close its backstop record");
@@ -25789,6 +25806,7 @@ function createCursorStore(genesis, maker, directory) {
25789
25806
  // src/quote/quote.ts
25790
25807
  import * as anchor4 from "@anchor-lang/core";
25791
25808
  import { DepthError as DepthError2 } from "@depthhq/sdk";
25809
+ import { quoteBounds as quoteBounds2 } from "@depthhq/bindings";
25792
25810
  function name(value) {
25793
25811
  return Object.keys(value)[0] ?? "unknown";
25794
25812
  }
@@ -25878,6 +25896,12 @@ class QuoteHandle {
25878
25896
  }
25879
25897
  if (snapshot.status === "validated" || snapshot.status === "waiting") {
25880
25898
  const market = await this.client.read("Market", this.client.marketAddress);
25899
+ const quote = await this.client.quoteState(this.id);
25900
+ const reference = BigInt(quote.oracleReferenceE8.toString());
25901
+ const price = BigInt(market.oraclePriceE8.toString());
25902
+ const drift = price > reference ? price - reference : reference - price;
25903
+ if (!quote.marketRevision.eq(market.revision) || market.oracleTimestamp.lt(quote.oracleTimestamp) || reference <= 0n || drift * 10000n > reference * BigInt(quoteBounds2(market.config.maxQuoteDeviationBps).driftBps) || quote.expiry.toNumber() <= Math.floor(Date.now() / 1000))
25904
+ return this.cancel();
25881
25905
  const queueState = await this.client.read("QuoteQueue", this.client.quoteQueueAddress);
25882
25906
  if (!queueState.market.equals(this.client.marketAddress)) {
25883
25907
  throw new Error("Quote queue belongs to another market");
@@ -25997,11 +26021,16 @@ class QuoteHandle {
25997
26021
  return this.status();
25998
26022
  }
25999
26023
  async close() {
26000
- if (!this.client.options.wallet.publicKey.equals(this.client.options.maker))
26001
- throw new Error("Only the maker owner can reclaim quote rent");
26002
26024
  const snapshot = await this.status();
26003
26025
  if (!["cancelled", "filled", "rejected"].includes(snapshot.status) || snapshot.attachedOrders > 0)
26004
26026
  throw new Error("Release the quote and settle its orders before closing");
26027
+ const actor = this.client.options.wallet.publicKey;
26028
+ if (!actor.equals(this.client.options.maker)) {
26029
+ const state = await this.client.quoteState(this.id);
26030
+ if (!state.operator.equals(actor))
26031
+ throw new Error("Only the maker or original payer can reclaim quote rent");
26032
+ return this.client.program.methods.closeOperatorQuote().accountsPartial({ operator: actor, quote: this.id }).rpc({ commitment: "confirmed" });
26033
+ }
26005
26034
  return this.client.program.methods.closeQuote().accountsPartial({ maker: this.client.options.maker, quote: this.id }).rpc({ commitment: "confirmed" });
26006
26035
  }
26007
26036
  }
@@ -26036,13 +26065,16 @@ class QuoteReplacement {
26036
26065
  }
26037
26066
  }
26038
26067
  async status() {
26039
- const prior = await this.client.quote.get(this.priorId).status();
26040
- if (prior.replacementNonce !== undefined && prior.replacementNonce !== this.nonce) {
26068
+ const priorAccount = await this.client.options.connection.getAccountInfo(this.priorId, "confirmed");
26069
+ const prior = priorAccount ? await this.client.quote.get(this.priorId).status() : undefined;
26070
+ if (prior?.replacementNonce !== undefined && prior.replacementNonce !== this.nonce) {
26041
26071
  throw new Error("Prior quote is linked to a different successor");
26042
26072
  }
26043
26073
  const account = await this.client.options.connection.getAccountInfo(this.nextId, "confirmed");
26044
26074
  const next = account ? await this.client.quote.get(this.nextId).status() : undefined;
26045
- const stopped = ["cancelled", "filled"].includes(prior.status);
26075
+ if (!prior && !next)
26076
+ throw new Error("Replacement quotes are unavailable");
26077
+ const stopped = !prior || ["cancelled", "filled", "rejected"].includes(prior.status);
26046
26078
  return {
26047
26079
  prior,
26048
26080
  next,
@@ -26050,7 +26082,21 @@ class QuoteReplacement {
26050
26082
  phase: next?.status === "active" || next?.status === "filled" ? "active" : next?.status === "rejected" ? "failed" : next ? "publishing" : stopped ? "ready" : "stopping"
26051
26083
  };
26052
26084
  }
26085
+ async prepare(input) {
26086
+ if (input.clientNonce !== undefined && input.clientNonce !== this.nonce)
26087
+ throw new Error("Replacement terms use a different client nonce");
26088
+ await this.begin();
26089
+ return this.client.quote.publish({
26090
+ ...input,
26091
+ clientNonce: this.nonce
26092
+ });
26093
+ }
26053
26094
  async begin() {
26095
+ if (!await this.client.options.connection.getAccountInfo(this.priorId, "confirmed")) {
26096
+ if (await this.client.options.connection.getAccountInfo(this.nextId, "confirmed"))
26097
+ return;
26098
+ throw new Error("Replacement quotes are unavailable");
26099
+ }
26054
26100
  const prior = await this.client.quoteState(this.priorId);
26055
26101
  const recorded = prior.replacementNonce == null ? undefined : BigInt(prior.replacementNonce.toString());
26056
26102
  if (recorded !== undefined && recorded !== this.nonce) {
@@ -26074,10 +26120,22 @@ class QuoteReplacement {
26074
26120
  throw new RangeError("Replacement timeout or polling interval is invalid");
26075
26121
  const deadline = Date.now() + timeoutMs;
26076
26122
  const previous = this.client.quote.get(this.priorId);
26077
- await this.begin();
26123
+ const next = await this.prepare(input);
26124
+ for (;; ) {
26125
+ const snapshot = await next.status();
26126
+ if (["validated", "waiting", "reserving", "active", "filled"].includes(snapshot.status))
26127
+ break;
26128
+ if (["rejected", "cancelled"].includes(snapshot.status))
26129
+ throw new Error("Successor quote was rejected; prior quote remains available");
26130
+ if (Date.now() >= deadline)
26131
+ throw new Error(`Successor quote ${this.nextId.toBase58()} is still validating`);
26132
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, deadline - Date.now())));
26133
+ }
26134
+ if (!await this.client.options.connection.getAccountInfo(this.priorId, "confirmed"))
26135
+ return next;
26078
26136
  for (;; ) {
26079
26137
  const snapshot = await previous.status();
26080
- if (["cancelled", "filled"].includes(snapshot.status))
26138
+ if (["cancelled", "filled", "rejected"].includes(snapshot.status))
26081
26139
  break;
26082
26140
  if (["active", "closing"].includes(snapshot.status)) {
26083
26141
  if (snapshot.status === "active" || snapshot.attachedOrders === 0)
@@ -26090,14 +26148,56 @@ class QuoteReplacement {
26090
26148
  }
26091
26149
  await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, deadline - Date.now())));
26092
26150
  }
26093
- const request = { ...input, clientNonce: this.nonce };
26094
- const next = await this.client.quote.publish(request);
26095
26151
  if (!next.id.equals(this.nextId))
26096
26152
  throw new Error("Replacement quote address changed");
26097
26153
  return next;
26098
26154
  }
26099
26155
  }
26100
26156
 
26157
+ // src/quote/cleanup.ts
26158
+ import { ComputeBudgetProgram, Transaction as Transaction2 } from "@solana/web3.js";
26159
+ async function cleanupQuotes(client, options = {}) {
26160
+ const limit = options.limit ?? 12;
26161
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 12)
26162
+ throw new RangeError("Cleanup limit must be between 1 and 12");
26163
+ const excluded = new Set(options.exclude);
26164
+ const actor = client.options.wallet.publicKey;
26165
+ const owner = actor.equals(client.options.maker);
26166
+ const transaction = new Transaction2().add(ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }));
26167
+ let closed = 0;
26168
+ let reclaimedLamports = 0n;
26169
+ let cursor;
26170
+ do {
26171
+ const page = await client.quote.list({ cursor, limit: 100 });
26172
+ for (const item of page.items) {
26173
+ if (excluded.has(item.id.toBase58()))
26174
+ continue;
26175
+ const state = await client.quoteState(item.id);
26176
+ if (!["cancelled", "filled", "rejected"].includes(Object.keys(state.status)[0]) || state.attachedOrders !== 0 || !owner && !state.operator.equals(actor))
26177
+ continue;
26178
+ if (state.replacementNonce != null) {
26179
+ const successor = client.quote.replacement(item.id, BigInt(state.replacementNonce.toString())).nextId;
26180
+ if (!await client.options.connection.getAccountInfo(successor, "confirmed"))
26181
+ continue;
26182
+ }
26183
+ const info = await client.options.connection.getAccountInfo(item.id, "confirmed");
26184
+ if (!info)
26185
+ continue;
26186
+ transaction.add(await (owner ? client.program.methods.closeQuote().accountsPartial({ maker: actor, quote: item.id }) : client.program.methods.closeOperatorQuote().accountsPartial({ operator: actor, quote: item.id })).instruction());
26187
+ reclaimedLamports += BigInt(info.lamports);
26188
+ if (++closed === limit)
26189
+ break;
26190
+ }
26191
+ cursor = closed < limit ? page.nextCursor ?? undefined : undefined;
26192
+ } while (cursor);
26193
+ if (!closed)
26194
+ return { closed, reclaimedLamports };
26195
+ const signature = await client.provider.sendAndConfirm(transaction, [], {
26196
+ commitment: "confirmed"
26197
+ });
26198
+ return { closed, reclaimedLamports, signature };
26199
+ }
26200
+
26101
26201
  // src/client/client.ts
26102
26202
  function pda(seeds) {
26103
26203
  return PublicKey7.findProgramAddressSync(seeds, DEPTH_PROGRAM_ID4)[0];
@@ -26116,7 +26216,7 @@ async function retryQuote(action, deadline) {
26116
26216
  try {
26117
26217
  return await action();
26118
26218
  } catch (error) {
26119
- if (!(error instanceof anchor6.AnchorError) || error.error.errorCode.code !== "WrongMarketAccountStatus" || Date.now() >= deadline)
26219
+ if (!(error instanceof anchor6.AnchorError) || !["WrongMarketAccountStatus", "StaleMarketSnapshot"].includes(error.error.errorCode.code) || Date.now() >= deadline)
26120
26220
  throw error;
26121
26221
  await new Promise((resolve) => setTimeout(resolve, 1000));
26122
26222
  }
@@ -26349,6 +26449,7 @@ class MakerClient {
26349
26449
  }
26350
26450
  };
26351
26451
  quote = {
26452
+ cleanup: (options) => cleanupQuotes(this, options),
26352
26453
  replacement: (prior, clientNonce) => new QuoteReplacement(this, prior, clientNonce),
26353
26454
  publish: async (request) => {
26354
26455
  const clientNonce = request.clientNonce ?? BigInt(new anchor6.BN(randomBytes3(8), "le").toString());
@@ -26643,6 +26744,8 @@ class MakerClient {
26643
26744
  };
26644
26745
  validateQuote(input, market) {
26645
26746
  const now = Math.floor(Date.now() / 1000);
26747
+ const reference = BigInt(market.oraclePriceE8.toString());
26748
+ const { preparationBps } = quoteBounds3(market.config.maxQuoteDeviationBps);
26646
26749
  if (!["draft", "active"].includes(stateName(market.status)))
26647
26750
  throw new Error("Market does not accept quotes");
26648
26751
  if (input.tiers.length === 0 || input.tiers.length > 4) {
@@ -26658,6 +26761,8 @@ class MakerClient {
26658
26761
  if (tier.bidE8 === 0n || tier.askE8 === 0n || tier.bidE8 >= tier.askE8) {
26659
26762
  throw new Error("Quote bid and ask are invalid");
26660
26763
  }
26764
+ if (reference <= 0n || [tier.bidE8, tier.askE8].some((price) => (price > reference ? price - reference : reference - price) * 10000n > reference * BigInt(preparationBps)))
26765
+ throw new Error("Quote prices exceed the preparation deviation limit");
26661
26766
  });
26662
26767
  u642(input.maxCapacity, "maxCapacity");
26663
26768
  if (input.maxCapacity < input.tiers.at(-1).size || input.maxCapacity > BigInt(market.config.maxAccountNotional.toString()) || input.maxCapacity > BigInt(market.config.maxMarketNotional.toString())) {
package/dist/node.js CHANGED
@@ -144,14 +144,23 @@ class BotStore {
144
144
  throw new Error("Maker bot checkpoint belongs to another wallet or network");
145
145
  if (state.referencePrice !== undefined && (typeof state.referencePrice !== "bigint" || state.referencePrice <= 0n))
146
146
  throw new Error("Maker bot checkpoint price is invalid");
147
- for (const value of [state.request?.clientNonce, state.replacement?.nonce])
147
+ for (const value of [
148
+ state.request?.clientNonce,
149
+ state.replacement?.nonce,
150
+ state.replacement?.request?.clientNonce
151
+ ])
148
152
  if (value !== undefined && (typeof value !== "bigint" || value < 0n || value >= 1n << 64n))
149
153
  throw new Error("Maker bot checkpoint nonce is invalid");
150
154
  for (const value of [state.quote, state.replacement?.prior])
151
155
  if (value !== undefined && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
152
156
  throw new Error("Maker bot checkpoint quote is invalid");
153
- if (state.request && (!Number.isSafeInteger(state.request.expiresAt) || typeof state.request.maxCapacity !== "bigint" || state.request.maxCapacity <= 0n || !Array.isArray(state.request.tiers) || !state.request.tiers.length))
154
- throw new Error("Maker bot checkpoint request is invalid");
157
+ 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
+ throw new Error("Quote write checkpoint is invalid");
159
+ for (const request of [state.request, state.replacement?.request])
160
+ if (request && (!Number.isSafeInteger(request.expiresAt) || typeof request.maxCapacity !== "bigint" || request.maxCapacity <= 0n || !Array.isArray(request.tiers) || !request.tiers.length))
161
+ throw new Error("Maker bot checkpoint request is invalid");
162
+ if (state.replacement?.request && state.replacement.request.clientNonce !== state.replacement.nonce)
163
+ throw new Error("Replacement checkpoint nonce is invalid");
155
164
  for (const fill of Object.values(state.fills))
156
165
  if (typeof fill.signature !== "string" || !Number.isSafeInteger(fill.slot) || fill.slot < 0)
157
166
  throw new Error("Maker bot checkpoint fill is invalid");
@@ -197,6 +206,7 @@ class BotStore {
197
206
 
198
207
  // src/bot/policy.ts
199
208
  import { positionPnl } from "@depthhq/sdk";
209
+ import { quoteBounds } from "@depthhq/bindings";
200
210
 
201
211
  // src/hedge/policy.ts
202
212
  function validateHedgePolicy(policy) {
@@ -312,7 +322,8 @@ function botQuote(policy, market, position, nonce, now, pricing) {
312
322
  if (!tiers.length)
313
323
  return;
314
324
  const half = Math.ceil(policy.spreadBps / 2);
315
- if (half > market.config.maxQuoteDeviationBps)
325
+ const preparation = quoteBounds(market.config.maxQuoteDeviationBps).preparationBps;
326
+ if (half > preparation)
316
327
  throw new Error("Maker spread exceeds oracle deviation policy");
317
328
  if (pricing)
318
329
  validateBook(pricing.book, now * 1000, pricing.maxPriceAgeMs);
@@ -333,7 +344,7 @@ function botQuote(policy, market, position, nonce, now, pricing) {
333
344
  const buy = pricing ? executionPrice(pricing.book.asks, units) : price;
334
345
  const bidE8 = sell * (10000n - bidCosts) / 10000n + skew;
335
346
  const askE8 = (buy * (10000n + askCosts) + 9999n) / 10000n + skew;
336
- const bound = price * BigInt(market.config.maxQuoteDeviationBps);
347
+ const bound = price * BigInt(preparation);
337
348
  if (bidE8 < 100000000n || bidE8 >= askE8 || abs(bidE8 - price) * 10000n > bound || abs(askE8 - price) * 10000n > bound)
338
349
  throw new Error("Executable maker prices exceed oracle deviation bounds");
339
350
  return { size, bidE8, askE8 };
@@ -364,6 +375,9 @@ class MakerBot {
364
375
  pricing;
365
376
  hedgeLease;
366
377
  pricingDirty = false;
378
+ lastCleanup = 0;
379
+ lastFills = 0;
380
+ inventoryVersion;
367
381
  constructor(maker, policy, store, hedge) {
368
382
  this.maker = maker;
369
383
  this.policy = policy;
@@ -426,18 +440,50 @@ class MakerBot {
426
440
  return;
427
441
  const account = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
428
442
  const confirmed = await this.maker.read("MarketAccount", this.maker.marketAccount());
443
+ const write = this.state.quoteWrite;
444
+ if (!this.state.hedge?.pending && (("reserving" in confirmed.status) || ("releasing" in confirmed.status))) {
445
+ const ids = [
446
+ this.state.quote,
447
+ write?.quote,
448
+ this.state.replacement ? this.maker.quote.replacement(new PublicKey(this.state.replacement.prior), this.state.replacement.nonce).nextId.toBase58() : undefined
449
+ ];
450
+ for (const id of new Set(ids.filter((id2) => Boolean(id2)))) {
451
+ const quote = await this.maker.quoteState(new PublicKey(id));
452
+ if ((("reserving" in quote.status) || ("releasing" in quote.status)) && quote.pendingComputation?.equals(confirmed.pendingComputation ?? PublicKey.default))
453
+ return "updating quote margin";
454
+ }
455
+ }
429
456
  let ready = "ready" in account.status && "ready" in confirmed.status && account.version.toString() === confirmed.version.toString();
430
- if (ready && (!this.position || this.position.version !== BigInt(account.version.toString()) || this.dirty)) {
431
- const prior = this.position?.version;
457
+ const currentVersion = BigInt(confirmed.version.toString());
458
+ 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));
461
+ if (ownCommit) {
462
+ this.inventoryVersion = currentVersion;
463
+ ready = true;
464
+ if (account.version.toString() === confirmed.version.toString()) {
465
+ this.state.quoteWrite = undefined;
466
+ await this.store.save(this.state);
467
+ }
468
+ }
469
+ if (ready && (!this.position || (this.inventoryVersion ?? this.position.version) !== currentVersion)) {
470
+ const prior = this.position;
432
471
  this.position = await this.maker.position.summary({ timeoutMs: 180000 });
433
472
  const fresh = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
434
473
  const latest = await this.maker.read("MarketAccount", this.maker.marketAccount());
435
474
  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());
436
475
  if (ready) {
437
- this.pricingDirty ||= prior !== this.position.version || this.dirty;
476
+ this.inventoryVersion = this.position.version;
477
+ if (write && this.position.version >= write.version) {
478
+ this.state.quoteWrite = undefined;
479
+ await this.store.save(this.state);
480
+ }
481
+ this.pricingDirty ||= prior !== undefined && (prior.position !== this.position.position || prior.collateral !== this.position.collateral);
438
482
  this.dirty = false;
439
483
  }
440
484
  }
485
+ if (ready)
486
+ this.dirty = false;
441
487
  const snapshot = await this.hedge.tick(this.state.hedge, () => this.saveHedge(), ready ? this.position?.position : undefined);
442
488
  if (snapshot.phase === "pending" || snapshot.phase === "blocked") {
443
489
  await this.cancel(true);
@@ -460,6 +506,20 @@ class MakerBot {
460
506
  maxPriceAgeMs: this.hedge.policy.maxPriceAgeMs
461
507
  };
462
508
  }
509
+ async quoteWrite(handle, release = false) {
510
+ const before = await this.maker.read("MarketAccount", this.maker.marketAccount());
511
+ const version = BigInt(before.version.toString());
512
+ const result = release ? await handle.cancel() : await handle.activate({ timeoutMs: 0 });
513
+ if ((release ? handle.releaseSignature : handle.reserveSignature) && "ready" in before.status && version === (this.inventoryVersion ?? this.position?.version)) {
514
+ this.state.quoteWrite = {
515
+ quote: handle.id.toBase58(),
516
+ version,
517
+ release
518
+ };
519
+ await this.store.save(this.state);
520
+ }
521
+ return result;
522
+ }
463
523
  snapshot(phase, reason) {
464
524
  return {
465
525
  phase,
@@ -483,6 +543,31 @@ class MakerBot {
483
543
  }
484
544
  async reconcile() {
485
545
  const state = this.state;
546
+ if (state.quote && !await this.maker.options.connection.getAccountInfo(new PublicKey(state.quote), "confirmed")) {
547
+ if (!state.request || state.request.expiresAt + 180 < Math.floor(Date.now() / 1000)) {
548
+ state.quote = undefined;
549
+ state.request = undefined;
550
+ state.referencePrice = undefined;
551
+ await this.store.save(state);
552
+ }
553
+ }
554
+ if (state.replacement) {
555
+ const flow = this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce);
556
+ const [prior, next] = await Promise.all([
557
+ this.maker.options.connection.getAccountInfo(new PublicKey(state.replacement.prior), "confirmed"),
558
+ this.maker.options.connection.getAccountInfo(flow.nextId, "confirmed")
559
+ ]);
560
+ if (!prior && next) {
561
+ state.quote = flow.nextId.toBase58();
562
+ state.request = state.replacement.request;
563
+ state.referencePrice = state.replacement.referencePrice;
564
+ state.replacement = undefined;
565
+ await this.store.save(state);
566
+ } else if (!prior && !next && (!state.replacement.request || state.replacement.request.expiresAt + 180 < Math.floor(Date.now() / 1000))) {
567
+ state.replacement = undefined;
568
+ await this.store.save(state);
569
+ }
570
+ }
486
571
  let cursor;
487
572
  const managed = [];
488
573
  do {
@@ -497,6 +582,7 @@ class MakerBot {
497
582
  const allowed = new Set([
498
583
  state.quote,
499
584
  state.replacement?.prior,
585
+ state.replacement ? this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).nextId.toBase58() : undefined,
500
586
  this.requestId()?.toBase58()
501
587
  ].filter(Boolean));
502
588
  for (const id of managed)
@@ -507,6 +593,8 @@ class MakerBot {
507
593
  }
508
594
  }
509
595
  async fills() {
596
+ if (Date.now() - this.lastFills < 1e4)
597
+ return;
510
598
  const state = this.state;
511
599
  let before;
512
600
  let head;
@@ -532,9 +620,28 @@ class MakerBot {
532
620
  state.cursor = head;
533
621
  await this.store.save(state);
534
622
  }
623
+ this.lastFills = Date.now();
535
624
  }
536
625
  async cancel(preserve = false) {
537
626
  const state = this.state;
627
+ let successorPending = false;
628
+ if (state.replacement?.request) {
629
+ const next = this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).nextId;
630
+ if (await this.maker.options.connection.getAccountInfo(next, "confirmed")) {
631
+ const handle2 = this.maker.quote.get(next);
632
+ const snapshot = await handle2.status();
633
+ if (!terminal.has(snapshot.status)) {
634
+ const result = [
635
+ "active",
636
+ "waiting",
637
+ "validated",
638
+ "validating",
639
+ "closing"
640
+ ].includes(snapshot.status) ? await this.quoteWrite(handle2, true) : snapshot;
641
+ successorPending = !terminal.has(result.status);
642
+ }
643
+ }
644
+ }
538
645
  if (!state.quote) {
539
646
  const request = this.requestId();
540
647
  if (request && await this.maker.options.connection.getAccountInfo(request, "confirmed"))
@@ -552,19 +659,73 @@ class MakerBot {
552
659
  const handle = this.maker.quote.get(new PublicKey(state.quote));
553
660
  const quote = await handle.status();
554
661
  if (terminal.has(quote.status)) {
662
+ if (successorPending)
663
+ return false;
555
664
  state.quote = undefined;
556
665
  state.request = undefined;
557
666
  state.referencePrice = undefined;
558
667
  if (!preserve)
559
668
  state.replacement = undefined;
560
- this.dirty = true;
561
669
  await this.store.save(state);
562
670
  return true;
563
671
  }
564
672
  if (["active", "waiting", "validated", "validating", "closing"].includes(quote.status))
565
- await handle.cancel();
673
+ await this.quoteWrite(handle, true);
566
674
  return false;
567
675
  }
676
+ async replace(market, now) {
677
+ const state = this.state;
678
+ const replacement = state.replacement;
679
+ const flow = this.maker.quote.replacement(new PublicKey(replacement.prior), replacement.nonce);
680
+ if (!replacement.request) {
681
+ const account = await this.maker.read("MarketAccount", this.maker.marketAccount());
682
+ if (!("ready" in account.status) || market.pendingOrders)
683
+ return this.snapshot("pending", "settlement in progress");
684
+ if (!this.position || this.position.version !== BigInt(account.version.toString())) {
685
+ this.position = await this.maker.position.summary({
686
+ timeoutMs: 180000
687
+ });
688
+ if (this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString()))
689
+ return this.snapshot("pending", "maker view changed");
690
+ }
691
+ this.inventoryVersion = this.position.version;
692
+ this.dirty = false;
693
+ replacement.request = botQuote(this.policy, market, this.position, replacement.nonce, now, this.pricing);
694
+ if (!replacement.request)
695
+ return this.snapshot("pending", "inventory or collateral limit");
696
+ replacement.referencePrice = BigInt(market.oraclePriceE8.toString());
697
+ await this.store.save(state);
698
+ }
699
+ await flow.begin();
700
+ const exists = await this.maker.options.connection.getAccountInfo(flow.nextId, "confirmed");
701
+ const next = exists ? this.maker.quote.get(flow.nextId) : await this.maker.quote.publish(replacement.request);
702
+ const snapshot = await next.status();
703
+ if (snapshot.status === "validating" && replacement.request.expiresAt > now)
704
+ return this.snapshot("pending", "validating successor quote");
705
+ if (["rejected", "cancelled"].includes(snapshot.status) || replacement.request.expiresAt <= now) {
706
+ await this.cancel();
707
+ return this.snapshot("pending", "releasing expired successor");
708
+ }
709
+ const prior = this.maker.quote.get(new PublicKey(replacement.prior));
710
+ const previous = await prior.status();
711
+ if (!terminal.has(previous.status) && ["validated", "waiting", "reserving"].includes(snapshot.status) && market.activeQuoteCount < 4 && BigInt(market.quotedNotional.toString()) + snapshot.maxCapacity <= BigInt(market.config.maxMarketNotional.toString())) {
712
+ await this.quoteWrite(next);
713
+ return this.snapshot("pending", "activating successor quote");
714
+ }
715
+ if (!terminal.has(previous.status)) {
716
+ if (["active", "waiting", "validated", "validating", "closing"].includes(previous.status))
717
+ await this.quoteWrite(prior, true);
718
+ return this.snapshot("pending", "releasing prior quote");
719
+ }
720
+ state.quote = flow.nextId.toBase58();
721
+ state.request = replacement.request;
722
+ state.referencePrice = replacement.referencePrice;
723
+ state.replacement = undefined;
724
+ this.pricingDirty = false;
725
+ await this.store.save(state);
726
+ await this.quoteWrite(next);
727
+ return this.snapshot("pending", "activating successor quote");
728
+ }
568
729
  async tick() {
569
730
  if (this.busy)
570
731
  throw new Error("Maker bot tick is already running");
@@ -573,6 +734,18 @@ class MakerBot {
573
734
  this.busy = true;
574
735
  try {
575
736
  const state = this.state;
737
+ if (Date.now() - this.lastCleanup >= 60000) {
738
+ await this.maker.quote.cleanup({
739
+ exclude: [
740
+ state.quoteWrite?.quote,
741
+ state.quote,
742
+ state.replacement?.prior,
743
+ this.requestId()?.toBase58(),
744
+ state.replacement ? this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).nextId.toBase58() : undefined
745
+ ].filter((id) => Boolean(id))
746
+ });
747
+ this.lastCleanup = Date.now();
748
+ }
576
749
  let [market, lamports] = await Promise.all([
577
750
  this.maker.read("Market", this.maker.marketAddress),
578
751
  this.maker.options.connection.getBalance(this.maker.options.wallet.publicKey, "confirmed")
@@ -600,6 +773,8 @@ class MakerBot {
600
773
  return this.snapshot(stopped ? "stopped" : "pending", changed);
601
774
  }
602
775
  }
776
+ if (state.replacement)
777
+ return this.replace(market, now);
603
778
  if (!state.quote && this.requestId() && await this.maker.options.connection.getAccountInfo(this.requestId(), "confirmed")) {
604
779
  state.quote = this.requestId().toBase58();
605
780
  await this.store.save(state);
@@ -610,20 +785,13 @@ class MakerBot {
610
785
  if (terminal.has(quote.status)) {
611
786
  state.quote = undefined;
612
787
  state.request = undefined;
613
- this.dirty = true;
614
788
  await this.store.save(state);
615
- } else if (state.replacement) {
616
- if (!["cancelled", "filled"].includes(quote.status)) {
617
- await this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).begin();
618
- await this.cancel(true);
619
- return this.snapshot("pending", "releasing prior quote");
620
- }
621
789
  } else if (["validating", "validated", "waiting", "reserving"].includes(quote.status)) {
622
790
  const current = await this.maker.quoteState(handle2.id);
623
791
  if (current.expiry.toNumber() <= now && ["validating", "validated", "waiting"].includes(quote.status))
624
792
  await this.cancel();
625
793
  else
626
- await handle2.activate({ timeoutMs: 0 });
794
+ await this.quoteWrite(handle2);
627
795
  return this.snapshot("pending", "activating quote");
628
796
  } else if (["closing", "releasing"].includes(quote.status)) {
629
797
  await this.cancel();
@@ -640,7 +808,7 @@ class MakerBot {
640
808
  this.pricingDirty = false;
641
809
  await this.store.save(state);
642
810
  await this.maker.quote.replacement(handle2.id, state.replacement.nonce).begin();
643
- await this.cancel(true);
811
+ await this.replace(market, now);
644
812
  return this.snapshot("pending", "repricing quote");
645
813
  }
646
814
  }
@@ -659,7 +827,7 @@ class MakerBot {
659
827
  return this.snapshot("idle", "maker view refreshed");
660
828
  }
661
829
  if (!state.request) {
662
- const request2 = botQuote(this.policy, market, this.position, state.replacement?.nonce ?? nonce(), now, this.pricing);
830
+ const request2 = botQuote(this.policy, market, this.position, nonce(), now, this.pricing);
663
831
  if (!request2)
664
832
  return this.snapshot("idle", "inventory or collateral limit");
665
833
  state.request = request2;
@@ -673,14 +841,13 @@ class MakerBot {
673
841
  await this.store.save(state);
674
842
  return this.snapshot("idle", "prepared quote expired");
675
843
  }
676
- const handle = state.replacement ? await this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).resume(request, { timeoutMs: 0 }) : await this.maker.quote.publish(request);
844
+ const handle = await this.maker.quote.publish(request);
677
845
  state.quote = handle.id.toBase58();
678
846
  state.replacement = undefined;
679
847
  await this.store.save(state);
680
- await handle.activate({ timeoutMs: 0 });
848
+ await this.quoteWrite(handle);
681
849
  return this.snapshot("pending", "quote submitted");
682
850
  } catch (error) {
683
- this.dirty = true;
684
851
  try {
685
852
  await this.cancel(true);
686
853
  } catch (cause) {
@@ -0,0 +1,9 @@
1
+ import type { MakerClient } from "../client/client.js";
2
+ export declare function cleanupQuotes(client: MakerClient, options?: {
3
+ limit?: number;
4
+ exclude?: string[];
5
+ }): Promise<{
6
+ closed: number;
7
+ reclaimedLamports: bigint;
8
+ signature?: string;
9
+ }>;
@@ -3,7 +3,7 @@ import type { MakerClient } from "../client/client.js";
3
3
  import type { QuoteRequest } from "../client/types.js";
4
4
  import type { QuoteHandle, QuoteSnapshot } from "./quote.js";
5
5
  export type ReplacementSnapshot = {
6
- prior: QuoteSnapshot;
6
+ prior?: QuoteSnapshot;
7
7
  next?: QuoteSnapshot;
8
8
  nextId: PublicKey;
9
9
  phase: "stopping" | "ready" | "publishing" | "active" | "failed";
@@ -15,6 +15,7 @@ export declare class QuoteReplacement {
15
15
  readonly nextId: PublicKey;
16
16
  constructor(client: MakerClient, priorId: PublicKey, nonce: bigint);
17
17
  status(): Promise<ReplacementSnapshot>;
18
+ prepare(input: QuoteRequest): Promise<QuoteHandle>;
18
19
  begin(): Promise<void>;
19
20
  resume(input: QuoteRequest, options?: {
20
21
  timeoutMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@depthhq/maker-sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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.2",
33
- "@depthhq/sdk": "0.1.6"
32
+ "@depthhq/bindings": "0.1.3",
33
+ "@depthhq/sdk": "0.1.7"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "node ../../scripts/build-package.mjs maker-sdk",