@candledottv/cli 0.11.12 → 0.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/index.js +2470 -167
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6210,6 +6210,55 @@ function serializeSignedTransaction(message, signature) {
|
|
|
6210
6210
|
function toBase642(bytes) {
|
|
6211
6211
|
return Buffer.from(bytes).toString("base64");
|
|
6212
6212
|
}
|
|
6213
|
+
function sanitizeDiagnostic(text, keep) {
|
|
6214
|
+
const clean2 = text.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/https?:\/\/\S+/gi, "<url>").replace(/(api[-_]?key|token|auth)=\S+/gi, "$1=<redacted>");
|
|
6215
|
+
return clean2.length > keep ? `${clean2.slice(0, keep)}…` : clean2;
|
|
6216
|
+
}
|
|
6217
|
+
function sanitizeSimulation(message, data) {
|
|
6218
|
+
let err = null;
|
|
6219
|
+
if (data.err !== undefined && data.err !== null) {
|
|
6220
|
+
let encoded;
|
|
6221
|
+
try {
|
|
6222
|
+
encoded = JSON.stringify(data.err);
|
|
6223
|
+
} catch {
|
|
6224
|
+
encoded = "unrepresentable";
|
|
6225
|
+
}
|
|
6226
|
+
err = encoded === undefined ? null : sanitizeDiagnostic(encoded, DIAGNOSTIC_ERR_CHARS - 1);
|
|
6227
|
+
}
|
|
6228
|
+
const lines = Array.isArray(data.logs) ? data.logs.filter((line) => typeof line === "string") : [];
|
|
6229
|
+
const kept = lines.slice(-DIAGNOSTIC_LOG_LINES);
|
|
6230
|
+
return {
|
|
6231
|
+
message: sanitizeDiagnostic(typeof message === "string" ? message : "", DIAGNOSTIC_MESSAGE_CHARS - 1),
|
|
6232
|
+
err,
|
|
6233
|
+
logs: kept.map((line) => sanitizeDiagnostic(line, DIAGNOSTIC_LOG_LINE_CHARS)),
|
|
6234
|
+
logsOmitted: lines.length - kept.length,
|
|
6235
|
+
...Number.isSafeInteger(data.unitsConsumed) ? { unitsConsumed: data.unitsConsumed } : {}
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
function definiteSendRejection(error) {
|
|
6239
|
+
if (!(error instanceof SolanaRpcError))
|
|
6240
|
+
return null;
|
|
6241
|
+
if (error.method !== "sendTransaction")
|
|
6242
|
+
return null;
|
|
6243
|
+
if (error.status !== undefined)
|
|
6244
|
+
return null;
|
|
6245
|
+
if (error.responseIdMatched !== true)
|
|
6246
|
+
return null;
|
|
6247
|
+
if (error.rateLimited)
|
|
6248
|
+
return null;
|
|
6249
|
+
if (error.rpcCode === RPC_PREFLIGHT_FAILED_CODE && error.simulation !== undefined && error.simulation.err !== null) {
|
|
6250
|
+
return { reason: "preflight-failed", rpcCode: RPC_PREFLIGHT_FAILED_CODE, diagnostics: error.simulation };
|
|
6251
|
+
}
|
|
6252
|
+
if (error.rpcCode === RPC_MIN_CONTEXT_SLOT_CODE && error.contextSlot !== undefined) {
|
|
6253
|
+
return {
|
|
6254
|
+
reason: "context-behind",
|
|
6255
|
+
rpcCode: RPC_MIN_CONTEXT_SLOT_CODE,
|
|
6256
|
+
diagnostics: null,
|
|
6257
|
+
contextSlot: error.contextSlot
|
|
6258
|
+
};
|
|
6259
|
+
}
|
|
6260
|
+
return null;
|
|
6261
|
+
}
|
|
6213
6262
|
function rpcRetryDelayMs(error) {
|
|
6214
6263
|
return Math.min(error.retryAfterMs ?? RPC_RETRY_DEFAULT_MS, RPC_RETRY_CAP_MS);
|
|
6215
6264
|
}
|
|
@@ -6273,16 +6322,56 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6273
6322
|
return await once(method, params, signal);
|
|
6274
6323
|
}
|
|
6275
6324
|
}
|
|
6325
|
+
async function read(method, params, opts, answersContext = true) {
|
|
6326
|
+
const required = opts?.minContextSlot;
|
|
6327
|
+
if (required === undefined) {
|
|
6328
|
+
const result = await call(method, params);
|
|
6329
|
+
const slot = answersContext ? contextSlotOf(result) : undefined;
|
|
6330
|
+
if (slot !== undefined)
|
|
6331
|
+
opts?.onContext?.(slot);
|
|
6332
|
+
return result;
|
|
6333
|
+
}
|
|
6334
|
+
let observed;
|
|
6335
|
+
for (let attempt = 0;; attempt += 1) {
|
|
6336
|
+
let slot;
|
|
6337
|
+
try {
|
|
6338
|
+
const result = await call(method, params);
|
|
6339
|
+
if (!answersContext)
|
|
6340
|
+
return result;
|
|
6341
|
+
slot = contextSlotOf(result);
|
|
6342
|
+
if (slot === undefined) {
|
|
6343
|
+
throw new SolanaRpcError(`RPC ${method} answered without a context slot, so minimum context slot ${required} cannot be verified`, { method, contextUnverified: true });
|
|
6344
|
+
}
|
|
6345
|
+
if (slot >= required) {
|
|
6346
|
+
opts?.onContext?.(slot);
|
|
6347
|
+
return result;
|
|
6348
|
+
}
|
|
6349
|
+
} catch (error) {
|
|
6350
|
+
if (!(error instanceof SolanaRpcError) || error.rpcCode !== RPC_MIN_CONTEXT_SLOT_CODE)
|
|
6351
|
+
throw error;
|
|
6352
|
+
slot = error.contextSlot;
|
|
6353
|
+
}
|
|
6354
|
+
if (slot !== undefined)
|
|
6355
|
+
observed = observed === undefined ? slot : Math.max(observed, slot);
|
|
6356
|
+
const wait = RPC_CONTEXT_BEHIND_SLEEPS_MS[attempt];
|
|
6357
|
+
if (wait === undefined) {
|
|
6358
|
+
throw new SolanaRpcError(`RPC ${method} failed: the node is behind minimum context slot ${required} (observed ${observed ?? "unknown"})`, { method, contextBehind: { required, observed } });
|
|
6359
|
+
}
|
|
6360
|
+
await sleep(wait);
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6276
6363
|
async function once(method, params, signal) {
|
|
6277
6364
|
id += 1;
|
|
6365
|
+
const requestId = id;
|
|
6278
6366
|
const res = await fetchFn(url, {
|
|
6279
6367
|
method: "POST",
|
|
6280
6368
|
headers: { "content-type": "application/json" },
|
|
6281
|
-
body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
|
|
6369
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: requestId, method, params }),
|
|
6282
6370
|
...signal !== undefined ? { signal } : {}
|
|
6283
6371
|
});
|
|
6284
6372
|
if (!res.ok) {
|
|
6285
6373
|
throw new SolanaRpcError(`RPC ${method} failed: HTTP ${res.status}`, {
|
|
6374
|
+
method,
|
|
6286
6375
|
status: res.status,
|
|
6287
6376
|
...res.status === 429 ? (() => {
|
|
6288
6377
|
const after = retryAfterMs(res.headers.get("retry-after"), Date.now());
|
|
@@ -6292,23 +6381,31 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6292
6381
|
}
|
|
6293
6382
|
const json = await res.json();
|
|
6294
6383
|
if (json.error) {
|
|
6384
|
+
const code = json.error.code;
|
|
6385
|
+
const data = json.error.data;
|
|
6386
|
+
const dataObject = data !== null && typeof data === "object" && !Array.isArray(data) ? data : undefined;
|
|
6387
|
+
const contextSlot = dataObject?.contextSlot;
|
|
6295
6388
|
throw new SolanaRpcError(`RPC ${method} failed: ${json.error.code ?? ""} ${json.error.message ?? ""}`.trim(), {
|
|
6296
|
-
...typeof
|
|
6389
|
+
...typeof code === "number" ? { rpcCode: code } : {},
|
|
6390
|
+
method,
|
|
6391
|
+
responseIdMatched: json.id === requestId,
|
|
6392
|
+
...code === RPC_PREFLIGHT_FAILED_CODE && dataObject ? { simulation: sanitizeSimulation(json.error.message, dataObject) } : {},
|
|
6393
|
+
...code === RPC_MIN_CONTEXT_SLOT_CODE && Number.isSafeInteger(contextSlot) ? { contextSlot } : {}
|
|
6297
6394
|
});
|
|
6298
6395
|
}
|
|
6299
6396
|
return json.result;
|
|
6300
6397
|
}
|
|
6301
6398
|
return {
|
|
6302
|
-
async getLatestBlockhash() {
|
|
6303
|
-
const r = await
|
|
6399
|
+
async getLatestBlockhash(opts) {
|
|
6400
|
+
const r = await read("getLatestBlockhash", [withMinContextSlot({ commitment: "finalized" }, opts)], opts);
|
|
6304
6401
|
return r.value.blockhash;
|
|
6305
6402
|
},
|
|
6306
|
-
async getBalance(address) {
|
|
6307
|
-
const r = await
|
|
6403
|
+
async getBalance(address, opts) {
|
|
6404
|
+
const r = await read("getBalance", [address, withMinContextSlot({ commitment: "finalized" }, opts)], opts);
|
|
6308
6405
|
return BigInt(r.value);
|
|
6309
6406
|
},
|
|
6310
|
-
async getTokenAccountsByOwner(owner, programId) {
|
|
6311
|
-
const r = await
|
|
6407
|
+
async getTokenAccountsByOwner(owner, programId, opts) {
|
|
6408
|
+
const r = await read("getTokenAccountsByOwner", [owner, { programId }, withMinContextSlot({ encoding: "jsonParsed", commitment: "finalized" }, opts)], opts);
|
|
6312
6409
|
return r.value.map((v) => ({
|
|
6313
6410
|
pubkey: v.pubkey,
|
|
6314
6411
|
mint: v.account.data.parsed.info.mint,
|
|
@@ -6318,8 +6415,8 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6318
6415
|
programId
|
|
6319
6416
|
}));
|
|
6320
6417
|
},
|
|
6321
|
-
async getFeeForMessage(messageBase64) {
|
|
6322
|
-
const r = await
|
|
6418
|
+
async getFeeForMessage(messageBase64, opts) {
|
|
6419
|
+
const r = await read("getFeeForMessage", [messageBase64, withMinContextSlot({ commitment: "finalized" }, opts)], opts);
|
|
6323
6420
|
return r.value === null ? null : BigInt(r.value);
|
|
6324
6421
|
},
|
|
6325
6422
|
async getMinimumBalanceForRentExemption(size) {
|
|
@@ -6328,8 +6425,8 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6328
6425
|
throw new Error("Invalid rent exemption quote");
|
|
6329
6426
|
return BigInt(rent);
|
|
6330
6427
|
},
|
|
6331
|
-
async getAccountInfo(address) {
|
|
6332
|
-
const r = await
|
|
6428
|
+
async getAccountInfo(address, opts) {
|
|
6429
|
+
const r = await read("getAccountInfo", [address, withMinContextSlot({ encoding: "base64", commitment: "finalized" }, opts)], opts);
|
|
6333
6430
|
const value = r.value;
|
|
6334
6431
|
if (!value)
|
|
6335
6432
|
return null;
|
|
@@ -6344,20 +6441,17 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6344
6441
|
data: new Uint8Array(Buffer.from(encoded, "base64"))
|
|
6345
6442
|
};
|
|
6346
6443
|
},
|
|
6347
|
-
async getEpoch() {
|
|
6348
|
-
const r = await
|
|
6444
|
+
async getEpoch(opts) {
|
|
6445
|
+
const r = await read("getEpochInfo", [withMinContextSlot({ commitment: "finalized" }, opts)], opts, false);
|
|
6349
6446
|
if (!Number.isSafeInteger(r?.epoch))
|
|
6350
6447
|
throw new Error("RPC getEpochInfo answered without an epoch");
|
|
6351
6448
|
return BigInt(r.epoch);
|
|
6352
6449
|
},
|
|
6353
|
-
async getMultipleAccounts(addresses) {
|
|
6450
|
+
async getMultipleAccounts(addresses, opts) {
|
|
6354
6451
|
const out = [];
|
|
6355
6452
|
for (let at = 0;at < addresses.length; at += 100) {
|
|
6356
6453
|
const chunk = addresses.slice(at, at + 100);
|
|
6357
|
-
const r = await
|
|
6358
|
-
chunk,
|
|
6359
|
-
{ encoding: "base64", commitment: "finalized" }
|
|
6360
|
-
]);
|
|
6454
|
+
const r = await read("getMultipleAccounts", [chunk, withMinContextSlot({ encoding: "base64", commitment: "finalized" }, opts)], opts);
|
|
6361
6455
|
if (!Array.isArray(r?.value) || r.value.length !== chunk.length) {
|
|
6362
6456
|
throw new Error("RPC getMultipleAccounts answered with the wrong number of accounts");
|
|
6363
6457
|
}
|
|
@@ -6366,17 +6460,17 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6366
6460
|
}
|
|
6367
6461
|
return out;
|
|
6368
6462
|
},
|
|
6369
|
-
async simulateTransaction(txBase64, addresses) {
|
|
6370
|
-
const r = await
|
|
6463
|
+
async simulateTransaction(txBase64, addresses, opts) {
|
|
6464
|
+
const r = await read("simulateTransaction", [
|
|
6371
6465
|
txBase64,
|
|
6372
|
-
{
|
|
6466
|
+
withMinContextSlot({
|
|
6373
6467
|
sigVerify: false,
|
|
6374
6468
|
replaceRecentBlockhash: true,
|
|
6375
6469
|
commitment: "finalized",
|
|
6376
6470
|
encoding: "base64",
|
|
6377
6471
|
accounts: { encoding: "base64", addresses }
|
|
6378
|
-
}
|
|
6379
|
-
]);
|
|
6472
|
+
}, opts)
|
|
6473
|
+
], opts);
|
|
6380
6474
|
const value = r?.value;
|
|
6381
6475
|
if (!value || typeof value !== "object")
|
|
6382
6476
|
throw new Error("RPC simulateTransaction answered without a value");
|
|
@@ -6400,22 +6494,26 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6400
6494
|
throw new SolanaRpcError("RPC getTokenSupply answered without a value");
|
|
6401
6495
|
return { decimals: r.value.decimals };
|
|
6402
6496
|
},
|
|
6403
|
-
async sendTransaction(txBase64) {
|
|
6497
|
+
async sendTransaction(txBase64, opts) {
|
|
6404
6498
|
return await call("sendTransaction", [
|
|
6405
6499
|
txBase64,
|
|
6406
|
-
{ encoding: "base64", skipPreflight: false, preflightCommitment: "finalized", maxRetries: 3 }
|
|
6500
|
+
withMinContextSlot({ encoding: "base64", skipPreflight: false, preflightCommitment: "finalized", maxRetries: 3 }, opts)
|
|
6407
6501
|
]);
|
|
6408
6502
|
},
|
|
6409
6503
|
async getSignatureStatus(signature) {
|
|
6410
6504
|
const r = await call("getSignatureStatuses", [[signature], { searchTransactionHistory: true }]);
|
|
6411
|
-
|
|
6505
|
+
const status = r.value[0] ?? null;
|
|
6506
|
+
if (status === null)
|
|
6507
|
+
return null;
|
|
6508
|
+
const { slot, ...rest } = status;
|
|
6509
|
+
return Number.isSafeInteger(slot) ? { ...rest, slot } : rest;
|
|
6412
6510
|
},
|
|
6413
6511
|
async hasSignatureHistory(address) {
|
|
6414
6512
|
const r = await call("getSignaturesForAddress", [address, { limit: 1 }]);
|
|
6415
6513
|
return Array.isArray(r) && r.length > 0;
|
|
6416
6514
|
},
|
|
6417
|
-
async isBlockhashValid(blockhash, commitment = "finalized") {
|
|
6418
|
-
const r = await
|
|
6515
|
+
async isBlockhashValid(blockhash, commitment = "finalized", opts) {
|
|
6516
|
+
const r = await read("isBlockhashValid", [blockhash, withMinContextSlot({ commitment }, opts)], opts);
|
|
6419
6517
|
if (typeof r?.value !== "boolean")
|
|
6420
6518
|
throw new Error("isBlockhashValid answered with a non-boolean value");
|
|
6421
6519
|
return r.value;
|
|
@@ -6463,7 +6561,73 @@ function createSolanaRpc(url, fetchFn, sleep) {
|
|
|
6463
6561
|
}
|
|
6464
6562
|
};
|
|
6465
6563
|
}
|
|
6466
|
-
|
|
6564
|
+
function withMinContextSlot(config, opts) {
|
|
6565
|
+
return opts?.minContextSlot === undefined ? config : { ...config, minContextSlot: opts.minContextSlot };
|
|
6566
|
+
}
|
|
6567
|
+
function contextSlotOf(result) {
|
|
6568
|
+
const context = result !== null && typeof result === "object" ? result.context : undefined;
|
|
6569
|
+
const slot = context !== null && typeof context === "object" ? context.slot : undefined;
|
|
6570
|
+
return Number.isSafeInteger(slot) ? slot : undefined;
|
|
6571
|
+
}
|
|
6572
|
+
function createContextFloor(seed) {
|
|
6573
|
+
let slot;
|
|
6574
|
+
const floor = {
|
|
6575
|
+
get slot() {
|
|
6576
|
+
return slot;
|
|
6577
|
+
},
|
|
6578
|
+
raise(observed) {
|
|
6579
|
+
if (!Number.isSafeInteger(observed) || observed < 0)
|
|
6580
|
+
return;
|
|
6581
|
+
if (slot === undefined || observed > slot)
|
|
6582
|
+
slot = observed;
|
|
6583
|
+
}
|
|
6584
|
+
};
|
|
6585
|
+
if (seed !== undefined)
|
|
6586
|
+
floor.raise(seed);
|
|
6587
|
+
return floor;
|
|
6588
|
+
}
|
|
6589
|
+
function withContextFloor(rpc, floor) {
|
|
6590
|
+
const required = (opts) => {
|
|
6591
|
+
const own = opts?.minContextSlot;
|
|
6592
|
+
return {
|
|
6593
|
+
get minContextSlot() {
|
|
6594
|
+
return floor.slot === undefined ? own : own === undefined ? floor.slot : Math.max(floor.slot, own);
|
|
6595
|
+
},
|
|
6596
|
+
...opts?.onContext !== undefined ? { onContext: opts.onContext } : {}
|
|
6597
|
+
};
|
|
6598
|
+
};
|
|
6599
|
+
const finalized = (opts) => {
|
|
6600
|
+
const base = required(opts);
|
|
6601
|
+
return {
|
|
6602
|
+
get minContextSlot() {
|
|
6603
|
+
return base.minContextSlot;
|
|
6604
|
+
},
|
|
6605
|
+
onContext: (slot) => {
|
|
6606
|
+
floor.raise(slot);
|
|
6607
|
+
opts?.onContext?.(slot);
|
|
6608
|
+
}
|
|
6609
|
+
};
|
|
6610
|
+
};
|
|
6611
|
+
return {
|
|
6612
|
+
getLatestBlockhash: (opts) => rpc.getLatestBlockhash(finalized(opts)),
|
|
6613
|
+
getBalance: (address, opts) => rpc.getBalance(address, finalized(opts)),
|
|
6614
|
+
getTokenAccountsByOwner: (owner, programId, opts) => rpc.getTokenAccountsByOwner(owner, programId, finalized(opts)),
|
|
6615
|
+
getAccountInfo: (address, opts) => rpc.getAccountInfo(address, finalized(opts)),
|
|
6616
|
+
getEpoch: (opts) => rpc.getEpoch(finalized(opts)),
|
|
6617
|
+
getMultipleAccounts: (addresses, opts) => rpc.getMultipleAccounts(addresses, finalized(opts)),
|
|
6618
|
+
simulateTransaction: (txBase64, addresses, opts) => rpc.simulateTransaction(txBase64, addresses, finalized(opts)),
|
|
6619
|
+
getFeeForMessage: (messageBase64, opts) => rpc.getFeeForMessage(messageBase64, finalized(opts)),
|
|
6620
|
+
sendTransaction: (txBase64, opts) => rpc.sendTransaction(txBase64, required(opts)),
|
|
6621
|
+
isBlockhashValid: (blockhash, commitment, opts) => rpc.isBlockhashValid(blockhash, commitment, commitment === undefined || commitment === "finalized" ? finalized(opts) : required(opts)),
|
|
6622
|
+
getSignatureStatus: (signature) => rpc.getSignatureStatus(signature),
|
|
6623
|
+
getMinimumBalanceForRentExemption: (size) => rpc.getMinimumBalanceForRentExemption(size),
|
|
6624
|
+
getTokenSupply: (mint) => rpc.getTokenSupply(mint),
|
|
6625
|
+
hasSignatureHistory: (address) => rpc.hasSignatureHistory(address),
|
|
6626
|
+
getProgramAccounts: (...args) => Reflect.apply(rpc.getProgramAccounts, rpc, args),
|
|
6627
|
+
getProgramAccountsV2: (...args) => Reflect.apply(rpc.getProgramAccountsV2, rpc, args)
|
|
6628
|
+
};
|
|
6629
|
+
}
|
|
6630
|
+
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111", TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", PDA_MARKER, PROGRAM_ACCOUNTS_V2_LIMIT = 1000, SolanaRpcError, RPC_PREFLIGHT_FAILED_CODE = -32002, RPC_MIN_CONTEXT_SLOT_CODE = -32016, RPC_CONTEXT_BEHIND_SLEEPS_MS, DIAGNOSTIC_LOG_LINES = 20, DIAGNOSTIC_LOG_LINE_CHARS = 240, DIAGNOSTIC_MESSAGE_CHARS = 300, DIAGNOSTIC_ERR_CHARS = 512, RPC_RATE_LIMIT_CODE = -32429, RPC_RETRY_DEFAULT_MS = 2000, RPC_RETRY_CAP_MS = 1e4, NEVER_RETRIED;
|
|
6467
6631
|
var init_solana_lite = __esm(() => {
|
|
6468
6632
|
init_ed25519();
|
|
6469
6633
|
init_sha256();
|
|
@@ -6474,6 +6638,12 @@ var init_solana_lite = __esm(() => {
|
|
|
6474
6638
|
retryAfterMs;
|
|
6475
6639
|
rpcCode;
|
|
6476
6640
|
rateLimited;
|
|
6641
|
+
method;
|
|
6642
|
+
responseIdMatched;
|
|
6643
|
+
simulation;
|
|
6644
|
+
contextSlot;
|
|
6645
|
+
contextBehind;
|
|
6646
|
+
contextUnverified;
|
|
6477
6647
|
constructor(message, facts = {}) {
|
|
6478
6648
|
super(message);
|
|
6479
6649
|
this.name = "SolanaRpcError";
|
|
@@ -6481,8 +6651,21 @@ var init_solana_lite = __esm(() => {
|
|
|
6481
6651
|
this.retryAfterMs = facts.retryAfterMs;
|
|
6482
6652
|
this.rpcCode = facts.rpcCode;
|
|
6483
6653
|
this.rateLimited = facts.status === 429 || facts.rpcCode === RPC_RATE_LIMIT_CODE || /too many requests/i.test(message);
|
|
6484
|
-
|
|
6485
|
-
|
|
6654
|
+
if (facts.method !== undefined)
|
|
6655
|
+
this.method = facts.method;
|
|
6656
|
+
if (facts.responseIdMatched !== undefined)
|
|
6657
|
+
this.responseIdMatched = facts.responseIdMatched;
|
|
6658
|
+
if (facts.simulation !== undefined)
|
|
6659
|
+
this.simulation = facts.simulation;
|
|
6660
|
+
if (facts.contextSlot !== undefined)
|
|
6661
|
+
this.contextSlot = facts.contextSlot;
|
|
6662
|
+
if (facts.contextBehind !== undefined)
|
|
6663
|
+
this.contextBehind = facts.contextBehind;
|
|
6664
|
+
if (facts.contextUnverified)
|
|
6665
|
+
this.contextUnverified = true;
|
|
6666
|
+
}
|
|
6667
|
+
};
|
|
6668
|
+
RPC_CONTEXT_BEHIND_SLEEPS_MS = [1000, 2000];
|
|
6486
6669
|
NEVER_RETRIED = new Set(["sendTransaction", "getProgramAccounts", "getProgramAccountsV2"]);
|
|
6487
6670
|
});
|
|
6488
6671
|
|
|
@@ -15462,7 +15645,9 @@ function classifyStatus(status) {
|
|
|
15462
15645
|
if (status === null || status === undefined)
|
|
15463
15646
|
return { kind: "missing" };
|
|
15464
15647
|
if (status.confirmationStatus === "finalized") {
|
|
15465
|
-
|
|
15648
|
+
if (status.err !== null && status.err !== undefined)
|
|
15649
|
+
return { kind: "failed", err: status.err };
|
|
15650
|
+
return Number.isSafeInteger(status.slot) ? { kind: "finalized", slot: status.slot } : { kind: "finalized" };
|
|
15466
15651
|
}
|
|
15467
15652
|
return { kind: "nonfinal", confirmationStatus: status.confirmationStatus, err: status.err };
|
|
15468
15653
|
}
|
|
@@ -15511,8 +15696,9 @@ async function resolvePending(reads, pending) {
|
|
|
15511
15696
|
return { kind: "expired", detail: `never observed and its blockhash is no longer valid: it cannot land` };
|
|
15512
15697
|
}
|
|
15513
15698
|
function settle(observation, signature) {
|
|
15514
|
-
if (observation.kind === "finalized")
|
|
15515
|
-
return { kind: "finalized" };
|
|
15699
|
+
if (observation.kind === "finalized") {
|
|
15700
|
+
return observation.slot === undefined ? { kind: "finalized" } : { kind: "finalized", slot: observation.slot };
|
|
15701
|
+
}
|
|
15516
15702
|
if (observation.kind === "failed") {
|
|
15517
15703
|
return { kind: "failed", detail: `transaction ${signature} failed on chain: ${JSON.stringify(observation.err)}` };
|
|
15518
15704
|
}
|
|
@@ -33127,12 +33313,12 @@ function refusalState(raw) {
|
|
|
33127
33313
|
const state = error.state;
|
|
33128
33314
|
return state === "disable-pending" || state === "enabled" ? state : null;
|
|
33129
33315
|
}
|
|
33130
|
-
async function broadcastAndFinalize(rpc, ctx, secret, feePayer, instructions, pending, recordPending, clearPending) {
|
|
33316
|
+
async function broadcastAndFinalize(rpc, ctx, secret, feePayer, instructions, pending, recordPending, clearPending, floor) {
|
|
33131
33317
|
const blockhash = await rpc.getLatestBlockhash();
|
|
33132
33318
|
const message = compileLegacyMessage({ feePayer, recentBlockhash: blockhash, instructions });
|
|
33133
|
-
return broadcastMessage(rpc, ctx, secret, message, blockhash, pending, recordPending, clearPending);
|
|
33319
|
+
return broadcastMessage(rpc, ctx, secret, message, blockhash, pending, recordPending, clearPending, floor);
|
|
33134
33320
|
}
|
|
33135
|
-
async function broadcastMessage(rpc, ctx, secret, message, blockhash, pending, recordPending, clearPending) {
|
|
33321
|
+
async function broadcastMessage(rpc, ctx, secret, message, blockhash, pending, recordPending, clearPending, floor) {
|
|
33136
33322
|
const { deps } = ctx;
|
|
33137
33323
|
const signatureBytes = signMessage(message, secret);
|
|
33138
33324
|
const signature = base58.encode(signatureBytes);
|
|
@@ -33147,24 +33333,52 @@ async function broadcastMessage(rpc, ctx, secret, message, blockhash, pending, r
|
|
|
33147
33333
|
return { status: "not-sent", error: "could not record the pending transaction locally; not broadcast" };
|
|
33148
33334
|
}
|
|
33149
33335
|
let echoNote = "";
|
|
33336
|
+
let guardedStatus;
|
|
33150
33337
|
try {
|
|
33151
33338
|
const echoed = await rpc.sendTransaction(toBase642(wire));
|
|
33152
33339
|
if (echoed !== signature) {
|
|
33153
33340
|
echoNote = `; the RPC echoed a different signature (${echoed}), which was ignored`;
|
|
33154
33341
|
}
|
|
33155
33342
|
} catch (error) {
|
|
33156
|
-
|
|
33157
|
-
|
|
33158
|
-
|
|
33159
|
-
|
|
33160
|
-
|
|
33161
|
-
|
|
33162
|
-
|
|
33343
|
+
const rejection = definiteSendRejection(error);
|
|
33344
|
+
if (rejection) {
|
|
33345
|
+
const detail = rejection.reason === "context-behind" ? `rejected before simulation (not broadcast): the RPC node is behind slot ${floor.slot ?? "required by this run"} (it is at ${rejection.contextSlot})` : `rejected at simulation (not broadcast): RPC ${rejection.rpcCode}: ${rejection.diagnostics?.message ?? "preflight failed"}`;
|
|
33346
|
+
try {
|
|
33347
|
+
guardedStatus = await rpc.getSignatureStatus(signature);
|
|
33348
|
+
} catch (guardError) {
|
|
33349
|
+
if (isRateLimited(guardError))
|
|
33350
|
+
notePostSignatureRateLimit(ctx, signature);
|
|
33351
|
+
return {
|
|
33352
|
+
status: "uncertain",
|
|
33353
|
+
signature,
|
|
33354
|
+
error: `${detail}; status guard failed (${sanitizeSimulation(describeRpcFailure(guardError), {}).message}); ${signature} may still land`,
|
|
33355
|
+
...rejection.diagnostics ? { diagnostics: rejection.diagnostics } : {}
|
|
33356
|
+
};
|
|
33357
|
+
}
|
|
33358
|
+
if (guardedStatus === null) {
|
|
33359
|
+
return {
|
|
33360
|
+
...rejection,
|
|
33361
|
+
status: "rejected",
|
|
33362
|
+
signature,
|
|
33363
|
+
error: detail,
|
|
33364
|
+
pendingCleared: await clearPending(signature)
|
|
33365
|
+
};
|
|
33366
|
+
}
|
|
33367
|
+
} else {
|
|
33368
|
+
if (isRateLimited(error))
|
|
33369
|
+
notePostSignatureRateLimit(ctx, signature);
|
|
33370
|
+
return {
|
|
33371
|
+
status: "uncertain",
|
|
33372
|
+
signature,
|
|
33373
|
+
error: `send did not answer cleanly (${describeRpcFailure(error)}); it may still land`
|
|
33374
|
+
};
|
|
33375
|
+
}
|
|
33163
33376
|
}
|
|
33164
33377
|
for (let i = 0;i < CONFIRM_MAX_POLLS; i++) {
|
|
33165
33378
|
let status;
|
|
33166
33379
|
try {
|
|
33167
|
-
status = await rpc.getSignatureStatus(signature);
|
|
33380
|
+
status = guardedStatus ?? await rpc.getSignatureStatus(signature);
|
|
33381
|
+
guardedStatus = undefined;
|
|
33168
33382
|
} catch (error) {
|
|
33169
33383
|
if (isRateLimited(error))
|
|
33170
33384
|
notePostSignatureRateLimit(ctx, signature);
|
|
@@ -33175,13 +33389,15 @@ async function broadcastMessage(rpc, ctx, secret, message, blockhash, pending, r
|
|
|
33175
33389
|
};
|
|
33176
33390
|
}
|
|
33177
33391
|
const observed = classifyStatus(status);
|
|
33392
|
+
const slot = status?.slot === undefined ? {} : { slot: status.slot };
|
|
33178
33393
|
if (observed.kind === "finalized")
|
|
33179
|
-
return { status: "finalized", signature };
|
|
33394
|
+
return { status: "finalized", signature, ...slot };
|
|
33180
33395
|
if (observed.kind === "failed") {
|
|
33181
33396
|
await clearPending(signature);
|
|
33182
33397
|
return {
|
|
33183
33398
|
status: "failed",
|
|
33184
33399
|
signature,
|
|
33400
|
+
...slot,
|
|
33185
33401
|
error: `transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}`
|
|
33186
33402
|
};
|
|
33187
33403
|
}
|
|
@@ -33326,7 +33542,27 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33326
33542
|
return 1;
|
|
33327
33543
|
}
|
|
33328
33544
|
const vaultKey = decodePubkey(vault);
|
|
33329
|
-
const
|
|
33545
|
+
const floor = createContextFloor();
|
|
33546
|
+
for (const receipt of entry.tee?.sweepReceipts ?? [])
|
|
33547
|
+
if (receipt.slot !== undefined)
|
|
33548
|
+
floor.raise(receipt.slot);
|
|
33549
|
+
const isContextFailure = (error) => error instanceof SolanaRpcError && Boolean(error.contextBehind || error.contextUnverified || floor.slot !== undefined && error.rpcCode === -32602);
|
|
33550
|
+
let contextFailure;
|
|
33551
|
+
const floored = withContextFloor(solana.rpc, floor);
|
|
33552
|
+
const rpc = new Proxy(floored, {
|
|
33553
|
+
get(target, key) {
|
|
33554
|
+
const method = target[key];
|
|
33555
|
+
return async (...args2) => {
|
|
33556
|
+
try {
|
|
33557
|
+
return await method(...args2);
|
|
33558
|
+
} catch (error) {
|
|
33559
|
+
if (isContextFailure(error))
|
|
33560
|
+
contextFailure = error;
|
|
33561
|
+
throw error;
|
|
33562
|
+
}
|
|
33563
|
+
};
|
|
33564
|
+
}
|
|
33565
|
+
});
|
|
33330
33566
|
const retained = entry.tee?.sweepReceipts ?? [];
|
|
33331
33567
|
const receipts = [];
|
|
33332
33568
|
const residuals = [];
|
|
@@ -33334,6 +33570,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33334
33570
|
const alreadyRecordedLocally = entry.tee?.sweptAt !== undefined;
|
|
33335
33571
|
const pendingStill = [];
|
|
33336
33572
|
const retainReceipt = async (receipt) => {
|
|
33573
|
+
if (receipt.slot !== undefined)
|
|
33574
|
+
floor.raise(receipt.slot);
|
|
33337
33575
|
receipts.push(receipt);
|
|
33338
33576
|
const kept = await commitActiveTee(active, (target) => {
|
|
33339
33577
|
const existing = target.tee?.sweepReceipts ?? [];
|
|
@@ -33361,6 +33599,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33361
33599
|
residuals.push({ kind: "local-record-failed", detail: kept.message });
|
|
33362
33600
|
return kept.ok;
|
|
33363
33601
|
};
|
|
33602
|
+
const cleanupFailures = new Map;
|
|
33364
33603
|
const clearPending = async (signature) => {
|
|
33365
33604
|
const kept = await commitActiveTee(active, (target) => {
|
|
33366
33605
|
target.tee = {
|
|
@@ -33368,12 +33607,58 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33368
33607
|
sweepPending: (target.tee?.sweepPending ?? []).filter((p) => p.signature !== signature)
|
|
33369
33608
|
};
|
|
33370
33609
|
});
|
|
33371
|
-
if (!kept.ok)
|
|
33610
|
+
if (!kept.ok) {
|
|
33372
33611
|
residuals.push({ kind: "local-record-failed", detail: kept.message });
|
|
33612
|
+
cleanupFailures.set(signature, sanitizeSimulation(kept.message, {}).message);
|
|
33613
|
+
}
|
|
33614
|
+
return kept.ok;
|
|
33615
|
+
};
|
|
33616
|
+
const attemptedAccounts = new Set;
|
|
33617
|
+
const broadcast2 = (instructions, pending) => {
|
|
33618
|
+
if (pending.account)
|
|
33619
|
+
attemptedAccounts.add(pending.account);
|
|
33620
|
+
return broadcastAndFinalize(rpc, ctx, secret, teePubkey, instructions, pending, recordPending, clearPending, floor);
|
|
33373
33621
|
};
|
|
33374
|
-
|
|
33622
|
+
let rejectionCount = 0;
|
|
33623
|
+
let blockingReason = "not signed: an earlier transaction is still in flight";
|
|
33375
33624
|
const settle2 = (outcome, pending, describe2, failedKind) => {
|
|
33625
|
+
if (outcome.status === "rejected") {
|
|
33626
|
+
rejectionCount++;
|
|
33627
|
+
const detail = `${outcome.error}; ${pending.kind === "sol" ? "SOL transfer" : pending.kind} of ${pending.amountRaw} ${pending.kind === "sol" ? "lamports" : "raw"}, signed as ${outcome.signature}`;
|
|
33628
|
+
residuals.push({
|
|
33629
|
+
...pending,
|
|
33630
|
+
kind: failedKind ?? `${describe2}-rejected`,
|
|
33631
|
+
detail,
|
|
33632
|
+
signature: outcome.signature,
|
|
33633
|
+
broadcast: false,
|
|
33634
|
+
rejection: { reason: outcome.reason, rpcCode: outcome.rpcCode, pendingCleared: outcome.pendingCleared },
|
|
33635
|
+
...outcome.diagnostics ? { diagnostics: outcome.diagnostics } : {}
|
|
33636
|
+
});
|
|
33637
|
+
if (!json) {
|
|
33638
|
+
deps.stdout.write(` ${detail}
|
|
33639
|
+
`);
|
|
33640
|
+
if (outcome.diagnostics) {
|
|
33641
|
+
if (outcome.diagnostics.err !== null)
|
|
33642
|
+
deps.stdout.write(` program error: ${outcome.diagnostics.err}
|
|
33643
|
+
`);
|
|
33644
|
+
for (const line of outcome.diagnostics.logs)
|
|
33645
|
+
deps.stdout.write(` log: ${line}
|
|
33646
|
+
`);
|
|
33647
|
+
}
|
|
33648
|
+
}
|
|
33649
|
+
if (!outcome.pendingCleared) {
|
|
33650
|
+
blockingReason = "not signed: a rejected transaction's local pending record could not be cleared";
|
|
33651
|
+
return false;
|
|
33652
|
+
}
|
|
33653
|
+
if (rejectionCount >= 3) {
|
|
33654
|
+
blockingReason = "not signed: 3 transactions were rejected at simulation in this run; re-run after checking the diagnostics";
|
|
33655
|
+
return false;
|
|
33656
|
+
}
|
|
33657
|
+
return true;
|
|
33658
|
+
}
|
|
33376
33659
|
if (outcome.status === "failed") {
|
|
33660
|
+
if (outcome.slot !== undefined)
|
|
33661
|
+
floor.raise(outcome.slot);
|
|
33377
33662
|
residuals.push({
|
|
33378
33663
|
kind: failedKind ?? `${describe2}-failed`,
|
|
33379
33664
|
detail: outcome.error,
|
|
@@ -33384,19 +33669,46 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33384
33669
|
return true;
|
|
33385
33670
|
}
|
|
33386
33671
|
if (outcome.status === "not-sent") {
|
|
33387
|
-
residuals.push({ kind: `${describe2}-not-sent`, detail: outcome.error });
|
|
33672
|
+
residuals.push({ ...pending, kind: `${describe2}-not-sent`, detail: outcome.error });
|
|
33673
|
+
blockingReason = "not signed: the local pending transaction could not be recorded";
|
|
33388
33674
|
return false;
|
|
33389
33675
|
}
|
|
33390
33676
|
residuals.push({
|
|
33391
33677
|
kind: "finality-uncertain",
|
|
33392
33678
|
detail: outcome.error,
|
|
33679
|
+
...pending.account ? { account: pending.account } : {},
|
|
33680
|
+
...outcome.diagnostics ? { diagnostics: outcome.diagnostics } : {},
|
|
33393
33681
|
...pending.mint ? { mint: pending.mint } : {},
|
|
33394
33682
|
amountRaw: pending.amountRaw
|
|
33395
33683
|
});
|
|
33684
|
+
blockingReason = "not signed: an earlier transaction is still in flight";
|
|
33396
33685
|
pendingStill.push({ ...pending, signature: outcome.signature, blockhash: "", submittedAt: "" });
|
|
33397
33686
|
return false;
|
|
33398
33687
|
};
|
|
33399
33688
|
let signingBlocked = false;
|
|
33689
|
+
const notAttempted = (acct) => {
|
|
33690
|
+
if (acct && (attemptedAccounts.has(acct.pubkey) || residuals.some((r) => r.account === acct.pubkey)))
|
|
33691
|
+
return;
|
|
33692
|
+
residuals.push({
|
|
33693
|
+
kind: "not-attempted",
|
|
33694
|
+
detail: `${acct ? "account" : "SOL transfer"} ${blockingReason}`,
|
|
33695
|
+
...acct ? { account: acct.pubkey, mint: acct.mint, amountRaw: acct.amountRaw } : {}
|
|
33696
|
+
});
|
|
33697
|
+
if (!acct)
|
|
33698
|
+
solHandledAsResidual = true;
|
|
33699
|
+
};
|
|
33700
|
+
const stopForContext = (error, acct) => {
|
|
33701
|
+
if (!isContextFailure(error))
|
|
33702
|
+
return false;
|
|
33703
|
+
blockingReason = "not signed: an RPC context failure stopped signing in this run";
|
|
33704
|
+
signingBlocked = true;
|
|
33705
|
+
residuals.push({
|
|
33706
|
+
kind: error.contextBehind ? "rpc-context-behind" : "rpc-context-unverified",
|
|
33707
|
+
detail: `Nothing was signed for ${acct ? "this account" : "this step"}: RPC ${solana.endpoint.host} ${sanitizeSimulation(describeRpcFailure(error), {}).message}. Fix: ${rpcFixLines(ctx).join(", ")}`,
|
|
33708
|
+
...acct ? { account: acct.pubkey, mint: acct.mint, amountRaw: acct.amountRaw } : {}
|
|
33709
|
+
});
|
|
33710
|
+
return true;
|
|
33711
|
+
};
|
|
33400
33712
|
const reads = {
|
|
33401
33713
|
status: (signature) => rpc.getSignatureStatus(signature),
|
|
33402
33714
|
blockhashValid: (blockhash) => rpc.isBlockhashValid(blockhash)
|
|
@@ -33409,6 +33721,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33409
33721
|
...p.mint ? { mint: p.mint } : {},
|
|
33410
33722
|
amountRaw: p.amountRaw,
|
|
33411
33723
|
signature: p.signature,
|
|
33724
|
+
...resolution.slot === undefined ? {} : { slot: resolution.slot },
|
|
33412
33725
|
finalizedAt: new Date(deps.now()).toISOString()
|
|
33413
33726
|
});
|
|
33414
33727
|
if (!json)
|
|
@@ -33417,7 +33730,10 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33417
33730
|
continue;
|
|
33418
33731
|
}
|
|
33419
33732
|
if (resolution.kind === "failed" || resolution.kind === "expired") {
|
|
33420
|
-
await clearPending(p.signature)
|
|
33733
|
+
if (!await clearPending(p.signature)) {
|
|
33734
|
+
signingBlocked = true;
|
|
33735
|
+
blockingReason = "not signed: an earlier pending record could not be cleared";
|
|
33736
|
+
}
|
|
33421
33737
|
if (!json)
|
|
33422
33738
|
deps.stdout.write(` pending ${p.signature}: ${resolution.detail}; its balance is swept again
|
|
33423
33739
|
`);
|
|
@@ -33433,17 +33749,21 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33433
33749
|
signingBlocked = true;
|
|
33434
33750
|
}
|
|
33435
33751
|
const candidateAccounts = new Set;
|
|
33752
|
+
const knownAccounts = new Map;
|
|
33436
33753
|
const lpCloses = [];
|
|
33437
33754
|
{
|
|
33438
33755
|
let token2022Accounts = [];
|
|
33439
33756
|
try {
|
|
33440
33757
|
token2022Accounts = await rpc.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID);
|
|
33441
33758
|
} catch (error) {
|
|
33442
|
-
|
|
33443
|
-
|
|
33444
|
-
|
|
33445
|
-
|
|
33759
|
+
if (!stopForContext(error))
|
|
33760
|
+
residuals.push({
|
|
33761
|
+
kind: "inventory",
|
|
33762
|
+
detail: `could not list Token-2022 accounts for position discovery: ${describeRpcFailure(error)}`
|
|
33763
|
+
});
|
|
33446
33764
|
}
|
|
33765
|
+
for (const acct of token2022Accounts)
|
|
33766
|
+
knownAccounts.set(acct.pubkey, acct);
|
|
33447
33767
|
const candidates = token2022Accounts.filter(isPositionCandidate);
|
|
33448
33768
|
for (const acct of candidates)
|
|
33449
33769
|
candidateAccounts.add(acct.pubkey);
|
|
@@ -33454,8 +33774,10 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33454
33774
|
`);
|
|
33455
33775
|
const verified = [];
|
|
33456
33776
|
for (const acct of candidates) {
|
|
33457
|
-
if (signingBlocked)
|
|
33458
|
-
|
|
33777
|
+
if (signingBlocked) {
|
|
33778
|
+
notAttempted(acct);
|
|
33779
|
+
continue;
|
|
33780
|
+
}
|
|
33459
33781
|
if (!apiKey || !entry.linkedWalletId) {
|
|
33460
33782
|
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "no bound API key is available to build the close; the NFT was left in place");
|
|
33461
33783
|
continue;
|
|
@@ -33486,6 +33808,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33486
33808
|
nftMint: acct.mint,
|
|
33487
33809
|
nftAccount: acct.pubkey
|
|
33488
33810
|
});
|
|
33811
|
+
if (contextFailure && stopForContext(contextFailure, acct))
|
|
33812
|
+
continue;
|
|
33489
33813
|
if (!verdict.ok) {
|
|
33490
33814
|
leftover(DAMM_CLOSE_TRANSACTION_REFUSED, acct, `${verdict.reason}; refused before signing`);
|
|
33491
33815
|
continue;
|
|
@@ -33494,13 +33818,15 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33494
33818
|
}
|
|
33495
33819
|
for (const { acct, verdict } of verified) {
|
|
33496
33820
|
if (signingBlocked) {
|
|
33497
|
-
|
|
33821
|
+
notAttempted(acct);
|
|
33498
33822
|
continue;
|
|
33499
33823
|
}
|
|
33500
33824
|
let blockhashValid;
|
|
33501
33825
|
try {
|
|
33502
33826
|
blockhashValid = await rpc.isBlockhashValid(verdict.tx.message.recentBlockhash, "confirmed");
|
|
33503
33827
|
} catch (error) {
|
|
33828
|
+
if (stopForContext(error, acct))
|
|
33829
|
+
continue;
|
|
33504
33830
|
leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `could not check the close build's blockhash: ${describeRpcFailure(error)}; not signed`);
|
|
33505
33831
|
continue;
|
|
33506
33832
|
}
|
|
@@ -33523,7 +33849,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33523
33849
|
account: acct.pubkey,
|
|
33524
33850
|
amountRaw: acct.amountRaw
|
|
33525
33851
|
};
|
|
33526
|
-
|
|
33852
|
+
attemptedAccounts.add(acct.pubkey);
|
|
33853
|
+
const outcome = await broadcastMessage(rpc, ctx, secret, verdict.tx.message.bytes, verdict.tx.message.recentBlockhash, pending, recordPending, clearPending, floor);
|
|
33527
33854
|
if (outcome.status !== "finalized") {
|
|
33528
33855
|
if (!settle2(outcome, pending, "lp-close"))
|
|
33529
33856
|
signingBlocked = true;
|
|
@@ -33534,6 +33861,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33534
33861
|
mint: acct.mint,
|
|
33535
33862
|
amountRaw: acct.amountRaw,
|
|
33536
33863
|
signature: outcome.signature,
|
|
33864
|
+
...outcome.slot === undefined ? {} : { slot: outcome.slot },
|
|
33537
33865
|
finalizedAt: new Date(deps.now()).toISOString()
|
|
33538
33866
|
});
|
|
33539
33867
|
if (!json)
|
|
@@ -33547,16 +33875,21 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33547
33875
|
try {
|
|
33548
33876
|
tokenAccounts.push(...await rpc.getTokenAccountsByOwner(address, programId));
|
|
33549
33877
|
} catch (error) {
|
|
33550
|
-
|
|
33551
|
-
|
|
33552
|
-
|
|
33553
|
-
|
|
33878
|
+
if (!stopForContext(error))
|
|
33879
|
+
residuals.push({
|
|
33880
|
+
kind: "inventory",
|
|
33881
|
+
detail: `could not list ${programId === TOKEN_PROGRAM_ID ? "token" : "Token-2022"} accounts: ${describeRpcFailure(error)}`
|
|
33882
|
+
});
|
|
33554
33883
|
}
|
|
33555
33884
|
}
|
|
33885
|
+
for (const acct of tokenAccounts)
|
|
33886
|
+
knownAccounts.set(acct.pubkey, acct);
|
|
33556
33887
|
let epoch;
|
|
33557
33888
|
for (const acct of tokenAccounts) {
|
|
33558
|
-
if (signingBlocked)
|
|
33559
|
-
|
|
33889
|
+
if (signingBlocked) {
|
|
33890
|
+
notAttempted(acct);
|
|
33891
|
+
continue;
|
|
33892
|
+
}
|
|
33560
33893
|
const token2022 = acct.programId === TOKEN_2022_PROGRAM_ID;
|
|
33561
33894
|
if (candidateAccounts.has(acct.pubkey)) {
|
|
33562
33895
|
if (!emergency)
|
|
@@ -33596,6 +33929,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33596
33929
|
owner: teePubkey,
|
|
33597
33930
|
amount
|
|
33598
33931
|
});
|
|
33932
|
+
if (contextFailure)
|
|
33933
|
+
throw contextFailure;
|
|
33599
33934
|
if (hook.ok)
|
|
33600
33935
|
extraAccounts = hook.accounts;
|
|
33601
33936
|
else {
|
|
@@ -33646,7 +33981,6 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33646
33981
|
const named = profile ? classifyTokenSendFailure({ profile, frozen: destinationFrozen, extraAccountsMissing }) : undefined;
|
|
33647
33982
|
if (!settle2(outcome, pending, "token-transfer", named)) {
|
|
33648
33983
|
signingBlocked = true;
|
|
33649
|
-
break;
|
|
33650
33984
|
}
|
|
33651
33985
|
continue;
|
|
33652
33986
|
}
|
|
@@ -33656,12 +33990,15 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33656
33990
|
mint: acct.mint,
|
|
33657
33991
|
amountRaw: acct.amountRaw,
|
|
33658
33992
|
signature,
|
|
33993
|
+
...outcome.slot === undefined ? {} : { slot: outcome.slot },
|
|
33659
33994
|
finalizedAt: new Date(deps.now()).toISOString()
|
|
33660
33995
|
});
|
|
33661
33996
|
if (!json)
|
|
33662
33997
|
deps.stdout.write(` moved ${acct.amountRaw} raw of ${acct.mint} and closed its account: ${signature}
|
|
33663
33998
|
`);
|
|
33664
33999
|
} catch (error) {
|
|
34000
|
+
if (stopForContext(error, acct))
|
|
34001
|
+
continue;
|
|
33665
34002
|
residuals.push({
|
|
33666
34003
|
kind: "token-transfer-failed",
|
|
33667
34004
|
detail: describeRpcFailure(error),
|
|
@@ -33671,6 +34008,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33671
34008
|
});
|
|
33672
34009
|
}
|
|
33673
34010
|
}
|
|
34011
|
+
if (signingBlocked)
|
|
34012
|
+
notAttempted();
|
|
33674
34013
|
try {
|
|
33675
34014
|
const balance = signingBlocked ? 0n : await rpc.getBalance(address);
|
|
33676
34015
|
if (balance > 0n) {
|
|
@@ -33709,6 +34048,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33709
34048
|
kind: "sol",
|
|
33710
34049
|
amountRaw: amount.toString(),
|
|
33711
34050
|
signature,
|
|
34051
|
+
...outcome.slot === undefined ? {} : { slot: outcome.slot },
|
|
33712
34052
|
finalizedAt: new Date(deps.now()).toISOString()
|
|
33713
34053
|
});
|
|
33714
34054
|
if (!json)
|
|
@@ -33719,8 +34059,12 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33719
34059
|
}
|
|
33720
34060
|
} catch (error) {
|
|
33721
34061
|
solHandledAsResidual = true;
|
|
33722
|
-
|
|
34062
|
+
if (!stopForContext(error))
|
|
34063
|
+
residuals.push({ kind: "sol-transfer-failed", detail: describeRpcFailure(error) });
|
|
33723
34064
|
}
|
|
34065
|
+
if (signingBlocked)
|
|
34066
|
+
for (const acct of knownAccounts.values())
|
|
34067
|
+
notAttempted(acct);
|
|
33724
34068
|
const inventory = { observedAt: new Date(deps.now()).toISOString(), verified: false, lamports: null, tokenAccounts: null };
|
|
33725
34069
|
try {
|
|
33726
34070
|
const listed = new Set(residuals.map((r) => r.account).filter((a) => a !== undefined));
|
|
@@ -33819,6 +34163,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33819
34163
|
signature: p.signature
|
|
33820
34164
|
})),
|
|
33821
34165
|
inventory,
|
|
34166
|
+
contextFloorSlot: floor.slot ?? null,
|
|
33822
34167
|
recordedOnServer,
|
|
33823
34168
|
...lpCloses.length > 0 ? { lpCloses } : {}
|
|
33824
34169
|
})}
|
|
@@ -33845,6 +34190,18 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
|
|
|
33845
34190
|
`);
|
|
33846
34191
|
if (pendingStill.length > 0)
|
|
33847
34192
|
deps.stdout.write(`${pendingStill.length} transaction(s) still in flight (${pendingStill.map((p) => p.signature).join(", ")}). Re-run this sweep: a finalized one becomes a receipt, an expired one is swept again.
|
|
34193
|
+
`);
|
|
34194
|
+
const rejected = residuals.filter((r) => r.rejection);
|
|
34195
|
+
if (pendingStill.length === 0 && rejected.length > 0 && rejected.every((r) => r.rejection?.pendingCleared)) {
|
|
34196
|
+
deps.stdout.write(`Nothing from this run is in flight: ${rejected.length} transaction(s) were rejected before broadcast. Re-run this sweep now.
|
|
34197
|
+
`);
|
|
34198
|
+
if (rejected.some((r) => r.rejection?.reason === "context-behind"))
|
|
34199
|
+
deps.stdout.write(`Fix: ${rpcFixLines(ctx).join(", ")}
|
|
34200
|
+
`);
|
|
34201
|
+
}
|
|
34202
|
+
for (const r of rejected)
|
|
34203
|
+
if (!r.rejection?.pendingCleared)
|
|
34204
|
+
deps.stdout.write(`The local pending record for ${r.signature} could not be removed (${cleanupFailures.get(r.signature ?? "")}). It was never broadcast; the next run drops it once its blockhash expires, normally within two minutes. Re-run after that.
|
|
33848
34205
|
`);
|
|
33849
34206
|
if (serverState === "disable-pending" && !emergency)
|
|
33850
34207
|
deps.stdout.write(`Remote signing authority is not verified denied. Re-run: candle tee disable ${address}, then re-run this sweep to record it.
|
|
@@ -49488,7 +49845,7 @@ var init_convert = __esm(() => {
|
|
|
49488
49845
|
});
|
|
49489
49846
|
|
|
49490
49847
|
// ../mcp/src/version.ts
|
|
49491
|
-
var SERVER_VERSION = "0.10.
|
|
49848
|
+
var SERVER_VERSION = "0.10.5";
|
|
49492
49849
|
|
|
49493
49850
|
// ../mcp/src/update-notice.ts
|
|
49494
49851
|
function newer(a, b) {
|
|
@@ -49940,6 +50297,630 @@ var init_orchestrate = __esm(() => {
|
|
|
49940
50297
|
};
|
|
49941
50298
|
});
|
|
49942
50299
|
|
|
50300
|
+
// ../mcp/src/hyperliquid.ts
|
|
50301
|
+
function pushUint2(out, value, width) {
|
|
50302
|
+
for (let i = width - 1;i >= 0; i--)
|
|
50303
|
+
out.push(Number(value >> BigInt(i * 8) & 0xffn));
|
|
50304
|
+
}
|
|
50305
|
+
function packInteger2(out, value) {
|
|
50306
|
+
if (value >= 0n) {
|
|
50307
|
+
if (value > UINT64_MAX2)
|
|
50308
|
+
throw new Error("msgpack: integer above uint64");
|
|
50309
|
+
if (value < 0x80n)
|
|
50310
|
+
out.push(Number(value));
|
|
50311
|
+
else if (value <= 0xffn) {
|
|
50312
|
+
out.push(204);
|
|
50313
|
+
pushUint2(out, value, 1);
|
|
50314
|
+
} else if (value <= 0xffffn) {
|
|
50315
|
+
out.push(205);
|
|
50316
|
+
pushUint2(out, value, 2);
|
|
50317
|
+
} else if (value <= 0xffffffffn) {
|
|
50318
|
+
out.push(206);
|
|
50319
|
+
pushUint2(out, value, 4);
|
|
50320
|
+
} else {
|
|
50321
|
+
out.push(207);
|
|
50322
|
+
pushUint2(out, value, 8);
|
|
50323
|
+
}
|
|
50324
|
+
return;
|
|
50325
|
+
}
|
|
50326
|
+
if (value < INT64_MIN2)
|
|
50327
|
+
throw new Error("msgpack: integer below int64");
|
|
50328
|
+
if (value >= -32n)
|
|
50329
|
+
out.push(Number(value & 0xffn));
|
|
50330
|
+
else if (value >= -128n) {
|
|
50331
|
+
out.push(208);
|
|
50332
|
+
pushUint2(out, value & 0xffn, 1);
|
|
50333
|
+
} else if (value >= -32768n) {
|
|
50334
|
+
out.push(209);
|
|
50335
|
+
pushUint2(out, value & 0xffffn, 2);
|
|
50336
|
+
} else if (value >= -2147483648n) {
|
|
50337
|
+
out.push(210);
|
|
50338
|
+
pushUint2(out, value & 0xffffffffn, 4);
|
|
50339
|
+
} else {
|
|
50340
|
+
out.push(211);
|
|
50341
|
+
pushUint2(out, value & UINT64_MAX2, 8);
|
|
50342
|
+
}
|
|
50343
|
+
}
|
|
50344
|
+
function packHeader2(out, length, fixBase, fixMax, codes) {
|
|
50345
|
+
if (length <= fixMax) {
|
|
50346
|
+
out.push(fixBase | length);
|
|
50347
|
+
return;
|
|
50348
|
+
}
|
|
50349
|
+
const [c8, c16, c32] = codes;
|
|
50350
|
+
if (c8 !== undefined && c8 !== 0 && length <= 255) {
|
|
50351
|
+
out.push(c8);
|
|
50352
|
+
pushUint2(out, BigInt(length), 1);
|
|
50353
|
+
} else if (length <= 65535) {
|
|
50354
|
+
out.push(c16);
|
|
50355
|
+
pushUint2(out, BigInt(length), 2);
|
|
50356
|
+
} else {
|
|
50357
|
+
out.push(c32);
|
|
50358
|
+
pushUint2(out, BigInt(length), 4);
|
|
50359
|
+
}
|
|
50360
|
+
}
|
|
50361
|
+
function packValue2(out, value) {
|
|
50362
|
+
if (value === null)
|
|
50363
|
+
out.push(192);
|
|
50364
|
+
else if (value === false)
|
|
50365
|
+
out.push(194);
|
|
50366
|
+
else if (value === true)
|
|
50367
|
+
out.push(195);
|
|
50368
|
+
else if (typeof value === "bigint")
|
|
50369
|
+
packInteger2(out, value);
|
|
50370
|
+
else if (typeof value === "number") {
|
|
50371
|
+
if (!Number.isSafeInteger(value))
|
|
50372
|
+
throw new Error(`msgpack: ${value} is not a safe integer`);
|
|
50373
|
+
packInteger2(out, BigInt(value));
|
|
50374
|
+
} else if (typeof value === "string") {
|
|
50375
|
+
const bytes = new TextEncoder().encode(value);
|
|
50376
|
+
packHeader2(out, bytes.length, 160, 31, [217, 218, 219]);
|
|
50377
|
+
for (const b of bytes)
|
|
50378
|
+
out.push(b);
|
|
50379
|
+
} else if (Array.isArray(value)) {
|
|
50380
|
+
packHeader2(out, value.length, 144, 15, [0, 220, 221]);
|
|
50381
|
+
for (const item of value)
|
|
50382
|
+
packValue2(out, item);
|
|
50383
|
+
} else if (typeof value === "object") {
|
|
50384
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
50385
|
+
for (const [key] of entries) {
|
|
50386
|
+
if (/^(0|[1-9][0-9]*)$/.test(key))
|
|
50387
|
+
throw new Error(`msgpack: integer-like key "${key}"`);
|
|
50388
|
+
}
|
|
50389
|
+
packHeader2(out, entries.length, 128, 15, [0, 222, 223]);
|
|
50390
|
+
for (const [key, item] of entries) {
|
|
50391
|
+
packValue2(out, key);
|
|
50392
|
+
packValue2(out, item);
|
|
50393
|
+
}
|
|
50394
|
+
} else {
|
|
50395
|
+
throw new Error(`msgpack: cannot encode a ${typeof value}`);
|
|
50396
|
+
}
|
|
50397
|
+
}
|
|
50398
|
+
function msgpackEncode2(value) {
|
|
50399
|
+
const out = [];
|
|
50400
|
+
packValue2(out, value);
|
|
50401
|
+
return Uint8Array.from(out);
|
|
50402
|
+
}
|
|
50403
|
+
function toHex2(bytes) {
|
|
50404
|
+
let out = "";
|
|
50405
|
+
for (const b of bytes)
|
|
50406
|
+
out += b.toString(16).padStart(2, "0");
|
|
50407
|
+
return out;
|
|
50408
|
+
}
|
|
50409
|
+
function hyperliquidActionHash2(action, nonce) {
|
|
50410
|
+
const n = BigInt(nonce);
|
|
50411
|
+
if (n < 0n || n > UINT64_MAX2)
|
|
50412
|
+
throw new Error("hyperliquidActionHash: nonce out of range");
|
|
50413
|
+
const out = Array.from(msgpackEncode2(action));
|
|
50414
|
+
pushUint2(out, n, 8);
|
|
50415
|
+
out.push(0);
|
|
50416
|
+
return `0x${toHex2(keccak_256(Uint8Array.from(out)))}`;
|
|
50417
|
+
}
|
|
50418
|
+
function hyperliquidL1TypedData2(connectionId, network2) {
|
|
50419
|
+
return {
|
|
50420
|
+
domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS2 },
|
|
50421
|
+
types: {
|
|
50422
|
+
Agent: [
|
|
50423
|
+
{ name: "source", type: "string" },
|
|
50424
|
+
{ name: "connectionId", type: "bytes32" }
|
|
50425
|
+
]
|
|
50426
|
+
},
|
|
50427
|
+
primary_type: "Agent",
|
|
50428
|
+
message: { source: network2 === "mainnet" ? "a" : "b", connectionId }
|
|
50429
|
+
};
|
|
50430
|
+
}
|
|
50431
|
+
function hyperliquidApproveBuilderFeeTypedData2(builder, nonce) {
|
|
50432
|
+
return {
|
|
50433
|
+
domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS2 },
|
|
50434
|
+
types: {
|
|
50435
|
+
"HyperliquidTransaction:ApproveBuilderFee": [
|
|
50436
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
50437
|
+
{ name: "maxFeeRate", type: "string" },
|
|
50438
|
+
{ name: "builder", type: "address" },
|
|
50439
|
+
{ name: "nonce", type: "uint64" }
|
|
50440
|
+
]
|
|
50441
|
+
},
|
|
50442
|
+
primary_type: "HyperliquidTransaction:ApproveBuilderFee",
|
|
50443
|
+
message: {
|
|
50444
|
+
hyperliquidChain: "Mainnet",
|
|
50445
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE2,
|
|
50446
|
+
builder: builder.toLowerCase(),
|
|
50447
|
+
nonce
|
|
50448
|
+
}
|
|
50449
|
+
};
|
|
50450
|
+
}
|
|
50451
|
+
function hyperliquidCanonicalJson2(value) {
|
|
50452
|
+
return canonical2(value);
|
|
50453
|
+
}
|
|
50454
|
+
function canonical2(value) {
|
|
50455
|
+
if (value === null || typeof value !== "object")
|
|
50456
|
+
return JSON.stringify(value);
|
|
50457
|
+
if (Array.isArray(value))
|
|
50458
|
+
return `[${value.map(canonical2).join(",")}]`;
|
|
50459
|
+
const record2 = value;
|
|
50460
|
+
return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonical2(record2[key])}`).join(",")}}`;
|
|
50461
|
+
}
|
|
50462
|
+
function sameValue2(a, b) {
|
|
50463
|
+
return canonical2(a) === canonical2(b);
|
|
50464
|
+
}
|
|
50465
|
+
function checkBuilderField2(action, builder) {
|
|
50466
|
+
if (!("builder" in action) || action.builder === undefined)
|
|
50467
|
+
return null;
|
|
50468
|
+
const field = action.builder;
|
|
50469
|
+
if (typeof field !== "object" || field === null)
|
|
50470
|
+
return "builder field is not an object";
|
|
50471
|
+
if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
|
|
50472
|
+
return "the order names a builder that is not Candle's";
|
|
50473
|
+
}
|
|
50474
|
+
if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS2) {
|
|
50475
|
+
return "the builder fee is outside Candle's 0.1% maximum";
|
|
50476
|
+
}
|
|
50477
|
+
return null;
|
|
50478
|
+
}
|
|
50479
|
+
function decimal2(value) {
|
|
50480
|
+
if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
|
|
50481
|
+
return null;
|
|
50482
|
+
const negative = value.startsWith("-");
|
|
50483
|
+
const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
|
|
50484
|
+
const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
|
|
50485
|
+
return negative ? -n : n;
|
|
50486
|
+
}
|
|
50487
|
+
function checkActionIntent2(action, intent) {
|
|
50488
|
+
const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
|
|
50489
|
+
const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
|
|
50490
|
+
if (!Array.isArray(orderList))
|
|
50491
|
+
return "invalid orders";
|
|
50492
|
+
for (const raw of orderList) {
|
|
50493
|
+
if (!raw || typeof raw !== "object")
|
|
50494
|
+
return "invalid order";
|
|
50495
|
+
const o = raw;
|
|
50496
|
+
if (!mainAsset(o.a))
|
|
50497
|
+
return "order asset is outside the main perp universe";
|
|
50498
|
+
if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal2(o.s) ?? 0n) <= 0n || (decimal2(o.p) ?? 0n) <= 0n)
|
|
50499
|
+
return "invalid order side, size, price or reduce-only flag";
|
|
50500
|
+
}
|
|
50501
|
+
if (action.type === "cancel") {
|
|
50502
|
+
if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
|
|
50503
|
+
return "invalid cancel asset";
|
|
50504
|
+
}
|
|
50505
|
+
if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
|
|
50506
|
+
return "invalid action asset";
|
|
50507
|
+
if (!intent)
|
|
50508
|
+
return null;
|
|
50509
|
+
const { method, params: p } = intent;
|
|
50510
|
+
const types2 = {
|
|
50511
|
+
setup: "approveBuilderFee",
|
|
50512
|
+
open: "order",
|
|
50513
|
+
close: "order",
|
|
50514
|
+
cancel: "cancel",
|
|
50515
|
+
modify: "modify",
|
|
50516
|
+
leverage: "updateLeverage",
|
|
50517
|
+
margin: "updateIsolatedMargin"
|
|
50518
|
+
};
|
|
50519
|
+
if (action.type !== types2[method])
|
|
50520
|
+
return "action type does not match the requested method";
|
|
50521
|
+
if (method === "leverage") {
|
|
50522
|
+
if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
|
|
50523
|
+
return "leverage or mode does not match the request";
|
|
50524
|
+
}
|
|
50525
|
+
if (method === "margin") {
|
|
50526
|
+
const amount = decimal2(p.amount);
|
|
50527
|
+
if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
|
|
50528
|
+
return "isolated margin amount does not match the request";
|
|
50529
|
+
}
|
|
50530
|
+
if (method === "open" || method === "close" || method === "modify") {
|
|
50531
|
+
const list = orderList;
|
|
50532
|
+
const first = list[0];
|
|
50533
|
+
if (!first)
|
|
50534
|
+
return "missing requested order";
|
|
50535
|
+
const typ = first.t;
|
|
50536
|
+
if (!typ?.limit)
|
|
50537
|
+
return "requested order is not a limit/IOC order";
|
|
50538
|
+
if (method === "open") {
|
|
50539
|
+
const buy = p.side === "long" || p.side === "buy";
|
|
50540
|
+
if (first.b !== buy || first.r !== false || decimal2(first.s) !== decimal2(p.size))
|
|
50541
|
+
return "order side, size or reduce-only flag does not match the request";
|
|
50542
|
+
const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
|
|
50543
|
+
if (typ.limit.tif !== tif)
|
|
50544
|
+
return "order time in force does not match the request";
|
|
50545
|
+
const triggers = [
|
|
50546
|
+
["takeProfit", "tp"],
|
|
50547
|
+
["stopLoss", "sl"]
|
|
50548
|
+
].filter(([field]) => p[field] != null);
|
|
50549
|
+
if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
|
|
50550
|
+
return "unexpected extra orders or grouping";
|
|
50551
|
+
for (let i = 0;i < triggers.length; i++) {
|
|
50552
|
+
const [field, kind] = triggers[i];
|
|
50553
|
+
const leg = list[i + 1];
|
|
50554
|
+
if (!leg)
|
|
50555
|
+
return "missing trigger order";
|
|
50556
|
+
const trigger = leg.t?.trigger;
|
|
50557
|
+
if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal2(leg.s) !== decimal2(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal2(trigger.triggerPx) !== decimal2(p[field]))
|
|
50558
|
+
return "trigger side, size, reduce-only flag or price does not match the request";
|
|
50559
|
+
}
|
|
50560
|
+
} else {
|
|
50561
|
+
if (list.length !== 1)
|
|
50562
|
+
return "unexpected extra orders";
|
|
50563
|
+
if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal2(first.s) ?? 0n) > (decimal2(p.size) ?? 0n)))
|
|
50564
|
+
return "close size or reduce-only flag does not match the request";
|
|
50565
|
+
if (method === "modify" && p.size != null && decimal2(first.s) !== decimal2(p.size))
|
|
50566
|
+
return "replacement size does not match the request";
|
|
50567
|
+
}
|
|
50568
|
+
if (p.price != null && decimal2(first.p) !== decimal2(p.price))
|
|
50569
|
+
return "order price does not match the request";
|
|
50570
|
+
}
|
|
50571
|
+
return null;
|
|
50572
|
+
}
|
|
50573
|
+
async function hyperliquidCloseOrder2(fetcher, network2, address, params) {
|
|
50574
|
+
const read = async (body) => {
|
|
50575
|
+
const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS2[network2].replace("/exchange", "/info"), {
|
|
50576
|
+
method: "POST",
|
|
50577
|
+
headers: { "content-type": "application/json" },
|
|
50578
|
+
body: JSON.stringify(body)
|
|
50579
|
+
});
|
|
50580
|
+
if (!res.ok)
|
|
50581
|
+
throw new Error("Cannot read the close position from Hyperliquid");
|
|
50582
|
+
return JSON.parse(await res.text());
|
|
50583
|
+
};
|
|
50584
|
+
const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
|
|
50585
|
+
const coin = String(params.coin).toUpperCase();
|
|
50586
|
+
const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
|
|
50587
|
+
const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
|
|
50588
|
+
const signed = decimal2(szi);
|
|
50589
|
+
if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
|
|
50590
|
+
throw new Error("No main-universe position to close");
|
|
50591
|
+
const held = signed < 0n ? -signed : signed;
|
|
50592
|
+
const asked = params.size == null ? held : decimal2(params.size);
|
|
50593
|
+
if (asked === null || asked <= 0n)
|
|
50594
|
+
throw new Error("Invalid close size");
|
|
50595
|
+
const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
|
|
50596
|
+
return { asset, isBuy: signed < 0n, size };
|
|
50597
|
+
}
|
|
50598
|
+
function verifyPerpsBuild2(build, opts) {
|
|
50599
|
+
try {
|
|
50600
|
+
const network2 = opts.network ?? "mainnet";
|
|
50601
|
+
if (!EVM_ADDRESS_RE2.test(opts.builder))
|
|
50602
|
+
return { ok: false, reason: "no valid Candle builder address to check against" };
|
|
50603
|
+
if (build.network !== undefined && build.network !== network2) {
|
|
50604
|
+
return { ok: false, reason: `the build is for ${build.network}, this client trades ${network2}` };
|
|
50605
|
+
}
|
|
50606
|
+
if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
|
|
50607
|
+
return { ok: false, reason: "the build is for a different wallet" };
|
|
50608
|
+
}
|
|
50609
|
+
if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
|
|
50610
|
+
return { ok: false, reason: "invalid nonce" };
|
|
50611
|
+
const typed = build.typedData;
|
|
50612
|
+
if (!typed || typeof typed !== "object")
|
|
50613
|
+
return { ok: false, reason: "no typed data" };
|
|
50614
|
+
const action = build.action;
|
|
50615
|
+
if (!action || typeof action !== "object")
|
|
50616
|
+
return { ok: false, reason: "no action" };
|
|
50617
|
+
const intentRefusal = checkActionIntent2(action, opts.intent);
|
|
50618
|
+
if (intentRefusal)
|
|
50619
|
+
return { ok: false, reason: intentRefusal };
|
|
50620
|
+
if (opts.closeOrder) {
|
|
50621
|
+
const order = action.orders[0];
|
|
50622
|
+
if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal2(order.s) !== decimal2(opts.closeOrder.size) || order.r !== true)
|
|
50623
|
+
return { ok: false, reason: "close side, asset or size does not match the requested position" };
|
|
50624
|
+
}
|
|
50625
|
+
if (typed.primary_type === "Agent") {
|
|
50626
|
+
const type = action.type;
|
|
50627
|
+
if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES2.includes(type)) {
|
|
50628
|
+
return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
|
|
50629
|
+
}
|
|
50630
|
+
if (type === "order") {
|
|
50631
|
+
const refusal = checkBuilderField2(action, opts.builder);
|
|
50632
|
+
if (refusal)
|
|
50633
|
+
return { ok: false, reason: refusal };
|
|
50634
|
+
} else if ("builder" in action) {
|
|
50635
|
+
return { ok: false, reason: `a ${type} action carries no builder` };
|
|
50636
|
+
}
|
|
50637
|
+
if ("vaultAddress" in action)
|
|
50638
|
+
return { ok: false, reason: "the action names a vault" };
|
|
50639
|
+
const expected = hyperliquidL1TypedData2(hyperliquidActionHash2(action, build.nonce), network2);
|
|
50640
|
+
if (!sameValue2(typed, expected)) {
|
|
50641
|
+
return { ok: false, reason: "the typed data does not commit to this action and nonce" };
|
|
50642
|
+
}
|
|
50643
|
+
return { ok: true, kind: "l1" };
|
|
50644
|
+
}
|
|
50645
|
+
if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
|
|
50646
|
+
const expected = hyperliquidApproveBuilderFeeTypedData2(opts.builder, build.nonce);
|
|
50647
|
+
if (!sameValue2(typed, expected)) {
|
|
50648
|
+
return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
|
|
50649
|
+
}
|
|
50650
|
+
const expectedAction = {
|
|
50651
|
+
type: "approveBuilderFee",
|
|
50652
|
+
hyperliquidChain: "Mainnet",
|
|
50653
|
+
signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID2,
|
|
50654
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE2,
|
|
50655
|
+
builder: opts.builder.toLowerCase(),
|
|
50656
|
+
nonce: build.nonce
|
|
50657
|
+
};
|
|
50658
|
+
if (!sameValue2(action, expectedAction)) {
|
|
50659
|
+
return { ok: false, reason: "the approval action does not match its typed data" };
|
|
50660
|
+
}
|
|
50661
|
+
return { ok: true, kind: "approveBuilderFee" };
|
|
50662
|
+
}
|
|
50663
|
+
return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
|
|
50664
|
+
} catch (err) {
|
|
50665
|
+
return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
|
|
50666
|
+
}
|
|
50667
|
+
}
|
|
50668
|
+
function hyperliquidSplitSignature2(signature) {
|
|
50669
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
50670
|
+
throw new Error("Not a 65-byte signature");
|
|
50671
|
+
let v = Number.parseInt(signature.slice(130, 132), 16);
|
|
50672
|
+
if (v < 27)
|
|
50673
|
+
v += 27;
|
|
50674
|
+
return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
|
|
50675
|
+
}
|
|
50676
|
+
function hyperliquidExchangeBody2(action, nonce, signature) {
|
|
50677
|
+
return { action, nonce, signature: hyperliquidSplitSignature2(signature), vaultAddress: null, expiresAfter: null };
|
|
50678
|
+
}
|
|
50679
|
+
function hyperliquidRelayBody2(typedData) {
|
|
50680
|
+
return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
|
|
50681
|
+
}
|
|
50682
|
+
var HYPERLIQUID_ALLOWED_ACTION_TYPES2, HYPERLIQUID_EXCHANGE_URLS2, CANDLE_HYPERLIQUID_BUILDER_ADDRESS2 = null, HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS2 = 100, HYPERLIQUID_MAX_BUILDER_FEE_RATE2 = "0.1%", HYPERLIQUID_SIGNATURE_CHAIN_ID2 = "0xa4b1", ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000", UINT64_MAX2, INT64_MIN2, EVM_ADDRESS_RE2;
|
|
50683
|
+
var init_hyperliquid = __esm(() => {
|
|
50684
|
+
init_sha3();
|
|
50685
|
+
HYPERLIQUID_ALLOWED_ACTION_TYPES2 = [
|
|
50686
|
+
"order",
|
|
50687
|
+
"cancel",
|
|
50688
|
+
"modify",
|
|
50689
|
+
"updateLeverage",
|
|
50690
|
+
"updateIsolatedMargin"
|
|
50691
|
+
];
|
|
50692
|
+
HYPERLIQUID_EXCHANGE_URLS2 = {
|
|
50693
|
+
mainnet: "https://api.hyperliquid.xyz/exchange",
|
|
50694
|
+
testnet: "https://api.hyperliquid-testnet.xyz/exchange"
|
|
50695
|
+
};
|
|
50696
|
+
UINT64_MAX2 = (1n << 64n) - 1n;
|
|
50697
|
+
INT64_MIN2 = -(1n << 63n);
|
|
50698
|
+
EVM_ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
50699
|
+
});
|
|
50700
|
+
|
|
50701
|
+
// ../mcp/src/perps.ts
|
|
50702
|
+
import { createPrivateKey as createPrivateKey2, sign as sign5 } from "node:crypto";
|
|
50703
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
50704
|
+
function fail3(code, message, extra = {}) {
|
|
50705
|
+
return { text: JSON.stringify({ success: false, error: { code, message }, ...extra }), isError: true };
|
|
50706
|
+
}
|
|
50707
|
+
async function call(cfg, fetch2, method, path, body) {
|
|
50708
|
+
const res = await fetch2(`${cfg.apiUrl.replace(/\/$/, "")}${path}`, {
|
|
50709
|
+
method,
|
|
50710
|
+
headers: { "Content-Type": "application/json", ...cfg.apiKey ? { "x-api-key": cfg.apiKey } : {} },
|
|
50711
|
+
...body !== undefined ? { body: JSON.stringify(body) } : {}
|
|
50712
|
+
});
|
|
50713
|
+
const text = await res.text();
|
|
50714
|
+
let parsed;
|
|
50715
|
+
try {
|
|
50716
|
+
parsed = JSON.parse(text);
|
|
50717
|
+
} catch {
|
|
50718
|
+
parsed = text;
|
|
50719
|
+
}
|
|
50720
|
+
if (!res.ok) {
|
|
50721
|
+
if (res.status === 404 && (typeof parsed !== "object" || parsed === null))
|
|
50722
|
+
throw new Refusal(fail3("PERPS_NOT_ENABLED", "This Candle deployment does not serve perps routes."));
|
|
50723
|
+
throw new Refusal({ text: typeof parsed === "string" ? parsed : JSON.stringify(parsed), isError: true });
|
|
50724
|
+
}
|
|
50725
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
50726
|
+
throw new Refusal(fail3("INVALID_RESPONSE", "Not a JSON object"));
|
|
50727
|
+
return parsed;
|
|
50728
|
+
}
|
|
50729
|
+
async function walletFor(cfg, fetch2, name2, scope) {
|
|
50730
|
+
const listed = await call(cfg, fetch2, "GET", "/api/v1/agent/wallets/trading");
|
|
50731
|
+
if (scope && !(Array.isArray(listed.scopes) && listed.scopes.includes(scope)))
|
|
50732
|
+
throw new Refusal(fail3("SCOPE_MISSING", `The key needs ${scope}.`));
|
|
50733
|
+
const rows = Array.isArray(listed.page) ? listed.page : [];
|
|
50734
|
+
const evm = rows.filter((row) => row.chain === "evm");
|
|
50735
|
+
const matches2 = name2 === undefined ? evm : evm.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
|
|
50736
|
+
if (matches2.length !== 1)
|
|
50737
|
+
throw new Refusal(fail3("TEE_WALLET_REQUIRED", evm.length === 0 ? "This key has no EVM TEE wallet bound to it." : `Name exactly one EVM TEE wallet: ${evm.map((row) => row.label ?? row.id).join(", ")}`));
|
|
50738
|
+
return { row: matches2[0], appId: typeof listed.privyAppId === "string" ? listed.privyAppId : "" };
|
|
50739
|
+
}
|
|
50740
|
+
async function builderPin2(cfg, fetch2, env) {
|
|
50741
|
+
const pinned = env.CANDLE_HYPERLIQUID_BUILDER?.trim() || CANDLE_HYPERLIQUID_BUILDER_ADDRESS2;
|
|
50742
|
+
if (pinned)
|
|
50743
|
+
return pinned.toLowerCase();
|
|
50744
|
+
const config = await call(cfg, fetch2, "GET", "/api/v1/agent/perps/config");
|
|
50745
|
+
if (typeof config.builder !== "string")
|
|
50746
|
+
throw new Refusal(fail3("PERPS_NOT_CONFIGURED", "The server reports no Hyperliquid builder address."));
|
|
50747
|
+
return config.builder.toLowerCase();
|
|
50748
|
+
}
|
|
50749
|
+
async function executePerps(tool, args, cfg, env, fetch2) {
|
|
50750
|
+
if (!cfg.apiKey)
|
|
50751
|
+
return fail3("MCP_VALIDATION", "CANDLE_AGENT_API_KEY is required for this tool.");
|
|
50752
|
+
const network2 = env.CANDLE_HYPERLIQUID_NETWORK === "testnet" ? "testnet" : "mainnet";
|
|
50753
|
+
const name2 = typeof args.wallet === "string" ? args.wallet : undefined;
|
|
50754
|
+
try {
|
|
50755
|
+
if (tool === "candle_perps_orders" || tool === "candle_perps_positions") {
|
|
50756
|
+
const { row: row2 } = await walletFor(cfg, fetch2, name2);
|
|
50757
|
+
const query = new URLSearchParams({ walletId: row2.id });
|
|
50758
|
+
if (typeof args.limit === "number")
|
|
50759
|
+
query.set("limit", String(args.limit));
|
|
50760
|
+
return { text: JSON.stringify(await call(cfg, fetch2, "GET", `${PATHS[tool]}?${query}`)) };
|
|
50761
|
+
}
|
|
50762
|
+
const willSubmit = args.submit !== false;
|
|
50763
|
+
const pemFile = env.CANDLE_KEY_SIGNER_PEM_FILE?.trim();
|
|
50764
|
+
let signerPem = null;
|
|
50765
|
+
if (willSubmit) {
|
|
50766
|
+
if (!pemFile)
|
|
50767
|
+
return fail3("SIGNER_UNAVAILABLE", "Set CANDLE_KEY_SIGNER_PEM_FILE to the key signer PEM (candle tee signer new --out <pem>) to sign perps actions.");
|
|
50768
|
+
try {
|
|
50769
|
+
signerPem = await readFile9(pemFile, "utf8");
|
|
50770
|
+
createPrivateKey2(signerPem);
|
|
50771
|
+
} catch {
|
|
50772
|
+
return fail3("SIGNER_UNAVAILABLE", "CANDLE_KEY_SIGNER_PEM_FILE does not hold a readable private key PEM.");
|
|
50773
|
+
}
|
|
50774
|
+
}
|
|
50775
|
+
const { row, appId } = await walletFor(cfg, fetch2, name2, "perps:write");
|
|
50776
|
+
const builder = await builderPin2(cfg, fetch2, env);
|
|
50777
|
+
const { wallet: _w, submit: _s, ...fields } = args;
|
|
50778
|
+
const build = await call(cfg, fetch2, "POST", PATHS[tool], {
|
|
50779
|
+
walletId: row.id,
|
|
50780
|
+
...fields,
|
|
50781
|
+
...tool === "candle_perps_open" ? { type: fields.price === undefined ? "market" : "limit" } : {}
|
|
50782
|
+
});
|
|
50783
|
+
if (build.ready === true)
|
|
50784
|
+
return { text: JSON.stringify(build) };
|
|
50785
|
+
const method = PATHS[tool].split("/").at(-1);
|
|
50786
|
+
const check = verifyPerpsBuild2(build, {
|
|
50787
|
+
builder,
|
|
50788
|
+
network: network2,
|
|
50789
|
+
address: row.address,
|
|
50790
|
+
intent: { method, params: fields }
|
|
50791
|
+
});
|
|
50792
|
+
if (!check.ok)
|
|
50793
|
+
return fail3("PERPS_BUILD_REFUSED", `Refused to sign this build: ${check.reason}. Nothing was signed.`);
|
|
50794
|
+
if (method === "close") {
|
|
50795
|
+
const closeOrder = await hyperliquidCloseOrder2(fetch2, network2, row.address, fields);
|
|
50796
|
+
const closeCheck = verifyPerpsBuild2(build, {
|
|
50797
|
+
builder,
|
|
50798
|
+
network: network2,
|
|
50799
|
+
address: row.address,
|
|
50800
|
+
intent: { method, params: fields },
|
|
50801
|
+
closeOrder
|
|
50802
|
+
});
|
|
50803
|
+
if (!closeCheck.ok)
|
|
50804
|
+
return fail3("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
|
|
50805
|
+
}
|
|
50806
|
+
if (!willSubmit || signerPem === null)
|
|
50807
|
+
return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
|
|
50808
|
+
if (!appId || !row.privyWalletId)
|
|
50809
|
+
return fail3("SIGNER_UNAVAILABLE", "The server did not return its relay identifiers.");
|
|
50810
|
+
const body = hyperliquidRelayBody2(build.typedData);
|
|
50811
|
+
const payload = hyperliquidCanonicalJson2({
|
|
50812
|
+
body,
|
|
50813
|
+
headers: { "privy-app-id": appId },
|
|
50814
|
+
method: "POST",
|
|
50815
|
+
url: `https://api.privy.io/v1/wallets/${row.privyWalletId}/rpc`,
|
|
50816
|
+
version: 1
|
|
50817
|
+
});
|
|
50818
|
+
const authorizationSignature2 = sign5("sha256", Buffer.from(payload), signerPem).toString("base64");
|
|
50819
|
+
const relay = await call(cfg, fetch2, "POST", `/api/v1/agent/wallets/${encodeURIComponent(row.id)}/sign`, {
|
|
50820
|
+
authorizationSignature: authorizationSignature2,
|
|
50821
|
+
body
|
|
50822
|
+
});
|
|
50823
|
+
const signature = relay.signature;
|
|
50824
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
50825
|
+
return fail3("INVALID_RESPONSE", "The relay did not return a signature.");
|
|
50826
|
+
const exchangeBody = hyperliquidExchangeBody2(build.action, build.nonce, signature);
|
|
50827
|
+
let exchange;
|
|
50828
|
+
try {
|
|
50829
|
+
const res = await fetch2(HYPERLIQUID_EXCHANGE_URLS2[network2], {
|
|
50830
|
+
method: "POST",
|
|
50831
|
+
headers: { "Content-Type": "application/json" },
|
|
50832
|
+
body: JSON.stringify(exchangeBody)
|
|
50833
|
+
});
|
|
50834
|
+
const text = await res.text();
|
|
50835
|
+
if (!res.ok)
|
|
50836
|
+
throw new Error(`HTTP ${res.status}`);
|
|
50837
|
+
exchange = JSON.parse(text);
|
|
50838
|
+
} catch (error) {
|
|
50839
|
+
return fail3("PERPS_SUBMIT_FAILED", `Signed, but the submit to Hyperliquid failed (${error instanceof Error ? error.message : "unknown"}). POST exchangeBody to Hyperliquid's /exchange to resubmit.`, { perpOrderId: build.perpOrderId, exchangeBody });
|
|
50840
|
+
}
|
|
50841
|
+
const accepted = typeof exchange === "object" && exchange !== null && exchange.status === "ok" && !(exchange.response?.data?.statuses ?? []).some((status) => typeof status === "object" && status !== null && ("error" in status));
|
|
50842
|
+
return {
|
|
50843
|
+
text: JSON.stringify({
|
|
50844
|
+
success: accepted,
|
|
50845
|
+
perpOrderId: build.perpOrderId,
|
|
50846
|
+
kind: build.kind,
|
|
50847
|
+
network: build.network,
|
|
50848
|
+
address: row.address,
|
|
50849
|
+
...build.cloid ? { cloid: build.cloid } : {},
|
|
50850
|
+
...build.childCloids ? { childCloids: build.childCloids } : {},
|
|
50851
|
+
nonce: build.nonce,
|
|
50852
|
+
signature,
|
|
50853
|
+
exchange
|
|
50854
|
+
}),
|
|
50855
|
+
...accepted ? {} : { isError: true }
|
|
50856
|
+
};
|
|
50857
|
+
} catch (error) {
|
|
50858
|
+
if (error instanceof Refusal)
|
|
50859
|
+
return error.tool;
|
|
50860
|
+
return fail3("MCP_TRANSPORT", error instanceof Error ? error.message : "The perps call failed.");
|
|
50861
|
+
}
|
|
50862
|
+
}
|
|
50863
|
+
var decimal3, wallet, submit, perpsShapes, PATHS, Refusal;
|
|
50864
|
+
var init_perps = __esm(() => {
|
|
50865
|
+
init_zod();
|
|
50866
|
+
init_hyperliquid();
|
|
50867
|
+
decimal3 = exports_external.string().regex(/^(0|[1-9][0-9]*)(\.[0-9]+)?$/, "a plain decimal string");
|
|
50868
|
+
wallet = exports_external.string().optional().describe("The EVM TEE wallet by id, address or label. Omit when the key has exactly one.");
|
|
50869
|
+
submit = exports_external.boolean().optional().describe("false: build and check only, sign and submit nothing. Default true.");
|
|
50870
|
+
perpsShapes = {
|
|
50871
|
+
candle_perps_setup: { wallet, submit },
|
|
50872
|
+
candle_perps_open: {
|
|
50873
|
+
coin: exports_external.string().describe("A market on Hyperliquid's main perp exchange, e.g. BTC"),
|
|
50874
|
+
side: exports_external.enum(["long", "short"]),
|
|
50875
|
+
size: decimal3.describe('Size in the coin, decimal (e.g. "0.01")'),
|
|
50876
|
+
price: decimal3.optional().describe("Limit price. Omit for a market order (IOC within slippageBps of the mid)."),
|
|
50877
|
+
tif: exports_external.enum(["Gtc", "Alo", "Ioc"]).optional().describe("With price: time in force (default Gtc)"),
|
|
50878
|
+
slippageBps: exports_external.number().int().min(1).optional(),
|
|
50879
|
+
takeProfit: decimal3.optional().describe("Reduce-only take-profit trigger price"),
|
|
50880
|
+
stopLoss: decimal3.optional().describe("Reduce-only stop-loss trigger price"),
|
|
50881
|
+
wallet,
|
|
50882
|
+
submit
|
|
50883
|
+
},
|
|
50884
|
+
candle_perps_close: {
|
|
50885
|
+
coin: exports_external.string(),
|
|
50886
|
+
size: decimal3.optional().describe("Part of the position to close; the whole position when omitted"),
|
|
50887
|
+
slippageBps: exports_external.number().int().min(1).optional(),
|
|
50888
|
+
wallet,
|
|
50889
|
+
submit
|
|
50890
|
+
},
|
|
50891
|
+
candle_perps_cancel: {
|
|
50892
|
+
cloid: exports_external.string().regex(/^0x[0-9a-fA-F]{32}$/).describe("The cloid of an order Candle built (an open, or one of its take-profit / stop-loss legs)"),
|
|
50893
|
+
wallet,
|
|
50894
|
+
submit
|
|
50895
|
+
},
|
|
50896
|
+
candle_perps_orders: { wallet, limit: exports_external.number().int().min(1).max(200).optional() },
|
|
50897
|
+
candle_perps_positions: { wallet },
|
|
50898
|
+
candle_perps_leverage: {
|
|
50899
|
+
coin: exports_external.string(),
|
|
50900
|
+
leverage: exports_external.number().int().min(1),
|
|
50901
|
+
mode: exports_external.enum(["cross", "isolated"]).optional(),
|
|
50902
|
+
wallet,
|
|
50903
|
+
submit
|
|
50904
|
+
}
|
|
50905
|
+
};
|
|
50906
|
+
PATHS = {
|
|
50907
|
+
candle_perps_setup: "/api/v1/agent/perps/setup",
|
|
50908
|
+
candle_perps_open: "/api/v1/agent/perps/open",
|
|
50909
|
+
candle_perps_close: "/api/v1/agent/perps/close",
|
|
50910
|
+
candle_perps_cancel: "/api/v1/agent/perps/cancel",
|
|
50911
|
+
candle_perps_orders: "/api/v1/agent/perps/orders",
|
|
50912
|
+
candle_perps_positions: "/api/v1/agent/perps/positions",
|
|
50913
|
+
candle_perps_leverage: "/api/v1/agent/perps/leverage"
|
|
50914
|
+
};
|
|
50915
|
+
Refusal = class Refusal extends Error {
|
|
50916
|
+
tool;
|
|
50917
|
+
constructor(tool) {
|
|
50918
|
+
super("refused");
|
|
50919
|
+
this.tool = tool;
|
|
50920
|
+
}
|
|
50921
|
+
};
|
|
50922
|
+
});
|
|
50923
|
+
|
|
49943
50924
|
// ../mcp/src/tools.ts
|
|
49944
50925
|
function resolveToolAllowlist(env) {
|
|
49945
50926
|
const raw = env.CANDLE_MCP_TOOLS?.trim();
|
|
@@ -50082,6 +51063,13 @@ function buildRequest(name2, args, cfg) {
|
|
|
50082
51063
|
init: { method: "GET", headers: jsonHeaders(apiKey) }
|
|
50083
51064
|
};
|
|
50084
51065
|
}
|
|
51066
|
+
case "candle_get_portfolio": {
|
|
51067
|
+
const apiKey = requireApiKey2(cfg);
|
|
51068
|
+
return {
|
|
51069
|
+
url: `${base2}/api/v1/agent/portfolio`,
|
|
51070
|
+
init: { method: "GET", headers: jsonHeaders(apiKey) }
|
|
51071
|
+
};
|
|
51072
|
+
}
|
|
50085
51073
|
case "candle_get_profile_trades": {
|
|
50086
51074
|
const apiKey = requireApiKey2(cfg);
|
|
50087
51075
|
const { keyPrefix, limit } = args;
|
|
@@ -50132,7 +51120,7 @@ function registerTools(server, env = process.env) {
|
|
|
50132
51120
|
title: "Token forensics",
|
|
50133
51121
|
description: `Gate a buy before making it: who launched it (resolved on-chain; pump.fun's shared updateAuthority is never the developer; when no developer is on chain, deployer.attribution names the launchpad or issuer instead, e.g. launched via stonk.fun or issued by xStocks), their went-to-zero rate and last coins, who bought in the deploy window (the creator's own wallets are marked disclosed; strangers in the same slot are the bundle signal), same-funder insider share, same-funder deployer cluster, and safety.summary with six sourced flags (mintAuthority, freezeAuthority, tokenExtensions, lpLock, sellability, liquidityDrain). Refuse an unprompted buy when flagged; incomplete or unknown is not clearance. launch.deployerLaunches is an inclusive informational count, never a warning. Every measurement carries a coverage note -- 'unavailable' is not 'clean'. No key needed.
|
|
50134
51122
|
|
|
50135
|
-
MARKET_NOT_FOUND means Candle has no market for that token and this could not run. That is also not 'clean': report that you could not check it, rather than reporting the token as safe. That refusal now carries error.coverage -- covered:false, a reason ('external_launchpad' when the token launched somewhere else, 'unknown_mint' when nobody has indexed it), the launchpad when known, and every check that consequently did not run. Read it instead of guessing. Most of the feed, and any Solana mint Jupiter's index knows, now answers with a partial report instead.`,
|
|
51123
|
+
MARKET_NOT_FOUND means Candle has no market for that token and this could not run. That is also not 'clean': report that you could not check it, rather than reporting the token as safe. That refusal now carries error.coverage -- covered:false, a reason ('external_launchpad' when the token launched somewhere else, 'unknown_mint' when nobody has indexed it), the launchpad when known, and every check that consequently did not run. Read it instead of guessing. Most of the feed, and any Solana mint Jupiter's index knows, now answers with a partial report instead. On Hood, a token Candle did not launch names its launch account (deployer.method cvc_launch_account) for pons.family and pools.trade launches, with no record of earlier coins; other Hood launchpads return no developer.`,
|
|
50136
51124
|
inputSchema: tokenForensicsShape
|
|
50137
51125
|
}, async (args) => callAndRelay("candle_token_forensics", args, cfg));
|
|
50138
51126
|
register("candle_get_feed", {
|
|
@@ -50184,7 +51172,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50184
51172
|
}, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
|
|
50185
51173
|
register("candle_get_profile_pnl", {
|
|
50186
51174
|
title: "Read an agent profile's P&L",
|
|
50187
|
-
description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their cost basis, each MARKED at Candle's current price where one " + "exists: `markPriceUsd`, `marketValueUsd` and `unrealizedUsd` per position, and `unrealizedUsd` " + "overall. A position with no price is counted in `unmarkedPositions` and left out of unrealized, " + "never valued at zero; `oldestMarkAt` says how old the marks are. Deposits, withdrawals and " + "transfers are excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before " + "quoting the number; they mean the total is partial. Reads only.",
|
|
51175
|
+
description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their cost basis, each MARKED at Candle's current price where one " + "exists: `markPriceUsd`, `marketValueUsd` and `unrealizedUsd` per position, and `unrealizedUsd` " + "overall. A position with no price is counted in `unmarkedPositions` and left out of unrealized, " + "never valued at zero; `oldestMarkAt` says how old the marks are. Deposits, withdrawals and " + "transfers are excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before " + "quoting the number; they mean the total is partial. `pnl.byWallet` splits the same fills by the " + "wallet that paid for each trade (main or linked, with the linked wallet's label), one row per " + "wallet with the same figures, for tracking one strategy per wallet. Each row is an independent " + "average-cost pool over its own fills, so the rows need not sum to the total (a token moved " + "between wallets, or two wallets holding one token at different costs); the total stays correct. " + "Every open position and every `byWallet` row (and its positions) carries `chain` ('solana' or " + "'hood'), and `pnl.byChain` has the total's figures over each chain's fills alone, both chains " + "always present, with `openPositions` there a count (the positions are in `pnl.openPositions`, by " + "`chain`). Each chain is its own average-cost pool, so the two need not sum exactly to the total. A " + "server that predates per-chain P&L omits `chain` and `byChain`. Reads only.",
|
|
50188
51176
|
inputSchema: profilePnlShape
|
|
50189
51177
|
}, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
|
|
50190
51178
|
register("candle_get_profile_trades", {
|
|
@@ -50192,6 +51180,11 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50192
51180
|
description: "Orders, actual fills, fees, timestamps and transaction hashes for this profile. Includes FAILED " + "trades, with an errorCode saying why each did not go through, so this answers 'what happened to " + "my order' as well as 'what did I trade'. Reads only; moves nothing.",
|
|
50193
51181
|
inputSchema: profileTradesShape
|
|
50194
51182
|
}, async (args) => callAndRelay("candle_get_profile_trades", args, cfg));
|
|
51183
|
+
register("candle_get_portfolio", {
|
|
51184
|
+
title: "Read what the account holds, on Solana and Hood",
|
|
51185
|
+
description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
|
|
51186
|
+
inputSchema: {}
|
|
51187
|
+
}, async () => callAndRelay("candle_get_portfolio", {}, cfg));
|
|
50195
51188
|
register("candle_resolve_token", {
|
|
50196
51189
|
title: "Resolve a contract address to a token",
|
|
50197
51190
|
description: "Turn a bare contract address or mint into Candle's market for it: chain, symbol, " + "decimals, quote asset, and whether Candle can trade it. Start here when a human gives " + "you an address and nothing else. The chain is read off the address's own shape and is " + "not guessed, so it does not need to be supplied. Reads only; moves nothing. A 404 means " + "Candle has no market for that address, which is an answer, not a failure to retry.",
|
|
@@ -50267,12 +51260,53 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50267
51260
|
const result = await executeLaunchAndSeed(args, cfg, fetch);
|
|
50268
51261
|
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
50269
51262
|
});
|
|
51263
|
+
const perpsTool = (tool) => async (args) => {
|
|
51264
|
+
const result = await executePerps(tool, args, cfg, env, fetch);
|
|
51265
|
+
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
51266
|
+
};
|
|
51267
|
+
const perpsWrite = " MOVES REAL FUNDS on Hyperliquid unless submit is false. Candle builds the action within this key's limits " + "(USD window, maximum leverage, maximum position, slippage and price bands, main-exchange markets only); this " + "server recomputes its hash and checks the action type and Candle's builder before signing, then submits it to " + "Hyperliquid itself. Needs perps:write on the key and CANDLE_KEY_SIGNER_PEM_FILE. A refusal names the limit " + "that refused it (error.limit).";
|
|
51268
|
+
register("candle_perps_setup", {
|
|
51269
|
+
title: "Set up Hyperliquid perps",
|
|
51270
|
+
description: "Approve Candle's Hyperliquid builder fee (0.1%) once for the key's EVM TEE wallet, and report the " + "account's mode and balance. Idempotent: nothing is signed when the approval is already on Hyperliquid." + perpsWrite,
|
|
51271
|
+
inputSchema: perpsShapes.candle_perps_setup
|
|
51272
|
+
}, perpsTool("candle_perps_setup"));
|
|
51273
|
+
register("candle_perps_open", {
|
|
51274
|
+
title: "Open a perps position",
|
|
51275
|
+
description: "Open or add to a perpetual position on Hyperliquid's main perp exchange: a market order (IOC within " + "slippageBps of the mid) without `price`, a limit order with it, and optional reduce-only takeProfit and " + "stopLoss triggers. Free, Believer and Pro pay a 0.1% builder fee; Max pays none." + perpsWrite,
|
|
51276
|
+
inputSchema: perpsShapes.candle_perps_open
|
|
51277
|
+
}, perpsTool("candle_perps_open"));
|
|
51278
|
+
register("candle_perps_close", {
|
|
51279
|
+
title: "Close a perps position",
|
|
51280
|
+
description: "Close all or part of a position with a reduce-only IOC order. It reserves nothing, so a key at its USD " + "cap can still close." + perpsWrite,
|
|
51281
|
+
inputSchema: perpsShapes.candle_perps_close
|
|
51282
|
+
}, perpsTool("candle_perps_close"));
|
|
51283
|
+
register("candle_perps_cancel", {
|
|
51284
|
+
title: "Cancel a perps order",
|
|
51285
|
+
description: "Cancel an order Candle built, by its cloid (from candle_perps_open or candle_perps_orders)." + perpsWrite,
|
|
51286
|
+
inputSchema: perpsShapes.candle_perps_cancel
|
|
51287
|
+
}, perpsTool("candle_perps_cancel"));
|
|
51288
|
+
register("candle_perps_orders", {
|
|
51289
|
+
title: "Perps orders",
|
|
51290
|
+
description: "Open orders on Hyperliquid for the key's EVM TEE wallet, and every action Candle built for it with what " + "the venue shows. Reads only; moves nothing.",
|
|
51291
|
+
inputSchema: perpsShapes.candle_perps_orders
|
|
51292
|
+
}, perpsTool("candle_perps_orders"));
|
|
51293
|
+
register("candle_perps_positions", {
|
|
51294
|
+
title: "Perps positions",
|
|
51295
|
+
description: "Positions, account value and withdrawable balance on Hyperliquid for the key's EVM TEE wallet, read " + "live by its address. Reads only; moves nothing.",
|
|
51296
|
+
inputSchema: perpsShapes.candle_perps_positions
|
|
51297
|
+
}, perpsTool("candle_perps_positions"));
|
|
51298
|
+
register("candle_perps_leverage", {
|
|
51299
|
+
title: "Set perps leverage",
|
|
51300
|
+
description: "Set a market's leverage and margin mode (cross or isolated) on the account, within the key's maximum leverage." + perpsWrite,
|
|
51301
|
+
inputSchema: perpsShapes.candle_perps_leverage
|
|
51302
|
+
}, perpsTool("candle_perps_leverage"));
|
|
50270
51303
|
}
|
|
50271
51304
|
var TOOL_NAMES, launchTokenShape, getMarketShape, tokenForensicsShape, getFeedShape, reportActivityShape, getAgentProfileShape, getOperationShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, _rawBuyAmount, seedableLaunchShape, launchAndSeedShape, transferShape, sweepShape;
|
|
50272
51305
|
var init_tools = __esm(() => {
|
|
50273
51306
|
init_zod();
|
|
50274
51307
|
init_convert();
|
|
50275
51308
|
init_orchestrate();
|
|
51309
|
+
init_perps();
|
|
50276
51310
|
init_update_notice();
|
|
50277
51311
|
TOOL_NAMES = [
|
|
50278
51312
|
"candle_launch_token",
|
|
@@ -50291,9 +51325,17 @@ var init_tools = __esm(() => {
|
|
|
50291
51325
|
"candle_set_profile_wallets",
|
|
50292
51326
|
"candle_get_profile_pnl",
|
|
50293
51327
|
"candle_get_profile_trades",
|
|
51328
|
+
"candle_get_portfolio",
|
|
50294
51329
|
"candle_resolve_token",
|
|
50295
51330
|
"candle_execution_status",
|
|
50296
|
-
"candle_get_operation"
|
|
51331
|
+
"candle_get_operation",
|
|
51332
|
+
"candle_perps_setup",
|
|
51333
|
+
"candle_perps_open",
|
|
51334
|
+
"candle_perps_close",
|
|
51335
|
+
"candle_perps_cancel",
|
|
51336
|
+
"candle_perps_orders",
|
|
51337
|
+
"candle_perps_positions",
|
|
51338
|
+
"candle_perps_leverage"
|
|
50297
51339
|
];
|
|
50298
51340
|
launchTokenShape = {
|
|
50299
51341
|
clientLaunchId: exports_external.string().describe("Caller-chosen idempotency key, unique per account"),
|
|
@@ -50468,7 +51510,7 @@ var init_server2 = __esm(() => {
|
|
|
50468
51510
|
// src/index.ts
|
|
50469
51511
|
import { spawn as spawn3 } from "node:child_process";
|
|
50470
51512
|
import { realpathSync } from "node:fs";
|
|
50471
|
-
import { chmod as chmod8, readFile as
|
|
51513
|
+
import { chmod as chmod8, readFile as readFile10, realpath, rename as rename6, unlink, writeFile as writeFile7 } from "node:fs/promises";
|
|
50472
51514
|
import { homedir as homedir6, hostname } from "node:os";
|
|
50473
51515
|
import { pathToFileURL } from "node:url";
|
|
50474
51516
|
|
|
@@ -50516,7 +51558,7 @@ init_render();
|
|
|
50516
51558
|
init_secret_store();
|
|
50517
51559
|
|
|
50518
51560
|
// src/version.ts
|
|
50519
|
-
var CLI_VERSION = "0.11.
|
|
51561
|
+
var CLI_VERSION = "0.11.14";
|
|
50520
51562
|
|
|
50521
51563
|
// src/commands/auth.ts
|
|
50522
51564
|
init_keys_embedded_wallet();
|
|
@@ -50932,6 +51974,11 @@ var ENVIRONMENT = [
|
|
|
50932
51974
|
name: "CANDLE_ENCLAVE_HELPER",
|
|
50933
51975
|
description: "Path to the signed candle-enclave.app helper (macOS)"
|
|
50934
51976
|
},
|
|
51977
|
+
{
|
|
51978
|
+
name: "CANDLE_HYPERLIQUID_BUILDER",
|
|
51979
|
+
description: "candle perps: Candle's Hyperliquid builder address, pinned for the check every build passes before it is signed; without it the release's, else the one the server reports"
|
|
51980
|
+
},
|
|
51981
|
+
{ name: "CANDLE_HYPERLIQUID_NETWORK", description: "candle perps: set to testnet to trade Hyperliquid testnet" },
|
|
50935
51982
|
{ name: "CANDLE_NO_UPDATE_NOTIFIER", description: "Set to 1 to silence the update notice" },
|
|
50936
51983
|
{
|
|
50937
51984
|
name: "CANDLE_KEYSTORE_PASSPHRASE",
|
|
@@ -51077,6 +52124,53 @@ var HELP = {
|
|
|
51077
52124
|
],
|
|
51078
52125
|
env: ENV_API
|
|
51079
52126
|
},
|
|
52127
|
+
perps: {
|
|
52128
|
+
group: "Trade",
|
|
52129
|
+
summary: "Hyperliquid perpetuals from your EVM TEE wallet: setup, open, close, cancel, orders, positions, leverage",
|
|
52130
|
+
description: "Trade perpetuals on Hyperliquid's main perp exchange from the EVM TEE wallet bound to this key (scope perps:write, opt-in). Candle builds each action within the key's limits; this machine recomputes its hash and checks the action type and Candle's builder before anything is signed, then Candle's relay signs and this machine submits to Hyperliquid. Free, Believer and Pro pay a 0.1% builder fee; Max pays none.",
|
|
52131
|
+
usage: ["candle perps <subcommand> [flags]"],
|
|
52132
|
+
rows: [
|
|
52133
|
+
{
|
|
52134
|
+
invocation: "setup [--wallet <tee>]",
|
|
52135
|
+
description: "Approve Candle's builder fee once; shows the account's mode and balance"
|
|
52136
|
+
},
|
|
52137
|
+
{
|
|
52138
|
+
invocation: "open <coin> <long|short> <size> [--price <px>]",
|
|
52139
|
+
description: "Market (IOC) without --price, limit with it; optional --tp and --sl"
|
|
52140
|
+
},
|
|
52141
|
+
{ invocation: "close <coin> [--size <n>]", description: "Reduce-only IOC close of all or part of a position" },
|
|
52142
|
+
{ invocation: "cancel <cloid>", description: "Cancel an order Candle built, by its cloid" },
|
|
52143
|
+
{
|
|
52144
|
+
invocation: "orders [--wallet <tee>]",
|
|
52145
|
+
description: "Open orders on Hyperliquid, and every action Candle built"
|
|
52146
|
+
},
|
|
52147
|
+
{ invocation: "positions [--wallet <tee>]", description: "Positions and account value, read live" },
|
|
52148
|
+
{ invocation: "leverage <coin> <x> [--isolated]", description: "Set a market's leverage and margin mode" }
|
|
52149
|
+
],
|
|
52150
|
+
flags: [
|
|
52151
|
+
{
|
|
52152
|
+
invocation: "--wallet <tee>",
|
|
52153
|
+
description: "The EVM TEE wallet by id, address or unique label (default: the only one)"
|
|
52154
|
+
},
|
|
52155
|
+
{ invocation: "--tif <Gtc|Alo|Ioc>", description: "open with --price: time in force (default Gtc)" },
|
|
52156
|
+
{
|
|
52157
|
+
invocation: "--slippage-bps <n>",
|
|
52158
|
+
description: "open at market and close: the bound against the mid (default 100)"
|
|
52159
|
+
},
|
|
52160
|
+
{ invocation: "--tp <px> / --sl <px>", description: "open: reduce-only take-profit and stop-loss triggers" },
|
|
52161
|
+
{ invocation: "--no-submit", description: "Build and check only; sign and submit nothing" },
|
|
52162
|
+
{ invocation: "--sign-only", description: "Build, check and sign through the relay; submit nothing" },
|
|
52163
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt" }
|
|
52164
|
+
],
|
|
52165
|
+
examples: [
|
|
52166
|
+
"candle perps setup",
|
|
52167
|
+
"candle perps open BTC long 0.01",
|
|
52168
|
+
"candle perps open ETH short 0.5 --price 3200 --tp 2900 --sl 3400",
|
|
52169
|
+
"candle perps close BTC",
|
|
52170
|
+
"candle perps leverage BTC 5 --isolated"
|
|
52171
|
+
],
|
|
52172
|
+
env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK"]
|
|
52173
|
+
},
|
|
51080
52174
|
transfer: {
|
|
51081
52175
|
group: "Trade",
|
|
51082
52176
|
summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
|
|
@@ -51138,24 +52232,33 @@ var HELP = {
|
|
|
51138
52232
|
},
|
|
51139
52233
|
portfolio: {
|
|
51140
52234
|
group: "Account",
|
|
51141
|
-
summary: "Every wallet's holdings, prices and value
|
|
51142
|
-
description: "Vault, TEE and embedded wallets, each token with amount, price and value, then
|
|
51143
|
-
usage: ["candle portfolio [--rpc-url <url>] [--json]"],
|
|
52235
|
+
summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
|
|
52236
|
+
description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade.",
|
|
52237
|
+
usage: ["candle portfolio [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
|
|
51144
52238
|
rows: [],
|
|
51145
52239
|
flags: [
|
|
52240
|
+
{ invocation: "--chain solana|hood", description: "Show one chain, and read nothing on the other" },
|
|
51146
52241
|
{
|
|
51147
52242
|
invocation: "--rpc-url <url>",
|
|
51148
52243
|
description: "Your Solana RPC, for the vault. Without one: CANDLE_SOLANA_RPC_URL, then the profile's, then the public endpoint"
|
|
51149
52244
|
},
|
|
52245
|
+
{
|
|
52246
|
+
invocation: "--evm-rpc-url <url>",
|
|
52247
|
+
description: "Your Hood RPC, for EVM vault keys. Without one: CANDLE_EVM_RPC_URL, then the built-in Hood RPC"
|
|
52248
|
+
},
|
|
51150
52249
|
KEYSTORE_FLAG
|
|
51151
52250
|
],
|
|
51152
|
-
examples: [
|
|
52251
|
+
examples: [
|
|
52252
|
+
"candle portfolio",
|
|
52253
|
+
"candle portfolio --chain hood",
|
|
52254
|
+
"candle portfolio --rpc-url https://your-rpc.example --evm-rpc-url https://your-hood-rpc.example --json"
|
|
52255
|
+
],
|
|
51153
52256
|
env: [...ENV_API, ...ENV_LOCAL_SIGNING]
|
|
51154
52257
|
},
|
|
51155
52258
|
pnl: {
|
|
51156
52259
|
group: "Account",
|
|
51157
|
-
summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's own)",
|
|
51158
|
-
description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the web P&L chart shows. Needs a key with the Read scope (account:read). With --profile <name>, that profile's key reads its own P&L. Unpriced positions are shown as unpriced and never valued at zero. Where Candle serves LP, DAMM v2 positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis), and vs holding (what the deposited tokens would be worth held); the total then covers tokens and LP.",
|
|
52260
|
+
summary: "P&L per chain and combined: realized, fees, unrealized and open positions (--profile for one key's own)",
|
|
52261
|
+
description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the web P&L chart shows. Needs a key with the Read scope (account:read). With --profile <name>, that profile's key reads its own P&L, with a row per paying wallet. Both show a Solana block, a Hood block, then the combined figure, and a CHAIN column on positions and wallet rows. Unpriced positions are shown as unpriced and never valued at zero. A bridge between Solana and Hood is not a trade: it opens and closes nothing, and bridge and Relay costs are not netted from P&L. Where Candle serves LP, DAMM v2 positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis), and vs holding (what the deposited tokens would be worth held); the total then covers tokens and LP.",
|
|
51159
52262
|
usage: ["candle pnl [--profile <name>] [--json]"],
|
|
51160
52263
|
rows: [],
|
|
51161
52264
|
examples: ["candle pnl", "candle pnl --profile scalper --json"],
|
|
@@ -58954,9 +60057,17 @@ var MCP_TOOL_NAMES = [
|
|
|
58954
60057
|
"candle_set_profile_wallets",
|
|
58955
60058
|
"candle_get_profile_pnl",
|
|
58956
60059
|
"candle_get_profile_trades",
|
|
60060
|
+
"candle_get_portfolio",
|
|
58957
60061
|
"candle_resolve_token",
|
|
58958
60062
|
"candle_execution_status",
|
|
58959
|
-
"candle_get_operation"
|
|
60063
|
+
"candle_get_operation",
|
|
60064
|
+
"candle_perps_setup",
|
|
60065
|
+
"candle_perps_open",
|
|
60066
|
+
"candle_perps_close",
|
|
60067
|
+
"candle_perps_cancel",
|
|
60068
|
+
"candle_perps_orders",
|
|
60069
|
+
"candle_perps_positions",
|
|
60070
|
+
"candle_perps_leverage"
|
|
58960
60071
|
];
|
|
58961
60072
|
var READ_ONLY_TOOL_NAMES = [
|
|
58962
60073
|
"candle_get_market",
|
|
@@ -59064,6 +60175,827 @@ async function mcp(args, ctx) {
|
|
|
59064
60175
|
}
|
|
59065
60176
|
}
|
|
59066
60177
|
|
|
60178
|
+
// src/commands/perps.ts
|
|
60179
|
+
init_args();
|
|
60180
|
+
import { sign as sign3 } from "node:crypto";
|
|
60181
|
+
|
|
60182
|
+
// src/hyperliquid.ts
|
|
60183
|
+
init_sha3();
|
|
60184
|
+
var HYPERLIQUID_ALLOWED_ACTION_TYPES = [
|
|
60185
|
+
"order",
|
|
60186
|
+
"cancel",
|
|
60187
|
+
"modify",
|
|
60188
|
+
"updateLeverage",
|
|
60189
|
+
"updateIsolatedMargin"
|
|
60190
|
+
];
|
|
60191
|
+
var HYPERLIQUID_EXCHANGE_URLS = {
|
|
60192
|
+
mainnet: "https://api.hyperliquid.xyz/exchange",
|
|
60193
|
+
testnet: "https://api.hyperliquid-testnet.xyz/exchange"
|
|
60194
|
+
};
|
|
60195
|
+
var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
|
|
60196
|
+
var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
|
|
60197
|
+
var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
|
|
60198
|
+
var HYPERLIQUID_SIGNATURE_CHAIN_ID = "0xa4b1";
|
|
60199
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
60200
|
+
var UINT64_MAX = (1n << 64n) - 1n;
|
|
60201
|
+
var INT64_MIN = -(1n << 63n);
|
|
60202
|
+
function pushUint(out, value, width) {
|
|
60203
|
+
for (let i = width - 1;i >= 0; i--)
|
|
60204
|
+
out.push(Number(value >> BigInt(i * 8) & 0xffn));
|
|
60205
|
+
}
|
|
60206
|
+
function packInteger(out, value) {
|
|
60207
|
+
if (value >= 0n) {
|
|
60208
|
+
if (value > UINT64_MAX)
|
|
60209
|
+
throw new Error("msgpack: integer above uint64");
|
|
60210
|
+
if (value < 0x80n)
|
|
60211
|
+
out.push(Number(value));
|
|
60212
|
+
else if (value <= 0xffn) {
|
|
60213
|
+
out.push(204);
|
|
60214
|
+
pushUint(out, value, 1);
|
|
60215
|
+
} else if (value <= 0xffffn) {
|
|
60216
|
+
out.push(205);
|
|
60217
|
+
pushUint(out, value, 2);
|
|
60218
|
+
} else if (value <= 0xffffffffn) {
|
|
60219
|
+
out.push(206);
|
|
60220
|
+
pushUint(out, value, 4);
|
|
60221
|
+
} else {
|
|
60222
|
+
out.push(207);
|
|
60223
|
+
pushUint(out, value, 8);
|
|
60224
|
+
}
|
|
60225
|
+
return;
|
|
60226
|
+
}
|
|
60227
|
+
if (value < INT64_MIN)
|
|
60228
|
+
throw new Error("msgpack: integer below int64");
|
|
60229
|
+
if (value >= -32n)
|
|
60230
|
+
out.push(Number(value & 0xffn));
|
|
60231
|
+
else if (value >= -128n) {
|
|
60232
|
+
out.push(208);
|
|
60233
|
+
pushUint(out, value & 0xffn, 1);
|
|
60234
|
+
} else if (value >= -32768n) {
|
|
60235
|
+
out.push(209);
|
|
60236
|
+
pushUint(out, value & 0xffffn, 2);
|
|
60237
|
+
} else if (value >= -2147483648n) {
|
|
60238
|
+
out.push(210);
|
|
60239
|
+
pushUint(out, value & 0xffffffffn, 4);
|
|
60240
|
+
} else {
|
|
60241
|
+
out.push(211);
|
|
60242
|
+
pushUint(out, value & UINT64_MAX, 8);
|
|
60243
|
+
}
|
|
60244
|
+
}
|
|
60245
|
+
function packHeader(out, length, fixBase, fixMax, codes) {
|
|
60246
|
+
if (length <= fixMax) {
|
|
60247
|
+
out.push(fixBase | length);
|
|
60248
|
+
return;
|
|
60249
|
+
}
|
|
60250
|
+
const [c8, c16, c32] = codes;
|
|
60251
|
+
if (c8 !== undefined && c8 !== 0 && length <= 255) {
|
|
60252
|
+
out.push(c8);
|
|
60253
|
+
pushUint(out, BigInt(length), 1);
|
|
60254
|
+
} else if (length <= 65535) {
|
|
60255
|
+
out.push(c16);
|
|
60256
|
+
pushUint(out, BigInt(length), 2);
|
|
60257
|
+
} else {
|
|
60258
|
+
out.push(c32);
|
|
60259
|
+
pushUint(out, BigInt(length), 4);
|
|
60260
|
+
}
|
|
60261
|
+
}
|
|
60262
|
+
function packValue(out, value) {
|
|
60263
|
+
if (value === null)
|
|
60264
|
+
out.push(192);
|
|
60265
|
+
else if (value === false)
|
|
60266
|
+
out.push(194);
|
|
60267
|
+
else if (value === true)
|
|
60268
|
+
out.push(195);
|
|
60269
|
+
else if (typeof value === "bigint")
|
|
60270
|
+
packInteger(out, value);
|
|
60271
|
+
else if (typeof value === "number") {
|
|
60272
|
+
if (!Number.isSafeInteger(value))
|
|
60273
|
+
throw new Error(`msgpack: ${value} is not a safe integer`);
|
|
60274
|
+
packInteger(out, BigInt(value));
|
|
60275
|
+
} else if (typeof value === "string") {
|
|
60276
|
+
const bytes = new TextEncoder().encode(value);
|
|
60277
|
+
packHeader(out, bytes.length, 160, 31, [217, 218, 219]);
|
|
60278
|
+
for (const b of bytes)
|
|
60279
|
+
out.push(b);
|
|
60280
|
+
} else if (Array.isArray(value)) {
|
|
60281
|
+
packHeader(out, value.length, 144, 15, [0, 220, 221]);
|
|
60282
|
+
for (const item of value)
|
|
60283
|
+
packValue(out, item);
|
|
60284
|
+
} else if (typeof value === "object") {
|
|
60285
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
60286
|
+
for (const [key] of entries) {
|
|
60287
|
+
if (/^(0|[1-9][0-9]*)$/.test(key))
|
|
60288
|
+
throw new Error(`msgpack: integer-like key "${key}"`);
|
|
60289
|
+
}
|
|
60290
|
+
packHeader(out, entries.length, 128, 15, [0, 222, 223]);
|
|
60291
|
+
for (const [key, item] of entries) {
|
|
60292
|
+
packValue(out, key);
|
|
60293
|
+
packValue(out, item);
|
|
60294
|
+
}
|
|
60295
|
+
} else {
|
|
60296
|
+
throw new Error(`msgpack: cannot encode a ${typeof value}`);
|
|
60297
|
+
}
|
|
60298
|
+
}
|
|
60299
|
+
function msgpackEncode(value) {
|
|
60300
|
+
const out = [];
|
|
60301
|
+
packValue(out, value);
|
|
60302
|
+
return Uint8Array.from(out);
|
|
60303
|
+
}
|
|
60304
|
+
function toHex(bytes) {
|
|
60305
|
+
let out = "";
|
|
60306
|
+
for (const b of bytes)
|
|
60307
|
+
out += b.toString(16).padStart(2, "0");
|
|
60308
|
+
return out;
|
|
60309
|
+
}
|
|
60310
|
+
function hyperliquidActionHash(action, nonce) {
|
|
60311
|
+
const n = BigInt(nonce);
|
|
60312
|
+
if (n < 0n || n > UINT64_MAX)
|
|
60313
|
+
throw new Error("hyperliquidActionHash: nonce out of range");
|
|
60314
|
+
const out = Array.from(msgpackEncode(action));
|
|
60315
|
+
pushUint(out, n, 8);
|
|
60316
|
+
out.push(0);
|
|
60317
|
+
return `0x${toHex(keccak_256(Uint8Array.from(out)))}`;
|
|
60318
|
+
}
|
|
60319
|
+
function hyperliquidL1TypedData(connectionId, network) {
|
|
60320
|
+
return {
|
|
60321
|
+
domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS },
|
|
60322
|
+
types: {
|
|
60323
|
+
Agent: [
|
|
60324
|
+
{ name: "source", type: "string" },
|
|
60325
|
+
{ name: "connectionId", type: "bytes32" }
|
|
60326
|
+
]
|
|
60327
|
+
},
|
|
60328
|
+
primary_type: "Agent",
|
|
60329
|
+
message: { source: network === "mainnet" ? "a" : "b", connectionId }
|
|
60330
|
+
};
|
|
60331
|
+
}
|
|
60332
|
+
function hyperliquidApproveBuilderFeeTypedData(builder, nonce) {
|
|
60333
|
+
return {
|
|
60334
|
+
domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS },
|
|
60335
|
+
types: {
|
|
60336
|
+
"HyperliquidTransaction:ApproveBuilderFee": [
|
|
60337
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
60338
|
+
{ name: "maxFeeRate", type: "string" },
|
|
60339
|
+
{ name: "builder", type: "address" },
|
|
60340
|
+
{ name: "nonce", type: "uint64" }
|
|
60341
|
+
]
|
|
60342
|
+
},
|
|
60343
|
+
primary_type: "HyperliquidTransaction:ApproveBuilderFee",
|
|
60344
|
+
message: {
|
|
60345
|
+
hyperliquidChain: "Mainnet",
|
|
60346
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
|
|
60347
|
+
builder: builder.toLowerCase(),
|
|
60348
|
+
nonce
|
|
60349
|
+
}
|
|
60350
|
+
};
|
|
60351
|
+
}
|
|
60352
|
+
function hyperliquidCanonicalJson(value) {
|
|
60353
|
+
return canonical(value);
|
|
60354
|
+
}
|
|
60355
|
+
function canonical(value) {
|
|
60356
|
+
if (value === null || typeof value !== "object")
|
|
60357
|
+
return JSON.stringify(value);
|
|
60358
|
+
if (Array.isArray(value))
|
|
60359
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
60360
|
+
const record = value;
|
|
60361
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
|
|
60362
|
+
}
|
|
60363
|
+
function sameValue(a, b) {
|
|
60364
|
+
return canonical(a) === canonical(b);
|
|
60365
|
+
}
|
|
60366
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
60367
|
+
function checkBuilderField(action, builder) {
|
|
60368
|
+
if (!("builder" in action) || action.builder === undefined)
|
|
60369
|
+
return null;
|
|
60370
|
+
const field = action.builder;
|
|
60371
|
+
if (typeof field !== "object" || field === null)
|
|
60372
|
+
return "builder field is not an object";
|
|
60373
|
+
if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
|
|
60374
|
+
return "the order names a builder that is not Candle's";
|
|
60375
|
+
}
|
|
60376
|
+
if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS) {
|
|
60377
|
+
return "the builder fee is outside Candle's 0.1% maximum";
|
|
60378
|
+
}
|
|
60379
|
+
return null;
|
|
60380
|
+
}
|
|
60381
|
+
function decimal(value) {
|
|
60382
|
+
if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
|
|
60383
|
+
return null;
|
|
60384
|
+
const negative = value.startsWith("-");
|
|
60385
|
+
const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
|
|
60386
|
+
const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
|
|
60387
|
+
return negative ? -n : n;
|
|
60388
|
+
}
|
|
60389
|
+
function checkActionIntent(action, intent) {
|
|
60390
|
+
const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
|
|
60391
|
+
const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
|
|
60392
|
+
if (!Array.isArray(orderList))
|
|
60393
|
+
return "invalid orders";
|
|
60394
|
+
for (const raw of orderList) {
|
|
60395
|
+
if (!raw || typeof raw !== "object")
|
|
60396
|
+
return "invalid order";
|
|
60397
|
+
const o = raw;
|
|
60398
|
+
if (!mainAsset(o.a))
|
|
60399
|
+
return "order asset is outside the main perp universe";
|
|
60400
|
+
if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal(o.s) ?? 0n) <= 0n || (decimal(o.p) ?? 0n) <= 0n)
|
|
60401
|
+
return "invalid order side, size, price or reduce-only flag";
|
|
60402
|
+
}
|
|
60403
|
+
if (action.type === "cancel") {
|
|
60404
|
+
if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
|
|
60405
|
+
return "invalid cancel asset";
|
|
60406
|
+
}
|
|
60407
|
+
if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
|
|
60408
|
+
return "invalid action asset";
|
|
60409
|
+
if (!intent)
|
|
60410
|
+
return null;
|
|
60411
|
+
const { method, params: p } = intent;
|
|
60412
|
+
const types2 = {
|
|
60413
|
+
setup: "approveBuilderFee",
|
|
60414
|
+
open: "order",
|
|
60415
|
+
close: "order",
|
|
60416
|
+
cancel: "cancel",
|
|
60417
|
+
modify: "modify",
|
|
60418
|
+
leverage: "updateLeverage",
|
|
60419
|
+
margin: "updateIsolatedMargin"
|
|
60420
|
+
};
|
|
60421
|
+
if (action.type !== types2[method])
|
|
60422
|
+
return "action type does not match the requested method";
|
|
60423
|
+
if (method === "leverage") {
|
|
60424
|
+
if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
|
|
60425
|
+
return "leverage or mode does not match the request";
|
|
60426
|
+
}
|
|
60427
|
+
if (method === "margin") {
|
|
60428
|
+
const amount = decimal(p.amount);
|
|
60429
|
+
if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
|
|
60430
|
+
return "isolated margin amount does not match the request";
|
|
60431
|
+
}
|
|
60432
|
+
if (method === "open" || method === "close" || method === "modify") {
|
|
60433
|
+
const list = orderList;
|
|
60434
|
+
const first = list[0];
|
|
60435
|
+
if (!first)
|
|
60436
|
+
return "missing requested order";
|
|
60437
|
+
const typ = first.t;
|
|
60438
|
+
if (!typ?.limit)
|
|
60439
|
+
return "requested order is not a limit/IOC order";
|
|
60440
|
+
if (method === "open") {
|
|
60441
|
+
const buy = p.side === "long" || p.side === "buy";
|
|
60442
|
+
if (first.b !== buy || first.r !== false || decimal(first.s) !== decimal(p.size))
|
|
60443
|
+
return "order side, size or reduce-only flag does not match the request";
|
|
60444
|
+
const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
|
|
60445
|
+
if (typ.limit.tif !== tif)
|
|
60446
|
+
return "order time in force does not match the request";
|
|
60447
|
+
const triggers = [
|
|
60448
|
+
["takeProfit", "tp"],
|
|
60449
|
+
["stopLoss", "sl"]
|
|
60450
|
+
].filter(([field]) => p[field] != null);
|
|
60451
|
+
if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
|
|
60452
|
+
return "unexpected extra orders or grouping";
|
|
60453
|
+
for (let i = 0;i < triggers.length; i++) {
|
|
60454
|
+
const [field, kind] = triggers[i];
|
|
60455
|
+
const leg = list[i + 1];
|
|
60456
|
+
if (!leg)
|
|
60457
|
+
return "missing trigger order";
|
|
60458
|
+
const trigger = leg.t?.trigger;
|
|
60459
|
+
if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal(leg.s) !== decimal(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal(trigger.triggerPx) !== decimal(p[field]))
|
|
60460
|
+
return "trigger side, size, reduce-only flag or price does not match the request";
|
|
60461
|
+
}
|
|
60462
|
+
} else {
|
|
60463
|
+
if (list.length !== 1)
|
|
60464
|
+
return "unexpected extra orders";
|
|
60465
|
+
if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
|
|
60466
|
+
return "close size or reduce-only flag does not match the request";
|
|
60467
|
+
if (method === "modify" && p.size != null && decimal(first.s) !== decimal(p.size))
|
|
60468
|
+
return "replacement size does not match the request";
|
|
60469
|
+
}
|
|
60470
|
+
if (p.price != null && decimal(first.p) !== decimal(p.price))
|
|
60471
|
+
return "order price does not match the request";
|
|
60472
|
+
}
|
|
60473
|
+
return null;
|
|
60474
|
+
}
|
|
60475
|
+
async function hyperliquidCloseOrder(fetcher, network, address, params) {
|
|
60476
|
+
const read = async (body) => {
|
|
60477
|
+
const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS[network].replace("/exchange", "/info"), {
|
|
60478
|
+
method: "POST",
|
|
60479
|
+
headers: { "content-type": "application/json" },
|
|
60480
|
+
body: JSON.stringify(body)
|
|
60481
|
+
});
|
|
60482
|
+
if (!res.ok)
|
|
60483
|
+
throw new Error("Cannot read the close position from Hyperliquid");
|
|
60484
|
+
return JSON.parse(await res.text());
|
|
60485
|
+
};
|
|
60486
|
+
const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
|
|
60487
|
+
const coin = String(params.coin).toUpperCase();
|
|
60488
|
+
const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
|
|
60489
|
+
const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
|
|
60490
|
+
const signed = decimal(szi);
|
|
60491
|
+
if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
|
|
60492
|
+
throw new Error("No main-universe position to close");
|
|
60493
|
+
const held = signed < 0n ? -signed : signed;
|
|
60494
|
+
const asked = params.size == null ? held : decimal(params.size);
|
|
60495
|
+
if (asked === null || asked <= 0n)
|
|
60496
|
+
throw new Error("Invalid close size");
|
|
60497
|
+
const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
|
|
60498
|
+
return { asset, isBuy: signed < 0n, size };
|
|
60499
|
+
}
|
|
60500
|
+
function verifyPerpsBuild(build, opts) {
|
|
60501
|
+
try {
|
|
60502
|
+
const network = opts.network ?? "mainnet";
|
|
60503
|
+
if (!EVM_ADDRESS_RE.test(opts.builder))
|
|
60504
|
+
return { ok: false, reason: "no valid Candle builder address to check against" };
|
|
60505
|
+
if (build.network !== undefined && build.network !== network) {
|
|
60506
|
+
return { ok: false, reason: `the build is for ${build.network}, this client trades ${network}` };
|
|
60507
|
+
}
|
|
60508
|
+
if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
|
|
60509
|
+
return { ok: false, reason: "the build is for a different wallet" };
|
|
60510
|
+
}
|
|
60511
|
+
if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
|
|
60512
|
+
return { ok: false, reason: "invalid nonce" };
|
|
60513
|
+
const typed = build.typedData;
|
|
60514
|
+
if (!typed || typeof typed !== "object")
|
|
60515
|
+
return { ok: false, reason: "no typed data" };
|
|
60516
|
+
const action = build.action;
|
|
60517
|
+
if (!action || typeof action !== "object")
|
|
60518
|
+
return { ok: false, reason: "no action" };
|
|
60519
|
+
const intentRefusal = checkActionIntent(action, opts.intent);
|
|
60520
|
+
if (intentRefusal)
|
|
60521
|
+
return { ok: false, reason: intentRefusal };
|
|
60522
|
+
if (opts.closeOrder) {
|
|
60523
|
+
const order = action.orders[0];
|
|
60524
|
+
if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
|
|
60525
|
+
return { ok: false, reason: "close side, asset or size does not match the requested position" };
|
|
60526
|
+
}
|
|
60527
|
+
if (typed.primary_type === "Agent") {
|
|
60528
|
+
const type = action.type;
|
|
60529
|
+
if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
|
|
60530
|
+
return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
|
|
60531
|
+
}
|
|
60532
|
+
if (type === "order") {
|
|
60533
|
+
const refusal = checkBuilderField(action, opts.builder);
|
|
60534
|
+
if (refusal)
|
|
60535
|
+
return { ok: false, reason: refusal };
|
|
60536
|
+
} else if ("builder" in action) {
|
|
60537
|
+
return { ok: false, reason: `a ${type} action carries no builder` };
|
|
60538
|
+
}
|
|
60539
|
+
if ("vaultAddress" in action)
|
|
60540
|
+
return { ok: false, reason: "the action names a vault" };
|
|
60541
|
+
const expected = hyperliquidL1TypedData(hyperliquidActionHash(action, build.nonce), network);
|
|
60542
|
+
if (!sameValue(typed, expected)) {
|
|
60543
|
+
return { ok: false, reason: "the typed data does not commit to this action and nonce" };
|
|
60544
|
+
}
|
|
60545
|
+
return { ok: true, kind: "l1" };
|
|
60546
|
+
}
|
|
60547
|
+
if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
|
|
60548
|
+
const expected = hyperliquidApproveBuilderFeeTypedData(opts.builder, build.nonce);
|
|
60549
|
+
if (!sameValue(typed, expected)) {
|
|
60550
|
+
return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
|
|
60551
|
+
}
|
|
60552
|
+
const expectedAction = {
|
|
60553
|
+
type: "approveBuilderFee",
|
|
60554
|
+
hyperliquidChain: "Mainnet",
|
|
60555
|
+
signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID,
|
|
60556
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
|
|
60557
|
+
builder: opts.builder.toLowerCase(),
|
|
60558
|
+
nonce: build.nonce
|
|
60559
|
+
};
|
|
60560
|
+
if (!sameValue(action, expectedAction)) {
|
|
60561
|
+
return { ok: false, reason: "the approval action does not match its typed data" };
|
|
60562
|
+
}
|
|
60563
|
+
return { ok: true, kind: "approveBuilderFee" };
|
|
60564
|
+
}
|
|
60565
|
+
return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
|
|
60566
|
+
} catch (err) {
|
|
60567
|
+
return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
|
|
60568
|
+
}
|
|
60569
|
+
}
|
|
60570
|
+
function hyperliquidSplitSignature(signature) {
|
|
60571
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
60572
|
+
throw new Error("Not a 65-byte signature");
|
|
60573
|
+
let v = Number.parseInt(signature.slice(130, 132), 16);
|
|
60574
|
+
if (v < 27)
|
|
60575
|
+
v += 27;
|
|
60576
|
+
return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
|
|
60577
|
+
}
|
|
60578
|
+
function hyperliquidExchangeBody(action, nonce, signature) {
|
|
60579
|
+
return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
|
|
60580
|
+
}
|
|
60581
|
+
function hyperliquidRelayBody(typedData) {
|
|
60582
|
+
return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
|
|
60583
|
+
}
|
|
60584
|
+
|
|
60585
|
+
// src/commands/perps.ts
|
|
60586
|
+
init_render();
|
|
60587
|
+
init_trading();
|
|
60588
|
+
var PERPS_SCOPE = "perps:write";
|
|
60589
|
+
var DEC_RE = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
|
|
60590
|
+
var CLOID_RE = /^0x[0-9a-fA-F]{32}$/;
|
|
60591
|
+
function usage4(ctx, line) {
|
|
60592
|
+
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
60593
|
+
return 2;
|
|
60594
|
+
}
|
|
60595
|
+
async function perpsRequest(ctx, key, path, body) {
|
|
60596
|
+
const result = await apiRequest(path, {
|
|
60597
|
+
apiUrl: ctx.apiUrl,
|
|
60598
|
+
credentials: { apiKey: key },
|
|
60599
|
+
auth: "key",
|
|
60600
|
+
method: body ? "POST" : "GET",
|
|
60601
|
+
body,
|
|
60602
|
+
fetch: ctx.deps.fetch,
|
|
60603
|
+
env: ctx.deps.env
|
|
60604
|
+
});
|
|
60605
|
+
if (!result.ok) {
|
|
60606
|
+
if (result.status === 404 && !result.code)
|
|
60607
|
+
throw new TradingError("PERPS_NOT_ENABLED", "This Candle deployment does not serve perps routes (HYPERLIQUID_ENABLED is off there, or the API predates them).");
|
|
60608
|
+
throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
|
|
60609
|
+
}
|
|
60610
|
+
if (!result.body || typeof result.body !== "object")
|
|
60611
|
+
throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
|
|
60612
|
+
return result.body;
|
|
60613
|
+
}
|
|
60614
|
+
function network(ctx) {
|
|
60615
|
+
return ctx.deps.env.CANDLE_HYPERLIQUID_NETWORK === "testnet" ? "testnet" : "mainnet";
|
|
60616
|
+
}
|
|
60617
|
+
async function builderPin(ctx, key) {
|
|
60618
|
+
const pinned = ctx.deps.env.CANDLE_HYPERLIQUID_BUILDER ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
|
|
60619
|
+
if (pinned) {
|
|
60620
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(pinned))
|
|
60621
|
+
throw new TradingError("INVALID_ADDRESS", "CANDLE_HYPERLIQUID_BUILDER must be an EVM address.");
|
|
60622
|
+
return pinned.toLowerCase();
|
|
60623
|
+
}
|
|
60624
|
+
const config = await perpsRequest(ctx, key, "/api/v1/agent/perps/config");
|
|
60625
|
+
if (typeof config.builder !== "string")
|
|
60626
|
+
throw new TradingError("PERPS_NOT_CONFIGURED", "The server reports no Hyperliquid builder address.");
|
|
60627
|
+
return config.builder.toLowerCase();
|
|
60628
|
+
}
|
|
60629
|
+
async function perpsWallet(ctx, key, name2, scope) {
|
|
60630
|
+
const { rows, appId, keyPrefix } = await listTradingWallets(ctx, key, scope);
|
|
60631
|
+
const evm = rows.filter((row2) => row2.chain === "evm");
|
|
60632
|
+
const matches = name2 === undefined ? evm : evm.filter((row2) => matchesName(row2, name2));
|
|
60633
|
+
if (matches.length !== 1) {
|
|
60634
|
+
throw new TradingError("TEE_WALLET_REQUIRED", evm.length === 0 ? "This key has no EVM TEE wallet bound to it. Hyperliquid perps sign only from one." : matches.length === 0 ? `No EVM TEE wallet on this key is called "${name2}". Bound to this key: ${evm.map(describeWallet).join("; ")}.` : `Name one EVM TEE wallet with --wallet: ${matches.map(describeWallet).join("; ")}.`);
|
|
60635
|
+
}
|
|
60636
|
+
const row = matches[0];
|
|
60637
|
+
if (scope === undefined)
|
|
60638
|
+
return { row };
|
|
60639
|
+
const wallet = await completeTradingWallet(ctx, row, appId, scope, "hood", {
|
|
60640
|
+
apiKey: key,
|
|
60641
|
+
...keyPrefix !== undefined ? { keyPrefix } : {}
|
|
60642
|
+
});
|
|
60643
|
+
return { row, wallet };
|
|
60644
|
+
}
|
|
60645
|
+
function typedDataAuthorization(wallet, typedData) {
|
|
60646
|
+
const body = hyperliquidRelayBody(typedData);
|
|
60647
|
+
const payload = hyperliquidCanonicalJson({
|
|
60648
|
+
body,
|
|
60649
|
+
headers: { "privy-app-id": wallet.appId },
|
|
60650
|
+
method: "POST",
|
|
60651
|
+
url: `https://api.privy.io/v1/wallets/${wallet.privyWalletId}/rpc`,
|
|
60652
|
+
version: 1
|
|
60653
|
+
});
|
|
60654
|
+
return { body, authorizationSignature: sign3("sha256", Buffer.from(payload), wallet.signer).toString("base64") };
|
|
60655
|
+
}
|
|
60656
|
+
function previewLines(build) {
|
|
60657
|
+
const action = build.action;
|
|
60658
|
+
if (action.type === "approveBuilderFee")
|
|
60659
|
+
return [`Approve Candle's builder ${safeText(action.builder)} at ${safeText(action.maxFeeRate)} (once).`];
|
|
60660
|
+
if (action.type === "cancel")
|
|
60661
|
+
return action.cancels.map((o) => `Cancel asset ${o.a} order ${o.o}.`);
|
|
60662
|
+
if (action.type === "updateLeverage")
|
|
60663
|
+
return [
|
|
60664
|
+
`Set asset ${safeText(action.asset)} to ${safeText(action.leverage)}x ${action.isCross ? "cross" : "isolated"}.`
|
|
60665
|
+
];
|
|
60666
|
+
const orders = action.type === "modify" ? [action.order] : action.orders;
|
|
60667
|
+
const lines = orders.map((o) => {
|
|
60668
|
+
const type = o.t;
|
|
60669
|
+
return `${action.type === "modify" ? "Modify to" : o.r ? "Close" : "Open"} ${o.b ? "buy" : "sell"} ${safeText(o.s)} asset ${safeText(o.a)} at ${safeText(o.p)}${type.limit ? ` (${safeText(type.limit.tif)})` : ` (${safeText(type.trigger?.tpsl)} trigger ${safeText(type.trigger?.triggerPx)})`}${o.r ? ", reduce-only" : ""}.`;
|
|
60670
|
+
});
|
|
60671
|
+
const builder = action.builder;
|
|
60672
|
+
lines.push(builder ? `Candle builder ${safeText(builder.b)} fee: ${builder.f / 10} bps.` : "Candle builder fee: none.");
|
|
60673
|
+
return lines;
|
|
60674
|
+
}
|
|
60675
|
+
async function confirm(ctx, build, address, yes) {
|
|
60676
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
60677
|
+
output.write(`Hyperliquid account: ${safeText(address)} (${safeText(build.network)})
|
|
60678
|
+
`);
|
|
60679
|
+
for (const line of previewLines(build))
|
|
60680
|
+
output.write(`${line}
|
|
60681
|
+
`);
|
|
60682
|
+
if (yes)
|
|
60683
|
+
return true;
|
|
60684
|
+
if (!ctx.deps.isTTY.stdin)
|
|
60685
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes.");
|
|
60686
|
+
return (await ctx.deps.promptLine("Sign and submit? [y/N] ")).trim().toLowerCase() === "y";
|
|
60687
|
+
}
|
|
60688
|
+
function venueRefusal(exchange) {
|
|
60689
|
+
if (!exchange || typeof exchange !== "object")
|
|
60690
|
+
return "Hyperliquid returned no answer.";
|
|
60691
|
+
const answer = exchange;
|
|
60692
|
+
if (answer.status !== "ok")
|
|
60693
|
+
return `Hyperliquid refused it: ${safeText(JSON.stringify(answer.response ?? answer))}`;
|
|
60694
|
+
const statuses = answer.response?.data?.statuses;
|
|
60695
|
+
for (const status of statuses ?? []) {
|
|
60696
|
+
if (status && typeof status === "object" && "error" in status)
|
|
60697
|
+
return `Hyperliquid refused the order: ${safeText(status.error)}`;
|
|
60698
|
+
}
|
|
60699
|
+
return null;
|
|
60700
|
+
}
|
|
60701
|
+
async function runPerpsWrite(ctx, opts) {
|
|
60702
|
+
const key = await tradingKey(ctx);
|
|
60703
|
+
const { wallet } = await perpsWallet(ctx, key, opts.wallet, PERPS_SCOPE);
|
|
60704
|
+
if (!wallet)
|
|
60705
|
+
throw new TradingError("TEE_WALLET_REQUIRED", "No EVM TEE wallet resolved.");
|
|
60706
|
+
const builder = await builderPin(ctx, key);
|
|
60707
|
+
const build = await perpsRequest(ctx, key, opts.path, { walletId: wallet.id, ...opts.body });
|
|
60708
|
+
if (build.ready === true)
|
|
60709
|
+
return printTradingResult(ctx, build);
|
|
60710
|
+
const net = network(ctx);
|
|
60711
|
+
const method = opts.path.split("/").at(-1);
|
|
60712
|
+
const check = verifyPerpsBuild(build, {
|
|
60713
|
+
builder,
|
|
60714
|
+
network: net,
|
|
60715
|
+
address: wallet.address,
|
|
60716
|
+
intent: { method, params: opts.body }
|
|
60717
|
+
});
|
|
60718
|
+
if (!check.ok)
|
|
60719
|
+
throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this build: ${check.reason}. Nothing was signed.`);
|
|
60720
|
+
if (method === "close") {
|
|
60721
|
+
const closeOrder = await hyperliquidCloseOrder(ctx.deps.fetch, net, wallet.address, opts.body);
|
|
60722
|
+
const closeCheck = verifyPerpsBuild(build, {
|
|
60723
|
+
builder,
|
|
60724
|
+
network: net,
|
|
60725
|
+
address: wallet.address,
|
|
60726
|
+
intent: { method, params: opts.body },
|
|
60727
|
+
closeOrder
|
|
60728
|
+
});
|
|
60729
|
+
if (!closeCheck.ok)
|
|
60730
|
+
throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
|
|
60731
|
+
}
|
|
60732
|
+
if (!opts.submit)
|
|
60733
|
+
return printTradingResult(ctx, { ...build, signed: false, submitted: false });
|
|
60734
|
+
if (!await confirm(ctx, build, wallet.address, opts.yes)) {
|
|
60735
|
+
throw new TradingError("CANCELLED", "Nothing was signed.");
|
|
60736
|
+
}
|
|
60737
|
+
const relay = await perpsRequest(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(wallet.id)}/sign`, typedDataAuthorization(wallet, build.typedData));
|
|
60738
|
+
const signature = relay.signature;
|
|
60739
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
60740
|
+
throw new TradingError("INVALID_RESPONSE", "The relay did not return a signature.");
|
|
60741
|
+
const exchangeBody = hyperliquidExchangeBody(build.action, build.nonce, signature);
|
|
60742
|
+
if (opts.signOnly)
|
|
60743
|
+
return printTradingResult(ctx, {
|
|
60744
|
+
success: true,
|
|
60745
|
+
perpOrderId: build.perpOrderId,
|
|
60746
|
+
kind: build.kind,
|
|
60747
|
+
network: build.network,
|
|
60748
|
+
address: wallet.address,
|
|
60749
|
+
nonce: build.nonce,
|
|
60750
|
+
signature,
|
|
60751
|
+
submitted: false,
|
|
60752
|
+
exchangeBody
|
|
60753
|
+
});
|
|
60754
|
+
let exchange;
|
|
60755
|
+
try {
|
|
60756
|
+
const res = await ctx.deps.fetch(HYPERLIQUID_EXCHANGE_URLS[net], {
|
|
60757
|
+
method: "POST",
|
|
60758
|
+
headers: { "content-type": "application/json" },
|
|
60759
|
+
body: JSON.stringify(exchangeBody)
|
|
60760
|
+
});
|
|
60761
|
+
const text = await res.text();
|
|
60762
|
+
if (!res.ok)
|
|
60763
|
+
throw new Error(`HTTP ${res.status}`);
|
|
60764
|
+
exchange = JSON.parse(text);
|
|
60765
|
+
} catch (error) {
|
|
60766
|
+
throw new TradingError("PERPS_SUBMIT_FAILED", `Signed, but the submit to Hyperliquid failed (${error instanceof Error ? error.message : "unknown"}). The signed action is in details.exchangeBody; nothing was lost.`, { exitCode: 3, details: { perpOrderId: build.perpOrderId, exchangeBody } });
|
|
60767
|
+
}
|
|
60768
|
+
const refusal = venueRefusal(exchange);
|
|
60769
|
+
if (refusal)
|
|
60770
|
+
throw new TradingError("PERPS_VENUE_REFUSED", refusal, {
|
|
60771
|
+
details: { perpOrderId: build.perpOrderId, exchange }
|
|
60772
|
+
});
|
|
60773
|
+
return printTradingResult(ctx, {
|
|
60774
|
+
success: true,
|
|
60775
|
+
perpOrderId: build.perpOrderId,
|
|
60776
|
+
kind: build.kind,
|
|
60777
|
+
network: build.network,
|
|
60778
|
+
address: wallet.address,
|
|
60779
|
+
...build.cloid ? { cloid: build.cloid } : {},
|
|
60780
|
+
...build.childCloids ? { childCloids: build.childCloids } : {},
|
|
60781
|
+
nonce: build.nonce,
|
|
60782
|
+
signature,
|
|
60783
|
+
exchange
|
|
60784
|
+
});
|
|
60785
|
+
}
|
|
60786
|
+
var WRITE_FLAGS = { valueFlags: ["--wallet"], booleanFlags: ["--yes", "--no-submit", "--sign-only"] };
|
|
60787
|
+
function sharedWrite(values, booleans) {
|
|
60788
|
+
return {
|
|
60789
|
+
...values["--wallet"] !== undefined ? { wallet: values["--wallet"] } : {},
|
|
60790
|
+
yes: booleans.has("--yes"),
|
|
60791
|
+
submit: !booleans.has("--no-submit"),
|
|
60792
|
+
signOnly: booleans.has("--sign-only")
|
|
60793
|
+
};
|
|
60794
|
+
}
|
|
60795
|
+
function slippage(value) {
|
|
60796
|
+
if (value === undefined)
|
|
60797
|
+
return;
|
|
60798
|
+
const n = Number(value);
|
|
60799
|
+
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
60800
|
+
}
|
|
60801
|
+
async function perpsSetup(args, ctx) {
|
|
60802
|
+
const parsed = parseArgs(args, WRITE_FLAGS);
|
|
60803
|
+
if ("error" in parsed)
|
|
60804
|
+
return usage4(ctx, parsed.error);
|
|
60805
|
+
if (parsed.positionals.length !== 0)
|
|
60806
|
+
return usage4(ctx, "Usage: candle perps setup [--wallet <tee>] [--yes]");
|
|
60807
|
+
try {
|
|
60808
|
+
return await runPerpsWrite(ctx, {
|
|
60809
|
+
path: "/api/v1/agent/perps/setup",
|
|
60810
|
+
body: {},
|
|
60811
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
60812
|
+
});
|
|
60813
|
+
} catch (error) {
|
|
60814
|
+
return tradingFailure(ctx, error);
|
|
60815
|
+
}
|
|
60816
|
+
}
|
|
60817
|
+
var OPEN_USAGE = "Usage: candle perps open <coin> <long|short> <size> [--price <px>] [--tif Gtc|Alo|Ioc] [--slippage-bps <n>] [--tp <px>] [--sl <px>] [--wallet <tee>] [--yes]";
|
|
60818
|
+
async function perpsOpen(args, ctx) {
|
|
60819
|
+
const parsed = parseArgs(args, {
|
|
60820
|
+
valueFlags: ["--wallet", "--price", "--tif", "--slippage-bps", "--tp", "--sl"],
|
|
60821
|
+
booleanFlags: ["--yes", "--no-submit", "--sign-only"]
|
|
60822
|
+
});
|
|
60823
|
+
if ("error" in parsed)
|
|
60824
|
+
return usage4(ctx, parsed.error);
|
|
60825
|
+
const [coin, side, size] = parsed.positionals;
|
|
60826
|
+
if (parsed.positionals.length !== 3 || !coin || !side || !size)
|
|
60827
|
+
return usage4(ctx, OPEN_USAGE);
|
|
60828
|
+
if (side !== "long" && side !== "short")
|
|
60829
|
+
return usage4(ctx, OPEN_USAGE);
|
|
60830
|
+
if (!DEC_RE.test(size))
|
|
60831
|
+
return usage4(ctx, "The size must be a plain decimal, e.g. 0.01.");
|
|
60832
|
+
const { values } = parsed;
|
|
60833
|
+
for (const flag of ["--price", "--tp", "--sl"]) {
|
|
60834
|
+
const value = values[flag];
|
|
60835
|
+
if (value !== undefined && !DEC_RE.test(value))
|
|
60836
|
+
return usage4(ctx, `${flag} must be a plain decimal.`);
|
|
60837
|
+
}
|
|
60838
|
+
const tif = values["--tif"];
|
|
60839
|
+
if (tif !== undefined && !["Gtc", "Alo", "Ioc"].includes(tif))
|
|
60840
|
+
return usage4(ctx, "--tif must be Gtc, Alo or Ioc.");
|
|
60841
|
+
if (tif !== undefined && tif !== "Ioc" && values["--price"] === undefined)
|
|
60842
|
+
return usage4(ctx, "--tif Gtc or Alo needs --price.");
|
|
60843
|
+
const slippageBps = slippage(values["--slippage-bps"]);
|
|
60844
|
+
if (slippageBps === null)
|
|
60845
|
+
return usage4(ctx, "--slippage-bps must be a whole number of at least 1.");
|
|
60846
|
+
const body = {
|
|
60847
|
+
coin,
|
|
60848
|
+
side,
|
|
60849
|
+
size,
|
|
60850
|
+
type: values["--price"] === undefined ? "market" : "limit",
|
|
60851
|
+
...values["--price"] !== undefined ? { price: values["--price"] } : {},
|
|
60852
|
+
...tif !== undefined && values["--price"] !== undefined ? { tif } : {},
|
|
60853
|
+
...slippageBps !== undefined ? { slippageBps } : {},
|
|
60854
|
+
...values["--tp"] !== undefined ? { takeProfit: values["--tp"] } : {},
|
|
60855
|
+
...values["--sl"] !== undefined ? { stopLoss: values["--sl"] } : {}
|
|
60856
|
+
};
|
|
60857
|
+
try {
|
|
60858
|
+
return await runPerpsWrite(ctx, { path: "/api/v1/agent/perps/open", body, ...sharedWrite(values, parsed.booleans) });
|
|
60859
|
+
} catch (error) {
|
|
60860
|
+
return tradingFailure(ctx, error);
|
|
60861
|
+
}
|
|
60862
|
+
}
|
|
60863
|
+
async function perpsClose(args, ctx) {
|
|
60864
|
+
const parsed = parseArgs(args, {
|
|
60865
|
+
valueFlags: ["--wallet", "--size", "--slippage-bps"],
|
|
60866
|
+
booleanFlags: ["--yes", "--no-submit", "--sign-only"]
|
|
60867
|
+
});
|
|
60868
|
+
if ("error" in parsed)
|
|
60869
|
+
return usage4(ctx, parsed.error);
|
|
60870
|
+
const [coin] = parsed.positionals;
|
|
60871
|
+
if (parsed.positionals.length !== 1 || !coin)
|
|
60872
|
+
return usage4(ctx, "Usage: candle perps close <coin> [--size <n>] [--slippage-bps <n>] [--wallet <tee>] [--yes]");
|
|
60873
|
+
const size = parsed.values["--size"];
|
|
60874
|
+
if (size !== undefined && !DEC_RE.test(size))
|
|
60875
|
+
return usage4(ctx, "--size must be a plain decimal.");
|
|
60876
|
+
const slippageBps = slippage(parsed.values["--slippage-bps"]);
|
|
60877
|
+
if (slippageBps === null)
|
|
60878
|
+
return usage4(ctx, "--slippage-bps must be a whole number of at least 1.");
|
|
60879
|
+
const body = {
|
|
60880
|
+
coin,
|
|
60881
|
+
...size !== undefined ? { size } : {},
|
|
60882
|
+
...slippageBps !== undefined ? { slippageBps } : {}
|
|
60883
|
+
};
|
|
60884
|
+
try {
|
|
60885
|
+
return await runPerpsWrite(ctx, {
|
|
60886
|
+
path: "/api/v1/agent/perps/close",
|
|
60887
|
+
body,
|
|
60888
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
60889
|
+
});
|
|
60890
|
+
} catch (error) {
|
|
60891
|
+
return tradingFailure(ctx, error);
|
|
60892
|
+
}
|
|
60893
|
+
}
|
|
60894
|
+
async function perpsCancel(args, ctx) {
|
|
60895
|
+
const parsed = parseArgs(args, WRITE_FLAGS);
|
|
60896
|
+
if ("error" in parsed)
|
|
60897
|
+
return usage4(ctx, parsed.error);
|
|
60898
|
+
const [cloid] = parsed.positionals;
|
|
60899
|
+
if (parsed.positionals.length !== 1 || !cloid || !CLOID_RE.test(cloid))
|
|
60900
|
+
return usage4(ctx, "Usage: candle perps cancel <cloid> [--wallet <tee>] [--yes] (cloid: 0x + 32 hex)");
|
|
60901
|
+
try {
|
|
60902
|
+
return await runPerpsWrite(ctx, {
|
|
60903
|
+
path: "/api/v1/agent/perps/cancel",
|
|
60904
|
+
body: { cloid: cloid.toLowerCase() },
|
|
60905
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
60906
|
+
});
|
|
60907
|
+
} catch (error) {
|
|
60908
|
+
return tradingFailure(ctx, error);
|
|
60909
|
+
}
|
|
60910
|
+
}
|
|
60911
|
+
async function perpsLeverage(args, ctx) {
|
|
60912
|
+
const parsed = parseArgs(args, {
|
|
60913
|
+
valueFlags: ["--wallet"],
|
|
60914
|
+
booleanFlags: ["--yes", "--no-submit", "--sign-only", "--isolated"]
|
|
60915
|
+
});
|
|
60916
|
+
if ("error" in parsed)
|
|
60917
|
+
return usage4(ctx, parsed.error);
|
|
60918
|
+
const [coin, leverage] = parsed.positionals;
|
|
60919
|
+
const n = Number(leverage);
|
|
60920
|
+
if (parsed.positionals.length !== 2 || !coin || !Number.isInteger(n) || n < 1)
|
|
60921
|
+
return usage4(ctx, "Usage: candle perps leverage <coin> <x> [--isolated] [--wallet <tee>] [--yes]");
|
|
60922
|
+
try {
|
|
60923
|
+
return await runPerpsWrite(ctx, {
|
|
60924
|
+
path: "/api/v1/agent/perps/leverage",
|
|
60925
|
+
body: { coin, leverage: n, mode: parsed.booleans.has("--isolated") ? "isolated" : "cross" },
|
|
60926
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
60927
|
+
});
|
|
60928
|
+
} catch (error) {
|
|
60929
|
+
return tradingFailure(ctx, error);
|
|
60930
|
+
}
|
|
60931
|
+
}
|
|
60932
|
+
async function perpsPositions(args, ctx) {
|
|
60933
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
|
|
60934
|
+
if ("error" in parsed)
|
|
60935
|
+
return usage4(ctx, parsed.error);
|
|
60936
|
+
if (parsed.positionals.length !== 0)
|
|
60937
|
+
return usage4(ctx, "Usage: candle perps positions [--wallet <tee>]");
|
|
60938
|
+
try {
|
|
60939
|
+
const key = await tradingKey(ctx);
|
|
60940
|
+
const { row } = await perpsWallet(ctx, key, parsed.values["--wallet"]);
|
|
60941
|
+
const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(row.id)}`);
|
|
60942
|
+
if (ctx.json)
|
|
60943
|
+
return printTradingResult(ctx, result);
|
|
60944
|
+
const out = ctx.deps.stdout;
|
|
60945
|
+
const account = result.account ?? {};
|
|
60946
|
+
out.write(`${safeText(result.address)} (${safeText(result.network)}, ${safeText(result.mode)} mode): account value ${safeText(account.accountValue)} USD, withdrawable ${safeText(result.withdrawable)} USD
|
|
60947
|
+
`);
|
|
60948
|
+
const positions = result.positions ?? [];
|
|
60949
|
+
if (positions.length === 0)
|
|
60950
|
+
out.write(`No open positions.
|
|
60951
|
+
`);
|
|
60952
|
+
for (const p of positions) {
|
|
60953
|
+
const leverage = p.leverage ?? {};
|
|
60954
|
+
out.write(` ${safeText(p.coin)} ${safeText(p.szi)} at ${safeText(p.entryPx)}, value ${safeText(p.positionValue)}, uPnL ${safeText(p.unrealizedPnl)}, ${safeText(leverage.value)}x ${safeText(leverage.type)}, liq ${safeText(p.liquidationPx ?? "none")}
|
|
60955
|
+
`);
|
|
60956
|
+
}
|
|
60957
|
+
return 0;
|
|
60958
|
+
} catch (error) {
|
|
60959
|
+
return tradingFailure(ctx, error);
|
|
60960
|
+
}
|
|
60961
|
+
}
|
|
60962
|
+
async function perpsOrders(args, ctx) {
|
|
60963
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallet", "--limit"] });
|
|
60964
|
+
if ("error" in parsed)
|
|
60965
|
+
return usage4(ctx, parsed.error);
|
|
60966
|
+
const limit = parsed.values["--limit"];
|
|
60967
|
+
if (parsed.positionals.length !== 0 || limit !== undefined && !/^[1-9][0-9]{0,2}$/.test(limit))
|
|
60968
|
+
return usage4(ctx, "Usage: candle perps orders [--wallet <tee>] [--limit <n>]");
|
|
60969
|
+
try {
|
|
60970
|
+
const key = await tradingKey(ctx);
|
|
60971
|
+
const { row } = await perpsWallet(ctx, key, parsed.values["--wallet"]);
|
|
60972
|
+
const query = new URLSearchParams({ walletId: row.id });
|
|
60973
|
+
if (limit !== undefined)
|
|
60974
|
+
query.set("limit", limit);
|
|
60975
|
+
const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/orders?${query}`);
|
|
60976
|
+
if (ctx.json)
|
|
60977
|
+
return printTradingResult(ctx, result);
|
|
60978
|
+
const out = ctx.deps.stdout;
|
|
60979
|
+
const open4 = result.open ?? [];
|
|
60980
|
+
out.write(`Open on Hyperliquid (${open4.length}):
|
|
60981
|
+
`);
|
|
60982
|
+
for (const o of open4) {
|
|
60983
|
+
out.write(` ${safeText(o.coin)} ${o.side === "B" ? "buy" : "sell"} ${safeText(o.sz)} at ${safeText(o.limitPx)}${o.reduceOnly ? " reduce-only" : ""}${o.isTrigger ? ` trigger ${safeText(o.triggerPx)}` : ""}, cloid ${safeText(o.cloid ?? "none")}
|
|
60984
|
+
`);
|
|
60985
|
+
}
|
|
60986
|
+
const recorded = result.recorded ?? [];
|
|
60987
|
+
out.write(`Built by Candle (${recorded.length}, newest first):
|
|
60988
|
+
`);
|
|
60989
|
+
for (const r of recorded) {
|
|
60990
|
+
out.write(` ${safeText(r.kind)} ${safeText(r.coin ?? "")} ${safeText(r.status)}${r.venueStatus ? ` (${safeText(r.venueStatus)})` : ""}${r.cloid ? `, cloid ${safeText(r.cloid)}` : ""}${r.targetCloid ? `, target ${safeText(r.targetCloid)}` : ""}
|
|
60991
|
+
`);
|
|
60992
|
+
}
|
|
60993
|
+
return 0;
|
|
60994
|
+
} catch (error) {
|
|
60995
|
+
return tradingFailure(ctx, error);
|
|
60996
|
+
}
|
|
60997
|
+
}
|
|
60998
|
+
|
|
59067
60999
|
// src/commands/plugins.ts
|
|
59068
61000
|
init_args();
|
|
59069
61001
|
|
|
@@ -59257,20 +61189,20 @@ async function writeSecretNames(deps, profile, names) {
|
|
|
59257
61189
|
else
|
|
59258
61190
|
await deps.updateProfile(profile, { secretNames: sorted });
|
|
59259
61191
|
}
|
|
59260
|
-
function
|
|
61192
|
+
function usage5(ctx, line) {
|
|
59261
61193
|
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
59262
61194
|
return 2;
|
|
59263
61195
|
}
|
|
59264
61196
|
async function secretsSet(args, ctx) {
|
|
59265
61197
|
const parsed = parseArgs(args, {});
|
|
59266
61198
|
if ("error" in parsed)
|
|
59267
|
-
return
|
|
61199
|
+
return usage5(ctx, parsed.error);
|
|
59268
61200
|
const [raw, extra] = parsed.positionals;
|
|
59269
61201
|
if (!raw || extra !== undefined)
|
|
59270
|
-
return
|
|
61202
|
+
return usage5(ctx, "Usage: candle secrets set <name>");
|
|
59271
61203
|
const name2 = canonicalSecretName(raw);
|
|
59272
61204
|
if (name2 === undefined) {
|
|
59273
|
-
return
|
|
61205
|
+
return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
59274
61206
|
}
|
|
59275
61207
|
if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
|
|
59276
61208
|
writeLocalFailure(ctx.deps, {
|
|
@@ -59282,7 +61214,7 @@ async function secretsSet(args, ctx) {
|
|
|
59282
61214
|
}
|
|
59283
61215
|
const value = await ctx.deps.promptSecret(`Value for ${name2} (input hidden): `);
|
|
59284
61216
|
if (value.length === 0)
|
|
59285
|
-
return
|
|
61217
|
+
return usage5(ctx, "An empty value was typed; nothing was stored.");
|
|
59286
61218
|
try {
|
|
59287
61219
|
await ctx.deps.secretsStore.set(secretRef(ctx.profile, name2), value);
|
|
59288
61220
|
} catch (error) {
|
|
@@ -59302,9 +61234,9 @@ async function secretsSet(args, ctx) {
|
|
|
59302
61234
|
async function secretsList(args, ctx) {
|
|
59303
61235
|
const parsed = parseArgs(args, {});
|
|
59304
61236
|
if ("error" in parsed)
|
|
59305
|
-
return
|
|
61237
|
+
return usage5(ctx, parsed.error);
|
|
59306
61238
|
if (parsed.positionals.length > 0)
|
|
59307
|
-
return
|
|
61239
|
+
return usage5(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
59308
61240
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
59309
61241
|
if (ctx.json) {
|
|
59310
61242
|
ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
|
|
@@ -59323,13 +61255,13 @@ async function secretsList(args, ctx) {
|
|
|
59323
61255
|
async function secretsRemove(args, ctx) {
|
|
59324
61256
|
const parsed = parseArgs(args, {});
|
|
59325
61257
|
if ("error" in parsed)
|
|
59326
|
-
return
|
|
61258
|
+
return usage5(ctx, parsed.error);
|
|
59327
61259
|
const [raw, extra] = parsed.positionals;
|
|
59328
61260
|
if (!raw || extra !== undefined)
|
|
59329
|
-
return
|
|
61261
|
+
return usage5(ctx, "Usage: candle secrets remove <name>");
|
|
59330
61262
|
const name2 = canonicalSecretName(raw);
|
|
59331
61263
|
if (name2 === undefined) {
|
|
59332
|
-
return
|
|
61264
|
+
return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
59333
61265
|
}
|
|
59334
61266
|
await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name2));
|
|
59335
61267
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
@@ -59455,11 +61387,11 @@ init_render();
|
|
|
59455
61387
|
function formatUsd(value) {
|
|
59456
61388
|
if (!Number.isFinite(value))
|
|
59457
61389
|
return "?";
|
|
59458
|
-
const
|
|
61390
|
+
const sign4 = value < 0 ? "-" : "";
|
|
59459
61391
|
const abs = Math.abs(value);
|
|
59460
61392
|
if (abs > 0 && abs < 0.005)
|
|
59461
|
-
return `${
|
|
59462
|
-
return `${
|
|
61393
|
+
return `${sign4}<$0.01`;
|
|
61394
|
+
return `${sign4}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
59463
61395
|
}
|
|
59464
61396
|
function formatPrice(value) {
|
|
59465
61397
|
if (!Number.isFinite(value) || value <= 0)
|
|
@@ -59486,6 +61418,8 @@ function shortAddress2(address) {
|
|
|
59486
61418
|
}
|
|
59487
61419
|
|
|
59488
61420
|
// src/commands/pnl.ts
|
|
61421
|
+
var CHAINS = ["solana", "hood"];
|
|
61422
|
+
var CHAIN_TITLES = { solana: "Solana", hood: "Hood" };
|
|
59489
61423
|
var NO_API_KEY4 = {
|
|
59490
61424
|
code: "NO_API_KEY",
|
|
59491
61425
|
message: "No API key for this profile.",
|
|
@@ -59553,6 +61487,7 @@ async function pnl(args, ctx) {
|
|
|
59553
61487
|
deps.stdout.write(`P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
|
|
59554
61488
|
|
|
59555
61489
|
`);
|
|
61490
|
+
writeChainBlocks(ctx, p.byChain);
|
|
59556
61491
|
writeSummary(ctx, {
|
|
59557
61492
|
realizedNetUsd: p.realizedNetUsd,
|
|
59558
61493
|
realizedGrossUsd: p.realizedGrossUsd,
|
|
@@ -59569,6 +61504,7 @@ async function pnl(args, ctx) {
|
|
|
59569
61504
|
lp
|
|
59570
61505
|
});
|
|
59571
61506
|
writePositions(ctx, p.openPositions, false);
|
|
61507
|
+
writeWallets(ctx, p.byWallet);
|
|
59572
61508
|
writeLpPositions(ctx, lp, false);
|
|
59573
61509
|
return 0;
|
|
59574
61510
|
}
|
|
@@ -59576,6 +61512,7 @@ async function pnl(args, ctx) {
|
|
|
59576
61512
|
deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
|
|
59577
61513
|
|
|
59578
61514
|
`);
|
|
61515
|
+
writeChainBlocks(ctx, { solana: books.solana, hood: books.hood });
|
|
59579
61516
|
writeSummary(ctx, {
|
|
59580
61517
|
...books.all,
|
|
59581
61518
|
positions: books.positions.length,
|
|
@@ -59589,6 +61526,68 @@ async function pnl(args, ctx) {
|
|
|
59589
61526
|
writeLpPositions(ctx, books.lp, true);
|
|
59590
61527
|
return 0;
|
|
59591
61528
|
}
|
|
61529
|
+
function isChainSummary(value) {
|
|
61530
|
+
return typeof value === "object" && value !== null && typeof value.realizedNetUsd === "number";
|
|
61531
|
+
}
|
|
61532
|
+
function writeChainBlocks(ctx, byChain) {
|
|
61533
|
+
const blocks = CHAINS.flatMap((chain2) => {
|
|
61534
|
+
const summary = byChain?.[chain2];
|
|
61535
|
+
return isChainSummary(summary) ? [[chain2, summary]] : [];
|
|
61536
|
+
});
|
|
61537
|
+
if (blocks.length === 0)
|
|
61538
|
+
return;
|
|
61539
|
+
for (const [chain2, s] of blocks) {
|
|
61540
|
+
const unmarked = s.unmarked ?? s.unmarkedPositions ?? 0;
|
|
61541
|
+
const marked = s.openPositions - unmarked;
|
|
61542
|
+
ctx.deps.stdout.write(`${CHAIN_TITLES[chain2]}
|
|
61543
|
+
`);
|
|
61544
|
+
writeLines(ctx, [
|
|
61545
|
+
[
|
|
61546
|
+
"Realized net",
|
|
61547
|
+
formatUsd(s.realizedNetUsd),
|
|
61548
|
+
`gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
|
|
61549
|
+
],
|
|
61550
|
+
[
|
|
61551
|
+
"Unrealized",
|
|
61552
|
+
formatUsd(s.unrealizedUsd),
|
|
61553
|
+
`${marked} of ${s.openPositions} open ${s.openPositions === 1 ? "position" : "positions"} marked${unmarked > 0 ? `; ${unmarked} unpriced, not counted` : ""}`
|
|
61554
|
+
],
|
|
61555
|
+
["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]
|
|
61556
|
+
]);
|
|
61557
|
+
if (s.unvalued > 0) {
|
|
61558
|
+
ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
|
|
61559
|
+
`);
|
|
61560
|
+
}
|
|
61561
|
+
ctx.deps.stdout.write(`
|
|
61562
|
+
`);
|
|
61563
|
+
}
|
|
61564
|
+
ctx.deps.stdout.write(`All chains
|
|
61565
|
+
`);
|
|
61566
|
+
}
|
|
61567
|
+
function writeLines(ctx, lines) {
|
|
61568
|
+
const width = Math.max(...lines.map(([label]) => label.length));
|
|
61569
|
+
const valueWidth = Math.max(...lines.map(([, value]) => value.length));
|
|
61570
|
+
for (const [label, value, note] of lines) {
|
|
61571
|
+
ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
|
|
61572
|
+
`);
|
|
61573
|
+
}
|
|
61574
|
+
}
|
|
61575
|
+
function writeWallets(ctx, wallets2) {
|
|
61576
|
+
if (!wallets2 || wallets2.length === 0)
|
|
61577
|
+
return;
|
|
61578
|
+
const rows = wallets2.map((w) => [
|
|
61579
|
+
`${w.label?.trim() ? `${w.label.trim()} ` : w.payerType === "main" ? "main " : ""}(${shortAddress2(w.wallet)})`,
|
|
61580
|
+
w.chain ?? "-",
|
|
61581
|
+
String(w.tradesConsidered),
|
|
61582
|
+
formatUsd(w.realizedNetUsd),
|
|
61583
|
+
formatUsd(w.unrealizedUsd),
|
|
61584
|
+
`${w.openPositions.length}${w.unmarkedPositions > 0 ? ` (${w.unmarkedPositions} unpriced)` : ""}`
|
|
61585
|
+
]);
|
|
61586
|
+
ctx.deps.stdout.write(`
|
|
61587
|
+
By wallet (each its own cost basis, so the rows need not sum to the total)
|
|
61588
|
+
${renderTable(["WALLET", "CHAIN", "TRADES", "REALIZED NET", "UNREALIZED", "OPEN"], rows.map((row) => row.map(terminalText)))}
|
|
61589
|
+
`);
|
|
61590
|
+
}
|
|
59592
61591
|
function writeSummary(ctx, s) {
|
|
59593
61592
|
const marked = s.positions - s.unmarked;
|
|
59594
61593
|
const lines = [
|
|
@@ -59628,12 +61627,7 @@ function writeSummary(ctx, s) {
|
|
|
59628
61627
|
} else {
|
|
59629
61628
|
lines.push(["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]);
|
|
59630
61629
|
}
|
|
59631
|
-
|
|
59632
|
-
const valueWidth = Math.max(...lines.map(([, value]) => value.length));
|
|
59633
|
-
for (const [label, value, note] of lines) {
|
|
59634
|
-
ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
|
|
59635
|
-
`);
|
|
59636
|
-
}
|
|
61630
|
+
writeLines(ctx, lines);
|
|
59637
61631
|
if (s.oldestMarkAt !== undefined) {
|
|
59638
61632
|
ctx.deps.stdout.write(`Marks as old as ${new Date(s.oldestMarkAt).toISOString()}.
|
|
59639
61633
|
`);
|
|
@@ -59694,12 +61688,13 @@ No open positions.
|
|
|
59694
61688
|
`);
|
|
59695
61689
|
return;
|
|
59696
61690
|
}
|
|
59697
|
-
const headers = ["TOKEN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
|
|
61691
|
+
const headers = ["TOKEN", "CHAIN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
|
|
59698
61692
|
if (withBook)
|
|
59699
61693
|
headers.push("BOOK");
|
|
59700
61694
|
const rows = positions.map((p) => {
|
|
59701
61695
|
const row = [
|
|
59702
61696
|
p.symbol?.trim() || shortAddress2(p.mint),
|
|
61697
|
+
p.chain ?? "-",
|
|
59703
61698
|
formatQuantity(p.quantity),
|
|
59704
61699
|
formatPrice(p.avgEntryUsd),
|
|
59705
61700
|
p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
|
|
@@ -59718,10 +61713,12 @@ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
|
|
|
59718
61713
|
// src/commands/portfolio.ts
|
|
59719
61714
|
init_args();
|
|
59720
61715
|
init_deps();
|
|
61716
|
+
init_evm_lite();
|
|
59721
61717
|
init_profiles();
|
|
59722
61718
|
init_render();
|
|
59723
61719
|
init_solana_endpoint();
|
|
59724
61720
|
init_solana_lite();
|
|
61721
|
+
init_errors();
|
|
59725
61722
|
init_store();
|
|
59726
61723
|
init_vault_support();
|
|
59727
61724
|
var SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
@@ -59732,31 +61729,74 @@ var KNOWN_SYMBOLS = {
|
|
|
59732
61729
|
};
|
|
59733
61730
|
var CHUNK = 100;
|
|
59734
61731
|
var TOKEN_READS_IN_FLIGHT = 8;
|
|
61732
|
+
var EVM_READS_IN_FLIGHT = 8;
|
|
61733
|
+
var CHAINS2 = ["solana", "hood"];
|
|
61734
|
+
var CHAIN_NAMES = { solana: "Solana", hood: "Hood" };
|
|
61735
|
+
var HOOD_NATIVE = "native";
|
|
61736
|
+
function hoodPriceKey(mint) {
|
|
61737
|
+
return mint === HOOD_NATIVE ? "hood:native" : `hood:${mint.toLowerCase()}`;
|
|
61738
|
+
}
|
|
61739
|
+
var HOOD_USDG = toChecksumAddress(HOOD_USDG_ADDRESS);
|
|
61740
|
+
var UNREAD_LABELS = {
|
|
61741
|
+
sol: "SOL",
|
|
61742
|
+
tokens: "tokens",
|
|
61743
|
+
eth: "ETH",
|
|
61744
|
+
usdg: "USDG",
|
|
61745
|
+
"hood-tokens": "tokens"
|
|
61746
|
+
};
|
|
59735
61747
|
var NO_API_KEY5 = {
|
|
59736
61748
|
code: "NO_API_KEY",
|
|
59737
61749
|
message: "No API key for this profile.",
|
|
59738
61750
|
suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
|
|
59739
61751
|
};
|
|
59740
61752
|
async function portfolio(args, ctx) {
|
|
59741
|
-
const parsed = parseArgs(args, {
|
|
61753
|
+
const parsed = parseArgs(args, {
|
|
61754
|
+
valueFlags: ["--rpc-url", "--evm-rpc-url", "--keystore", "--chain"],
|
|
61755
|
+
pathFlags: ["--keystore"]
|
|
61756
|
+
});
|
|
59742
61757
|
if ("error" in parsed)
|
|
59743
61758
|
return usage(ctx, parsed.error);
|
|
59744
61759
|
if (parsed.positionals.length > 0)
|
|
59745
61760
|
return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
59746
61761
|
const { deps } = ctx;
|
|
61762
|
+
const chainFlag = parsed.values["--chain"];
|
|
61763
|
+
if (chainFlag !== undefined && !CHAINS2.includes(chainFlag)) {
|
|
61764
|
+
return usage(ctx, `--chain must be solana or hood, not ${chainFlag}.`);
|
|
61765
|
+
}
|
|
61766
|
+
const chains = chainFlag === undefined ? [...CHAINS2] : [chainFlag];
|
|
61767
|
+
const showSolana = chains.includes("solana");
|
|
61768
|
+
const showHood = chains.includes("hood");
|
|
61769
|
+
if (!showSolana && parsed.values["--rpc-url"] !== undefined) {
|
|
61770
|
+
return usage(ctx, "--rpc-url is the Solana RPC and has no effect with --chain hood; use --evm-rpc-url.");
|
|
61771
|
+
}
|
|
61772
|
+
if (!showHood && parsed.values["--evm-rpc-url"] !== undefined) {
|
|
61773
|
+
return usage(ctx, "--evm-rpc-url is the Hood RPC and has no effect with --chain solana.");
|
|
61774
|
+
}
|
|
59747
61775
|
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
59748
61776
|
if (!apiKey) {
|
|
59749
61777
|
writeLocalFailure(deps, NO_API_KEY5, ctx.json);
|
|
59750
61778
|
return 1;
|
|
59751
61779
|
}
|
|
59752
|
-
|
|
59753
|
-
if (
|
|
59754
|
-
|
|
61780
|
+
let solana;
|
|
61781
|
+
if (showSolana) {
|
|
61782
|
+
const opened = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
|
|
61783
|
+
if ("error" in opened)
|
|
61784
|
+
return usage(ctx, opened.error);
|
|
61785
|
+
solana = opened;
|
|
61786
|
+
}
|
|
61787
|
+
let evmRpc;
|
|
61788
|
+
if (showHood) {
|
|
61789
|
+
const resolved = resolveEvmRpcUrl(parsed.values["--evm-rpc-url"], deps.env[EVM_RPC_URL_ENV], "--evm-rpc-url");
|
|
61790
|
+
if ("error" in resolved)
|
|
61791
|
+
return usage(ctx, resolved.error);
|
|
61792
|
+
evmRpc = resolved;
|
|
61793
|
+
}
|
|
59755
61794
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
59756
61795
|
if ("error" in resolvedVault)
|
|
59757
61796
|
return usage(ctx, resolvedVault.error);
|
|
59758
61797
|
return runVaultCommand(ctx, async ({ hold }) => {
|
|
59759
61798
|
let vaultEntries;
|
|
61799
|
+
let evmEntries;
|
|
59760
61800
|
let vaultReason;
|
|
59761
61801
|
const raw = await readVaultRaw(resolvedVault.path);
|
|
59762
61802
|
if (raw === null) {
|
|
@@ -59767,17 +61807,26 @@ async function portfolio(args, ctx) {
|
|
|
59767
61807
|
if (!requirePromptStreams(ctx, "portfolio"))
|
|
59768
61808
|
return 1;
|
|
59769
61809
|
const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
|
|
59770
|
-
vaultEntries = vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role }));
|
|
61810
|
+
vaultEntries = showSolana ? vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role })) : [];
|
|
61811
|
+
evmEntries = showHood ? vault.index.entries.filter((entry) => entry.chain === "evm" && entry.role === "vault").map((entry) => ({ address: entry.address, label: entry.label })) : [];
|
|
59771
61812
|
}
|
|
59772
61813
|
await printIdentity(ctx);
|
|
59773
|
-
const rpcHost = solana
|
|
59774
|
-
if (vaultEntries && vaultEntries.length > 0) {
|
|
61814
|
+
const rpcHost = solana?.endpoint.host;
|
|
61815
|
+
if (solana && vaultEntries && vaultEntries.length > 0) {
|
|
59775
61816
|
await solana.disclose();
|
|
59776
61817
|
const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
|
|
59777
61818
|
deps.stderr.write(`Reading ${vaultEntries.length} vault ${vaultEntries.length === 1 ? "address" : "addresses"} from ${rpcHost} in ${requests} requests. That endpoint sees them together; Candle sees none of them.
|
|
59778
61819
|
`);
|
|
59779
61820
|
}
|
|
59780
|
-
|
|
61821
|
+
let evmHost;
|
|
61822
|
+
if (evmRpc && evmEntries && evmEntries.length > 0) {
|
|
61823
|
+
evmHost = rpcHostOf2(evmRpc.url);
|
|
61824
|
+
const n = evmEntries.length;
|
|
61825
|
+
deps.stderr.write(`Reading ETH and USDG for ${n} EVM vault ${n === 1 ? "address" : "addresses"} from ${evmHost}${evmRpc.builtIn ? " (the built-in Hood RPC)" : ""} in ${1 + 2 * n} requests. That endpoint sees them together; Candle sees none of them.
|
|
61826
|
+
`);
|
|
61827
|
+
}
|
|
61828
|
+
const evmRead2 = evmRpc && evmEntries && evmEntries.length > 0 ? await startEvmRead(evmEntries.map((entry) => entry.address), evmRpc.url, deps.fetch) : undefined;
|
|
61829
|
+
const [candle, vaultRead, evmBalances] = await Promise.all([
|
|
59781
61830
|
apiRequest("/api/v1/agent/portfolio", {
|
|
59782
61831
|
auth: "key",
|
|
59783
61832
|
credentials: { apiKey },
|
|
@@ -59785,11 +61834,17 @@ async function portfolio(args, ctx) {
|
|
|
59785
61834
|
fetch: deps.fetch,
|
|
59786
61835
|
env: deps.env
|
|
59787
61836
|
}),
|
|
59788
|
-
vaultEntries ? readOwnRpc(vaultEntries.map((entry) => entry.address), solana, ctx) : Promise.resolve(undefined)
|
|
61837
|
+
solana && vaultEntries && vaultEntries.length > 0 ? readOwnRpc(vaultEntries.map((entry) => entry.address), solana, ctx) : Promise.resolve(undefined),
|
|
61838
|
+
evmRead2 ? evmRead2.balances() : Promise.resolve(undefined)
|
|
59789
61839
|
]);
|
|
59790
61840
|
if (vaultRead?.failure !== undefined) {
|
|
59791
61841
|
const n = vaultRead.unavailable.length;
|
|
59792
61842
|
deps.stderr.write(`${n} vault ${n === 1 ? "address" : "addresses"} could not be read: ${vaultRead.failure}.
|
|
61843
|
+
`);
|
|
61844
|
+
}
|
|
61845
|
+
if (evmBalances?.failure !== undefined) {
|
|
61846
|
+
const n = evmBalances.unavailable.length;
|
|
61847
|
+
deps.stderr.write(`${n} EVM vault ${n === 1 ? "address" : "addresses"} on Hood could not be read in full: ${terminalText(evmBalances.failure)}.
|
|
59793
61848
|
`);
|
|
59794
61849
|
}
|
|
59795
61850
|
if (!candle.ok) {
|
|
@@ -59798,6 +61853,7 @@ async function portfolio(args, ctx) {
|
|
|
59798
61853
|
}
|
|
59799
61854
|
const fromCandle = candle.body;
|
|
59800
61855
|
const prices = { ...fromCandle.prices ?? {} };
|
|
61856
|
+
const candleHood = showHood ? fromCandle.hood : undefined;
|
|
59801
61857
|
let priceFailure;
|
|
59802
61858
|
if (vaultRead) {
|
|
59803
61859
|
const wanted = new Set;
|
|
@@ -59824,84 +61880,244 @@ async function portfolio(args, ctx) {
|
|
|
59824
61880
|
priceFailure ??= priced.message;
|
|
59825
61881
|
}
|
|
59826
61882
|
}
|
|
61883
|
+
if (evmBalances) {
|
|
61884
|
+
const wanted = [];
|
|
61885
|
+
const held = [...evmBalances.byAddress.values()];
|
|
61886
|
+
if (held.some((b) => (b.wei ?? 0n) > 0n))
|
|
61887
|
+
wanted.push(HOOD_NATIVE);
|
|
61888
|
+
if (held.some((b) => (b.usdg ?? 0n) > 0n))
|
|
61889
|
+
wanted.push(HOOD_USDG);
|
|
61890
|
+
const missing = wanted.filter((asset) => prices[hoodPriceKey(asset)] === undefined);
|
|
61891
|
+
if (missing.length > 0) {
|
|
61892
|
+
const priced = await apiRequest("/api/v1/agent/prices", {
|
|
61893
|
+
method: "POST",
|
|
61894
|
+
body: { hood: missing },
|
|
61895
|
+
auth: "key",
|
|
61896
|
+
credentials: { apiKey },
|
|
61897
|
+
apiUrl: ctx.apiUrl,
|
|
61898
|
+
fetch: deps.fetch,
|
|
61899
|
+
env: deps.env
|
|
61900
|
+
});
|
|
61901
|
+
if (priced.ok)
|
|
61902
|
+
Object.assign(prices, priced.body.prices ?? {});
|
|
61903
|
+
else
|
|
61904
|
+
priceFailure ??= priced.message;
|
|
61905
|
+
}
|
|
61906
|
+
}
|
|
59827
61907
|
if (priceFailure !== undefined) {
|
|
59828
61908
|
deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
|
|
59829
61909
|
`);
|
|
59830
61910
|
}
|
|
61911
|
+
const lpSection = showSolana ? fromCandle.lp : undefined;
|
|
59831
61912
|
const lpByWallet = new Map;
|
|
59832
61913
|
const lpOf = (address) => {
|
|
59833
61914
|
const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
|
|
59834
61915
|
lpByWallet.set(address, entry);
|
|
59835
61916
|
return entry;
|
|
59836
61917
|
};
|
|
59837
|
-
for (const position of
|
|
61918
|
+
for (const position of lpSection?.positions ?? [])
|
|
59838
61919
|
lpOf(position.wallet).positions.push(lpRow(position, prices));
|
|
59839
|
-
for (const unread of
|
|
61920
|
+
for (const unread of lpSection?.unreadable ?? [])
|
|
59840
61921
|
lpOf(unread.wallet).unread.push(unread.position);
|
|
59841
|
-
const wallet = (address, read, extra) => {
|
|
59842
|
-
const
|
|
59843
|
-
const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
|
|
59844
|
-
const lp2 = fromCandle.lp ? lpOf(address) : undefined;
|
|
61922
|
+
const wallet = (chain2, address, read, extra) => {
|
|
61923
|
+
const lp2 = chain2 === "solana" && lpSection ? lpOf(address) : undefined;
|
|
59845
61924
|
return {
|
|
61925
|
+
chain: chain2,
|
|
59846
61926
|
address,
|
|
59847
61927
|
...extra,
|
|
59848
|
-
holdings,
|
|
59849
|
-
...unread.length > 0 ? { unread } : {},
|
|
61928
|
+
holdings: read.holdings,
|
|
61929
|
+
...read.unread.length > 0 ? { unread: read.unread } : {},
|
|
61930
|
+
...read.truncated ? { truncated: true } : {},
|
|
59850
61931
|
...lp2 ? { lpPositions: lp2.positions } : {},
|
|
59851
61932
|
...lp2 && lp2.unread.length > 0 ? { lpUnread: lp2.unread } : {},
|
|
59852
|
-
valueUsd: (holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
|
|
59853
|
-
unpriced: (holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
|
|
61933
|
+
valueUsd: (read.holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
|
|
61934
|
+
unpriced: (read.holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
|
|
59854
61935
|
};
|
|
59855
61936
|
};
|
|
61937
|
+
const subtotals = (wallets2) => Object.fromEntries(chains.map((chain2) => {
|
|
61938
|
+
const on = wallets2.filter((w) => w.chain === chain2);
|
|
61939
|
+
return [
|
|
61940
|
+
chain2,
|
|
61941
|
+
{
|
|
61942
|
+
valueUsd: on.reduce((sum, w) => sum + w.valueUsd, 0),
|
|
61943
|
+
unpriced: on.reduce((sum, w) => sum + w.unpriced, 0)
|
|
61944
|
+
}
|
|
61945
|
+
];
|
|
61946
|
+
}));
|
|
59856
61947
|
const group = (name2, wallets2, read, reason) => ({
|
|
59857
61948
|
group: name2,
|
|
59858
61949
|
read,
|
|
59859
61950
|
...reason !== undefined ? { reason } : {},
|
|
59860
61951
|
wallets: wallets2,
|
|
59861
61952
|
valueUsd: wallets2.reduce((sum, w) => sum + w.valueUsd, 0),
|
|
59862
|
-
unpriced: wallets2.reduce((sum, w) => sum + w.unpriced, 0)
|
|
61953
|
+
unpriced: wallets2.reduce((sum, w) => sum + w.unpriced, 0),
|
|
61954
|
+
byChain: subtotals(wallets2)
|
|
59863
61955
|
});
|
|
59864
|
-
const
|
|
61956
|
+
const solanaRows = (rows) => showSolana ? rows ?? [] : [];
|
|
59865
61957
|
const groups = [
|
|
59866
|
-
group("vault",
|
|
59867
|
-
|
|
59868
|
-
|
|
59869
|
-
|
|
59870
|
-
|
|
59871
|
-
|
|
59872
|
-
|
|
59873
|
-
|
|
59874
|
-
|
|
59875
|
-
|
|
61958
|
+
group("vault", [
|
|
61959
|
+
...(vaultEntries ?? []).map((entry) => wallet("solana", entry.address, solanaHoldings(vaultRead?.byAddress.get(entry.address), prices), {
|
|
61960
|
+
label: entry.label,
|
|
61961
|
+
role: entry.role
|
|
61962
|
+
})),
|
|
61963
|
+
...(evmEntries ?? []).map((entry) => {
|
|
61964
|
+
const read = evmBalances?.byAddress.get(entry.address);
|
|
61965
|
+
return wallet("hood", entry.address, hoodHoldings(read && {
|
|
61966
|
+
wei: read.wei === null ? null : read.wei.toString(),
|
|
61967
|
+
tokens: read.usdg === null ? null : read.usdg > 0n ? [
|
|
61968
|
+
{
|
|
61969
|
+
mint: HOOD_USDG,
|
|
61970
|
+
amountRaw: read.usdg.toString(),
|
|
61971
|
+
decimals: HOOD_USDG_DECIMALS,
|
|
61972
|
+
symbol: "USDG"
|
|
61973
|
+
}
|
|
61974
|
+
] : []
|
|
61975
|
+
}, prices, "usdg"), { label: entry.label, role: "vault" });
|
|
61976
|
+
})
|
|
61977
|
+
], vaultEntries !== undefined, vaultReason),
|
|
61978
|
+
group("tee", [
|
|
61979
|
+
...solanaRows(fromCandle.tee).map((row) => wallet("solana", row.address, solanaHoldings(row, prices), {
|
|
61980
|
+
id: row.id,
|
|
61981
|
+
...row.label ? { label: row.label } : {},
|
|
61982
|
+
active: row.active
|
|
61983
|
+
})),
|
|
61984
|
+
...(candleHood?.tee ?? []).map((row) => wallet("hood", row.address, hoodHoldings(row, prices, "hood-tokens"), {
|
|
61985
|
+
id: row.id,
|
|
61986
|
+
...row.label ? { label: row.label } : {},
|
|
61987
|
+
active: row.active
|
|
61988
|
+
}))
|
|
61989
|
+
], true),
|
|
61990
|
+
group("embedded", [
|
|
61991
|
+
...solanaRows(fromCandle.embedded).map((row) => wallet("solana", row.address, solanaHoldings(row, prices), {})),
|
|
61992
|
+
...(candleHood?.embedded ?? []).map((row) => wallet("hood", row.address, hoodHoldings(row, prices, "hood-tokens"), {}))
|
|
61993
|
+
], true)
|
|
59876
61994
|
];
|
|
59877
|
-
const
|
|
59878
|
-
|
|
59879
|
-
|
|
61995
|
+
const unavailableByChain = {
|
|
61996
|
+
solana: showSolana ? [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []] : [],
|
|
61997
|
+
hood: [...evmBalances?.unavailable ?? [], ...candleHood?.unavailable ?? []]
|
|
61998
|
+
};
|
|
61999
|
+
const unavailable = chains.flatMap((chain2) => unavailableByChain[chain2]);
|
|
62000
|
+
const lpUnread = (lpSection?.unreadable ?? []).length;
|
|
62001
|
+
const candleExplained = (fromCandle.unavailable ?? []).length > 0 || (fromCandle.hood?.unavailable ?? []).length > 0 || (fromCandle.lp?.unreadable ?? []).length > 0;
|
|
62002
|
+
const listCutOff = fromCandle.complete === false && !candleExplained;
|
|
62003
|
+
const complete = !listCutOff && unavailable.length === 0 && lpUnread === 0;
|
|
59880
62004
|
const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
|
|
59881
62005
|
const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
|
|
59882
|
-
const lp =
|
|
59883
|
-
positions:
|
|
59884
|
-
unpriced:
|
|
62006
|
+
const lp = lpSection ? {
|
|
62007
|
+
positions: lpSection.positions.length,
|
|
62008
|
+
unpriced: lpSection.positions.filter((p) => p.valueUsd === null).length,
|
|
59885
62009
|
unreadable: lpUnread,
|
|
59886
|
-
valueUsd:
|
|
62010
|
+
valueUsd: lpSection.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
|
|
59887
62011
|
} : undefined;
|
|
62012
|
+
const all = groups.flatMap((g) => g.wallets);
|
|
62013
|
+
const byChain = subtotals(all);
|
|
62014
|
+
for (const chain2 of chains) {
|
|
62015
|
+
const entry = byChain[chain2];
|
|
62016
|
+
if (!entry)
|
|
62017
|
+
continue;
|
|
62018
|
+
entry.wallets = all.filter((w) => w.chain === chain2).length;
|
|
62019
|
+
entry.unavailable = unavailableByChain[chain2].length;
|
|
62020
|
+
}
|
|
62021
|
+
const hoodTotals = byChain.hood;
|
|
62022
|
+
if (hoodTotals) {
|
|
62023
|
+
const reasons = { ...candleHood?.unpricedByReason ?? {} };
|
|
62024
|
+
let vaultUnpriced = 0;
|
|
62025
|
+
for (const w of groups[0]?.wallets ?? []) {
|
|
62026
|
+
if (w.chain !== "hood")
|
|
62027
|
+
continue;
|
|
62028
|
+
for (const h of w.holdings ?? []) {
|
|
62029
|
+
if (h.priceUsd !== null)
|
|
62030
|
+
continue;
|
|
62031
|
+
vaultUnpriced += 1;
|
|
62032
|
+
const reason = h.unpricedReason ?? "source-unavailable";
|
|
62033
|
+
reasons[reason] = (reasons[reason] ?? 0) + 1;
|
|
62034
|
+
}
|
|
62035
|
+
}
|
|
62036
|
+
if (candleHood)
|
|
62037
|
+
hoodTotals.unpriced = (candleHood.unpriced ?? 0) + vaultUnpriced;
|
|
62038
|
+
hoodTotals.unpricedByReason = reasons;
|
|
62039
|
+
}
|
|
59888
62040
|
if (ctx.json) {
|
|
59889
62041
|
writeJson(deps, {
|
|
59890
62042
|
ok: true,
|
|
62043
|
+
chains,
|
|
59891
62044
|
totalUsd,
|
|
59892
62045
|
unpriced,
|
|
59893
62046
|
complete,
|
|
59894
62047
|
unavailable,
|
|
59895
|
-
|
|
62048
|
+
unavailableByChain: Object.fromEntries(chains.map((chain2) => [chain2, unavailableByChain[chain2]])),
|
|
62049
|
+
rpcHost: rpcHost ?? null,
|
|
62050
|
+
...evmHost !== undefined ? { evmRpcHost: evmHost } : {},
|
|
59896
62051
|
...lp ? { lp } : {},
|
|
62052
|
+
byChain,
|
|
59897
62053
|
groups
|
|
59898
62054
|
});
|
|
59899
62055
|
return complete ? 0 : 3;
|
|
59900
62056
|
}
|
|
59901
|
-
writeTable(ctx, groups, {
|
|
62057
|
+
writeTable(ctx, groups, {
|
|
62058
|
+
totalUsd,
|
|
62059
|
+
unpriced,
|
|
62060
|
+
chains,
|
|
62061
|
+
byChain,
|
|
62062
|
+
unavailableByChain,
|
|
62063
|
+
listCutOff,
|
|
62064
|
+
lp
|
|
62065
|
+
});
|
|
59902
62066
|
return complete ? 0 : 3;
|
|
59903
62067
|
});
|
|
59904
62068
|
}
|
|
62069
|
+
async function startEvmRead(addresses, rpcUrl, fetchFn) {
|
|
62070
|
+
const rpc = createEvmRpc(rpcUrl, fetchFn);
|
|
62071
|
+
const host = rpcHostOf2(rpcUrl);
|
|
62072
|
+
const unique = [...new Set(addresses)];
|
|
62073
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
62074
|
+
let chainId;
|
|
62075
|
+
try {
|
|
62076
|
+
chainId = await rpc.chainId();
|
|
62077
|
+
} catch (error) {
|
|
62078
|
+
const failure = message(error);
|
|
62079
|
+
return {
|
|
62080
|
+
balances: async () => ({
|
|
62081
|
+
byAddress: new Map(unique.map((address) => [address, { wei: null, usdg: null }])),
|
|
62082
|
+
unavailable: unique,
|
|
62083
|
+
failure
|
|
62084
|
+
})
|
|
62085
|
+
};
|
|
62086
|
+
}
|
|
62087
|
+
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
62088
|
+
throw new VaultError("EVM_CHAIN_MISMATCH", `${host} answered chain id ${chainId}; candle portfolio reads EVM vault keys on Hood, chain id ${HOOD_CHAIN_ID}, only.`, {
|
|
62089
|
+
suggestion: "Nothing was read or priced. Point --evm-rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC, or pass --chain solana."
|
|
62090
|
+
});
|
|
62091
|
+
}
|
|
62092
|
+
return {
|
|
62093
|
+
balances: async () => {
|
|
62094
|
+
const byAddress = new Map;
|
|
62095
|
+
let failure;
|
|
62096
|
+
let next = 0;
|
|
62097
|
+
await Promise.all(Array.from({ length: Math.min(EVM_READS_IN_FLIGHT, unique.length) }, async () => {
|
|
62098
|
+
while (next < unique.length) {
|
|
62099
|
+
const address = unique[next++];
|
|
62100
|
+
const read = async (call) => {
|
|
62101
|
+
try {
|
|
62102
|
+
return await call();
|
|
62103
|
+
} catch (error) {
|
|
62104
|
+
failure ??= message(error);
|
|
62105
|
+
return null;
|
|
62106
|
+
}
|
|
62107
|
+
};
|
|
62108
|
+
const wei = await read(() => rpc.getBalance(address));
|
|
62109
|
+
const usdg = await read(() => rpc.erc20BalanceOf(HOOD_USDG, address));
|
|
62110
|
+
byAddress.set(address, { wei, usdg });
|
|
62111
|
+
}
|
|
62112
|
+
}));
|
|
62113
|
+
const unavailable = unique.filter((address) => {
|
|
62114
|
+
const read = byAddress.get(address);
|
|
62115
|
+
return read === undefined || read.wei === null || read.usdg === null;
|
|
62116
|
+
});
|
|
62117
|
+
return { byAddress, unavailable, ...failure === undefined ? {} : { failure } };
|
|
62118
|
+
}
|
|
62119
|
+
};
|
|
62120
|
+
}
|
|
59905
62121
|
async function readOwnRpc(addresses, solana, ctx) {
|
|
59906
62122
|
const { rpc } = solana;
|
|
59907
62123
|
const unique = [...new Set(addresses)];
|
|
@@ -59977,6 +62193,14 @@ async function readOwnRpc(addresses, solana, ctx) {
|
|
|
59977
62193
|
}
|
|
59978
62194
|
return { byAddress, unavailable, ...failure === undefined ? {} : { failure } };
|
|
59979
62195
|
}
|
|
62196
|
+
function solanaHoldings(read, prices) {
|
|
62197
|
+
if (read === undefined || read.lamports === null && read.tokens === null)
|
|
62198
|
+
return { holdings: null, unread: [] };
|
|
62199
|
+
return {
|
|
62200
|
+
holdings: valueHoldings(read, prices),
|
|
62201
|
+
unread: [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []]
|
|
62202
|
+
};
|
|
62203
|
+
}
|
|
59980
62204
|
function valueHoldings(read, prices) {
|
|
59981
62205
|
const raw = [];
|
|
59982
62206
|
if (read.lamports !== null && BigInt(read.lamports) > 0n) {
|
|
@@ -59989,6 +62213,7 @@ function valueHoldings(read, prices) {
|
|
|
59989
62213
|
const priceUsd = price?.priceUsd ?? null;
|
|
59990
62214
|
const amount = formatAmount(h.amountRaw, h.decimals);
|
|
59991
62215
|
return {
|
|
62216
|
+
chain: "solana",
|
|
59992
62217
|
...h,
|
|
59993
62218
|
symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
|
|
59994
62219
|
amount,
|
|
@@ -59998,6 +62223,46 @@ function valueHoldings(read, prices) {
|
|
|
59998
62223
|
};
|
|
59999
62224
|
});
|
|
60000
62225
|
}
|
|
62226
|
+
function hoodHoldings(read, prices, tokensPart) {
|
|
62227
|
+
if (read === undefined || read.wei === null && read.tokens === null)
|
|
62228
|
+
return { holdings: null, unread: [] };
|
|
62229
|
+
const raw = [];
|
|
62230
|
+
if (read.wei !== null && BigInt(read.wei) > 0n) {
|
|
62231
|
+
raw.push({ mint: HOOD_NATIVE, amountRaw: read.wei, decimals: NATIVE_DECIMALS, program: "native", symbol: "ETH" });
|
|
62232
|
+
}
|
|
62233
|
+
for (const token of read.tokens ?? []) {
|
|
62234
|
+
if (!/^\d+$/.test(token.amountRaw) || BigInt(token.amountRaw) === 0n)
|
|
62235
|
+
continue;
|
|
62236
|
+
raw.push({
|
|
62237
|
+
mint: token.mint,
|
|
62238
|
+
amountRaw: token.amountRaw,
|
|
62239
|
+
decimals: token.decimals,
|
|
62240
|
+
program: "erc20",
|
|
62241
|
+
...token.symbol ? { symbol: token.symbol } : {}
|
|
62242
|
+
});
|
|
62243
|
+
}
|
|
62244
|
+
const holdings = raw.map(({ symbol, ...h }) => {
|
|
62245
|
+
const price = prices[hoodPriceKey(h.mint)];
|
|
62246
|
+
const priceUsd = price?.priceUsd ?? null;
|
|
62247
|
+
const amount = formatAmount(h.amountRaw, h.decimals);
|
|
62248
|
+
const known = h.mint.toLowerCase() === HOOD_USDG_ADDRESS ? "USDG" : undefined;
|
|
62249
|
+
return {
|
|
62250
|
+
chain: "hood",
|
|
62251
|
+
...h,
|
|
62252
|
+
symbol: known ?? symbol ?? price?.symbol ?? null,
|
|
62253
|
+
amount,
|
|
62254
|
+
priceUsd,
|
|
62255
|
+
valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
|
|
62256
|
+
priceSource: price?.source ?? null,
|
|
62257
|
+
...priceUsd === null ? { unpricedReason: price?.unpricedReason ?? "source-unavailable" } : {}
|
|
62258
|
+
};
|
|
62259
|
+
});
|
|
62260
|
+
return {
|
|
62261
|
+
holdings,
|
|
62262
|
+
unread: [...read.wei === null ? ["eth"] : [], ...read.tokens === null ? [tokensPart] : []],
|
|
62263
|
+
...read.truncated ? { truncated: true } : {}
|
|
62264
|
+
};
|
|
62265
|
+
}
|
|
60001
62266
|
function lpRow(position, prices) {
|
|
60002
62267
|
return {
|
|
60003
62268
|
position: position.position,
|
|
@@ -60042,22 +62307,23 @@ function writeTable(ctx, groups, totals) {
|
|
|
60042
62307
|
lpRows.push([g.group, name2, shortAddress2(position), "-", "not read", "-", "-"]);
|
|
60043
62308
|
}
|
|
60044
62309
|
if (w.holdings === null) {
|
|
60045
|
-
rows.push([g.group, name2, "-", "not read", "-", "-"]);
|
|
62310
|
+
rows.push([g.group, w.chain, name2, "-", "not read", "-", "-"]);
|
|
60046
62311
|
continue;
|
|
60047
62312
|
}
|
|
60048
62313
|
const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
|
|
60049
62314
|
for (const h of sorted) {
|
|
60050
62315
|
rows.push([
|
|
60051
62316
|
g.group,
|
|
62317
|
+
w.chain,
|
|
60052
62318
|
name2,
|
|
60053
62319
|
h.symbol ?? shortAddress2(h.mint),
|
|
60054
62320
|
h.amount,
|
|
60055
62321
|
h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
|
|
60056
|
-
h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
|
|
62322
|
+
h.valueUsd === null ? h.unpricedReason ?? "-" : formatUsd(h.valueUsd)
|
|
60057
62323
|
]);
|
|
60058
62324
|
}
|
|
60059
62325
|
for (const part of w.unread ?? [])
|
|
60060
|
-
rows.push([g.group, name2, part
|
|
62326
|
+
rows.push([g.group, w.chain, name2, UNREAD_LABELS[part], "not read", "-", "-"]);
|
|
60061
62327
|
}
|
|
60062
62328
|
const empty = g.wallets.length - shown.length;
|
|
60063
62329
|
const count = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
|
|
@@ -60069,7 +62335,7 @@ function writeTable(ctx, groups, totals) {
|
|
|
60069
62335
|
}
|
|
60070
62336
|
if (rows.length > 0)
|
|
60071
62337
|
deps.stdout.write(`
|
|
60072
|
-
${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
|
|
62338
|
+
${renderTable(["GROUP", "CHAIN", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
|
|
60073
62339
|
`);
|
|
60074
62340
|
else
|
|
60075
62341
|
deps.stdout.write(`
|
|
@@ -60080,6 +62346,17 @@ Nothing held in any wallet read.
|
|
|
60080
62346
|
LP positions
|
|
60081
62347
|
${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
|
|
60082
62348
|
`);
|
|
62349
|
+
for (const chain2 of totals.chains) {
|
|
62350
|
+
const sub = totals.byChain[chain2];
|
|
62351
|
+
if (!sub)
|
|
62352
|
+
continue;
|
|
62353
|
+
const reasons = Object.entries(sub.unpricedByReason ?? {}).filter(([, n]) => (n ?? 0) > 0).map(([reason, n]) => `${n} ${reason}`);
|
|
62354
|
+
notes.push([
|
|
62355
|
+
chain2,
|
|
62356
|
+
formatUsd(sub.valueUsd),
|
|
62357
|
+
`${sub.wallets} ${sub.wallets === 1 ? "wallet" : "wallets"}${sub.unpriced > 0 ? `, ${sub.unpriced} unpriced${reasons.length > 0 ? ` (${reasons.join(", ")})` : ""}` : ""}`
|
|
62358
|
+
]);
|
|
62359
|
+
}
|
|
60083
62360
|
notes.push([
|
|
60084
62361
|
"total",
|
|
60085
62362
|
formatUsd(totals.totalUsd),
|
|
@@ -60090,16 +62367,31 @@ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEE
|
|
|
60090
62367
|
deps.stdout.write(`
|
|
60091
62368
|
`);
|
|
60092
62369
|
for (const [label, value, note] of notes) {
|
|
60093
|
-
deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
|
|
62370
|
+
deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${terminalText(note)}
|
|
62371
|
+
`);
|
|
62372
|
+
}
|
|
62373
|
+
const unreadByChain = totals.chains.map((chain2) => [chain2, totals.unavailableByChain[chain2].length]).filter(([, n]) => n > 0);
|
|
62374
|
+
const unavailable = unreadByChain.reduce((sum, [, n]) => sum + n, 0);
|
|
62375
|
+
if (unavailable > 0) {
|
|
62376
|
+
deps.stdout.write(`${unavailable} ${unavailable === 1 ? "wallet" : "wallets"} could not be read in full (${unreadByChain.map(([chain2, n]) => `${n} on ${CHAIN_NAMES[chain2]}`).join(", ")}). What was not read is marked "not read" and is not in the total.
|
|
60094
62377
|
`);
|
|
60095
62378
|
}
|
|
60096
|
-
if (totals.
|
|
60097
|
-
deps.stdout.write(
|
|
62379
|
+
if (totals.listCutOff) {
|
|
62380
|
+
deps.stdout.write(`Candle listed only part of this account's wallets. The total is partial.
|
|
62381
|
+
`);
|
|
62382
|
+
}
|
|
62383
|
+
const truncated = groups.flatMap((g) => g.wallets).filter((w) => w.truncated).length;
|
|
62384
|
+
if (truncated > 0) {
|
|
62385
|
+
deps.stdout.write(`${truncated} Hood ${truncated === 1 ? "wallet holds" : "wallets hold"} more tokens than Candle lists. The rest are not shown and not in the total.
|
|
60098
62386
|
`);
|
|
60099
62387
|
}
|
|
60100
62388
|
if ((totals.lp?.unreadable ?? 0) > 0) {
|
|
60101
62389
|
const n = totals.lp?.unreadable ?? 0;
|
|
60102
62390
|
deps.stdout.write(`${n} LP ${n === 1 ? "position" : "positions"} could not be read (the pool did not answer). ${n === 1 ? "It is" : "They are"} marked "not read" and not in the total.
|
|
62391
|
+
`);
|
|
62392
|
+
}
|
|
62393
|
+
if (groups.some((g) => g.group === "vault" && g.wallets.some((w) => w.chain === "hood"))) {
|
|
62394
|
+
deps.stdout.write(`EVM vault keys are read for ETH and USDG only; other tokens they hold are not shown.
|
|
60103
62395
|
`);
|
|
60104
62396
|
}
|
|
60105
62397
|
}
|
|
@@ -60627,7 +62919,7 @@ function displayLines(input) {
|
|
|
60627
62919
|
lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
|
|
60628
62920
|
return lines;
|
|
60629
62921
|
}
|
|
60630
|
-
async function
|
|
62922
|
+
async function sign4(args, ctx) {
|
|
60631
62923
|
const lifted = takeRepeatedFlag(args, "--wallet");
|
|
60632
62924
|
if ("error" in lifted)
|
|
60633
62925
|
return usage(ctx, lifted.error);
|
|
@@ -64135,10 +66427,10 @@ init_solana_lite();
|
|
|
64135
66427
|
init_token_2022();
|
|
64136
66428
|
init_errors();
|
|
64137
66429
|
var USDC_MINT2 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
64138
|
-
function decimalToRaw2(
|
|
64139
|
-
if (!/^\d+(\.\d+)?$/.test(
|
|
66430
|
+
function decimalToRaw2(decimal2, decimals) {
|
|
66431
|
+
if (!/^\d+(\.\d+)?$/.test(decimal2))
|
|
64140
66432
|
return null;
|
|
64141
|
-
const [whole, frac = ""] =
|
|
66433
|
+
const [whole, frac = ""] = decimal2.split(".");
|
|
64142
66434
|
if (frac.length > decimals)
|
|
64143
66435
|
return null;
|
|
64144
66436
|
return BigInt((whole ?? "0") + frac.padEnd(decimals, "0"));
|
|
@@ -64706,11 +66998,11 @@ function mapLegacyTeeEntry(entry, keyId, grant, createdAtFallback) {
|
|
|
64706
66998
|
}
|
|
64707
66999
|
function buildTeeMeta(entry, lifecycle, grant) {
|
|
64708
67000
|
const source = entry.tee;
|
|
64709
|
-
const
|
|
64710
|
-
if (
|
|
64711
|
-
throw new VaultError("VAULT_INDEX_INVALID", `Legacy entry ${entry.address} names network ${String(
|
|
67001
|
+
const network2 = source?.network ?? "solana-mainnet";
|
|
67002
|
+
if (network2 !== "solana-mainnet") {
|
|
67003
|
+
throw new VaultError("VAULT_INDEX_INVALID", `Legacy entry ${entry.address} names network ${String(network2)}; only solana-mainnet migrates.`);
|
|
64712
67004
|
}
|
|
64713
|
-
const meta = { network, lifecycle };
|
|
67005
|
+
const meta = { network: network2, lifecycle };
|
|
64714
67006
|
carryOptional(meta, source, [
|
|
64715
67007
|
"vaultDestination",
|
|
64716
67008
|
"boundKeyPrefix",
|
|
@@ -64722,7 +67014,7 @@ function buildTeeMeta(entry, lifecycle, grant) {
|
|
|
64722
67014
|
"sweptAt"
|
|
64723
67015
|
]);
|
|
64724
67016
|
if (lifecycle === "local-candidate") {
|
|
64725
|
-
return { network, lifecycle };
|
|
67017
|
+
return { network: network2, lifecycle };
|
|
64726
67018
|
}
|
|
64727
67019
|
if (lifecycle === "enabled") {
|
|
64728
67020
|
if (meta.vaultDestination === undefined) {
|
|
@@ -68134,7 +70426,7 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
|
|
|
68134
70426
|
await commitVault(vault, { index: { hd, entries }, addKeys: derived.entries.map((entry) => entry.blob) }, ctx.deps);
|
|
68135
70427
|
return { entries, hd };
|
|
68136
70428
|
}
|
|
68137
|
-
function teeFieldsFor(row, ctx,
|
|
70429
|
+
function teeFieldsFor(row, ctx, network2 = "solana-mainnet") {
|
|
68138
70430
|
const remoteState = row.sweptAt !== undefined ? "swept" : row.revokedAt !== undefined ? "quarantined" : "enabled";
|
|
68139
70431
|
const grantIdentity = {
|
|
68140
70432
|
account: "",
|
|
@@ -68142,7 +70434,7 @@ function teeFieldsFor(row, ctx, network = "solana-mainnet") {
|
|
|
68142
70434
|
source: "recorded-at-operation"
|
|
68143
70435
|
};
|
|
68144
70436
|
const common = {
|
|
68145
|
-
network,
|
|
70437
|
+
network: network2,
|
|
68146
70438
|
...row.vaultDestination !== undefined ? { vaultDestination: row.vaultDestination } : {},
|
|
68147
70439
|
...row.boundKeyPrefix !== undefined ? { boundKeyPrefix: row.boundKeyPrefix } : {},
|
|
68148
70440
|
...row.remoteAuthority !== undefined ? { remoteAuthority: row.remoteAuthority } : {},
|
|
@@ -69129,7 +71421,7 @@ function solOf(lamports) {
|
|
|
69129
71421
|
const digits = lamports.padStart(10, "0");
|
|
69130
71422
|
return `${digits.slice(0, -9)}.${digits.slice(-9)}`.replace(/\.?0+$/, "");
|
|
69131
71423
|
}
|
|
69132
|
-
function
|
|
71424
|
+
function previewLines2(preview) {
|
|
69133
71425
|
const lines = [
|
|
69134
71426
|
`Close ${preview.accountCount} empty token account${preview.accountCount === 1 ? "" : "s"} on the embedded wallet ${safeText(preview.wallet)}`,
|
|
69135
71427
|
`Rent returned to that same wallet: ${safeText(preview.totalSol)} SOL in ${preview.transactions} transaction${preview.transactions === 1 ? "" : "s"}, network fee about ${solOf(preview.estimatedFeeLamports)} SOL, net ${safeText(preview.netSol)} SOL`
|
|
@@ -69209,7 +71501,7 @@ async function walletsCloseEmpty(args, ctx) {
|
|
|
69209
71501
|
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
69210
71502
|
output.write(`No empty token accounts to close on the embedded wallet ${safeText(wallet)}.
|
|
69211
71503
|
`);
|
|
69212
|
-
for (const line of
|
|
71504
|
+
for (const line of previewLines2(preview).slice(2))
|
|
69213
71505
|
output.write(`${line}
|
|
69214
71506
|
`);
|
|
69215
71507
|
return printTradingResult(ctx, {
|
|
@@ -69219,7 +71511,7 @@ async function walletsCloseEmpty(args, ctx) {
|
|
|
69219
71511
|
preview
|
|
69220
71512
|
});
|
|
69221
71513
|
}
|
|
69222
|
-
const confirmed = await confirmClose(ctx,
|
|
71514
|
+
const confirmed = await confirmClose(ctx, previewLines2(preview), parsed.booleans.has("--yes"));
|
|
69223
71515
|
if (!confirmed)
|
|
69224
71516
|
return printTradingResult(ctx, {
|
|
69225
71517
|
success: true,
|
|
@@ -69312,7 +71604,7 @@ function trustTable(rows) {
|
|
|
69312
71604
|
}
|
|
69313
71605
|
async function setTrust(args, ctx, trusted) {
|
|
69314
71606
|
const { deps, apiUrl, json } = ctx;
|
|
69315
|
-
const
|
|
71607
|
+
const usage6 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
|
|
69316
71608
|
const command = trusted ? "wallets trust" : "wallets untrust";
|
|
69317
71609
|
const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
|
|
69318
71610
|
if ("error" in parsed) {
|
|
@@ -69320,7 +71612,7 @@ async function setTrust(args, ctx, trusted) {
|
|
|
69320
71612
|
return 2;
|
|
69321
71613
|
}
|
|
69322
71614
|
if (parsed.positionals.length === 0) {
|
|
69323
|
-
writeUsageFailure(deps, `Name at least one wallet. ${
|
|
71615
|
+
writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
|
|
69324
71616
|
return 2;
|
|
69325
71617
|
}
|
|
69326
71618
|
const skipConfirm = !trusted && parsed.booleans.has("--yes");
|
|
@@ -69592,6 +71884,17 @@ var COMMANDS = {
|
|
|
69592
71884
|
pnl: { bare: pnl },
|
|
69593
71885
|
portfolio: { bare: portfolio },
|
|
69594
71886
|
lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
|
|
71887
|
+
perps: {
|
|
71888
|
+
subcommands: {
|
|
71889
|
+
setup: perpsSetup,
|
|
71890
|
+
open: perpsOpen,
|
|
71891
|
+
close: perpsClose,
|
|
71892
|
+
cancel: perpsCancel,
|
|
71893
|
+
orders: perpsOrders,
|
|
71894
|
+
positions: perpsPositions,
|
|
71895
|
+
leverage: perpsLeverage
|
|
71896
|
+
}
|
|
71897
|
+
},
|
|
69595
71898
|
auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
|
|
69596
71899
|
keys: {
|
|
69597
71900
|
subcommands: {
|
|
@@ -69663,7 +71966,7 @@ var COMMANDS = {
|
|
|
69663
71966
|
}
|
|
69664
71967
|
},
|
|
69665
71968
|
external: { subcommands: { new: externalNew, list: externalList, sweep: externalSweep } },
|
|
69666
|
-
sign: { subcommands: { message: signMessage2 }, bare:
|
|
71969
|
+
sign: { subcommands: { message: signMessage2 }, bare: sign4 },
|
|
69667
71970
|
secrets: { subcommands: { set: secretsSet, list: secretsList, remove: secretsRemove } },
|
|
69668
71971
|
plugins: { bare: plugins },
|
|
69669
71972
|
doctor: { bare: doctor },
|
|
@@ -69967,8 +72270,8 @@ async function buildRealDeps() {
|
|
|
69967
72270
|
const { runStdioServer: runStdioServer2 } = await Promise.resolve().then(() => (init_server2(), exports_server));
|
|
69968
72271
|
await runStdioServer2(env);
|
|
69969
72272
|
},
|
|
69970
|
-
readFile: (path) =>
|
|
69971
|
-
readBytes: (path) =>
|
|
72273
|
+
readFile: (path) => readFile10(path, "utf8"),
|
|
72274
|
+
readBytes: (path) => readFile10(path),
|
|
69972
72275
|
readStdin: () => new Promise((resolve4, reject) => {
|
|
69973
72276
|
const chunks = [];
|
|
69974
72277
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|