@madeinusmate/grvt-cli 0.1.0
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/README.md +1154 -0
- package/dist/chunk-I7C5KKUG.js +46 -0
- package/dist/chunk-I7C5KKUG.js.map +1 -0
- package/dist/cli.js +1880 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +771 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +462 -0
- package/dist/index.d.ts +462 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-LBUHKB7A.js +14 -0
- package/dist/schema-LBUHKB7A.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1880 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_CONFIG,
|
|
4
|
+
SECRET_KEYS,
|
|
5
|
+
configSchema,
|
|
6
|
+
redactConfig
|
|
7
|
+
} from "./chunk-I7C5KKUG.js";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/commands/config.ts
|
|
13
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14
|
+
import { parse as parseTOML2, stringify as stringifyTOML2 } from "smol-toml";
|
|
15
|
+
|
|
16
|
+
// src/core/config/store.ts
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
21
|
+
var getConfigDir = () => {
|
|
22
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"];
|
|
23
|
+
const base = xdgConfig || join(homedir(), ".config");
|
|
24
|
+
return join(base, "grvt");
|
|
25
|
+
};
|
|
26
|
+
var getConfigPath = () => join(getConfigDir(), "config.toml");
|
|
27
|
+
var configPath = () => getConfigPath();
|
|
28
|
+
var loadConfig = () => {
|
|
29
|
+
const path = getConfigPath();
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
return DEFAULT_CONFIG;
|
|
32
|
+
}
|
|
33
|
+
const raw = readFileSync(path, "utf-8");
|
|
34
|
+
const parsed = parseTOML(raw);
|
|
35
|
+
return configSchema.parse(parsed);
|
|
36
|
+
};
|
|
37
|
+
var saveConfig = (config) => {
|
|
38
|
+
const dir = getConfigDir();
|
|
39
|
+
if (!existsSync(dir)) {
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
const path = getConfigPath();
|
|
43
|
+
const toml = stringifyTOML(config);
|
|
44
|
+
writeFileSync(path, toml, { mode: 384 });
|
|
45
|
+
try {
|
|
46
|
+
chmodSync(path, 384);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var getConfigValue = (config, key) => {
|
|
51
|
+
const parts = key.split(".");
|
|
52
|
+
let current = config;
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
current = current[part];
|
|
58
|
+
}
|
|
59
|
+
return current;
|
|
60
|
+
};
|
|
61
|
+
var setConfigValue = (config, key, value) => {
|
|
62
|
+
const parts = key.split(".");
|
|
63
|
+
const clone = structuredClone(config);
|
|
64
|
+
let current = clone;
|
|
65
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
66
|
+
const part = parts[i];
|
|
67
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
68
|
+
current[part] = {};
|
|
69
|
+
}
|
|
70
|
+
current = current[part];
|
|
71
|
+
}
|
|
72
|
+
const lastKey = parts[parts.length - 1];
|
|
73
|
+
const coerced = coerceValue(value, key);
|
|
74
|
+
current[lastKey] = coerced;
|
|
75
|
+
return configSchema.parse(clone);
|
|
76
|
+
};
|
|
77
|
+
var unsetConfigValue = (config, key) => {
|
|
78
|
+
const parts = key.split(".");
|
|
79
|
+
const clone = structuredClone(config);
|
|
80
|
+
let current = clone;
|
|
81
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
82
|
+
const part = parts[i];
|
|
83
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
current = current[part];
|
|
87
|
+
}
|
|
88
|
+
delete current[parts[parts.length - 1]];
|
|
89
|
+
return configSchema.parse(clone);
|
|
90
|
+
};
|
|
91
|
+
var NUMERIC_KEYS = /* @__PURE__ */ new Set([
|
|
92
|
+
"http.timeoutMs",
|
|
93
|
+
"http.retries",
|
|
94
|
+
"http.backoffMs",
|
|
95
|
+
"http.maxBackoffMs"
|
|
96
|
+
]);
|
|
97
|
+
var BOOLEAN_KEYS = /* @__PURE__ */ new Set([
|
|
98
|
+
"outputDefaults.pretty",
|
|
99
|
+
"outputDefaults.silent"
|
|
100
|
+
]);
|
|
101
|
+
var coerceValue = (value, key) => {
|
|
102
|
+
if (BOOLEAN_KEYS.has(key) || value === "true" || value === "false") {
|
|
103
|
+
return value === "true";
|
|
104
|
+
}
|
|
105
|
+
if (NUMERIC_KEYS.has(key)) {
|
|
106
|
+
const num = Number(value);
|
|
107
|
+
if (!Number.isNaN(num)) return num;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/core/client/endpoints.ts
|
|
113
|
+
var BASE_URLS = {
|
|
114
|
+
dev: {
|
|
115
|
+
edge: "https://edge.dev.gravitymarkets.io",
|
|
116
|
+
marketData: "https://market-data.dev.gravitymarkets.io",
|
|
117
|
+
trading: "https://trades.dev.gravitymarkets.io"
|
|
118
|
+
},
|
|
119
|
+
staging: {
|
|
120
|
+
edge: "https://edge.staging.gravitymarkets.io",
|
|
121
|
+
marketData: "https://market-data.staging.gravitymarkets.io",
|
|
122
|
+
trading: "https://trades.staging.gravitymarkets.io"
|
|
123
|
+
},
|
|
124
|
+
testnet: {
|
|
125
|
+
edge: "https://edge.testnet.grvt.io",
|
|
126
|
+
marketData: "https://market-data.testnet.grvt.io",
|
|
127
|
+
trading: "https://trades.testnet.grvt.io"
|
|
128
|
+
},
|
|
129
|
+
prod: {
|
|
130
|
+
edge: "https://edge.grvt.io",
|
|
131
|
+
marketData: "https://market-data.grvt.io",
|
|
132
|
+
trading: "https://trades.grvt.io"
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var CHAIN_IDS = {
|
|
136
|
+
dev: { l1: 11155111, l2: 327 },
|
|
137
|
+
staging: { l1: 11155111, l2: 327 },
|
|
138
|
+
testnet: { l1: 11155111, l2: 326 },
|
|
139
|
+
prod: { l1: 1, l2: 325 }
|
|
140
|
+
};
|
|
141
|
+
var ENDPOINTS = {
|
|
142
|
+
auth: {
|
|
143
|
+
login: "/auth/api_key/login"
|
|
144
|
+
},
|
|
145
|
+
marketData: {
|
|
146
|
+
instrument: "/full/v1/instrument",
|
|
147
|
+
allInstruments: "/full/v1/all_instruments",
|
|
148
|
+
instruments: "/full/v1/instruments",
|
|
149
|
+
currency: "/full/v1/currency",
|
|
150
|
+
marginRules: "/full/v1/margin_rules",
|
|
151
|
+
mini: "/full/v1/mini",
|
|
152
|
+
ticker: "/full/v1/ticker",
|
|
153
|
+
tradeHistory: "/full/v1/trade_history",
|
|
154
|
+
kline: "/full/v1/kline",
|
|
155
|
+
funding: "/full/v1/funding",
|
|
156
|
+
book: "/full/v1/book"
|
|
157
|
+
},
|
|
158
|
+
trading: {
|
|
159
|
+
createOrder: "/full/v1/create_order",
|
|
160
|
+
cancelOrder: "/full/v1/cancel_order",
|
|
161
|
+
cancelAllOrders: "/full/v1/cancel_all_orders",
|
|
162
|
+
getOrder: "/full/v1/order",
|
|
163
|
+
openOrders: "/full/v1/open_orders",
|
|
164
|
+
orderHistory: "/full/v1/order_history",
|
|
165
|
+
cancelOnDisconnect: "/full/v1/cancel_on_disconnect",
|
|
166
|
+
fillHistory: "/full/v1/fill_history",
|
|
167
|
+
positions: "/full/v1/positions",
|
|
168
|
+
fundingPaymentHistory: "/full/v1/funding_payment_history",
|
|
169
|
+
getAllInitialLeverage: "/full/v1/get_all_initial_leverage",
|
|
170
|
+
setInitialLeverage: "/full/v1/set_initial_leverage",
|
|
171
|
+
setDeriskMmRatio: "/full/v1/set_derisk_mm_ratio"
|
|
172
|
+
},
|
|
173
|
+
account: {
|
|
174
|
+
accountSummary: "/full/v1/account_summary",
|
|
175
|
+
accountHistory: "/full/v1/account_history",
|
|
176
|
+
fundingAccountSummary: "/full/v1/funding_account_summary",
|
|
177
|
+
aggregatedAccountSummary: "/full/v1/aggregated_account_summary"
|
|
178
|
+
},
|
|
179
|
+
funds: {
|
|
180
|
+
depositHistory: "/full/v1/deposit_history",
|
|
181
|
+
transfer: "/full/v1/transfer",
|
|
182
|
+
transferHistory: "/full/v1/transfer_history",
|
|
183
|
+
withdrawal: "/full/v1/withdrawal",
|
|
184
|
+
withdrawalHistory: "/full/v1/withdrawal_history"
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var getBaseUrls = (env) => BASE_URLS[env];
|
|
188
|
+
var getChainIds = (env) => CHAIN_IDS[env];
|
|
189
|
+
|
|
190
|
+
// src/core/client/http.ts
|
|
191
|
+
var createHttpClient = (options) => {
|
|
192
|
+
const {
|
|
193
|
+
env,
|
|
194
|
+
cookie,
|
|
195
|
+
accountId,
|
|
196
|
+
timeoutMs = 1e4,
|
|
197
|
+
retries = 3,
|
|
198
|
+
backoffMs = 200,
|
|
199
|
+
maxBackoffMs = 1e4
|
|
200
|
+
} = options;
|
|
201
|
+
const urls = getBaseUrls(env);
|
|
202
|
+
const post = async (baseType, path, body) => {
|
|
203
|
+
const url = `${urls[baseType]}${path}`;
|
|
204
|
+
const headers = {
|
|
205
|
+
"Content-Type": "application/json"
|
|
206
|
+
};
|
|
207
|
+
if (cookie) {
|
|
208
|
+
headers["Cookie"] = cookie;
|
|
209
|
+
}
|
|
210
|
+
if (accountId) {
|
|
211
|
+
headers["X-Grvt-Account-Id"] = accountId;
|
|
212
|
+
}
|
|
213
|
+
let lastError;
|
|
214
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
215
|
+
try {
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
218
|
+
const response = await fetch(url, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers,
|
|
221
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
222
|
+
signal: controller.signal
|
|
223
|
+
});
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
if (response.status === 429 || response.status >= 500) {
|
|
226
|
+
const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
227
|
+
err.statusCode = response.status;
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
const text = await response.text().catch(() => "");
|
|
232
|
+
const err = new Error(`HTTP ${response.status}: ${text || response.statusText}`);
|
|
233
|
+
err.statusCode = response.status;
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
return await response.json();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
239
|
+
const isRetryable = lastError.statusCode === 429 || (lastError.statusCode ?? 0) >= 500 || lastError.name === "AbortError";
|
|
240
|
+
if (!isRetryable || attempt >= retries) {
|
|
241
|
+
throw lastError;
|
|
242
|
+
}
|
|
243
|
+
const delay = Math.min(backoffMs * Math.pow(2, attempt), maxBackoffMs);
|
|
244
|
+
await sleep(delay);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw lastError ?? new Error("Request failed");
|
|
248
|
+
};
|
|
249
|
+
return { post };
|
|
250
|
+
};
|
|
251
|
+
var extractAuthFromResponse = async (env, apiKey) => {
|
|
252
|
+
const urls = getBaseUrls(env);
|
|
253
|
+
const url = `${urls.edge}/auth/api_key/login`;
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
Cookie: "rm=true;"
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({ api_key: apiKey }),
|
|
261
|
+
redirect: "manual"
|
|
262
|
+
});
|
|
263
|
+
const bodyText = await response.text().catch(() => "");
|
|
264
|
+
if (!response.ok && response.status !== 302) {
|
|
265
|
+
throw new Error(`Auth failed (HTTP ${response.status}): ${bodyText || response.statusText}`);
|
|
266
|
+
}
|
|
267
|
+
if (bodyText) {
|
|
268
|
+
try {
|
|
269
|
+
const bodyJson = JSON.parse(bodyText);
|
|
270
|
+
if (bodyJson["status"] === "failure" || bodyJson["error"]) {
|
|
271
|
+
throw new Error(`Auth failed: ${bodyJson["error"] ?? bodyJson["message"] ?? "unknown error"}`);
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {
|
|
274
|
+
if (e instanceof Error && e.message.startsWith("Auth failed")) throw e;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const setCookies = response.headers.getSetCookie?.() ?? [];
|
|
278
|
+
let gravityCookie = setCookies.map((c2) => c2.split(";")[0]).find((c2) => c2?.startsWith("gravity="));
|
|
279
|
+
if (!gravityCookie) {
|
|
280
|
+
const rawSetCookie = response.headers.get("set-cookie");
|
|
281
|
+
if (rawSetCookie) {
|
|
282
|
+
const match = rawSetCookie.match(/gravity=[^;]+/);
|
|
283
|
+
if (match) {
|
|
284
|
+
gravityCookie = match[0];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!gravityCookie) {
|
|
289
|
+
throw new Error("Auth response did not contain gravity session cookie. Verify your API key is valid.");
|
|
290
|
+
}
|
|
291
|
+
const accountId = response.headers.get("x-grvt-account-id");
|
|
292
|
+
if (!accountId) {
|
|
293
|
+
throw new Error("Auth response did not contain X-Grvt-Account-Id header");
|
|
294
|
+
}
|
|
295
|
+
return { cookie: gravityCookie, accountId: accountId.trim() };
|
|
296
|
+
};
|
|
297
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
298
|
+
|
|
299
|
+
// src/core/output/table.ts
|
|
300
|
+
import Table from "cli-table3";
|
|
301
|
+
|
|
302
|
+
// src/core/output/colors.ts
|
|
303
|
+
import chalk from "chalk";
|
|
304
|
+
var _disabled = false;
|
|
305
|
+
var disableColors = () => {
|
|
306
|
+
_disabled = true;
|
|
307
|
+
};
|
|
308
|
+
var c = () => {
|
|
309
|
+
if (_disabled) {
|
|
310
|
+
return {
|
|
311
|
+
red: (s) => s,
|
|
312
|
+
green: (s) => s,
|
|
313
|
+
yellow: (s) => s,
|
|
314
|
+
cyan: { bold: (s) => s }
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return chalk;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/core/output/table.ts
|
|
321
|
+
var renderTable = (data) => {
|
|
322
|
+
if (data.length === 0) return "No data";
|
|
323
|
+
const keys = Object.keys(data[0]);
|
|
324
|
+
const table = new Table({
|
|
325
|
+
head: keys.map((k) => c().cyan.bold(k)),
|
|
326
|
+
style: { head: [], border: [] }
|
|
327
|
+
});
|
|
328
|
+
for (const row of data) {
|
|
329
|
+
table.push(keys.map((k) => formatCell(row[k])));
|
|
330
|
+
}
|
|
331
|
+
return table.toString();
|
|
332
|
+
};
|
|
333
|
+
var renderVerticalTable = (data) => {
|
|
334
|
+
const table = new Table({
|
|
335
|
+
style: { head: [], border: [] }
|
|
336
|
+
});
|
|
337
|
+
for (const [key, value] of Object.entries(data)) {
|
|
338
|
+
table.push({ [c().cyan.bold(key)]: formatCell(value) });
|
|
339
|
+
}
|
|
340
|
+
return table.toString();
|
|
341
|
+
};
|
|
342
|
+
var formatCell = (value) => {
|
|
343
|
+
if (value === null || value === void 0) return "";
|
|
344
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
345
|
+
return String(value);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/core/output/format.ts
|
|
349
|
+
var resolveOutputOptions = (opts) => {
|
|
350
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
351
|
+
const format = opts["output"] ?? (isTTY ? "table" : "json");
|
|
352
|
+
return {
|
|
353
|
+
output: format,
|
|
354
|
+
pretty: Boolean(opts["pretty"]),
|
|
355
|
+
silent: Boolean(opts["silent"])
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
var formatOutput = (data, options) => {
|
|
359
|
+
switch (options.output) {
|
|
360
|
+
case "json":
|
|
361
|
+
return options.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
362
|
+
case "ndjson":
|
|
363
|
+
if (Array.isArray(data)) {
|
|
364
|
+
return data.map((item) => JSON.stringify(item)).join("\n");
|
|
365
|
+
}
|
|
366
|
+
return JSON.stringify(data);
|
|
367
|
+
case "raw":
|
|
368
|
+
if (typeof data === "string") return data;
|
|
369
|
+
return JSON.stringify(data);
|
|
370
|
+
case "table":
|
|
371
|
+
if (Array.isArray(data)) {
|
|
372
|
+
if (data.length === 0) return "No data";
|
|
373
|
+
if (typeof data[0] === "object" && data[0] !== null) {
|
|
374
|
+
return renderTable(data);
|
|
375
|
+
}
|
|
376
|
+
return data.map(String).join("\n");
|
|
377
|
+
}
|
|
378
|
+
if (typeof data === "object" && data !== null) {
|
|
379
|
+
return renderVerticalTable(data);
|
|
380
|
+
}
|
|
381
|
+
return String(data);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var printOutput = (data, options) => {
|
|
385
|
+
const formatted = formatOutput(data, options);
|
|
386
|
+
process.stdout.write(formatted + "\n");
|
|
387
|
+
};
|
|
388
|
+
var logInfo = (message, silent) => {
|
|
389
|
+
if (!silent) {
|
|
390
|
+
process.stderr.write(message + "\n");
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/core/output/errors.ts
|
|
395
|
+
var EXIT_USAGE = 2;
|
|
396
|
+
var EXIT_AUTH = 3;
|
|
397
|
+
var EXIT_API = 5;
|
|
398
|
+
var exitWithError = (message, code = EXIT_API) => {
|
|
399
|
+
process.stderr.write(c().red(`Error: ${message}
|
|
400
|
+
`));
|
|
401
|
+
return process.exit(code);
|
|
402
|
+
};
|
|
403
|
+
var exitUsage = (message) => exitWithError(message, EXIT_USAGE);
|
|
404
|
+
var exitAuth = (message) => exitWithError(message, EXIT_AUTH);
|
|
405
|
+
var normalizeError = (error) => {
|
|
406
|
+
if (error instanceof Error) {
|
|
407
|
+
const httpErr = error;
|
|
408
|
+
if (httpErr.statusCode === 401 || httpErr.statusCode === 403) {
|
|
409
|
+
return { message: error.message, code: EXIT_AUTH };
|
|
410
|
+
}
|
|
411
|
+
if (httpErr.statusCode !== void 0) {
|
|
412
|
+
return { message: error.message, code: EXIT_API };
|
|
413
|
+
}
|
|
414
|
+
return { message: error.message, code: EXIT_API };
|
|
415
|
+
}
|
|
416
|
+
return { message: String(error), code: EXIT_API };
|
|
417
|
+
};
|
|
418
|
+
var handleCommandError = (error) => {
|
|
419
|
+
const { message, code } = normalizeError(error);
|
|
420
|
+
return exitWithError(message, code);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/core/command-helpers.ts
|
|
424
|
+
var getGlobalOpts = (cmd) => {
|
|
425
|
+
let root = cmd;
|
|
426
|
+
while (root.parent) {
|
|
427
|
+
root = root.parent;
|
|
428
|
+
}
|
|
429
|
+
return root.opts();
|
|
430
|
+
};
|
|
431
|
+
var resolveContext = (cmd, requireAuth = true) => {
|
|
432
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
433
|
+
if (globalOpts["color"] === false) {
|
|
434
|
+
disableColors();
|
|
435
|
+
}
|
|
436
|
+
const config = loadConfig();
|
|
437
|
+
const outputOptions = resolveOutputOptions(globalOpts);
|
|
438
|
+
const timeoutMs = Number(globalOpts["timeoutMs"]) || config.http.timeoutMs;
|
|
439
|
+
const retries = Number(globalOpts["retries"]) || config.http.retries;
|
|
440
|
+
if (requireAuth && (!config.cookie || !config.accountId)) {
|
|
441
|
+
exitAuth("Not authenticated. Run `grvt auth login` first.");
|
|
442
|
+
}
|
|
443
|
+
const client = createHttpClient({
|
|
444
|
+
env: config.env,
|
|
445
|
+
cookie: config.cookie,
|
|
446
|
+
accountId: config.accountId,
|
|
447
|
+
timeoutMs,
|
|
448
|
+
retries,
|
|
449
|
+
backoffMs: config.http.backoffMs,
|
|
450
|
+
maxBackoffMs: config.http.maxBackoffMs
|
|
451
|
+
});
|
|
452
|
+
return { config, client, outputOptions, globalOpts };
|
|
453
|
+
};
|
|
454
|
+
var wrapAction = (requireAuth, action) => async (...args) => {
|
|
455
|
+
const cmd = args[args.length - 1];
|
|
456
|
+
const opts = cmd.opts();
|
|
457
|
+
try {
|
|
458
|
+
const ctx = resolveContext(cmd, requireAuth);
|
|
459
|
+
await action(ctx, opts, cmd);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
handleCommandError(error);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
var output = (data, opts) => {
|
|
465
|
+
printOutput(data, opts);
|
|
466
|
+
};
|
|
467
|
+
var resolveSubAccountId = (opts, config) => {
|
|
468
|
+
const id = opts["subAccountId"] ?? config.subAccountId;
|
|
469
|
+
if (!id) {
|
|
470
|
+
exitAuth("--sub-account-id is required (or set subAccountId in config)");
|
|
471
|
+
}
|
|
472
|
+
return id;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/core/safety/confirm.ts
|
|
476
|
+
import { createInterface } from "readline";
|
|
477
|
+
var confirm = async (message, skipConfirm) => {
|
|
478
|
+
if (skipConfirm) return true;
|
|
479
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
rl.question(c().yellow(`${message} [y/N] `), (answer) => {
|
|
482
|
+
rl.close();
|
|
483
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/commands/config.ts
|
|
489
|
+
var registerConfigCommands = (program2) => {
|
|
490
|
+
const configCmd = program2.command("config").description("Manage local configuration");
|
|
491
|
+
configCmd.command("path").description("Print the config file path").action(() => {
|
|
492
|
+
process.stdout.write(configPath() + "\n");
|
|
493
|
+
});
|
|
494
|
+
configCmd.command("get").argument("[key]", "config key (dot-notation)").description("Get a config value").action(async (key, _opts, cmd) => {
|
|
495
|
+
try {
|
|
496
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
497
|
+
if (globalOpts["color"] === false) disableColors();
|
|
498
|
+
const outputOptions = resolveOutputOptions(globalOpts);
|
|
499
|
+
const config = loadConfig();
|
|
500
|
+
if (!key) {
|
|
501
|
+
printOutput(redactConfig(config), outputOptions);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const value = getConfigValue(config, key);
|
|
505
|
+
if (value === void 0) {
|
|
506
|
+
exitUsage(`Key not found: ${key}`);
|
|
507
|
+
}
|
|
508
|
+
if (SECRET_KEYS.includes(key)) {
|
|
509
|
+
printOutput("****", outputOptions);
|
|
510
|
+
} else {
|
|
511
|
+
printOutput(value, outputOptions);
|
|
512
|
+
}
|
|
513
|
+
} catch (error) {
|
|
514
|
+
handleCommandError(error);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
configCmd.command("set").argument("<key>", "config key (dot-notation)").argument("<value>", "value to set").description("Set a config value").action(async (key, value) => {
|
|
518
|
+
try {
|
|
519
|
+
const config = loadConfig();
|
|
520
|
+
const updated = setConfigValue(config, key, value);
|
|
521
|
+
saveConfig(updated);
|
|
522
|
+
process.stderr.write(`Set ${key}
|
|
523
|
+
`);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
handleCommandError(error);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
configCmd.command("unset").argument("<key>", "config key to unset").description("Unset a config value").action(async (key) => {
|
|
529
|
+
try {
|
|
530
|
+
const config = loadConfig();
|
|
531
|
+
const updated = unsetConfigValue(config, key);
|
|
532
|
+
saveConfig(updated);
|
|
533
|
+
process.stderr.write(`Unset ${key}
|
|
534
|
+
`);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
handleCommandError(error);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
configCmd.command("list").description("List all config values (secrets redacted)").action(async (_opts, cmd) => {
|
|
540
|
+
try {
|
|
541
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
542
|
+
if (globalOpts["color"] === false) disableColors();
|
|
543
|
+
const outputOptions = resolveOutputOptions(globalOpts);
|
|
544
|
+
printOutput(redactConfig(loadConfig()), outputOptions);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
handleCommandError(error);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
configCmd.command("export").requiredOption("--file <path>", "path to export to").option("--include-secrets", "include secret values (apiKey, privateKey, cookie)").option("--yes", "skip confirmation").description("Export config to a file").action(async (opts, cmd) => {
|
|
550
|
+
try {
|
|
551
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
552
|
+
const skipConfirm = Boolean(opts.yes || globalOpts["yes"]);
|
|
553
|
+
const config = loadConfig();
|
|
554
|
+
if (opts.includeSecrets) {
|
|
555
|
+
const ok = await confirm("Export includes secrets. Continue?", skipConfirm);
|
|
556
|
+
if (!ok) {
|
|
557
|
+
process.stderr.write("Aborted.\n");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const toml = stringifyTOML2(config);
|
|
561
|
+
writeFileSync2(opts.file, toml, "utf-8");
|
|
562
|
+
} else {
|
|
563
|
+
const toml = stringifyTOML2(redactConfig(config));
|
|
564
|
+
writeFileSync2(opts.file, toml, "utf-8");
|
|
565
|
+
}
|
|
566
|
+
process.stderr.write(`Exported to ${opts.file}
|
|
567
|
+
`);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
handleCommandError(error);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
configCmd.command("import").requiredOption("--file <path>", "path to import from").description("Import config from a file").action(async (opts) => {
|
|
573
|
+
try {
|
|
574
|
+
const raw = readFileSync2(opts.file, "utf-8");
|
|
575
|
+
const parsed = parseTOML2(raw);
|
|
576
|
+
const { configSchema: configSchema2 } = await import("./schema-LBUHKB7A.js");
|
|
577
|
+
const validated = configSchema2.parse(parsed);
|
|
578
|
+
saveConfig(validated);
|
|
579
|
+
process.stderr.write(`Imported from ${opts.file}
|
|
580
|
+
`);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
handleCommandError(error);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/core/auth/login.ts
|
|
588
|
+
var performLogin = async (options) => {
|
|
589
|
+
const config = loadConfig();
|
|
590
|
+
const env = options.env ?? config.env;
|
|
591
|
+
const { cookie, accountId } = await extractAuthFromResponse(env, options.apiKey);
|
|
592
|
+
saveConfig({
|
|
593
|
+
...config,
|
|
594
|
+
env,
|
|
595
|
+
apiKey: options.apiKey,
|
|
596
|
+
cookie,
|
|
597
|
+
accountId,
|
|
598
|
+
...options.privateKey ? { privateKey: options.privateKey } : {}
|
|
599
|
+
});
|
|
600
|
+
return { accountId, env };
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/core/auth/session.ts
|
|
604
|
+
var login = async (options) => performLogin(options);
|
|
605
|
+
var logout = () => {
|
|
606
|
+
const config = loadConfig();
|
|
607
|
+
saveConfig({
|
|
608
|
+
...config,
|
|
609
|
+
apiKey: void 0,
|
|
610
|
+
privateKey: void 0,
|
|
611
|
+
cookie: void 0,
|
|
612
|
+
accountId: void 0
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
var verifySession = async () => {
|
|
616
|
+
const config = loadConfig();
|
|
617
|
+
if (!config.cookie || !config.accountId) {
|
|
618
|
+
return { valid: false, env: config.env };
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const client = createHttpClient({
|
|
622
|
+
env: config.env,
|
|
623
|
+
cookie: config.cookie,
|
|
624
|
+
accountId: config.accountId,
|
|
625
|
+
timeoutMs: config.http.timeoutMs
|
|
626
|
+
});
|
|
627
|
+
await client.post("trading", ENDPOINTS.account.fundingAccountSummary, {});
|
|
628
|
+
return { valid: true, env: config.env, accountId: config.accountId };
|
|
629
|
+
} catch {
|
|
630
|
+
return { valid: false, env: config.env, accountId: config.accountId };
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// src/commands/auth.ts
|
|
635
|
+
var registerAuthCommands = (program2) => {
|
|
636
|
+
const authCmd = program2.command("auth").description("Authentication and session management");
|
|
637
|
+
authCmd.command("login").description("Authenticate with GRVT API").option("--api-key <key>", "GRVT API key").option("--private-key <key>", "Ethereum private key for order signing").option("--env <environment>", "environment: dev|staging|testnet|prod").action(async (opts, cmd) => {
|
|
638
|
+
try {
|
|
639
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
640
|
+
if (globalOpts["color"] === false) disableColors();
|
|
641
|
+
const config = loadConfig();
|
|
642
|
+
const apiKey = opts.apiKey ?? config.apiKey;
|
|
643
|
+
if (!apiKey) {
|
|
644
|
+
return exitUsage("--api-key is required (or set apiKey in config)");
|
|
645
|
+
}
|
|
646
|
+
const env = opts.env ?? config.env;
|
|
647
|
+
logInfo(`Logging in to ${env}...`, Boolean(globalOpts["silent"]));
|
|
648
|
+
const result = await login({ apiKey, env, privateKey: opts.privateKey });
|
|
649
|
+
logInfo(c().green(`Authenticated on ${result.env} (accountId: ${result.accountId})`), Boolean(globalOpts["silent"]));
|
|
650
|
+
} catch (error) {
|
|
651
|
+
handleCommandError(error);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
authCmd.command("status").description("Check if current session is valid").action(async (_opts, cmd) => {
|
|
655
|
+
try {
|
|
656
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
657
|
+
if (globalOpts["color"] === false) disableColors();
|
|
658
|
+
const outputOptions = resolveOutputOptions(globalOpts);
|
|
659
|
+
const status = await verifySession();
|
|
660
|
+
printOutput(status, outputOptions);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
handleCommandError(error);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
authCmd.command("logout").description("Clear stored credentials and session").action(async (_opts, cmd) => {
|
|
666
|
+
try {
|
|
667
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
668
|
+
if (globalOpts["color"] === false) disableColors();
|
|
669
|
+
logout();
|
|
670
|
+
logInfo(c().green("Logged out."), Boolean(globalOpts["silent"]));
|
|
671
|
+
} catch (error) {
|
|
672
|
+
handleCommandError(error);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
authCmd.command("whoami").description("Show current account info").action(async (_opts, cmd) => {
|
|
676
|
+
try {
|
|
677
|
+
const globalOpts = getGlobalOpts(cmd);
|
|
678
|
+
if (globalOpts["color"] === false) disableColors();
|
|
679
|
+
const outputOptions = resolveOutputOptions(globalOpts);
|
|
680
|
+
const config = loadConfig();
|
|
681
|
+
printOutput({
|
|
682
|
+
env: config.env,
|
|
683
|
+
accountId: config.accountId ?? "not set",
|
|
684
|
+
subAccountId: config.subAccountId ?? "not set",
|
|
685
|
+
hasApiKey: Boolean(config.apiKey),
|
|
686
|
+
hasPrivateKey: Boolean(config.privateKey),
|
|
687
|
+
hasSession: Boolean(config.cookie)
|
|
688
|
+
}, outputOptions);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
handleCommandError(error);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/core/pagination/cursor.ts
|
|
696
|
+
var paginateCursor = async (options) => {
|
|
697
|
+
const { fetchPage, all, outputOptions } = options;
|
|
698
|
+
let { cursor } = options;
|
|
699
|
+
if (!all) {
|
|
700
|
+
const page = await fetchPage(cursor);
|
|
701
|
+
return page.result;
|
|
702
|
+
}
|
|
703
|
+
const isStreaming = outputOptions.output === "ndjson";
|
|
704
|
+
const collected = [];
|
|
705
|
+
while (true) {
|
|
706
|
+
const page = await fetchPage(cursor);
|
|
707
|
+
if (isStreaming) {
|
|
708
|
+
for (const item of page.result) {
|
|
709
|
+
process.stdout.write(formatOutput(item, outputOptions) + "\n");
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
collected.push(...page.result);
|
|
713
|
+
}
|
|
714
|
+
if (!page.next || page.result.length === 0) {
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
cursor = page.next;
|
|
718
|
+
}
|
|
719
|
+
return collected;
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// src/core/safety/validate.ts
|
|
723
|
+
import { readFileSync as readFileSync3, readSync } from "fs";
|
|
724
|
+
var requireOption = (value, name) => {
|
|
725
|
+
if (value === void 0 || value === null || value === "") {
|
|
726
|
+
return exitUsage(`--${name} is required`);
|
|
727
|
+
}
|
|
728
|
+
return String(value);
|
|
729
|
+
};
|
|
730
|
+
var parseJsonInput = (jsonPath) => {
|
|
731
|
+
let raw;
|
|
732
|
+
if (jsonPath === "-") {
|
|
733
|
+
raw = readStdin();
|
|
734
|
+
} else {
|
|
735
|
+
const filePath = jsonPath.startsWith("@") ? jsonPath.slice(1) : jsonPath;
|
|
736
|
+
try {
|
|
737
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
738
|
+
} catch (error) {
|
|
739
|
+
return exitUsage(`Cannot read file: ${filePath} (${error instanceof Error ? error.message : String(error)})`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
return JSON.parse(raw);
|
|
744
|
+
} catch {
|
|
745
|
+
return exitUsage("Invalid JSON input");
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
var readStdin = () => {
|
|
749
|
+
const chunks = [];
|
|
750
|
+
const fd = 0;
|
|
751
|
+
const buf = Buffer.alloc(4096);
|
|
752
|
+
let bytesRead;
|
|
753
|
+
try {
|
|
754
|
+
while ((bytesRead = readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
755
|
+
chunks.push(buf.subarray(0, bytesRead));
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
760
|
+
};
|
|
761
|
+
var parseTimestamp = (value) => {
|
|
762
|
+
const num = Number(value);
|
|
763
|
+
if (!Number.isNaN(num)) {
|
|
764
|
+
if (num < 1e12) {
|
|
765
|
+
return String(BigInt(num) * 1000000000n);
|
|
766
|
+
}
|
|
767
|
+
if (num < 1e15) {
|
|
768
|
+
return String(BigInt(num) * 1000000n);
|
|
769
|
+
}
|
|
770
|
+
return String(BigInt(num));
|
|
771
|
+
}
|
|
772
|
+
const date = new Date(value);
|
|
773
|
+
if (Number.isNaN(date.getTime())) {
|
|
774
|
+
return exitUsage(`Invalid timestamp: ${value}`);
|
|
775
|
+
}
|
|
776
|
+
return String(BigInt(date.getTime()) * 1000000n);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/commands/account.ts
|
|
780
|
+
var getFundingAccountSummary = async (client) => {
|
|
781
|
+
const result = await client.post("trading", ENDPOINTS.account.fundingAccountSummary, {});
|
|
782
|
+
return result.result;
|
|
783
|
+
};
|
|
784
|
+
var getSubAccountSummary = async (client, subAccountId) => {
|
|
785
|
+
const body = { sub_account_id: subAccountId };
|
|
786
|
+
const result = await client.post("trading", ENDPOINTS.account.accountSummary, body);
|
|
787
|
+
return result.result;
|
|
788
|
+
};
|
|
789
|
+
var getAccountHistory = async (client, params) => {
|
|
790
|
+
return client.post("trading", ENDPOINTS.account.accountHistory, params);
|
|
791
|
+
};
|
|
792
|
+
var getAggregatedAccountSummary = async (client) => {
|
|
793
|
+
const result = await client.post("trading", ENDPOINTS.account.aggregatedAccountSummary, {});
|
|
794
|
+
return result.result;
|
|
795
|
+
};
|
|
796
|
+
var registerAccountCommands = (program2) => {
|
|
797
|
+
const accountCmd = program2.command("account").description("Account summaries and sub-account info");
|
|
798
|
+
accountCmd.command("funding").description("Get funding account summary").action(wrapAction(true, async (ctx) => {
|
|
799
|
+
const result = await getFundingAccountSummary(ctx.client);
|
|
800
|
+
output(result, ctx.outputOptions);
|
|
801
|
+
}));
|
|
802
|
+
accountCmd.command("summary").description("Get sub-account summary").option("--sub-account-id <id>", "sub-account ID").action(wrapAction(true, async (ctx, opts) => {
|
|
803
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
804
|
+
const result = await getSubAccountSummary(ctx.client, subAccountId);
|
|
805
|
+
output(result, ctx.outputOptions);
|
|
806
|
+
}));
|
|
807
|
+
accountCmd.command("aggregated").description("Get aggregated account summary across all sub-accounts").action(wrapAction(true, async (ctx) => {
|
|
808
|
+
const result = await getAggregatedAccountSummary(ctx.client);
|
|
809
|
+
output(result, ctx.outputOptions);
|
|
810
|
+
}));
|
|
811
|
+
accountCmd.command("history").description("Get hourly snapshots of sub-account state").option("--sub-account-id <id>", "sub-account ID").option("--start-time <time>", "start time (unix seconds, ms, ns, or ISO)").option("--end-time <time>", "end time").option("--limit <n>", "max results per page").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
812
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
813
|
+
const results = await paginateCursor({
|
|
814
|
+
fetchPage: async (cursor) => {
|
|
815
|
+
const body = {
|
|
816
|
+
sub_account_id: subAccountId,
|
|
817
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
818
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
819
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
820
|
+
...cursor ? { cursor } : {}
|
|
821
|
+
};
|
|
822
|
+
const resp = await getAccountHistory(ctx.client, body);
|
|
823
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
824
|
+
},
|
|
825
|
+
cursor: opts["cursor"],
|
|
826
|
+
all: Boolean(opts["all"]),
|
|
827
|
+
outputOptions: ctx.outputOptions
|
|
828
|
+
});
|
|
829
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
830
|
+
output(results, ctx.outputOptions);
|
|
831
|
+
}
|
|
832
|
+
}));
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// src/commands/market.ts
|
|
836
|
+
var registerMarketCommands = (program2) => {
|
|
837
|
+
const marketCmd = program2.command("market").description("Market data (mostly unauthenticated)");
|
|
838
|
+
marketCmd.command("instruments").description("List instruments").option("--kind <kinds>", "filter by kind (comma-separated: PERPETUAL,FUTURE,CALL,PUT)").option("--base <currencies>", "filter by base currency (comma-separated)").option("--quote <currencies>", "filter by quote currency (comma-separated)").option("--active", "only active instruments", true).option("--limit <n>", "max results").action(wrapAction(false, async (ctx, opts) => {
|
|
839
|
+
const hasFilters = opts["kind"] || opts["base"] || opts["quote"];
|
|
840
|
+
const endpoint = hasFilters ? ENDPOINTS.marketData.instruments : ENDPOINTS.marketData.allInstruments;
|
|
841
|
+
const body = {
|
|
842
|
+
is_active: Boolean(opts["active"]),
|
|
843
|
+
...opts["kind"] ? { kind: opts["kind"].split(",") } : {},
|
|
844
|
+
...opts["base"] ? { base: opts["base"].split(",") } : {},
|
|
845
|
+
...opts["quote"] ? { quote: opts["quote"].split(",") } : {},
|
|
846
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {}
|
|
847
|
+
};
|
|
848
|
+
const result = await ctx.client.post("marketData", endpoint, body);
|
|
849
|
+
output(result.result, ctx.outputOptions);
|
|
850
|
+
}));
|
|
851
|
+
marketCmd.command("orderbook").description("Get order book").requiredOption("--instrument <name>", "instrument symbol").option("--depth <n>", "depth: 10, 50, 100, 500", "10").action(wrapAction(false, async (ctx, opts) => {
|
|
852
|
+
const body = {
|
|
853
|
+
instrument: opts["instrument"],
|
|
854
|
+
depth: Number(opts["depth"])
|
|
855
|
+
};
|
|
856
|
+
const result = await ctx.client.post("marketData", ENDPOINTS.marketData.book, body);
|
|
857
|
+
output(result.result, ctx.outputOptions);
|
|
858
|
+
}));
|
|
859
|
+
marketCmd.command("trades").description("Get trade history").requiredOption("--instrument <name>", "instrument symbol").option("--start-time <time>", "start time (unix seconds, ms, ns, or ISO)").option("--end-time <time>", "end time").option("--limit <n>", "max trades").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(false, async (ctx, opts) => {
|
|
860
|
+
const results = await paginateCursor({
|
|
861
|
+
fetchPage: async (cursor) => {
|
|
862
|
+
const body = {
|
|
863
|
+
instrument: opts["instrument"],
|
|
864
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
865
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
866
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
867
|
+
...cursor ? { cursor } : {}
|
|
868
|
+
};
|
|
869
|
+
const res = await ctx.client.post("marketData", ENDPOINTS.marketData.tradeHistory, body);
|
|
870
|
+
return { result: res.result ?? [], next: res.next };
|
|
871
|
+
},
|
|
872
|
+
cursor: opts["cursor"],
|
|
873
|
+
all: Boolean(opts["all"]),
|
|
874
|
+
outputOptions: ctx.outputOptions
|
|
875
|
+
});
|
|
876
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
877
|
+
output(results, ctx.outputOptions);
|
|
878
|
+
}
|
|
879
|
+
}));
|
|
880
|
+
marketCmd.command("candles").description("Get candlestick (OHLCV) data").requiredOption("--instrument <name>", "instrument symbol").requiredOption("--interval <interval>", "candle interval (e.g. CI_1_M, CI_5_M, CI_1_H, CI_1_D)").option("--type <type>", "price type: TRADE, MARK, INDEX, MID", "TRADE").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max candles").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(false, async (ctx, opts) => {
|
|
881
|
+
const results = await paginateCursor({
|
|
882
|
+
fetchPage: async (cursor) => {
|
|
883
|
+
const body = {
|
|
884
|
+
instrument: opts["instrument"],
|
|
885
|
+
interval: opts["interval"],
|
|
886
|
+
type: opts["type"] ?? "TRADE",
|
|
887
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
888
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
889
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
890
|
+
...cursor ? { cursor } : {}
|
|
891
|
+
};
|
|
892
|
+
const res = await ctx.client.post("marketData", ENDPOINTS.marketData.kline, body);
|
|
893
|
+
return { result: res.result ?? [], next: res.next };
|
|
894
|
+
},
|
|
895
|
+
cursor: opts["cursor"],
|
|
896
|
+
all: Boolean(opts["all"]),
|
|
897
|
+
outputOptions: ctx.outputOptions
|
|
898
|
+
});
|
|
899
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
900
|
+
output(results, ctx.outputOptions);
|
|
901
|
+
}
|
|
902
|
+
}));
|
|
903
|
+
marketCmd.command("funding-rate").description("Get funding rate history").requiredOption("--instrument <name>", "instrument symbol").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max entries").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(false, async (ctx, opts) => {
|
|
904
|
+
const results = await paginateCursor({
|
|
905
|
+
fetchPage: async (cursor) => {
|
|
906
|
+
const body = {
|
|
907
|
+
instrument: opts["instrument"],
|
|
908
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
909
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
910
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
911
|
+
...cursor ? { cursor } : {}
|
|
912
|
+
};
|
|
913
|
+
const res = await ctx.client.post("marketData", ENDPOINTS.marketData.funding, body);
|
|
914
|
+
return { result: res.result ?? [], next: res.next };
|
|
915
|
+
},
|
|
916
|
+
cursor: opts["cursor"],
|
|
917
|
+
all: Boolean(opts["all"]),
|
|
918
|
+
outputOptions: ctx.outputOptions
|
|
919
|
+
});
|
|
920
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
921
|
+
output(results, ctx.outputOptions);
|
|
922
|
+
}
|
|
923
|
+
}));
|
|
924
|
+
marketCmd.command("instrument").description("Get detailed metadata for a single instrument").requiredOption("--instrument <name>", "instrument symbol (e.g. BTC_USDT_Perp)").action(wrapAction(false, async (ctx, opts) => {
|
|
925
|
+
const body = { instrument: opts["instrument"] };
|
|
926
|
+
const result = await ctx.client.post("marketData", ENDPOINTS.marketData.instrument, body);
|
|
927
|
+
output(result.result, ctx.outputOptions);
|
|
928
|
+
}));
|
|
929
|
+
marketCmd.command("currency").description("List all supported currencies with IDs and decimals").action(wrapAction(false, async (ctx) => {
|
|
930
|
+
const result = await ctx.client.post("marketData", ENDPOINTS.marketData.currency, {});
|
|
931
|
+
output(result.result, ctx.outputOptions);
|
|
932
|
+
}));
|
|
933
|
+
marketCmd.command("mini-ticker").description("Get lightweight price info for an instrument").requiredOption("--instrument <name>", "instrument symbol").action(wrapAction(false, async (ctx, opts) => {
|
|
934
|
+
const body = { instrument: opts["instrument"] };
|
|
935
|
+
const result = await ctx.client.post("marketData", ENDPOINTS.marketData.mini, body);
|
|
936
|
+
output(result.result, ctx.outputOptions);
|
|
937
|
+
}));
|
|
938
|
+
marketCmd.command("ticker").description("Get full ticker data for an instrument").requiredOption("--instrument <name>", "instrument symbol").option("--greeks", "include greeks data (for options)").option("--derived", "include derived data").action(wrapAction(false, async (ctx, opts) => {
|
|
939
|
+
const body = {
|
|
940
|
+
instrument: opts["instrument"],
|
|
941
|
+
...opts["greeks"] ? { greeks: true } : {},
|
|
942
|
+
...opts["derived"] ? { derived: true } : {}
|
|
943
|
+
};
|
|
944
|
+
const result = await ctx.client.post("marketData", ENDPOINTS.marketData.ticker, body);
|
|
945
|
+
output(result.result, ctx.outputOptions);
|
|
946
|
+
}));
|
|
947
|
+
marketCmd.command("margin-rules").description("Get margin rules for an instrument").requiredOption("--instrument <name>", "instrument symbol (e.g. BTC_USDT_Perp)").action(wrapAction(false, async (ctx, opts) => {
|
|
948
|
+
const body = { instrument: opts["instrument"] };
|
|
949
|
+
const result = await ctx.client.post("marketData", ENDPOINTS.marketData.marginRules, body);
|
|
950
|
+
output(result, ctx.outputOptions);
|
|
951
|
+
}));
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/commands/trade/orders.ts
|
|
955
|
+
import { randomBytes } from "crypto";
|
|
956
|
+
|
|
957
|
+
// src/core/instruments/cache.ts
|
|
958
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
959
|
+
import { join as join2 } from "path";
|
|
960
|
+
import { homedir as homedir2 } from "os";
|
|
961
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
962
|
+
var getCacheDir = () => {
|
|
963
|
+
const xdgCache = process.env["XDG_CACHE_HOME"];
|
|
964
|
+
const base = xdgCache || join2(homedir2(), ".cache");
|
|
965
|
+
return join2(base, "grvt");
|
|
966
|
+
};
|
|
967
|
+
var getCachePath = (env) => join2(getCacheDir(), `instruments-${env}.json`);
|
|
968
|
+
var loadCache = (env) => {
|
|
969
|
+
const path = getCachePath(env);
|
|
970
|
+
if (!existsSync2(path)) return null;
|
|
971
|
+
try {
|
|
972
|
+
const raw = readFileSync4(path, "utf-8");
|
|
973
|
+
const cache = JSON.parse(raw);
|
|
974
|
+
if (cache.env !== env || Date.now() - cache.fetched_at > CACHE_TTL_MS) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
return cache;
|
|
978
|
+
} catch {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
var saveCache = (env, instruments) => {
|
|
983
|
+
const dir = getCacheDir();
|
|
984
|
+
if (!existsSync2(dir)) {
|
|
985
|
+
mkdirSync2(dir, { recursive: true });
|
|
986
|
+
}
|
|
987
|
+
const cache = {
|
|
988
|
+
env,
|
|
989
|
+
fetched_at: Date.now(),
|
|
990
|
+
instruments
|
|
991
|
+
};
|
|
992
|
+
writeFileSync3(getCachePath(env), JSON.stringify(cache), "utf-8");
|
|
993
|
+
};
|
|
994
|
+
var getInstruments = async (env, cookie, accountId) => {
|
|
995
|
+
const cached = loadCache(env);
|
|
996
|
+
if (cached) return cached.instruments;
|
|
997
|
+
const client = createHttpClient({ env, cookie, accountId });
|
|
998
|
+
const response = await client.post(
|
|
999
|
+
"marketData",
|
|
1000
|
+
ENDPOINTS.marketData.allInstruments,
|
|
1001
|
+
{ is_active: true }
|
|
1002
|
+
);
|
|
1003
|
+
const instruments = {};
|
|
1004
|
+
for (const inst of response.result ?? []) {
|
|
1005
|
+
instruments[inst.instrument] = inst;
|
|
1006
|
+
}
|
|
1007
|
+
saveCache(env, instruments);
|
|
1008
|
+
return instruments;
|
|
1009
|
+
};
|
|
1010
|
+
var getInstrument = async (env, instrument, cookie, accountId) => {
|
|
1011
|
+
const instruments = await getInstruments(env, cookie, accountId);
|
|
1012
|
+
const meta = instruments[instrument];
|
|
1013
|
+
if (!meta) {
|
|
1014
|
+
throw new Error(`Instrument not found: ${instrument}. Run 'grvt market instruments' to see available instruments.`);
|
|
1015
|
+
}
|
|
1016
|
+
return meta;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// src/core/signing/eip712.ts
|
|
1020
|
+
var PRICE_MULTIPLIER = 1000000000n;
|
|
1021
|
+
var TIME_IN_FORCE_MAP = {
|
|
1022
|
+
GOOD_TILL_TIME: 1,
|
|
1023
|
+
ALL_OR_NONE: 2,
|
|
1024
|
+
IMMEDIATE_OR_CANCEL: 3,
|
|
1025
|
+
FILL_OR_KILL: 4
|
|
1026
|
+
};
|
|
1027
|
+
var decimalToBigInt = (value, multiplier) => {
|
|
1028
|
+
const parts = value.split(".");
|
|
1029
|
+
const whole = parts[0] ?? "0";
|
|
1030
|
+
const frac = parts[1] ?? "";
|
|
1031
|
+
const multiplierDigits = multiplier.toString().length - 1;
|
|
1032
|
+
const paddedFrac = frac.padEnd(multiplierDigits, "0").slice(0, multiplierDigits);
|
|
1033
|
+
return BigInt(whole) * multiplier + BigInt(paddedFrac);
|
|
1034
|
+
};
|
|
1035
|
+
var buildOrderTypedData = (payload, env) => {
|
|
1036
|
+
const chainIds = getChainIds(env);
|
|
1037
|
+
const domain = {
|
|
1038
|
+
name: "GRVT Exchange",
|
|
1039
|
+
version: "0",
|
|
1040
|
+
chainId: BigInt(chainIds.l2)
|
|
1041
|
+
};
|
|
1042
|
+
const types = {
|
|
1043
|
+
Order: [
|
|
1044
|
+
{ name: "subAccountID", type: "uint64" },
|
|
1045
|
+
{ name: "isMarket", type: "bool" },
|
|
1046
|
+
{ name: "timeInForce", type: "uint8" },
|
|
1047
|
+
{ name: "postOnly", type: "bool" },
|
|
1048
|
+
{ name: "reduceOnly", type: "bool" },
|
|
1049
|
+
{ name: "legs", type: "OrderLeg[]" },
|
|
1050
|
+
{ name: "nonce", type: "uint32" },
|
|
1051
|
+
{ name: "expiration", type: "int64" }
|
|
1052
|
+
],
|
|
1053
|
+
OrderLeg: [
|
|
1054
|
+
{ name: "assetID", type: "uint256" },
|
|
1055
|
+
{ name: "contractSize", type: "uint64" },
|
|
1056
|
+
{ name: "limitPrice", type: "uint64" },
|
|
1057
|
+
{ name: "isBuyingContract", type: "bool" }
|
|
1058
|
+
]
|
|
1059
|
+
};
|
|
1060
|
+
const legs = payload.legs.map((leg) => {
|
|
1061
|
+
const meta = payload.instruments[leg.instrument];
|
|
1062
|
+
if (!meta) {
|
|
1063
|
+
throw new Error(`Instrument metadata not found for: ${leg.instrument}`);
|
|
1064
|
+
}
|
|
1065
|
+
const sizeMultiplier = BigInt(10) ** BigInt(meta.base_decimals);
|
|
1066
|
+
return {
|
|
1067
|
+
assetID: BigInt(meta.instrument_hash),
|
|
1068
|
+
contractSize: decimalToBigInt(leg.size, sizeMultiplier),
|
|
1069
|
+
limitPrice: decimalToBigInt(leg.limit_price ?? "0", PRICE_MULTIPLIER),
|
|
1070
|
+
isBuyingContract: leg.is_buying_asset
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
1073
|
+
const message = {
|
|
1074
|
+
subAccountID: BigInt(payload.subAccountId),
|
|
1075
|
+
isMarket: payload.isMarket,
|
|
1076
|
+
timeInForce: TIME_IN_FORCE_MAP[payload.timeInForce],
|
|
1077
|
+
postOnly: payload.postOnly,
|
|
1078
|
+
reduceOnly: payload.reduceOnly,
|
|
1079
|
+
legs,
|
|
1080
|
+
nonce: payload.nonce,
|
|
1081
|
+
expiration: BigInt(payload.expiration)
|
|
1082
|
+
};
|
|
1083
|
+
return { domain, types, primaryType: "Order", message };
|
|
1084
|
+
};
|
|
1085
|
+
var buildTransferTypedData = (payload, env) => {
|
|
1086
|
+
const chainIds = getChainIds(env);
|
|
1087
|
+
const domain = {
|
|
1088
|
+
name: "GRVT Exchange",
|
|
1089
|
+
version: "0",
|
|
1090
|
+
chainId: BigInt(chainIds.l2)
|
|
1091
|
+
};
|
|
1092
|
+
const types = {
|
|
1093
|
+
Transfer: [
|
|
1094
|
+
{ name: "fromAccount", type: "address" },
|
|
1095
|
+
{ name: "fromSubAccount", type: "uint64" },
|
|
1096
|
+
{ name: "toAccount", type: "address" },
|
|
1097
|
+
{ name: "toSubAccount", type: "uint64" },
|
|
1098
|
+
{ name: "tokenCurrency", type: "uint8" },
|
|
1099
|
+
{ name: "numTokens", type: "uint64" },
|
|
1100
|
+
{ name: "nonce", type: "uint32" },
|
|
1101
|
+
{ name: "expiration", type: "int64" }
|
|
1102
|
+
]
|
|
1103
|
+
};
|
|
1104
|
+
const tokenMultiplier = BigInt(10) ** BigInt(payload.balanceDecimals);
|
|
1105
|
+
const message = {
|
|
1106
|
+
fromAccount: payload.fromAccount,
|
|
1107
|
+
fromSubAccount: BigInt(payload.fromSubAccount),
|
|
1108
|
+
toAccount: payload.toAccount,
|
|
1109
|
+
toSubAccount: BigInt(payload.toSubAccount),
|
|
1110
|
+
tokenCurrency: payload.tokenCurrency,
|
|
1111
|
+
numTokens: decimalToBigInt(payload.numTokens, tokenMultiplier),
|
|
1112
|
+
nonce: payload.nonce,
|
|
1113
|
+
expiration: BigInt(payload.expiration)
|
|
1114
|
+
};
|
|
1115
|
+
return { domain, types, primaryType: "Transfer", message };
|
|
1116
|
+
};
|
|
1117
|
+
var buildWithdrawalTypedData = (payload, env) => {
|
|
1118
|
+
const chainIds = getChainIds(env);
|
|
1119
|
+
const domain = {
|
|
1120
|
+
name: "GRVT Exchange",
|
|
1121
|
+
version: "0",
|
|
1122
|
+
chainId: BigInt(chainIds.l2)
|
|
1123
|
+
};
|
|
1124
|
+
const types = {
|
|
1125
|
+
Withdrawal: [
|
|
1126
|
+
{ name: "fromAccount", type: "address" },
|
|
1127
|
+
{ name: "toEthAddress", type: "address" },
|
|
1128
|
+
{ name: "tokenCurrency", type: "uint8" },
|
|
1129
|
+
{ name: "numTokens", type: "uint64" },
|
|
1130
|
+
{ name: "nonce", type: "uint32" },
|
|
1131
|
+
{ name: "expiration", type: "int64" }
|
|
1132
|
+
]
|
|
1133
|
+
};
|
|
1134
|
+
const tokenMultiplier = BigInt(10) ** BigInt(payload.balanceDecimals);
|
|
1135
|
+
const message = {
|
|
1136
|
+
fromAccount: payload.fromAccount,
|
|
1137
|
+
toEthAddress: payload.toEthAddress,
|
|
1138
|
+
tokenCurrency: payload.tokenCurrency,
|
|
1139
|
+
numTokens: decimalToBigInt(payload.numTokens, tokenMultiplier),
|
|
1140
|
+
nonce: payload.nonce,
|
|
1141
|
+
expiration: BigInt(payload.expiration)
|
|
1142
|
+
};
|
|
1143
|
+
return { domain, types, primaryType: "Withdrawal", message };
|
|
1144
|
+
};
|
|
1145
|
+
var buildDeriskTypedData = (payload, env) => {
|
|
1146
|
+
const chainIds = getChainIds(env);
|
|
1147
|
+
const domain = {
|
|
1148
|
+
name: "GRVT Exchange",
|
|
1149
|
+
version: "0",
|
|
1150
|
+
chainId: BigInt(chainIds.l2)
|
|
1151
|
+
};
|
|
1152
|
+
const types = {
|
|
1153
|
+
SetDeriskMMRatio: [
|
|
1154
|
+
{ name: "subAccountID", type: "uint64" },
|
|
1155
|
+
{ name: "ratio", type: "uint64" },
|
|
1156
|
+
{ name: "nonce", type: "uint32" },
|
|
1157
|
+
{ name: "expiration", type: "int64" }
|
|
1158
|
+
]
|
|
1159
|
+
};
|
|
1160
|
+
const message = {
|
|
1161
|
+
subAccountID: BigInt(payload.subAccountId),
|
|
1162
|
+
ratio: decimalToBigInt(payload.ratio, PRICE_MULTIPLIER),
|
|
1163
|
+
nonce: payload.nonce,
|
|
1164
|
+
expiration: BigInt(payload.expiration)
|
|
1165
|
+
};
|
|
1166
|
+
return { domain, types, primaryType: "SetDeriskMMRatio", message };
|
|
1167
|
+
};
|
|
1168
|
+
var generateNonce = () => Math.floor(Math.random() * 2147483647);
|
|
1169
|
+
var generateExpiration = (durationSeconds = 3600) => {
|
|
1170
|
+
const expirationMs = Date.now() + durationSeconds * 1e3;
|
|
1171
|
+
return String(BigInt(expirationMs) * 1000000n);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// src/core/signing/signer.ts
|
|
1175
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
1176
|
+
import { parseSignature } from "viem";
|
|
1177
|
+
var createSigner = (privateKey) => {
|
|
1178
|
+
const key = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
|
|
1179
|
+
return privateKeyToAccount(key);
|
|
1180
|
+
};
|
|
1181
|
+
var signTypedData = async (privateKey, typedData) => {
|
|
1182
|
+
const account = createSigner(privateKey);
|
|
1183
|
+
const sig = await account.signTypedData(typedData);
|
|
1184
|
+
const parsed = parseSignature(sig);
|
|
1185
|
+
return {
|
|
1186
|
+
signer: account.address,
|
|
1187
|
+
r: parsed.r,
|
|
1188
|
+
s: parsed.s,
|
|
1189
|
+
v: Number(parsed.v)
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
// src/commands/trade/orders.ts
|
|
1194
|
+
var generateClientOrderId = () => {
|
|
1195
|
+
const min = 2n ** 63n;
|
|
1196
|
+
const range = 2n ** 63n;
|
|
1197
|
+
const rand = BigInt(`0x${randomBytes(8).toString("hex")}`) % range;
|
|
1198
|
+
return (min + rand).toString();
|
|
1199
|
+
};
|
|
1200
|
+
var cancelOrder = async (client, params) => {
|
|
1201
|
+
const result = await client.post("trading", ENDPOINTS.trading.cancelOrder, params);
|
|
1202
|
+
return result.result;
|
|
1203
|
+
};
|
|
1204
|
+
var cancelAllOrders = async (client, params) => {
|
|
1205
|
+
const result = await client.post("trading", ENDPOINTS.trading.cancelAllOrders, params);
|
|
1206
|
+
return result.result;
|
|
1207
|
+
};
|
|
1208
|
+
var getOrder = async (client, params) => {
|
|
1209
|
+
const result = await client.post("trading", ENDPOINTS.trading.getOrder, params);
|
|
1210
|
+
return result.result;
|
|
1211
|
+
};
|
|
1212
|
+
var getOpenOrders = async (client, params) => {
|
|
1213
|
+
const result = await client.post("trading", ENDPOINTS.trading.openOrders, params);
|
|
1214
|
+
return result.result;
|
|
1215
|
+
};
|
|
1216
|
+
var getOrderHistory = async (client, params) => {
|
|
1217
|
+
return client.post("trading", ENDPOINTS.trading.orderHistory, params);
|
|
1218
|
+
};
|
|
1219
|
+
var registerOrderCommands = (parent) => {
|
|
1220
|
+
const orderCmd = parent.command("order").description("Order lifecycle management");
|
|
1221
|
+
orderCmd.command("create").description("Create a new order").option("--sub-account-id <id>", "sub-account ID").requiredOption("--instrument <name>", "instrument symbol").requiredOption("--side <side>", "order side: buy|sell").requiredOption("--type <type>", "order type: market|limit").requiredOption("--qty <amount>", "order quantity").option("--price <price>", "limit price (required for limit orders)").option("--time-in-force <tif>", "GTT|IOC|AON|FOK", "GOOD_TILL_TIME").option("--reduce-only", "reduce-only order").option("--post-only", "post-only order").option("--client-order-id <id>", "client order ID (auto-generated if not set)").option("--expiration-seconds <n>", "order expiration in seconds", "3600").option("--json <path>", "read full request body from file (@path) or stdin (-)").option("--dry-run", "validate and show payload without sending").action(wrapAction(true, async (ctx, opts) => {
|
|
1222
|
+
let orderPayload;
|
|
1223
|
+
if (opts["json"]) {
|
|
1224
|
+
orderPayload = parseJsonInput(opts["json"]);
|
|
1225
|
+
} else {
|
|
1226
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1227
|
+
const instrument = opts["instrument"];
|
|
1228
|
+
const side = opts["side"].toUpperCase();
|
|
1229
|
+
const type = opts["type"].toLowerCase();
|
|
1230
|
+
const qty = opts["qty"];
|
|
1231
|
+
const price = opts["price"];
|
|
1232
|
+
if (type === "limit" && !price) {
|
|
1233
|
+
exitUsage("--price is required for limit orders");
|
|
1234
|
+
}
|
|
1235
|
+
if (!ctx.config.privateKey) {
|
|
1236
|
+
exitAuth("Private key required for order signing. Run `grvt config set privateKey <key>` or `grvt auth login --private-key <key>`.");
|
|
1237
|
+
}
|
|
1238
|
+
const instrumentMeta = await getInstrument(ctx.config.env, instrument, ctx.config.cookie, ctx.config.accountId);
|
|
1239
|
+
const isMarket = type === "market";
|
|
1240
|
+
const leg = {
|
|
1241
|
+
instrument,
|
|
1242
|
+
size: qty,
|
|
1243
|
+
limit_price: isMarket ? "0" : price ?? "0",
|
|
1244
|
+
is_buying_asset: side === "BUY"
|
|
1245
|
+
};
|
|
1246
|
+
const tifMap = {
|
|
1247
|
+
GTT: "GOOD_TILL_TIME",
|
|
1248
|
+
IOC: "IMMEDIATE_OR_CANCEL",
|
|
1249
|
+
AON: "ALL_OR_NONE",
|
|
1250
|
+
FOK: "FILL_OR_KILL",
|
|
1251
|
+
GOOD_TILL_TIME: "GOOD_TILL_TIME",
|
|
1252
|
+
IMMEDIATE_OR_CANCEL: "IMMEDIATE_OR_CANCEL",
|
|
1253
|
+
ALL_OR_NONE: "ALL_OR_NONE",
|
|
1254
|
+
FILL_OR_KILL: "FILL_OR_KILL"
|
|
1255
|
+
};
|
|
1256
|
+
const tifInput = opts["timeInForce"] ?? "GOOD_TILL_TIME";
|
|
1257
|
+
const timeInForce = tifMap[tifInput.toUpperCase()];
|
|
1258
|
+
if (!timeInForce) {
|
|
1259
|
+
return exitUsage(`Invalid time-in-force: ${tifInput}. Use GTT, IOC, AON, or FOK.`);
|
|
1260
|
+
}
|
|
1261
|
+
const nonce = generateNonce();
|
|
1262
|
+
const expiration = generateExpiration(Number(opts["expirationSeconds"] ?? 3600));
|
|
1263
|
+
const clientOrderId = opts["clientOrderId"] ?? generateClientOrderId();
|
|
1264
|
+
const typedData = buildOrderTypedData({
|
|
1265
|
+
subAccountId,
|
|
1266
|
+
isMarket,
|
|
1267
|
+
timeInForce,
|
|
1268
|
+
postOnly: Boolean(opts["postOnly"]),
|
|
1269
|
+
reduceOnly: Boolean(opts["reduceOnly"]),
|
|
1270
|
+
legs: [leg],
|
|
1271
|
+
instruments: {
|
|
1272
|
+
[instrument]: {
|
|
1273
|
+
instrument_hash: instrumentMeta.instrument_hash,
|
|
1274
|
+
base_decimals: instrumentMeta.base_decimals ?? 9
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
expiration,
|
|
1278
|
+
nonce
|
|
1279
|
+
}, ctx.config.env);
|
|
1280
|
+
const sigComponents = await signTypedData(ctx.config.privateKey, typedData);
|
|
1281
|
+
orderPayload = {
|
|
1282
|
+
order: {
|
|
1283
|
+
sub_account_id: subAccountId,
|
|
1284
|
+
is_market: isMarket,
|
|
1285
|
+
time_in_force: timeInForce,
|
|
1286
|
+
post_only: Boolean(opts["postOnly"]),
|
|
1287
|
+
reduce_only: Boolean(opts["reduceOnly"]),
|
|
1288
|
+
legs: [leg],
|
|
1289
|
+
signature: {
|
|
1290
|
+
signer: sigComponents.signer,
|
|
1291
|
+
r: sigComponents.r,
|
|
1292
|
+
s: sigComponents.s,
|
|
1293
|
+
v: sigComponents.v,
|
|
1294
|
+
expiration,
|
|
1295
|
+
nonce
|
|
1296
|
+
},
|
|
1297
|
+
metadata: { client_order_id: clientOrderId }
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
if (opts["dryRun"]) {
|
|
1302
|
+
logInfo("Dry run - payload that would be sent:", Boolean(ctx.globalOpts["silent"]));
|
|
1303
|
+
printOutput(orderPayload, { ...ctx.outputOptions, output: "json", pretty: true });
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const result = await ctx.client.post("trading", ENDPOINTS.trading.createOrder, orderPayload);
|
|
1307
|
+
output(result.result, ctx.outputOptions);
|
|
1308
|
+
}));
|
|
1309
|
+
orderCmd.command("get").description("Get an order by ID").option("--sub-account-id <id>", "sub-account ID").option("--order-id <id>", "order ID").option("--client-order-id <id>", "client order ID").action(wrapAction(true, async (ctx, opts) => {
|
|
1310
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1311
|
+
if (!opts["orderId"] && !opts["clientOrderId"]) {
|
|
1312
|
+
exitUsage("Either --order-id or --client-order-id is required");
|
|
1313
|
+
}
|
|
1314
|
+
const body = {
|
|
1315
|
+
sub_account_id: subAccountId,
|
|
1316
|
+
...opts["orderId"] ? { order_id: opts["orderId"] } : {},
|
|
1317
|
+
...opts["clientOrderId"] ? { client_order_id: opts["clientOrderId"] } : {}
|
|
1318
|
+
};
|
|
1319
|
+
const result = await getOrder(ctx.client, body);
|
|
1320
|
+
output(result, ctx.outputOptions);
|
|
1321
|
+
}));
|
|
1322
|
+
orderCmd.command("open").description("List open orders").option("--sub-account-id <id>", "sub-account ID").option("--kind <kinds>", "filter by kind (comma-separated)").option("--base <currencies>", "filter by base currency").option("--quote <currencies>", "filter by quote currency").action(wrapAction(true, async (ctx, opts) => {
|
|
1323
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1324
|
+
const body = {
|
|
1325
|
+
sub_account_id: subAccountId,
|
|
1326
|
+
...opts["kind"] ? { kind: opts["kind"].split(",") } : {},
|
|
1327
|
+
...opts["base"] ? { base: opts["base"].split(",") } : {},
|
|
1328
|
+
...opts["quote"] ? { quote: opts["quote"].split(",") } : {}
|
|
1329
|
+
};
|
|
1330
|
+
const result = await getOpenOrders(ctx.client, body);
|
|
1331
|
+
output(result, ctx.outputOptions);
|
|
1332
|
+
}));
|
|
1333
|
+
orderCmd.command("cancel").description("Cancel a single order").option("--sub-account-id <id>", "sub-account ID").option("--order-id <id>", "order ID").option("--client-order-id <id>", "client order ID").option("--dry-run", "validate and show payload without sending").action(wrapAction(true, async (ctx, opts) => {
|
|
1334
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1335
|
+
if (!opts["orderId"] && !opts["clientOrderId"]) {
|
|
1336
|
+
exitUsage("Either --order-id or --client-order-id is required");
|
|
1337
|
+
}
|
|
1338
|
+
const body = {
|
|
1339
|
+
sub_account_id: subAccountId,
|
|
1340
|
+
...opts["orderId"] ? { order_id: opts["orderId"] } : {},
|
|
1341
|
+
...opts["clientOrderId"] ? { client_order_id: opts["clientOrderId"] } : {}
|
|
1342
|
+
};
|
|
1343
|
+
if (opts["dryRun"]) {
|
|
1344
|
+
logInfo("Dry run - payload that would be sent:", Boolean(ctx.globalOpts["silent"]));
|
|
1345
|
+
printOutput(body, { ...ctx.outputOptions, output: "json", pretty: true });
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const result = await cancelOrder(ctx.client, body);
|
|
1349
|
+
output(result, ctx.outputOptions);
|
|
1350
|
+
}));
|
|
1351
|
+
orderCmd.command("cancel-all").description("Cancel all open orders").option("--sub-account-id <id>", "sub-account ID").option("--instrument <name>", "filter by instrument").option("--dry-run", "validate and show payload without sending").action(wrapAction(true, async (ctx, opts) => {
|
|
1352
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1353
|
+
const body = {
|
|
1354
|
+
sub_account_id: subAccountId,
|
|
1355
|
+
...opts["instrument"] ? { instrument: opts["instrument"] } : {}
|
|
1356
|
+
};
|
|
1357
|
+
if (opts["dryRun"]) {
|
|
1358
|
+
logInfo("Dry run - payload that would be sent:", Boolean(ctx.globalOpts["silent"]));
|
|
1359
|
+
printOutput(body, { ...ctx.outputOptions, output: "json", pretty: true });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const skipConfirm = Boolean(ctx.globalOpts["yes"]);
|
|
1363
|
+
const ok = await confirm("Cancel ALL open orders?", skipConfirm);
|
|
1364
|
+
if (!ok) {
|
|
1365
|
+
logInfo("Aborted.", Boolean(ctx.globalOpts["silent"]));
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const result = await cancelAllOrders(ctx.client, body);
|
|
1369
|
+
output(result, ctx.outputOptions);
|
|
1370
|
+
}));
|
|
1371
|
+
orderCmd.command("history").description("Get order history").option("--sub-account-id <id>", "sub-account ID").option("--kind <kinds>", "filter by kind (comma-separated)").option("--base <currencies>", "filter by base currency").option("--quote <currencies>", "filter by quote currency").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max results").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
1372
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1373
|
+
const results = await paginateCursor({
|
|
1374
|
+
fetchPage: async (cursor) => {
|
|
1375
|
+
const body = {
|
|
1376
|
+
sub_account_id: subAccountId,
|
|
1377
|
+
...opts["kind"] ? { kind: opts["kind"].split(",") } : {},
|
|
1378
|
+
...opts["base"] ? { base: opts["base"].split(",") } : {},
|
|
1379
|
+
...opts["quote"] ? { quote: opts["quote"].split(",") } : {},
|
|
1380
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
1381
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
1382
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
1383
|
+
...cursor ? { cursor } : {}
|
|
1384
|
+
};
|
|
1385
|
+
const resp = await getOrderHistory(ctx.client, body);
|
|
1386
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
1387
|
+
},
|
|
1388
|
+
cursor: opts["cursor"],
|
|
1389
|
+
all: Boolean(opts["all"]),
|
|
1390
|
+
outputOptions: ctx.outputOptions
|
|
1391
|
+
});
|
|
1392
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
1393
|
+
output(results, ctx.outputOptions);
|
|
1394
|
+
}
|
|
1395
|
+
}));
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
// src/commands/trade/fills.ts
|
|
1399
|
+
var getFillHistory = async (client, params) => {
|
|
1400
|
+
return client.post("trading", ENDPOINTS.trading.fillHistory, params);
|
|
1401
|
+
};
|
|
1402
|
+
var registerFillCommands = (parent) => {
|
|
1403
|
+
parent.command("fills").description("Get fill (trade execution) history").option("--sub-account-id <id>", "sub-account ID").option("--kind <kinds>", "filter by kind (comma-separated)").option("--base <currencies>", "filter by base currency").option("--quote <currencies>", "filter by quote currency").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max results").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
1404
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1405
|
+
const results = await paginateCursor({
|
|
1406
|
+
fetchPage: async (cursor) => {
|
|
1407
|
+
const body = {
|
|
1408
|
+
sub_account_id: subAccountId,
|
|
1409
|
+
...opts["kind"] ? { kind: opts["kind"].split(",") } : {},
|
|
1410
|
+
...opts["base"] ? { base: opts["base"].split(",") } : {},
|
|
1411
|
+
...opts["quote"] ? { quote: opts["quote"].split(",") } : {},
|
|
1412
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
1413
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
1414
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
1415
|
+
...cursor ? { cursor } : {}
|
|
1416
|
+
};
|
|
1417
|
+
const resp = await getFillHistory(ctx.client, body);
|
|
1418
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
1419
|
+
},
|
|
1420
|
+
cursor: opts["cursor"],
|
|
1421
|
+
all: Boolean(opts["all"]),
|
|
1422
|
+
outputOptions: ctx.outputOptions
|
|
1423
|
+
});
|
|
1424
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
1425
|
+
output(results, ctx.outputOptions);
|
|
1426
|
+
}
|
|
1427
|
+
}));
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
// src/commands/trade/positions.ts
|
|
1431
|
+
var getPositions = async (client, params) => {
|
|
1432
|
+
const result = await client.post("trading", ENDPOINTS.trading.positions, params);
|
|
1433
|
+
return result.result;
|
|
1434
|
+
};
|
|
1435
|
+
var registerPositionCommands = (parent) => {
|
|
1436
|
+
parent.command("positions").description("Get current positions").option("--sub-account-id <id>", "sub-account ID").option("--kind <kinds>", "filter by kind (comma-separated)").option("--base <currencies>", "filter by base currency").option("--quote <currencies>", "filter by quote currency").action(wrapAction(true, async (ctx, opts) => {
|
|
1437
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1438
|
+
const body = {
|
|
1439
|
+
sub_account_id: subAccountId,
|
|
1440
|
+
...opts["kind"] ? { kind: opts["kind"].split(",") } : {},
|
|
1441
|
+
...opts["base"] ? { base: opts["base"].split(",") } : {},
|
|
1442
|
+
...opts["quote"] ? { quote: opts["quote"].split(",") } : {}
|
|
1443
|
+
};
|
|
1444
|
+
const result = await getPositions(ctx.client, body);
|
|
1445
|
+
output(result, ctx.outputOptions);
|
|
1446
|
+
}));
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
// src/commands/trade/funding-payments.ts
|
|
1450
|
+
var getFundingPayments = async (client, params) => {
|
|
1451
|
+
return client.post("trading", ENDPOINTS.trading.fundingPaymentHistory, params);
|
|
1452
|
+
};
|
|
1453
|
+
var registerFundingPaymentCommands = (parent) => {
|
|
1454
|
+
parent.command("funding-payments").description("Get funding payment history").option("--sub-account-id <id>", "sub-account ID").option("--kind <kinds>", "filter by kind (comma-separated)").option("--base <currencies>", "filter by base currency").option("--quote <currencies>", "filter by quote currency").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max results").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
1455
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1456
|
+
const results = await paginateCursor({
|
|
1457
|
+
fetchPage: async (cursor) => {
|
|
1458
|
+
const body = {
|
|
1459
|
+
sub_account_id: subAccountId,
|
|
1460
|
+
...opts["kind"] ? { kind: opts["kind"].split(",") } : {},
|
|
1461
|
+
...opts["base"] ? { base: opts["base"].split(",") } : {},
|
|
1462
|
+
...opts["quote"] ? { quote: opts["quote"].split(",") } : {},
|
|
1463
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
1464
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
1465
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
1466
|
+
...cursor ? { cursor } : {}
|
|
1467
|
+
};
|
|
1468
|
+
const resp = await getFundingPayments(ctx.client, body);
|
|
1469
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
1470
|
+
},
|
|
1471
|
+
cursor: opts["cursor"],
|
|
1472
|
+
all: Boolean(opts["all"]),
|
|
1473
|
+
outputOptions: ctx.outputOptions
|
|
1474
|
+
});
|
|
1475
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
1476
|
+
output(results, ctx.outputOptions);
|
|
1477
|
+
}
|
|
1478
|
+
}));
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
// src/commands/trade/leverage.ts
|
|
1482
|
+
var registerLeverageCommands = (parent) => {
|
|
1483
|
+
const leverageCmd = parent.command("leverage").description("Leverage settings");
|
|
1484
|
+
leverageCmd.command("get").description("Get initial leverage settings for all instruments (or one)").option("--sub-account-id <id>", "sub-account ID").option("--instrument <name>", "filter by instrument (client-side)").action(wrapAction(true, async (ctx, opts) => {
|
|
1485
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1486
|
+
const body = { sub_account_id: subAccountId };
|
|
1487
|
+
const result = await ctx.client.post("trading", ENDPOINTS.trading.getAllInitialLeverage, body);
|
|
1488
|
+
const data = result.result;
|
|
1489
|
+
const instrument = opts["instrument"];
|
|
1490
|
+
if (instrument && Array.isArray(data)) {
|
|
1491
|
+
const filtered = data.filter((entry) => entry["instrument"] === instrument);
|
|
1492
|
+
output(filtered.length === 1 ? filtered[0] : filtered, ctx.outputOptions);
|
|
1493
|
+
} else {
|
|
1494
|
+
output(data, ctx.outputOptions);
|
|
1495
|
+
}
|
|
1496
|
+
}));
|
|
1497
|
+
leverageCmd.command("set").description("Set initial leverage for an instrument").requiredOption("--instrument <name>", "instrument symbol").requiredOption("--leverage <value>", "leverage value (e.g. 10)").option("--sub-account-id <id>", "sub-account ID").action(wrapAction(true, async (ctx, opts) => {
|
|
1498
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1499
|
+
const body = {
|
|
1500
|
+
sub_account_id: subAccountId,
|
|
1501
|
+
instrument: opts["instrument"],
|
|
1502
|
+
leverage: opts["leverage"]
|
|
1503
|
+
};
|
|
1504
|
+
const result = await ctx.client.post("trading", ENDPOINTS.trading.setInitialLeverage, body);
|
|
1505
|
+
output(result.result, ctx.outputOptions);
|
|
1506
|
+
}));
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
// src/commands/trade/cancel-on-disconnect.ts
|
|
1510
|
+
var registerCancelOnDisconnectCommands = (parent) => {
|
|
1511
|
+
parent.command("cancel-on-disconnect").description("Set, refresh, or disable cancel-on-disconnect countdown (1000\u2013300000ms, 0 to disable)").requiredOption("--countdown <ms>", "countdown in milliseconds (0 to disable)").option("--sub-account-id <id>", "sub-account ID").action(wrapAction(true, async (ctx, opts) => {
|
|
1512
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1513
|
+
const countdown = Number(opts["countdown"]);
|
|
1514
|
+
if (isNaN(countdown) || countdown < 0) {
|
|
1515
|
+
return exitUsage("--countdown must be a non-negative integer (ms)");
|
|
1516
|
+
}
|
|
1517
|
+
if (countdown !== 0 && (countdown < 1e3 || countdown > 3e5)) {
|
|
1518
|
+
return exitUsage("--countdown must be 0 (disable) or between 1000 and 300000 ms");
|
|
1519
|
+
}
|
|
1520
|
+
const body = {
|
|
1521
|
+
sub_account_id: subAccountId,
|
|
1522
|
+
countdown_time_ms: countdown
|
|
1523
|
+
};
|
|
1524
|
+
const result = await ctx.client.post("trading", ENDPOINTS.trading.cancelOnDisconnect, body);
|
|
1525
|
+
output(result.result, ctx.outputOptions);
|
|
1526
|
+
}));
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
// src/commands/trade/derisk.ts
|
|
1530
|
+
var registerDeriskCommands = (parent) => {
|
|
1531
|
+
parent.command("derisk").description("Set derisk-to-maintenance-margin ratio").requiredOption("--ratio <value>", "derisk MM ratio (decimal, e.g. 0.5)").option("--sub-account-id <id>", "sub-account ID").option("--dry-run", "validate and show payload without sending").action(wrapAction(true, async (ctx, opts) => {
|
|
1532
|
+
if (!ctx.config.privateKey) {
|
|
1533
|
+
exitAuth("Private key required for derisk signing. Run `grvt config set privateKey <key>`.");
|
|
1534
|
+
}
|
|
1535
|
+
const subAccountId = resolveSubAccountId(opts, ctx.config);
|
|
1536
|
+
const ratio = opts["ratio"];
|
|
1537
|
+
const nonce = generateNonce();
|
|
1538
|
+
const expiration = generateExpiration(3600);
|
|
1539
|
+
const typedData = buildDeriskTypedData({
|
|
1540
|
+
subAccountId,
|
|
1541
|
+
ratio,
|
|
1542
|
+
expiration,
|
|
1543
|
+
nonce
|
|
1544
|
+
}, ctx.config.env);
|
|
1545
|
+
const sigComponents = await signTypedData(ctx.config.privateKey, typedData);
|
|
1546
|
+
const body = {
|
|
1547
|
+
sub_account_id: subAccountId,
|
|
1548
|
+
ratio,
|
|
1549
|
+
signature: {
|
|
1550
|
+
signer: sigComponents.signer,
|
|
1551
|
+
r: sigComponents.r,
|
|
1552
|
+
s: sigComponents.s,
|
|
1553
|
+
v: sigComponents.v,
|
|
1554
|
+
expiration,
|
|
1555
|
+
nonce
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
if (opts["dryRun"]) {
|
|
1559
|
+
logInfo("Dry run - payload that would be sent:", Boolean(ctx.globalOpts["silent"]));
|
|
1560
|
+
printOutput(body, { ...ctx.outputOptions, output: "json", pretty: true });
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const result = await ctx.client.post("trading", ENDPOINTS.trading.setDeriskMmRatio, body);
|
|
1564
|
+
output(result.result, ctx.outputOptions);
|
|
1565
|
+
}));
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// src/commands/trade/index.ts
|
|
1569
|
+
var registerTradeCommands = (program2) => {
|
|
1570
|
+
const tradeCmd = program2.command("trade").description("Trading: orders, fills, positions, funding, leverage, derisk");
|
|
1571
|
+
registerOrderCommands(tradeCmd);
|
|
1572
|
+
registerFillCommands(tradeCmd);
|
|
1573
|
+
registerPositionCommands(tradeCmd);
|
|
1574
|
+
registerFundingPaymentCommands(tradeCmd);
|
|
1575
|
+
registerLeverageCommands(tradeCmd);
|
|
1576
|
+
registerCancelOnDisconnectCommands(tradeCmd);
|
|
1577
|
+
registerDeriskCommands(tradeCmd);
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// src/commands/funds/deposits.ts
|
|
1581
|
+
var getDepositHistory = async (client, params) => {
|
|
1582
|
+
return client.post("trading", ENDPOINTS.funds.depositHistory, params);
|
|
1583
|
+
};
|
|
1584
|
+
var registerDepositCommands = (parent) => {
|
|
1585
|
+
const depositCmd = parent.command("deposit").description("Deposit management");
|
|
1586
|
+
depositCmd.command("history").description("Get deposit history").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max results").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
1587
|
+
const results = await paginateCursor({
|
|
1588
|
+
fetchPage: async (cursor) => {
|
|
1589
|
+
const body = {
|
|
1590
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
1591
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
1592
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
1593
|
+
...cursor ? { cursor } : {}
|
|
1594
|
+
};
|
|
1595
|
+
const resp = await getDepositHistory(ctx.client, body);
|
|
1596
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
1597
|
+
},
|
|
1598
|
+
cursor: opts["cursor"],
|
|
1599
|
+
all: Boolean(opts["all"]),
|
|
1600
|
+
outputOptions: ctx.outputOptions
|
|
1601
|
+
});
|
|
1602
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
1603
|
+
output(results, ctx.outputOptions);
|
|
1604
|
+
}
|
|
1605
|
+
}));
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
// src/core/currencies/cache.ts
|
|
1609
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1610
|
+
import { join as join3 } from "path";
|
|
1611
|
+
import { homedir as homedir3 } from "os";
|
|
1612
|
+
var CACHE_TTL_MS2 = 60 * 60 * 1e3;
|
|
1613
|
+
var getCacheDir2 = () => {
|
|
1614
|
+
const xdgCache = process.env["XDG_CACHE_HOME"];
|
|
1615
|
+
const base = xdgCache || join3(homedir3(), ".cache");
|
|
1616
|
+
return join3(base, "grvt");
|
|
1617
|
+
};
|
|
1618
|
+
var getCachePath2 = (env) => join3(getCacheDir2(), `currencies-${env}.json`);
|
|
1619
|
+
var loadCache2 = (env) => {
|
|
1620
|
+
const path = getCachePath2(env);
|
|
1621
|
+
if (!existsSync3(path)) return null;
|
|
1622
|
+
try {
|
|
1623
|
+
const raw = readFileSync5(path, "utf-8");
|
|
1624
|
+
const cache = JSON.parse(raw);
|
|
1625
|
+
if (cache.env !== env || Date.now() - cache.fetched_at > CACHE_TTL_MS2) {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
return cache;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
var saveCache2 = (env, currencies) => {
|
|
1634
|
+
const dir = getCacheDir2();
|
|
1635
|
+
if (!existsSync3(dir)) {
|
|
1636
|
+
mkdirSync3(dir, { recursive: true });
|
|
1637
|
+
}
|
|
1638
|
+
const cache = { env, fetched_at: Date.now(), currencies };
|
|
1639
|
+
writeFileSync4(getCachePath2(env), JSON.stringify(cache), "utf-8");
|
|
1640
|
+
};
|
|
1641
|
+
var getCurrencies = async (env, cookie, accountId) => {
|
|
1642
|
+
const cached = loadCache2(env);
|
|
1643
|
+
if (cached) return cached.currencies;
|
|
1644
|
+
const client = createHttpClient({ env, cookie, accountId });
|
|
1645
|
+
const response = await client.post(
|
|
1646
|
+
"marketData",
|
|
1647
|
+
ENDPOINTS.marketData.currency,
|
|
1648
|
+
{}
|
|
1649
|
+
);
|
|
1650
|
+
const currencies = {};
|
|
1651
|
+
for (const cur of response.result ?? []) {
|
|
1652
|
+
currencies[cur.symbol] = cur;
|
|
1653
|
+
}
|
|
1654
|
+
saveCache2(env, currencies);
|
|
1655
|
+
return currencies;
|
|
1656
|
+
};
|
|
1657
|
+
var getCurrencyId = async (env, symbol, cookie, accountId) => {
|
|
1658
|
+
const currencies = await getCurrencies(env, cookie, accountId);
|
|
1659
|
+
const cur = currencies[symbol.toUpperCase()];
|
|
1660
|
+
if (!cur) {
|
|
1661
|
+
throw new Error(`Currency not found: ${symbol}. Run 'grvt market currency' to see available currencies.`);
|
|
1662
|
+
}
|
|
1663
|
+
return cur.id;
|
|
1664
|
+
};
|
|
1665
|
+
var getCurrencyDecimals = async (env, symbol, cookie, accountId) => {
|
|
1666
|
+
const currencies = await getCurrencies(env, cookie, accountId);
|
|
1667
|
+
const cur = currencies[symbol.toUpperCase()];
|
|
1668
|
+
if (!cur) {
|
|
1669
|
+
throw new Error(`Currency not found: ${symbol}. Run 'grvt market currency' to see available currencies.`);
|
|
1670
|
+
}
|
|
1671
|
+
return cur.balance_decimals;
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
// src/commands/funds/transfers.ts
|
|
1675
|
+
var getTransferHistory = async (client, params) => {
|
|
1676
|
+
return client.post("trading", ENDPOINTS.funds.transferHistory, params);
|
|
1677
|
+
};
|
|
1678
|
+
var registerTransferCommands = (parent) => {
|
|
1679
|
+
const transferCmd = parent.command("transfer").description("Transfer management");
|
|
1680
|
+
transferCmd.command("create").description("Transfer between sub-accounts").requiredOption("--from-sub-account-id <id>", "source sub-account ID").requiredOption("--to-sub-account-id <id>", "destination sub-account ID").requiredOption("--currency <symbol>", "currency to transfer (e.g. USDT)").requiredOption("--amount <amount>", "amount to transfer").option("--json <path>", "read full request body from file (@path) or stdin (-)").option("--dry-run", "validate and show payload without sending").action(wrapAction(true, async (ctx, opts) => {
|
|
1681
|
+
let body;
|
|
1682
|
+
if (opts["json"]) {
|
|
1683
|
+
body = parseJsonInput(opts["json"]);
|
|
1684
|
+
} else {
|
|
1685
|
+
if (!ctx.config.privateKey) {
|
|
1686
|
+
exitAuth("Private key required for transfer signing. Run `grvt config set privateKey <key>`.");
|
|
1687
|
+
}
|
|
1688
|
+
const fromSubAccountId = requireOption(opts["fromSubAccountId"], "from-sub-account-id");
|
|
1689
|
+
const toSubAccountId = requireOption(opts["toSubAccountId"], "to-sub-account-id");
|
|
1690
|
+
const currency = opts["currency"].toUpperCase();
|
|
1691
|
+
const amount = opts["amount"];
|
|
1692
|
+
const fromAccount = ctx.config.accountId;
|
|
1693
|
+
const toAccount = fromAccount;
|
|
1694
|
+
const signerAccount = createSigner(ctx.config.privateKey);
|
|
1695
|
+
const tokenCurrency = await getCurrencyId(ctx.config.env, currency, ctx.config.cookie, ctx.config.accountId);
|
|
1696
|
+
const balanceDecimals = await getCurrencyDecimals(ctx.config.env, currency, ctx.config.cookie, ctx.config.accountId);
|
|
1697
|
+
const nonce = generateNonce();
|
|
1698
|
+
const expiration = generateExpiration(3600);
|
|
1699
|
+
const typedData = buildTransferTypedData({
|
|
1700
|
+
fromAccount: signerAccount.address,
|
|
1701
|
+
fromSubAccount: fromSubAccountId,
|
|
1702
|
+
toAccount: signerAccount.address,
|
|
1703
|
+
toSubAccount: toSubAccountId,
|
|
1704
|
+
tokenCurrency,
|
|
1705
|
+
numTokens: amount,
|
|
1706
|
+
balanceDecimals,
|
|
1707
|
+
expiration,
|
|
1708
|
+
nonce
|
|
1709
|
+
}, ctx.config.env);
|
|
1710
|
+
const sigComponents = await signTypedData(ctx.config.privateKey, typedData);
|
|
1711
|
+
body = {
|
|
1712
|
+
from_account_id: fromAccount,
|
|
1713
|
+
from_sub_account_id: fromSubAccountId,
|
|
1714
|
+
to_account_id: toAccount,
|
|
1715
|
+
to_sub_account_id: toSubAccountId,
|
|
1716
|
+
currency,
|
|
1717
|
+
num_tokens: amount,
|
|
1718
|
+
signature: {
|
|
1719
|
+
signer: sigComponents.signer,
|
|
1720
|
+
r: sigComponents.r,
|
|
1721
|
+
s: sigComponents.s,
|
|
1722
|
+
v: sigComponents.v,
|
|
1723
|
+
expiration,
|
|
1724
|
+
nonce
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
if (opts["dryRun"]) {
|
|
1729
|
+
logInfo("Dry run - payload that would be sent:", Boolean(ctx.globalOpts["silent"]));
|
|
1730
|
+
printOutput(body, { ...ctx.outputOptions, output: "json", pretty: true });
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const skipConfirm = Boolean(ctx.globalOpts["yes"]);
|
|
1734
|
+
const ok = await confirm(
|
|
1735
|
+
`Transfer ${body["num_tokens"]} ${body["currency"]} from ${body["from_sub_account_id"]} to ${body["to_sub_account_id"]}?`,
|
|
1736
|
+
skipConfirm
|
|
1737
|
+
);
|
|
1738
|
+
if (!ok) {
|
|
1739
|
+
logInfo("Aborted.", Boolean(ctx.globalOpts["silent"]));
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const result = await ctx.client.post("trading", ENDPOINTS.funds.transfer, body);
|
|
1743
|
+
output(result.result, ctx.outputOptions);
|
|
1744
|
+
}));
|
|
1745
|
+
transferCmd.command("history").description("Get transfer history").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max results").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
1746
|
+
const results = await paginateCursor({
|
|
1747
|
+
fetchPage: async (cursor) => {
|
|
1748
|
+
const body = {
|
|
1749
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
1750
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
1751
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
1752
|
+
...cursor ? { cursor } : {}
|
|
1753
|
+
};
|
|
1754
|
+
const resp = await getTransferHistory(ctx.client, body);
|
|
1755
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
1756
|
+
},
|
|
1757
|
+
cursor: opts["cursor"],
|
|
1758
|
+
all: Boolean(opts["all"]),
|
|
1759
|
+
outputOptions: ctx.outputOptions
|
|
1760
|
+
});
|
|
1761
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
1762
|
+
output(results, ctx.outputOptions);
|
|
1763
|
+
}
|
|
1764
|
+
}));
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
// src/commands/funds/withdrawals.ts
|
|
1768
|
+
var getWithdrawalHistory = async (client, params) => {
|
|
1769
|
+
return client.post("trading", ENDPOINTS.funds.withdrawalHistory, params);
|
|
1770
|
+
};
|
|
1771
|
+
var registerWithdrawalCommands = (parent) => {
|
|
1772
|
+
const withdrawCmd = parent.command("withdraw").description("Withdrawal management");
|
|
1773
|
+
withdrawCmd.command("create").description("Withdraw from sub-account to Ethereum address").option("--sub-account-id <id>", "source sub-account ID").requiredOption("--to-address <address>", "destination Ethereum address").requiredOption("--currency <symbol>", "currency to withdraw (e.g. USDT)").requiredOption("--amount <amount>", "amount to withdraw").option("--main-account-id <id>", "main account ID (defaults to current account)").option("--json <path>", "read full request body from file (@path) or stdin (-)").option("--dry-run", "validate and show payload without sending").action(wrapAction(true, async (ctx, opts) => {
|
|
1774
|
+
let body;
|
|
1775
|
+
if (opts["json"]) {
|
|
1776
|
+
body = parseJsonInput(opts["json"]);
|
|
1777
|
+
} else {
|
|
1778
|
+
if (!ctx.config.privateKey) {
|
|
1779
|
+
exitAuth("Private key required for withdrawal signing. Run `grvt config set privateKey <key>`.");
|
|
1780
|
+
}
|
|
1781
|
+
const toAddress = opts["toAddress"];
|
|
1782
|
+
if (!toAddress.startsWith("0x") || toAddress.length !== 42) {
|
|
1783
|
+
exitUsage("--to-address must be a valid Ethereum address (0x...)");
|
|
1784
|
+
}
|
|
1785
|
+
const subAccountId = opts["subAccountId"] ?? ctx.config.subAccountId;
|
|
1786
|
+
if (!subAccountId) {
|
|
1787
|
+
exitUsage("--sub-account-id is required (or set subAccountId in config)");
|
|
1788
|
+
}
|
|
1789
|
+
const mainAccountId = opts["mainAccountId"] ?? requireOption(ctx.config.accountId, "main-account-id");
|
|
1790
|
+
const currency = opts["currency"].toUpperCase();
|
|
1791
|
+
const amount = opts["amount"];
|
|
1792
|
+
const signerAccount = createSigner(ctx.config.privateKey);
|
|
1793
|
+
const tokenCurrency = await getCurrencyId(ctx.config.env, currency, ctx.config.cookie, ctx.config.accountId);
|
|
1794
|
+
const balanceDecimals = await getCurrencyDecimals(ctx.config.env, currency, ctx.config.cookie, ctx.config.accountId);
|
|
1795
|
+
const nonce = generateNonce();
|
|
1796
|
+
const expiration = generateExpiration(3600);
|
|
1797
|
+
const typedData = buildWithdrawalTypedData({
|
|
1798
|
+
fromAccount: signerAccount.address,
|
|
1799
|
+
toEthAddress: toAddress,
|
|
1800
|
+
tokenCurrency,
|
|
1801
|
+
numTokens: amount,
|
|
1802
|
+
balanceDecimals,
|
|
1803
|
+
expiration,
|
|
1804
|
+
nonce
|
|
1805
|
+
}, ctx.config.env);
|
|
1806
|
+
const sigComponents = await signTypedData(ctx.config.privateKey, typedData);
|
|
1807
|
+
body = {
|
|
1808
|
+
main_account_id: mainAccountId,
|
|
1809
|
+
from_sub_account_id: subAccountId,
|
|
1810
|
+
to_eth_address: toAddress,
|
|
1811
|
+
currency,
|
|
1812
|
+
num_tokens: amount,
|
|
1813
|
+
signature: {
|
|
1814
|
+
signer: sigComponents.signer,
|
|
1815
|
+
r: sigComponents.r,
|
|
1816
|
+
s: sigComponents.s,
|
|
1817
|
+
v: sigComponents.v,
|
|
1818
|
+
expiration,
|
|
1819
|
+
nonce
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (opts["dryRun"]) {
|
|
1824
|
+
logInfo("Dry run - payload that would be sent:", Boolean(ctx.globalOpts["silent"]));
|
|
1825
|
+
printOutput(body, { ...ctx.outputOptions, output: "json", pretty: true });
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const skipConfirm = Boolean(ctx.globalOpts["yes"]);
|
|
1829
|
+
const ok = await confirm(
|
|
1830
|
+
`Withdraw ${body["num_tokens"]} ${body["currency"]} to ${body["to_eth_address"]}?`,
|
|
1831
|
+
skipConfirm
|
|
1832
|
+
);
|
|
1833
|
+
if (!ok) {
|
|
1834
|
+
logInfo("Aborted.", Boolean(ctx.globalOpts["silent"]));
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const result = await ctx.client.post("trading", ENDPOINTS.funds.withdrawal, body);
|
|
1838
|
+
output(result.result, ctx.outputOptions);
|
|
1839
|
+
}));
|
|
1840
|
+
withdrawCmd.command("history").description("Get withdrawal history").option("--start-time <time>", "start time").option("--end-time <time>", "end time").option("--limit <n>", "max results").option("--cursor <cursor>", "pagination cursor").option("--all", "auto-paginate all results").action(wrapAction(true, async (ctx, opts) => {
|
|
1841
|
+
const results = await paginateCursor({
|
|
1842
|
+
fetchPage: async (cursor) => {
|
|
1843
|
+
const body = {
|
|
1844
|
+
...opts["startTime"] ? { start_time: parseTimestamp(opts["startTime"]) } : {},
|
|
1845
|
+
...opts["endTime"] ? { end_time: parseTimestamp(opts["endTime"]) } : {},
|
|
1846
|
+
...opts["limit"] ? { limit: Number(opts["limit"]) } : {},
|
|
1847
|
+
...cursor ? { cursor } : {}
|
|
1848
|
+
};
|
|
1849
|
+
const resp = await getWithdrawalHistory(ctx.client, body);
|
|
1850
|
+
return { result: resp.result ?? [], next: resp.next };
|
|
1851
|
+
},
|
|
1852
|
+
cursor: opts["cursor"],
|
|
1853
|
+
all: Boolean(opts["all"]),
|
|
1854
|
+
outputOptions: ctx.outputOptions
|
|
1855
|
+
});
|
|
1856
|
+
if (ctx.outputOptions.output !== "ndjson" || !opts["all"]) {
|
|
1857
|
+
output(results, ctx.outputOptions);
|
|
1858
|
+
}
|
|
1859
|
+
}));
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
// src/commands/funds/index.ts
|
|
1863
|
+
var registerFundsCommands = (program2) => {
|
|
1864
|
+
const fundsCmd = program2.command("funds").description("Fund management: deposits, transfers, withdrawals");
|
|
1865
|
+
registerDepositCommands(fundsCmd);
|
|
1866
|
+
registerTransferCommands(fundsCmd);
|
|
1867
|
+
registerWithdrawalCommands(fundsCmd);
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
// src/cli.ts
|
|
1871
|
+
var program = new Command();
|
|
1872
|
+
program.name("grvt").description("CLI tool to trade on GRVT markets").version("0.1.0").option("--output <format>", "output format: json|ndjson|table|raw", "table").option("--pretty", "pretty-print JSON output").option("--silent", "suppress logs, only data to stdout").option("--no-color", "disable colored output").option("--yes", "skip confirmation prompts").option("--retries <n>", "number of retries on failure", "3").option("--timeout-ms <n>", "request timeout in milliseconds", "10000");
|
|
1873
|
+
registerConfigCommands(program);
|
|
1874
|
+
registerAuthCommands(program);
|
|
1875
|
+
registerAccountCommands(program);
|
|
1876
|
+
registerMarketCommands(program);
|
|
1877
|
+
registerTradeCommands(program);
|
|
1878
|
+
registerFundsCommands(program);
|
|
1879
|
+
program.parse();
|
|
1880
|
+
//# sourceMappingURL=cli.js.map
|