@candledottv/cli 0.11.13 → 0.11.15
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 +79 -25
- package/dist/index.js +3263 -219
- 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;
|
|
33373
33615
|
};
|
|
33374
|
-
const
|
|
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);
|
|
33621
|
+
};
|
|
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.
|
|
@@ -42485,20 +42842,20 @@ var require_resolve = __commonJS((exports) => {
|
|
|
42485
42842
|
return false;
|
|
42486
42843
|
}
|
|
42487
42844
|
function countKeys(schema) {
|
|
42488
|
-
let
|
|
42845
|
+
let count2 = 0;
|
|
42489
42846
|
for (const key in schema) {
|
|
42490
42847
|
if (key === "$ref")
|
|
42491
42848
|
return Infinity;
|
|
42492
|
-
|
|
42849
|
+
count2++;
|
|
42493
42850
|
if (SIMPLE_INLINED.has(key))
|
|
42494
42851
|
continue;
|
|
42495
42852
|
if (typeof schema[key] == "object") {
|
|
42496
|
-
(0, util_1.eachItem)(schema[key], (sch) =>
|
|
42853
|
+
(0, util_1.eachItem)(schema[key], (sch) => count2 += countKeys(sch));
|
|
42497
42854
|
}
|
|
42498
|
-
if (
|
|
42855
|
+
if (count2 === Infinity)
|
|
42499
42856
|
return Infinity;
|
|
42500
42857
|
}
|
|
42501
|
-
return
|
|
42858
|
+
return count2;
|
|
42502
42859
|
}
|
|
42503
42860
|
function getFullPath(resolver, id = "", normalize4) {
|
|
42504
42861
|
if (normalize4 !== false)
|
|
@@ -45553,8 +45910,8 @@ var require_contains = __commonJS((exports) => {
|
|
|
45553
45910
|
cxt.result(valid, () => cxt.reset());
|
|
45554
45911
|
function validateItemsWithCount() {
|
|
45555
45912
|
const schValid = gen2.name("_valid");
|
|
45556
|
-
const
|
|
45557
|
-
validateItems(schValid, () => gen2.if(schValid, () => checkLimits(
|
|
45913
|
+
const count2 = gen2.let("count", 0);
|
|
45914
|
+
validateItems(schValid, () => gen2.if(schValid, () => checkLimits(count2)));
|
|
45558
45915
|
}
|
|
45559
45916
|
function validateItems(_valid, block2) {
|
|
45560
45917
|
gen2.forRange("i", 0, len, (i) => {
|
|
@@ -45567,16 +45924,16 @@ var require_contains = __commonJS((exports) => {
|
|
|
45567
45924
|
block2();
|
|
45568
45925
|
});
|
|
45569
45926
|
}
|
|
45570
|
-
function checkLimits(
|
|
45571
|
-
gen2.code((0, codegen_1._)`${
|
|
45927
|
+
function checkLimits(count2) {
|
|
45928
|
+
gen2.code((0, codegen_1._)`${count2}++`);
|
|
45572
45929
|
if (max === undefined) {
|
|
45573
|
-
gen2.if((0, codegen_1._)`${
|
|
45930
|
+
gen2.if((0, codegen_1._)`${count2} >= ${min}`, () => gen2.assign(valid, true).break());
|
|
45574
45931
|
} else {
|
|
45575
|
-
gen2.if((0, codegen_1._)`${
|
|
45932
|
+
gen2.if((0, codegen_1._)`${count2} > ${max}`, () => gen2.assign(valid, false).break());
|
|
45576
45933
|
if (min === 1)
|
|
45577
45934
|
gen2.assign(valid, true);
|
|
45578
45935
|
else
|
|
45579
|
-
gen2.if((0, codegen_1._)`${
|
|
45936
|
+
gen2.if((0, codegen_1._)`${count2} >= ${min}`, () => gen2.assign(valid, true));
|
|
45580
45937
|
}
|
|
45581
45938
|
}
|
|
45582
45939
|
}
|
|
@@ -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.6";
|
|
49492
49849
|
|
|
49493
49850
|
// ../mcp/src/update-notice.ts
|
|
49494
49851
|
function newer(a, b) {
|
|
@@ -49524,7 +49881,7 @@ var init_update_notice = __esm(() => {
|
|
|
49524
49881
|
});
|
|
49525
49882
|
|
|
49526
49883
|
// ../mcp/src/orchestrate.ts
|
|
49527
|
-
import { randomUUID as
|
|
49884
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
49528
49885
|
function requireApiKey(cfg) {
|
|
49529
49886
|
if (!cfg.apiKey) {
|
|
49530
49887
|
throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
|
|
@@ -49636,7 +49993,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
|
|
|
49636
49993
|
}
|
|
49637
49994
|
async function executeTrade(args, cfg, doFetch) {
|
|
49638
49995
|
const apiKey = requireApiKey(cfg);
|
|
49639
|
-
const clientTradeId = args.clientTradeId ??
|
|
49996
|
+
const clientTradeId = args.clientTradeId ?? randomUUID6();
|
|
49640
49997
|
if (args.amount !== undefined && args.percent !== undefined) {
|
|
49641
49998
|
return errText("pass exactly one of amount or percent, not both", { clientTradeId });
|
|
49642
49999
|
}
|
|
@@ -49774,7 +50131,7 @@ async function executeTrade(args, cfg, doFetch) {
|
|
|
49774
50131
|
}
|
|
49775
50132
|
async function executeLaunchAndSeed(args, cfg, doFetch) {
|
|
49776
50133
|
const apiKey = requireApiKey(cfg);
|
|
49777
|
-
const clientLaunchId = args.clientLaunchId ??
|
|
50134
|
+
const clientLaunchId = args.clientLaunchId ?? randomUUID6();
|
|
49778
50135
|
const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...launchFields } = args;
|
|
49779
50136
|
let buyAmount;
|
|
49780
50137
|
if (devBuy !== undefined) {
|
|
@@ -49940,6 +50297,878 @@ 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
|
+
function rawUnits(amount, decimals) {
|
|
50864
|
+
const [whole = "", fraction = ""] = amount.split(".");
|
|
50865
|
+
if (fraction.length > decimals)
|
|
50866
|
+
return null;
|
|
50867
|
+
const raw = BigInt(whole + fraction.padEnd(decimals, "0"));
|
|
50868
|
+
return raw > 0n ? raw.toString() : null;
|
|
50869
|
+
}
|
|
50870
|
+
function pickWallet(rows, chain2, name2, what) {
|
|
50871
|
+
const onChain = rows.filter((row) => row.chain === chain2);
|
|
50872
|
+
const matches2 = name2 === undefined ? onChain : onChain.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
|
|
50873
|
+
if (matches2.length !== 1)
|
|
50874
|
+
throw new Refusal(fail3("TEE_WALLET_REQUIRED", onChain.length === 0 ? `This key has no ${what} bound to it.` : `Name exactly one ${what}: ${onChain.map((row) => row.label ?? row.id).join(", ")}`));
|
|
50875
|
+
return matches2[0];
|
|
50876
|
+
}
|
|
50877
|
+
function depositProblem(build, expect) {
|
|
50878
|
+
const destination = build.destination ?? {};
|
|
50879
|
+
const fee = build.fee ?? {};
|
|
50880
|
+
const same = (a, b) => typeof a === "string" && a.toLowerCase() === b.toLowerCase();
|
|
50881
|
+
if (build.walletId !== expect.walletId)
|
|
50882
|
+
return "the build names another paying wallet";
|
|
50883
|
+
if (build.asset !== expect.asset || build.amountRaw !== expect.amountRaw)
|
|
50884
|
+
return "the build is for another amount";
|
|
50885
|
+
if (destination.chainId !== HYPERLIQUID_RELAY_CHAIN_ID2)
|
|
50886
|
+
return "the build does not land on Hyperliquid";
|
|
50887
|
+
if (destination.currency !== HYPERLIQUID_RELAY_USDC2)
|
|
50888
|
+
return "the build does not deliver Hyperliquid USDC";
|
|
50889
|
+
if (!same(destination.address, expect.account))
|
|
50890
|
+
return "the build lands on another account";
|
|
50891
|
+
if (fee.bps !== 0 || fee.feeRaw !== "0")
|
|
50892
|
+
return "the build carries a Candle fee";
|
|
50893
|
+
if (expect.hood !== (build.chain === "hood"))
|
|
50894
|
+
return "the build pays from the wrong chain";
|
|
50895
|
+
if (!expect.hood && !(Array.isArray(build.transactionsBase64) && build.transactionsBase64.length === 1))
|
|
50896
|
+
return "the build is not one Solana deposit";
|
|
50897
|
+
return null;
|
|
50898
|
+
}
|
|
50899
|
+
async function executePerpsDeposit(args, cfg, env, fetch2) {
|
|
50900
|
+
if (!cfg.apiKey)
|
|
50901
|
+
return fail3("MCP_VALIDATION", "CANDLE_AGENT_API_KEY is required for this tool.");
|
|
50902
|
+
const asset = args.asset;
|
|
50903
|
+
const spec = DEPOSIT_ASSETS[asset];
|
|
50904
|
+
if (!spec)
|
|
50905
|
+
return fail3("MCP_VALIDATION", "asset must be SOL, USDC, ETH or USDG.");
|
|
50906
|
+
const amountRaw = typeof args.amount === "string" ? rawUnits(args.amount, spec.decimals) : null;
|
|
50907
|
+
if (!amountRaw)
|
|
50908
|
+
return fail3("MCP_VALIDATION", `amount must be a positive ${asset} amount with at most ${spec.decimals} decimals.`);
|
|
50909
|
+
const hood = spec.chain === "evm";
|
|
50910
|
+
const clientDepositId = typeof args.clientDepositId === "string" ? args.clientDepositId : `deposit-${crypto.randomUUID()}`;
|
|
50911
|
+
const willSubmit = args.submit !== false;
|
|
50912
|
+
let signerPem = null;
|
|
50913
|
+
if (willSubmit) {
|
|
50914
|
+
const pemFile = env.CANDLE_KEY_SIGNER_PEM_FILE?.trim();
|
|
50915
|
+
if (!pemFile)
|
|
50916
|
+
return fail3("SIGNER_UNAVAILABLE", "Set CANDLE_KEY_SIGNER_PEM_FILE to the key signer PEM (candle tee signer new --out <pem>) to sign a deposit.");
|
|
50917
|
+
try {
|
|
50918
|
+
signerPem = await readFile9(pemFile, "utf8");
|
|
50919
|
+
createPrivateKey2(signerPem);
|
|
50920
|
+
} catch {
|
|
50921
|
+
return fail3("SIGNER_UNAVAILABLE", "CANDLE_KEY_SIGNER_PEM_FILE does not hold a readable private key PEM.");
|
|
50922
|
+
}
|
|
50923
|
+
}
|
|
50924
|
+
try {
|
|
50925
|
+
const listed = await call(cfg, fetch2, "GET", "/api/v1/agent/wallets/trading");
|
|
50926
|
+
if (!(Array.isArray(listed.scopes) && listed.scopes.includes("swap:write")))
|
|
50927
|
+
throw new Refusal(fail3("SCOPE_MISSING", "A deposit needs swap:write on the key."));
|
|
50928
|
+
const rows = Array.isArray(listed.page) ? listed.page : [];
|
|
50929
|
+
const appId = typeof listed.privyAppId === "string" ? listed.privyAppId : "";
|
|
50930
|
+
const name2 = typeof args.wallet === "string" ? args.wallet : undefined;
|
|
50931
|
+
const source = pickWallet(rows, spec.chain, name2, `${hood ? "Hood" : "Solana"} TEE wallet`);
|
|
50932
|
+
if (hood && args.perpsWallet !== undefined)
|
|
50933
|
+
return fail3("MCP_VALIDATION", "From a Hood TEE wallet the deposit lands on that wallet's own account; omit perpsWallet.");
|
|
50934
|
+
const account = hood ? source : pickWallet(rows, "evm", typeof args.perpsWallet === "string" ? args.perpsWallet : undefined, "EVM TEE wallet");
|
|
50935
|
+
const build = await call(cfg, fetch2, "POST", "/api/v1/agent/perps/deposit", {
|
|
50936
|
+
clientDepositId,
|
|
50937
|
+
walletId: source.id,
|
|
50938
|
+
asset,
|
|
50939
|
+
amountRaw,
|
|
50940
|
+
...typeof args.maxSlippageBps === "number" ? { maxSlippageBps: args.maxSlippageBps } : {},
|
|
50941
|
+
...hood ? {} : { perpsWalletId: account.id }
|
|
50942
|
+
});
|
|
50943
|
+
if (build.job !== undefined)
|
|
50944
|
+
return { text: JSON.stringify(build) };
|
|
50945
|
+
const problem = depositProblem(build, { walletId: source.id, asset, amountRaw, account: account.address, hood });
|
|
50946
|
+
if (problem)
|
|
50947
|
+
return fail3("PERPS_BUILD_REFUSED", `Refused to sign this deposit: ${problem}. Nothing was signed.`);
|
|
50948
|
+
if (!willSubmit || signerPem === null)
|
|
50949
|
+
return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
|
|
50950
|
+
if (!appId || !source.privyWalletId)
|
|
50951
|
+
return fail3("SIGNER_UNAVAILABLE", "The server did not return its relay identifiers.");
|
|
50952
|
+
const pem = signerPem;
|
|
50953
|
+
const relaySign2 = async (body2) => {
|
|
50954
|
+
const payload = hyperliquidCanonicalJson2({
|
|
50955
|
+
body: body2,
|
|
50956
|
+
headers: { "privy-app-id": appId },
|
|
50957
|
+
method: "POST",
|
|
50958
|
+
url: `https://api.privy.io/v1/wallets/${source.privyWalletId}/rpc`,
|
|
50959
|
+
version: 1
|
|
50960
|
+
});
|
|
50961
|
+
const authorizationSignature2 = sign5("sha256", Buffer.from(payload), pem).toString("base64");
|
|
50962
|
+
const relay = await call(cfg, fetch2, "POST", `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/sign`, {
|
|
50963
|
+
authorizationSignature: authorizationSignature2,
|
|
50964
|
+
body: body2
|
|
50965
|
+
});
|
|
50966
|
+
if (typeof relay.signedTransaction !== "string")
|
|
50967
|
+
throw new Refusal(fail3("INVALID_RESPONSE", "The relay did not return a signed transaction."));
|
|
50968
|
+
return relay.signedTransaction;
|
|
50969
|
+
};
|
|
50970
|
+
const ids = { clientDepositId, depositId: build.depositId };
|
|
50971
|
+
const submitPath = "/api/v1/agent/perps/deposit/submit";
|
|
50972
|
+
if (!hood) {
|
|
50973
|
+
const transaction = build.transactionsBase64[0];
|
|
50974
|
+
const signed = await relaySign2({ method: "signTransaction", params: { encoding: "base64", transaction } });
|
|
50975
|
+
return {
|
|
50976
|
+
text: JSON.stringify(await call(cfg, fetch2, "POST", submitPath, { ...ids, signedTransactionsBase64: [signed] }))
|
|
50977
|
+
};
|
|
50978
|
+
}
|
|
50979
|
+
const allowed = asset === "USDG" ? ["approval", "bridgeDeposit"] : ["bridgeDeposit"];
|
|
50980
|
+
const planned = build.plannedLegCount;
|
|
50981
|
+
const hex4 = (value) => `0x${BigInt(String(value)).toString(16)}`;
|
|
50982
|
+
const signedKinds = new Set;
|
|
50983
|
+
let body = build;
|
|
50984
|
+
while (body.mode === "sequenced") {
|
|
50985
|
+
const leg = body.nextLeg;
|
|
50986
|
+
const kind = String(body.legKind);
|
|
50987
|
+
if (!leg || body.operationId !== build.operationId || body.plannedLegCount !== planned || typeof planned !== "number" || planned > allowed.length || !allowed.includes(kind) || signedKinds.has(kind) || leg.chainId !== 4663)
|
|
50988
|
+
return fail3("PERPS_BUILD_REFUSED", `Refused to sign deposit leg ${kind}: it is not the plan this deposit was built with.`, { clientDepositId, operationId: build.operationId });
|
|
50989
|
+
signedKinds.add(kind);
|
|
50990
|
+
const signed = await relaySign2({
|
|
50991
|
+
method: "eth_signTransaction",
|
|
50992
|
+
params: {
|
|
50993
|
+
transaction: {
|
|
50994
|
+
chain_id: leg.chainId,
|
|
50995
|
+
data: leg.data,
|
|
50996
|
+
from: source.address,
|
|
50997
|
+
gas_limit: hex4(leg.gas),
|
|
50998
|
+
max_fee_per_gas: hex4(leg.maxFeePerGas),
|
|
50999
|
+
max_priority_fee_per_gas: hex4(leg.maxPriorityFeePerGas),
|
|
51000
|
+
nonce: leg.nonce,
|
|
51001
|
+
to: leg.to,
|
|
51002
|
+
type: 2,
|
|
51003
|
+
value: hex4(leg.value)
|
|
51004
|
+
}
|
|
51005
|
+
}
|
|
51006
|
+
});
|
|
51007
|
+
body = await call(cfg, fetch2, "POST", submitPath, {
|
|
51008
|
+
...ids,
|
|
51009
|
+
operationId: build.operationId,
|
|
51010
|
+
signedTransaction: signed
|
|
51011
|
+
});
|
|
51012
|
+
}
|
|
51013
|
+
return { text: JSON.stringify({ ...body, clientDepositId }) };
|
|
51014
|
+
} catch (error) {
|
|
51015
|
+
if (error instanceof Refusal)
|
|
51016
|
+
return error.tool;
|
|
51017
|
+
return fail3("MCP_TRANSPORT", error instanceof Error ? error.message : "The deposit call failed.");
|
|
51018
|
+
}
|
|
51019
|
+
}
|
|
51020
|
+
var decimal3, wallet, submit, perpsShapes, PATHS, Refusal, HYPERLIQUID_RELAY_CHAIN_ID2 = 1337, HYPERLIQUID_RELAY_USDC2 = "0x00000000000000000000000000000000", DEPOSIT_ASSETS, perpsDepositShape;
|
|
51021
|
+
var init_perps = __esm(() => {
|
|
51022
|
+
init_zod();
|
|
51023
|
+
init_hyperliquid();
|
|
51024
|
+
decimal3 = exports_external.string().regex(/^(0|[1-9][0-9]*)(\.[0-9]+)?$/, "a plain decimal string");
|
|
51025
|
+
wallet = exports_external.string().optional().describe("The EVM TEE wallet by id, address or label. Omit when the key has exactly one.");
|
|
51026
|
+
submit = exports_external.boolean().optional().describe("false: build and check only, sign and submit nothing. Default true.");
|
|
51027
|
+
perpsShapes = {
|
|
51028
|
+
candle_perps_setup: { wallet, submit },
|
|
51029
|
+
candle_perps_open: {
|
|
51030
|
+
coin: exports_external.string().describe("A market on Hyperliquid's main perp exchange, e.g. BTC"),
|
|
51031
|
+
side: exports_external.enum(["long", "short"]),
|
|
51032
|
+
size: decimal3.describe('Size in the coin, decimal (e.g. "0.01")'),
|
|
51033
|
+
price: decimal3.optional().describe("Limit price. Omit for a market order (IOC within slippageBps of the mid)."),
|
|
51034
|
+
tif: exports_external.enum(["Gtc", "Alo", "Ioc"]).optional().describe("With price: time in force (default Gtc)"),
|
|
51035
|
+
slippageBps: exports_external.number().int().min(1).optional(),
|
|
51036
|
+
takeProfit: decimal3.optional().describe("Reduce-only take-profit trigger price"),
|
|
51037
|
+
stopLoss: decimal3.optional().describe("Reduce-only stop-loss trigger price"),
|
|
51038
|
+
wallet,
|
|
51039
|
+
submit
|
|
51040
|
+
},
|
|
51041
|
+
candle_perps_close: {
|
|
51042
|
+
coin: exports_external.string(),
|
|
51043
|
+
size: decimal3.optional().describe("Part of the position to close; the whole position when omitted"),
|
|
51044
|
+
slippageBps: exports_external.number().int().min(1).optional(),
|
|
51045
|
+
wallet,
|
|
51046
|
+
submit
|
|
51047
|
+
},
|
|
51048
|
+
candle_perps_cancel: {
|
|
51049
|
+
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)"),
|
|
51050
|
+
wallet,
|
|
51051
|
+
submit
|
|
51052
|
+
},
|
|
51053
|
+
candle_perps_orders: { wallet, limit: exports_external.number().int().min(1).max(200).optional() },
|
|
51054
|
+
candle_perps_positions: { wallet },
|
|
51055
|
+
candle_perps_leverage: {
|
|
51056
|
+
coin: exports_external.string(),
|
|
51057
|
+
leverage: exports_external.number().int().min(1),
|
|
51058
|
+
mode: exports_external.enum(["cross", "isolated"]).optional(),
|
|
51059
|
+
wallet,
|
|
51060
|
+
submit
|
|
51061
|
+
}
|
|
51062
|
+
};
|
|
51063
|
+
PATHS = {
|
|
51064
|
+
candle_perps_setup: "/api/v1/agent/perps/setup",
|
|
51065
|
+
candle_perps_open: "/api/v1/agent/perps/open",
|
|
51066
|
+
candle_perps_close: "/api/v1/agent/perps/close",
|
|
51067
|
+
candle_perps_cancel: "/api/v1/agent/perps/cancel",
|
|
51068
|
+
candle_perps_orders: "/api/v1/agent/perps/orders",
|
|
51069
|
+
candle_perps_positions: "/api/v1/agent/perps/positions",
|
|
51070
|
+
candle_perps_leverage: "/api/v1/agent/perps/leverage"
|
|
51071
|
+
};
|
|
51072
|
+
Refusal = class Refusal extends Error {
|
|
51073
|
+
tool;
|
|
51074
|
+
constructor(tool) {
|
|
51075
|
+
super("refused");
|
|
51076
|
+
this.tool = tool;
|
|
51077
|
+
}
|
|
51078
|
+
};
|
|
51079
|
+
DEPOSIT_ASSETS = {
|
|
51080
|
+
SOL: { chain: "solana", decimals: 9 },
|
|
51081
|
+
USDC: { chain: "solana", decimals: 6 },
|
|
51082
|
+
ETH: { chain: "evm", decimals: 18 },
|
|
51083
|
+
USDG: { chain: "evm", decimals: 6 }
|
|
51084
|
+
};
|
|
51085
|
+
perpsDepositShape = {
|
|
51086
|
+
asset: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]).describe("What to deposit. SOL or USDC pay from the key's Solana TEE wallet; ETH or USDG from its Hood one."),
|
|
51087
|
+
amount: decimal3.describe('Amount of the asset, decimal (e.g. "25")'),
|
|
51088
|
+
wallet: exports_external.string().optional().describe("The paying TEE wallet by id, address or label. Omit when the key has exactly one on that chain."),
|
|
51089
|
+
perpsWallet: exports_external.string().optional().describe("From Solana only: the EVM TEE wallet whose Hyperliquid account receives it. Omit when the key has one."),
|
|
51090
|
+
clientDepositId: exports_external.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe("Idempotency id; the same id answers the deposit it already built. Default: a new one."),
|
|
51091
|
+
maxSlippageBps: exports_external.number().int().min(1).max(1000).optional().describe("Relay slippage (default 100)"),
|
|
51092
|
+
submit
|
|
51093
|
+
};
|
|
51094
|
+
});
|
|
51095
|
+
|
|
51096
|
+
// ../mcp/src/plans.ts
|
|
51097
|
+
function planLabel2(plan) {
|
|
51098
|
+
return Object.hasOwn(PLAN_LABELS2, plan) ? PLAN_LABELS2[plan] : plan;
|
|
51099
|
+
}
|
|
51100
|
+
function formatPlanBps2(bps) {
|
|
51101
|
+
return bps === 0 ? "none" : `${Number((bps / 100).toFixed(4))}%`;
|
|
51102
|
+
}
|
|
51103
|
+
function count2(n) {
|
|
51104
|
+
return n.toLocaleString("en-US");
|
|
51105
|
+
}
|
|
51106
|
+
function priceCell2(entry) {
|
|
51107
|
+
if (entry.price)
|
|
51108
|
+
return `$${count2(entry.price.pricePerMonthUsd)} a month`;
|
|
51109
|
+
return entry.plan === "free" ? "free" : "not sold";
|
|
51110
|
+
}
|
|
51111
|
+
function planTableRows2(table) {
|
|
51112
|
+
const plans2 = table.plans;
|
|
51113
|
+
const row = (label, cell) => [label, ...plans2.map(cell)];
|
|
51114
|
+
const rows = [
|
|
51115
|
+
row("Price", priceCell2),
|
|
51116
|
+
row("Agent trade fee", (e) => formatPlanBps2(e.feeBps)),
|
|
51117
|
+
row("Perps builder fee", (e) => formatPlanBps2(e.perpFeeBps)),
|
|
51118
|
+
row("Requests per minute", (e) => count2(e.limits.rateLimitPerMin)),
|
|
51119
|
+
row("Launches per day", (e) => count2(e.limits.dailyLaunchCap)),
|
|
51120
|
+
row("Uploads per minute", (e) => count2(e.limits.uploadsPerMin)),
|
|
51121
|
+
row("Linked wallets", (e) => count2(e.limits.linkedWallets))
|
|
51122
|
+
];
|
|
51123
|
+
const known = Object.keys(PLAN_CAPABILITY_LABELS2);
|
|
51124
|
+
const served = new Set;
|
|
51125
|
+
for (const entry of plans2)
|
|
51126
|
+
for (const key of Object.keys(entry.capabilities ?? {}))
|
|
51127
|
+
served.add(key);
|
|
51128
|
+
const keys = [
|
|
51129
|
+
...known.filter((k) => served.has(k)),
|
|
51130
|
+
...[...served].filter((k) => !Object.hasOwn(PLAN_CAPABILITY_LABELS2, k))
|
|
51131
|
+
];
|
|
51132
|
+
for (const key of keys) {
|
|
51133
|
+
const label = Object.hasOwn(PLAN_CAPABILITY_LABELS2, key) ? PLAN_CAPABILITY_LABELS2[key] : key;
|
|
51134
|
+
rows.push(row(label, (e) => {
|
|
51135
|
+
const value = e.capabilities?.[key];
|
|
51136
|
+
return value === true ? "yes" : value === false ? "no" : "-";
|
|
51137
|
+
}));
|
|
51138
|
+
}
|
|
51139
|
+
return { headers: ["", ...plans2.map((e) => planLabel2(e.plan))], rows };
|
|
51140
|
+
}
|
|
51141
|
+
function planPromotionLine2(table) {
|
|
51142
|
+
if (!(table.promoMaxDays > 0))
|
|
51143
|
+
return null;
|
|
51144
|
+
const days = table.promoMaxDays === 1 ? "1 day" : `${table.promoMaxDays} days`;
|
|
51145
|
+
return `A first Pro purchase includes Max for its first ${days}, then continues on Pro.`;
|
|
51146
|
+
}
|
|
51147
|
+
function planTableMarkdown(table) {
|
|
51148
|
+
const { headers: headers2, rows } = planTableRows2(table);
|
|
51149
|
+
const line = (cells) => `| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
|
|
51150
|
+
return [line(headers2), line(headers2.map(() => "---")), ...rows.map(line)].join(`
|
|
51151
|
+
`);
|
|
51152
|
+
}
|
|
51153
|
+
var PLAN_CAPABILITY_NOTE2 = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.", PLAN_CAPABILITY_LABELS2, PLAN_LABELS2;
|
|
51154
|
+
var init_plans = __esm(() => {
|
|
51155
|
+
PLAN_CAPABILITY_LABELS2 = {
|
|
51156
|
+
tradeCandleTokens: "Trade Candle-launched tokens",
|
|
51157
|
+
tradeBaseAssets: "Trade base assets",
|
|
51158
|
+
freeBaseTransfers: "Base-pair swaps and own-wallet bridges (when enabled), no Candle fee",
|
|
51159
|
+
sellExternalTokens: "Sell tokens not launched on Candle",
|
|
51160
|
+
buyExternalTokens: "Buy tokens not launched on Candle",
|
|
51161
|
+
hyperliquidPerps: "Hyperliquid perps (when enabled)",
|
|
51162
|
+
selfLaunch: "Self-launch from a linked wallet",
|
|
51163
|
+
atomicLaunch: "Atomic launch with first buys",
|
|
51164
|
+
createLinkedWallets: "Create linked wallets",
|
|
51165
|
+
importLinkedWallets: "Import linked wallets",
|
|
51166
|
+
limitOrders: "Limit orders",
|
|
51167
|
+
quant: "Quant on Telegram"
|
|
51168
|
+
};
|
|
51169
|
+
PLAN_LABELS2 = { free: "Free", believer: "Believer", pro: "Pro", max: "Max" };
|
|
51170
|
+
});
|
|
51171
|
+
|
|
49943
51172
|
// ../mcp/src/tools.ts
|
|
49944
51173
|
function resolveToolAllowlist(env) {
|
|
49945
51174
|
const raw = env.CANDLE_MCP_TOOLS?.trim();
|
|
@@ -50038,6 +51267,9 @@ function buildRequest(name2, args, cfg) {
|
|
|
50038
51267
|
init: { method: "GET", headers: jsonHeaders() }
|
|
50039
51268
|
};
|
|
50040
51269
|
}
|
|
51270
|
+
case "candle_get_plans": {
|
|
51271
|
+
return { url: `${base2}/api/v1/agent/plans`, init: { method: "GET", headers: jsonHeaders() } };
|
|
51272
|
+
}
|
|
50041
51273
|
case "candle_swap": {
|
|
50042
51274
|
const apiKey = requireApiKey2(cfg);
|
|
50043
51275
|
return {
|
|
@@ -50107,6 +51339,22 @@ function buildRequest(name2, args, cfg) {
|
|
|
50107
51339
|
}
|
|
50108
51340
|
}
|
|
50109
51341
|
}
|
|
51342
|
+
function plansMarkdown(body) {
|
|
51343
|
+
try {
|
|
51344
|
+
const table = JSON.parse(body);
|
|
51345
|
+
if (!Array.isArray(table.plans))
|
|
51346
|
+
return "";
|
|
51347
|
+
const promotion = planPromotionLine2(table);
|
|
51348
|
+
const markdown = `${planTableMarkdown(table)}
|
|
51349
|
+
|
|
51350
|
+
${PLAN_CAPABILITY_NOTE2}`;
|
|
51351
|
+
return promotion ? `${markdown}
|
|
51352
|
+
|
|
51353
|
+
${promotion}` : markdown;
|
|
51354
|
+
} catch {
|
|
51355
|
+
return "";
|
|
51356
|
+
}
|
|
51357
|
+
}
|
|
50110
51358
|
async function callAndRelay(name2, args, cfg) {
|
|
50111
51359
|
const { url, init } = buildRequest(name2, args, cfg);
|
|
50112
51360
|
const res = await fetch(url, init);
|
|
@@ -50161,6 +51409,24 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50161
51409
|
description: "Read a Candle user's public agent profile: whether agent features are enabled and launch counts.",
|
|
50162
51410
|
inputSchema: getAgentProfileShape
|
|
50163
51411
|
}, async (args) => callAndRelay("candle_get_agent_profile", args, cfg));
|
|
51412
|
+
register("candle_get_plans", {
|
|
51413
|
+
title: "Plans: prices, fees, limits and what each can do",
|
|
51414
|
+
description: "The plan table this Candle deployment serves (GET /api/v1/agent/plans). No key needed. Reads only. For " + "each plan: price (null when not sold), feeBps (the agent fee charged on top of every trade or dev buy the " + "API builds), perpFeeBps (the Hyperliquid builder fee), limits for a new key (requests per minute, launches " + "per day, uploads per minute, linked wallets) and capabilities: buyExternalTokens, sellExternalTokens, " + "tradeBaseAssets, tradeCandleTokens, selfLaunch, atomicLaunch, createLinkedWallets, importLinkedWallets, " + "hyperliquidPerps, limitOrders, quant, freeBaseTransfers. promoMaxDays is the days of Max a first Pro " + "purchase includes (0: no promotion). Quote prices and fees from here, never from memory: they differ by " + "deployment and change at the three-plan launch (Free, Pro, Max). The account's own plan and fee are in " + "candle_execution_status's tier. A TIER_REQUIRED refusal names a capability this table shows the account's " + "plan lacks. A capability true here is plan eligibility, not deployment availability: perps and own-wallet bridges " + "are each behind a deployment switch and need their wallet, scopes and setup. Returns the server's JSON, then the same table as Markdown.",
|
|
51415
|
+
inputSchema: {}
|
|
51416
|
+
}, async () => {
|
|
51417
|
+
const { url, init } = buildRequest("candle_get_plans", {}, cfg);
|
|
51418
|
+
const res = await fetch(url, init);
|
|
51419
|
+
noteVersionHeaders(res);
|
|
51420
|
+
const text = await res.text();
|
|
51421
|
+
if (!res.ok)
|
|
51422
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
51423
|
+
return {
|
|
51424
|
+
content: [
|
|
51425
|
+
{ type: "text", text },
|
|
51426
|
+
{ type: "text", text: plansMarkdown(text) }
|
|
51427
|
+
]
|
|
51428
|
+
};
|
|
51429
|
+
});
|
|
50164
51430
|
register("candle_get_operation", {
|
|
50165
51431
|
title: "What happened to a write",
|
|
50166
51432
|
description: "Look up a trade or launch by the id its write used, and find out whether it landed. " + `Reads only; moves nothing.
|
|
@@ -50191,7 +51457,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50191
51457
|
}, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
|
|
50192
51458
|
register("candle_get_profile_pnl", {
|
|
50193
51459
|
title: "Read an agent profile's P&L",
|
|
50194
|
-
description: "
|
|
51460
|
+
description: "This profile's share of the account's P&L: realized profit on the sales it made, the Candle fees " + "charged against it, and the positions that belong to it (the wallet's bound key, else the key " + "whose buy opened them) 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 and withdrawals are " + "excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before quoting the " + "number; they mean the total is partial. The figures come from the account's one P&L read, the " + "same as the console's, to the cent: `pnl.totalUsd` (realized net plus unrealized) is the " + "console's P&L. Positions live in wallets: each wallet is its own average-cost pool, the total " + "is the sum of `pnl.byWallet` (one row per wallet, main or linked, with the linked wallet's " + "label; `unknown:solana` or `unknown:hood` is the row for fills no record places in a wallet), " + "and one token held in two wallets is two positions, each with its `wallet`. A recorded move " + "between the account's wallets carries its cost and realizes nothing. Each position carries " + "`agent` (the key it belongs to) and `dust` when it is worth under one cent; dust stays listed " + "and counted, and `openPositionsExDust` leaves it out. `closed` gives the closed positions as " + "`madeUsd` + `lostUsd` + `partialSellsUsd` = `realizedNetUsd`, with `wins` and `losses`. " + "`lookback` and `truncated` are the account's activity bound, and `tradesConsidered` counts this " + "key's ledger fills. Since 2026-10-02 the total is the sum of wallets rather than one pool across " + "them, so realized figures can differ from earlier reads; a server that predates it omits " + "`totalUsd`, `closed`, `openPositionsExDust`, `wallet`, `agent` and `dust`. " + "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`). The two chains sum to the total. A " + "server that predates per-chain P&L omits `chain` and `byChain`. While Hyperliquid is enabled, " + "`hyperliquid` separately reports main-perp realized gross, signed funding, fees (inclusive of builder fees), " + "and net = gross + funding - fees, for currently bound EVM TEE wallets since the current TEE binding. " + "Check `read` and `truncated`; historical bindings and unrealized perps are excluded. Reads only.",
|
|
50195
51461
|
inputSchema: profilePnlShape
|
|
50196
51462
|
}, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
|
|
50197
51463
|
register("candle_get_profile_trades", {
|
|
@@ -50261,6 +51527,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50261
51527
|
|
|
50262
51528
|
` + "Arguments: `mint` and `side` are required. Amounts are DECIMAL, never raw base units " + '(amount: "0.5", not lamports). Omitting the amount on a sell sells the whole ' + `position.
|
|
50263
51529
|
|
|
51530
|
+
` + "Plans: every plan can buy and sell Candle-launched tokens and base assets, and SELL a token it holds that " + "Candle did not launch. BUYING such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the " + `plan's agent fee on top (feeBps in candle_get_plans).
|
|
51531
|
+
|
|
50264
51532
|
` + "Pass `paper: true` to rehearse: every admission rule runs and the quote is recorded, but " + "nothing broadcasts and no funds move. A paper buy credits this key's paper inventory, " + "including for external Solana mints routed through Jupiter. A later sell by amount or " + "percent closes that book without reading the live wallet and without MARKET_NOT_FOUND " + "-- including when `paper` is omitted on the exit, as long as the paper position exists. " + "Do this before the first live trade of a new strategy, and whenever you are unsure a " + `trade would be admitted at all.
|
|
50265
51533
|
|
|
50266
51534
|
` + `After the call:
|
|
@@ -50279,12 +51547,62 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
50279
51547
|
const result = await executeLaunchAndSeed(args, cfg, fetch);
|
|
50280
51548
|
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
50281
51549
|
});
|
|
51550
|
+
const perpsTool = (tool) => async (args) => {
|
|
51551
|
+
const result = await executePerps(tool, args, cfg, env, fetch);
|
|
51552
|
+
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
51553
|
+
};
|
|
51554
|
+
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).";
|
|
51555
|
+
register("candle_perps_setup", {
|
|
51556
|
+
title: "Set up Hyperliquid perps",
|
|
51557
|
+
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,
|
|
51558
|
+
inputSchema: perpsShapes.candle_perps_setup
|
|
51559
|
+
}, perpsTool("candle_perps_setup"));
|
|
51560
|
+
register("candle_perps_open", {
|
|
51561
|
+
title: "Open a perps position",
|
|
51562
|
+
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. Every plan except Max pays Candle a builder fee on each order (0.1% today; perpFeeBps in " + "candle_get_plans); Max pays none." + perpsWrite,
|
|
51563
|
+
inputSchema: perpsShapes.candle_perps_open
|
|
51564
|
+
}, perpsTool("candle_perps_open"));
|
|
51565
|
+
register("candle_perps_close", {
|
|
51566
|
+
title: "Close a perps position",
|
|
51567
|
+
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,
|
|
51568
|
+
inputSchema: perpsShapes.candle_perps_close
|
|
51569
|
+
}, perpsTool("candle_perps_close"));
|
|
51570
|
+
register("candle_perps_cancel", {
|
|
51571
|
+
title: "Cancel a perps order",
|
|
51572
|
+
description: "Cancel an order Candle built, by its cloid (from candle_perps_open or candle_perps_orders)." + perpsWrite,
|
|
51573
|
+
inputSchema: perpsShapes.candle_perps_cancel
|
|
51574
|
+
}, perpsTool("candle_perps_cancel"));
|
|
51575
|
+
register("candle_perps_orders", {
|
|
51576
|
+
title: "Perps orders",
|
|
51577
|
+
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.",
|
|
51578
|
+
inputSchema: perpsShapes.candle_perps_orders
|
|
51579
|
+
}, perpsTool("candle_perps_orders"));
|
|
51580
|
+
register("candle_perps_positions", {
|
|
51581
|
+
title: "Perps positions",
|
|
51582
|
+
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.",
|
|
51583
|
+
inputSchema: perpsShapes.candle_perps_positions
|
|
51584
|
+
}, perpsTool("candle_perps_positions"));
|
|
51585
|
+
register("candle_perps_leverage", {
|
|
51586
|
+
title: "Set perps leverage",
|
|
51587
|
+
description: "Set a market's leverage and margin mode (cross or isolated) on the account, within the key's maximum leverage." + perpsWrite,
|
|
51588
|
+
inputSchema: perpsShapes.candle_perps_leverage
|
|
51589
|
+
}, perpsTool("candle_perps_leverage"));
|
|
51590
|
+
register("candle_perps_deposit", {
|
|
51591
|
+
title: "Deposit to Hyperliquid",
|
|
51592
|
+
description: "MOVES REAL FUNDS unless submit is false. Deposit onto the key's Hyperliquid account through Relay: SOL or " + "USDC from its Solana TEE wallet, or ETH or USDG from its Hood TEE wallet, credited as Hyperliquid perps " + "USDC at the key's EVM TEE wallet's own address (Candle resolves it; no recipient is taken). Refused below a " + "floor that covers Hyperliquid's 1 USDC first-deposit charge. No Candle fee. Needs swap:write and a raw cap " + "on the asset, and CANDLE_KEY_SIGNER_PEM_FILE to sign. There is no withdrawal from Hyperliquid yet.",
|
|
51593
|
+
inputSchema: perpsDepositShape
|
|
51594
|
+
}, async (args) => {
|
|
51595
|
+
const result = await executePerpsDeposit(args, cfg, env, fetch);
|
|
51596
|
+
return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
|
|
51597
|
+
});
|
|
50282
51598
|
}
|
|
50283
51599
|
var TOOL_NAMES, launchTokenShape, getMarketShape, tokenForensicsShape, getFeedShape, reportActivityShape, getAgentProfileShape, getOperationShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, _rawBuyAmount, seedableLaunchShape, launchAndSeedShape, transferShape, sweepShape;
|
|
50284
51600
|
var init_tools = __esm(() => {
|
|
50285
51601
|
init_zod();
|
|
50286
51602
|
init_convert();
|
|
50287
51603
|
init_orchestrate();
|
|
51604
|
+
init_perps();
|
|
51605
|
+
init_plans();
|
|
50288
51606
|
init_update_notice();
|
|
50289
51607
|
TOOL_NAMES = [
|
|
50290
51608
|
"candle_launch_token",
|
|
@@ -50293,6 +51611,7 @@ var init_tools = __esm(() => {
|
|
|
50293
51611
|
"candle_token_forensics",
|
|
50294
51612
|
"candle_report_activity",
|
|
50295
51613
|
"candle_get_agent_profile",
|
|
51614
|
+
"candle_get_plans",
|
|
50296
51615
|
"candle_trade",
|
|
50297
51616
|
"candle_launch_and_seed",
|
|
50298
51617
|
"candle_swap",
|
|
@@ -50306,7 +51625,15 @@ var init_tools = __esm(() => {
|
|
|
50306
51625
|
"candle_get_portfolio",
|
|
50307
51626
|
"candle_resolve_token",
|
|
50308
51627
|
"candle_execution_status",
|
|
50309
|
-
"candle_get_operation"
|
|
51628
|
+
"candle_get_operation",
|
|
51629
|
+
"candle_perps_setup",
|
|
51630
|
+
"candle_perps_open",
|
|
51631
|
+
"candle_perps_close",
|
|
51632
|
+
"candle_perps_cancel",
|
|
51633
|
+
"candle_perps_orders",
|
|
51634
|
+
"candle_perps_positions",
|
|
51635
|
+
"candle_perps_leverage",
|
|
51636
|
+
"candle_perps_deposit"
|
|
50310
51637
|
];
|
|
50311
51638
|
launchTokenShape = {
|
|
50312
51639
|
clientLaunchId: exports_external.string().describe("Caller-chosen idempotency key, unique per account"),
|
|
@@ -50382,7 +51709,7 @@ var init_tools = __esm(() => {
|
|
|
50382
51709
|
side: exports_external.enum(["buy", "sell"]),
|
|
50383
51710
|
amount: exports_external.string().optional().describe("Decimal amount. Buys: how much of THIS TOKEN'S OWN quote asset to spend (SOL for a " + 'SOL-launched token, USDC for a USDC-quoted one, and so on: e.g. "0.5"). Sells: how many ' + "TOKENS to sell. Pass exactly one of amount or percent."),
|
|
50384
51711
|
percent: exports_external.number().optional().describe("Sells only: sell this percent (integer 1-100) of the holding. Live trades size against the " + "embedded wallet. Paper trades (`paper: true`) size against this key's paper inventory -- " + "the position a previous paper buy credited -- because paper never moves the live wallet."),
|
|
50385
|
-
quoteAsset: exports_external.string().optional().describe('What the wallet spends on a buy or receives on a sell: "sol", "usdc" or "cndl" on Solana, ' + '"eth" or "usdg" on Hood.
|
|
51712
|
+
quoteAsset: exports_external.string().optional().describe('What the wallet spends on a buy or receives on a sell: "sol", "usdc" or "cndl" on Solana, ' + '"eth" or "usdg" on Hood. Echoing the `quoteAsset` a quote (POST /api/v1/trade/agent/quote) returned is safe. On Solana it applies only ' + "to an arbitrary mint Candle never launched (a buy needs Pro or Max; a sell works on any plan) and is ignored for a Candle token, " + "whose quote comes from the token itself. On Hood it is the settlement asset of a DEX " + "trade; a USDG buy adds an approval transaction an ETH buy does not. It is not the route: " + "the cheapest path to the asset is chosen separately. Defaults to sol / ETH settlement."),
|
|
50386
51713
|
maxSlippageBps: exports_external.number().optional().describe("Max slippage in basis points; API default applies when omitted"),
|
|
50387
51714
|
clientTradeId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result. Retrying with the " + "SAME id is safe (idempotent replay); a new id is a SECOND trade."),
|
|
50388
51715
|
paper: exports_external.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, exports_external.boolean().optional().describe("Rehearse instead of trading. The request passes every admission rule a live trade passes " + "-- the same planner, spend gate, key cap and loss limits -- and records the quote, but " + "nothing is ever broadcast and no funds move. Use it to check that a strategy is admitted " + "before risking anything on it. A paper fill is optimistic by construction: it books the " + "quoted price, so the gap between a paper arm and a live one IS the execution cost. " + "A sell of a mint this key already paper-bought also closes that paper book when the " + "live wallet is empty, even if this flag is omitted."))
|
|
@@ -50432,12 +51759,13 @@ async function runStdioServer(env = process.env, transport = new StdioServerTran
|
|
|
50432
51759
|
}
|
|
50433
51760
|
var INSTRUCTIONS = `Candle is a trading and token-launch rail for agents. You hold a scoped API key, never a private key; signing and funding stay with the key owner's wallet.
|
|
50434
51761
|
|
|
50435
|
-
START HERE —
|
|
51762
|
+
START HERE — six tools need NO credential. Call these first to confirm the server is wired before asking anyone for anything:
|
|
50436
51763
|
candle_get_market price, market cap, volume, curve state for one token
|
|
50437
51764
|
candle_get_feed the roster: hot streak, new pairs, graduated, blue chip
|
|
50438
51765
|
candle_resolve_token a ticker or partial name -> mint address + chain
|
|
50439
51766
|
candle_token_forensics call this before quoting or buying. Returns the on-chain developer (never a launchpad shared authority; deployer.attribution names the launchpad or issuer when no developer is on chain), their went-to-zero rate and last coins, who bought in the deploy window (strangers in the same slot are the bundle signal), same-funder insider share, same-funder cluster, and safety.summary with six sourced flags. Refuse an unprompted buy when flagged; incomplete or unknown is not clearance. launch.deployerLaunches is an inclusive informational count, never a warning
|
|
50440
51767
|
candle_get_agent_profile your own tier, caps and verified activity
|
|
51768
|
+
candle_get_plans every plan's price, fees, limits and what it can do; quote these, never from memory
|
|
50441
51769
|
|
|
50442
51770
|
COVERAGE — read this before you treat an error as a broken server.
|
|
50443
51771
|
candle_get_feed indexes the wider market (pump.fun, pons.family and other external launchpads).
|
|
@@ -50481,7 +51809,7 @@ var init_server2 = __esm(() => {
|
|
|
50481
51809
|
// src/index.ts
|
|
50482
51810
|
import { spawn as spawn3 } from "node:child_process";
|
|
50483
51811
|
import { realpathSync } from "node:fs";
|
|
50484
|
-
import { chmod as chmod8, readFile as
|
|
51812
|
+
import { chmod as chmod8, readFile as readFile10, realpath, rename as rename6, unlink, writeFile as writeFile7 } from "node:fs/promises";
|
|
50485
51813
|
import { homedir as homedir6, hostname } from "node:os";
|
|
50486
51814
|
import { pathToFileURL } from "node:url";
|
|
50487
51815
|
|
|
@@ -50529,7 +51857,7 @@ init_render();
|
|
|
50529
51857
|
init_secret_store();
|
|
50530
51858
|
|
|
50531
51859
|
// src/version.ts
|
|
50532
|
-
var CLI_VERSION = "0.11.
|
|
51860
|
+
var CLI_VERSION = "0.11.15";
|
|
50533
51861
|
|
|
50534
51862
|
// src/commands/auth.ts
|
|
50535
51863
|
init_keys_embedded_wallet();
|
|
@@ -50945,6 +52273,11 @@ var ENVIRONMENT = [
|
|
|
50945
52273
|
name: "CANDLE_ENCLAVE_HELPER",
|
|
50946
52274
|
description: "Path to the signed candle-enclave.app helper (macOS)"
|
|
50947
52275
|
},
|
|
52276
|
+
{
|
|
52277
|
+
name: "CANDLE_HYPERLIQUID_BUILDER",
|
|
52278
|
+
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"
|
|
52279
|
+
},
|
|
52280
|
+
{ name: "CANDLE_HYPERLIQUID_NETWORK", description: "candle perps: set to testnet to trade Hyperliquid testnet" },
|
|
50948
52281
|
{ name: "CANDLE_NO_UPDATE_NOTIFIER", description: "Set to 1 to silence the update notice" },
|
|
50949
52282
|
{
|
|
50950
52283
|
name: "CANDLE_KEYSTORE_PASSPHRASE",
|
|
@@ -51017,7 +52350,7 @@ var HELP = {
|
|
|
51017
52350
|
doctor: {
|
|
51018
52351
|
group: "Start here",
|
|
51019
52352
|
summary: "Diagnose CLI setup: credentials, storage backend, API reachability, security key helper",
|
|
51020
|
-
description: "One PASS/FAIL/SKIP table over the runtime, the storage backend, both credentials, API reachability, wallet delegation, the install method and whether the security key helper (candle-fido2) is beside the binary. Its output is meant to be pasted into a bug report. Exits nonzero on any FAIL.",
|
|
52353
|
+
description: "One PASS/FAIL/SKIP table over the runtime, the storage backend, both credentials, API reachability, wallet delegation, the install method, the account's plan and its trade fee, and whether the security key helper (candle-fido2) is beside the binary. Its output is meant to be pasted into a bug report. Exits nonzero on any FAIL.",
|
|
51021
52354
|
usage: ["candle doctor [--role owner|bot|auto]"],
|
|
51022
52355
|
rows: [],
|
|
51023
52356
|
examples: ["candle doctor", "candle doctor --json"],
|
|
@@ -51026,7 +52359,7 @@ var HELP = {
|
|
|
51026
52359
|
swap: {
|
|
51027
52360
|
group: "Trade",
|
|
51028
52361
|
summary: "Quote, confirm and swap on Solana or Hood, bridge between them; read an operation by id",
|
|
51029
|
-
description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent, and the first buy after a launch is this command rather than part of the launch. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, the key needs a raw cap on the origin asset, and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault.",
|
|
52362
|
+
description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent, and the first buy after a launch is this command rather than part of the launch. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, the key needs a raw cap on the origin asset, and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and sell a token it holds that was not launched on Candle; buying such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force.",
|
|
51030
52363
|
usage: ["candle swap <from> <to> [flags]", "candle swap status <id>"],
|
|
51031
52364
|
rows: [
|
|
51032
52365
|
{
|
|
@@ -51090,6 +52423,64 @@ var HELP = {
|
|
|
51090
52423
|
],
|
|
51091
52424
|
env: ENV_API
|
|
51092
52425
|
},
|
|
52426
|
+
perps: {
|
|
52427
|
+
group: "Trade",
|
|
52428
|
+
summary: "Hyperliquid perpetuals from your EVM TEE wallet: setup, deposit, open, close, cancel, orders, positions, leverage",
|
|
52429
|
+
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. Every plan is eligible for perps, once the deployment has perps enabled. Every plan except Max pays Candle a builder fee on each order (0.1% today); Max pays none. candle plans shows the rate in force. Deposits move SOL, USDC, ETH or USDG onto the Hyperliquid account through Relay, and the account's P&L (candle pnl) carries a Hyperliquid section.",
|
|
52430
|
+
usage: ["candle perps <subcommand> [flags]"],
|
|
52431
|
+
rows: [
|
|
52432
|
+
{
|
|
52433
|
+
invocation: "setup [--wallet <tee>]",
|
|
52434
|
+
description: "Approve Candle's builder fee once; shows the account's mode and balance"
|
|
52435
|
+
},
|
|
52436
|
+
{
|
|
52437
|
+
invocation: "deposit <amount> <SOL|USDC|ETH|USDG> [--wallet <tee>] [--to <evm tee>] | deposit status <id>",
|
|
52438
|
+
description: "Move funds onto the Hyperliquid account through Relay, from the key's Solana or Hood TEE wallet (swap:write); status reads one"
|
|
52439
|
+
},
|
|
52440
|
+
{
|
|
52441
|
+
invocation: "open <coin> <long|short> <size> [--price <px>]",
|
|
52442
|
+
description: "Market (IOC) without --price, limit with it; optional --tp and --sl"
|
|
52443
|
+
},
|
|
52444
|
+
{ invocation: "close <coin> [--size <n>]", description: "Reduce-only IOC close of all or part of a position" },
|
|
52445
|
+
{ invocation: "cancel <cloid>", description: "Cancel an order Candle built, by its cloid" },
|
|
52446
|
+
{
|
|
52447
|
+
invocation: "orders [--wallet <tee>]",
|
|
52448
|
+
description: "Open orders on Hyperliquid, and every action Candle built"
|
|
52449
|
+
},
|
|
52450
|
+
{ invocation: "positions [--wallet <tee>]", description: "Positions and account value, read live" },
|
|
52451
|
+
{ invocation: "leverage <coin> <x> [--isolated]", description: "Set a market's leverage and margin mode" }
|
|
52452
|
+
],
|
|
52453
|
+
flags: [
|
|
52454
|
+
{
|
|
52455
|
+
invocation: "--wallet <tee>",
|
|
52456
|
+
description: "The EVM TEE wallet by id, address or unique label (default: the only one); for deposit, the paying TEE wallet"
|
|
52457
|
+
},
|
|
52458
|
+
{ invocation: "--tif <Gtc|Alo|Ioc>", description: "open with --price: time in force (default Gtc)" },
|
|
52459
|
+
{
|
|
52460
|
+
invocation: "--slippage-bps <n>",
|
|
52461
|
+
description: "open at market and close: the bound against the mid; deposit: Relay's slippage (default 100)"
|
|
52462
|
+
},
|
|
52463
|
+
{ invocation: "--tp <px> / --sl <px>", description: "open: reduce-only take-profit and stop-loss triggers" },
|
|
52464
|
+
{
|
|
52465
|
+
invocation: "--to <evm tee>",
|
|
52466
|
+
description: "deposit from Solana: the EVM TEE wallet whose account receives it"
|
|
52467
|
+
},
|
|
52468
|
+
{ invocation: "--id <clientDepositId>", description: "deposit: the idempotency id (default: a new one)" },
|
|
52469
|
+
{ invocation: "--rpc-url <url>", description: "deposit from Solana: the RPC the deposit check reads" },
|
|
52470
|
+
{ invocation: "--no-submit", description: "Build and check only; sign and submit nothing" },
|
|
52471
|
+
{ invocation: "--sign-only", description: "Build, check and sign through the relay; submit nothing" },
|
|
52472
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt" }
|
|
52473
|
+
],
|
|
52474
|
+
examples: [
|
|
52475
|
+
"candle perps setup",
|
|
52476
|
+
"candle perps deposit 25 USDC",
|
|
52477
|
+
"candle perps open BTC long 0.01",
|
|
52478
|
+
"candle perps open ETH short 0.5 --price 3200 --tp 2900 --sl 3400",
|
|
52479
|
+
"candle perps close BTC",
|
|
52480
|
+
"candle perps leverage BTC 5 --isolated"
|
|
52481
|
+
],
|
|
52482
|
+
env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK"]
|
|
52483
|
+
},
|
|
51093
52484
|
transfer: {
|
|
51094
52485
|
group: "Trade",
|
|
51095
52486
|
summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
|
|
@@ -51127,7 +52518,7 @@ var HELP = {
|
|
|
51127
52518
|
launch: {
|
|
51128
52519
|
group: "Trade",
|
|
51129
52520
|
summary: "Create a token on Solana or Hood (the first buy is a separate swap)",
|
|
51130
|
-
description: "Creates a token with no first buy, so the launch and the position are two decisions rather than one. Needs the launch:write scope and an operator-enabled allowLaunch. The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee), each only after the one before it landed.",
|
|
52521
|
+
description: "Creates a token with no first buy, so the launch and the position are two decisions rather than one. Needs the launch:write scope and an operator-enabled allowLaunch. The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans).",
|
|
51131
52522
|
usage: [
|
|
51132
52523
|
"candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--quote-asset <asset>] [--dex-version v3|v4]"
|
|
51133
52524
|
],
|
|
@@ -51151,29 +52542,47 @@ var HELP = {
|
|
|
51151
52542
|
},
|
|
51152
52543
|
portfolio: {
|
|
51153
52544
|
group: "Account",
|
|
51154
|
-
summary: "Every wallet's holdings, prices and value
|
|
51155
|
-
description: "Vault, TEE and embedded wallets, each token with amount, price and value, then
|
|
51156
|
-
usage: ["candle portfolio [--rpc-url <url>] [--json]"],
|
|
52545
|
+
summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
|
|
52546
|
+
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.",
|
|
52547
|
+
usage: ["candle portfolio [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
|
|
51157
52548
|
rows: [],
|
|
51158
52549
|
flags: [
|
|
52550
|
+
{ invocation: "--chain solana|hood", description: "Show one chain, and read nothing on the other" },
|
|
51159
52551
|
{
|
|
51160
52552
|
invocation: "--rpc-url <url>",
|
|
51161
52553
|
description: "Your Solana RPC, for the vault. Without one: CANDLE_SOLANA_RPC_URL, then the profile's, then the public endpoint"
|
|
51162
52554
|
},
|
|
52555
|
+
{
|
|
52556
|
+
invocation: "--evm-rpc-url <url>",
|
|
52557
|
+
description: "Your Hood RPC, for EVM vault keys. Without one: CANDLE_EVM_RPC_URL, then the built-in Hood RPC"
|
|
52558
|
+
},
|
|
51163
52559
|
KEYSTORE_FLAG
|
|
51164
52560
|
],
|
|
51165
|
-
examples: [
|
|
52561
|
+
examples: [
|
|
52562
|
+
"candle portfolio",
|
|
52563
|
+
"candle portfolio --chain hood",
|
|
52564
|
+
"candle portfolio --rpc-url https://your-rpc.example --evm-rpc-url https://your-hood-rpc.example --json"
|
|
52565
|
+
],
|
|
51166
52566
|
env: [...ENV_API, ...ENV_LOCAL_SIGNING]
|
|
51167
52567
|
},
|
|
51168
52568
|
pnl: {
|
|
51169
52569
|
group: "Account",
|
|
51170
|
-
summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's
|
|
51171
|
-
description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the
|
|
52570
|
+
summary: "P&L per chain and combined: realized, fees, unrealized and open positions (--profile for one key's share)",
|
|
52571
|
+
description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the console shows, to the cent for the same moment. Needs a key with the Read scope (account:read). With --profile <name>, that profile's key reads its share of the same P&L: realized on the sales it made, and the open positions that belong to it (the wallet's bound key, else the key whose buy opened them), with a row per wallet that sums to the total. Positions live in wallets: each wallet is its own average-cost pool, the account is the sum of its wallets, and a recorded move between two of the account's wallets carries its cost and realizes nothing. Both show a Solana block, a Hood block, then the combined figure, the closed positions as money made, minus money lost, plus partial sells, and a CHAIN column (and WALLET, where the API names it) on positions. The account's books add a By agent table (each key, then Manual for web-app trades, then the account total). Unpriced positions are shown as unpriced and never valued at zero; a position worth under one cent is marked dust and still counted. 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); Total stays the console's token figure, and Total with LP covers tokens and LP.",
|
|
51172
52572
|
usage: ["candle pnl [--profile <name>] [--json]"],
|
|
51173
52573
|
rows: [],
|
|
51174
52574
|
examples: ["candle pnl", "candle pnl --profile scalper --json"],
|
|
51175
52575
|
env: ENV_API
|
|
51176
52576
|
},
|
|
52577
|
+
plans: {
|
|
52578
|
+
group: "Account",
|
|
52579
|
+
summary: "The plans this API serves: prices, fees, limits and what each can do",
|
|
52580
|
+
description: "The plan table in force on the API this profile points at (GET /api/v1/agent/plans): each plan's monthly price, the agent trade fee, the Hyperliquid perps builder fee, the limits a new key gets, and one row per capability (buying and selling tokens not launched on Candle, self-launch, atomic launch, linked wallets, perps, limit orders). Every number is the server's; the CLI bundles none. Needs no key and sends none. Which plan this account is on is candle doctor's Plan row.",
|
|
52581
|
+
usage: ["candle plans [--json]"],
|
|
52582
|
+
rows: [],
|
|
52583
|
+
examples: ["candle plans", "candle plans --json", "candle plans --api-url https://staging.api.candle.tv"],
|
|
52584
|
+
env: ["CANDLE_API_URL"]
|
|
52585
|
+
},
|
|
51177
52586
|
keys: {
|
|
51178
52587
|
group: "Account",
|
|
51179
52588
|
summary: "API keys, and the wallets each key may use",
|
|
@@ -51583,9 +52992,16 @@ var HELP = {
|
|
|
51583
52992
|
group: "Maintain",
|
|
51584
52993
|
summary: "Verify a release asset's Sigstore bundle",
|
|
51585
52994
|
description: "Verifies a release asset against the trusted root compiled into this binary. No network, no credentials, and nothing else installed: the bundle carries the certificate and the transparency-log entry.",
|
|
51586
|
-
usage: ["candle verify <file> --bundle <path>"],
|
|
52995
|
+
usage: ["candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]"],
|
|
51587
52996
|
rows: [],
|
|
51588
|
-
flags: [
|
|
52997
|
+
flags: [
|
|
52998
|
+
{ invocation: "--bundle <path>", description: "The .sigstore.json bundle beside the asset" },
|
|
52999
|
+
{
|
|
53000
|
+
invocation: "--identity <uri>",
|
|
53001
|
+
description: "The signing identity to require; defaults to the release identity for the version in a latest.json beside the bundle"
|
|
53002
|
+
},
|
|
53003
|
+
{ invocation: "--issuer <url>", description: "The certificate issuer to require; defaults to GitHub Actions'" }
|
|
53004
|
+
],
|
|
51589
53005
|
examples: ["candle verify ./candle-darwin-arm64 --bundle ./candle-darwin-arm64.sigstore.json"]
|
|
51590
53006
|
},
|
|
51591
53007
|
completion: {
|
|
@@ -58957,6 +60373,7 @@ var MCP_TOOL_NAMES = [
|
|
|
58957
60373
|
"candle_get_feed",
|
|
58958
60374
|
"candle_token_forensics",
|
|
58959
60375
|
"candle_get_agent_profile",
|
|
60376
|
+
"candle_get_plans",
|
|
58960
60377
|
"candle_report_activity",
|
|
58961
60378
|
"candle_trade",
|
|
58962
60379
|
"candle_swap",
|
|
@@ -58970,14 +60387,23 @@ var MCP_TOOL_NAMES = [
|
|
|
58970
60387
|
"candle_get_portfolio",
|
|
58971
60388
|
"candle_resolve_token",
|
|
58972
60389
|
"candle_execution_status",
|
|
58973
|
-
"candle_get_operation"
|
|
60390
|
+
"candle_get_operation",
|
|
60391
|
+
"candle_perps_setup",
|
|
60392
|
+
"candle_perps_open",
|
|
60393
|
+
"candle_perps_close",
|
|
60394
|
+
"candle_perps_cancel",
|
|
60395
|
+
"candle_perps_orders",
|
|
60396
|
+
"candle_perps_positions",
|
|
60397
|
+
"candle_perps_leverage",
|
|
60398
|
+
"candle_perps_deposit"
|
|
58974
60399
|
];
|
|
58975
60400
|
var READ_ONLY_TOOL_NAMES = [
|
|
58976
60401
|
"candle_get_market",
|
|
58977
60402
|
"candle_get_feed",
|
|
58978
60403
|
"candle_token_forensics",
|
|
58979
60404
|
"candle_get_agent_profile",
|
|
58980
|
-
"candle_resolve_token"
|
|
60405
|
+
"candle_resolve_token",
|
|
60406
|
+
"candle_get_plans"
|
|
58981
60407
|
];
|
|
58982
60408
|
var CREDENTIAL_ENV_NAMES = [
|
|
58983
60409
|
"CANDLE_API_KEY",
|
|
@@ -59078,6 +60504,1141 @@ async function mcp(args, ctx) {
|
|
|
59078
60504
|
}
|
|
59079
60505
|
}
|
|
59080
60506
|
|
|
60507
|
+
// src/commands/perps.ts
|
|
60508
|
+
init_args();
|
|
60509
|
+
init_bridge();
|
|
60510
|
+
init_evm_lite();
|
|
60511
|
+
import { randomUUID as randomUUID4, sign as sign3 } from "node:crypto";
|
|
60512
|
+
|
|
60513
|
+
// src/hyperliquid.ts
|
|
60514
|
+
init_sha3();
|
|
60515
|
+
var HYPERLIQUID_ALLOWED_ACTION_TYPES = [
|
|
60516
|
+
"order",
|
|
60517
|
+
"cancel",
|
|
60518
|
+
"modify",
|
|
60519
|
+
"updateLeverage",
|
|
60520
|
+
"updateIsolatedMargin"
|
|
60521
|
+
];
|
|
60522
|
+
var HYPERLIQUID_EXCHANGE_URLS = {
|
|
60523
|
+
mainnet: "https://api.hyperliquid.xyz/exchange",
|
|
60524
|
+
testnet: "https://api.hyperliquid-testnet.xyz/exchange"
|
|
60525
|
+
};
|
|
60526
|
+
var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
|
|
60527
|
+
var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
|
|
60528
|
+
var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
|
|
60529
|
+
var HYPERLIQUID_SIGNATURE_CHAIN_ID = "0xa4b1";
|
|
60530
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
60531
|
+
var UINT64_MAX = (1n << 64n) - 1n;
|
|
60532
|
+
var INT64_MIN = -(1n << 63n);
|
|
60533
|
+
function pushUint(out, value, width) {
|
|
60534
|
+
for (let i = width - 1;i >= 0; i--)
|
|
60535
|
+
out.push(Number(value >> BigInt(i * 8) & 0xffn));
|
|
60536
|
+
}
|
|
60537
|
+
function packInteger(out, value) {
|
|
60538
|
+
if (value >= 0n) {
|
|
60539
|
+
if (value > UINT64_MAX)
|
|
60540
|
+
throw new Error("msgpack: integer above uint64");
|
|
60541
|
+
if (value < 0x80n)
|
|
60542
|
+
out.push(Number(value));
|
|
60543
|
+
else if (value <= 0xffn) {
|
|
60544
|
+
out.push(204);
|
|
60545
|
+
pushUint(out, value, 1);
|
|
60546
|
+
} else if (value <= 0xffffn) {
|
|
60547
|
+
out.push(205);
|
|
60548
|
+
pushUint(out, value, 2);
|
|
60549
|
+
} else if (value <= 0xffffffffn) {
|
|
60550
|
+
out.push(206);
|
|
60551
|
+
pushUint(out, value, 4);
|
|
60552
|
+
} else {
|
|
60553
|
+
out.push(207);
|
|
60554
|
+
pushUint(out, value, 8);
|
|
60555
|
+
}
|
|
60556
|
+
return;
|
|
60557
|
+
}
|
|
60558
|
+
if (value < INT64_MIN)
|
|
60559
|
+
throw new Error("msgpack: integer below int64");
|
|
60560
|
+
if (value >= -32n)
|
|
60561
|
+
out.push(Number(value & 0xffn));
|
|
60562
|
+
else if (value >= -128n) {
|
|
60563
|
+
out.push(208);
|
|
60564
|
+
pushUint(out, value & 0xffn, 1);
|
|
60565
|
+
} else if (value >= -32768n) {
|
|
60566
|
+
out.push(209);
|
|
60567
|
+
pushUint(out, value & 0xffffn, 2);
|
|
60568
|
+
} else if (value >= -2147483648n) {
|
|
60569
|
+
out.push(210);
|
|
60570
|
+
pushUint(out, value & 0xffffffffn, 4);
|
|
60571
|
+
} else {
|
|
60572
|
+
out.push(211);
|
|
60573
|
+
pushUint(out, value & UINT64_MAX, 8);
|
|
60574
|
+
}
|
|
60575
|
+
}
|
|
60576
|
+
function packHeader(out, length, fixBase, fixMax, codes) {
|
|
60577
|
+
if (length <= fixMax) {
|
|
60578
|
+
out.push(fixBase | length);
|
|
60579
|
+
return;
|
|
60580
|
+
}
|
|
60581
|
+
const [c8, c16, c32] = codes;
|
|
60582
|
+
if (c8 !== undefined && c8 !== 0 && length <= 255) {
|
|
60583
|
+
out.push(c8);
|
|
60584
|
+
pushUint(out, BigInt(length), 1);
|
|
60585
|
+
} else if (length <= 65535) {
|
|
60586
|
+
out.push(c16);
|
|
60587
|
+
pushUint(out, BigInt(length), 2);
|
|
60588
|
+
} else {
|
|
60589
|
+
out.push(c32);
|
|
60590
|
+
pushUint(out, BigInt(length), 4);
|
|
60591
|
+
}
|
|
60592
|
+
}
|
|
60593
|
+
function packValue(out, value) {
|
|
60594
|
+
if (value === null)
|
|
60595
|
+
out.push(192);
|
|
60596
|
+
else if (value === false)
|
|
60597
|
+
out.push(194);
|
|
60598
|
+
else if (value === true)
|
|
60599
|
+
out.push(195);
|
|
60600
|
+
else if (typeof value === "bigint")
|
|
60601
|
+
packInteger(out, value);
|
|
60602
|
+
else if (typeof value === "number") {
|
|
60603
|
+
if (!Number.isSafeInteger(value))
|
|
60604
|
+
throw new Error(`msgpack: ${value} is not a safe integer`);
|
|
60605
|
+
packInteger(out, BigInt(value));
|
|
60606
|
+
} else if (typeof value === "string") {
|
|
60607
|
+
const bytes = new TextEncoder().encode(value);
|
|
60608
|
+
packHeader(out, bytes.length, 160, 31, [217, 218, 219]);
|
|
60609
|
+
for (const b of bytes)
|
|
60610
|
+
out.push(b);
|
|
60611
|
+
} else if (Array.isArray(value)) {
|
|
60612
|
+
packHeader(out, value.length, 144, 15, [0, 220, 221]);
|
|
60613
|
+
for (const item of value)
|
|
60614
|
+
packValue(out, item);
|
|
60615
|
+
} else if (typeof value === "object") {
|
|
60616
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
60617
|
+
for (const [key] of entries) {
|
|
60618
|
+
if (/^(0|[1-9][0-9]*)$/.test(key))
|
|
60619
|
+
throw new Error(`msgpack: integer-like key "${key}"`);
|
|
60620
|
+
}
|
|
60621
|
+
packHeader(out, entries.length, 128, 15, [0, 222, 223]);
|
|
60622
|
+
for (const [key, item] of entries) {
|
|
60623
|
+
packValue(out, key);
|
|
60624
|
+
packValue(out, item);
|
|
60625
|
+
}
|
|
60626
|
+
} else {
|
|
60627
|
+
throw new Error(`msgpack: cannot encode a ${typeof value}`);
|
|
60628
|
+
}
|
|
60629
|
+
}
|
|
60630
|
+
function msgpackEncode(value) {
|
|
60631
|
+
const out = [];
|
|
60632
|
+
packValue(out, value);
|
|
60633
|
+
return Uint8Array.from(out);
|
|
60634
|
+
}
|
|
60635
|
+
function toHex(bytes) {
|
|
60636
|
+
let out = "";
|
|
60637
|
+
for (const b of bytes)
|
|
60638
|
+
out += b.toString(16).padStart(2, "0");
|
|
60639
|
+
return out;
|
|
60640
|
+
}
|
|
60641
|
+
function hyperliquidActionHash(action, nonce) {
|
|
60642
|
+
const n = BigInt(nonce);
|
|
60643
|
+
if (n < 0n || n > UINT64_MAX)
|
|
60644
|
+
throw new Error("hyperliquidActionHash: nonce out of range");
|
|
60645
|
+
const out = Array.from(msgpackEncode(action));
|
|
60646
|
+
pushUint(out, n, 8);
|
|
60647
|
+
out.push(0);
|
|
60648
|
+
return `0x${toHex(keccak_256(Uint8Array.from(out)))}`;
|
|
60649
|
+
}
|
|
60650
|
+
function hyperliquidL1TypedData(connectionId, network) {
|
|
60651
|
+
return {
|
|
60652
|
+
domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS },
|
|
60653
|
+
types: {
|
|
60654
|
+
Agent: [
|
|
60655
|
+
{ name: "source", type: "string" },
|
|
60656
|
+
{ name: "connectionId", type: "bytes32" }
|
|
60657
|
+
]
|
|
60658
|
+
},
|
|
60659
|
+
primary_type: "Agent",
|
|
60660
|
+
message: { source: network === "mainnet" ? "a" : "b", connectionId }
|
|
60661
|
+
};
|
|
60662
|
+
}
|
|
60663
|
+
function hyperliquidApproveBuilderFeeTypedData(builder, nonce) {
|
|
60664
|
+
return {
|
|
60665
|
+
domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS },
|
|
60666
|
+
types: {
|
|
60667
|
+
"HyperliquidTransaction:ApproveBuilderFee": [
|
|
60668
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
60669
|
+
{ name: "maxFeeRate", type: "string" },
|
|
60670
|
+
{ name: "builder", type: "address" },
|
|
60671
|
+
{ name: "nonce", type: "uint64" }
|
|
60672
|
+
]
|
|
60673
|
+
},
|
|
60674
|
+
primary_type: "HyperliquidTransaction:ApproveBuilderFee",
|
|
60675
|
+
message: {
|
|
60676
|
+
hyperliquidChain: "Mainnet",
|
|
60677
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
|
|
60678
|
+
builder: builder.toLowerCase(),
|
|
60679
|
+
nonce
|
|
60680
|
+
}
|
|
60681
|
+
};
|
|
60682
|
+
}
|
|
60683
|
+
function hyperliquidCanonicalJson(value) {
|
|
60684
|
+
return canonical(value);
|
|
60685
|
+
}
|
|
60686
|
+
function canonical(value) {
|
|
60687
|
+
if (value === null || typeof value !== "object")
|
|
60688
|
+
return JSON.stringify(value);
|
|
60689
|
+
if (Array.isArray(value))
|
|
60690
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
60691
|
+
const record = value;
|
|
60692
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
|
|
60693
|
+
}
|
|
60694
|
+
function sameValue(a, b) {
|
|
60695
|
+
return canonical(a) === canonical(b);
|
|
60696
|
+
}
|
|
60697
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
60698
|
+
function checkBuilderField(action, builder) {
|
|
60699
|
+
if (!("builder" in action) || action.builder === undefined)
|
|
60700
|
+
return null;
|
|
60701
|
+
const field = action.builder;
|
|
60702
|
+
if (typeof field !== "object" || field === null)
|
|
60703
|
+
return "builder field is not an object";
|
|
60704
|
+
if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
|
|
60705
|
+
return "the order names a builder that is not Candle's";
|
|
60706
|
+
}
|
|
60707
|
+
if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS) {
|
|
60708
|
+
return "the builder fee is outside Candle's 0.1% maximum";
|
|
60709
|
+
}
|
|
60710
|
+
return null;
|
|
60711
|
+
}
|
|
60712
|
+
function decimal(value) {
|
|
60713
|
+
if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
|
|
60714
|
+
return null;
|
|
60715
|
+
const negative = value.startsWith("-");
|
|
60716
|
+
const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
|
|
60717
|
+
const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
|
|
60718
|
+
return negative ? -n : n;
|
|
60719
|
+
}
|
|
60720
|
+
function checkActionIntent(action, intent) {
|
|
60721
|
+
const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
|
|
60722
|
+
const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
|
|
60723
|
+
if (!Array.isArray(orderList))
|
|
60724
|
+
return "invalid orders";
|
|
60725
|
+
for (const raw of orderList) {
|
|
60726
|
+
if (!raw || typeof raw !== "object")
|
|
60727
|
+
return "invalid order";
|
|
60728
|
+
const o = raw;
|
|
60729
|
+
if (!mainAsset(o.a))
|
|
60730
|
+
return "order asset is outside the main perp universe";
|
|
60731
|
+
if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal(o.s) ?? 0n) <= 0n || (decimal(o.p) ?? 0n) <= 0n)
|
|
60732
|
+
return "invalid order side, size, price or reduce-only flag";
|
|
60733
|
+
}
|
|
60734
|
+
if (action.type === "cancel") {
|
|
60735
|
+
if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
|
|
60736
|
+
return "invalid cancel asset";
|
|
60737
|
+
}
|
|
60738
|
+
if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
|
|
60739
|
+
return "invalid action asset";
|
|
60740
|
+
if (!intent)
|
|
60741
|
+
return null;
|
|
60742
|
+
const { method, params: p } = intent;
|
|
60743
|
+
const types2 = {
|
|
60744
|
+
setup: "approveBuilderFee",
|
|
60745
|
+
open: "order",
|
|
60746
|
+
close: "order",
|
|
60747
|
+
cancel: "cancel",
|
|
60748
|
+
modify: "modify",
|
|
60749
|
+
leverage: "updateLeverage",
|
|
60750
|
+
margin: "updateIsolatedMargin"
|
|
60751
|
+
};
|
|
60752
|
+
if (action.type !== types2[method])
|
|
60753
|
+
return "action type does not match the requested method";
|
|
60754
|
+
if (method === "leverage") {
|
|
60755
|
+
if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
|
|
60756
|
+
return "leverage or mode does not match the request";
|
|
60757
|
+
}
|
|
60758
|
+
if (method === "margin") {
|
|
60759
|
+
const amount = decimal(p.amount);
|
|
60760
|
+
if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
|
|
60761
|
+
return "isolated margin amount does not match the request";
|
|
60762
|
+
}
|
|
60763
|
+
if (method === "open" || method === "close" || method === "modify") {
|
|
60764
|
+
const list = orderList;
|
|
60765
|
+
const first = list[0];
|
|
60766
|
+
if (!first)
|
|
60767
|
+
return "missing requested order";
|
|
60768
|
+
const typ = first.t;
|
|
60769
|
+
if (!typ?.limit)
|
|
60770
|
+
return "requested order is not a limit/IOC order";
|
|
60771
|
+
if (method === "open") {
|
|
60772
|
+
const buy = p.side === "long" || p.side === "buy";
|
|
60773
|
+
if (first.b !== buy || first.r !== false || decimal(first.s) !== decimal(p.size))
|
|
60774
|
+
return "order side, size or reduce-only flag does not match the request";
|
|
60775
|
+
const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
|
|
60776
|
+
if (typ.limit.tif !== tif)
|
|
60777
|
+
return "order time in force does not match the request";
|
|
60778
|
+
const triggers = [
|
|
60779
|
+
["takeProfit", "tp"],
|
|
60780
|
+
["stopLoss", "sl"]
|
|
60781
|
+
].filter(([field]) => p[field] != null);
|
|
60782
|
+
if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
|
|
60783
|
+
return "unexpected extra orders or grouping";
|
|
60784
|
+
for (let i = 0;i < triggers.length; i++) {
|
|
60785
|
+
const [field, kind] = triggers[i];
|
|
60786
|
+
const leg = list[i + 1];
|
|
60787
|
+
if (!leg)
|
|
60788
|
+
return "missing trigger order";
|
|
60789
|
+
const trigger = leg.t?.trigger;
|
|
60790
|
+
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]))
|
|
60791
|
+
return "trigger side, size, reduce-only flag or price does not match the request";
|
|
60792
|
+
}
|
|
60793
|
+
} else {
|
|
60794
|
+
if (list.length !== 1)
|
|
60795
|
+
return "unexpected extra orders";
|
|
60796
|
+
if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
|
|
60797
|
+
return "close size or reduce-only flag does not match the request";
|
|
60798
|
+
if (method === "modify" && p.size != null && decimal(first.s) !== decimal(p.size))
|
|
60799
|
+
return "replacement size does not match the request";
|
|
60800
|
+
}
|
|
60801
|
+
if (p.price != null && decimal(first.p) !== decimal(p.price))
|
|
60802
|
+
return "order price does not match the request";
|
|
60803
|
+
}
|
|
60804
|
+
return null;
|
|
60805
|
+
}
|
|
60806
|
+
async function hyperliquidCloseOrder(fetcher, network, address, params) {
|
|
60807
|
+
const read = async (body) => {
|
|
60808
|
+
const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS[network].replace("/exchange", "/info"), {
|
|
60809
|
+
method: "POST",
|
|
60810
|
+
headers: { "content-type": "application/json" },
|
|
60811
|
+
body: JSON.stringify(body)
|
|
60812
|
+
});
|
|
60813
|
+
if (!res.ok)
|
|
60814
|
+
throw new Error("Cannot read the close position from Hyperliquid");
|
|
60815
|
+
return JSON.parse(await res.text());
|
|
60816
|
+
};
|
|
60817
|
+
const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
|
|
60818
|
+
const coin = String(params.coin).toUpperCase();
|
|
60819
|
+
const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
|
|
60820
|
+
const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
|
|
60821
|
+
const signed = decimal(szi);
|
|
60822
|
+
if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
|
|
60823
|
+
throw new Error("No main-universe position to close");
|
|
60824
|
+
const held = signed < 0n ? -signed : signed;
|
|
60825
|
+
const asked = params.size == null ? held : decimal(params.size);
|
|
60826
|
+
if (asked === null || asked <= 0n)
|
|
60827
|
+
throw new Error("Invalid close size");
|
|
60828
|
+
const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
|
|
60829
|
+
return { asset, isBuy: signed < 0n, size };
|
|
60830
|
+
}
|
|
60831
|
+
function verifyPerpsBuild(build, opts) {
|
|
60832
|
+
try {
|
|
60833
|
+
const network = opts.network ?? "mainnet";
|
|
60834
|
+
if (!EVM_ADDRESS_RE.test(opts.builder))
|
|
60835
|
+
return { ok: false, reason: "no valid Candle builder address to check against" };
|
|
60836
|
+
if (build.network !== undefined && build.network !== network) {
|
|
60837
|
+
return { ok: false, reason: `the build is for ${build.network}, this client trades ${network}` };
|
|
60838
|
+
}
|
|
60839
|
+
if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
|
|
60840
|
+
return { ok: false, reason: "the build is for a different wallet" };
|
|
60841
|
+
}
|
|
60842
|
+
if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
|
|
60843
|
+
return { ok: false, reason: "invalid nonce" };
|
|
60844
|
+
const typed = build.typedData;
|
|
60845
|
+
if (!typed || typeof typed !== "object")
|
|
60846
|
+
return { ok: false, reason: "no typed data" };
|
|
60847
|
+
const action = build.action;
|
|
60848
|
+
if (!action || typeof action !== "object")
|
|
60849
|
+
return { ok: false, reason: "no action" };
|
|
60850
|
+
const intentRefusal = checkActionIntent(action, opts.intent);
|
|
60851
|
+
if (intentRefusal)
|
|
60852
|
+
return { ok: false, reason: intentRefusal };
|
|
60853
|
+
if (opts.closeOrder) {
|
|
60854
|
+
const order = action.orders[0];
|
|
60855
|
+
if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
|
|
60856
|
+
return { ok: false, reason: "close side, asset or size does not match the requested position" };
|
|
60857
|
+
}
|
|
60858
|
+
if (typed.primary_type === "Agent") {
|
|
60859
|
+
const type = action.type;
|
|
60860
|
+
if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
|
|
60861
|
+
return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
|
|
60862
|
+
}
|
|
60863
|
+
if (type === "order") {
|
|
60864
|
+
const refusal = checkBuilderField(action, opts.builder);
|
|
60865
|
+
if (refusal)
|
|
60866
|
+
return { ok: false, reason: refusal };
|
|
60867
|
+
} else if ("builder" in action) {
|
|
60868
|
+
return { ok: false, reason: `a ${type} action carries no builder` };
|
|
60869
|
+
}
|
|
60870
|
+
if ("vaultAddress" in action)
|
|
60871
|
+
return { ok: false, reason: "the action names a vault" };
|
|
60872
|
+
const expected = hyperliquidL1TypedData(hyperliquidActionHash(action, build.nonce), network);
|
|
60873
|
+
if (!sameValue(typed, expected)) {
|
|
60874
|
+
return { ok: false, reason: "the typed data does not commit to this action and nonce" };
|
|
60875
|
+
}
|
|
60876
|
+
return { ok: true, kind: "l1" };
|
|
60877
|
+
}
|
|
60878
|
+
if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
|
|
60879
|
+
const expected = hyperliquidApproveBuilderFeeTypedData(opts.builder, build.nonce);
|
|
60880
|
+
if (!sameValue(typed, expected)) {
|
|
60881
|
+
return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
|
|
60882
|
+
}
|
|
60883
|
+
const expectedAction = {
|
|
60884
|
+
type: "approveBuilderFee",
|
|
60885
|
+
hyperliquidChain: "Mainnet",
|
|
60886
|
+
signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID,
|
|
60887
|
+
maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
|
|
60888
|
+
builder: opts.builder.toLowerCase(),
|
|
60889
|
+
nonce: build.nonce
|
|
60890
|
+
};
|
|
60891
|
+
if (!sameValue(action, expectedAction)) {
|
|
60892
|
+
return { ok: false, reason: "the approval action does not match its typed data" };
|
|
60893
|
+
}
|
|
60894
|
+
return { ok: true, kind: "approveBuilderFee" };
|
|
60895
|
+
}
|
|
60896
|
+
return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
|
|
60897
|
+
} catch (err) {
|
|
60898
|
+
return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
|
|
60899
|
+
}
|
|
60900
|
+
}
|
|
60901
|
+
function hyperliquidSplitSignature(signature) {
|
|
60902
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
60903
|
+
throw new Error("Not a 65-byte signature");
|
|
60904
|
+
let v = Number.parseInt(signature.slice(130, 132), 16);
|
|
60905
|
+
if (v < 27)
|
|
60906
|
+
v += 27;
|
|
60907
|
+
return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
|
|
60908
|
+
}
|
|
60909
|
+
function hyperliquidExchangeBody(action, nonce, signature) {
|
|
60910
|
+
return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
|
|
60911
|
+
}
|
|
60912
|
+
function hyperliquidRelayBody(typedData) {
|
|
60913
|
+
return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
|
|
60914
|
+
}
|
|
60915
|
+
|
|
60916
|
+
// src/commands/perps.ts
|
|
60917
|
+
init_render();
|
|
60918
|
+
init_trading();
|
|
60919
|
+
var PERPS_SCOPE = "perps:write";
|
|
60920
|
+
var DEC_RE = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
|
|
60921
|
+
var CLOID_RE = /^0x[0-9a-fA-F]{32}$/;
|
|
60922
|
+
function usage4(ctx, line) {
|
|
60923
|
+
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
60924
|
+
return 2;
|
|
60925
|
+
}
|
|
60926
|
+
async function perpsRequest(ctx, key, path, body) {
|
|
60927
|
+
const result = await apiRequest(path, {
|
|
60928
|
+
apiUrl: ctx.apiUrl,
|
|
60929
|
+
credentials: { apiKey: key },
|
|
60930
|
+
auth: "key",
|
|
60931
|
+
method: body ? "POST" : "GET",
|
|
60932
|
+
body,
|
|
60933
|
+
fetch: ctx.deps.fetch,
|
|
60934
|
+
env: ctx.deps.env
|
|
60935
|
+
});
|
|
60936
|
+
if (!result.ok) {
|
|
60937
|
+
if (result.status === 404 && !result.code)
|
|
60938
|
+
throw new TradingError("PERPS_NOT_ENABLED", "This Candle deployment does not serve perps routes (HYPERLIQUID_ENABLED is off there, or the API predates them).");
|
|
60939
|
+
throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
|
|
60940
|
+
}
|
|
60941
|
+
if (!result.body || typeof result.body !== "object")
|
|
60942
|
+
throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
|
|
60943
|
+
return result.body;
|
|
60944
|
+
}
|
|
60945
|
+
function network(ctx) {
|
|
60946
|
+
return ctx.deps.env.CANDLE_HYPERLIQUID_NETWORK === "testnet" ? "testnet" : "mainnet";
|
|
60947
|
+
}
|
|
60948
|
+
async function builderPin(ctx, key) {
|
|
60949
|
+
const pinned = ctx.deps.env.CANDLE_HYPERLIQUID_BUILDER ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
|
|
60950
|
+
if (pinned) {
|
|
60951
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(pinned))
|
|
60952
|
+
throw new TradingError("INVALID_ADDRESS", "CANDLE_HYPERLIQUID_BUILDER must be an EVM address.");
|
|
60953
|
+
return pinned.toLowerCase();
|
|
60954
|
+
}
|
|
60955
|
+
const config = await perpsRequest(ctx, key, "/api/v1/agent/perps/config");
|
|
60956
|
+
if (typeof config.builder !== "string")
|
|
60957
|
+
throw new TradingError("PERPS_NOT_CONFIGURED", "The server reports no Hyperliquid builder address.");
|
|
60958
|
+
return config.builder.toLowerCase();
|
|
60959
|
+
}
|
|
60960
|
+
async function perpsWallet(ctx, key, name2, scope) {
|
|
60961
|
+
const { rows, appId, keyPrefix } = await listTradingWallets(ctx, key, scope);
|
|
60962
|
+
const evm = rows.filter((row2) => row2.chain === "evm");
|
|
60963
|
+
const matches = name2 === undefined ? evm : evm.filter((row2) => matchesName(row2, name2));
|
|
60964
|
+
if (matches.length !== 1) {
|
|
60965
|
+
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("; ")}.`);
|
|
60966
|
+
}
|
|
60967
|
+
const row = matches[0];
|
|
60968
|
+
if (scope === undefined)
|
|
60969
|
+
return { row };
|
|
60970
|
+
const wallet = await completeTradingWallet(ctx, row, appId, scope, "hood", {
|
|
60971
|
+
apiKey: key,
|
|
60972
|
+
...keyPrefix !== undefined ? { keyPrefix } : {}
|
|
60973
|
+
});
|
|
60974
|
+
return { row, wallet };
|
|
60975
|
+
}
|
|
60976
|
+
function typedDataAuthorization(wallet, typedData) {
|
|
60977
|
+
const body = hyperliquidRelayBody(typedData);
|
|
60978
|
+
const payload = hyperliquidCanonicalJson({
|
|
60979
|
+
body,
|
|
60980
|
+
headers: { "privy-app-id": wallet.appId },
|
|
60981
|
+
method: "POST",
|
|
60982
|
+
url: `https://api.privy.io/v1/wallets/${wallet.privyWalletId}/rpc`,
|
|
60983
|
+
version: 1
|
|
60984
|
+
});
|
|
60985
|
+
return { body, authorizationSignature: sign3("sha256", Buffer.from(payload), wallet.signer).toString("base64") };
|
|
60986
|
+
}
|
|
60987
|
+
function previewLines(build) {
|
|
60988
|
+
const action = build.action;
|
|
60989
|
+
if (action.type === "approveBuilderFee")
|
|
60990
|
+
return [`Approve Candle's builder ${safeText(action.builder)} at ${safeText(action.maxFeeRate)} (once).`];
|
|
60991
|
+
if (action.type === "cancel")
|
|
60992
|
+
return action.cancels.map((o) => `Cancel asset ${o.a} order ${o.o}.`);
|
|
60993
|
+
if (action.type === "updateLeverage")
|
|
60994
|
+
return [
|
|
60995
|
+
`Set asset ${safeText(action.asset)} to ${safeText(action.leverage)}x ${action.isCross ? "cross" : "isolated"}.`
|
|
60996
|
+
];
|
|
60997
|
+
const orders = action.type === "modify" ? [action.order] : action.orders;
|
|
60998
|
+
const lines = orders.map((o) => {
|
|
60999
|
+
const type = o.t;
|
|
61000
|
+
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" : ""}.`;
|
|
61001
|
+
});
|
|
61002
|
+
const builder = action.builder;
|
|
61003
|
+
lines.push(builder ? `Candle builder ${safeText(builder.b)} fee: ${builder.f / 10} bps.` : "Candle builder fee: none.");
|
|
61004
|
+
return lines;
|
|
61005
|
+
}
|
|
61006
|
+
async function confirm(ctx, build, address, yes) {
|
|
61007
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
61008
|
+
output.write(`Hyperliquid account: ${safeText(address)} (${safeText(build.network)})
|
|
61009
|
+
`);
|
|
61010
|
+
for (const line of previewLines(build))
|
|
61011
|
+
output.write(`${line}
|
|
61012
|
+
`);
|
|
61013
|
+
if (yes)
|
|
61014
|
+
return true;
|
|
61015
|
+
if (!ctx.deps.isTTY.stdin)
|
|
61016
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes.");
|
|
61017
|
+
return (await ctx.deps.promptLine("Sign and submit? [y/N] ")).trim().toLowerCase() === "y";
|
|
61018
|
+
}
|
|
61019
|
+
function venueRefusal(exchange) {
|
|
61020
|
+
if (!exchange || typeof exchange !== "object")
|
|
61021
|
+
return "Hyperliquid returned no answer.";
|
|
61022
|
+
const answer = exchange;
|
|
61023
|
+
if (answer.status !== "ok")
|
|
61024
|
+
return `Hyperliquid refused it: ${safeText(JSON.stringify(answer.response ?? answer))}`;
|
|
61025
|
+
const statuses = answer.response?.data?.statuses;
|
|
61026
|
+
for (const status of statuses ?? []) {
|
|
61027
|
+
if (status && typeof status === "object" && "error" in status)
|
|
61028
|
+
return `Hyperliquid refused the order: ${safeText(status.error)}`;
|
|
61029
|
+
}
|
|
61030
|
+
return null;
|
|
61031
|
+
}
|
|
61032
|
+
async function runPerpsWrite(ctx, opts) {
|
|
61033
|
+
const key = await tradingKey(ctx);
|
|
61034
|
+
const { wallet } = await perpsWallet(ctx, key, opts.wallet, PERPS_SCOPE);
|
|
61035
|
+
if (!wallet)
|
|
61036
|
+
throw new TradingError("TEE_WALLET_REQUIRED", "No EVM TEE wallet resolved.");
|
|
61037
|
+
const builder = await builderPin(ctx, key);
|
|
61038
|
+
const build = await perpsRequest(ctx, key, opts.path, { walletId: wallet.id, ...opts.body });
|
|
61039
|
+
if (build.ready === true)
|
|
61040
|
+
return printTradingResult(ctx, build);
|
|
61041
|
+
const net = network(ctx);
|
|
61042
|
+
const method = opts.path.split("/").at(-1);
|
|
61043
|
+
const check = verifyPerpsBuild(build, {
|
|
61044
|
+
builder,
|
|
61045
|
+
network: net,
|
|
61046
|
+
address: wallet.address,
|
|
61047
|
+
intent: { method, params: opts.body }
|
|
61048
|
+
});
|
|
61049
|
+
if (!check.ok)
|
|
61050
|
+
throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this build: ${check.reason}. Nothing was signed.`);
|
|
61051
|
+
if (method === "close") {
|
|
61052
|
+
const closeOrder = await hyperliquidCloseOrder(ctx.deps.fetch, net, wallet.address, opts.body);
|
|
61053
|
+
const closeCheck = verifyPerpsBuild(build, {
|
|
61054
|
+
builder,
|
|
61055
|
+
network: net,
|
|
61056
|
+
address: wallet.address,
|
|
61057
|
+
intent: { method, params: opts.body },
|
|
61058
|
+
closeOrder
|
|
61059
|
+
});
|
|
61060
|
+
if (!closeCheck.ok)
|
|
61061
|
+
throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
|
|
61062
|
+
}
|
|
61063
|
+
if (!opts.submit)
|
|
61064
|
+
return printTradingResult(ctx, { ...build, signed: false, submitted: false });
|
|
61065
|
+
if (!await confirm(ctx, build, wallet.address, opts.yes)) {
|
|
61066
|
+
throw new TradingError("CANCELLED", "Nothing was signed.");
|
|
61067
|
+
}
|
|
61068
|
+
const relay = await perpsRequest(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(wallet.id)}/sign`, typedDataAuthorization(wallet, build.typedData));
|
|
61069
|
+
const signature = relay.signature;
|
|
61070
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
61071
|
+
throw new TradingError("INVALID_RESPONSE", "The relay did not return a signature.");
|
|
61072
|
+
const exchangeBody = hyperliquidExchangeBody(build.action, build.nonce, signature);
|
|
61073
|
+
if (opts.signOnly)
|
|
61074
|
+
return printTradingResult(ctx, {
|
|
61075
|
+
success: true,
|
|
61076
|
+
perpOrderId: build.perpOrderId,
|
|
61077
|
+
kind: build.kind,
|
|
61078
|
+
network: build.network,
|
|
61079
|
+
address: wallet.address,
|
|
61080
|
+
nonce: build.nonce,
|
|
61081
|
+
signature,
|
|
61082
|
+
submitted: false,
|
|
61083
|
+
exchangeBody
|
|
61084
|
+
});
|
|
61085
|
+
let exchange;
|
|
61086
|
+
try {
|
|
61087
|
+
const res = await ctx.deps.fetch(HYPERLIQUID_EXCHANGE_URLS[net], {
|
|
61088
|
+
method: "POST",
|
|
61089
|
+
headers: { "content-type": "application/json" },
|
|
61090
|
+
body: JSON.stringify(exchangeBody)
|
|
61091
|
+
});
|
|
61092
|
+
const text = await res.text();
|
|
61093
|
+
if (!res.ok)
|
|
61094
|
+
throw new Error(`HTTP ${res.status}`);
|
|
61095
|
+
exchange = JSON.parse(text);
|
|
61096
|
+
} catch (error) {
|
|
61097
|
+
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 } });
|
|
61098
|
+
}
|
|
61099
|
+
const refusal = venueRefusal(exchange);
|
|
61100
|
+
if (refusal)
|
|
61101
|
+
throw new TradingError("PERPS_VENUE_REFUSED", refusal, {
|
|
61102
|
+
details: { perpOrderId: build.perpOrderId, exchange }
|
|
61103
|
+
});
|
|
61104
|
+
return printTradingResult(ctx, {
|
|
61105
|
+
success: true,
|
|
61106
|
+
perpOrderId: build.perpOrderId,
|
|
61107
|
+
kind: build.kind,
|
|
61108
|
+
network: build.network,
|
|
61109
|
+
address: wallet.address,
|
|
61110
|
+
...build.cloid ? { cloid: build.cloid } : {},
|
|
61111
|
+
...build.childCloids ? { childCloids: build.childCloids } : {},
|
|
61112
|
+
nonce: build.nonce,
|
|
61113
|
+
signature,
|
|
61114
|
+
exchange
|
|
61115
|
+
});
|
|
61116
|
+
}
|
|
61117
|
+
var WRITE_FLAGS = { valueFlags: ["--wallet"], booleanFlags: ["--yes", "--no-submit", "--sign-only"] };
|
|
61118
|
+
function sharedWrite(values, booleans) {
|
|
61119
|
+
return {
|
|
61120
|
+
...values["--wallet"] !== undefined ? { wallet: values["--wallet"] } : {},
|
|
61121
|
+
yes: booleans.has("--yes"),
|
|
61122
|
+
submit: !booleans.has("--no-submit"),
|
|
61123
|
+
signOnly: booleans.has("--sign-only")
|
|
61124
|
+
};
|
|
61125
|
+
}
|
|
61126
|
+
function slippage(value) {
|
|
61127
|
+
if (value === undefined)
|
|
61128
|
+
return;
|
|
61129
|
+
const n = Number(value);
|
|
61130
|
+
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
61131
|
+
}
|
|
61132
|
+
async function perpsSetup(args, ctx) {
|
|
61133
|
+
const parsed = parseArgs(args, WRITE_FLAGS);
|
|
61134
|
+
if ("error" in parsed)
|
|
61135
|
+
return usage4(ctx, parsed.error);
|
|
61136
|
+
if (parsed.positionals.length !== 0)
|
|
61137
|
+
return usage4(ctx, "Usage: candle perps setup [--wallet <tee>] [--yes]");
|
|
61138
|
+
try {
|
|
61139
|
+
return await runPerpsWrite(ctx, {
|
|
61140
|
+
path: "/api/v1/agent/perps/setup",
|
|
61141
|
+
body: {},
|
|
61142
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
61143
|
+
});
|
|
61144
|
+
} catch (error) {
|
|
61145
|
+
return tradingFailure(ctx, error);
|
|
61146
|
+
}
|
|
61147
|
+
}
|
|
61148
|
+
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]";
|
|
61149
|
+
async function perpsOpen(args, ctx) {
|
|
61150
|
+
const parsed = parseArgs(args, {
|
|
61151
|
+
valueFlags: ["--wallet", "--price", "--tif", "--slippage-bps", "--tp", "--sl"],
|
|
61152
|
+
booleanFlags: ["--yes", "--no-submit", "--sign-only"]
|
|
61153
|
+
});
|
|
61154
|
+
if ("error" in parsed)
|
|
61155
|
+
return usage4(ctx, parsed.error);
|
|
61156
|
+
const [coin, side, size] = parsed.positionals;
|
|
61157
|
+
if (parsed.positionals.length !== 3 || !coin || !side || !size)
|
|
61158
|
+
return usage4(ctx, OPEN_USAGE);
|
|
61159
|
+
if (side !== "long" && side !== "short")
|
|
61160
|
+
return usage4(ctx, OPEN_USAGE);
|
|
61161
|
+
if (!DEC_RE.test(size))
|
|
61162
|
+
return usage4(ctx, "The size must be a plain decimal, e.g. 0.01.");
|
|
61163
|
+
const { values } = parsed;
|
|
61164
|
+
for (const flag of ["--price", "--tp", "--sl"]) {
|
|
61165
|
+
const value = values[flag];
|
|
61166
|
+
if (value !== undefined && !DEC_RE.test(value))
|
|
61167
|
+
return usage4(ctx, `${flag} must be a plain decimal.`);
|
|
61168
|
+
}
|
|
61169
|
+
const tif = values["--tif"];
|
|
61170
|
+
if (tif !== undefined && !["Gtc", "Alo", "Ioc"].includes(tif))
|
|
61171
|
+
return usage4(ctx, "--tif must be Gtc, Alo or Ioc.");
|
|
61172
|
+
if (tif !== undefined && tif !== "Ioc" && values["--price"] === undefined)
|
|
61173
|
+
return usage4(ctx, "--tif Gtc or Alo needs --price.");
|
|
61174
|
+
const slippageBps = slippage(values["--slippage-bps"]);
|
|
61175
|
+
if (slippageBps === null)
|
|
61176
|
+
return usage4(ctx, "--slippage-bps must be a whole number of at least 1.");
|
|
61177
|
+
const body = {
|
|
61178
|
+
coin,
|
|
61179
|
+
side,
|
|
61180
|
+
size,
|
|
61181
|
+
type: values["--price"] === undefined ? "market" : "limit",
|
|
61182
|
+
...values["--price"] !== undefined ? { price: values["--price"] } : {},
|
|
61183
|
+
...tif !== undefined && values["--price"] !== undefined ? { tif } : {},
|
|
61184
|
+
...slippageBps !== undefined ? { slippageBps } : {},
|
|
61185
|
+
...values["--tp"] !== undefined ? { takeProfit: values["--tp"] } : {},
|
|
61186
|
+
...values["--sl"] !== undefined ? { stopLoss: values["--sl"] } : {}
|
|
61187
|
+
};
|
|
61188
|
+
try {
|
|
61189
|
+
return await runPerpsWrite(ctx, { path: "/api/v1/agent/perps/open", body, ...sharedWrite(values, parsed.booleans) });
|
|
61190
|
+
} catch (error) {
|
|
61191
|
+
return tradingFailure(ctx, error);
|
|
61192
|
+
}
|
|
61193
|
+
}
|
|
61194
|
+
async function perpsClose(args, ctx) {
|
|
61195
|
+
const parsed = parseArgs(args, {
|
|
61196
|
+
valueFlags: ["--wallet", "--size", "--slippage-bps"],
|
|
61197
|
+
booleanFlags: ["--yes", "--no-submit", "--sign-only"]
|
|
61198
|
+
});
|
|
61199
|
+
if ("error" in parsed)
|
|
61200
|
+
return usage4(ctx, parsed.error);
|
|
61201
|
+
const [coin] = parsed.positionals;
|
|
61202
|
+
if (parsed.positionals.length !== 1 || !coin)
|
|
61203
|
+
return usage4(ctx, "Usage: candle perps close <coin> [--size <n>] [--slippage-bps <n>] [--wallet <tee>] [--yes]");
|
|
61204
|
+
const size = parsed.values["--size"];
|
|
61205
|
+
if (size !== undefined && !DEC_RE.test(size))
|
|
61206
|
+
return usage4(ctx, "--size must be a plain decimal.");
|
|
61207
|
+
const slippageBps = slippage(parsed.values["--slippage-bps"]);
|
|
61208
|
+
if (slippageBps === null)
|
|
61209
|
+
return usage4(ctx, "--slippage-bps must be a whole number of at least 1.");
|
|
61210
|
+
const body = {
|
|
61211
|
+
coin,
|
|
61212
|
+
...size !== undefined ? { size } : {},
|
|
61213
|
+
...slippageBps !== undefined ? { slippageBps } : {}
|
|
61214
|
+
};
|
|
61215
|
+
try {
|
|
61216
|
+
return await runPerpsWrite(ctx, {
|
|
61217
|
+
path: "/api/v1/agent/perps/close",
|
|
61218
|
+
body,
|
|
61219
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
61220
|
+
});
|
|
61221
|
+
} catch (error) {
|
|
61222
|
+
return tradingFailure(ctx, error);
|
|
61223
|
+
}
|
|
61224
|
+
}
|
|
61225
|
+
async function perpsCancel(args, ctx) {
|
|
61226
|
+
const parsed = parseArgs(args, WRITE_FLAGS);
|
|
61227
|
+
if ("error" in parsed)
|
|
61228
|
+
return usage4(ctx, parsed.error);
|
|
61229
|
+
const [cloid] = parsed.positionals;
|
|
61230
|
+
if (parsed.positionals.length !== 1 || !cloid || !CLOID_RE.test(cloid))
|
|
61231
|
+
return usage4(ctx, "Usage: candle perps cancel <cloid> [--wallet <tee>] [--yes] (cloid: 0x + 32 hex)");
|
|
61232
|
+
try {
|
|
61233
|
+
return await runPerpsWrite(ctx, {
|
|
61234
|
+
path: "/api/v1/agent/perps/cancel",
|
|
61235
|
+
body: { cloid: cloid.toLowerCase() },
|
|
61236
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
61237
|
+
});
|
|
61238
|
+
} catch (error) {
|
|
61239
|
+
return tradingFailure(ctx, error);
|
|
61240
|
+
}
|
|
61241
|
+
}
|
|
61242
|
+
async function perpsLeverage(args, ctx) {
|
|
61243
|
+
const parsed = parseArgs(args, {
|
|
61244
|
+
valueFlags: ["--wallet"],
|
|
61245
|
+
booleanFlags: ["--yes", "--no-submit", "--sign-only", "--isolated"]
|
|
61246
|
+
});
|
|
61247
|
+
if ("error" in parsed)
|
|
61248
|
+
return usage4(ctx, parsed.error);
|
|
61249
|
+
const [coin, leverage] = parsed.positionals;
|
|
61250
|
+
const n = Number(leverage);
|
|
61251
|
+
if (parsed.positionals.length !== 2 || !coin || !Number.isInteger(n) || n < 1)
|
|
61252
|
+
return usage4(ctx, "Usage: candle perps leverage <coin> <x> [--isolated] [--wallet <tee>] [--yes]");
|
|
61253
|
+
try {
|
|
61254
|
+
return await runPerpsWrite(ctx, {
|
|
61255
|
+
path: "/api/v1/agent/perps/leverage",
|
|
61256
|
+
body: { coin, leverage: n, mode: parsed.booleans.has("--isolated") ? "isolated" : "cross" },
|
|
61257
|
+
...sharedWrite(parsed.values, parsed.booleans)
|
|
61258
|
+
});
|
|
61259
|
+
} catch (error) {
|
|
61260
|
+
return tradingFailure(ctx, error);
|
|
61261
|
+
}
|
|
61262
|
+
}
|
|
61263
|
+
async function perpsPositions(args, ctx) {
|
|
61264
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
|
|
61265
|
+
if ("error" in parsed)
|
|
61266
|
+
return usage4(ctx, parsed.error);
|
|
61267
|
+
if (parsed.positionals.length !== 0)
|
|
61268
|
+
return usage4(ctx, "Usage: candle perps positions [--wallet <tee>]");
|
|
61269
|
+
try {
|
|
61270
|
+
const key = await tradingKey(ctx);
|
|
61271
|
+
const { row } = await perpsWallet(ctx, key, parsed.values["--wallet"]);
|
|
61272
|
+
const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(row.id)}`);
|
|
61273
|
+
if (ctx.json)
|
|
61274
|
+
return printTradingResult(ctx, result);
|
|
61275
|
+
const out = ctx.deps.stdout;
|
|
61276
|
+
const account = result.account ?? {};
|
|
61277
|
+
out.write(`${safeText(result.address)} (${safeText(result.network)}, ${safeText(result.mode)} mode): account value ${safeText(account.accountValue)} USD, withdrawable ${safeText(result.withdrawable)} USD
|
|
61278
|
+
`);
|
|
61279
|
+
const positions = result.positions ?? [];
|
|
61280
|
+
if (positions.length === 0)
|
|
61281
|
+
out.write(`No open positions.
|
|
61282
|
+
`);
|
|
61283
|
+
for (const p of positions) {
|
|
61284
|
+
const leverage = p.leverage ?? {};
|
|
61285
|
+
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")}
|
|
61286
|
+
`);
|
|
61287
|
+
}
|
|
61288
|
+
return 0;
|
|
61289
|
+
} catch (error) {
|
|
61290
|
+
return tradingFailure(ctx, error);
|
|
61291
|
+
}
|
|
61292
|
+
}
|
|
61293
|
+
async function perpsOrders(args, ctx) {
|
|
61294
|
+
const parsed = parseArgs(args, { valueFlags: ["--wallet", "--limit"] });
|
|
61295
|
+
if ("error" in parsed)
|
|
61296
|
+
return usage4(ctx, parsed.error);
|
|
61297
|
+
const limit = parsed.values["--limit"];
|
|
61298
|
+
if (parsed.positionals.length !== 0 || limit !== undefined && !/^[1-9][0-9]{0,2}$/.test(limit))
|
|
61299
|
+
return usage4(ctx, "Usage: candle perps orders [--wallet <tee>] [--limit <n>]");
|
|
61300
|
+
try {
|
|
61301
|
+
const key = await tradingKey(ctx);
|
|
61302
|
+
const { row } = await perpsWallet(ctx, key, parsed.values["--wallet"]);
|
|
61303
|
+
const query = new URLSearchParams({ walletId: row.id });
|
|
61304
|
+
if (limit !== undefined)
|
|
61305
|
+
query.set("limit", limit);
|
|
61306
|
+
const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/orders?${query}`);
|
|
61307
|
+
if (ctx.json)
|
|
61308
|
+
return printTradingResult(ctx, result);
|
|
61309
|
+
const out = ctx.deps.stdout;
|
|
61310
|
+
const open4 = result.open ?? [];
|
|
61311
|
+
out.write(`Open on Hyperliquid (${open4.length}):
|
|
61312
|
+
`);
|
|
61313
|
+
for (const o of open4) {
|
|
61314
|
+
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")}
|
|
61315
|
+
`);
|
|
61316
|
+
}
|
|
61317
|
+
const recorded = result.recorded ?? [];
|
|
61318
|
+
out.write(`Built by Candle (${recorded.length}, newest first):
|
|
61319
|
+
`);
|
|
61320
|
+
for (const r of recorded) {
|
|
61321
|
+
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)}` : ""}
|
|
61322
|
+
`);
|
|
61323
|
+
}
|
|
61324
|
+
return 0;
|
|
61325
|
+
} catch (error) {
|
|
61326
|
+
return tradingFailure(ctx, error);
|
|
61327
|
+
}
|
|
61328
|
+
}
|
|
61329
|
+
var HYPERLIQUID_RELAY_CHAIN_ID = 1337;
|
|
61330
|
+
var HYPERLIQUID_RELAY_USDC = "0x00000000000000000000000000000000";
|
|
61331
|
+
var DEPOSIT_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
61332
|
+
var DEPOSIT_USAGE = "Usage: candle perps deposit <amount> <SOL|USDC|ETH|USDG> [--wallet <tee>] [--to <evm tee>] [--id <clientDepositId>] [--slippage-bps <n>] [--rpc-url <url>] [--yes] [--no-submit] | candle perps deposit status <clientDepositId>";
|
|
61333
|
+
function isDepositAsset(value) {
|
|
61334
|
+
return value !== undefined && Object.hasOwn(BRIDGE_ASSETS, value);
|
|
61335
|
+
}
|
|
61336
|
+
function depositBuildProblem(build, expect) {
|
|
61337
|
+
const destination = build.destination ?? {};
|
|
61338
|
+
if (build.walletId !== expect.walletId)
|
|
61339
|
+
return "the build names another paying wallet";
|
|
61340
|
+
if (build.asset !== expect.asset || build.amountRaw !== expect.amountRaw)
|
|
61341
|
+
return "the build is for another amount";
|
|
61342
|
+
if (destination.chainId !== HYPERLIQUID_RELAY_CHAIN_ID)
|
|
61343
|
+
return `the build lands on chain ${safeText(destination.chainId)}`;
|
|
61344
|
+
if (destination.currency !== HYPERLIQUID_RELAY_USDC)
|
|
61345
|
+
return "the build does not deliver Hyperliquid USDC";
|
|
61346
|
+
if (typeof destination.address !== "string" || !sameEvmAddress(destination.address, expect.destination))
|
|
61347
|
+
return `the build lands on ${safeText(destination.address)}, not ${expect.destination}`;
|
|
61348
|
+
const fee = build.fee ?? {};
|
|
61349
|
+
if (fee.bps !== 0 || fee.feeRaw !== "0")
|
|
61350
|
+
return "the build carries a Candle fee";
|
|
61351
|
+
return null;
|
|
61352
|
+
}
|
|
61353
|
+
async function perpsDepositStatus(ctx, id) {
|
|
61354
|
+
const key = await tradingKey(ctx);
|
|
61355
|
+
const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/deposit/${encodeURIComponent(id)}`);
|
|
61356
|
+
if (ctx.json)
|
|
61357
|
+
return printTradingResult(ctx, result);
|
|
61358
|
+
const job = result.job ?? {};
|
|
61359
|
+
const destination = job.destination ?? {};
|
|
61360
|
+
const relay = job.relay;
|
|
61361
|
+
ctx.deps.stdout.write(`Deposit ${safeText(id)}: ${safeText(job.status)}${job.signature ? ` (${safeText(job.signature)})` : ""}, ${safeText(job.amountRaw)} raw ${safeText(job.asset)} to ${safeText(destination.address)} on Hyperliquid. Relay: ${relay ? safeText(relay.status) : "not sent yet, or unknown"}.
|
|
61362
|
+
`);
|
|
61363
|
+
return 0;
|
|
61364
|
+
}
|
|
61365
|
+
async function perpsDeposit(args, ctx) {
|
|
61366
|
+
if (args[0] === "status") {
|
|
61367
|
+
if (args.length !== 2 || !DEPOSIT_ID_RE.test(args[1]))
|
|
61368
|
+
return usage4(ctx, DEPOSIT_USAGE);
|
|
61369
|
+
try {
|
|
61370
|
+
return await perpsDepositStatus(ctx, args[1]);
|
|
61371
|
+
} catch (error) {
|
|
61372
|
+
return tradingFailure(ctx, error);
|
|
61373
|
+
}
|
|
61374
|
+
}
|
|
61375
|
+
const parsed = parseArgs(args, {
|
|
61376
|
+
valueFlags: ["--wallet", "--to", "--id", "--slippage-bps", "--rpc-url"],
|
|
61377
|
+
booleanFlags: ["--yes", "--no-submit"]
|
|
61378
|
+
});
|
|
61379
|
+
if ("error" in parsed)
|
|
61380
|
+
return usage4(ctx, parsed.error);
|
|
61381
|
+
const [amount, asset] = parsed.positionals;
|
|
61382
|
+
if (parsed.positionals.length !== 2 || !amount || !DEC_RE.test(amount) || !isDepositAsset(asset))
|
|
61383
|
+
return usage4(ctx, DEPOSIT_USAGE);
|
|
61384
|
+
const { values } = parsed;
|
|
61385
|
+
const id = values["--id"] ?? `deposit-${randomUUID4()}`;
|
|
61386
|
+
if (!DEPOSIT_ID_RE.test(id))
|
|
61387
|
+
return usage4(ctx, "--id must be 1 to 128 letters, digits, '.', '_', ':' or '-'.");
|
|
61388
|
+
const slippageBps = Number(values["--slippage-bps"] ?? "100");
|
|
61389
|
+
if (!Number.isInteger(slippageBps) || slippageBps < 1 || slippageBps > 1000)
|
|
61390
|
+
return usage4(ctx, "--slippage-bps must be a whole number from 1 to 1000.");
|
|
61391
|
+
const origin = BRIDGE_ASSETS[asset].chain;
|
|
61392
|
+
try {
|
|
61393
|
+
const key = await tradingKey(ctx);
|
|
61394
|
+
const payer = await tradingPayer(ctx, key, values["--wallet"], "swap:write", origin);
|
|
61395
|
+
if (payer.kind !== "tee")
|
|
61396
|
+
throw new TradingError("TEE_WALLET_REQUIRED", `A Hyperliquid deposit pays only from this key's ${origin === "hood" ? "Hood" : "Solana"} TEE wallet, and ${safeText(payer.address)} is the embedded wallet. Name the TEE wallet with --wallet. Nothing was built.`);
|
|
61397
|
+
const wallet = payer.wallet;
|
|
61398
|
+
let destination;
|
|
61399
|
+
if (origin === "hood") {
|
|
61400
|
+
if (values["--to"] !== undefined)
|
|
61401
|
+
throw new TradingError("BRIDGE_DESTINATION_MISSING", "From a Hood TEE wallet the deposit lands on that wallet's own Hyperliquid account; drop --to. Nothing was built.");
|
|
61402
|
+
destination = { id: wallet.id, address: wallet.address };
|
|
61403
|
+
} else {
|
|
61404
|
+
destination = (await perpsWallet(ctx, key, values["--to"])).row;
|
|
61405
|
+
}
|
|
61406
|
+
const amountRaw = rawAmount(amount, BRIDGE_ASSETS[asset].decimals);
|
|
61407
|
+
ctx.deps.stderr.write(`Deposit: ${id}
|
|
61408
|
+
`);
|
|
61409
|
+
const build = await perpsRequest(ctx, key, "/api/v1/agent/perps/deposit", {
|
|
61410
|
+
clientDepositId: id,
|
|
61411
|
+
walletId: wallet.id,
|
|
61412
|
+
asset,
|
|
61413
|
+
amountRaw,
|
|
61414
|
+
maxSlippageBps: slippageBps,
|
|
61415
|
+
...origin === "solana" ? { perpsWalletId: destination.id } : {}
|
|
61416
|
+
});
|
|
61417
|
+
if (build.job)
|
|
61418
|
+
return printTradingResult(ctx, build);
|
|
61419
|
+
const refused = (problem2) => new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this deposit: ${problem2}. Nothing was signed.`);
|
|
61420
|
+
const problem = depositBuildProblem(build, {
|
|
61421
|
+
walletId: wallet.id,
|
|
61422
|
+
asset,
|
|
61423
|
+
amountRaw,
|
|
61424
|
+
destination: destination.address
|
|
61425
|
+
});
|
|
61426
|
+
if (problem)
|
|
61427
|
+
throw refused(problem);
|
|
61428
|
+
let sequenced;
|
|
61429
|
+
let transaction;
|
|
61430
|
+
const hoodOrigin = asset === "ETH" || asset === "USDG" ? asset : undefined;
|
|
61431
|
+
if (hoodOrigin) {
|
|
61432
|
+
const parsedLeg = sequencedSchema.safeParse(build);
|
|
61433
|
+
if (!parsedLeg.success)
|
|
61434
|
+
throw refused("Candle did not answer with a sequenced Hood leg");
|
|
61435
|
+
sequenced = parsedLeg.data;
|
|
61436
|
+
const plan = plannedLegKinds(sequenced.legKind, sequenced.plannedLegCount, false, "bridgeDeposit");
|
|
61437
|
+
if (!bridgePlanAdmitted(hoodOrigin, plan))
|
|
61438
|
+
throw refused(`the plan is ${sequenced.plannedLegCount} leg(s) starting with ${sequenced.legKind}`);
|
|
61439
|
+
const legProblem = relayHoodLegProblem(sequenced.legKind, sequenced.nextLeg, {
|
|
61440
|
+
origin: hoodOrigin,
|
|
61441
|
+
payer: wallet.address,
|
|
61442
|
+
amountRaw
|
|
61443
|
+
});
|
|
61444
|
+
if (legProblem)
|
|
61445
|
+
throw refused(legProblem);
|
|
61446
|
+
} else {
|
|
61447
|
+
const txs = Array.isArray(build.transactionsBase64) ? build.transactionsBase64 : [];
|
|
61448
|
+
if (txs.length !== 1 || typeof txs[0] !== "string")
|
|
61449
|
+
throw refused(`Candle returned ${txs.length} transactions, not one deposit`);
|
|
61450
|
+
transaction = txs[0];
|
|
61451
|
+
const reader = await lazySolanaClient(ctx, values["--rpc-url"])();
|
|
61452
|
+
const solanaProblem = await tradingRead(ctx, reader, () => relaySolanaDepositProblem(transaction, wallet.address, reader.rpc));
|
|
61453
|
+
if (solanaProblem)
|
|
61454
|
+
throw refused(solanaProblem);
|
|
61455
|
+
}
|
|
61456
|
+
const decimals = Number(build.outDecimals);
|
|
61457
|
+
const minimum = decimalAmount(String(build.minimumOutRaw), decimals);
|
|
61458
|
+
const estimate2 = decimalAmount(String(build.expectedOutRaw), decimals);
|
|
61459
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
61460
|
+
output.write(`Deposit ${amount} ${asset} from ${safeText(wallet.address)} (${origin === "hood" ? "Hood" : "Solana"} TEE wallet) to Hyperliquid account ${safeText(destination.address)} through Relay.
|
|
61461
|
+
`);
|
|
61462
|
+
output.write(`Relay delivers at least ${minimum} USDC (estimate ${estimate2})${build.firstDeposit ? "; Hyperliquid keeps 1 USDC to activate this new account" : ""}. Candle fee: none.
|
|
61463
|
+
`);
|
|
61464
|
+
output.write("There is no withdrawal from Hyperliquid in this release, and `candle tee disable` or `tee sweep` do not reach it.\n");
|
|
61465
|
+
if (sequenced)
|
|
61466
|
+
output.write(`Legs, signed one at a time: ${sequenced.plannedLegCount === 2 ? "approval, bridgeDeposit" : "bridgeDeposit"}
|
|
61467
|
+
`);
|
|
61468
|
+
if (parsed.booleans.has("--no-submit"))
|
|
61469
|
+
return printTradingResult(ctx, { ...build, signed: false, submitted: false });
|
|
61470
|
+
if (!parsed.booleans.has("--yes")) {
|
|
61471
|
+
if (!ctx.deps.isTTY.stdin)
|
|
61472
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes.");
|
|
61473
|
+
if ((await ctx.deps.promptLine("Sign and send? [y/N] ")).trim().toLowerCase() !== "y") {
|
|
61474
|
+
if (sequenced)
|
|
61475
|
+
ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}.
|
|
61476
|
+
`);
|
|
61477
|
+
throw new TradingError("CANCELLED", "Nothing was signed.");
|
|
61478
|
+
}
|
|
61479
|
+
}
|
|
61480
|
+
if (typeof build.expiresAt !== "number" || build.expiresAt <= ctx.deps.now())
|
|
61481
|
+
throw new TradingError("QUOTE_EXPIRED", "The deposit build expired before signing. Start again with a new --id.");
|
|
61482
|
+
let result;
|
|
61483
|
+
if (sequenced && hoodOrigin) {
|
|
61484
|
+
const run2 = await runSequencedLegs(ctx, key, {
|
|
61485
|
+
wallet,
|
|
61486
|
+
first: sequenced,
|
|
61487
|
+
submitPath: "/api/v1/agent/perps/deposit/submit",
|
|
61488
|
+
submitFields: { clientDepositId: id, depositId: build.depositId },
|
|
61489
|
+
unwrap: (answer) => answer,
|
|
61490
|
+
primaryLeg: "bridgeDeposit",
|
|
61491
|
+
allowedLegs: bridgeLegKinds(hoodOrigin),
|
|
61492
|
+
checkLeg: (kind, leg) => relayHoodLegProblem(kind, leg, { origin: hoodOrigin, payer: wallet.address, amountRaw }) === null,
|
|
61493
|
+
onLanded: async () => {}
|
|
61494
|
+
});
|
|
61495
|
+
result = { ...run2.final, landedLegs: run2.landed };
|
|
61496
|
+
} else {
|
|
61497
|
+
const signed = await relaySign(ctx, key, wallet, transaction);
|
|
61498
|
+
result = await perpsRequest(ctx, key, "/api/v1/agent/perps/deposit/submit", {
|
|
61499
|
+
clientDepositId: id,
|
|
61500
|
+
depositId: build.depositId,
|
|
61501
|
+
signedTransactionsBase64: [signed]
|
|
61502
|
+
});
|
|
61503
|
+
}
|
|
61504
|
+
ctx.deps.stderr.write(`Deposit sent; Relay fills it on Hyperliquid. Check it with candle perps deposit status ${id}.
|
|
61505
|
+
`);
|
|
61506
|
+
return printTradingResult(ctx, {
|
|
61507
|
+
...result,
|
|
61508
|
+
clientDepositId: id,
|
|
61509
|
+
expectedOutRaw: build.expectedOutRaw,
|
|
61510
|
+
minimumOutRaw: build.minimumOutRaw,
|
|
61511
|
+
outDecimals: build.outDecimals
|
|
61512
|
+
});
|
|
61513
|
+
} catch (error) {
|
|
61514
|
+
return tradingFailure(ctx, error);
|
|
61515
|
+
}
|
|
61516
|
+
}
|
|
61517
|
+
|
|
61518
|
+
// src/commands/plans.ts
|
|
61519
|
+
init_args();
|
|
61520
|
+
|
|
61521
|
+
// src/plans.ts
|
|
61522
|
+
var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.";
|
|
61523
|
+
var PLAN_CAPABILITY_LABELS = {
|
|
61524
|
+
tradeCandleTokens: "Trade Candle-launched tokens",
|
|
61525
|
+
tradeBaseAssets: "Trade base assets",
|
|
61526
|
+
freeBaseTransfers: "Base-pair swaps and own-wallet bridges (when enabled), no Candle fee",
|
|
61527
|
+
sellExternalTokens: "Sell tokens not launched on Candle",
|
|
61528
|
+
buyExternalTokens: "Buy tokens not launched on Candle",
|
|
61529
|
+
hyperliquidPerps: "Hyperliquid perps (when enabled)",
|
|
61530
|
+
selfLaunch: "Self-launch from a linked wallet",
|
|
61531
|
+
atomicLaunch: "Atomic launch with first buys",
|
|
61532
|
+
createLinkedWallets: "Create linked wallets",
|
|
61533
|
+
importLinkedWallets: "Import linked wallets",
|
|
61534
|
+
limitOrders: "Limit orders",
|
|
61535
|
+
quant: "Quant on Telegram"
|
|
61536
|
+
};
|
|
61537
|
+
var PLAN_LABELS = { free: "Free", believer: "Believer", pro: "Pro", max: "Max" };
|
|
61538
|
+
function planLabel(plan) {
|
|
61539
|
+
return Object.hasOwn(PLAN_LABELS, plan) ? PLAN_LABELS[plan] : plan;
|
|
61540
|
+
}
|
|
61541
|
+
function formatPlanBps(bps) {
|
|
61542
|
+
return bps === 0 ? "none" : `${Number((bps / 100).toFixed(4))}%`;
|
|
61543
|
+
}
|
|
61544
|
+
function count(n) {
|
|
61545
|
+
return n.toLocaleString("en-US");
|
|
61546
|
+
}
|
|
61547
|
+
function priceCell(entry) {
|
|
61548
|
+
if (entry.price)
|
|
61549
|
+
return `$${count(entry.price.pricePerMonthUsd)} a month`;
|
|
61550
|
+
return entry.plan === "free" ? "free" : "not sold";
|
|
61551
|
+
}
|
|
61552
|
+
function planTableRows(table) {
|
|
61553
|
+
const plans = table.plans;
|
|
61554
|
+
const row = (label, cell) => [label, ...plans.map(cell)];
|
|
61555
|
+
const rows = [
|
|
61556
|
+
row("Price", priceCell),
|
|
61557
|
+
row("Agent trade fee", (e) => formatPlanBps(e.feeBps)),
|
|
61558
|
+
row("Perps builder fee", (e) => formatPlanBps(e.perpFeeBps)),
|
|
61559
|
+
row("Requests per minute", (e) => count(e.limits.rateLimitPerMin)),
|
|
61560
|
+
row("Launches per day", (e) => count(e.limits.dailyLaunchCap)),
|
|
61561
|
+
row("Uploads per minute", (e) => count(e.limits.uploadsPerMin)),
|
|
61562
|
+
row("Linked wallets", (e) => count(e.limits.linkedWallets))
|
|
61563
|
+
];
|
|
61564
|
+
const known = Object.keys(PLAN_CAPABILITY_LABELS);
|
|
61565
|
+
const served = new Set;
|
|
61566
|
+
for (const entry of plans)
|
|
61567
|
+
for (const key of Object.keys(entry.capabilities ?? {}))
|
|
61568
|
+
served.add(key);
|
|
61569
|
+
const keys = [
|
|
61570
|
+
...known.filter((k) => served.has(k)),
|
|
61571
|
+
...[...served].filter((k) => !Object.hasOwn(PLAN_CAPABILITY_LABELS, k))
|
|
61572
|
+
];
|
|
61573
|
+
for (const key of keys) {
|
|
61574
|
+
const label = Object.hasOwn(PLAN_CAPABILITY_LABELS, key) ? PLAN_CAPABILITY_LABELS[key] : key;
|
|
61575
|
+
rows.push(row(label, (e) => {
|
|
61576
|
+
const value = e.capabilities?.[key];
|
|
61577
|
+
return value === true ? "yes" : value === false ? "no" : "-";
|
|
61578
|
+
}));
|
|
61579
|
+
}
|
|
61580
|
+
return { headers: ["", ...plans.map((e) => planLabel(e.plan))], rows };
|
|
61581
|
+
}
|
|
61582
|
+
function planPromotionLine(table) {
|
|
61583
|
+
if (!(table.promoMaxDays > 0))
|
|
61584
|
+
return null;
|
|
61585
|
+
const days = table.promoMaxDays === 1 ? "1 day" : `${table.promoMaxDays} days`;
|
|
61586
|
+
return `A first Pro purchase includes Max for its first ${days}, then continues on Pro.`;
|
|
61587
|
+
}
|
|
61588
|
+
|
|
61589
|
+
// src/commands/plans.ts
|
|
61590
|
+
init_render();
|
|
61591
|
+
var USAGE3 = "Usage: candle plans";
|
|
61592
|
+
async function plans(args, ctx) {
|
|
61593
|
+
const parsed = parseArgs(args, {});
|
|
61594
|
+
if ("error" in parsed || parsed.positionals.length > 0) {
|
|
61595
|
+
writeUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
|
|
61596
|
+
${USAGE3}` : USAGE3, ctx.json);
|
|
61597
|
+
return 2;
|
|
61598
|
+
}
|
|
61599
|
+
const result = await apiRequest("/api/v1/agent/plans", {
|
|
61600
|
+
auth: "none",
|
|
61601
|
+
credentials: {},
|
|
61602
|
+
apiUrl: ctx.apiUrl,
|
|
61603
|
+
fetch: ctx.deps.fetch,
|
|
61604
|
+
env: ctx.deps.env
|
|
61605
|
+
});
|
|
61606
|
+
if (!result.ok) {
|
|
61607
|
+
if (result.status === 404 && !ctx.json) {
|
|
61608
|
+
ctx.deps.stderr.write(`${ctx.apiUrl} does not serve the plan table yet (GET /api/v1/agent/plans answered 404).
|
|
61609
|
+
`);
|
|
61610
|
+
return 1;
|
|
61611
|
+
}
|
|
61612
|
+
writeFailure(ctx.deps, result, { apiUrl: ctx.apiUrl, authType: "none" }, ctx.json);
|
|
61613
|
+
return 1;
|
|
61614
|
+
}
|
|
61615
|
+
const body = result.body;
|
|
61616
|
+
if (ctx.json) {
|
|
61617
|
+
ctx.deps.stdout.write(`${JSON.stringify(body)}
|
|
61618
|
+
`);
|
|
61619
|
+
return 0;
|
|
61620
|
+
}
|
|
61621
|
+
const { headers, rows } = planTableRows(body);
|
|
61622
|
+
const out = ctx.deps.stdout;
|
|
61623
|
+
out.write(`Plans served by ${ctx.apiUrl}
|
|
61624
|
+
|
|
61625
|
+
`);
|
|
61626
|
+
out.write(`${renderTable(headers.map(terminalText), rows.map((row) => row.map(terminalText)))}
|
|
61627
|
+
`);
|
|
61628
|
+
const promotion = planPromotionLine(body);
|
|
61629
|
+
if (promotion)
|
|
61630
|
+
out.write(`
|
|
61631
|
+
${promotion}
|
|
61632
|
+
`);
|
|
61633
|
+
out.write(`
|
|
61634
|
+
${PLAN_CAPABILITY_NOTE}
|
|
61635
|
+
`);
|
|
61636
|
+
out.write(`
|
|
61637
|
+
The agent trade fee is charged on top of each trade or dev buy the API builds. Your account's plan: candle doctor.
|
|
61638
|
+
`);
|
|
61639
|
+
return 0;
|
|
61640
|
+
}
|
|
61641
|
+
|
|
59081
61642
|
// src/commands/plugins.ts
|
|
59082
61643
|
init_args();
|
|
59083
61644
|
|
|
@@ -59271,20 +61832,20 @@ async function writeSecretNames(deps, profile, names) {
|
|
|
59271
61832
|
else
|
|
59272
61833
|
await deps.updateProfile(profile, { secretNames: sorted });
|
|
59273
61834
|
}
|
|
59274
|
-
function
|
|
61835
|
+
function usage5(ctx, line) {
|
|
59275
61836
|
writeUsageFailure(ctx.deps, line, ctx.json);
|
|
59276
61837
|
return 2;
|
|
59277
61838
|
}
|
|
59278
61839
|
async function secretsSet(args, ctx) {
|
|
59279
61840
|
const parsed = parseArgs(args, {});
|
|
59280
61841
|
if ("error" in parsed)
|
|
59281
|
-
return
|
|
61842
|
+
return usage5(ctx, parsed.error);
|
|
59282
61843
|
const [raw, extra] = parsed.positionals;
|
|
59283
61844
|
if (!raw || extra !== undefined)
|
|
59284
|
-
return
|
|
61845
|
+
return usage5(ctx, "Usage: candle secrets set <name>");
|
|
59285
61846
|
const name2 = canonicalSecretName(raw);
|
|
59286
61847
|
if (name2 === undefined) {
|
|
59287
|
-
return
|
|
61848
|
+
return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
59288
61849
|
}
|
|
59289
61850
|
if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
|
|
59290
61851
|
writeLocalFailure(ctx.deps, {
|
|
@@ -59296,7 +61857,7 @@ async function secretsSet(args, ctx) {
|
|
|
59296
61857
|
}
|
|
59297
61858
|
const value = await ctx.deps.promptSecret(`Value for ${name2} (input hidden): `);
|
|
59298
61859
|
if (value.length === 0)
|
|
59299
|
-
return
|
|
61860
|
+
return usage5(ctx, "An empty value was typed; nothing was stored.");
|
|
59300
61861
|
try {
|
|
59301
61862
|
await ctx.deps.secretsStore.set(secretRef(ctx.profile, name2), value);
|
|
59302
61863
|
} catch (error) {
|
|
@@ -59316,9 +61877,9 @@ async function secretsSet(args, ctx) {
|
|
|
59316
61877
|
async function secretsList(args, ctx) {
|
|
59317
61878
|
const parsed = parseArgs(args, {});
|
|
59318
61879
|
if ("error" in parsed)
|
|
59319
|
-
return
|
|
61880
|
+
return usage5(ctx, parsed.error);
|
|
59320
61881
|
if (parsed.positionals.length > 0)
|
|
59321
|
-
return
|
|
61882
|
+
return usage5(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
59322
61883
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
59323
61884
|
if (ctx.json) {
|
|
59324
61885
|
ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
|
|
@@ -59337,13 +61898,13 @@ async function secretsList(args, ctx) {
|
|
|
59337
61898
|
async function secretsRemove(args, ctx) {
|
|
59338
61899
|
const parsed = parseArgs(args, {});
|
|
59339
61900
|
if ("error" in parsed)
|
|
59340
|
-
return
|
|
61901
|
+
return usage5(ctx, parsed.error);
|
|
59341
61902
|
const [raw, extra] = parsed.positionals;
|
|
59342
61903
|
if (!raw || extra !== undefined)
|
|
59343
|
-
return
|
|
61904
|
+
return usage5(ctx, "Usage: candle secrets remove <name>");
|
|
59344
61905
|
const name2 = canonicalSecretName(raw);
|
|
59345
61906
|
if (name2 === undefined) {
|
|
59346
|
-
return
|
|
61907
|
+
return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
|
|
59347
61908
|
}
|
|
59348
61909
|
await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name2));
|
|
59349
61910
|
const names = await storedSecretNames(ctx.deps, ctx.profile);
|
|
@@ -59469,11 +62030,11 @@ init_render();
|
|
|
59469
62030
|
function formatUsd(value) {
|
|
59470
62031
|
if (!Number.isFinite(value))
|
|
59471
62032
|
return "?";
|
|
59472
|
-
const
|
|
62033
|
+
const sign4 = value < 0 ? "-" : "";
|
|
59473
62034
|
const abs = Math.abs(value);
|
|
59474
62035
|
if (abs > 0 && abs < 0.005)
|
|
59475
|
-
return `${
|
|
59476
|
-
return `${
|
|
62036
|
+
return `${sign4}<$0.01`;
|
|
62037
|
+
return `${sign4}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
59477
62038
|
}
|
|
59478
62039
|
function formatPrice(value) {
|
|
59479
62040
|
if (!Number.isFinite(value) || value <= 0)
|
|
@@ -59499,7 +62060,47 @@ function shortAddress2(address) {
|
|
|
59499
62060
|
return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address;
|
|
59500
62061
|
}
|
|
59501
62062
|
|
|
62063
|
+
// src/commands/pnl-hyperliquid.ts
|
|
62064
|
+
init_render();
|
|
62065
|
+
function writeHyperliquidPnl(ctx, section) {
|
|
62066
|
+
if (!section)
|
|
62067
|
+
return;
|
|
62068
|
+
const out = ctx.deps.stdout;
|
|
62069
|
+
out.write(`
|
|
62070
|
+
Hyperliquid
|
|
62071
|
+
`);
|
|
62072
|
+
if (!section.read) {
|
|
62073
|
+
out.write(`Unavailable: ${terminalText(section.reason)}
|
|
62074
|
+
`);
|
|
62075
|
+
return;
|
|
62076
|
+
}
|
|
62077
|
+
out.write(`Network: ${section.network}
|
|
62078
|
+
`);
|
|
62079
|
+
out.write(`Realized gross ${formatUsd(section.realizedGrossUsd)}
|
|
62080
|
+
`);
|
|
62081
|
+
out.write(`Funding ${formatUsd(section.fundingUsd)} (received positive, paid negative)
|
|
62082
|
+
`);
|
|
62083
|
+
out.write(`Fees ${formatUsd(section.feesUsd)} (includes builder fees)
|
|
62084
|
+
`);
|
|
62085
|
+
out.write(`Realized net ${formatUsd(section.realizedNetUsd)} (gross + funding - fees)
|
|
62086
|
+
`);
|
|
62087
|
+
out.write(`${section.fills} fills; ${section.fundingPayments} funding payments. Unrealized perps are excluded.
|
|
62088
|
+
`);
|
|
62089
|
+
out.write(`Coverage: currently bound EVM TEE wallets since the current TEE binding; previous bindings excluded.
|
|
62090
|
+
`);
|
|
62091
|
+
for (const wallet of section.byWallet) {
|
|
62092
|
+
out.write(`${terminalText(wallet.address)} since ${new Date(wallet.startTime).toISOString()}
|
|
62093
|
+
`);
|
|
62094
|
+
}
|
|
62095
|
+
if (section.truncated) {
|
|
62096
|
+
out.write(`History is truncated: at most ${section.fillsLimit} recent fills and ${section.fundingLimit} funding rows per wallet, ${section.walletsLimit} wallets. This is a partial figure.
|
|
62097
|
+
`);
|
|
62098
|
+
}
|
|
62099
|
+
}
|
|
62100
|
+
|
|
59502
62101
|
// src/commands/pnl.ts
|
|
62102
|
+
var CHAINS = ["solana", "hood"];
|
|
62103
|
+
var CHAIN_TITLES = { solana: "Solana", hood: "Hood" };
|
|
59503
62104
|
var NO_API_KEY4 = {
|
|
59504
62105
|
code: "NO_API_KEY",
|
|
59505
62106
|
message: "No API key for this profile.",
|
|
@@ -59563,10 +62164,14 @@ async function pnl(args, ctx) {
|
|
|
59563
62164
|
return 0;
|
|
59564
62165
|
}
|
|
59565
62166
|
if (perProfile) {
|
|
59566
|
-
const { pnl: p, lp } = body;
|
|
59567
|
-
|
|
62167
|
+
const { pnl: p, lp, hyperliquid } = body;
|
|
62168
|
+
const engine = typeof p.totalUsd === "number";
|
|
62169
|
+
deps.stdout.write(engine ? `P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's share of the account's P&L
|
|
62170
|
+
|
|
62171
|
+
` : `P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
|
|
59568
62172
|
|
|
59569
62173
|
`);
|
|
62174
|
+
writeChainBlocks(ctx, p.byChain);
|
|
59570
62175
|
writeSummary(ctx, {
|
|
59571
62176
|
realizedNetUsd: p.realizedNetUsd,
|
|
59572
62177
|
realizedGrossUsd: p.realizedGrossUsd,
|
|
@@ -59575,21 +62180,27 @@ async function pnl(args, ctx) {
|
|
|
59575
62180
|
unmarked: p.unmarkedPositions,
|
|
59576
62181
|
unvalued: p.unvalued,
|
|
59577
62182
|
counted: p.counted,
|
|
62183
|
+
totalUsd: p.totalUsd,
|
|
62184
|
+
openPositionsExDust: p.openPositionsExDust,
|
|
62185
|
+
closed: p.closed,
|
|
59578
62186
|
positions: p.openPositions.length,
|
|
59579
62187
|
truncated: p.truncated,
|
|
59580
62188
|
lookback: p.lookback,
|
|
59581
|
-
lookbackUnit: "trades",
|
|
62189
|
+
lookbackUnit: engine ? "of the account's ledger rows" : "trades",
|
|
59582
62190
|
oldestMarkAt: p.oldestMarkAt,
|
|
59583
62191
|
lp
|
|
59584
62192
|
});
|
|
59585
62193
|
writePositions(ctx, p.openPositions, false);
|
|
62194
|
+
writeWallets(ctx, p.byWallet, engine);
|
|
59586
62195
|
writeLpPositions(ctx, lp, false);
|
|
62196
|
+
writeHyperliquidPnl(ctx, hyperliquid);
|
|
59587
62197
|
return 0;
|
|
59588
62198
|
}
|
|
59589
62199
|
const books = body;
|
|
59590
62200
|
deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
|
|
59591
62201
|
|
|
59592
62202
|
`);
|
|
62203
|
+
writeChainBlocks(ctx, { solana: books.solana, hood: books.hood });
|
|
59593
62204
|
writeSummary(ctx, {
|
|
59594
62205
|
...books.all,
|
|
59595
62206
|
positions: books.positions.length,
|
|
@@ -59599,10 +62210,81 @@ async function pnl(args, ctx) {
|
|
|
59599
62210
|
oldestMarkAt: books.oldestMarkAt,
|
|
59600
62211
|
lp: books.lp
|
|
59601
62212
|
});
|
|
62213
|
+
writeWalletEvents(ctx, books);
|
|
62214
|
+
writeAgents(ctx, books.byAgent, books.all);
|
|
59602
62215
|
writePositions(ctx, books.positions, true);
|
|
59603
62216
|
writeLpPositions(ctx, books.lp, true);
|
|
59604
62217
|
return 0;
|
|
59605
62218
|
}
|
|
62219
|
+
function isChainSummary(value) {
|
|
62220
|
+
return typeof value === "object" && value !== null && typeof value.realizedNetUsd === "number";
|
|
62221
|
+
}
|
|
62222
|
+
function writeChainBlocks(ctx, byChain) {
|
|
62223
|
+
const blocks = CHAINS.flatMap((chain2) => {
|
|
62224
|
+
const summary = byChain?.[chain2];
|
|
62225
|
+
return isChainSummary(summary) ? [[chain2, summary]] : [];
|
|
62226
|
+
});
|
|
62227
|
+
if (blocks.length === 0)
|
|
62228
|
+
return;
|
|
62229
|
+
for (const [chain2, s] of blocks) {
|
|
62230
|
+
const unmarked = s.unmarked ?? s.unmarkedPositions ?? 0;
|
|
62231
|
+
const marked = s.openPositions - unmarked;
|
|
62232
|
+
ctx.deps.stdout.write(`${CHAIN_TITLES[chain2]}
|
|
62233
|
+
`);
|
|
62234
|
+
writeLines(ctx, [
|
|
62235
|
+
[
|
|
62236
|
+
"Realized net",
|
|
62237
|
+
formatUsd(s.realizedNetUsd),
|
|
62238
|
+
`gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
|
|
62239
|
+
],
|
|
62240
|
+
[
|
|
62241
|
+
"Unrealized",
|
|
62242
|
+
formatUsd(s.unrealizedUsd),
|
|
62243
|
+
`${marked} of ${s.openPositions} open ${s.openPositions === 1 ? "position" : "positions"} marked${unmarked > 0 ? `; ${unmarked} unpriced, not counted` : ""}`
|
|
62244
|
+
],
|
|
62245
|
+
["Total", formatUsd(totalOf(s)), "realized net plus unrealized"]
|
|
62246
|
+
]);
|
|
62247
|
+
if (s.unvalued > 0) {
|
|
62248
|
+
ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
|
|
62249
|
+
`);
|
|
62250
|
+
}
|
|
62251
|
+
ctx.deps.stdout.write(`
|
|
62252
|
+
`);
|
|
62253
|
+
}
|
|
62254
|
+
ctx.deps.stdout.write(`All chains
|
|
62255
|
+
`);
|
|
62256
|
+
}
|
|
62257
|
+
function totalOf(s) {
|
|
62258
|
+
return typeof s.totalUsd === "number" ? s.totalUsd : s.realizedNetUsd + s.unrealizedUsd;
|
|
62259
|
+
}
|
|
62260
|
+
function isUnplacedWallet(wallet) {
|
|
62261
|
+
return wallet.startsWith("unknown:");
|
|
62262
|
+
}
|
|
62263
|
+
function writeLines(ctx, lines) {
|
|
62264
|
+
const width = Math.max(...lines.map(([label]) => label.length));
|
|
62265
|
+
const valueWidth = Math.max(...lines.map(([, value]) => value.length));
|
|
62266
|
+
for (const [label, value, note] of lines) {
|
|
62267
|
+
ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
|
|
62268
|
+
`);
|
|
62269
|
+
}
|
|
62270
|
+
}
|
|
62271
|
+
function writeWallets(ctx, wallets2, engine) {
|
|
62272
|
+
if (!wallets2 || wallets2.length === 0)
|
|
62273
|
+
return;
|
|
62274
|
+
const rows = wallets2.map((w) => [
|
|
62275
|
+
isUnplacedWallet(w.wallet) ? "not placed in a wallet" : `${w.label?.trim() ? `${w.label.trim()} ` : w.payerType === "main" ? "main " : ""}(${shortAddress2(w.wallet)})`,
|
|
62276
|
+
w.chain ?? "-",
|
|
62277
|
+
String(w.tradesConsidered),
|
|
62278
|
+
formatUsd(w.realizedNetUsd),
|
|
62279
|
+
formatUsd(w.unrealizedUsd),
|
|
62280
|
+
`${w.openPositions.length}${w.unmarkedPositions > 0 ? ` (${w.unmarkedPositions} unpriced)` : ""}`
|
|
62281
|
+
]);
|
|
62282
|
+
const heading = engine ? "By wallet (each its own cost basis; the rows sum to the total)" : "By wallet (each its own cost basis, so the rows need not sum to the total)";
|
|
62283
|
+
ctx.deps.stdout.write(`
|
|
62284
|
+
${heading}
|
|
62285
|
+
${renderTable(["WALLET", "CHAIN", "TRADES", "REALIZED NET", "UNREALIZED", "OPEN"], rows.map((row) => row.map(terminalText)))}
|
|
62286
|
+
`);
|
|
62287
|
+
}
|
|
59606
62288
|
function writeSummary(ctx, s) {
|
|
59607
62289
|
const marked = s.positions - s.unmarked;
|
|
59608
62290
|
const lines = [
|
|
@@ -59617,6 +62299,7 @@ function writeSummary(ctx, s) {
|
|
|
59617
62299
|
`${marked} of ${s.positions} open ${s.positions === 1 ? "position" : "positions"} marked${s.unmarked > 0 ? `; ${s.unmarked} unpriced, not counted` : ""}`
|
|
59618
62300
|
]
|
|
59619
62301
|
];
|
|
62302
|
+
const total = totalOf(s);
|
|
59620
62303
|
const lp = s.lp;
|
|
59621
62304
|
if (lp?.read) {
|
|
59622
62305
|
const plural2 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
@@ -59632,20 +62315,22 @@ function writeSummary(ctx, s) {
|
|
|
59632
62315
|
"LP vs holding",
|
|
59633
62316
|
lp.vsHoldingPositions > 0 ? formatUsd(lp.vsHoldingUsd) : "-",
|
|
59634
62317
|
lp.vsHoldingPositions > 0 ? `${lp.vsHoldingPositions} of ${lp.valued} valued, against holding the deposited tokens (now ${formatUsd(lp.holdValueUsd)})` : "no valued LP position with every deposit priced"
|
|
59635
|
-
], [
|
|
59636
|
-
"Total",
|
|
59637
|
-
formatUsd(
|
|
59638
|
-
"
|
|
62318
|
+
], ["Total", formatUsd(total), "realized net plus unrealized, tokens only, as the console shows it"], [
|
|
62319
|
+
"Total with LP",
|
|
62320
|
+
formatUsd(total + lp.realizedUsd + lp.unrealizedUsd),
|
|
62321
|
+
"the total plus LP realized and unrealized"
|
|
59639
62322
|
]);
|
|
59640
62323
|
} else if (lp) {
|
|
59641
|
-
lines.push(["LP", "not read", `${terminalText(lp.reason)}; not in the total`], ["Total", formatUsd(
|
|
62324
|
+
lines.push(["LP", "not read", `${terminalText(lp.reason)}; not in the total`], ["Total", formatUsd(total), "realized net plus unrealized, tokens only"]);
|
|
59642
62325
|
} else {
|
|
59643
|
-
lines.push(["Total", formatUsd(
|
|
62326
|
+
lines.push(["Total", formatUsd(total), "realized net plus unrealized"]);
|
|
59644
62327
|
}
|
|
59645
|
-
|
|
59646
|
-
|
|
59647
|
-
|
|
59648
|
-
|
|
62328
|
+
writeLines(ctx, lines);
|
|
62329
|
+
if (s.closed)
|
|
62330
|
+
writeClosed(ctx, s.closed);
|
|
62331
|
+
const dust = s.openPositionsExDust !== undefined ? s.positions - s.openPositionsExDust : 0;
|
|
62332
|
+
if (dust > 0) {
|
|
62333
|
+
ctx.deps.stdout.write(`${dust} open ${dust === 1 ? "position is" : "positions are"} dust (worth under one cent): listed and marked, and counted in the figures.
|
|
59649
62334
|
`);
|
|
59650
62335
|
}
|
|
59651
62336
|
if (s.oldestMarkAt !== undefined) {
|
|
@@ -59675,6 +62360,50 @@ function writeSummary(ctx, s) {
|
|
|
59675
62360
|
}
|
|
59676
62361
|
}
|
|
59677
62362
|
}
|
|
62363
|
+
function writeClosed(ctx, c) {
|
|
62364
|
+
const counts = [`${c.wins} won`, `${c.losses} lost`];
|
|
62365
|
+
if (c.costUnknown > 0)
|
|
62366
|
+
counts.push(`${c.costUnknown} with unknown cost`);
|
|
62367
|
+
ctx.deps.stdout.write(`Closed: ${c.closed} ${c.closed === 1 ? "position" : "positions"} (${counts.join(", ")}). Money made ${formatUsd(c.madeUsd)}, money lost ${formatUsd(c.lostUsd)} and partial sells ${formatUsd(c.partialSellsUsd)} add up to the realized net.
|
|
62368
|
+
`);
|
|
62369
|
+
}
|
|
62370
|
+
function writeWalletEvents(ctx, books) {
|
|
62371
|
+
const unknown = books.costUnknown?.length ?? 0;
|
|
62372
|
+
if (unknown > 0) {
|
|
62373
|
+
ctx.deps.stdout.write(`${unknown} ${unknown === 1 ? "arrival" : "arrivals"} of tokens came with no cost anyone can state: selling them realizes nothing (--json lists them).
|
|
62374
|
+
`);
|
|
62375
|
+
}
|
|
62376
|
+
const out = books.movedOutUntracked?.length ?? 0;
|
|
62377
|
+
if (out > 0) {
|
|
62378
|
+
ctx.deps.stdout.write(`${out} ${out === 1 ? "move" : "moves"} out of a wallet went where Candle does not follow: the tokens left at cost, not as a sale (--json lists them).
|
|
62379
|
+
`);
|
|
62380
|
+
}
|
|
62381
|
+
}
|
|
62382
|
+
function writeAgents(ctx, agents, all) {
|
|
62383
|
+
if (!agents || agents.length === 0)
|
|
62384
|
+
return;
|
|
62385
|
+
const open4 = (count2, unmarked) => `${count2}${unmarked > 0 ? ` (${unmarked} unpriced)` : ""}`;
|
|
62386
|
+
const rows = agents.map((a) => [
|
|
62387
|
+
a.keyPrefix === null ? "Manual (web app)" : a.label && a.label !== a.keyPrefix ? `${a.label} (${a.keyPrefix})` : a.keyPrefix,
|
|
62388
|
+
formatUsd(a.realizedNetUsd),
|
|
62389
|
+
formatUsd(a.unrealizedUsd),
|
|
62390
|
+
formatUsd(totalOf(a)),
|
|
62391
|
+
open4(a.openPositions, a.unmarked),
|
|
62392
|
+
String(a.closedRounds)
|
|
62393
|
+
]);
|
|
62394
|
+
rows.push([
|
|
62395
|
+
"Account total",
|
|
62396
|
+
formatUsd(all.realizedNetUsd),
|
|
62397
|
+
formatUsd(all.unrealizedUsd),
|
|
62398
|
+
formatUsd(totalOf(all)),
|
|
62399
|
+
open4(all.openPositions, all.unmarked),
|
|
62400
|
+
all.closedRounds !== undefined ? String(all.closedRounds) : "-"
|
|
62401
|
+
]);
|
|
62402
|
+
ctx.deps.stdout.write(`
|
|
62403
|
+
By agent (each key, then Manual; the rows sum to the account total)
|
|
62404
|
+
${renderTable(["AGENT", "REALIZED NET", "UNREALIZED", "TOTAL", "OPEN", "CLOSED"], rows.map((row) => row.map(terminalText)))}
|
|
62405
|
+
`);
|
|
62406
|
+
}
|
|
59678
62407
|
function writeLpPositions(ctx, lp, withBook) {
|
|
59679
62408
|
if (!lp?.read || lp.positions.length === 0)
|
|
59680
62409
|
return;
|
|
@@ -59708,12 +62437,16 @@ No open positions.
|
|
|
59708
62437
|
`);
|
|
59709
62438
|
return;
|
|
59710
62439
|
}
|
|
59711
|
-
const
|
|
62440
|
+
const withWallet = positions.some((p) => p.wallet !== undefined);
|
|
62441
|
+
const headers = ["TOKEN", "CHAIN", ...withWallet ? ["WALLET"] : [], "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
|
|
59712
62442
|
if (withBook)
|
|
59713
62443
|
headers.push("BOOK");
|
|
62444
|
+
const wallet = (w) => w === undefined || isUnplacedWallet(w) ? "-" : shortAddress2(w);
|
|
59714
62445
|
const rows = positions.map((p) => {
|
|
59715
62446
|
const row = [
|
|
59716
|
-
p.symbol?.trim() || shortAddress2(p.mint)
|
|
62447
|
+
`${p.symbol?.trim() || shortAddress2(p.mint)}${p.dust === true ? " (dust)" : ""}`,
|
|
62448
|
+
p.chain ?? "-",
|
|
62449
|
+
...withWallet ? [wallet(p.wallet)] : [],
|
|
59717
62450
|
formatQuantity(p.quantity),
|
|
59718
62451
|
formatPrice(p.avgEntryUsd),
|
|
59719
62452
|
p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
|
|
@@ -59732,10 +62465,12 @@ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
|
|
|
59732
62465
|
// src/commands/portfolio.ts
|
|
59733
62466
|
init_args();
|
|
59734
62467
|
init_deps();
|
|
62468
|
+
init_evm_lite();
|
|
59735
62469
|
init_profiles();
|
|
59736
62470
|
init_render();
|
|
59737
62471
|
init_solana_endpoint();
|
|
59738
62472
|
init_solana_lite();
|
|
62473
|
+
init_errors();
|
|
59739
62474
|
init_store();
|
|
59740
62475
|
init_vault_support();
|
|
59741
62476
|
var SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
@@ -59746,31 +62481,74 @@ var KNOWN_SYMBOLS = {
|
|
|
59746
62481
|
};
|
|
59747
62482
|
var CHUNK = 100;
|
|
59748
62483
|
var TOKEN_READS_IN_FLIGHT = 8;
|
|
62484
|
+
var EVM_READS_IN_FLIGHT = 8;
|
|
62485
|
+
var CHAINS2 = ["solana", "hood"];
|
|
62486
|
+
var CHAIN_NAMES = { solana: "Solana", hood: "Hood" };
|
|
62487
|
+
var HOOD_NATIVE = "native";
|
|
62488
|
+
function hoodPriceKey(mint) {
|
|
62489
|
+
return mint === HOOD_NATIVE ? "hood:native" : `hood:${mint.toLowerCase()}`;
|
|
62490
|
+
}
|
|
62491
|
+
var HOOD_USDG = toChecksumAddress(HOOD_USDG_ADDRESS);
|
|
62492
|
+
var UNREAD_LABELS = {
|
|
62493
|
+
sol: "SOL",
|
|
62494
|
+
tokens: "tokens",
|
|
62495
|
+
eth: "ETH",
|
|
62496
|
+
usdg: "USDG",
|
|
62497
|
+
"hood-tokens": "tokens"
|
|
62498
|
+
};
|
|
59749
62499
|
var NO_API_KEY5 = {
|
|
59750
62500
|
code: "NO_API_KEY",
|
|
59751
62501
|
message: "No API key for this profile.",
|
|
59752
62502
|
suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
|
|
59753
62503
|
};
|
|
59754
62504
|
async function portfolio(args, ctx) {
|
|
59755
|
-
const parsed = parseArgs(args, {
|
|
62505
|
+
const parsed = parseArgs(args, {
|
|
62506
|
+
valueFlags: ["--rpc-url", "--evm-rpc-url", "--keystore", "--chain"],
|
|
62507
|
+
pathFlags: ["--keystore"]
|
|
62508
|
+
});
|
|
59756
62509
|
if ("error" in parsed)
|
|
59757
62510
|
return usage(ctx, parsed.error);
|
|
59758
62511
|
if (parsed.positionals.length > 0)
|
|
59759
62512
|
return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
|
|
59760
62513
|
const { deps } = ctx;
|
|
62514
|
+
const chainFlag = parsed.values["--chain"];
|
|
62515
|
+
if (chainFlag !== undefined && !CHAINS2.includes(chainFlag)) {
|
|
62516
|
+
return usage(ctx, `--chain must be solana or hood, not ${chainFlag}.`);
|
|
62517
|
+
}
|
|
62518
|
+
const chains = chainFlag === undefined ? [...CHAINS2] : [chainFlag];
|
|
62519
|
+
const showSolana = chains.includes("solana");
|
|
62520
|
+
const showHood = chains.includes("hood");
|
|
62521
|
+
if (!showSolana && parsed.values["--rpc-url"] !== undefined) {
|
|
62522
|
+
return usage(ctx, "--rpc-url is the Solana RPC and has no effect with --chain hood; use --evm-rpc-url.");
|
|
62523
|
+
}
|
|
62524
|
+
if (!showHood && parsed.values["--evm-rpc-url"] !== undefined) {
|
|
62525
|
+
return usage(ctx, "--evm-rpc-url is the Hood RPC and has no effect with --chain solana.");
|
|
62526
|
+
}
|
|
59761
62527
|
const apiKey = await resolveApiKey(deps, ctx.profile);
|
|
59762
62528
|
if (!apiKey) {
|
|
59763
62529
|
writeLocalFailure(deps, NO_API_KEY5, ctx.json);
|
|
59764
62530
|
return 1;
|
|
59765
62531
|
}
|
|
59766
|
-
|
|
59767
|
-
if (
|
|
59768
|
-
|
|
62532
|
+
let solana;
|
|
62533
|
+
if (showSolana) {
|
|
62534
|
+
const opened = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
|
|
62535
|
+
if ("error" in opened)
|
|
62536
|
+
return usage(ctx, opened.error);
|
|
62537
|
+
solana = opened;
|
|
62538
|
+
}
|
|
62539
|
+
let evmRpc;
|
|
62540
|
+
if (showHood) {
|
|
62541
|
+
const resolved = resolveEvmRpcUrl(parsed.values["--evm-rpc-url"], deps.env[EVM_RPC_URL_ENV], "--evm-rpc-url");
|
|
62542
|
+
if ("error" in resolved)
|
|
62543
|
+
return usage(ctx, resolved.error);
|
|
62544
|
+
evmRpc = resolved;
|
|
62545
|
+
}
|
|
59769
62546
|
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
59770
62547
|
if ("error" in resolvedVault)
|
|
59771
62548
|
return usage(ctx, resolvedVault.error);
|
|
59772
62549
|
return runVaultCommand(ctx, async ({ hold }) => {
|
|
59773
62550
|
let vaultEntries;
|
|
62551
|
+
let evmEntries;
|
|
59774
62552
|
let vaultReason;
|
|
59775
62553
|
const raw = await readVaultRaw(resolvedVault.path);
|
|
59776
62554
|
if (raw === null) {
|
|
@@ -59781,17 +62559,26 @@ async function portfolio(args, ctx) {
|
|
|
59781
62559
|
if (!requirePromptStreams(ctx, "portfolio"))
|
|
59782
62560
|
return 1;
|
|
59783
62561
|
const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
|
|
59784
|
-
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 }));
|
|
62562
|
+
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 })) : [];
|
|
62563
|
+
evmEntries = showHood ? vault.index.entries.filter((entry) => entry.chain === "evm" && entry.role === "vault").map((entry) => ({ address: entry.address, label: entry.label })) : [];
|
|
59785
62564
|
}
|
|
59786
62565
|
await printIdentity(ctx);
|
|
59787
|
-
const rpcHost = solana
|
|
59788
|
-
if (vaultEntries && vaultEntries.length > 0) {
|
|
62566
|
+
const rpcHost = solana?.endpoint.host;
|
|
62567
|
+
if (solana && vaultEntries && vaultEntries.length > 0) {
|
|
59789
62568
|
await solana.disclose();
|
|
59790
62569
|
const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
|
|
59791
62570
|
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.
|
|
59792
62571
|
`);
|
|
59793
62572
|
}
|
|
59794
|
-
|
|
62573
|
+
let evmHost;
|
|
62574
|
+
if (evmRpc && evmEntries && evmEntries.length > 0) {
|
|
62575
|
+
evmHost = rpcHostOf2(evmRpc.url);
|
|
62576
|
+
const n = evmEntries.length;
|
|
62577
|
+
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.
|
|
62578
|
+
`);
|
|
62579
|
+
}
|
|
62580
|
+
const evmRead2 = evmRpc && evmEntries && evmEntries.length > 0 ? await startEvmRead(evmEntries.map((entry) => entry.address), evmRpc.url, deps.fetch) : undefined;
|
|
62581
|
+
const [candle, vaultRead, evmBalances] = await Promise.all([
|
|
59795
62582
|
apiRequest("/api/v1/agent/portfolio", {
|
|
59796
62583
|
auth: "key",
|
|
59797
62584
|
credentials: { apiKey },
|
|
@@ -59799,11 +62586,17 @@ async function portfolio(args, ctx) {
|
|
|
59799
62586
|
fetch: deps.fetch,
|
|
59800
62587
|
env: deps.env
|
|
59801
62588
|
}),
|
|
59802
|
-
vaultEntries ? readOwnRpc(vaultEntries.map((entry) => entry.address), solana, ctx) : Promise.resolve(undefined)
|
|
62589
|
+
solana && vaultEntries && vaultEntries.length > 0 ? readOwnRpc(vaultEntries.map((entry) => entry.address), solana, ctx) : Promise.resolve(undefined),
|
|
62590
|
+
evmRead2 ? evmRead2.balances() : Promise.resolve(undefined)
|
|
59803
62591
|
]);
|
|
59804
62592
|
if (vaultRead?.failure !== undefined) {
|
|
59805
62593
|
const n = vaultRead.unavailable.length;
|
|
59806
62594
|
deps.stderr.write(`${n} vault ${n === 1 ? "address" : "addresses"} could not be read: ${vaultRead.failure}.
|
|
62595
|
+
`);
|
|
62596
|
+
}
|
|
62597
|
+
if (evmBalances?.failure !== undefined) {
|
|
62598
|
+
const n = evmBalances.unavailable.length;
|
|
62599
|
+
deps.stderr.write(`${n} EVM vault ${n === 1 ? "address" : "addresses"} on Hood could not be read in full: ${terminalText(evmBalances.failure)}.
|
|
59807
62600
|
`);
|
|
59808
62601
|
}
|
|
59809
62602
|
if (!candle.ok) {
|
|
@@ -59812,6 +62605,7 @@ async function portfolio(args, ctx) {
|
|
|
59812
62605
|
}
|
|
59813
62606
|
const fromCandle = candle.body;
|
|
59814
62607
|
const prices = { ...fromCandle.prices ?? {} };
|
|
62608
|
+
const candleHood = showHood ? fromCandle.hood : undefined;
|
|
59815
62609
|
let priceFailure;
|
|
59816
62610
|
if (vaultRead) {
|
|
59817
62611
|
const wanted = new Set;
|
|
@@ -59838,84 +62632,244 @@ async function portfolio(args, ctx) {
|
|
|
59838
62632
|
priceFailure ??= priced.message;
|
|
59839
62633
|
}
|
|
59840
62634
|
}
|
|
62635
|
+
if (evmBalances) {
|
|
62636
|
+
const wanted = [];
|
|
62637
|
+
const held = [...evmBalances.byAddress.values()];
|
|
62638
|
+
if (held.some((b) => (b.wei ?? 0n) > 0n))
|
|
62639
|
+
wanted.push(HOOD_NATIVE);
|
|
62640
|
+
if (held.some((b) => (b.usdg ?? 0n) > 0n))
|
|
62641
|
+
wanted.push(HOOD_USDG);
|
|
62642
|
+
const missing = wanted.filter((asset) => prices[hoodPriceKey(asset)] === undefined);
|
|
62643
|
+
if (missing.length > 0) {
|
|
62644
|
+
const priced = await apiRequest("/api/v1/agent/prices", {
|
|
62645
|
+
method: "POST",
|
|
62646
|
+
body: { hood: missing },
|
|
62647
|
+
auth: "key",
|
|
62648
|
+
credentials: { apiKey },
|
|
62649
|
+
apiUrl: ctx.apiUrl,
|
|
62650
|
+
fetch: deps.fetch,
|
|
62651
|
+
env: deps.env
|
|
62652
|
+
});
|
|
62653
|
+
if (priced.ok)
|
|
62654
|
+
Object.assign(prices, priced.body.prices ?? {});
|
|
62655
|
+
else
|
|
62656
|
+
priceFailure ??= priced.message;
|
|
62657
|
+
}
|
|
62658
|
+
}
|
|
59841
62659
|
if (priceFailure !== undefined) {
|
|
59842
62660
|
deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
|
|
59843
62661
|
`);
|
|
59844
62662
|
}
|
|
62663
|
+
const lpSection = showSolana ? fromCandle.lp : undefined;
|
|
59845
62664
|
const lpByWallet = new Map;
|
|
59846
62665
|
const lpOf = (address) => {
|
|
59847
62666
|
const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
|
|
59848
62667
|
lpByWallet.set(address, entry);
|
|
59849
62668
|
return entry;
|
|
59850
62669
|
};
|
|
59851
|
-
for (const position of
|
|
62670
|
+
for (const position of lpSection?.positions ?? [])
|
|
59852
62671
|
lpOf(position.wallet).positions.push(lpRow(position, prices));
|
|
59853
|
-
for (const unread of
|
|
62672
|
+
for (const unread of lpSection?.unreadable ?? [])
|
|
59854
62673
|
lpOf(unread.wallet).unread.push(unread.position);
|
|
59855
|
-
const wallet = (address, read, extra) => {
|
|
59856
|
-
const
|
|
59857
|
-
const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
|
|
59858
|
-
const lp2 = fromCandle.lp ? lpOf(address) : undefined;
|
|
62674
|
+
const wallet = (chain2, address, read, extra) => {
|
|
62675
|
+
const lp2 = chain2 === "solana" && lpSection ? lpOf(address) : undefined;
|
|
59859
62676
|
return {
|
|
62677
|
+
chain: chain2,
|
|
59860
62678
|
address,
|
|
59861
62679
|
...extra,
|
|
59862
|
-
holdings,
|
|
59863
|
-
...unread.length > 0 ? { unread } : {},
|
|
62680
|
+
holdings: read.holdings,
|
|
62681
|
+
...read.unread.length > 0 ? { unread: read.unread } : {},
|
|
62682
|
+
...read.truncated ? { truncated: true } : {},
|
|
59864
62683
|
...lp2 ? { lpPositions: lp2.positions } : {},
|
|
59865
62684
|
...lp2 && lp2.unread.length > 0 ? { lpUnread: lp2.unread } : {},
|
|
59866
|
-
valueUsd: (holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
|
|
59867
|
-
unpriced: (holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
|
|
62685
|
+
valueUsd: (read.holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
|
|
62686
|
+
unpriced: (read.holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
|
|
59868
62687
|
};
|
|
59869
62688
|
};
|
|
62689
|
+
const subtotals = (wallets2) => Object.fromEntries(chains.map((chain2) => {
|
|
62690
|
+
const on = wallets2.filter((w) => w.chain === chain2);
|
|
62691
|
+
return [
|
|
62692
|
+
chain2,
|
|
62693
|
+
{
|
|
62694
|
+
valueUsd: on.reduce((sum, w) => sum + w.valueUsd, 0),
|
|
62695
|
+
unpriced: on.reduce((sum, w) => sum + w.unpriced, 0)
|
|
62696
|
+
}
|
|
62697
|
+
];
|
|
62698
|
+
}));
|
|
59870
62699
|
const group = (name2, wallets2, read, reason) => ({
|
|
59871
62700
|
group: name2,
|
|
59872
62701
|
read,
|
|
59873
62702
|
...reason !== undefined ? { reason } : {},
|
|
59874
62703
|
wallets: wallets2,
|
|
59875
62704
|
valueUsd: wallets2.reduce((sum, w) => sum + w.valueUsd, 0),
|
|
59876
|
-
unpriced: wallets2.reduce((sum, w) => sum + w.unpriced, 0)
|
|
62705
|
+
unpriced: wallets2.reduce((sum, w) => sum + w.unpriced, 0),
|
|
62706
|
+
byChain: subtotals(wallets2)
|
|
59877
62707
|
});
|
|
59878
|
-
const
|
|
62708
|
+
const solanaRows = (rows) => showSolana ? rows ?? [] : [];
|
|
59879
62709
|
const groups = [
|
|
59880
|
-
group("vault",
|
|
59881
|
-
|
|
59882
|
-
|
|
59883
|
-
|
|
59884
|
-
|
|
59885
|
-
|
|
59886
|
-
|
|
59887
|
-
|
|
59888
|
-
|
|
59889
|
-
|
|
62710
|
+
group("vault", [
|
|
62711
|
+
...(vaultEntries ?? []).map((entry) => wallet("solana", entry.address, solanaHoldings(vaultRead?.byAddress.get(entry.address), prices), {
|
|
62712
|
+
label: entry.label,
|
|
62713
|
+
role: entry.role
|
|
62714
|
+
})),
|
|
62715
|
+
...(evmEntries ?? []).map((entry) => {
|
|
62716
|
+
const read = evmBalances?.byAddress.get(entry.address);
|
|
62717
|
+
return wallet("hood", entry.address, hoodHoldings(read && {
|
|
62718
|
+
wei: read.wei === null ? null : read.wei.toString(),
|
|
62719
|
+
tokens: read.usdg === null ? null : read.usdg > 0n ? [
|
|
62720
|
+
{
|
|
62721
|
+
mint: HOOD_USDG,
|
|
62722
|
+
amountRaw: read.usdg.toString(),
|
|
62723
|
+
decimals: HOOD_USDG_DECIMALS,
|
|
62724
|
+
symbol: "USDG"
|
|
62725
|
+
}
|
|
62726
|
+
] : []
|
|
62727
|
+
}, prices, "usdg"), { label: entry.label, role: "vault" });
|
|
62728
|
+
})
|
|
62729
|
+
], vaultEntries !== undefined, vaultReason),
|
|
62730
|
+
group("tee", [
|
|
62731
|
+
...solanaRows(fromCandle.tee).map((row) => wallet("solana", row.address, solanaHoldings(row, prices), {
|
|
62732
|
+
id: row.id,
|
|
62733
|
+
...row.label ? { label: row.label } : {},
|
|
62734
|
+
active: row.active
|
|
62735
|
+
})),
|
|
62736
|
+
...(candleHood?.tee ?? []).map((row) => wallet("hood", row.address, hoodHoldings(row, prices, "hood-tokens"), {
|
|
62737
|
+
id: row.id,
|
|
62738
|
+
...row.label ? { label: row.label } : {},
|
|
62739
|
+
active: row.active
|
|
62740
|
+
}))
|
|
62741
|
+
], true),
|
|
62742
|
+
group("embedded", [
|
|
62743
|
+
...solanaRows(fromCandle.embedded).map((row) => wallet("solana", row.address, solanaHoldings(row, prices), {})),
|
|
62744
|
+
...(candleHood?.embedded ?? []).map((row) => wallet("hood", row.address, hoodHoldings(row, prices, "hood-tokens"), {}))
|
|
62745
|
+
], true)
|
|
59890
62746
|
];
|
|
59891
|
-
const
|
|
59892
|
-
|
|
59893
|
-
|
|
62747
|
+
const unavailableByChain = {
|
|
62748
|
+
solana: showSolana ? [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []] : [],
|
|
62749
|
+
hood: [...evmBalances?.unavailable ?? [], ...candleHood?.unavailable ?? []]
|
|
62750
|
+
};
|
|
62751
|
+
const unavailable = chains.flatMap((chain2) => unavailableByChain[chain2]);
|
|
62752
|
+
const lpUnread = (lpSection?.unreadable ?? []).length;
|
|
62753
|
+
const candleExplained = (fromCandle.unavailable ?? []).length > 0 || (fromCandle.hood?.unavailable ?? []).length > 0 || (fromCandle.lp?.unreadable ?? []).length > 0;
|
|
62754
|
+
const listCutOff = fromCandle.complete === false && !candleExplained;
|
|
62755
|
+
const complete = !listCutOff && unavailable.length === 0 && lpUnread === 0;
|
|
59894
62756
|
const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
|
|
59895
62757
|
const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
|
|
59896
|
-
const lp =
|
|
59897
|
-
positions:
|
|
59898
|
-
unpriced:
|
|
62758
|
+
const lp = lpSection ? {
|
|
62759
|
+
positions: lpSection.positions.length,
|
|
62760
|
+
unpriced: lpSection.positions.filter((p) => p.valueUsd === null).length,
|
|
59899
62761
|
unreadable: lpUnread,
|
|
59900
|
-
valueUsd:
|
|
62762
|
+
valueUsd: lpSection.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
|
|
59901
62763
|
} : undefined;
|
|
62764
|
+
const all = groups.flatMap((g) => g.wallets);
|
|
62765
|
+
const byChain = subtotals(all);
|
|
62766
|
+
for (const chain2 of chains) {
|
|
62767
|
+
const entry = byChain[chain2];
|
|
62768
|
+
if (!entry)
|
|
62769
|
+
continue;
|
|
62770
|
+
entry.wallets = all.filter((w) => w.chain === chain2).length;
|
|
62771
|
+
entry.unavailable = unavailableByChain[chain2].length;
|
|
62772
|
+
}
|
|
62773
|
+
const hoodTotals = byChain.hood;
|
|
62774
|
+
if (hoodTotals) {
|
|
62775
|
+
const reasons = { ...candleHood?.unpricedByReason ?? {} };
|
|
62776
|
+
let vaultUnpriced = 0;
|
|
62777
|
+
for (const w of groups[0]?.wallets ?? []) {
|
|
62778
|
+
if (w.chain !== "hood")
|
|
62779
|
+
continue;
|
|
62780
|
+
for (const h of w.holdings ?? []) {
|
|
62781
|
+
if (h.priceUsd !== null)
|
|
62782
|
+
continue;
|
|
62783
|
+
vaultUnpriced += 1;
|
|
62784
|
+
const reason = h.unpricedReason ?? "source-unavailable";
|
|
62785
|
+
reasons[reason] = (reasons[reason] ?? 0) + 1;
|
|
62786
|
+
}
|
|
62787
|
+
}
|
|
62788
|
+
if (candleHood)
|
|
62789
|
+
hoodTotals.unpriced = (candleHood.unpriced ?? 0) + vaultUnpriced;
|
|
62790
|
+
hoodTotals.unpricedByReason = reasons;
|
|
62791
|
+
}
|
|
59902
62792
|
if (ctx.json) {
|
|
59903
62793
|
writeJson(deps, {
|
|
59904
62794
|
ok: true,
|
|
62795
|
+
chains,
|
|
59905
62796
|
totalUsd,
|
|
59906
62797
|
unpriced,
|
|
59907
62798
|
complete,
|
|
59908
62799
|
unavailable,
|
|
59909
|
-
|
|
62800
|
+
unavailableByChain: Object.fromEntries(chains.map((chain2) => [chain2, unavailableByChain[chain2]])),
|
|
62801
|
+
rpcHost: rpcHost ?? null,
|
|
62802
|
+
...evmHost !== undefined ? { evmRpcHost: evmHost } : {},
|
|
59910
62803
|
...lp ? { lp } : {},
|
|
62804
|
+
byChain,
|
|
59911
62805
|
groups
|
|
59912
62806
|
});
|
|
59913
62807
|
return complete ? 0 : 3;
|
|
59914
62808
|
}
|
|
59915
|
-
writeTable(ctx, groups, {
|
|
62809
|
+
writeTable(ctx, groups, {
|
|
62810
|
+
totalUsd,
|
|
62811
|
+
unpriced,
|
|
62812
|
+
chains,
|
|
62813
|
+
byChain,
|
|
62814
|
+
unavailableByChain,
|
|
62815
|
+
listCutOff,
|
|
62816
|
+
lp
|
|
62817
|
+
});
|
|
59916
62818
|
return complete ? 0 : 3;
|
|
59917
62819
|
});
|
|
59918
62820
|
}
|
|
62821
|
+
async function startEvmRead(addresses, rpcUrl, fetchFn) {
|
|
62822
|
+
const rpc = createEvmRpc(rpcUrl, fetchFn);
|
|
62823
|
+
const host = rpcHostOf2(rpcUrl);
|
|
62824
|
+
const unique = [...new Set(addresses)];
|
|
62825
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
62826
|
+
let chainId;
|
|
62827
|
+
try {
|
|
62828
|
+
chainId = await rpc.chainId();
|
|
62829
|
+
} catch (error) {
|
|
62830
|
+
const failure = message(error);
|
|
62831
|
+
return {
|
|
62832
|
+
balances: async () => ({
|
|
62833
|
+
byAddress: new Map(unique.map((address) => [address, { wei: null, usdg: null }])),
|
|
62834
|
+
unavailable: unique,
|
|
62835
|
+
failure
|
|
62836
|
+
})
|
|
62837
|
+
};
|
|
62838
|
+
}
|
|
62839
|
+
if (chainId !== BigInt(HOOD_CHAIN_ID)) {
|
|
62840
|
+
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.`, {
|
|
62841
|
+
suggestion: "Nothing was read or priced. Point --evm-rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC, or pass --chain solana."
|
|
62842
|
+
});
|
|
62843
|
+
}
|
|
62844
|
+
return {
|
|
62845
|
+
balances: async () => {
|
|
62846
|
+
const byAddress = new Map;
|
|
62847
|
+
let failure;
|
|
62848
|
+
let next = 0;
|
|
62849
|
+
await Promise.all(Array.from({ length: Math.min(EVM_READS_IN_FLIGHT, unique.length) }, async () => {
|
|
62850
|
+
while (next < unique.length) {
|
|
62851
|
+
const address = unique[next++];
|
|
62852
|
+
const read = async (call) => {
|
|
62853
|
+
try {
|
|
62854
|
+
return await call();
|
|
62855
|
+
} catch (error) {
|
|
62856
|
+
failure ??= message(error);
|
|
62857
|
+
return null;
|
|
62858
|
+
}
|
|
62859
|
+
};
|
|
62860
|
+
const wei = await read(() => rpc.getBalance(address));
|
|
62861
|
+
const usdg = await read(() => rpc.erc20BalanceOf(HOOD_USDG, address));
|
|
62862
|
+
byAddress.set(address, { wei, usdg });
|
|
62863
|
+
}
|
|
62864
|
+
}));
|
|
62865
|
+
const unavailable = unique.filter((address) => {
|
|
62866
|
+
const read = byAddress.get(address);
|
|
62867
|
+
return read === undefined || read.wei === null || read.usdg === null;
|
|
62868
|
+
});
|
|
62869
|
+
return { byAddress, unavailable, ...failure === undefined ? {} : { failure } };
|
|
62870
|
+
}
|
|
62871
|
+
};
|
|
62872
|
+
}
|
|
59919
62873
|
async function readOwnRpc(addresses, solana, ctx) {
|
|
59920
62874
|
const { rpc } = solana;
|
|
59921
62875
|
const unique = [...new Set(addresses)];
|
|
@@ -59991,6 +62945,14 @@ async function readOwnRpc(addresses, solana, ctx) {
|
|
|
59991
62945
|
}
|
|
59992
62946
|
return { byAddress, unavailable, ...failure === undefined ? {} : { failure } };
|
|
59993
62947
|
}
|
|
62948
|
+
function solanaHoldings(read, prices) {
|
|
62949
|
+
if (read === undefined || read.lamports === null && read.tokens === null)
|
|
62950
|
+
return { holdings: null, unread: [] };
|
|
62951
|
+
return {
|
|
62952
|
+
holdings: valueHoldings(read, prices),
|
|
62953
|
+
unread: [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []]
|
|
62954
|
+
};
|
|
62955
|
+
}
|
|
59994
62956
|
function valueHoldings(read, prices) {
|
|
59995
62957
|
const raw = [];
|
|
59996
62958
|
if (read.lamports !== null && BigInt(read.lamports) > 0n) {
|
|
@@ -60003,6 +62965,7 @@ function valueHoldings(read, prices) {
|
|
|
60003
62965
|
const priceUsd = price?.priceUsd ?? null;
|
|
60004
62966
|
const amount = formatAmount(h.amountRaw, h.decimals);
|
|
60005
62967
|
return {
|
|
62968
|
+
chain: "solana",
|
|
60006
62969
|
...h,
|
|
60007
62970
|
symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
|
|
60008
62971
|
amount,
|
|
@@ -60012,6 +62975,46 @@ function valueHoldings(read, prices) {
|
|
|
60012
62975
|
};
|
|
60013
62976
|
});
|
|
60014
62977
|
}
|
|
62978
|
+
function hoodHoldings(read, prices, tokensPart) {
|
|
62979
|
+
if (read === undefined || read.wei === null && read.tokens === null)
|
|
62980
|
+
return { holdings: null, unread: [] };
|
|
62981
|
+
const raw = [];
|
|
62982
|
+
if (read.wei !== null && BigInt(read.wei) > 0n) {
|
|
62983
|
+
raw.push({ mint: HOOD_NATIVE, amountRaw: read.wei, decimals: NATIVE_DECIMALS, program: "native", symbol: "ETH" });
|
|
62984
|
+
}
|
|
62985
|
+
for (const token of read.tokens ?? []) {
|
|
62986
|
+
if (!/^\d+$/.test(token.amountRaw) || BigInt(token.amountRaw) === 0n)
|
|
62987
|
+
continue;
|
|
62988
|
+
raw.push({
|
|
62989
|
+
mint: token.mint,
|
|
62990
|
+
amountRaw: token.amountRaw,
|
|
62991
|
+
decimals: token.decimals,
|
|
62992
|
+
program: "erc20",
|
|
62993
|
+
...token.symbol ? { symbol: token.symbol } : {}
|
|
62994
|
+
});
|
|
62995
|
+
}
|
|
62996
|
+
const holdings = raw.map(({ symbol, ...h }) => {
|
|
62997
|
+
const price = prices[hoodPriceKey(h.mint)];
|
|
62998
|
+
const priceUsd = price?.priceUsd ?? null;
|
|
62999
|
+
const amount = formatAmount(h.amountRaw, h.decimals);
|
|
63000
|
+
const known = h.mint.toLowerCase() === HOOD_USDG_ADDRESS ? "USDG" : undefined;
|
|
63001
|
+
return {
|
|
63002
|
+
chain: "hood",
|
|
63003
|
+
...h,
|
|
63004
|
+
symbol: known ?? symbol ?? price?.symbol ?? null,
|
|
63005
|
+
amount,
|
|
63006
|
+
priceUsd,
|
|
63007
|
+
valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
|
|
63008
|
+
priceSource: price?.source ?? null,
|
|
63009
|
+
...priceUsd === null ? { unpricedReason: price?.unpricedReason ?? "source-unavailable" } : {}
|
|
63010
|
+
};
|
|
63011
|
+
});
|
|
63012
|
+
return {
|
|
63013
|
+
holdings,
|
|
63014
|
+
unread: [...read.wei === null ? ["eth"] : [], ...read.tokens === null ? [tokensPart] : []],
|
|
63015
|
+
...read.truncated ? { truncated: true } : {}
|
|
63016
|
+
};
|
|
63017
|
+
}
|
|
60015
63018
|
function lpRow(position, prices) {
|
|
60016
63019
|
return {
|
|
60017
63020
|
position: position.position,
|
|
@@ -60056,34 +63059,35 @@ function writeTable(ctx, groups, totals) {
|
|
|
60056
63059
|
lpRows.push([g.group, name2, shortAddress2(position), "-", "not read", "-", "-"]);
|
|
60057
63060
|
}
|
|
60058
63061
|
if (w.holdings === null) {
|
|
60059
|
-
rows.push([g.group, name2, "-", "not read", "-", "-"]);
|
|
63062
|
+
rows.push([g.group, w.chain, name2, "-", "not read", "-", "-"]);
|
|
60060
63063
|
continue;
|
|
60061
63064
|
}
|
|
60062
63065
|
const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
|
|
60063
63066
|
for (const h of sorted) {
|
|
60064
63067
|
rows.push([
|
|
60065
63068
|
g.group,
|
|
63069
|
+
w.chain,
|
|
60066
63070
|
name2,
|
|
60067
63071
|
h.symbol ?? shortAddress2(h.mint),
|
|
60068
63072
|
h.amount,
|
|
60069
63073
|
h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
|
|
60070
|
-
h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
|
|
63074
|
+
h.valueUsd === null ? h.unpricedReason ?? "-" : formatUsd(h.valueUsd)
|
|
60071
63075
|
]);
|
|
60072
63076
|
}
|
|
60073
63077
|
for (const part of w.unread ?? [])
|
|
60074
|
-
rows.push([g.group, name2, part
|
|
63078
|
+
rows.push([g.group, w.chain, name2, UNREAD_LABELS[part], "not read", "-", "-"]);
|
|
60075
63079
|
}
|
|
60076
63080
|
const empty = g.wallets.length - shown.length;
|
|
60077
|
-
const
|
|
63081
|
+
const count2 = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
|
|
60078
63082
|
notes.push([
|
|
60079
63083
|
g.group,
|
|
60080
63084
|
g.read ? formatUsd(g.valueUsd) : "-",
|
|
60081
|
-
g.read ? `${
|
|
63085
|
+
g.read ? `${count2}${empty > 0 ? `, ${empty} empty not shown` : ""}${lpCount > 0 ? `, ${lpCount} LP ${lpCount === 1 ? "position" : "positions"}` : ""}${lpUnread > 0 ? `, ${lpUnread} LP not read` : ""}${g.unpriced > 0 ? `, ${g.unpriced} unpriced` : ""}` : g.reason ?? "not read"
|
|
60082
63086
|
]);
|
|
60083
63087
|
}
|
|
60084
63088
|
if (rows.length > 0)
|
|
60085
63089
|
deps.stdout.write(`
|
|
60086
|
-
${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
|
|
63090
|
+
${renderTable(["GROUP", "CHAIN", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
|
|
60087
63091
|
`);
|
|
60088
63092
|
else
|
|
60089
63093
|
deps.stdout.write(`
|
|
@@ -60094,6 +63098,17 @@ Nothing held in any wallet read.
|
|
|
60094
63098
|
LP positions
|
|
60095
63099
|
${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
|
|
60096
63100
|
`);
|
|
63101
|
+
for (const chain2 of totals.chains) {
|
|
63102
|
+
const sub = totals.byChain[chain2];
|
|
63103
|
+
if (!sub)
|
|
63104
|
+
continue;
|
|
63105
|
+
const reasons = Object.entries(sub.unpricedByReason ?? {}).filter(([, n]) => (n ?? 0) > 0).map(([reason, n]) => `${n} ${reason}`);
|
|
63106
|
+
notes.push([
|
|
63107
|
+
chain2,
|
|
63108
|
+
formatUsd(sub.valueUsd),
|
|
63109
|
+
`${sub.wallets} ${sub.wallets === 1 ? "wallet" : "wallets"}${sub.unpriced > 0 ? `, ${sub.unpriced} unpriced${reasons.length > 0 ? ` (${reasons.join(", ")})` : ""}` : ""}`
|
|
63110
|
+
]);
|
|
63111
|
+
}
|
|
60097
63112
|
notes.push([
|
|
60098
63113
|
"total",
|
|
60099
63114
|
formatUsd(totals.totalUsd),
|
|
@@ -60104,16 +63119,31 @@ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEE
|
|
|
60104
63119
|
deps.stdout.write(`
|
|
60105
63120
|
`);
|
|
60106
63121
|
for (const [label, value, note] of notes) {
|
|
60107
|
-
deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
|
|
63122
|
+
deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${terminalText(note)}
|
|
63123
|
+
`);
|
|
63124
|
+
}
|
|
63125
|
+
const unreadByChain = totals.chains.map((chain2) => [chain2, totals.unavailableByChain[chain2].length]).filter(([, n]) => n > 0);
|
|
63126
|
+
const unavailable = unreadByChain.reduce((sum, [, n]) => sum + n, 0);
|
|
63127
|
+
if (unavailable > 0) {
|
|
63128
|
+
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.
|
|
63129
|
+
`);
|
|
63130
|
+
}
|
|
63131
|
+
if (totals.listCutOff) {
|
|
63132
|
+
deps.stdout.write(`Candle listed only part of this account's wallets. The total is partial.
|
|
60108
63133
|
`);
|
|
60109
63134
|
}
|
|
60110
|
-
|
|
60111
|
-
|
|
63135
|
+
const truncated = groups.flatMap((g) => g.wallets).filter((w) => w.truncated).length;
|
|
63136
|
+
if (truncated > 0) {
|
|
63137
|
+
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.
|
|
60112
63138
|
`);
|
|
60113
63139
|
}
|
|
60114
63140
|
if ((totals.lp?.unreadable ?? 0) > 0) {
|
|
60115
63141
|
const n = totals.lp?.unreadable ?? 0;
|
|
60116
63142
|
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.
|
|
63143
|
+
`);
|
|
63144
|
+
}
|
|
63145
|
+
if (groups.some((g) => g.group === "vault" && g.wallets.some((w) => w.chain === "hood"))) {
|
|
63146
|
+
deps.stdout.write(`EVM vault keys are read for ETH and USDG only; other tokens they hold are not shown.
|
|
60117
63147
|
`);
|
|
60118
63148
|
}
|
|
60119
63149
|
}
|
|
@@ -60641,7 +63671,7 @@ function displayLines(input) {
|
|
|
60641
63671
|
lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
|
|
60642
63672
|
return lines;
|
|
60643
63673
|
}
|
|
60644
|
-
async function
|
|
63674
|
+
async function sign4(args, ctx) {
|
|
60645
63675
|
const lifted = takeRepeatedFlag(args, "--wallet");
|
|
60646
63676
|
if ("error" in lifted)
|
|
60647
63677
|
return usage(ctx, lifted.error);
|
|
@@ -60932,7 +63962,7 @@ init_evm_lite();
|
|
|
60932
63962
|
init_profiles();
|
|
60933
63963
|
init_render();
|
|
60934
63964
|
init_trading();
|
|
60935
|
-
var
|
|
63965
|
+
var USAGE4 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
|
|
60936
63966
|
var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
|
|
60937
63967
|
var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
60938
63968
|
async function readLinkedWallets(ctx, key) {
|
|
@@ -61018,7 +64048,7 @@ async function transfer(args, ctx) {
|
|
|
61018
64048
|
const flags = parsed.values;
|
|
61019
64049
|
const asset = flags["--asset"]?.toUpperCase();
|
|
61020
64050
|
if (parsed.positionals.length !== 0 || !flags["--to"] || !flags["--amount"] || [flags["--asset"], flags["--mint"], flags["--token"]].filter(Boolean).length !== 1 || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags["--mint"] !== undefined && !BASE58_ADDRESS.test(flags["--mint"]) || flags["--token"] !== undefined && !checkEvmAddress(flags["--token"]).ok) {
|
|
61021
|
-
writeUsageFailure(ctx.deps,
|
|
64051
|
+
writeUsageFailure(ctx.deps, USAGE4, ctx.json);
|
|
61022
64052
|
return 2;
|
|
61023
64053
|
}
|
|
61024
64054
|
const to = flags["--to"];
|
|
@@ -63660,11 +66690,11 @@ Anyone who reads them can move every derived key's funds.
|
|
|
63660
66690
|
`);
|
|
63661
66691
|
return 0;
|
|
63662
66692
|
}
|
|
63663
|
-
function randomPositions(
|
|
66693
|
+
function randomPositions(count2, of = PHRASE_WORDS) {
|
|
63664
66694
|
const chosen = new Set;
|
|
63665
66695
|
const scratch = new Uint32Array(1);
|
|
63666
66696
|
const limit = Math.floor(4294967296 / of) * of;
|
|
63667
|
-
while (chosen.size <
|
|
66697
|
+
while (chosen.size < count2) {
|
|
63668
66698
|
crypto.getRandomValues(scratch);
|
|
63669
66699
|
const draw = scratch[0] ?? 0;
|
|
63670
66700
|
if (draw >= limit)
|
|
@@ -64149,10 +67179,10 @@ init_solana_lite();
|
|
|
64149
67179
|
init_token_2022();
|
|
64150
67180
|
init_errors();
|
|
64151
67181
|
var USDC_MINT2 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
64152
|
-
function decimalToRaw2(
|
|
64153
|
-
if (!/^\d+(\.\d+)?$/.test(
|
|
67182
|
+
function decimalToRaw2(decimal2, decimals) {
|
|
67183
|
+
if (!/^\d+(\.\d+)?$/.test(decimal2))
|
|
64154
67184
|
return null;
|
|
64155
|
-
const [whole, frac = ""] =
|
|
67185
|
+
const [whole, frac = ""] = decimal2.split(".");
|
|
64156
67186
|
if (frac.length > decimals)
|
|
64157
67187
|
return null;
|
|
64158
67188
|
return BigInt((whole ?? "0") + frac.padEnd(decimals, "0"));
|
|
@@ -64720,11 +67750,11 @@ function mapLegacyTeeEntry(entry, keyId, grant, createdAtFallback) {
|
|
|
64720
67750
|
}
|
|
64721
67751
|
function buildTeeMeta(entry, lifecycle, grant) {
|
|
64722
67752
|
const source = entry.tee;
|
|
64723
|
-
const
|
|
64724
|
-
if (
|
|
64725
|
-
throw new VaultError("VAULT_INDEX_INVALID", `Legacy entry ${entry.address} names network ${String(
|
|
67753
|
+
const network2 = source?.network ?? "solana-mainnet";
|
|
67754
|
+
if (network2 !== "solana-mainnet") {
|
|
67755
|
+
throw new VaultError("VAULT_INDEX_INVALID", `Legacy entry ${entry.address} names network ${String(network2)}; only solana-mainnet migrates.`);
|
|
64726
67756
|
}
|
|
64727
|
-
const meta = { network, lifecycle };
|
|
67757
|
+
const meta = { network: network2, lifecycle };
|
|
64728
67758
|
carryOptional(meta, source, [
|
|
64729
67759
|
"vaultDestination",
|
|
64730
67760
|
"boundKeyPrefix",
|
|
@@ -64736,7 +67766,7 @@ function buildTeeMeta(entry, lifecycle, grant) {
|
|
|
64736
67766
|
"sweptAt"
|
|
64737
67767
|
]);
|
|
64738
67768
|
if (lifecycle === "local-candidate") {
|
|
64739
|
-
return { network, lifecycle };
|
|
67769
|
+
return { network: network2, lifecycle };
|
|
64740
67770
|
}
|
|
64741
67771
|
if (lifecycle === "enabled") {
|
|
64742
67772
|
if (meta.vaultDestination === undefined) {
|
|
@@ -65072,8 +68102,8 @@ async function readEvmBalances(addresses, rpcUrl, fetchFn) {
|
|
|
65072
68102
|
}
|
|
65073
68103
|
return read;
|
|
65074
68104
|
}
|
|
65075
|
-
function evmRequestsPlanned(
|
|
65076
|
-
return `${1 +
|
|
68105
|
+
function evmRequestsPlanned(count2) {
|
|
68106
|
+
return `${1 + count2} to ${1 + 2 * count2}`;
|
|
65077
68107
|
}
|
|
65078
68108
|
async function vaultList(args, ctx) {
|
|
65079
68109
|
const parsed = parseArgs(args, {
|
|
@@ -67700,9 +70730,9 @@ function resolveTarget(index, old, id) {
|
|
|
67700
70730
|
throw new VaultError("VAULT_LABEL_NOT_FOUND", id === undefined ? `No key in this vault is called ${old}, and no key has that address or id.` : `No key in this vault has the id ${id}.`, { suggestion: "List them with their labels: `candle vault status --unlock`" });
|
|
67701
70731
|
}
|
|
67702
70732
|
const candidates = match.candidates.map((entry) => `${entry.address} (${entry.id})`).join(", ");
|
|
67703
|
-
const
|
|
70733
|
+
const count2 = match.candidates.length;
|
|
67704
70734
|
const first = match.candidates[0];
|
|
67705
|
-
throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${
|
|
70735
|
+
throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${count2} keys in this vault are called ${old}, so this rename would not say which one it meant. Nothing was written.` : `${count2} keys in this vault have the address ${old}, so this rename would not say which one it meant. Nothing was written.`, {
|
|
67706
70736
|
suggestion: match.by === "label" ? `Name one by address or id. The candidates are ${candidates}.` : `Name one by id. The candidates are ${candidates}. Re-run: \`candle vault rename ${old} <new-label> --id ${first.id}\`.`
|
|
67707
70737
|
});
|
|
67708
70738
|
}
|
|
@@ -67866,7 +70896,7 @@ async function discardIncompleteRestore(ctx, path, sidecarExisted) {
|
|
|
67866
70896
|
}
|
|
67867
70897
|
}
|
|
67868
70898
|
var RESTORED_BRANCHES = ["solanaVault", "solanaTee", "solanaExternal"];
|
|
67869
|
-
function parseCounts(
|
|
70899
|
+
function parseCounts(count2, teeCount, externalCount, rpcUrl, evmCount, evmTeeCount) {
|
|
67870
70900
|
const parse = (raw, flag) => {
|
|
67871
70901
|
if (raw === undefined)
|
|
67872
70902
|
return;
|
|
@@ -67875,7 +70905,7 @@ function parseCounts(count, teeCount, externalCount, rpcUrl, evmCount, evmTeeCou
|
|
|
67875
70905
|
return { error: `${flag} must be a whole number, zero or greater.` };
|
|
67876
70906
|
return value;
|
|
67877
70907
|
};
|
|
67878
|
-
const vaultCount = parse(
|
|
70908
|
+
const vaultCount = parse(count2, "--count");
|
|
67879
70909
|
if (typeof vaultCount === "object" && vaultCount !== null)
|
|
67880
70910
|
return vaultCount;
|
|
67881
70911
|
const teeParsed = parse(teeCount, "--tee-count");
|
|
@@ -68148,7 +71178,7 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
|
|
|
68148
71178
|
await commitVault(vault, { index: { hd, entries }, addKeys: derived.entries.map((entry) => entry.blob) }, ctx.deps);
|
|
68149
71179
|
return { entries, hd };
|
|
68150
71180
|
}
|
|
68151
|
-
function teeFieldsFor(row, ctx,
|
|
71181
|
+
function teeFieldsFor(row, ctx, network2 = "solana-mainnet") {
|
|
68152
71182
|
const remoteState = row.sweptAt !== undefined ? "swept" : row.revokedAt !== undefined ? "quarantined" : "enabled";
|
|
68153
71183
|
const grantIdentity = {
|
|
68154
71184
|
account: "",
|
|
@@ -68156,7 +71186,7 @@ function teeFieldsFor(row, ctx, network = "solana-mainnet") {
|
|
|
68156
71186
|
source: "recorded-at-operation"
|
|
68157
71187
|
};
|
|
68158
71188
|
const common = {
|
|
68159
|
-
network,
|
|
71189
|
+
network: network2,
|
|
68160
71190
|
...row.vaultDestination !== undefined ? { vaultDestination: row.vaultDestination } : {},
|
|
68161
71191
|
...row.boundKeyPrefix !== undefined ? { boundKeyPrefix: row.boundKeyPrefix } : {},
|
|
68162
71192
|
...row.remoteAuthority !== undefined ? { remoteAuthority: row.remoteAuthority } : {},
|
|
@@ -69005,7 +72035,7 @@ init_args();
|
|
|
69005
72035
|
init_release();
|
|
69006
72036
|
import { dirname as dirname10, join as join11 } from "node:path";
|
|
69007
72037
|
init_render();
|
|
69008
|
-
var
|
|
72038
|
+
var USAGE5 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
|
|
69009
72039
|
async function resolveIdentity(deps, bundlePath, flag) {
|
|
69010
72040
|
if (flag)
|
|
69011
72041
|
return { kind: "ok", uri: flag, provenance: "identity from --identity" };
|
|
@@ -69037,19 +72067,19 @@ async function verify(args, ctx) {
|
|
|
69037
72067
|
});
|
|
69038
72068
|
if ("error" in parsed) {
|
|
69039
72069
|
writeUsageFailure(deps, `${parsed.error}
|
|
69040
|
-
${
|
|
72070
|
+
${USAGE5}`, json);
|
|
69041
72071
|
return 2;
|
|
69042
72072
|
}
|
|
69043
72073
|
const file = parsed.positionals[0];
|
|
69044
72074
|
if (parsed.positionals.length !== 1 || file === undefined) {
|
|
69045
72075
|
writeUsageFailure(deps, `verify takes exactly one file.
|
|
69046
|
-
${
|
|
72076
|
+
${USAGE5}`, json);
|
|
69047
72077
|
return 2;
|
|
69048
72078
|
}
|
|
69049
72079
|
const bundlePath = parsed.values["--bundle"];
|
|
69050
72080
|
if (!bundlePath) {
|
|
69051
72081
|
writeUsageFailure(deps, `--bundle is required.
|
|
69052
|
-
${
|
|
72082
|
+
${USAGE5}`, json);
|
|
69053
72083
|
return 2;
|
|
69054
72084
|
}
|
|
69055
72085
|
const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
|
|
@@ -69063,7 +72093,7 @@ ${USAGE4}`, json);
|
|
|
69063
72093
|
}
|
|
69064
72094
|
if (resolved.kind === "absent") {
|
|
69065
72095
|
writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
|
|
69066
|
-
${
|
|
72096
|
+
${USAGE5}`, json);
|
|
69067
72097
|
return 2;
|
|
69068
72098
|
}
|
|
69069
72099
|
const identity = resolved.uri;
|
|
@@ -69111,8 +72141,8 @@ init_wallets();
|
|
|
69111
72141
|
init_args();
|
|
69112
72142
|
init_render();
|
|
69113
72143
|
init_trading();
|
|
69114
|
-
import { randomUUID as
|
|
69115
|
-
var
|
|
72144
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
72145
|
+
var USAGE6 = "Usage: candle wallet close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
|
|
69116
72146
|
var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
69117
72147
|
var LISTED_ROWS = 50;
|
|
69118
72148
|
function splitKeep(args) {
|
|
@@ -69143,7 +72173,7 @@ function solOf(lamports) {
|
|
|
69143
72173
|
const digits = lamports.padStart(10, "0");
|
|
69144
72174
|
return `${digits.slice(0, -9)}.${digits.slice(-9)}`.replace(/\.?0+$/, "");
|
|
69145
72175
|
}
|
|
69146
|
-
function
|
|
72176
|
+
function previewLines2(preview) {
|
|
69147
72177
|
const lines = [
|
|
69148
72178
|
`Close ${preview.accountCount} empty token account${preview.accountCount === 1 ? "" : "s"} on the embedded wallet ${safeText(preview.wallet)}`,
|
|
69149
72179
|
`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`
|
|
@@ -69187,7 +72217,7 @@ async function embeddedSolana(ctx, key, wallet) {
|
|
|
69187
72217
|
async function walletsCloseEmpty(args, ctx) {
|
|
69188
72218
|
const lifted = splitKeep(args);
|
|
69189
72219
|
if ("error" in lifted) {
|
|
69190
|
-
writeUsageFailure(ctx.deps, `${lifted.error}. ${
|
|
72220
|
+
writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE6}`, ctx.json);
|
|
69191
72221
|
return 2;
|
|
69192
72222
|
}
|
|
69193
72223
|
const parsed = parseArgs(lifted.rest, { valueFlags: ["--wallet", "--client-trade-id"], booleanFlags: ["--yes"] });
|
|
@@ -69196,9 +72226,9 @@ async function walletsCloseEmpty(args, ctx) {
|
|
|
69196
72226
|
return 2;
|
|
69197
72227
|
}
|
|
69198
72228
|
const named = parsed.values["--client-trade-id"];
|
|
69199
|
-
const id = named ?? `close-${
|
|
72229
|
+
const id = named ?? `close-${randomUUID5()}`;
|
|
69200
72230
|
if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
|
|
69201
|
-
writeUsageFailure(ctx.deps,
|
|
72231
|
+
writeUsageFailure(ctx.deps, USAGE6, ctx.json);
|
|
69202
72232
|
return 2;
|
|
69203
72233
|
}
|
|
69204
72234
|
const keep = lifted.keep;
|
|
@@ -69223,7 +72253,7 @@ async function walletsCloseEmpty(args, ctx) {
|
|
|
69223
72253
|
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
69224
72254
|
output.write(`No empty token accounts to close on the embedded wallet ${safeText(wallet)}.
|
|
69225
72255
|
`);
|
|
69226
|
-
for (const line of
|
|
72256
|
+
for (const line of previewLines2(preview).slice(2))
|
|
69227
72257
|
output.write(`${line}
|
|
69228
72258
|
`);
|
|
69229
72259
|
return printTradingResult(ctx, {
|
|
@@ -69233,7 +72263,7 @@ async function walletsCloseEmpty(args, ctx) {
|
|
|
69233
72263
|
preview
|
|
69234
72264
|
});
|
|
69235
72265
|
}
|
|
69236
|
-
const confirmed = await confirmClose(ctx,
|
|
72266
|
+
const confirmed = await confirmClose(ctx, previewLines2(preview), parsed.booleans.has("--yes"));
|
|
69237
72267
|
if (!confirmed)
|
|
69238
72268
|
return printTradingResult(ctx, {
|
|
69239
72269
|
success: true,
|
|
@@ -69326,7 +72356,7 @@ function trustTable(rows) {
|
|
|
69326
72356
|
}
|
|
69327
72357
|
async function setTrust(args, ctx, trusted) {
|
|
69328
72358
|
const { deps, apiUrl, json } = ctx;
|
|
69329
|
-
const
|
|
72359
|
+
const usage6 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
|
|
69330
72360
|
const command = trusted ? "wallets trust" : "wallets untrust";
|
|
69331
72361
|
const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
|
|
69332
72362
|
if ("error" in parsed) {
|
|
@@ -69334,7 +72364,7 @@ async function setTrust(args, ctx, trusted) {
|
|
|
69334
72364
|
return 2;
|
|
69335
72365
|
}
|
|
69336
72366
|
if (parsed.positionals.length === 0) {
|
|
69337
|
-
writeUsageFailure(deps, `Name at least one wallet. ${
|
|
72367
|
+
writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
|
|
69338
72368
|
return 2;
|
|
69339
72369
|
}
|
|
69340
72370
|
const skipConfirm = !trusted && parsed.booleans.has("--yes");
|
|
@@ -69605,7 +72635,20 @@ var COMMANDS = {
|
|
|
69605
72635
|
launch: { bare: launch },
|
|
69606
72636
|
pnl: { bare: pnl },
|
|
69607
72637
|
portfolio: { bare: portfolio },
|
|
72638
|
+
plans: { bare: plans },
|
|
69608
72639
|
lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
|
|
72640
|
+
perps: {
|
|
72641
|
+
subcommands: {
|
|
72642
|
+
setup: perpsSetup,
|
|
72643
|
+
deposit: perpsDeposit,
|
|
72644
|
+
open: perpsOpen,
|
|
72645
|
+
close: perpsClose,
|
|
72646
|
+
cancel: perpsCancel,
|
|
72647
|
+
orders: perpsOrders,
|
|
72648
|
+
positions: perpsPositions,
|
|
72649
|
+
leverage: perpsLeverage
|
|
72650
|
+
}
|
|
72651
|
+
},
|
|
69609
72652
|
auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
|
|
69610
72653
|
keys: {
|
|
69611
72654
|
subcommands: {
|
|
@@ -69677,7 +72720,7 @@ var COMMANDS = {
|
|
|
69677
72720
|
}
|
|
69678
72721
|
},
|
|
69679
72722
|
external: { subcommands: { new: externalNew, list: externalList, sweep: externalSweep } },
|
|
69680
|
-
sign: { subcommands: { message: signMessage2 }, bare:
|
|
72723
|
+
sign: { subcommands: { message: signMessage2 }, bare: sign4 },
|
|
69681
72724
|
secrets: { subcommands: { set: secretsSet, list: secretsList, remove: secretsRemove } },
|
|
69682
72725
|
plugins: { bare: plugins },
|
|
69683
72726
|
doctor: { bare: doctor },
|
|
@@ -69719,6 +72762,7 @@ var NEVER_GUARDED = new Set([
|
|
|
69719
72762
|
"profile",
|
|
69720
72763
|
"doctor",
|
|
69721
72764
|
"verify",
|
|
72765
|
+
"plans",
|
|
69722
72766
|
"update",
|
|
69723
72767
|
"help",
|
|
69724
72768
|
"completion",
|
|
@@ -69981,8 +73025,8 @@ async function buildRealDeps() {
|
|
|
69981
73025
|
const { runStdioServer: runStdioServer2 } = await Promise.resolve().then(() => (init_server2(), exports_server));
|
|
69982
73026
|
await runStdioServer2(env);
|
|
69983
73027
|
},
|
|
69984
|
-
readFile: (path) =>
|
|
69985
|
-
readBytes: (path) =>
|
|
73028
|
+
readFile: (path) => readFile10(path, "utf8"),
|
|
73029
|
+
readBytes: (path) => readFile10(path),
|
|
69986
73030
|
readStdin: () => new Promise((resolve4, reject) => {
|
|
69987
73031
|
const chunks = [];
|
|
69988
73032
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|