@depthhq/maker-sdk 0.0.0-stage → 0.1.2
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/automation/automation.d.ts +24 -0
- package/dist/automation/types.d.ts +40 -0
- package/dist/backstop/backstop.d.ts +35 -0
- package/dist/bot/bot.d.ts +42 -0
- package/dist/bot/policy.d.ts +41 -0
- package/dist/bot/store.d.ts +32 -0
- package/dist/chain/arcium.d.ts +18 -0
- package/dist/chain/random.d.ts +1 -0
- package/dist/client/client.d.ts +181 -0
- package/dist/client/types.d.ts +100 -0
- package/dist/fill/browser.d.ts +8 -0
- package/dist/fill/cursor.d.ts +8 -0
- package/dist/fill/history.d.ts +25 -0
- package/dist/fill/store.d.ts +2 -0
- package/dist/hedge/access.d.ts +40 -0
- package/dist/hedge/amount.d.ts +2 -0
- package/dist/hedge/engine.d.ts +9 -0
- package/dist/hedge/hyperliquid.d.ts +35 -0
- package/dist/hedge/policy.d.ts +4 -0
- package/dist/hedge/schema.d.ts +2 -0
- package/dist/hedge/setup.d.ts +22 -0
- package/dist/hedge/types.d.ts +89 -0
- package/dist/hedge/wallet.d.ts +8 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +28208 -0
- package/dist/index.node.js +26729 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.js +1195 -0
- package/dist/quote/quote.d.ts +38 -0
- package/dist/quote/replacement.d.ts +23 -0
- package/package.json +37 -3
- package/README.md +0 -3
package/dist/node.js
ADDED
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
// src/fill/cursor.ts
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
class FillCursorStore {
|
|
8
|
+
genesis;
|
|
9
|
+
maker;
|
|
10
|
+
path;
|
|
11
|
+
constructor(genesis, maker, directory) {
|
|
12
|
+
this.genesis = genesis;
|
|
13
|
+
this.maker = maker;
|
|
14
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(genesis) || !/^[1-9A-HJ-NP-Za-km-z]+$/.test(maker))
|
|
15
|
+
throw new RangeError("Fill cursor identity is invalid");
|
|
16
|
+
this.path = join(directory ?? process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "depth", "fills", `${genesis}-${maker}.json`);
|
|
17
|
+
}
|
|
18
|
+
async load() {
|
|
19
|
+
let contents;
|
|
20
|
+
try {
|
|
21
|
+
contents = await readFile(this.path, "utf8");
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
return;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
const saved = JSON.parse(contents);
|
|
28
|
+
if (saved.genesis !== this.genesis || saved.maker !== this.maker || typeof saved.cursor !== "string" || !saved.cursor)
|
|
29
|
+
throw new Error("Fill cursor file does not match this maker and network");
|
|
30
|
+
return saved.cursor;
|
|
31
|
+
}
|
|
32
|
+
async save(cursor) {
|
|
33
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(cursor))
|
|
34
|
+
throw new RangeError("Fill cursor signature is invalid");
|
|
35
|
+
await mkdir(dirname(this.path), { recursive: true, mode: 448 });
|
|
36
|
+
const temporary = `${this.path}.${process.pid}.${randomBytes(8).toString("hex")}`;
|
|
37
|
+
await writeFile(temporary, JSON.stringify({
|
|
38
|
+
genesis: this.genesis,
|
|
39
|
+
maker: this.maker,
|
|
40
|
+
cursor
|
|
41
|
+
}), { mode: 384 });
|
|
42
|
+
await rename(temporary, this.path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// src/bot/bot.ts
|
|
46
|
+
import { createHash, randomBytes as randomBytes3 } from "node:crypto";
|
|
47
|
+
import { PublicKey } from "@solana/web3.js";
|
|
48
|
+
import { DEPTH_PROGRAM_ID } from "@depthhq/bindings";
|
|
49
|
+
|
|
50
|
+
// src/bot/store.ts
|
|
51
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
52
|
+
import { mkdir as mkdir2, open, readFile as readFile2, rename as rename2, unlink } from "node:fs/promises";
|
|
53
|
+
import { hostname } from "node:os";
|
|
54
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
55
|
+
|
|
56
|
+
class BotStore {
|
|
57
|
+
genesis;
|
|
58
|
+
maker;
|
|
59
|
+
operator;
|
|
60
|
+
path;
|
|
61
|
+
lock;
|
|
62
|
+
constructor(genesis, maker, operator, directory) {
|
|
63
|
+
this.genesis = genesis;
|
|
64
|
+
this.maker = maker;
|
|
65
|
+
this.operator = operator;
|
|
66
|
+
for (const value of [genesis, maker, operator])
|
|
67
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(value))
|
|
68
|
+
throw new Error("Maker bot identity is invalid");
|
|
69
|
+
this.path = join2(directory, `${genesis}-${maker}-${operator}.json`);
|
|
70
|
+
}
|
|
71
|
+
async acquire() {
|
|
72
|
+
if (this.lock)
|
|
73
|
+
throw new Error("Maker bot already holds its lease");
|
|
74
|
+
await mkdir2(dirname2(this.path), { recursive: true, mode: 448 });
|
|
75
|
+
const path = `${this.path}.lock`;
|
|
76
|
+
const token = randomBytes2(16).toString("hex");
|
|
77
|
+
for (let attempt = 0;attempt < 2; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
const handle = await open(path, "wx", 384);
|
|
80
|
+
try {
|
|
81
|
+
await handle.writeFile(JSON.stringify({ host: hostname(), pid: process.pid, token }));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
await handle.close();
|
|
84
|
+
await unlink(path);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
this.lock = { token, close: () => handle.close() };
|
|
88
|
+
return;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code !== "EEXIST")
|
|
91
|
+
throw error;
|
|
92
|
+
const contents = await readFile2(path, "utf8");
|
|
93
|
+
const lock = JSON.parse(contents);
|
|
94
|
+
if (lock.host !== hostname() || !Number.isSafeInteger(lock.pid) || lock.pid <= 0)
|
|
95
|
+
throw new Error("Maker bot lease belongs to another host");
|
|
96
|
+
try {
|
|
97
|
+
process.kill(lock.pid, 0);
|
|
98
|
+
throw new Error("Another maker bot is running for this wallet");
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
if (cause.code !== "ESRCH")
|
|
101
|
+
throw cause;
|
|
102
|
+
}
|
|
103
|
+
if (await readFile2(path, "utf8") !== contents)
|
|
104
|
+
throw new Error("Maker bot lease changed during recovery");
|
|
105
|
+
await unlink(path);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
throw new Error("Maker bot lease could not be acquired");
|
|
109
|
+
}
|
|
110
|
+
async release() {
|
|
111
|
+
const lock = this.lock;
|
|
112
|
+
if (!lock)
|
|
113
|
+
return;
|
|
114
|
+
this.lock = undefined;
|
|
115
|
+
await lock.close();
|
|
116
|
+
let contents;
|
|
117
|
+
try {
|
|
118
|
+
contents = await readFile2(`${this.path}.lock`, "utf8");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code === "ENOENT")
|
|
121
|
+
return;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
const value = JSON.parse(contents);
|
|
125
|
+
if (value.token === lock.token)
|
|
126
|
+
await unlink(`${this.path}.lock`);
|
|
127
|
+
}
|
|
128
|
+
async load() {
|
|
129
|
+
let source;
|
|
130
|
+
try {
|
|
131
|
+
source = await readFile2(this.path, "utf8");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.code === "ENOENT")
|
|
134
|
+
return {
|
|
135
|
+
genesis: this.genesis,
|
|
136
|
+
maker: this.maker,
|
|
137
|
+
operator: this.operator,
|
|
138
|
+
fills: {}
|
|
139
|
+
};
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const state = JSON.parse(source, (_key, value) => value && typeof value === "object" && Object.keys(value).length === 1 && typeof value.$bigint === "string" ? BigInt(value.$bigint) : value);
|
|
143
|
+
if (state.genesis !== this.genesis || state.maker !== this.maker || state.operator !== this.operator || !state.fills || typeof state.fills !== "object")
|
|
144
|
+
throw new Error("Maker bot checkpoint belongs to another wallet or network");
|
|
145
|
+
if (state.referencePrice !== undefined && (typeof state.referencePrice !== "bigint" || state.referencePrice <= 0n))
|
|
146
|
+
throw new Error("Maker bot checkpoint price is invalid");
|
|
147
|
+
for (const value of [state.request?.clientNonce, state.replacement?.nonce])
|
|
148
|
+
if (value !== undefined && (typeof value !== "bigint" || value < 0n || value >= 1n << 64n))
|
|
149
|
+
throw new Error("Maker bot checkpoint nonce is invalid");
|
|
150
|
+
for (const value of [state.quote, state.replacement?.prior])
|
|
151
|
+
if (value !== undefined && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
|
|
152
|
+
throw new Error("Maker bot checkpoint quote is invalid");
|
|
153
|
+
if (state.request && (!Number.isSafeInteger(state.request.expiresAt) || typeof state.request.maxCapacity !== "bigint" || state.request.maxCapacity <= 0n || !Array.isArray(state.request.tiers) || !state.request.tiers.length))
|
|
154
|
+
throw new Error("Maker bot checkpoint request is invalid");
|
|
155
|
+
for (const fill of Object.values(state.fills))
|
|
156
|
+
if (typeof fill.signature !== "string" || !Number.isSafeInteger(fill.slot) || fill.slot < 0)
|
|
157
|
+
throw new Error("Maker bot checkpoint fill is invalid");
|
|
158
|
+
if (state.hedge) {
|
|
159
|
+
const hedge = state.hedge;
|
|
160
|
+
if (typeof hedge.identity !== "string" || !Number.isSafeInteger(hedge.nonce) || hedge.nonce < 0 || !Array.isArray(hedge.history) || hedge.history.length > 100 || hedge.unhedgedSince !== undefined && !Number.isSafeInteger(hedge.unhedgedSince))
|
|
161
|
+
throw new Error("Hedge checkpoint is invalid");
|
|
162
|
+
for (const order of [
|
|
163
|
+
...hedge.history,
|
|
164
|
+
...hedge.pending ? [hedge.pending] : []
|
|
165
|
+
])
|
|
166
|
+
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)
|
|
167
|
+
throw new Error("Hedge checkpoint order is invalid");
|
|
168
|
+
for (const record of hedge.history)
|
|
169
|
+
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))
|
|
170
|
+
throw new Error("Hedge checkpoint receipt is invalid");
|
|
171
|
+
}
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
174
|
+
async save(state) {
|
|
175
|
+
if (!this.lock)
|
|
176
|
+
throw new Error("Acquire the maker bot lease before saving");
|
|
177
|
+
const lease = JSON.parse(await readFile2(`${this.path}.lock`, "utf8"));
|
|
178
|
+
if (lease.token !== this.lock.token || lease.pid !== process.pid || lease.host !== hostname())
|
|
179
|
+
throw new Error("Maker bot lease was lost");
|
|
180
|
+
const path = `${this.path}.${process.pid}.${randomBytes2(8).toString("hex")}`;
|
|
181
|
+
const handle = await open(path, "wx", 384);
|
|
182
|
+
try {
|
|
183
|
+
await handle.writeFile(JSON.stringify(state, (_key, value) => typeof value === "bigint" ? { $bigint: value.toString() } : value));
|
|
184
|
+
await handle.sync();
|
|
185
|
+
} finally {
|
|
186
|
+
await handle.close();
|
|
187
|
+
}
|
|
188
|
+
await rename2(path, this.path);
|
|
189
|
+
const directory = await open(dirname2(this.path), "r");
|
|
190
|
+
try {
|
|
191
|
+
await directory.sync();
|
|
192
|
+
} finally {
|
|
193
|
+
await directory.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/bot/policy.ts
|
|
199
|
+
import { positionPnl } from "@depthhq/sdk";
|
|
200
|
+
|
|
201
|
+
// src/hedge/policy.ts
|
|
202
|
+
function validateHedgePolicy(policy) {
|
|
203
|
+
if (!Number.isInteger(policy.maxLeverage) || policy.maxLeverage < 1 || policy.maxLeverage > 10)
|
|
204
|
+
throw new RangeError("Maximum hedge leverage must be 1 to 10");
|
|
205
|
+
if (!Number.isInteger(policy.ratioBps) || policy.ratioBps < 1 || policy.ratioBps > 1e4)
|
|
206
|
+
throw new RangeError("Hedge ratio must be 1 to 10000 bps");
|
|
207
|
+
if (policy.thresholdNotional <= 0n || policy.maxOrderNotional <= 0n || policy.maxUnhedgedNotional <= policy.thresholdNotional)
|
|
208
|
+
throw new RangeError("Hedge notional limits are invalid");
|
|
209
|
+
for (const value of [
|
|
210
|
+
policy.batchMs,
|
|
211
|
+
policy.maxUnhedgedMs,
|
|
212
|
+
policy.maxPriceAgeMs,
|
|
213
|
+
policy.orderTtlMs
|
|
214
|
+
])
|
|
215
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 300000)
|
|
216
|
+
throw new RangeError("Hedge time limits must be 1 to 300000 ms");
|
|
217
|
+
if (policy.batchMs >= policy.maxUnhedgedMs || policy.orderTtlMs > policy.maxUnhedgedMs)
|
|
218
|
+
throw new RangeError("Hedge batching and order expiry must fit the exposure time limit");
|
|
219
|
+
if (!Number.isInteger(policy.maxSlippageBps) || policy.maxSlippageBps < 1 || policy.maxSlippageBps > 100)
|
|
220
|
+
throw new RangeError("Hedge slippage must be 1 to 100 bps");
|
|
221
|
+
}
|
|
222
|
+
function validateBook(book, now, maxAge) {
|
|
223
|
+
if (!Number.isSafeInteger(book.timestamp) || book.timestamp > now + 1000 || now - book.timestamp > maxAge)
|
|
224
|
+
throw new Error("External order book is stale");
|
|
225
|
+
for (const [side, levels] of [
|
|
226
|
+
["bid", book.bids],
|
|
227
|
+
["ask", book.asks]
|
|
228
|
+
]) {
|
|
229
|
+
if (!levels.length)
|
|
230
|
+
throw new Error("External order book is empty");
|
|
231
|
+
for (let index = 0;index < levels.length; index++) {
|
|
232
|
+
const level = levels[index];
|
|
233
|
+
if (level.priceE8 <= 0n || level.size <= 0n || index > 0 && (side === "bid" ? level.priceE8 > levels[index - 1].priceE8 : level.priceE8 < levels[index - 1].priceE8))
|
|
234
|
+
throw new Error("External order book is invalid");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (book.bids[0].priceE8 >= book.asks[0].priceE8)
|
|
238
|
+
throw new Error("External order book is crossed");
|
|
239
|
+
}
|
|
240
|
+
function executionPrice(levels, size) {
|
|
241
|
+
if (size <= 0n)
|
|
242
|
+
throw new RangeError("Hedge execution size must be positive");
|
|
243
|
+
let remaining = size;
|
|
244
|
+
let cost = 0n;
|
|
245
|
+
for (const level of levels) {
|
|
246
|
+
const fill = remaining < level.size ? remaining : level.size;
|
|
247
|
+
cost += fill * level.priceE8;
|
|
248
|
+
remaining -= fill;
|
|
249
|
+
if (remaining === 0n)
|
|
250
|
+
return (cost + size - 1n) / size;
|
|
251
|
+
}
|
|
252
|
+
throw new Error("External order book cannot cover the quote tier");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/hedge/types.ts
|
|
256
|
+
var abs = (value) => value < 0n ? -value : value;
|
|
257
|
+
var notional = (size, priceE8) => (abs(size) * priceE8 + 99999999999n) / 100000000000n;
|
|
258
|
+
|
|
259
|
+
// src/bot/policy.ts
|
|
260
|
+
function validateBotPolicy(policy) {
|
|
261
|
+
if (policy.maxCapacity <= 0n || policy.maxInventoryNotional <= 0n || policy.maxCapacity > policy.maxInventoryNotional)
|
|
262
|
+
throw new RangeError("Maker capacity must fit a positive inventory limit");
|
|
263
|
+
for (const [name, value] of Object.entries(policy))
|
|
264
|
+
if (typeof value === "number" && (!Number.isSafeInteger(value) || value <= 0) && !(["inventorySkewBps", "executionCostBps"].includes(name) && value === 0))
|
|
265
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
266
|
+
if (policy.refreshSeconds >= policy.lifetimeSeconds)
|
|
267
|
+
throw new RangeError("Refresh must precede quote expiry");
|
|
268
|
+
if (policy.spreadBps >= 20000 || policy.repriceBps > 1e4)
|
|
269
|
+
throw new RangeError("Maker price bounds are invalid");
|
|
270
|
+
if ((policy.inventorySkewBps ?? 0) > 100 || (policy.executionCostBps ?? 0) > 100)
|
|
271
|
+
throw new RangeError("Maker pricing adjustments must be 0 to 100 bps");
|
|
272
|
+
if ((policy.fundingHours ?? 1) > 168)
|
|
273
|
+
throw new RangeError("Maker funding horizon must be at most 168 hours");
|
|
274
|
+
const reserve = policy.gasReserveLamports ?? 50000000n;
|
|
275
|
+
if (reserve < 10000000n || reserve > BigInt(Number.MAX_SAFE_INTEGER))
|
|
276
|
+
throw new RangeError("Maker SOL reserve must be at least 10000000 lamports");
|
|
277
|
+
}
|
|
278
|
+
function quoteHealth(market, now) {
|
|
279
|
+
if (!("active" in market.status) || !("none" in market.incident))
|
|
280
|
+
return "market paused";
|
|
281
|
+
const age = now - market.oracleTimestamp.toNumber();
|
|
282
|
+
if (age < 0 || age > market.config.maxOracleAgeSeconds)
|
|
283
|
+
return "oracle stale";
|
|
284
|
+
if (now > market.lastFundingTimestamp.toNumber() + market.config.fundingIntervalSeconds)
|
|
285
|
+
return "funding overdue";
|
|
286
|
+
if (BigInt(market.oraclePriceE8.toString()) < 100000000n)
|
|
287
|
+
return "oracle price invalid";
|
|
288
|
+
}
|
|
289
|
+
function botQuote(policy, market, position, nonce, now, pricing) {
|
|
290
|
+
validateBotPolicy(policy);
|
|
291
|
+
const reason = quoteHealth(market, now);
|
|
292
|
+
if (reason)
|
|
293
|
+
throw new Error(reason);
|
|
294
|
+
if (position.status !== "ready" || position.position === undefined || position.collateral === undefined || position.reserved === undefined)
|
|
295
|
+
throw new Error("Maker private position is not ready");
|
|
296
|
+
const price = BigInt(market.oraclePriceE8.toString());
|
|
297
|
+
const exposure = (position.position < 0n ? -position.position : position.position) * price / 100000000000n;
|
|
298
|
+
const marginBps = BigInt(market.config.initialMarginBps);
|
|
299
|
+
if (marginBps <= 0n)
|
|
300
|
+
throw new Error("Maker margin policy is invalid");
|
|
301
|
+
const pnl = position.position === 0n ? 0n : positionPnl(position, price, BigInt(market.fundingIndex.toString())).net;
|
|
302
|
+
const free = position.collateral + pnl - position.reserved - (exposure * marginBps + 9999n) / 10000n;
|
|
303
|
+
const limits = [
|
|
304
|
+
policy.maxCapacity,
|
|
305
|
+
policy.maxInventoryNotional - exposure,
|
|
306
|
+
free > 0n ? free * 10000n / marginBps : 0n,
|
|
307
|
+
BigInt(market.config.maxAccountNotional.toString()) - exposure,
|
|
308
|
+
BigInt(market.config.maxMarketNotional.toString())
|
|
309
|
+
];
|
|
310
|
+
const capacity = limits.reduce((minimum, value) => value < minimum ? value : minimum);
|
|
311
|
+
const tiers = market.config.quoteTiers.map((value) => BigInt(value.toString())).filter((size) => size > 0n && size <= capacity);
|
|
312
|
+
if (!tiers.length)
|
|
313
|
+
return;
|
|
314
|
+
const half = Math.ceil(policy.spreadBps / 2);
|
|
315
|
+
if (half > market.config.maxQuoteDeviationBps)
|
|
316
|
+
throw new Error("Maker spread exceeds oracle deviation policy");
|
|
317
|
+
if (pricing)
|
|
318
|
+
validateBook(pricing.book, now * 1000, pricing.maxPriceAgeMs);
|
|
319
|
+
if (pricing && (!Number.isFinite(pricing.takerFeeBps) || pricing.takerFeeBps < 0 || !Number.isFinite(pricing.hourlyFundingBps)))
|
|
320
|
+
throw new Error("External execution costs are invalid");
|
|
321
|
+
const netPosition = position.position + (pricing?.externalPosition ?? 0n);
|
|
322
|
+
const netExposure = notional(netPosition, price);
|
|
323
|
+
const skewBps = netExposure * BigInt(policy.inventorySkewBps ?? 0) / policy.maxInventoryNotional;
|
|
324
|
+
const boundedSkew = skewBps < BigInt(policy.inventorySkewBps ?? 0) ? skewBps : BigInt(policy.inventorySkewBps ?? 0);
|
|
325
|
+
const skew = price * boundedSkew * (netPosition > 0n ? -1n : 1n) / 10000n;
|
|
326
|
+
const costs = BigInt(half + (policy.executionCostBps ?? 0));
|
|
327
|
+
const horizon = policy.fundingHours ?? 1;
|
|
328
|
+
const bidCosts = costs + BigInt(Math.ceil((pricing?.takerFeeBps ?? 0) + Math.max(0, -(pricing?.hourlyFundingBps ?? 0)) * horizon));
|
|
329
|
+
const askCosts = costs + BigInt(Math.ceil((pricing?.takerFeeBps ?? 0) + Math.max(0, pricing?.hourlyFundingBps ?? 0) * horizon));
|
|
330
|
+
const quotes = tiers.map((size) => {
|
|
331
|
+
const units = (size * 100000000000n + price - 1n) / price;
|
|
332
|
+
const sell = pricing ? executionPrice(pricing.book.bids, units) : price;
|
|
333
|
+
const buy = pricing ? executionPrice(pricing.book.asks, units) : price;
|
|
334
|
+
const bidE8 = sell * (10000n - bidCosts) / 10000n + skew;
|
|
335
|
+
const askE8 = (buy * (10000n + askCosts) + 9999n) / 10000n + skew;
|
|
336
|
+
const bound = price * BigInt(market.config.maxQuoteDeviationBps);
|
|
337
|
+
if (bidE8 < 100000000n || bidE8 >= askE8 || abs(bidE8 - price) * 10000n > bound || abs(askE8 - price) * 10000n > bound)
|
|
338
|
+
throw new Error("Executable maker prices exceed oracle deviation bounds");
|
|
339
|
+
return { size, bidE8, askE8 };
|
|
340
|
+
});
|
|
341
|
+
const duration = Math.min(policy.lifetimeSeconds, market.config.maxQuoteLifetimeSeconds);
|
|
342
|
+
return {
|
|
343
|
+
tiers: quotes,
|
|
344
|
+
maxCapacity: capacity,
|
|
345
|
+
expiresAt: now + duration,
|
|
346
|
+
clientNonce: nonce
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/bot/bot.ts
|
|
351
|
+
var terminal = new Set(["filled", "cancelled", "rejected"]);
|
|
352
|
+
var nonce = () => BigInt(`0x${randomBytes3(8).toString("hex")}`);
|
|
353
|
+
|
|
354
|
+
class MakerBot {
|
|
355
|
+
maker;
|
|
356
|
+
policy;
|
|
357
|
+
store;
|
|
358
|
+
hedge;
|
|
359
|
+
state;
|
|
360
|
+
position;
|
|
361
|
+
dirty = true;
|
|
362
|
+
busy = false;
|
|
363
|
+
stopping = false;
|
|
364
|
+
pricing;
|
|
365
|
+
hedgeLease;
|
|
366
|
+
pricingDirty = false;
|
|
367
|
+
constructor(maker, policy, store, hedge) {
|
|
368
|
+
this.maker = maker;
|
|
369
|
+
this.policy = policy;
|
|
370
|
+
this.store = store;
|
|
371
|
+
this.hedge = hedge;
|
|
372
|
+
}
|
|
373
|
+
static async create(options) {
|
|
374
|
+
validateBotPolicy(options.policy);
|
|
375
|
+
const client = options.maker;
|
|
376
|
+
const store = new BotStore(await client.options.connection.getGenesisHash(), client.options.maker.toBase58(), client.options.wallet.publicKey.toBase58(), options.stateDirectory);
|
|
377
|
+
await store.acquire();
|
|
378
|
+
const bot = new MakerBot(client, options.policy, store, options.hedge);
|
|
379
|
+
try {
|
|
380
|
+
bot.state = await store.load();
|
|
381
|
+
if (!options.hedge && bot.state.hedge)
|
|
382
|
+
throw new Error("Restore the configured hedge adapter before restarting a previously hedged maker");
|
|
383
|
+
if (options.hedge) {
|
|
384
|
+
if (bot.state.hedge && bot.state.hedge.identity !== options.hedge.adapter.identity)
|
|
385
|
+
throw new Error("Hedge checkpoint belongs to another account or network");
|
|
386
|
+
bot.hedgeLease = new BotStore(PublicKey.default.toBase58(), new PublicKey(createHash("sha256").update(options.hedge.adapter.identity).digest()).toBase58(), PublicKey.default.toBase58(), options.stateDirectory);
|
|
387
|
+
await bot.hedgeLease.acquire();
|
|
388
|
+
const shared = await bot.hedgeLease.load();
|
|
389
|
+
if (shared.hedge)
|
|
390
|
+
bot.state.hedge = shared.hedge;
|
|
391
|
+
bot.state.hedge ??= {
|
|
392
|
+
identity: options.hedge.adapter.identity,
|
|
393
|
+
nonce: 0,
|
|
394
|
+
history: []
|
|
395
|
+
};
|
|
396
|
+
if (bot.state.hedge.identity !== options.hedge.adapter.identity)
|
|
397
|
+
throw new Error("Shared hedge checkpoint belongs to another account");
|
|
398
|
+
const depthAccount = `${store.genesis}:${client.marketAccount().toBase58()}`;
|
|
399
|
+
if (bot.state.hedge.depthAccount && bot.state.hedge.depthAccount !== depthAccount)
|
|
400
|
+
throw new Error("Hedge account is already assigned to another Depth maker or network");
|
|
401
|
+
bot.state.hedge.depthAccount = depthAccount;
|
|
402
|
+
await bot.saveHedge();
|
|
403
|
+
await store.save(bot.state);
|
|
404
|
+
}
|
|
405
|
+
await bot.reconcile();
|
|
406
|
+
return bot;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
await bot.hedgeLease?.release();
|
|
409
|
+
await store.release();
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async saveHedge() {
|
|
414
|
+
const store = this.hedgeLease;
|
|
415
|
+
await store.save({
|
|
416
|
+
genesis: store.genesis,
|
|
417
|
+
maker: store.maker,
|
|
418
|
+
operator: store.operator,
|
|
419
|
+
fills: {},
|
|
420
|
+
hedge: this.state.hedge
|
|
421
|
+
});
|
|
422
|
+
await this.store.save(this.state);
|
|
423
|
+
}
|
|
424
|
+
async hedgeTick() {
|
|
425
|
+
if (!this.hedge)
|
|
426
|
+
return;
|
|
427
|
+
const account = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
|
|
428
|
+
const confirmed = await this.maker.read("MarketAccount", this.maker.marketAccount());
|
|
429
|
+
let ready = "ready" in account.status && "ready" in confirmed.status && account.version.toString() === confirmed.version.toString();
|
|
430
|
+
if (ready && (!this.position || this.position.version !== BigInt(account.version.toString()) || this.dirty)) {
|
|
431
|
+
const prior = this.position?.version;
|
|
432
|
+
this.position = await this.maker.position.summary({ timeoutMs: 180000 });
|
|
433
|
+
const fresh = await this.maker.read("MarketAccount", this.maker.marketAccount(), "finalized");
|
|
434
|
+
const latest = await this.maker.read("MarketAccount", this.maker.marketAccount());
|
|
435
|
+
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
|
+
if (ready) {
|
|
437
|
+
this.pricingDirty ||= prior !== this.position.version || this.dirty;
|
|
438
|
+
this.dirty = false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const snapshot = await this.hedge.tick(this.state.hedge, () => this.saveHedge(), ready ? this.position?.position : undefined);
|
|
442
|
+
if (snapshot.phase === "pending" || snapshot.phase === "blocked") {
|
|
443
|
+
await this.cancel(true);
|
|
444
|
+
return snapshot.reason ?? "hedge reconciliation pending";
|
|
445
|
+
}
|
|
446
|
+
const [book, costs] = await Promise.all([
|
|
447
|
+
this.hedge.adapter.book(),
|
|
448
|
+
this.hedge.adapter.costs()
|
|
449
|
+
]);
|
|
450
|
+
if (this.pricing) {
|
|
451
|
+
const before = this.pricing.book.bids[0].priceE8 + this.pricing.book.asks[0].priceE8;
|
|
452
|
+
const after = book.bids[0].priceE8 + book.asks[0].priceE8;
|
|
453
|
+
const change = after > before ? after - before : before - after;
|
|
454
|
+
this.pricingDirty ||= change * 10000n >= before * BigInt(this.policy.repriceBps) || costs.takerFeeBps !== this.pricing.takerFeeBps || costs.hourlyFundingBps !== this.pricing.hourlyFundingBps;
|
|
455
|
+
}
|
|
456
|
+
this.pricing = {
|
|
457
|
+
book,
|
|
458
|
+
...costs,
|
|
459
|
+
externalPosition: snapshot.externalPosition,
|
|
460
|
+
maxPriceAgeMs: this.hedge.policy.maxPriceAgeMs
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
snapshot(phase, reason) {
|
|
464
|
+
return {
|
|
465
|
+
phase,
|
|
466
|
+
reason,
|
|
467
|
+
quoteId: this.state?.quote,
|
|
468
|
+
fillCount: Object.keys(this.state?.fills ?? {}).length
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
requestId() {
|
|
472
|
+
const value = this.state?.request?.clientNonce;
|
|
473
|
+
if (value === undefined)
|
|
474
|
+
return;
|
|
475
|
+
const bytes = Buffer.alloc(8);
|
|
476
|
+
bytes.writeBigUInt64LE(value);
|
|
477
|
+
return PublicKey.findProgramAddressSync([
|
|
478
|
+
Buffer.from("quote"),
|
|
479
|
+
this.maker.marketAddress.toBuffer(),
|
|
480
|
+
this.maker.options.maker.toBuffer(),
|
|
481
|
+
bytes
|
|
482
|
+
], DEPTH_PROGRAM_ID)[0];
|
|
483
|
+
}
|
|
484
|
+
async reconcile() {
|
|
485
|
+
const state = this.state;
|
|
486
|
+
let cursor;
|
|
487
|
+
const managed = [];
|
|
488
|
+
do {
|
|
489
|
+
const page = await this.maker.quote.list({ cursor, limit: 100 });
|
|
490
|
+
for (const item of page.items) {
|
|
491
|
+
const quote = await this.maker.quoteState(item.id);
|
|
492
|
+
if (quote.operator.equals(this.maker.options.wallet.publicKey) && !terminal.has(Object.keys(quote.status)[0]))
|
|
493
|
+
managed.push(item.id.toBase58());
|
|
494
|
+
}
|
|
495
|
+
cursor = page.nextCursor ?? undefined;
|
|
496
|
+
} while (cursor);
|
|
497
|
+
const allowed = new Set([
|
|
498
|
+
state.quote,
|
|
499
|
+
state.replacement?.prior,
|
|
500
|
+
this.requestId()?.toBase58()
|
|
501
|
+
].filter(Boolean));
|
|
502
|
+
for (const id of managed)
|
|
503
|
+
if (!allowed.has(id)) {
|
|
504
|
+
const quote = await this.maker.quote.get(new PublicKey(id)).status();
|
|
505
|
+
if (["active", "waiting", "validated", "validating", "closing"].includes(quote.status))
|
|
506
|
+
await this.maker.quote.get(new PublicKey(id)).cancel();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async fills() {
|
|
510
|
+
const state = this.state;
|
|
511
|
+
let before;
|
|
512
|
+
let head;
|
|
513
|
+
const records = new Map;
|
|
514
|
+
do {
|
|
515
|
+
const page = await this.maker.fills.list({
|
|
516
|
+
before,
|
|
517
|
+
until: state.cursor,
|
|
518
|
+
limit: 100
|
|
519
|
+
});
|
|
520
|
+
head ??= page.head;
|
|
521
|
+
for (const fill of page.fills)
|
|
522
|
+
if (!state.fills[fill.id])
|
|
523
|
+
records.set(fill.id, { signature: fill.signature, slot: fill.slot });
|
|
524
|
+
before = page.cursor;
|
|
525
|
+
} while (before);
|
|
526
|
+
if (records.size) {
|
|
527
|
+
for (const [id, value] of records)
|
|
528
|
+
state.fills[id] = value;
|
|
529
|
+
this.dirty = true;
|
|
530
|
+
}
|
|
531
|
+
if (head) {
|
|
532
|
+
state.cursor = head;
|
|
533
|
+
await this.store.save(state);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async cancel(preserve = false) {
|
|
537
|
+
const state = this.state;
|
|
538
|
+
if (!state.quote) {
|
|
539
|
+
const request = this.requestId();
|
|
540
|
+
if (request && await this.maker.options.connection.getAccountInfo(request, "confirmed"))
|
|
541
|
+
state.quote = request.toBase58();
|
|
542
|
+
else {
|
|
543
|
+
if (state.request && (preserve || state.request.expiresAt + 180 > Math.floor(Date.now() / 1000)))
|
|
544
|
+
return false;
|
|
545
|
+
state.request = undefined;
|
|
546
|
+
if (!preserve)
|
|
547
|
+
state.replacement = undefined;
|
|
548
|
+
await this.store.save(state);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const handle = this.maker.quote.get(new PublicKey(state.quote));
|
|
553
|
+
const quote = await handle.status();
|
|
554
|
+
if (terminal.has(quote.status)) {
|
|
555
|
+
state.quote = undefined;
|
|
556
|
+
state.request = undefined;
|
|
557
|
+
state.referencePrice = undefined;
|
|
558
|
+
if (!preserve)
|
|
559
|
+
state.replacement = undefined;
|
|
560
|
+
this.dirty = true;
|
|
561
|
+
await this.store.save(state);
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
if (["active", "waiting", "validated", "validating", "closing"].includes(quote.status))
|
|
565
|
+
await handle.cancel();
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
async tick() {
|
|
569
|
+
if (this.busy)
|
|
570
|
+
throw new Error("Maker bot tick is already running");
|
|
571
|
+
if (!this.state)
|
|
572
|
+
throw new Error("Maker bot has been closed");
|
|
573
|
+
this.busy = true;
|
|
574
|
+
try {
|
|
575
|
+
const state = this.state;
|
|
576
|
+
let [market, lamports] = await Promise.all([
|
|
577
|
+
this.maker.read("Market", this.maker.marketAddress),
|
|
578
|
+
this.maker.options.connection.getBalance(this.maker.options.wallet.publicKey, "confirmed")
|
|
579
|
+
]);
|
|
580
|
+
let now = Math.floor(Date.now() / 1000);
|
|
581
|
+
const reason = this.stopping ? "shutdown" : quoteHealth(market, now) ?? (BigInt(lamports) < (this.policy.gasReserveLamports ?? 50000000n) ? "operator SOL reserve" : undefined);
|
|
582
|
+
if (reason) {
|
|
583
|
+
const stopped = await this.cancel();
|
|
584
|
+
if (this.stopping && this.hedge && state.hedge)
|
|
585
|
+
await this.hedge.tick(state.hedge, () => this.saveHedge(), undefined, undefined, true);
|
|
586
|
+
if (!this.stopping && this.hedge && state.hedge?.pending)
|
|
587
|
+
await this.hedge.tick(state.hedge, () => this.saveHedge(), undefined);
|
|
588
|
+
return this.snapshot(stopped && !state.hedge?.pending ? "stopped" : "pending", reason);
|
|
589
|
+
}
|
|
590
|
+
await this.fills();
|
|
591
|
+
const hedgeReason = await this.hedgeTick();
|
|
592
|
+
if (hedgeReason)
|
|
593
|
+
return this.snapshot("pending", hedgeReason);
|
|
594
|
+
if (this.hedge) {
|
|
595
|
+
market = await this.maker.read("Market", this.maker.marketAddress);
|
|
596
|
+
now = Math.floor(Date.now() / 1000);
|
|
597
|
+
const changed = quoteHealth(market, now);
|
|
598
|
+
if (changed) {
|
|
599
|
+
const stopped = await this.cancel();
|
|
600
|
+
return this.snapshot(stopped ? "stopped" : "pending", changed);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (!state.quote && this.requestId() && await this.maker.options.connection.getAccountInfo(this.requestId(), "confirmed")) {
|
|
604
|
+
state.quote = this.requestId().toBase58();
|
|
605
|
+
await this.store.save(state);
|
|
606
|
+
}
|
|
607
|
+
if (state.quote) {
|
|
608
|
+
const handle2 = this.maker.quote.get(new PublicKey(state.quote));
|
|
609
|
+
const quote = await handle2.status();
|
|
610
|
+
if (terminal.has(quote.status)) {
|
|
611
|
+
state.quote = undefined;
|
|
612
|
+
state.request = undefined;
|
|
613
|
+
this.dirty = true;
|
|
614
|
+
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
|
+
} else if (["validating", "validated", "waiting", "reserving"].includes(quote.status)) {
|
|
622
|
+
const current = await this.maker.quoteState(handle2.id);
|
|
623
|
+
if (current.expiry.toNumber() <= now && ["validating", "validated", "waiting"].includes(quote.status))
|
|
624
|
+
await this.cancel();
|
|
625
|
+
else
|
|
626
|
+
await handle2.activate({ timeoutMs: 0 });
|
|
627
|
+
return this.snapshot("pending", "activating quote");
|
|
628
|
+
} else if (["closing", "releasing"].includes(quote.status)) {
|
|
629
|
+
await this.cancel();
|
|
630
|
+
return this.snapshot("pending", "releasing quote");
|
|
631
|
+
} else if (quote.status === "active") {
|
|
632
|
+
const price = BigInt(market.oraclePriceE8.toString());
|
|
633
|
+
const difference = state.referencePrice === undefined ? undefined : (price > state.referencePrice ? price - state.referencePrice : state.referencePrice - price) * 10000n;
|
|
634
|
+
const current = await this.maker.quoteState(handle2.id);
|
|
635
|
+
const duration = Math.min(this.policy.lifetimeSeconds, market.config.maxQuoteLifetimeSeconds);
|
|
636
|
+
const refresh = current.expiry.toNumber() - now <= duration - Math.min(this.policy.refreshSeconds, duration - 1);
|
|
637
|
+
if (!this.dirty && !this.pricingDirty && !refresh && difference !== undefined && difference < BigInt(this.policy.repriceBps) * state.referencePrice)
|
|
638
|
+
return this.snapshot("active");
|
|
639
|
+
state.replacement = { prior: state.quote, nonce: nonce() };
|
|
640
|
+
this.pricingDirty = false;
|
|
641
|
+
await this.store.save(state);
|
|
642
|
+
await this.maker.quote.replacement(handle2.id, state.replacement.nonce).begin();
|
|
643
|
+
await this.cancel(true);
|
|
644
|
+
return this.snapshot("pending", "repricing quote");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (market.pendingOrders > 0)
|
|
648
|
+
return this.snapshot("idle", "settlement in progress");
|
|
649
|
+
const account = await this.maker.read("MarketAccount", this.maker.marketAccount());
|
|
650
|
+
if (!("ready" in account.status))
|
|
651
|
+
return this.snapshot("idle", "maker account busy");
|
|
652
|
+
if (this.dirty || !this.position || this.position.version !== BigInt(account.version.toString())) {
|
|
653
|
+
this.position = await this.maker.position.summary({
|
|
654
|
+
timeoutMs: 180000
|
|
655
|
+
});
|
|
656
|
+
if (this.position.status !== "ready" || this.position.version !== BigInt(account.version.toString()))
|
|
657
|
+
return this.snapshot("idle", "maker view changed");
|
|
658
|
+
this.dirty = false;
|
|
659
|
+
return this.snapshot("idle", "maker view refreshed");
|
|
660
|
+
}
|
|
661
|
+
if (!state.request) {
|
|
662
|
+
const request2 = botQuote(this.policy, market, this.position, state.replacement?.nonce ?? nonce(), now, this.pricing);
|
|
663
|
+
if (!request2)
|
|
664
|
+
return this.snapshot("idle", "inventory or collateral limit");
|
|
665
|
+
state.request = request2;
|
|
666
|
+
this.pricingDirty = false;
|
|
667
|
+
state.referencePrice = BigInt(market.oraclePriceE8.toString());
|
|
668
|
+
await this.store.save(state);
|
|
669
|
+
}
|
|
670
|
+
const request = state.request;
|
|
671
|
+
if (request.expiresAt <= now) {
|
|
672
|
+
state.request = undefined;
|
|
673
|
+
await this.store.save(state);
|
|
674
|
+
return this.snapshot("idle", "prepared quote expired");
|
|
675
|
+
}
|
|
676
|
+
const handle = state.replacement ? await this.maker.quote.replacement(new PublicKey(state.replacement.prior), state.replacement.nonce).resume(request, { timeoutMs: 0 }) : await this.maker.quote.publish(request);
|
|
677
|
+
state.quote = handle.id.toBase58();
|
|
678
|
+
state.replacement = undefined;
|
|
679
|
+
await this.store.save(state);
|
|
680
|
+
await handle.activate({ timeoutMs: 0 });
|
|
681
|
+
return this.snapshot("pending", "quote submitted");
|
|
682
|
+
} catch (error) {
|
|
683
|
+
this.dirty = true;
|
|
684
|
+
try {
|
|
685
|
+
await this.cancel(true);
|
|
686
|
+
} catch (cause) {
|
|
687
|
+
throw new AggregateError([error, cause], "Maker bot failed and quote cancellation could not be confirmed");
|
|
688
|
+
}
|
|
689
|
+
throw error;
|
|
690
|
+
} finally {
|
|
691
|
+
this.busy = false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
stop() {
|
|
695
|
+
this.stopping = true;
|
|
696
|
+
}
|
|
697
|
+
async close() {
|
|
698
|
+
if (this.busy)
|
|
699
|
+
throw new Error("Wait for the active maker tick before closing");
|
|
700
|
+
await this.store.release();
|
|
701
|
+
await this.hedgeLease?.release();
|
|
702
|
+
this.state = undefined;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
var create_maker_bot = MakerBot.create;
|
|
706
|
+
// src/hedge/engine.ts
|
|
707
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
708
|
+
class HedgeEngine {
|
|
709
|
+
adapter;
|
|
710
|
+
policy;
|
|
711
|
+
snapshot;
|
|
712
|
+
busy = false;
|
|
713
|
+
constructor(adapter, policy) {
|
|
714
|
+
this.adapter = adapter;
|
|
715
|
+
this.policy = policy;
|
|
716
|
+
validateHedgePolicy(policy);
|
|
717
|
+
if (adapter.sizeStep <= 0n || adapter.minNotional <= 0n)
|
|
718
|
+
throw new Error("External order limits are invalid");
|
|
719
|
+
}
|
|
720
|
+
async tick(state, save, depthPosition, timestamp, reconcileOnly = false) {
|
|
721
|
+
if (this.busy)
|
|
722
|
+
throw new Error("Hedge engine tick is already running");
|
|
723
|
+
if (state.identity !== this.adapter.identity)
|
|
724
|
+
throw new Error("Hedge checkpoint belongs to another account or network");
|
|
725
|
+
this.busy = true;
|
|
726
|
+
const clock = () => timestamp ?? Date.now();
|
|
727
|
+
let now = clock();
|
|
728
|
+
const result = (phase, reason, externalPosition) => {
|
|
729
|
+
this.snapshot = {
|
|
730
|
+
venue: "hyperliquid",
|
|
731
|
+
network: this.adapter.network,
|
|
732
|
+
account: this.adapter.address,
|
|
733
|
+
updatedAt: now,
|
|
734
|
+
phase,
|
|
735
|
+
reason,
|
|
736
|
+
depthPosition,
|
|
737
|
+
externalPosition,
|
|
738
|
+
unhedgedPosition: externalPosition === undefined || depthPosition === undefined ? undefined : depthPosition + externalPosition,
|
|
739
|
+
pending: state.pending,
|
|
740
|
+
history: state.history.slice(-50)
|
|
741
|
+
};
|
|
742
|
+
return this.snapshot;
|
|
743
|
+
};
|
|
744
|
+
try {
|
|
745
|
+
if (state.pending) {
|
|
746
|
+
const order2 = state.pending;
|
|
747
|
+
const receipt2 = await this.adapter.receipt(order2);
|
|
748
|
+
now = clock();
|
|
749
|
+
if (receipt2.status === "unknown") {
|
|
750
|
+
if (reconcileOnly)
|
|
751
|
+
return result("blocked", "Paused with an unresolved hedge receipt; no order will be resubmitted");
|
|
752
|
+
if (now >= order2.expiresAt)
|
|
753
|
+
return result("blocked", "Hedge submission is unresolved; reconcile the persisted order before trading");
|
|
754
|
+
const retry = await this.adapter.submit(order2);
|
|
755
|
+
now = clock();
|
|
756
|
+
if (retry.status === "unknown" || retry.status === "open")
|
|
757
|
+
return result("pending", "Hedge confirmation pending");
|
|
758
|
+
state.history.push({
|
|
759
|
+
...order2,
|
|
760
|
+
...retry,
|
|
761
|
+
completedAt: Math.max(now, order2.nonce)
|
|
762
|
+
});
|
|
763
|
+
} else if (receipt2.status === "open") {
|
|
764
|
+
state.nonce = Math.max(now, state.nonce + 1);
|
|
765
|
+
await save();
|
|
766
|
+
await this.adapter.cancel(order2, state.nonce);
|
|
767
|
+
return result("pending", "Cancelling an outstanding hedge order");
|
|
768
|
+
} else {
|
|
769
|
+
state.history.push({
|
|
770
|
+
...order2,
|
|
771
|
+
...receipt2,
|
|
772
|
+
completedAt: Math.max(now, order2.nonce)
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
state.history = state.history.slice(-100);
|
|
776
|
+
state.pending = undefined;
|
|
777
|
+
await save();
|
|
778
|
+
}
|
|
779
|
+
if (reconcileOnly) {
|
|
780
|
+
const account2 = await this.adapter.account();
|
|
781
|
+
return result("waiting", "Automation paused; existing positions remain open", account2.position);
|
|
782
|
+
}
|
|
783
|
+
if (depthPosition === undefined)
|
|
784
|
+
return result("pending", "Waiting for finalized Depth inventory");
|
|
785
|
+
const [account, book] = await Promise.all([
|
|
786
|
+
this.adapter.account(),
|
|
787
|
+
this.adapter.book()
|
|
788
|
+
]);
|
|
789
|
+
now = clock();
|
|
790
|
+
validateBook(book, now, this.policy.maxPriceAgeMs);
|
|
791
|
+
if (!Number.isSafeInteger(account.timestamp) || account.timestamp > now + 1000 || now - account.timestamp > this.policy.maxPriceAgeMs)
|
|
792
|
+
throw new Error("External account snapshot is stale");
|
|
793
|
+
if (!Number.isInteger(account.leverage) || account.leverage < 1 || account.leverage > this.policy.maxLeverage)
|
|
794
|
+
throw new Error("External leverage exceeds hedge policy");
|
|
795
|
+
const latest = state.history[state.history.length - 1];
|
|
796
|
+
if (latest && account.timestamp < latest.completedAt)
|
|
797
|
+
return result("pending", "Waiting for the external account to include the confirmed hedge", account.position);
|
|
798
|
+
if (account.openOrders.length)
|
|
799
|
+
return result("blocked", "Hedge account has unmanaged open orders", account.position);
|
|
800
|
+
const price = (book.bids[0].priceE8 + book.asks[0].priceE8) / 2n;
|
|
801
|
+
const target = -(depthPosition * BigInt(this.policy.ratioBps) / 10000n / this.adapter.sizeStep) * this.adapter.sizeStep;
|
|
802
|
+
const delta = target - account.position;
|
|
803
|
+
const risk = notional(depthPosition + account.position, price);
|
|
804
|
+
const difference = notional(delta, price);
|
|
805
|
+
const hard = risk >= this.policy.maxUnhedgedNotional;
|
|
806
|
+
if (difference === 0n) {
|
|
807
|
+
state.unhedgedSince = undefined;
|
|
808
|
+
await save();
|
|
809
|
+
return result(hard ? "blocked" : "balanced", hard ? "Unhedged exposure exceeds policy" : undefined, account.position);
|
|
810
|
+
}
|
|
811
|
+
state.unhedgedSince ??= now;
|
|
812
|
+
await save();
|
|
813
|
+
const age = now - state.unhedgedSince;
|
|
814
|
+
if (!hard && age < this.policy.maxUnhedgedMs && (difference < this.policy.thresholdNotional || age < this.policy.batchMs))
|
|
815
|
+
return result("waiting", "Net exposure is within the batching window", account.position);
|
|
816
|
+
const buy = delta > 0n;
|
|
817
|
+
const levels = buy ? book.asks : book.bids;
|
|
818
|
+
const rawLimit = levels[0].priceE8 * BigInt(1e4 + (buy ? 1 : -1) * this.policy.maxSlippageBps) / 10000n;
|
|
819
|
+
const limit = this.adapter.price(rawLimit, buy);
|
|
820
|
+
let capacity = this.policy.maxOrderNotional * 100000000000n / limit;
|
|
821
|
+
const available = levels.filter((level) => buy ? level.priceE8 <= limit : level.priceE8 >= limit).reduce((sum, level) => sum + level.size, 0n);
|
|
822
|
+
capacity = capacity < available ? capacity : available;
|
|
823
|
+
const reducing = account.position !== 0n && account.position > 0n !== buy;
|
|
824
|
+
if (reducing)
|
|
825
|
+
capacity = capacity < abs(account.position) ? capacity : abs(account.position);
|
|
826
|
+
else {
|
|
827
|
+
if (!Number.isInteger(account.leverage) || account.leverage < 1 || account.availableMargin < 0n)
|
|
828
|
+
throw new Error("External margin state is invalid");
|
|
829
|
+
const marginCapacity = account.availableMargin * BigInt(account.leverage) * 99n * 100000000000n / (100n * limit);
|
|
830
|
+
capacity = capacity < marginCapacity ? capacity : marginCapacity;
|
|
831
|
+
}
|
|
832
|
+
let size = abs(delta) < capacity ? abs(delta) : capacity;
|
|
833
|
+
size = size / this.adapter.sizeStep * this.adapter.sizeStep;
|
|
834
|
+
if (size === 0n || !reducing && notional(size, limit) < this.adapter.minNotional)
|
|
835
|
+
return result(hard || age >= this.policy.maxUnhedgedMs ? "blocked" : "waiting", "Hedge size is below venue limits or available margin", account.position);
|
|
836
|
+
const order = {
|
|
837
|
+
cloid: `0x${randomBytes4(16).toString("hex")}`,
|
|
838
|
+
nonce: Math.max(now, state.nonce + 1),
|
|
839
|
+
expiresAt: now + this.policy.orderTtlMs,
|
|
840
|
+
buy,
|
|
841
|
+
size,
|
|
842
|
+
limitPriceE8: limit,
|
|
843
|
+
reduceOnly: reducing
|
|
844
|
+
};
|
|
845
|
+
state.nonce = order.nonce;
|
|
846
|
+
state.pending = order;
|
|
847
|
+
await save();
|
|
848
|
+
const receipt = await this.adapter.submit(order);
|
|
849
|
+
now = clock();
|
|
850
|
+
if (receipt.status !== "unknown" && receipt.status !== "open") {
|
|
851
|
+
state.history.push({
|
|
852
|
+
...order,
|
|
853
|
+
...receipt,
|
|
854
|
+
completedAt: Math.max(now, order.nonce)
|
|
855
|
+
});
|
|
856
|
+
state.history = state.history.slice(-100);
|
|
857
|
+
state.pending = undefined;
|
|
858
|
+
await save();
|
|
859
|
+
}
|
|
860
|
+
return result(receipt.status === "rejected" ? "blocked" : "pending", receipt.reason ?? "Hedge submitted; reconciling confirmed exposure", account.position);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
result("blocked", error instanceof Error ? error.message : "External hedge service unavailable");
|
|
863
|
+
throw error;
|
|
864
|
+
} finally {
|
|
865
|
+
this.busy = false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// src/hedge/hyperliquid.ts
|
|
870
|
+
import { HttpTransport, InfoClient } from "@nktkas/hyperliquid";
|
|
871
|
+
import {
|
|
872
|
+
canonicalize,
|
|
873
|
+
getWalletAddress,
|
|
874
|
+
signL1Action
|
|
875
|
+
} from "@nktkas/hyperliquid/signing";
|
|
876
|
+
import {
|
|
877
|
+
OrderRequest,
|
|
878
|
+
CancelByCloidRequest
|
|
879
|
+
} from "@nktkas/hyperliquid/api/exchange";
|
|
880
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
881
|
+
|
|
882
|
+
// src/hedge/amount.ts
|
|
883
|
+
function units(value, decimals, exact = true) {
|
|
884
|
+
if (!/^-?\d+(?:\.\d+)?$/.test(value))
|
|
885
|
+
throw new Error("External decimal is invalid");
|
|
886
|
+
const negative = value.startsWith("-");
|
|
887
|
+
const [whole, fraction = ""] = value.replace(/^-/, "").split(".");
|
|
888
|
+
const extra = fraction.slice(decimals);
|
|
889
|
+
if (exact && /[1-9]/.test(extra))
|
|
890
|
+
throw new Error("External decimal exceeds precision");
|
|
891
|
+
const amount = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction.slice(0, decimals).padEnd(decimals, "0") || "0");
|
|
892
|
+
return negative ? -amount - (!exact && /[1-9]/.test(extra) ? 1n : 0n) : amount;
|
|
893
|
+
}
|
|
894
|
+
function decimal(value, decimals) {
|
|
895
|
+
const magnitude = value < 0n ? -value : value;
|
|
896
|
+
const base = 10n ** BigInt(decimals);
|
|
897
|
+
const fraction = (magnitude % base).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
898
|
+
return `${value < 0n ? "-" : ""}${magnitude / base}${fraction ? `.${fraction}` : ""}`;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/hedge/hyperliquid.ts
|
|
902
|
+
class HyperliquidAdapter {
|
|
903
|
+
network;
|
|
904
|
+
address;
|
|
905
|
+
wallet;
|
|
906
|
+
transport;
|
|
907
|
+
info;
|
|
908
|
+
asset;
|
|
909
|
+
sizeDecimals;
|
|
910
|
+
minNotional = 10000000n;
|
|
911
|
+
identity;
|
|
912
|
+
sizeStep;
|
|
913
|
+
cache = new Map;
|
|
914
|
+
cached(key, ttl, load) {
|
|
915
|
+
const prior = this.cache.get(key);
|
|
916
|
+
if (prior && prior.expiresAt > Date.now())
|
|
917
|
+
return prior.value;
|
|
918
|
+
const value = load().catch((error) => {
|
|
919
|
+
this.cache.delete(key);
|
|
920
|
+
throw error;
|
|
921
|
+
});
|
|
922
|
+
this.cache.set(key, { expiresAt: Date.now() + ttl, value });
|
|
923
|
+
return value;
|
|
924
|
+
}
|
|
925
|
+
constructor(network, address, wallet, transport, info, asset, sizeDecimals) {
|
|
926
|
+
this.network = network;
|
|
927
|
+
this.address = address;
|
|
928
|
+
this.wallet = wallet;
|
|
929
|
+
this.transport = transport;
|
|
930
|
+
this.info = info;
|
|
931
|
+
this.asset = asset;
|
|
932
|
+
this.sizeDecimals = sizeDecimals;
|
|
933
|
+
this.identity = `hyperliquid:${network}:${address.toLowerCase()}:SOL`;
|
|
934
|
+
this.sizeStep = 10n ** BigInt(9 - sizeDecimals);
|
|
935
|
+
}
|
|
936
|
+
static async create(options) {
|
|
937
|
+
if (Boolean(options.wallet) === Boolean(options.agentKey))
|
|
938
|
+
throw new Error("Provide one Hyperliquid agent signer source");
|
|
939
|
+
const wallet = options.wallet ?? privateKeyToAccount(options.agentKey);
|
|
940
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(options.account))
|
|
941
|
+
throw new Error("Hyperliquid account address is invalid");
|
|
942
|
+
const network = options.network ?? "testnet";
|
|
943
|
+
const transport = new HttpTransport({
|
|
944
|
+
isTestnet: network === "testnet",
|
|
945
|
+
timeout: 1e4
|
|
946
|
+
});
|
|
947
|
+
const info = new InfoClient({ transport });
|
|
948
|
+
const signer = await getWalletAddress(wallet);
|
|
949
|
+
if (signer.toLowerCase() === options.account.toLowerCase())
|
|
950
|
+
throw new Error("Use a dedicated approved Hyperliquid agent wallet");
|
|
951
|
+
const [meta, agents] = await Promise.all([
|
|
952
|
+
info.meta(),
|
|
953
|
+
info.extraAgents({ user: options.account })
|
|
954
|
+
]);
|
|
955
|
+
if (!agents.some((agent) => agent.address.toLowerCase() === signer.toLowerCase() && (agent.validUntil === null || agent.validUntil > Date.now())))
|
|
956
|
+
throw new Error("Hyperliquid agent is not approved or has expired");
|
|
957
|
+
const asset = meta.universe.findIndex((item) => item.name === "SOL" && !item.isDelisted);
|
|
958
|
+
if (asset < 0)
|
|
959
|
+
throw new Error("Hyperliquid SOL perpetual is unavailable");
|
|
960
|
+
const sizeDecimals = meta.universe[asset].szDecimals;
|
|
961
|
+
if (!Number.isInteger(sizeDecimals) || sizeDecimals < 0 || sizeDecimals > 6)
|
|
962
|
+
throw new Error("Unsupported Hyperliquid SOL precision");
|
|
963
|
+
return new HyperliquidAdapter(network, options.account, wallet, transport, info, asset, sizeDecimals);
|
|
964
|
+
}
|
|
965
|
+
async book() {
|
|
966
|
+
const book = await this.cached("book", 500, () => this.info.l2Book({ coin: "SOL" }));
|
|
967
|
+
if (!book)
|
|
968
|
+
throw new Error("Hyperliquid SOL order book is unavailable");
|
|
969
|
+
const levels = (side) => side.map((level) => ({
|
|
970
|
+
priceE8: units(level.px, 8),
|
|
971
|
+
size: units(level.sz, 9)
|
|
972
|
+
}));
|
|
973
|
+
return {
|
|
974
|
+
timestamp: book.time,
|
|
975
|
+
bids: levels(book.levels[0]),
|
|
976
|
+
asks: levels(book.levels[1])
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
async account() {
|
|
980
|
+
const [state, orders, data] = await Promise.all([
|
|
981
|
+
this.info.clearinghouseState({ user: this.address }),
|
|
982
|
+
this.cached("orders", 5000, () => this.info.openOrders({ user: this.address })),
|
|
983
|
+
this.cached("asset", 30000, () => this.info.activeAssetData({ user: this.address, coin: "SOL" }))
|
|
984
|
+
]);
|
|
985
|
+
if (state.assetPositions.some((entry) => entry.position.coin !== "SOL" && units(entry.position.szi, 9) !== 0n))
|
|
986
|
+
throw new Error("Use a dedicated SOL hedge account without unrelated positions");
|
|
987
|
+
const position = state.assetPositions.find((entry) => entry.position.coin === "SOL")?.position;
|
|
988
|
+
if (data.leverage.type !== "cross")
|
|
989
|
+
throw new Error("The hedge adapter requires cross margin on its dedicated account");
|
|
990
|
+
const value = units(state.marginSummary.accountValue, 6, false);
|
|
991
|
+
const used = units(state.marginSummary.totalMarginUsed, 6, false) + 1n;
|
|
992
|
+
return {
|
|
993
|
+
timestamp: state.time,
|
|
994
|
+
position: position ? units(position.szi, 9) : 0n,
|
|
995
|
+
availableMargin: value > used ? value - used : 0n,
|
|
996
|
+
leverage: data.leverage.value,
|
|
997
|
+
openOrders: orders.map((order) => String(order.oid))
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
async costs() {
|
|
1001
|
+
const [fees, data] = await Promise.all([
|
|
1002
|
+
this.cached("fees", 60000, () => this.info.userFees({ user: this.address })),
|
|
1003
|
+
this.cached("funding", 1e4, () => this.info.metaAndAssetCtxs())
|
|
1004
|
+
]);
|
|
1005
|
+
const index = data[0].universe.findIndex((item) => item.name === "SOL");
|
|
1006
|
+
if (index < 0)
|
|
1007
|
+
throw new Error("Hyperliquid SOL funding is unavailable");
|
|
1008
|
+
const bps = (value) => Number(units(value, 12)) / 1e8;
|
|
1009
|
+
return {
|
|
1010
|
+
takerFeeBps: bps(fees.userCrossRate),
|
|
1011
|
+
hourlyFundingBps: bps(data[1][index].funding)
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
price(value, buy) {
|
|
1015
|
+
if (value <= 0n)
|
|
1016
|
+
throw new Error("Hedge price is invalid");
|
|
1017
|
+
const digits = (value / 100000000n).toString().length;
|
|
1018
|
+
const decimals = Math.min(6 - this.sizeDecimals, Math.max(0, 5 - digits));
|
|
1019
|
+
const tick = 10n ** BigInt(8 - decimals);
|
|
1020
|
+
return (buy ? value / tick : (value + tick - 1n) / tick) * tick;
|
|
1021
|
+
}
|
|
1022
|
+
async submit(order) {
|
|
1023
|
+
const outstanding = await this.info.openOrders({ user: this.address });
|
|
1024
|
+
if (outstanding.some((entry) => entry.cloid !== order.cloid))
|
|
1025
|
+
throw new Error("Hedge account has unmanaged open orders");
|
|
1026
|
+
const action = canonicalize(OrderRequest.entries.action, {
|
|
1027
|
+
type: "order",
|
|
1028
|
+
orders: [
|
|
1029
|
+
{
|
|
1030
|
+
a: this.asset,
|
|
1031
|
+
b: order.buy,
|
|
1032
|
+
p: decimal(order.limitPriceE8, 8),
|
|
1033
|
+
s: decimal(order.size, 9),
|
|
1034
|
+
r: order.reduceOnly,
|
|
1035
|
+
t: { limit: { tif: "Ioc" } },
|
|
1036
|
+
c: order.cloid
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
grouping: "na"
|
|
1040
|
+
});
|
|
1041
|
+
const payload = {
|
|
1042
|
+
action,
|
|
1043
|
+
nonce: order.nonce,
|
|
1044
|
+
expiresAfter: order.expiresAt
|
|
1045
|
+
};
|
|
1046
|
+
const signature = await signL1Action({
|
|
1047
|
+
...payload,
|
|
1048
|
+
wallet: this.wallet,
|
|
1049
|
+
isTestnet: this.network === "testnet"
|
|
1050
|
+
});
|
|
1051
|
+
const response = await this.transport.request("exchange", { ...payload, signature });
|
|
1052
|
+
if (response.status !== "ok") {
|
|
1053
|
+
const message = typeof response.response === "string" ? response.response : "Hyperliquid order rejected";
|
|
1054
|
+
if (/nonce|duplicate/i.test(message))
|
|
1055
|
+
return {
|
|
1056
|
+
status: "unknown",
|
|
1057
|
+
filledSize: 0n,
|
|
1058
|
+
reason: "Persisted hedge nonce already submitted; reconciling"
|
|
1059
|
+
};
|
|
1060
|
+
return { status: "rejected", filledSize: 0n, reason: message };
|
|
1061
|
+
}
|
|
1062
|
+
const result = response.response;
|
|
1063
|
+
const status = result.data?.statuses?.[0];
|
|
1064
|
+
if (status?.error)
|
|
1065
|
+
return { status: "rejected", filledSize: 0n, reason: status.error };
|
|
1066
|
+
return this.receipt(order);
|
|
1067
|
+
}
|
|
1068
|
+
async receipt(order) {
|
|
1069
|
+
const status = await this.info.orderStatus({
|
|
1070
|
+
user: this.address,
|
|
1071
|
+
oid: order.cloid
|
|
1072
|
+
});
|
|
1073
|
+
if (status.status !== "order")
|
|
1074
|
+
return { status: "unknown", filledSize: 0n };
|
|
1075
|
+
const detail = status.order;
|
|
1076
|
+
if (detail.order.coin !== "SOL" || detail.order.cloid !== order.cloid || detail.order.side === "B" !== order.buy || units(detail.order.origSz, 9) !== order.size || units(detail.order.limitPx, 8) !== order.limitPriceE8 || detail.order.reduceOnly !== order.reduceOnly)
|
|
1077
|
+
throw new Error("Hyperliquid order does not match the hedge checkpoint");
|
|
1078
|
+
if (detail.status === "open" || detail.status === "triggered")
|
|
1079
|
+
return { status: "open", orderId: detail.order.oid, filledSize: 0n };
|
|
1080
|
+
const fills = await this.info.userFillsByTime({
|
|
1081
|
+
user: this.address,
|
|
1082
|
+
startTime: order.nonce - 1000,
|
|
1083
|
+
endTime: Date.now()
|
|
1084
|
+
});
|
|
1085
|
+
if (fills.length >= 2000)
|
|
1086
|
+
throw new Error("Hyperliquid fill window exceeds the reconciliation limit");
|
|
1087
|
+
const seen = new Set;
|
|
1088
|
+
const matching = fills.filter((fill) => {
|
|
1089
|
+
if (fill.oid !== detail.order.oid || fill.coin !== "SOL" || seen.has(fill.tid))
|
|
1090
|
+
return false;
|
|
1091
|
+
seen.add(fill.tid);
|
|
1092
|
+
return true;
|
|
1093
|
+
});
|
|
1094
|
+
const filledSize = matching.reduce((sum, fill) => sum + units(fill.sz, 9), 0n);
|
|
1095
|
+
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)))
|
|
1096
|
+
throw new Error("Hyperliquid fill does not match the hedge order");
|
|
1097
|
+
if (filledSize > order.size)
|
|
1098
|
+
throw new Error("Hyperliquid filled size exceeds the hedge order");
|
|
1099
|
+
if (detail.status === "filled" && filledSize !== order.size)
|
|
1100
|
+
return {
|
|
1101
|
+
status: "unknown",
|
|
1102
|
+
orderId: detail.order.oid,
|
|
1103
|
+
filledSize,
|
|
1104
|
+
reason: "Hedge fill indexing is incomplete"
|
|
1105
|
+
};
|
|
1106
|
+
const cost = matching.reduce((sum, fill) => sum + units(fill.sz, 9) * units(fill.px, 8), 0n);
|
|
1107
|
+
return {
|
|
1108
|
+
status: detail.status === "filled" ? "filled" : /rejected/i.test(detail.status) ? "rejected" : "cancelled",
|
|
1109
|
+
orderId: detail.order.oid,
|
|
1110
|
+
filledSize,
|
|
1111
|
+
averagePriceE8: filledSize ? cost / filledSize : undefined,
|
|
1112
|
+
fee: matching.reduce((sum, fill) => sum + units(fill.fee, 6), 0n),
|
|
1113
|
+
hashes: [...new Set(matching.map((fill) => fill.hash))],
|
|
1114
|
+
reason: detail.status
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
async cancel(order, nonce2 = order.nonce + 1) {
|
|
1118
|
+
const action = canonicalize(CancelByCloidRequest.entries.action, {
|
|
1119
|
+
type: "cancelByCloid",
|
|
1120
|
+
cancels: [{ asset: this.asset, cloid: order.cloid }]
|
|
1121
|
+
});
|
|
1122
|
+
const signature = await signL1Action({
|
|
1123
|
+
wallet: this.wallet,
|
|
1124
|
+
action,
|
|
1125
|
+
nonce: nonce2,
|
|
1126
|
+
isTestnet: this.network === "testnet"
|
|
1127
|
+
});
|
|
1128
|
+
await this.transport.request("exchange", { action, nonce: nonce2, signature });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
var create_hyperliquid = HyperliquidAdapter.create;
|
|
1132
|
+
// src/hedge/setup.ts
|
|
1133
|
+
import { ExchangeClient, HttpTransport as HttpTransport2, InfoClient as InfoClient2 } from "@nktkas/hyperliquid";
|
|
1134
|
+
import {
|
|
1135
|
+
getWalletAddress as getWalletAddress2
|
|
1136
|
+
} from "@nktkas/hyperliquid/signing";
|
|
1137
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
1138
|
+
async function setup_hyperliquid(options) {
|
|
1139
|
+
if (Boolean(options.wallet) === Boolean(options.accountKey))
|
|
1140
|
+
throw new Error("Provide one Hyperliquid account signer source");
|
|
1141
|
+
if (!Number.isInteger(options.leverage) || options.leverage < 1 || options.leverage > 10)
|
|
1142
|
+
throw new Error("Setup leverage must be 1 to 10");
|
|
1143
|
+
if (Boolean(options.agent) === Boolean(options.agentKey))
|
|
1144
|
+
throw new Error("Provide one Hyperliquid agent identity source");
|
|
1145
|
+
const agent = options.agent ?? privateKeyToAccount2(options.agentKey).address;
|
|
1146
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(agent))
|
|
1147
|
+
throw new Error("Hyperliquid agent address is invalid");
|
|
1148
|
+
const wallet = options.wallet ?? privateKeyToAccount2(options.accountKey);
|
|
1149
|
+
const account = await getWalletAddress2(wallet);
|
|
1150
|
+
if (options.account && account.toLowerCase() !== options.account.toLowerCase())
|
|
1151
|
+
throw new Error("The setup signer does not own the configured Hyperliquid account");
|
|
1152
|
+
if (account.toLowerCase() === agent.toLowerCase())
|
|
1153
|
+
throw new Error("The account and agent must be separate wallets");
|
|
1154
|
+
const transport = new HttpTransport2({
|
|
1155
|
+
isTestnet: (options.network ?? "testnet") === "testnet"
|
|
1156
|
+
});
|
|
1157
|
+
const info = new InfoClient2({ transport });
|
|
1158
|
+
const state = await info.clearinghouseState({ user: account });
|
|
1159
|
+
if (units(state.marginSummary.accountValue, 6, false) < 10000000n)
|
|
1160
|
+
throw new Error(`Fund Hyperliquid ${options.network ?? "testnet"} account ${account} with at least 10 USDC before setup`);
|
|
1161
|
+
const client = new ExchangeClient({ wallet, transport });
|
|
1162
|
+
const agents = await info.extraAgents({ user: account });
|
|
1163
|
+
if (!agents.some((entry) => entry.address.toLowerCase() === agent.toLowerCase() && (entry.validUntil === null || entry.validUntil > Date.now())))
|
|
1164
|
+
await client.approveAgent({
|
|
1165
|
+
agentAddress: agent,
|
|
1166
|
+
agentName: options.agentName ?? "Depth maker"
|
|
1167
|
+
});
|
|
1168
|
+
const meta = await info.meta();
|
|
1169
|
+
const asset = meta.universe.findIndex((entry) => entry.name === "SOL" && !entry.isDelisted);
|
|
1170
|
+
if (asset < 0)
|
|
1171
|
+
throw new Error("Hyperliquid SOL perpetual is unavailable");
|
|
1172
|
+
await client.updateLeverage({
|
|
1173
|
+
asset,
|
|
1174
|
+
isCross: true,
|
|
1175
|
+
leverage: options.leverage
|
|
1176
|
+
});
|
|
1177
|
+
return {
|
|
1178
|
+
account,
|
|
1179
|
+
agent,
|
|
1180
|
+
network: options.network ?? "testnet",
|
|
1181
|
+
leverage: options.leverage
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
export {
|
|
1185
|
+
validateHedgePolicy,
|
|
1186
|
+
validateBotPolicy,
|
|
1187
|
+
setup_hyperliquid,
|
|
1188
|
+
create_maker_bot,
|
|
1189
|
+
create_hyperliquid,
|
|
1190
|
+
MakerBot,
|
|
1191
|
+
HyperliquidAdapter,
|
|
1192
|
+
HedgeEngine,
|
|
1193
|
+
FillCursorStore,
|
|
1194
|
+
BotStore
|
|
1195
|
+
};
|