@depthhq/maker-sdk 0.1.9 → 0.1.11
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/backstop/backstop.d.ts +1 -0
- package/dist/bot/bot.d.ts +5 -0
- package/dist/bot/store.d.ts +7 -0
- package/dist/client/client.d.ts +8 -0
- package/dist/client/types.d.ts +6 -0
- package/dist/index.js +116 -11
- package/dist/index.node.js +116 -11
- package/dist/node.js +192 -23
- package/dist/quote/cleanup.d.ts +9 -0
- package/dist/quote/replacement.d.ts +2 -1
- package/package.json +3 -3
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>;
|
package/dist/bot/store.d.ts
CHANGED
|
@@ -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;
|
package/dist/client/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -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
|
|
27519
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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())) {
|
package/dist/index.node.js
CHANGED
|
@@ -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
|
|
26040
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 [
|
|
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.
|
|
154
|
-
throw new Error("
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
431
|
-
|
|
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.
|
|
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,75 @@ 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
|
|
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
|
+
const size = this.position.position;
|
|
712
|
+
const exposure = ((size < 0n ? -size : size) * BigInt(market.oraclePriceE8.toString()) + 99999999999n) / 100000000000n;
|
|
713
|
+
if (!terminal.has(previous.status) && ["validated", "waiting", "reserving"].includes(snapshot.status) && market.activeQuoteCount < 4 && exposure + previous.maxCapacity + snapshot.maxCapacity <= this.policy.maxInventoryNotional && BigInt(market.quotedNotional.toString()) + snapshot.maxCapacity <= BigInt(market.config.maxMarketNotional.toString())) {
|
|
714
|
+
await this.quoteWrite(next);
|
|
715
|
+
return this.snapshot("pending", "activating successor quote");
|
|
716
|
+
}
|
|
717
|
+
if (!terminal.has(previous.status)) {
|
|
718
|
+
if (["active", "waiting", "validated", "validating", "closing"].includes(previous.status))
|
|
719
|
+
await this.quoteWrite(prior, true);
|
|
720
|
+
return this.snapshot("pending", "releasing prior quote");
|
|
721
|
+
}
|
|
722
|
+
state.quote = flow.nextId.toBase58();
|
|
723
|
+
state.request = replacement.request;
|
|
724
|
+
state.referencePrice = replacement.referencePrice;
|
|
725
|
+
state.replacement = undefined;
|
|
726
|
+
this.pricingDirty = false;
|
|
727
|
+
await this.store.save(state);
|
|
728
|
+
await this.quoteWrite(next);
|
|
729
|
+
return this.snapshot("pending", "activating successor quote");
|
|
730
|
+
}
|
|
568
731
|
async tick() {
|
|
569
732
|
if (this.busy)
|
|
570
733
|
throw new Error("Maker bot tick is already running");
|
|
@@ -573,6 +736,18 @@ class MakerBot {
|
|
|
573
736
|
this.busy = true;
|
|
574
737
|
try {
|
|
575
738
|
const state = this.state;
|
|
739
|
+
if (Date.now() - this.lastCleanup >= 60000) {
|
|
740
|
+
await this.maker.quote.cleanup({
|
|
741
|
+
exclude: [
|
|
742
|
+
state.quoteWrite?.quote,
|
|
743
|
+
state.quote,
|
|
744
|
+
state.replacement?.prior,
|
|
745
|
+
this.requestId()?.toBase58(),
|
|
746
|
+
state.replacement ? this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).nextId.toBase58() : undefined
|
|
747
|
+
].filter((id) => Boolean(id))
|
|
748
|
+
});
|
|
749
|
+
this.lastCleanup = Date.now();
|
|
750
|
+
}
|
|
576
751
|
let [market, lamports] = await Promise.all([
|
|
577
752
|
this.maker.read("Market", this.maker.marketAddress),
|
|
578
753
|
this.maker.options.connection.getBalance(this.maker.options.wallet.publicKey, "confirmed")
|
|
@@ -600,6 +775,8 @@ class MakerBot {
|
|
|
600
775
|
return this.snapshot(stopped ? "stopped" : "pending", changed);
|
|
601
776
|
}
|
|
602
777
|
}
|
|
778
|
+
if (state.replacement)
|
|
779
|
+
return this.replace(market, now);
|
|
603
780
|
if (!state.quote && this.requestId() && await this.maker.options.connection.getAccountInfo(this.requestId(), "confirmed")) {
|
|
604
781
|
state.quote = this.requestId().toBase58();
|
|
605
782
|
await this.store.save(state);
|
|
@@ -610,20 +787,13 @@ class MakerBot {
|
|
|
610
787
|
if (terminal.has(quote.status)) {
|
|
611
788
|
state.quote = undefined;
|
|
612
789
|
state.request = undefined;
|
|
613
|
-
this.dirty = true;
|
|
614
790
|
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
791
|
} else if (["validating", "validated", "waiting", "reserving"].includes(quote.status)) {
|
|
622
792
|
const current = await this.maker.quoteState(handle2.id);
|
|
623
793
|
if (current.expiry.toNumber() <= now && ["validating", "validated", "waiting"].includes(quote.status))
|
|
624
794
|
await this.cancel();
|
|
625
795
|
else
|
|
626
|
-
await
|
|
796
|
+
await this.quoteWrite(handle2);
|
|
627
797
|
return this.snapshot("pending", "activating quote");
|
|
628
798
|
} else if (["closing", "releasing"].includes(quote.status)) {
|
|
629
799
|
await this.cancel();
|
|
@@ -640,7 +810,7 @@ class MakerBot {
|
|
|
640
810
|
this.pricingDirty = false;
|
|
641
811
|
await this.store.save(state);
|
|
642
812
|
await this.maker.quote.replacement(handle2.id, state.replacement.nonce).begin();
|
|
643
|
-
await this.
|
|
813
|
+
await this.replace(market, now);
|
|
644
814
|
return this.snapshot("pending", "repricing quote");
|
|
645
815
|
}
|
|
646
816
|
}
|
|
@@ -659,7 +829,7 @@ class MakerBot {
|
|
|
659
829
|
return this.snapshot("idle", "maker view refreshed");
|
|
660
830
|
}
|
|
661
831
|
if (!state.request) {
|
|
662
|
-
const request2 = botQuote(this.policy, market, this.position,
|
|
832
|
+
const request2 = botQuote(this.policy, market, this.position, nonce(), now, this.pricing);
|
|
663
833
|
if (!request2)
|
|
664
834
|
return this.snapshot("idle", "inventory or collateral limit");
|
|
665
835
|
state.request = request2;
|
|
@@ -673,14 +843,13 @@ class MakerBot {
|
|
|
673
843
|
await this.store.save(state);
|
|
674
844
|
return this.snapshot("idle", "prepared quote expired");
|
|
675
845
|
}
|
|
676
|
-
const handle =
|
|
846
|
+
const handle = await this.maker.quote.publish(request);
|
|
677
847
|
state.quote = handle.id.toBase58();
|
|
678
848
|
state.replacement = undefined;
|
|
679
849
|
await this.store.save(state);
|
|
680
|
-
await
|
|
850
|
+
await this.quoteWrite(handle);
|
|
681
851
|
return this.snapshot("pending", "quote submitted");
|
|
682
852
|
} catch (error) {
|
|
683
|
-
this.dirty = true;
|
|
684
853
|
try {
|
|
685
854
|
await this.cancel(true);
|
|
686
855
|
} catch (cause) {
|
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.1.11",
|
|
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.
|
|
33
|
-
"@depthhq/sdk": "0.1.
|
|
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",
|