@depthhq/maker-sdk 0.1.12 → 0.1.14

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;
@@ -39,6 +39,7 @@ export type HedgeReceipt = {
39
39
  status: "filled" | "cancelled" | "rejected" | "open" | "unknown";
40
40
  orderId?: number;
41
41
  filledSize: bigint;
42
+ filledAt?: number;
42
43
  averagePriceE8?: bigint;
43
44
  fee?: bigint;
44
45
  hashes?: string[];
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";
@@ -26418,7 +26419,7 @@ function order(value, receipt = false) {
26418
26419
  return false;
26419
26420
  if (!receipt)
26420
26421
  return true;
26421
- return ["filled", "cancelled", "rejected"].includes(String(value.status)) && integer(value.completedAt) && value.completedAt >= value.nonce && amount(value.filledSize) && BigInt(value.filledSize) >= 0n && BigInt(value.filledSize) <= BigInt(value.size) && optional(value.orderId, integer) && optional(value.averagePriceE8, amount) && optional(value.fee, amount) && optional(value.reason, text) && optional(value.hashes, (hashes) => Array.isArray(hashes) && hashes.length <= 2000 && hashes.every((hash) => typeof hash === "string" && /^0x[0-9a-fA-F]{64}$/.test(hash)));
26422
+ return ["filled", "cancelled", "rejected"].includes(String(value.status)) && integer(value.completedAt) && value.completedAt >= value.nonce && amount(value.filledSize) && BigInt(value.filledSize) >= 0n && BigInt(value.filledSize) <= BigInt(value.size) && (value.filledAt === undefined || integer(value.filledAt) && value.filledAt >= value.nonce - 1000 && value.filledAt <= value.completedAt + 1000) && optional(value.orderId, integer) && optional(value.averagePriceE8, amount) && optional(value.fee, amount) && optional(value.reason, text) && optional(value.hashes, (hashes) => Array.isArray(hashes) && hashes.length <= 2000 && hashes.every((hash) => typeof hash === "string" && /^0x[0-9a-fA-F]{64}$/.test(hash)));
26422
26423
  }
26423
26424
  function hedgeView(value, owner) {
26424
26425
  if (!object(value) || value.maker !== owner || typeof value.enabled !== "boolean")
@@ -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";
@@ -24932,7 +24933,7 @@ function order(value, receipt = false) {
24932
24933
  return false;
24933
24934
  if (!receipt)
24934
24935
  return true;
24935
- return ["filled", "cancelled", "rejected"].includes(String(value.status)) && integer(value.completedAt) && value.completedAt >= value.nonce && amount(value.filledSize) && BigInt(value.filledSize) >= 0n && BigInt(value.filledSize) <= BigInt(value.size) && optional(value.orderId, integer) && optional(value.averagePriceE8, amount) && optional(value.fee, amount) && optional(value.reason, text) && optional(value.hashes, (hashes) => Array.isArray(hashes) && hashes.length <= 2000 && hashes.every((hash) => typeof hash === "string" && /^0x[0-9a-fA-F]{64}$/.test(hash)));
24936
+ return ["filled", "cancelled", "rejected"].includes(String(value.status)) && integer(value.completedAt) && value.completedAt >= value.nonce && amount(value.filledSize) && BigInt(value.filledSize) >= 0n && BigInt(value.filledSize) <= BigInt(value.size) && (value.filledAt === undefined || integer(value.filledAt) && value.filledAt >= value.nonce - 1000 && value.filledAt <= value.completedAt + 1000) && optional(value.orderId, integer) && optional(value.averagePriceE8, amount) && optional(value.fee, amount) && optional(value.reason, text) && optional(value.hashes, (hashes) => Array.isArray(hashes) && hashes.length <= 2000 && hashes.every((hash) => typeof hash === "string" && /^0x[0-9a-fA-F]{64}$/.test(hash)));
24936
24937
  }
24937
24938
  function hedgeView(value, owner) {
24938
24939
  if (!object(value) || value.maker !== owner || typeof value.enabled !== "boolean")
@@ -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])
@@ -175,7 +180,7 @@ class BotStore {
175
180
  if (!/^0x[0-9a-f]{32}$/.test(order.cloid) || !Number.isSafeInteger(order.nonce) || order.nonce < 0 || !Number.isSafeInteger(order.expiresAt) || order.expiresAt <= order.nonce || typeof order.buy !== "boolean" || typeof order.reduceOnly !== "boolean" || typeof order.size !== "bigint" || order.size <= 0n || typeof order.limitPriceE8 !== "bigint" || order.limitPriceE8 <= 0n)
176
181
  throw new Error("Hedge checkpoint order is invalid");
177
182
  for (const record of hedge.history)
178
- if (!["filled", "cancelled", "rejected"].includes(record.status) || typeof record.filledSize !== "bigint" || record.filledSize < 0n || record.filledSize > record.size || !Number.isSafeInteger(record.completedAt) || record.completedAt < record.nonce || record.orderId !== undefined && (!Number.isSafeInteger(record.orderId) || record.orderId < 0))
183
+ if (!["filled", "cancelled", "rejected"].includes(record.status) || typeof record.filledSize !== "bigint" || record.filledSize < 0n || record.filledSize > record.size || !Number.isSafeInteger(record.completedAt) || record.completedAt < record.nonce || record.filledAt !== undefined && (!Number.isSafeInteger(record.filledAt) || record.filledAt < record.nonce - 1000 || record.filledAt > record.completedAt + 1000) || record.orderId !== undefined && (!Number.isSafeInteger(record.orderId) || record.orderId < 0))
179
184
  throw new Error("Hedge checkpoint receipt is invalid");
180
185
  }
181
186
  return state;
@@ -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;
@@ -438,8 +454,10 @@ class MakerBot {
438
454
  async hedgeTick() {
439
455
  if (!this.hedge)
440
456
  return;
441
- const account = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
442
- const confirmed = await this.maker.read("MarketAccount", this.maker.marketAccount());
457
+ const [account, confirmed] = await Promise.all([
458
+ this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized"),
459
+ this.maker.read("MarketAccount", this.maker.marketAccount())
460
+ ]);
443
461
  const write = this.state.quoteWrite;
444
462
  if (!this.state.hedge?.pending && (("reserving" in confirmed.status) || ("releasing" in confirmed.status))) {
445
463
  const ids = [
@@ -456,8 +474,18 @@ class MakerBot {
456
474
  let ready = "ready" in account.status && "ready" in confirmed.status && account.version.toString() === confirmed.version.toString();
457
475
  const currentVersion = BigInt(confirmed.version.toString());
458
476
  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));
477
+ const completedQuote = write && "ready" in confirmed.status && currentVersion === write.version + 1n && await this.maker.quoteState(new PublicKey(write.quote));
478
+ const completedWrite = candidate && completedQuote ? completedQuote.status : undefined;
479
+ const ownCommit = write && completedWrite && (write.release ? "cancelled" in completedWrite || "filled" in completedWrite : ("active" in completedWrite));
480
+ const riskWrite = [
481
+ this.state.quoteRisk,
482
+ this.state.replacement?.risk
483
+ ].find((risk) => risk?.quote === write?.quote);
484
+ if (completedQuote && "active" in completedQuote.status && riskWrite) {
485
+ riskWrite.version = currentVersion;
486
+ riskWrite.balanceNonce = Buffer.from(completedQuote.balanceNonce).toString("hex");
487
+ await this.store.save(this.state);
488
+ }
461
489
  if (ownCommit) {
462
490
  this.inventoryVersion = currentVersion;
463
491
  ready = true;
@@ -466,12 +494,17 @@ class MakerBot {
466
494
  await this.store.save(this.state);
467
495
  }
468
496
  }
469
- if (ready && (!this.position || this.position.status !== "ready" || this.position.position === undefined || (this.inventoryVersion ?? this.position.version) !== currentVersion)) {
497
+ 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
498
  const prior = this.position;
471
- this.position = await this.maker.position.summary({ timeoutMs: 180000 });
472
- const fresh = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
473
- const latest = await this.maker.read("MarketAccount", this.maker.marketAccount());
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());
499
+ this.position = await this.maker.position.summary({ timeoutMs: 0 });
500
+ ready = false;
501
+ if (this.position.status === "ready") {
502
+ const [fresh, latest] = await Promise.all([
503
+ this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized"),
504
+ this.maker.read("MarketAccount", this.maker.marketAccount())
505
+ ]);
506
+ ready = "ready" in fresh.status && "ready" in latest.status && fresh.version.toString() === latest.version.toString() && this.position.version === BigInt(fresh.version.toString());
507
+ }
475
508
  if (ready) {
476
509
  this.inventoryVersion = this.position.version;
477
510
  if (write && this.position.version >= write.version) {
@@ -482,13 +515,53 @@ class MakerBot {
482
515
  this.dirty = false;
483
516
  }
484
517
  }
485
- if (ready)
518
+ if (ready && this.position?.status === "ready") {
519
+ this.pricingDirty ||= this.verifiedPosition !== undefined && (this.verifiedPosition.position !== this.position.position || this.verifiedPosition.collateral !== this.position.collateral);
520
+ this.verifiedPosition = this.position;
486
521
  this.dirty = false;
522
+ }
487
523
  const snapshot = await this.hedge.tick(this.state.hedge, () => this.saveHedge(), ready ? this.position?.position : undefined);
488
524
  if (snapshot.phase === "pending" || snapshot.phase === "blocked") {
525
+ const risk = this.state.quoteRisk;
526
+ 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");
527
+ if (inventoryWait && risk && risk.quote === this.state.quote && this.hedge.policy.ratioBps === 1e4) {
528
+ const [book2, external] = await Promise.all([
529
+ this.hedge.adapter.book(),
530
+ this.hedge.adapter.account()
531
+ ]);
532
+ validateBook(book2, Date.now(), this.hedge.policy.maxPriceAgeMs);
533
+ 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)
534
+ throw new Error("External account is unavailable for quote continuity");
535
+ const market = await this.maker.read("Market", this.maker.marketAddress);
536
+ const price = BigInt(market.oraclePriceE8.toString());
537
+ 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);
538
+ const quote = await this.maker.quoteState(new PublicKey(risk.quote));
539
+ const balanceNonce = Buffer.from(quote.balanceNonce).toString("hex");
540
+ const unchanged = currentVersion === risk.version;
541
+ const ownFill = "ready" in confirmed.status && currentVersion === risk.version + 1n && risk.balanceNonce !== undefined && balanceNonce !== risk.balanceNonce;
542
+ if (ownFill) {
543
+ risk.version = currentVersion;
544
+ risk.balanceNonce = balanceNonce;
545
+ this.state.inventoryPendingSince ??= Date.now();
546
+ await this.store.save(this.state);
547
+ }
548
+ const bounded = (unchanged || ownFill) && external.position === expected && risk.notional * price <= this.hedge.policy.maxUnhedgedNotional * risk.price;
549
+ if (market.pendingOrders > 0 && bounded)
550
+ return snapshot.reason;
551
+ if (bounded && unchanged && this.state.inventoryPendingSince === undefined && this.state.hedge.unhedgedSince === undefined)
552
+ return snapshot.reason;
553
+ this.state.inventoryPendingSince ??= Date.now();
554
+ await this.store.save(this.state);
555
+ if (bounded && Date.now() - this.state.inventoryPendingSince < this.hedge.policy.maxUnhedgedMs)
556
+ return snapshot.reason;
557
+ }
489
558
  await this.cancel(true);
490
559
  return snapshot.reason ?? "hedge reconciliation pending";
491
560
  }
561
+ if (this.state.inventoryPendingSince !== undefined) {
562
+ this.state.inventoryPendingSince = undefined;
563
+ await this.store.save(this.state);
564
+ }
492
565
  const [book, costs] = await Promise.all([
493
566
  this.hedge.adapter.book(),
494
567
  this.hedge.adapter.costs()
@@ -497,7 +570,9 @@ class MakerBot {
497
570
  const before = this.pricing.book.bids[0].priceE8 + this.pricing.book.asks[0].priceE8;
498
571
  const after = book.bids[0].priceE8 + book.asks[0].priceE8;
499
572
  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;
573
+ const priorCosts = executionCosts(this.policy, this.pricing);
574
+ const nextCosts = executionCosts(this.policy, costs);
575
+ this.pricingDirty ||= change * 10000n >= before * BigInt(this.policy.repriceBps) || priorCosts.bid !== nextCosts.bid || priorCosts.ask !== nextCosts.ask;
501
576
  }
502
577
  this.pricing = {
503
578
  book,
@@ -561,6 +636,7 @@ class MakerBot {
561
636
  state.quote = flow.nextId.toBase58();
562
637
  state.request = state.replacement.request;
563
638
  state.referencePrice = state.replacement.referencePrice;
639
+ state.quoteRisk = state.replacement.risk;
564
640
  state.replacement = undefined;
565
641
  await this.store.save(state);
566
642
  } else if (!prior && !next && (!state.replacement.request || state.replacement.request.expiresAt + 180 < Math.floor(Date.now() / 1000))) {
@@ -636,7 +712,8 @@ class MakerBot {
636
712
  "waiting",
637
713
  "validated",
638
714
  "validating",
639
- "closing"
715
+ "closing",
716
+ "reserving"
640
717
  ].includes(snapshot.status) ? await this.quoteWrite(handle2, true) : snapshot;
641
718
  successorPending = !terminal.has(result.status);
642
719
  }
@@ -669,7 +746,14 @@ class MakerBot {
669
746
  await this.store.save(state);
670
747
  return true;
671
748
  }
672
- if (["active", "waiting", "validated", "validating", "closing"].includes(quote.status))
749
+ if ([
750
+ "active",
751
+ "waiting",
752
+ "validated",
753
+ "validating",
754
+ "closing",
755
+ "reserving"
756
+ ].includes(quote.status))
673
757
  await this.quoteWrite(handle, true);
674
758
  return false;
675
759
  }
@@ -677,13 +761,15 @@ class MakerBot {
677
761
  const state = this.state;
678
762
  const replacement = state.replacement;
679
763
  const flow = this.maker.quote.replacement(new PublicKey(replacement.prior), replacement.nonce);
764
+ if (now - market.oracleTimestamp.toNumber() > market.config.maxOracleAgeSeconds - Math.min(Math.floor(market.config.maxOracleAgeSeconds / 2), this.policy.lifetimeSeconds))
765
+ return this.snapshot("pending", "waiting for a fresh oracle update");
680
766
  if (!replacement.request) {
681
767
  const account = await this.maker.read("MarketAccount", this.maker.marketAccount());
682
768
  if (!("ready" in account.status) || market.pendingOrders)
683
769
  return this.snapshot("pending", "settlement in progress");
684
770
  if (!this.position || this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString())) {
685
771
  this.position = await this.maker.position.summary({
686
- timeoutMs: 180000
772
+ timeoutMs: 0
687
773
  });
688
774
  if (this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString()))
689
775
  return this.snapshot("pending", "maker view changed");
@@ -693,10 +779,23 @@ class MakerBot {
693
779
  }
694
780
  this.inventoryVersion = this.position.version;
695
781
  this.dirty = false;
696
- replacement.request = botQuote(this.policy, market, this.position, replacement.nonce, now, this.pricing);
782
+ const previous2 = await this.maker.quote.get(new PublicKey(replacement.prior)).status();
783
+ const policy = this.quotePolicy(market);
784
+ const planned = botQuote(policy, market, this.position, replacement.nonce, now, this.pricing);
785
+ let credit = 0n;
786
+ if (previous2.status === "active" && (!planned || planned.maxCapacity < policy.maxCapacity)) {
787
+ this.reservationView ??= this.maker.quote.get(new PublicKey(replacement.prior));
788
+ const margin = await this.reservationView.reservation();
789
+ if (margin === undefined)
790
+ return this.snapshot("pending", "reading remaining quote margin");
791
+ credit = margin;
792
+ this.reservationView = undefined;
793
+ }
794
+ replacement.request = botQuote(policy, market, this.position, replacement.nonce, now, this.pricing, credit);
697
795
  if (!replacement.request)
698
796
  return this.snapshot("pending", "inventory or collateral limit");
699
797
  replacement.referencePrice = BigInt(market.oraclePriceE8.toString());
798
+ replacement.risk = this.quoteRisk(flow.nextId, replacement.request, market);
700
799
  await this.store.save(state);
701
800
  }
702
801
  await flow.begin();
@@ -706,16 +805,39 @@ class MakerBot {
706
805
  if (snapshot.status === "validating" && replacement.request.expiresAt > now)
707
806
  return this.snapshot("pending", "validating successor quote");
708
807
  if (["rejected", "cancelled"].includes(snapshot.status) || replacement.request.expiresAt <= now) {
709
- await this.cancel();
710
- return this.snapshot("pending", "releasing expired successor");
808
+ await flow.discard();
809
+ state.replacement = undefined;
810
+ this.reservationView = undefined;
811
+ await this.store.save(state);
812
+ return this.snapshot("pending", "retrying expired successor");
711
813
  }
712
814
  const prior = this.maker.quote.get(new PublicKey(replacement.prior));
713
815
  const previous = await prior.status();
714
- const size = this.position.position;
715
- const exposure = ((size < 0n ? -size : size) * BigInt(market.oraclePriceE8.toString()) + 99999999999n) / 100000000000n;
716
- 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())) {
717
- await this.quoteWrite(next);
718
- return this.snapshot("pending", "activating successor quote");
816
+ if (previous.status === "active" && ["validated", "reserving"].includes(snapshot.status)) {
817
+ const price = BigInt(market.oraclePriceE8.toString());
818
+ const size = this.position.position;
819
+ const exposure = ((size < 0n ? -size : size) * price + 99999999999n) / 100000000000n;
820
+ const risk = this.quoteRisk(next.id, replacement.request, market);
821
+ if (snapshot.status === "validated" && (exposure + snapshot.maxCapacity > this.policy.maxInventoryNotional || risk && risk.notional > this.hedge.policy.maxUnhedgedNotional)) {
822
+ await flow.discard();
823
+ state.replacement = undefined;
824
+ await this.store.save(state);
825
+ return this.snapshot("pending", "repricing changed inventory");
826
+ }
827
+ if (snapshot.status === "validated")
828
+ replacement.risk = risk;
829
+ await this.store.save(state);
830
+ const account = await this.maker.read("MarketAccount", this.maker.marketAccount());
831
+ await flow.rotate(next);
832
+ if (next.reserveSignature) {
833
+ state.quoteWrite = {
834
+ quote: next.id.toBase58(),
835
+ version: BigInt(account.version.toString()),
836
+ release: false
837
+ };
838
+ await this.store.save(state);
839
+ }
840
+ return this.snapshot("pending", "rotating quote reservation");
719
841
  }
720
842
  if (!terminal.has(previous.status)) {
721
843
  if (["active", "waiting", "validated", "validating", "closing"].includes(previous.status))
@@ -725,6 +847,7 @@ class MakerBot {
725
847
  state.quote = flow.nextId.toBase58();
726
848
  state.request = replacement.request;
727
849
  state.referencePrice = replacement.referencePrice;
850
+ state.quoteRisk = replacement.risk;
728
851
  state.replacement = undefined;
729
852
  this.pricingDirty = false;
730
853
  await this.store.save(state);
@@ -765,6 +888,8 @@ class MakerBot {
765
888
  await this.hedge.tick(state.hedge, () => this.saveHedge(), undefined);
766
889
  return this.snapshot(stopped && !state.hedge?.pending ? "stopped" : "pending", reason);
767
890
  }
891
+ if (state.quote)
892
+ await this.renew(market, now);
768
893
  await this.fills();
769
894
  const hedgeReason = await this.hedgeTick();
770
895
  if (hedgeReason)
@@ -802,12 +927,29 @@ class MakerBot {
802
927
  await this.cancel();
803
928
  return this.snapshot("pending", "releasing quote");
804
929
  } else if (quote.status === "active") {
930
+ if (this.dirty && !this.hedge) {
931
+ const account2 = await this.maker.read("MarketAccount", this.maker.marketAccount());
932
+ if ("ready" in account2.status) {
933
+ const before = this.position;
934
+ this.position = await this.maker.position.summary({
935
+ timeoutMs: 0
936
+ });
937
+ if (this.position.status === "ready" && this.position.version === BigInt(account2.version.toString())) {
938
+ this.dirty = false;
939
+ this.pricingDirty ||= before !== undefined && before.position !== this.position.position;
940
+ this.inventoryVersion = this.position.version;
941
+ }
942
+ }
943
+ if (this.dirty)
944
+ return this.snapshot("pending", "reading maker inventory");
945
+ }
805
946
  const price = BigInt(market.oraclePriceE8.toString());
806
947
  const difference = state.referencePrice === undefined ? undefined : (price > state.referencePrice ? price - state.referencePrice : state.referencePrice - price) * 10000n;
807
948
  const current = await this.maker.quoteState(handle2.id);
808
- const duration = Math.min(this.policy.lifetimeSeconds, market.config.maxQuoteLifetimeSeconds);
809
- const refresh = current.expiry.toNumber() - now <= duration - Math.min(this.policy.refreshSeconds, duration - 1);
810
- if (!this.dirty && !this.pricingDirty && !refresh && difference !== undefined && difference < BigInt(this.policy.repriceBps) * state.referencePrice)
949
+ const reference = BigInt(current.oracleReferenceE8.toString());
950
+ const drift = price > reference ? price - reference : reference - price;
951
+ const compatible = current.marketRevision.eq(market.revision) && drift * 10000n <= reference * BigInt(Math.min(Math.floor(market.config.maxQuoteDeviationBps / 4), 25));
952
+ if (!this.dirty && !this.pricingDirty && compatible && current.expiry.toNumber() > now && difference !== undefined && difference < BigInt(this.policy.repriceBps) * state.referencePrice)
811
953
  return this.snapshot("active");
812
954
  state.replacement = { prior: state.quote, nonce: nonce() };
813
955
  this.pricingDirty = false;
@@ -824,7 +966,7 @@ class MakerBot {
824
966
  return this.snapshot("idle", "maker account busy");
825
967
  if (this.dirty || !this.position || this.position.version !== BigInt(account.version.toString())) {
826
968
  this.position = await this.maker.position.summary({
827
- timeoutMs: 180000
969
+ timeoutMs: 0
828
970
  });
829
971
  if (this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString()))
830
972
  return this.snapshot("idle", "maker view changed");
@@ -832,12 +974,13 @@ class MakerBot {
832
974
  return this.snapshot("idle", "maker view refreshed");
833
975
  }
834
976
  if (!state.request) {
835
- const request2 = botQuote(this.policy, market, this.position, nonce(), now, this.pricing);
977
+ const request2 = botQuote(this.quotePolicy(market), market, this.position, nonce(), now, this.pricing);
836
978
  if (!request2)
837
979
  return this.snapshot("idle", "inventory or collateral limit");
838
980
  state.request = request2;
839
981
  this.pricingDirty = false;
840
982
  state.referencePrice = BigInt(market.oraclePriceE8.toString());
983
+ state.quoteRisk = this.quoteRisk(this.requestId(), request2, market);
841
984
  await this.store.save(state);
842
985
  }
843
986
  const request = state.request;
@@ -863,6 +1006,55 @@ class MakerBot {
863
1006
  this.busy = false;
864
1007
  }
865
1008
  }
1009
+ async renew(market, now) {
1010
+ const state = this.state;
1011
+ if (this.hedge && (state.hedge?.pending || state.inventoryPendingSince !== undefined && Date.now() - state.inventoryPendingSince >= this.hedge.policy.maxUnhedgedMs))
1012
+ return;
1013
+ const handle = this.maker.quote.get(new PublicKey(state.quote));
1014
+ const quote = await this.maker.quoteState(handle.id);
1015
+ if (!("active" in quote.status) || quote.expiry.toNumber() <= now)
1016
+ return;
1017
+ const duration = Math.min(this.policy.lifetimeSeconds, market.config.maxQuoteLifetimeSeconds);
1018
+ if (quote.expiry.toNumber() - now > Math.max(this.policy.refreshSeconds, duration - this.policy.refreshSeconds))
1019
+ return;
1020
+ const price = BigInt(market.oraclePriceE8.toString());
1021
+ const reference = BigInt(quote.oracleReferenceE8.toString());
1022
+ const drift = price > reference ? price - reference : reference - price;
1023
+ if (!quote.marketRevision.eq(market.revision) || reference <= 0n || drift * 10000n > reference * BigInt(Math.min(Math.floor(market.config.maxQuoteDeviationBps / 4), 25)))
1024
+ return;
1025
+ const renewed = await handle.renew({ validForSeconds: duration });
1026
+ if (state.request)
1027
+ state.request.expiresAt = renewed.expiresAt;
1028
+ await this.store.save(state);
1029
+ }
1030
+ quotePolicy(market) {
1031
+ if (!this.hedge || !this.pricing || this.position?.position === undefined)
1032
+ return this.policy;
1033
+ const price = BigInt(market.oraclePriceE8.toString());
1034
+ const size = this.position.position + this.pricing.externalPosition;
1035
+ const risk = ((size < 0n ? -size : size) * price + 99999999999n) / 100000000000n;
1036
+ const remaining = this.hedge.policy.maxUnhedgedNotional * 9n / 10n - risk;
1037
+ const capacity = remaining * 10000n / BigInt(1e4 + market.config.maxQuoteDeviationBps);
1038
+ return {
1039
+ ...this.policy,
1040
+ maxCapacity: capacity < this.policy.maxCapacity ? capacity > 0n ? capacity : 1n : this.policy.maxCapacity
1041
+ };
1042
+ }
1043
+ quoteRisk(id, request, market) {
1044
+ if (!this.hedge || !this.pricing || this.position?.position === undefined)
1045
+ return;
1046
+ const price = BigInt(market.oraclePriceE8.toString());
1047
+ const size = this.position.position + this.pricing.externalPosition;
1048
+ const notional2 = ((size < 0n ? -size : size) * price + 99999999999n) / 100000000000n + (request.maxCapacity * BigInt(1e4 + market.config.maxQuoteDeviationBps) + 9999n) / 10000n;
1049
+ return {
1050
+ quote: id.toBase58(),
1051
+ notional: notional2,
1052
+ price,
1053
+ version: this.position.version,
1054
+ externalPosition: this.pricing.externalPosition,
1055
+ hedgeNonce: this.state.hedge.nonce
1056
+ };
1057
+ }
866
1058
  stop() {
867
1059
  this.stopping = true;
868
1060
  }
@@ -965,7 +1157,7 @@ class HedgeEngine {
965
1157
  if (!Number.isInteger(account.leverage) || account.leverage < 1 || account.leverage > this.policy.maxLeverage)
966
1158
  throw new Error("External leverage exceeds hedge policy");
967
1159
  const latest = state.history[state.history.length - 1];
968
- if (latest && account.timestamp < latest.completedAt)
1160
+ if (latest && account.timestamp < (latest.filledAt ?? latest.completedAt))
969
1161
  return result("pending", "Waiting for the external account to include the confirmed hedge", account.position);
970
1162
  if (account.openOrders.length)
971
1163
  return result("blocked", "Hedge account has unmanaged open orders", account.position);
@@ -1029,7 +1221,7 @@ class HedgeEngine {
1029
1221
  state.pending = undefined;
1030
1222
  await save();
1031
1223
  }
1032
- return result(receipt.status === "rejected" ? "blocked" : "pending", receipt.reason ?? "Hedge submitted; reconciling confirmed exposure", account.position);
1224
+ return result(receipt.status === "rejected" ? "blocked" : "pending", receipt.status === "rejected" ? receipt.reason ?? "Hedge rejected" : state.pending ? "Hedge confirmation pending" : "Hedge submitted; reconciling confirmed exposure", account.position);
1033
1225
  } catch (error) {
1034
1226
  result("blocked", error instanceof Error ? error.message : "External hedge service unavailable");
1035
1227
  throw error;
@@ -1264,7 +1456,7 @@ class HyperliquidAdapter {
1264
1456
  return true;
1265
1457
  });
1266
1458
  const filledSize = matching.reduce((sum, fill) => sum + units(fill.sz, 9), 0n);
1267
- if (matching.some((fill) => fill.side === "B" !== order.buy || units(fill.sz, 9) <= 0n || units(fill.px, 8) <= 0n || fill.feeToken !== "USDC" || !/^0x[0-9a-fA-F]{64}$/.test(fill.hash)))
1459
+ if (matching.some((fill) => fill.side === "B" !== order.buy || units(fill.sz, 9) <= 0n || units(fill.px, 8) <= 0n || !Number.isSafeInteger(fill.time) || fill.time < order.nonce - 1000 || fill.time > Date.now() + 1000 || fill.feeToken !== "USDC" || !/^0x[0-9a-fA-F]{64}$/.test(fill.hash)))
1268
1460
  throw new Error("Hyperliquid fill does not match the hedge order");
1269
1461
  if (filledSize > order.size)
1270
1462
  throw new Error("Hyperliquid filled size exceeds the hedge order");
@@ -1280,6 +1472,7 @@ class HyperliquidAdapter {
1280
1472
  status: detail.status === "filled" ? "filled" : /rejected/i.test(detail.status) ? "rejected" : "cancelled",
1281
1473
  orderId: detail.order.oid,
1282
1474
  filledSize,
1475
+ filledAt: matching.length ? Math.max(...matching.map((fill) => fill.time)) : undefined,
1283
1476
  averagePriceE8: filledSize ? cost / filledSize : undefined,
1284
1477
  fee: matching.reduce((sum, fill) => sum + units(fill.fee, 6), 0n),
1285
1478
  hashes: [...new Set(matching.map((fill) => fill.hash))],
@@ -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.12",
3
+ "version": "0.1.14",
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",