@candledottv/cli 0.11.13 → 0.11.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -3
  2. package/dist/index.js +2454 -165
  3. 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 json.error.code === "number" ? { rpcCode: json.error.code } : {}
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 call("getLatestBlockhash", [{ commitment: "finalized" }]);
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 call("getBalance", [address, { commitment: "finalized" }]);
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 call("getTokenAccountsByOwner", [owner, { programId }, { encoding: "jsonParsed", commitment: "finalized" }]);
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 call("getFeeForMessage", [messageBase64, { commitment: "finalized" }]);
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 call("getAccountInfo", [address, { encoding: "base64", commitment: "finalized" }]);
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 call("getEpochInfo", [{ commitment: "finalized" }]);
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 call("getMultipleAccounts", [
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 call("simulateTransaction", [
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
- return r.value[0] ?? null;
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 call("isBlockhashValid", [blockhash, { commitment }]);
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
- 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_RATE_LIMIT_CODE = -32429, RPC_RETRY_DEFAULT_MS = 2000, RPC_RETRY_CAP_MS = 1e4, NEVER_RETRIED;
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
- return status.err === null || status.err === undefined ? { kind: "finalized" } : { kind: "failed", err: status.err };
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
- if (isRateLimited(error))
33157
- notePostSignatureRateLimit(ctx, signature);
33158
- return {
33159
- status: "uncertain",
33160
- signature,
33161
- error: `send did not answer cleanly (${describeRpcFailure(error)}); it may still land`
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 rpc = solana.rpc;
33545
+ const floor = createContextFloor();
33546
+ for (const receipt of entry.tee?.sweepReceipts ?? [])
33547
+ if (receipt.slot !== undefined)
33548
+ floor.raise(receipt.slot);
33549
+ const isContextFailure = (error) => error instanceof SolanaRpcError && Boolean(error.contextBehind || error.contextUnverified || floor.slot !== undefined && error.rpcCode === -32602);
33550
+ let contextFailure;
33551
+ const floored = withContextFloor(solana.rpc, floor);
33552
+ const rpc = new Proxy(floored, {
33553
+ get(target, key) {
33554
+ const method = target[key];
33555
+ return async (...args2) => {
33556
+ try {
33557
+ return await method(...args2);
33558
+ } catch (error) {
33559
+ if (isContextFailure(error))
33560
+ contextFailure = error;
33561
+ throw error;
33562
+ }
33563
+ };
33564
+ }
33565
+ });
33330
33566
  const retained = entry.tee?.sweepReceipts ?? [];
33331
33567
  const receipts = [];
33332
33568
  const residuals = [];
@@ -33334,6 +33570,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
33334
33570
  const alreadyRecordedLocally = entry.tee?.sweptAt !== undefined;
33335
33571
  const pendingStill = [];
33336
33572
  const retainReceipt = async (receipt) => {
33573
+ if (receipt.slot !== undefined)
33574
+ floor.raise(receipt.slot);
33337
33575
  receipts.push(receipt);
33338
33576
  const kept = await commitActiveTee(active, (target) => {
33339
33577
  const existing = target.tee?.sweepReceipts ?? [];
@@ -33361,6 +33599,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
33361
33599
  residuals.push({ kind: "local-record-failed", detail: kept.message });
33362
33600
  return kept.ok;
33363
33601
  };
33602
+ const cleanupFailures = new Map;
33364
33603
  const clearPending = async (signature) => {
33365
33604
  const kept = await commitActiveTee(active, (target) => {
33366
33605
  target.tee = {
@@ -33368,12 +33607,58 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
33368
33607
  sweepPending: (target.tee?.sweepPending ?? []).filter((p) => p.signature !== signature)
33369
33608
  };
33370
33609
  });
33371
- if (!kept.ok)
33610
+ if (!kept.ok) {
33372
33611
  residuals.push({ kind: "local-record-failed", detail: kept.message });
33612
+ cleanupFailures.set(signature, sanitizeSimulation(kept.message, {}).message);
33613
+ }
33614
+ return kept.ok;
33615
+ };
33616
+ const attemptedAccounts = new Set;
33617
+ const broadcast2 = (instructions, pending) => {
33618
+ if (pending.account)
33619
+ attemptedAccounts.add(pending.account);
33620
+ return broadcastAndFinalize(rpc, ctx, secret, teePubkey, instructions, pending, recordPending, clearPending, floor);
33373
33621
  };
33374
- const broadcast2 = (instructions, pending) => broadcastAndFinalize(rpc, ctx, secret, teePubkey, instructions, pending, recordPending, clearPending);
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
- residuals.push({
33443
- kind: "inventory",
33444
- detail: `could not list Token-2022 accounts for position discovery: ${describeRpcFailure(error)}`
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
- break;
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
- leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "not signed: an earlier transaction is still in flight");
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
- const outcome = await broadcastMessage(rpc, ctx, secret, verdict.tx.message.bytes, verdict.tx.message.recentBlockhash, pending, recordPending, clearPending);
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
- residuals.push({
33551
- kind: "inventory",
33552
- detail: `could not list ${programId === TOKEN_PROGRAM_ID ? "token" : "Token-2022"} accounts: ${describeRpcFailure(error)}`
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
- break;
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
- residuals.push({ kind: "sol-transfer-failed", detail: describeRpcFailure(error) });
34062
+ if (!stopForContext(error))
34063
+ residuals.push({ kind: "sol-transfer-failed", detail: describeRpcFailure(error) });
33723
34064
  }
34065
+ if (signingBlocked)
34066
+ for (const acct of knownAccounts.values())
34067
+ notAttempted(acct);
33724
34068
  const inventory = { observedAt: new Date(deps.now()).toISOString(), verified: false, lamports: null, tokenAccounts: null };
33725
34069
  try {
33726
34070
  const listed = new Set(residuals.map((r) => r.account).filter((a) => a !== undefined));
@@ -33819,6 +34163,7 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
33819
34163
  signature: p.signature
33820
34164
  })),
33821
34165
  inventory,
34166
+ contextFloorSlot: floor.slot ?? null,
33822
34167
  recordedOnServer,
33823
34168
  ...lpCloses.length > 0 ? { lpCloses } : {}
33824
34169
  })}
@@ -33845,6 +34190,18 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
33845
34190
  `);
33846
34191
  if (pendingStill.length > 0)
33847
34192
  deps.stdout.write(`${pendingStill.length} transaction(s) still in flight (${pendingStill.map((p) => p.signature).join(", ")}). Re-run this sweep: a finalized one becomes a receipt, an expired one is swept again.
34193
+ `);
34194
+ const rejected = residuals.filter((r) => r.rejection);
34195
+ if (pendingStill.length === 0 && rejected.length > 0 && rejected.every((r) => r.rejection?.pendingCleared)) {
34196
+ deps.stdout.write(`Nothing from this run is in flight: ${rejected.length} transaction(s) were rejected before broadcast. Re-run this sweep now.
34197
+ `);
34198
+ if (rejected.some((r) => r.rejection?.reason === "context-behind"))
34199
+ deps.stdout.write(`Fix: ${rpcFixLines(ctx).join(", ")}
34200
+ `);
34201
+ }
34202
+ for (const r of rejected)
34203
+ if (!r.rejection?.pendingCleared)
34204
+ deps.stdout.write(`The local pending record for ${r.signature} could not be removed (${cleanupFailures.get(r.signature ?? "")}). It was never broadcast; the next run drops it once its blockhash expires, normally within two minutes. Re-run after that.
33848
34205
  `);
33849
34206
  if (serverState === "disable-pending" && !emergency)
33850
34207
  deps.stdout.write(`Remote signing authority is not verified denied. Re-run: candle tee disable ${address}, then re-run this sweep to record it.
@@ -49488,7 +49845,7 @@ var init_convert = __esm(() => {
49488
49845
  });
49489
49846
 
49490
49847
  // ../mcp/src/version.ts
49491
- var SERVER_VERSION = "0.10.4";
49848
+ var SERVER_VERSION = "0.10.5";
49492
49849
 
49493
49850
  // ../mcp/src/update-notice.ts
49494
49851
  function newer(a, b) {
@@ -49940,6 +50297,630 @@ var init_orchestrate = __esm(() => {
49940
50297
  };
49941
50298
  });
49942
50299
 
50300
+ // ../mcp/src/hyperliquid.ts
50301
+ function pushUint2(out, value, width) {
50302
+ for (let i = width - 1;i >= 0; i--)
50303
+ out.push(Number(value >> BigInt(i * 8) & 0xffn));
50304
+ }
50305
+ function packInteger2(out, value) {
50306
+ if (value >= 0n) {
50307
+ if (value > UINT64_MAX2)
50308
+ throw new Error("msgpack: integer above uint64");
50309
+ if (value < 0x80n)
50310
+ out.push(Number(value));
50311
+ else if (value <= 0xffn) {
50312
+ out.push(204);
50313
+ pushUint2(out, value, 1);
50314
+ } else if (value <= 0xffffn) {
50315
+ out.push(205);
50316
+ pushUint2(out, value, 2);
50317
+ } else if (value <= 0xffffffffn) {
50318
+ out.push(206);
50319
+ pushUint2(out, value, 4);
50320
+ } else {
50321
+ out.push(207);
50322
+ pushUint2(out, value, 8);
50323
+ }
50324
+ return;
50325
+ }
50326
+ if (value < INT64_MIN2)
50327
+ throw new Error("msgpack: integer below int64");
50328
+ if (value >= -32n)
50329
+ out.push(Number(value & 0xffn));
50330
+ else if (value >= -128n) {
50331
+ out.push(208);
50332
+ pushUint2(out, value & 0xffn, 1);
50333
+ } else if (value >= -32768n) {
50334
+ out.push(209);
50335
+ pushUint2(out, value & 0xffffn, 2);
50336
+ } else if (value >= -2147483648n) {
50337
+ out.push(210);
50338
+ pushUint2(out, value & 0xffffffffn, 4);
50339
+ } else {
50340
+ out.push(211);
50341
+ pushUint2(out, value & UINT64_MAX2, 8);
50342
+ }
50343
+ }
50344
+ function packHeader2(out, length, fixBase, fixMax, codes) {
50345
+ if (length <= fixMax) {
50346
+ out.push(fixBase | length);
50347
+ return;
50348
+ }
50349
+ const [c8, c16, c32] = codes;
50350
+ if (c8 !== undefined && c8 !== 0 && length <= 255) {
50351
+ out.push(c8);
50352
+ pushUint2(out, BigInt(length), 1);
50353
+ } else if (length <= 65535) {
50354
+ out.push(c16);
50355
+ pushUint2(out, BigInt(length), 2);
50356
+ } else {
50357
+ out.push(c32);
50358
+ pushUint2(out, BigInt(length), 4);
50359
+ }
50360
+ }
50361
+ function packValue2(out, value) {
50362
+ if (value === null)
50363
+ out.push(192);
50364
+ else if (value === false)
50365
+ out.push(194);
50366
+ else if (value === true)
50367
+ out.push(195);
50368
+ else if (typeof value === "bigint")
50369
+ packInteger2(out, value);
50370
+ else if (typeof value === "number") {
50371
+ if (!Number.isSafeInteger(value))
50372
+ throw new Error(`msgpack: ${value} is not a safe integer`);
50373
+ packInteger2(out, BigInt(value));
50374
+ } else if (typeof value === "string") {
50375
+ const bytes = new TextEncoder().encode(value);
50376
+ packHeader2(out, bytes.length, 160, 31, [217, 218, 219]);
50377
+ for (const b of bytes)
50378
+ out.push(b);
50379
+ } else if (Array.isArray(value)) {
50380
+ packHeader2(out, value.length, 144, 15, [0, 220, 221]);
50381
+ for (const item of value)
50382
+ packValue2(out, item);
50383
+ } else if (typeof value === "object") {
50384
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
50385
+ for (const [key] of entries) {
50386
+ if (/^(0|[1-9][0-9]*)$/.test(key))
50387
+ throw new Error(`msgpack: integer-like key "${key}"`);
50388
+ }
50389
+ packHeader2(out, entries.length, 128, 15, [0, 222, 223]);
50390
+ for (const [key, item] of entries) {
50391
+ packValue2(out, key);
50392
+ packValue2(out, item);
50393
+ }
50394
+ } else {
50395
+ throw new Error(`msgpack: cannot encode a ${typeof value}`);
50396
+ }
50397
+ }
50398
+ function msgpackEncode2(value) {
50399
+ const out = [];
50400
+ packValue2(out, value);
50401
+ return Uint8Array.from(out);
50402
+ }
50403
+ function toHex2(bytes) {
50404
+ let out = "";
50405
+ for (const b of bytes)
50406
+ out += b.toString(16).padStart(2, "0");
50407
+ return out;
50408
+ }
50409
+ function hyperliquidActionHash2(action, nonce) {
50410
+ const n = BigInt(nonce);
50411
+ if (n < 0n || n > UINT64_MAX2)
50412
+ throw new Error("hyperliquidActionHash: nonce out of range");
50413
+ const out = Array.from(msgpackEncode2(action));
50414
+ pushUint2(out, n, 8);
50415
+ out.push(0);
50416
+ return `0x${toHex2(keccak_256(Uint8Array.from(out)))}`;
50417
+ }
50418
+ function hyperliquidL1TypedData2(connectionId, network2) {
50419
+ return {
50420
+ domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS2 },
50421
+ types: {
50422
+ Agent: [
50423
+ { name: "source", type: "string" },
50424
+ { name: "connectionId", type: "bytes32" }
50425
+ ]
50426
+ },
50427
+ primary_type: "Agent",
50428
+ message: { source: network2 === "mainnet" ? "a" : "b", connectionId }
50429
+ };
50430
+ }
50431
+ function hyperliquidApproveBuilderFeeTypedData2(builder, nonce) {
50432
+ return {
50433
+ domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS2 },
50434
+ types: {
50435
+ "HyperliquidTransaction:ApproveBuilderFee": [
50436
+ { name: "hyperliquidChain", type: "string" },
50437
+ { name: "maxFeeRate", type: "string" },
50438
+ { name: "builder", type: "address" },
50439
+ { name: "nonce", type: "uint64" }
50440
+ ]
50441
+ },
50442
+ primary_type: "HyperliquidTransaction:ApproveBuilderFee",
50443
+ message: {
50444
+ hyperliquidChain: "Mainnet",
50445
+ maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE2,
50446
+ builder: builder.toLowerCase(),
50447
+ nonce
50448
+ }
50449
+ };
50450
+ }
50451
+ function hyperliquidCanonicalJson2(value) {
50452
+ return canonical2(value);
50453
+ }
50454
+ function canonical2(value) {
50455
+ if (value === null || typeof value !== "object")
50456
+ return JSON.stringify(value);
50457
+ if (Array.isArray(value))
50458
+ return `[${value.map(canonical2).join(",")}]`;
50459
+ const record2 = value;
50460
+ return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonical2(record2[key])}`).join(",")}}`;
50461
+ }
50462
+ function sameValue2(a, b) {
50463
+ return canonical2(a) === canonical2(b);
50464
+ }
50465
+ function checkBuilderField2(action, builder) {
50466
+ if (!("builder" in action) || action.builder === undefined)
50467
+ return null;
50468
+ const field = action.builder;
50469
+ if (typeof field !== "object" || field === null)
50470
+ return "builder field is not an object";
50471
+ if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
50472
+ return "the order names a builder that is not Candle's";
50473
+ }
50474
+ if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS2) {
50475
+ return "the builder fee is outside Candle's 0.1% maximum";
50476
+ }
50477
+ return null;
50478
+ }
50479
+ function decimal2(value) {
50480
+ if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
50481
+ return null;
50482
+ const negative = value.startsWith("-");
50483
+ const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
50484
+ const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
50485
+ return negative ? -n : n;
50486
+ }
50487
+ function checkActionIntent2(action, intent) {
50488
+ const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
50489
+ const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
50490
+ if (!Array.isArray(orderList))
50491
+ return "invalid orders";
50492
+ for (const raw of orderList) {
50493
+ if (!raw || typeof raw !== "object")
50494
+ return "invalid order";
50495
+ const o = raw;
50496
+ if (!mainAsset(o.a))
50497
+ return "order asset is outside the main perp universe";
50498
+ if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal2(o.s) ?? 0n) <= 0n || (decimal2(o.p) ?? 0n) <= 0n)
50499
+ return "invalid order side, size, price or reduce-only flag";
50500
+ }
50501
+ if (action.type === "cancel") {
50502
+ if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
50503
+ return "invalid cancel asset";
50504
+ }
50505
+ if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
50506
+ return "invalid action asset";
50507
+ if (!intent)
50508
+ return null;
50509
+ const { method, params: p } = intent;
50510
+ const types2 = {
50511
+ setup: "approveBuilderFee",
50512
+ open: "order",
50513
+ close: "order",
50514
+ cancel: "cancel",
50515
+ modify: "modify",
50516
+ leverage: "updateLeverage",
50517
+ margin: "updateIsolatedMargin"
50518
+ };
50519
+ if (action.type !== types2[method])
50520
+ return "action type does not match the requested method";
50521
+ if (method === "leverage") {
50522
+ if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
50523
+ return "leverage or mode does not match the request";
50524
+ }
50525
+ if (method === "margin") {
50526
+ const amount = decimal2(p.amount);
50527
+ if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
50528
+ return "isolated margin amount does not match the request";
50529
+ }
50530
+ if (method === "open" || method === "close" || method === "modify") {
50531
+ const list = orderList;
50532
+ const first = list[0];
50533
+ if (!first)
50534
+ return "missing requested order";
50535
+ const typ = first.t;
50536
+ if (!typ?.limit)
50537
+ return "requested order is not a limit/IOC order";
50538
+ if (method === "open") {
50539
+ const buy = p.side === "long" || p.side === "buy";
50540
+ if (first.b !== buy || first.r !== false || decimal2(first.s) !== decimal2(p.size))
50541
+ return "order side, size or reduce-only flag does not match the request";
50542
+ const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
50543
+ if (typ.limit.tif !== tif)
50544
+ return "order time in force does not match the request";
50545
+ const triggers = [
50546
+ ["takeProfit", "tp"],
50547
+ ["stopLoss", "sl"]
50548
+ ].filter(([field]) => p[field] != null);
50549
+ if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
50550
+ return "unexpected extra orders or grouping";
50551
+ for (let i = 0;i < triggers.length; i++) {
50552
+ const [field, kind] = triggers[i];
50553
+ const leg = list[i + 1];
50554
+ if (!leg)
50555
+ return "missing trigger order";
50556
+ const trigger = leg.t?.trigger;
50557
+ if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal2(leg.s) !== decimal2(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal2(trigger.triggerPx) !== decimal2(p[field]))
50558
+ return "trigger side, size, reduce-only flag or price does not match the request";
50559
+ }
50560
+ } else {
50561
+ if (list.length !== 1)
50562
+ return "unexpected extra orders";
50563
+ if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal2(first.s) ?? 0n) > (decimal2(p.size) ?? 0n)))
50564
+ return "close size or reduce-only flag does not match the request";
50565
+ if (method === "modify" && p.size != null && decimal2(first.s) !== decimal2(p.size))
50566
+ return "replacement size does not match the request";
50567
+ }
50568
+ if (p.price != null && decimal2(first.p) !== decimal2(p.price))
50569
+ return "order price does not match the request";
50570
+ }
50571
+ return null;
50572
+ }
50573
+ async function hyperliquidCloseOrder2(fetcher, network2, address, params) {
50574
+ const read = async (body) => {
50575
+ const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS2[network2].replace("/exchange", "/info"), {
50576
+ method: "POST",
50577
+ headers: { "content-type": "application/json" },
50578
+ body: JSON.stringify(body)
50579
+ });
50580
+ if (!res.ok)
50581
+ throw new Error("Cannot read the close position from Hyperliquid");
50582
+ return JSON.parse(await res.text());
50583
+ };
50584
+ const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
50585
+ const coin = String(params.coin).toUpperCase();
50586
+ const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
50587
+ const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
50588
+ const signed = decimal2(szi);
50589
+ if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
50590
+ throw new Error("No main-universe position to close");
50591
+ const held = signed < 0n ? -signed : signed;
50592
+ const asked = params.size == null ? held : decimal2(params.size);
50593
+ if (asked === null || asked <= 0n)
50594
+ throw new Error("Invalid close size");
50595
+ const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
50596
+ return { asset, isBuy: signed < 0n, size };
50597
+ }
50598
+ function verifyPerpsBuild2(build, opts) {
50599
+ try {
50600
+ const network2 = opts.network ?? "mainnet";
50601
+ if (!EVM_ADDRESS_RE2.test(opts.builder))
50602
+ return { ok: false, reason: "no valid Candle builder address to check against" };
50603
+ if (build.network !== undefined && build.network !== network2) {
50604
+ return { ok: false, reason: `the build is for ${build.network}, this client trades ${network2}` };
50605
+ }
50606
+ if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
50607
+ return { ok: false, reason: "the build is for a different wallet" };
50608
+ }
50609
+ if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
50610
+ return { ok: false, reason: "invalid nonce" };
50611
+ const typed = build.typedData;
50612
+ if (!typed || typeof typed !== "object")
50613
+ return { ok: false, reason: "no typed data" };
50614
+ const action = build.action;
50615
+ if (!action || typeof action !== "object")
50616
+ return { ok: false, reason: "no action" };
50617
+ const intentRefusal = checkActionIntent2(action, opts.intent);
50618
+ if (intentRefusal)
50619
+ return { ok: false, reason: intentRefusal };
50620
+ if (opts.closeOrder) {
50621
+ const order = action.orders[0];
50622
+ if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal2(order.s) !== decimal2(opts.closeOrder.size) || order.r !== true)
50623
+ return { ok: false, reason: "close side, asset or size does not match the requested position" };
50624
+ }
50625
+ if (typed.primary_type === "Agent") {
50626
+ const type = action.type;
50627
+ if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES2.includes(type)) {
50628
+ return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
50629
+ }
50630
+ if (type === "order") {
50631
+ const refusal = checkBuilderField2(action, opts.builder);
50632
+ if (refusal)
50633
+ return { ok: false, reason: refusal };
50634
+ } else if ("builder" in action) {
50635
+ return { ok: false, reason: `a ${type} action carries no builder` };
50636
+ }
50637
+ if ("vaultAddress" in action)
50638
+ return { ok: false, reason: "the action names a vault" };
50639
+ const expected = hyperliquidL1TypedData2(hyperliquidActionHash2(action, build.nonce), network2);
50640
+ if (!sameValue2(typed, expected)) {
50641
+ return { ok: false, reason: "the typed data does not commit to this action and nonce" };
50642
+ }
50643
+ return { ok: true, kind: "l1" };
50644
+ }
50645
+ if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
50646
+ const expected = hyperliquidApproveBuilderFeeTypedData2(opts.builder, build.nonce);
50647
+ if (!sameValue2(typed, expected)) {
50648
+ return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
50649
+ }
50650
+ const expectedAction = {
50651
+ type: "approveBuilderFee",
50652
+ hyperliquidChain: "Mainnet",
50653
+ signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID2,
50654
+ maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE2,
50655
+ builder: opts.builder.toLowerCase(),
50656
+ nonce: build.nonce
50657
+ };
50658
+ if (!sameValue2(action, expectedAction)) {
50659
+ return { ok: false, reason: "the approval action does not match its typed data" };
50660
+ }
50661
+ return { ok: true, kind: "approveBuilderFee" };
50662
+ }
50663
+ return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
50664
+ } catch (err) {
50665
+ return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
50666
+ }
50667
+ }
50668
+ function hyperliquidSplitSignature2(signature) {
50669
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
50670
+ throw new Error("Not a 65-byte signature");
50671
+ let v = Number.parseInt(signature.slice(130, 132), 16);
50672
+ if (v < 27)
50673
+ v += 27;
50674
+ return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
50675
+ }
50676
+ function hyperliquidExchangeBody2(action, nonce, signature) {
50677
+ return { action, nonce, signature: hyperliquidSplitSignature2(signature), vaultAddress: null, expiresAfter: null };
50678
+ }
50679
+ function hyperliquidRelayBody2(typedData) {
50680
+ return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
50681
+ }
50682
+ var HYPERLIQUID_ALLOWED_ACTION_TYPES2, HYPERLIQUID_EXCHANGE_URLS2, CANDLE_HYPERLIQUID_BUILDER_ADDRESS2 = null, HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS2 = 100, HYPERLIQUID_MAX_BUILDER_FEE_RATE2 = "0.1%", HYPERLIQUID_SIGNATURE_CHAIN_ID2 = "0xa4b1", ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000", UINT64_MAX2, INT64_MIN2, EVM_ADDRESS_RE2;
50683
+ var init_hyperliquid = __esm(() => {
50684
+ init_sha3();
50685
+ HYPERLIQUID_ALLOWED_ACTION_TYPES2 = [
50686
+ "order",
50687
+ "cancel",
50688
+ "modify",
50689
+ "updateLeverage",
50690
+ "updateIsolatedMargin"
50691
+ ];
50692
+ HYPERLIQUID_EXCHANGE_URLS2 = {
50693
+ mainnet: "https://api.hyperliquid.xyz/exchange",
50694
+ testnet: "https://api.hyperliquid-testnet.xyz/exchange"
50695
+ };
50696
+ UINT64_MAX2 = (1n << 64n) - 1n;
50697
+ INT64_MIN2 = -(1n << 63n);
50698
+ EVM_ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
50699
+ });
50700
+
50701
+ // ../mcp/src/perps.ts
50702
+ import { createPrivateKey as createPrivateKey2, sign as sign5 } from "node:crypto";
50703
+ import { readFile as readFile9 } from "node:fs/promises";
50704
+ function fail3(code, message, extra = {}) {
50705
+ return { text: JSON.stringify({ success: false, error: { code, message }, ...extra }), isError: true };
50706
+ }
50707
+ async function call(cfg, fetch2, method, path, body) {
50708
+ const res = await fetch2(`${cfg.apiUrl.replace(/\/$/, "")}${path}`, {
50709
+ method,
50710
+ headers: { "Content-Type": "application/json", ...cfg.apiKey ? { "x-api-key": cfg.apiKey } : {} },
50711
+ ...body !== undefined ? { body: JSON.stringify(body) } : {}
50712
+ });
50713
+ const text = await res.text();
50714
+ let parsed;
50715
+ try {
50716
+ parsed = JSON.parse(text);
50717
+ } catch {
50718
+ parsed = text;
50719
+ }
50720
+ if (!res.ok) {
50721
+ if (res.status === 404 && (typeof parsed !== "object" || parsed === null))
50722
+ throw new Refusal(fail3("PERPS_NOT_ENABLED", "This Candle deployment does not serve perps routes."));
50723
+ throw new Refusal({ text: typeof parsed === "string" ? parsed : JSON.stringify(parsed), isError: true });
50724
+ }
50725
+ if (typeof parsed !== "object" || parsed === null)
50726
+ throw new Refusal(fail3("INVALID_RESPONSE", "Not a JSON object"));
50727
+ return parsed;
50728
+ }
50729
+ async function walletFor(cfg, fetch2, name2, scope) {
50730
+ const listed = await call(cfg, fetch2, "GET", "/api/v1/agent/wallets/trading");
50731
+ if (scope && !(Array.isArray(listed.scopes) && listed.scopes.includes(scope)))
50732
+ throw new Refusal(fail3("SCOPE_MISSING", `The key needs ${scope}.`));
50733
+ const rows = Array.isArray(listed.page) ? listed.page : [];
50734
+ const evm = rows.filter((row) => row.chain === "evm");
50735
+ const matches2 = name2 === undefined ? evm : evm.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
50736
+ if (matches2.length !== 1)
50737
+ throw new Refusal(fail3("TEE_WALLET_REQUIRED", evm.length === 0 ? "This key has no EVM TEE wallet bound to it." : `Name exactly one EVM TEE wallet: ${evm.map((row) => row.label ?? row.id).join(", ")}`));
50738
+ return { row: matches2[0], appId: typeof listed.privyAppId === "string" ? listed.privyAppId : "" };
50739
+ }
50740
+ async function builderPin2(cfg, fetch2, env) {
50741
+ const pinned = env.CANDLE_HYPERLIQUID_BUILDER?.trim() || CANDLE_HYPERLIQUID_BUILDER_ADDRESS2;
50742
+ if (pinned)
50743
+ return pinned.toLowerCase();
50744
+ const config = await call(cfg, fetch2, "GET", "/api/v1/agent/perps/config");
50745
+ if (typeof config.builder !== "string")
50746
+ throw new Refusal(fail3("PERPS_NOT_CONFIGURED", "The server reports no Hyperliquid builder address."));
50747
+ return config.builder.toLowerCase();
50748
+ }
50749
+ async function executePerps(tool, args, cfg, env, fetch2) {
50750
+ if (!cfg.apiKey)
50751
+ return fail3("MCP_VALIDATION", "CANDLE_AGENT_API_KEY is required for this tool.");
50752
+ const network2 = env.CANDLE_HYPERLIQUID_NETWORK === "testnet" ? "testnet" : "mainnet";
50753
+ const name2 = typeof args.wallet === "string" ? args.wallet : undefined;
50754
+ try {
50755
+ if (tool === "candle_perps_orders" || tool === "candle_perps_positions") {
50756
+ const { row: row2 } = await walletFor(cfg, fetch2, name2);
50757
+ const query = new URLSearchParams({ walletId: row2.id });
50758
+ if (typeof args.limit === "number")
50759
+ query.set("limit", String(args.limit));
50760
+ return { text: JSON.stringify(await call(cfg, fetch2, "GET", `${PATHS[tool]}?${query}`)) };
50761
+ }
50762
+ const willSubmit = args.submit !== false;
50763
+ const pemFile = env.CANDLE_KEY_SIGNER_PEM_FILE?.trim();
50764
+ let signerPem = null;
50765
+ if (willSubmit) {
50766
+ if (!pemFile)
50767
+ return fail3("SIGNER_UNAVAILABLE", "Set CANDLE_KEY_SIGNER_PEM_FILE to the key signer PEM (candle tee signer new --out <pem>) to sign perps actions.");
50768
+ try {
50769
+ signerPem = await readFile9(pemFile, "utf8");
50770
+ createPrivateKey2(signerPem);
50771
+ } catch {
50772
+ return fail3("SIGNER_UNAVAILABLE", "CANDLE_KEY_SIGNER_PEM_FILE does not hold a readable private key PEM.");
50773
+ }
50774
+ }
50775
+ const { row, appId } = await walletFor(cfg, fetch2, name2, "perps:write");
50776
+ const builder = await builderPin2(cfg, fetch2, env);
50777
+ const { wallet: _w, submit: _s, ...fields } = args;
50778
+ const build = await call(cfg, fetch2, "POST", PATHS[tool], {
50779
+ walletId: row.id,
50780
+ ...fields,
50781
+ ...tool === "candle_perps_open" ? { type: fields.price === undefined ? "market" : "limit" } : {}
50782
+ });
50783
+ if (build.ready === true)
50784
+ return { text: JSON.stringify(build) };
50785
+ const method = PATHS[tool].split("/").at(-1);
50786
+ const check = verifyPerpsBuild2(build, {
50787
+ builder,
50788
+ network: network2,
50789
+ address: row.address,
50790
+ intent: { method, params: fields }
50791
+ });
50792
+ if (!check.ok)
50793
+ return fail3("PERPS_BUILD_REFUSED", `Refused to sign this build: ${check.reason}. Nothing was signed.`);
50794
+ if (method === "close") {
50795
+ const closeOrder = await hyperliquidCloseOrder2(fetch2, network2, row.address, fields);
50796
+ const closeCheck = verifyPerpsBuild2(build, {
50797
+ builder,
50798
+ network: network2,
50799
+ address: row.address,
50800
+ intent: { method, params: fields },
50801
+ closeOrder
50802
+ });
50803
+ if (!closeCheck.ok)
50804
+ return fail3("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
50805
+ }
50806
+ if (!willSubmit || signerPem === null)
50807
+ return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
50808
+ if (!appId || !row.privyWalletId)
50809
+ return fail3("SIGNER_UNAVAILABLE", "The server did not return its relay identifiers.");
50810
+ const body = hyperliquidRelayBody2(build.typedData);
50811
+ const payload = hyperliquidCanonicalJson2({
50812
+ body,
50813
+ headers: { "privy-app-id": appId },
50814
+ method: "POST",
50815
+ url: `https://api.privy.io/v1/wallets/${row.privyWalletId}/rpc`,
50816
+ version: 1
50817
+ });
50818
+ const authorizationSignature2 = sign5("sha256", Buffer.from(payload), signerPem).toString("base64");
50819
+ const relay = await call(cfg, fetch2, "POST", `/api/v1/agent/wallets/${encodeURIComponent(row.id)}/sign`, {
50820
+ authorizationSignature: authorizationSignature2,
50821
+ body
50822
+ });
50823
+ const signature = relay.signature;
50824
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature))
50825
+ return fail3("INVALID_RESPONSE", "The relay did not return a signature.");
50826
+ const exchangeBody = hyperliquidExchangeBody2(build.action, build.nonce, signature);
50827
+ let exchange;
50828
+ try {
50829
+ const res = await fetch2(HYPERLIQUID_EXCHANGE_URLS2[network2], {
50830
+ method: "POST",
50831
+ headers: { "Content-Type": "application/json" },
50832
+ body: JSON.stringify(exchangeBody)
50833
+ });
50834
+ const text = await res.text();
50835
+ if (!res.ok)
50836
+ throw new Error(`HTTP ${res.status}`);
50837
+ exchange = JSON.parse(text);
50838
+ } catch (error) {
50839
+ return fail3("PERPS_SUBMIT_FAILED", `Signed, but the submit to Hyperliquid failed (${error instanceof Error ? error.message : "unknown"}). POST exchangeBody to Hyperliquid's /exchange to resubmit.`, { perpOrderId: build.perpOrderId, exchangeBody });
50840
+ }
50841
+ const accepted = typeof exchange === "object" && exchange !== null && exchange.status === "ok" && !(exchange.response?.data?.statuses ?? []).some((status) => typeof status === "object" && status !== null && ("error" in status));
50842
+ return {
50843
+ text: JSON.stringify({
50844
+ success: accepted,
50845
+ perpOrderId: build.perpOrderId,
50846
+ kind: build.kind,
50847
+ network: build.network,
50848
+ address: row.address,
50849
+ ...build.cloid ? { cloid: build.cloid } : {},
50850
+ ...build.childCloids ? { childCloids: build.childCloids } : {},
50851
+ nonce: build.nonce,
50852
+ signature,
50853
+ exchange
50854
+ }),
50855
+ ...accepted ? {} : { isError: true }
50856
+ };
50857
+ } catch (error) {
50858
+ if (error instanceof Refusal)
50859
+ return error.tool;
50860
+ return fail3("MCP_TRANSPORT", error instanceof Error ? error.message : "The perps call failed.");
50861
+ }
50862
+ }
50863
+ var decimal3, wallet, submit, perpsShapes, PATHS, Refusal;
50864
+ var init_perps = __esm(() => {
50865
+ init_zod();
50866
+ init_hyperliquid();
50867
+ decimal3 = exports_external.string().regex(/^(0|[1-9][0-9]*)(\.[0-9]+)?$/, "a plain decimal string");
50868
+ wallet = exports_external.string().optional().describe("The EVM TEE wallet by id, address or label. Omit when the key has exactly one.");
50869
+ submit = exports_external.boolean().optional().describe("false: build and check only, sign and submit nothing. Default true.");
50870
+ perpsShapes = {
50871
+ candle_perps_setup: { wallet, submit },
50872
+ candle_perps_open: {
50873
+ coin: exports_external.string().describe("A market on Hyperliquid's main perp exchange, e.g. BTC"),
50874
+ side: exports_external.enum(["long", "short"]),
50875
+ size: decimal3.describe('Size in the coin, decimal (e.g. "0.01")'),
50876
+ price: decimal3.optional().describe("Limit price. Omit for a market order (IOC within slippageBps of the mid)."),
50877
+ tif: exports_external.enum(["Gtc", "Alo", "Ioc"]).optional().describe("With price: time in force (default Gtc)"),
50878
+ slippageBps: exports_external.number().int().min(1).optional(),
50879
+ takeProfit: decimal3.optional().describe("Reduce-only take-profit trigger price"),
50880
+ stopLoss: decimal3.optional().describe("Reduce-only stop-loss trigger price"),
50881
+ wallet,
50882
+ submit
50883
+ },
50884
+ candle_perps_close: {
50885
+ coin: exports_external.string(),
50886
+ size: decimal3.optional().describe("Part of the position to close; the whole position when omitted"),
50887
+ slippageBps: exports_external.number().int().min(1).optional(),
50888
+ wallet,
50889
+ submit
50890
+ },
50891
+ candle_perps_cancel: {
50892
+ cloid: exports_external.string().regex(/^0x[0-9a-fA-F]{32}$/).describe("The cloid of an order Candle built (an open, or one of its take-profit / stop-loss legs)"),
50893
+ wallet,
50894
+ submit
50895
+ },
50896
+ candle_perps_orders: { wallet, limit: exports_external.number().int().min(1).max(200).optional() },
50897
+ candle_perps_positions: { wallet },
50898
+ candle_perps_leverage: {
50899
+ coin: exports_external.string(),
50900
+ leverage: exports_external.number().int().min(1),
50901
+ mode: exports_external.enum(["cross", "isolated"]).optional(),
50902
+ wallet,
50903
+ submit
50904
+ }
50905
+ };
50906
+ PATHS = {
50907
+ candle_perps_setup: "/api/v1/agent/perps/setup",
50908
+ candle_perps_open: "/api/v1/agent/perps/open",
50909
+ candle_perps_close: "/api/v1/agent/perps/close",
50910
+ candle_perps_cancel: "/api/v1/agent/perps/cancel",
50911
+ candle_perps_orders: "/api/v1/agent/perps/orders",
50912
+ candle_perps_positions: "/api/v1/agent/perps/positions",
50913
+ candle_perps_leverage: "/api/v1/agent/perps/leverage"
50914
+ };
50915
+ Refusal = class Refusal extends Error {
50916
+ tool;
50917
+ constructor(tool) {
50918
+ super("refused");
50919
+ this.tool = tool;
50920
+ }
50921
+ };
50922
+ });
50923
+
49943
50924
  // ../mcp/src/tools.ts
49944
50925
  function resolveToolAllowlist(env) {
49945
50926
  const raw = env.CANDLE_MCP_TOOLS?.trim();
@@ -50279,12 +51260,53 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
50279
51260
  const result = await executeLaunchAndSeed(args, cfg, fetch);
50280
51261
  return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
50281
51262
  });
51263
+ const perpsTool = (tool) => async (args) => {
51264
+ const result = await executePerps(tool, args, cfg, env, fetch);
51265
+ return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
51266
+ };
51267
+ const perpsWrite = " MOVES REAL FUNDS on Hyperliquid unless submit is false. Candle builds the action within this key's limits " + "(USD window, maximum leverage, maximum position, slippage and price bands, main-exchange markets only); this " + "server recomputes its hash and checks the action type and Candle's builder before signing, then submits it to " + "Hyperliquid itself. Needs perps:write on the key and CANDLE_KEY_SIGNER_PEM_FILE. A refusal names the limit " + "that refused it (error.limit).";
51268
+ register("candle_perps_setup", {
51269
+ title: "Set up Hyperliquid perps",
51270
+ description: "Approve Candle's Hyperliquid builder fee (0.1%) once for the key's EVM TEE wallet, and report the " + "account's mode and balance. Idempotent: nothing is signed when the approval is already on Hyperliquid." + perpsWrite,
51271
+ inputSchema: perpsShapes.candle_perps_setup
51272
+ }, perpsTool("candle_perps_setup"));
51273
+ register("candle_perps_open", {
51274
+ title: "Open a perps position",
51275
+ description: "Open or add to a perpetual position on Hyperliquid's main perp exchange: a market order (IOC within " + "slippageBps of the mid) without `price`, a limit order with it, and optional reduce-only takeProfit and " + "stopLoss triggers. Free, Believer and Pro pay a 0.1% builder fee; Max pays none." + perpsWrite,
51276
+ inputSchema: perpsShapes.candle_perps_open
51277
+ }, perpsTool("candle_perps_open"));
51278
+ register("candle_perps_close", {
51279
+ title: "Close a perps position",
51280
+ description: "Close all or part of a position with a reduce-only IOC order. It reserves nothing, so a key at its USD " + "cap can still close." + perpsWrite,
51281
+ inputSchema: perpsShapes.candle_perps_close
51282
+ }, perpsTool("candle_perps_close"));
51283
+ register("candle_perps_cancel", {
51284
+ title: "Cancel a perps order",
51285
+ description: "Cancel an order Candle built, by its cloid (from candle_perps_open or candle_perps_orders)." + perpsWrite,
51286
+ inputSchema: perpsShapes.candle_perps_cancel
51287
+ }, perpsTool("candle_perps_cancel"));
51288
+ register("candle_perps_orders", {
51289
+ title: "Perps orders",
51290
+ description: "Open orders on Hyperliquid for the key's EVM TEE wallet, and every action Candle built for it with what " + "the venue shows. Reads only; moves nothing.",
51291
+ inputSchema: perpsShapes.candle_perps_orders
51292
+ }, perpsTool("candle_perps_orders"));
51293
+ register("candle_perps_positions", {
51294
+ title: "Perps positions",
51295
+ description: "Positions, account value and withdrawable balance on Hyperliquid for the key's EVM TEE wallet, read " + "live by its address. Reads only; moves nothing.",
51296
+ inputSchema: perpsShapes.candle_perps_positions
51297
+ }, perpsTool("candle_perps_positions"));
51298
+ register("candle_perps_leverage", {
51299
+ title: "Set perps leverage",
51300
+ description: "Set a market's leverage and margin mode (cross or isolated) on the account, within the key's maximum leverage." + perpsWrite,
51301
+ inputSchema: perpsShapes.candle_perps_leverage
51302
+ }, perpsTool("candle_perps_leverage"));
50282
51303
  }
50283
51304
  var TOOL_NAMES, launchTokenShape, getMarketShape, tokenForensicsShape, getFeedShape, reportActivityShape, getAgentProfileShape, getOperationShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, _rawBuyAmount, seedableLaunchShape, launchAndSeedShape, transferShape, sweepShape;
50284
51305
  var init_tools = __esm(() => {
50285
51306
  init_zod();
50286
51307
  init_convert();
50287
51308
  init_orchestrate();
51309
+ init_perps();
50288
51310
  init_update_notice();
50289
51311
  TOOL_NAMES = [
50290
51312
  "candle_launch_token",
@@ -50306,7 +51328,14 @@ var init_tools = __esm(() => {
50306
51328
  "candle_get_portfolio",
50307
51329
  "candle_resolve_token",
50308
51330
  "candle_execution_status",
50309
- "candle_get_operation"
51331
+ "candle_get_operation",
51332
+ "candle_perps_setup",
51333
+ "candle_perps_open",
51334
+ "candle_perps_close",
51335
+ "candle_perps_cancel",
51336
+ "candle_perps_orders",
51337
+ "candle_perps_positions",
51338
+ "candle_perps_leverage"
50310
51339
  ];
50311
51340
  launchTokenShape = {
50312
51341
  clientLaunchId: exports_external.string().describe("Caller-chosen idempotency key, unique per account"),
@@ -50481,7 +51510,7 @@ var init_server2 = __esm(() => {
50481
51510
  // src/index.ts
50482
51511
  import { spawn as spawn3 } from "node:child_process";
50483
51512
  import { realpathSync } from "node:fs";
50484
- import { chmod as chmod8, readFile as readFile9, realpath, rename as rename6, unlink, writeFile as writeFile7 } from "node:fs/promises";
51513
+ import { chmod as chmod8, readFile as readFile10, realpath, rename as rename6, unlink, writeFile as writeFile7 } from "node:fs/promises";
50485
51514
  import { homedir as homedir6, hostname } from "node:os";
50486
51515
  import { pathToFileURL } from "node:url";
50487
51516
 
@@ -50529,7 +51558,7 @@ init_render();
50529
51558
  init_secret_store();
50530
51559
 
50531
51560
  // src/version.ts
50532
- var CLI_VERSION = "0.11.13";
51561
+ var CLI_VERSION = "0.11.14";
50533
51562
 
50534
51563
  // src/commands/auth.ts
50535
51564
  init_keys_embedded_wallet();
@@ -50945,6 +51974,11 @@ var ENVIRONMENT = [
50945
51974
  name: "CANDLE_ENCLAVE_HELPER",
50946
51975
  description: "Path to the signed candle-enclave.app helper (macOS)"
50947
51976
  },
51977
+ {
51978
+ name: "CANDLE_HYPERLIQUID_BUILDER",
51979
+ description: "candle perps: Candle's Hyperliquid builder address, pinned for the check every build passes before it is signed; without it the release's, else the one the server reports"
51980
+ },
51981
+ { name: "CANDLE_HYPERLIQUID_NETWORK", description: "candle perps: set to testnet to trade Hyperliquid testnet" },
50948
51982
  { name: "CANDLE_NO_UPDATE_NOTIFIER", description: "Set to 1 to silence the update notice" },
50949
51983
  {
50950
51984
  name: "CANDLE_KEYSTORE_PASSPHRASE",
@@ -51090,6 +52124,53 @@ var HELP = {
51090
52124
  ],
51091
52125
  env: ENV_API
51092
52126
  },
52127
+ perps: {
52128
+ group: "Trade",
52129
+ summary: "Hyperliquid perpetuals from your EVM TEE wallet: setup, open, close, cancel, orders, positions, leverage",
52130
+ description: "Trade perpetuals on Hyperliquid's main perp exchange from the EVM TEE wallet bound to this key (scope perps:write, opt-in). Candle builds each action within the key's limits; this machine recomputes its hash and checks the action type and Candle's builder before anything is signed, then Candle's relay signs and this machine submits to Hyperliquid. Free, Believer and Pro pay a 0.1% builder fee; Max pays none.",
52131
+ usage: ["candle perps <subcommand> [flags]"],
52132
+ rows: [
52133
+ {
52134
+ invocation: "setup [--wallet <tee>]",
52135
+ description: "Approve Candle's builder fee once; shows the account's mode and balance"
52136
+ },
52137
+ {
52138
+ invocation: "open <coin> <long|short> <size> [--price <px>]",
52139
+ description: "Market (IOC) without --price, limit with it; optional --tp and --sl"
52140
+ },
52141
+ { invocation: "close <coin> [--size <n>]", description: "Reduce-only IOC close of all or part of a position" },
52142
+ { invocation: "cancel <cloid>", description: "Cancel an order Candle built, by its cloid" },
52143
+ {
52144
+ invocation: "orders [--wallet <tee>]",
52145
+ description: "Open orders on Hyperliquid, and every action Candle built"
52146
+ },
52147
+ { invocation: "positions [--wallet <tee>]", description: "Positions and account value, read live" },
52148
+ { invocation: "leverage <coin> <x> [--isolated]", description: "Set a market's leverage and margin mode" }
52149
+ ],
52150
+ flags: [
52151
+ {
52152
+ invocation: "--wallet <tee>",
52153
+ description: "The EVM TEE wallet by id, address or unique label (default: the only one)"
52154
+ },
52155
+ { invocation: "--tif <Gtc|Alo|Ioc>", description: "open with --price: time in force (default Gtc)" },
52156
+ {
52157
+ invocation: "--slippage-bps <n>",
52158
+ description: "open at market and close: the bound against the mid (default 100)"
52159
+ },
52160
+ { invocation: "--tp <px> / --sl <px>", description: "open: reduce-only take-profit and stop-loss triggers" },
52161
+ { invocation: "--no-submit", description: "Build and check only; sign and submit nothing" },
52162
+ { invocation: "--sign-only", description: "Build, check and sign through the relay; submit nothing" },
52163
+ { invocation: "--yes", description: "Skip the confirmation prompt" }
52164
+ ],
52165
+ examples: [
52166
+ "candle perps setup",
52167
+ "candle perps open BTC long 0.01",
52168
+ "candle perps open ETH short 0.5 --price 3200 --tp 2900 --sl 3400",
52169
+ "candle perps close BTC",
52170
+ "candle perps leverage BTC 5 --isolated"
52171
+ ],
52172
+ env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK"]
52173
+ },
51093
52174
  transfer: {
51094
52175
  group: "Trade",
51095
52176
  summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
@@ -51151,24 +52232,33 @@ var HELP = {
51151
52232
  },
51152
52233
  portfolio: {
51153
52234
  group: "Account",
51154
- summary: "Every wallet's holdings, prices and value in one table (vault read over your own RPC)",
51155
- description: "Vault, TEE and embedded wallets, each token with amount, price and value, then a total. TEE and embedded balances come from Candle; vault and external wallets are read over your own RPC, and Candle is sent only the mints they hold, for prices. Unpriced tokens are shown as unpriced and left out of the total. Where Candle serves LP, each 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.",
51156
- usage: ["candle portfolio [--rpc-url <url>] [--json]"],
52235
+ summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
52236
+ description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade.",
52237
+ usage: ["candle portfolio [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
51157
52238
  rows: [],
51158
52239
  flags: [
52240
+ { invocation: "--chain solana|hood", description: "Show one chain, and read nothing on the other" },
51159
52241
  {
51160
52242
  invocation: "--rpc-url <url>",
51161
52243
  description: "Your Solana RPC, for the vault. Without one: CANDLE_SOLANA_RPC_URL, then the profile's, then the public endpoint"
51162
52244
  },
52245
+ {
52246
+ invocation: "--evm-rpc-url <url>",
52247
+ description: "Your Hood RPC, for EVM vault keys. Without one: CANDLE_EVM_RPC_URL, then the built-in Hood RPC"
52248
+ },
51163
52249
  KEYSTORE_FLAG
51164
52250
  ],
51165
- examples: ["candle portfolio", "candle portfolio --rpc-url https://your-rpc.example --json"],
52251
+ examples: [
52252
+ "candle portfolio",
52253
+ "candle portfolio --chain hood",
52254
+ "candle portfolio --rpc-url https://your-rpc.example --evm-rpc-url https://your-hood-rpc.example --json"
52255
+ ],
51166
52256
  env: [...ENV_API, ...ENV_LOCAL_SIGNING]
51167
52257
  },
51168
52258
  pnl: {
51169
52259
  group: "Account",
51170
- summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's own)",
51171
- description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the web P&L chart shows. Needs a key with the Read scope (account:read). With --profile <name>, that profile's key reads its own P&L. Unpriced positions are shown as unpriced and never valued at zero. Where Candle serves LP, DAMM v2 positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis), and vs holding (what the deposited tokens would be worth held); the total then covers tokens and LP.",
52260
+ summary: "P&L per chain and combined: realized, fees, unrealized and open positions (--profile for one key's own)",
52261
+ description: "Without --profile, the account's books: every profile, the web app and the CLI, one ledger, the same figures the web P&L chart shows. Needs a key with the Read scope (account:read). With --profile <name>, that profile's key reads its own P&L, with a row per paying wallet. Both show a Solana block, a Hood block, then the combined figure, and a CHAIN column on positions and wallet rows. Unpriced positions are shown as unpriced and never valued at zero. A bridge between Solana and Hood is not a trade: it opens and closes nothing, and bridge and Relay costs are not netted from P&L. Where Candle serves LP, DAMM v2 positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis), and vs holding (what the deposited tokens would be worth held); the total then covers tokens and LP.",
51172
52262
  usage: ["candle pnl [--profile <name>] [--json]"],
51173
52263
  rows: [],
51174
52264
  examples: ["candle pnl", "candle pnl --profile scalper --json"],
@@ -58970,7 +60060,14 @@ var MCP_TOOL_NAMES = [
58970
60060
  "candle_get_portfolio",
58971
60061
  "candle_resolve_token",
58972
60062
  "candle_execution_status",
58973
- "candle_get_operation"
60063
+ "candle_get_operation",
60064
+ "candle_perps_setup",
60065
+ "candle_perps_open",
60066
+ "candle_perps_close",
60067
+ "candle_perps_cancel",
60068
+ "candle_perps_orders",
60069
+ "candle_perps_positions",
60070
+ "candle_perps_leverage"
58974
60071
  ];
58975
60072
  var READ_ONLY_TOOL_NAMES = [
58976
60073
  "candle_get_market",
@@ -59078,6 +60175,827 @@ async function mcp(args, ctx) {
59078
60175
  }
59079
60176
  }
59080
60177
 
60178
+ // src/commands/perps.ts
60179
+ init_args();
60180
+ import { sign as sign3 } from "node:crypto";
60181
+
60182
+ // src/hyperliquid.ts
60183
+ init_sha3();
60184
+ var HYPERLIQUID_ALLOWED_ACTION_TYPES = [
60185
+ "order",
60186
+ "cancel",
60187
+ "modify",
60188
+ "updateLeverage",
60189
+ "updateIsolatedMargin"
60190
+ ];
60191
+ var HYPERLIQUID_EXCHANGE_URLS = {
60192
+ mainnet: "https://api.hyperliquid.xyz/exchange",
60193
+ testnet: "https://api.hyperliquid-testnet.xyz/exchange"
60194
+ };
60195
+ var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
60196
+ var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
60197
+ var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
60198
+ var HYPERLIQUID_SIGNATURE_CHAIN_ID = "0xa4b1";
60199
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
60200
+ var UINT64_MAX = (1n << 64n) - 1n;
60201
+ var INT64_MIN = -(1n << 63n);
60202
+ function pushUint(out, value, width) {
60203
+ for (let i = width - 1;i >= 0; i--)
60204
+ out.push(Number(value >> BigInt(i * 8) & 0xffn));
60205
+ }
60206
+ function packInteger(out, value) {
60207
+ if (value >= 0n) {
60208
+ if (value > UINT64_MAX)
60209
+ throw new Error("msgpack: integer above uint64");
60210
+ if (value < 0x80n)
60211
+ out.push(Number(value));
60212
+ else if (value <= 0xffn) {
60213
+ out.push(204);
60214
+ pushUint(out, value, 1);
60215
+ } else if (value <= 0xffffn) {
60216
+ out.push(205);
60217
+ pushUint(out, value, 2);
60218
+ } else if (value <= 0xffffffffn) {
60219
+ out.push(206);
60220
+ pushUint(out, value, 4);
60221
+ } else {
60222
+ out.push(207);
60223
+ pushUint(out, value, 8);
60224
+ }
60225
+ return;
60226
+ }
60227
+ if (value < INT64_MIN)
60228
+ throw new Error("msgpack: integer below int64");
60229
+ if (value >= -32n)
60230
+ out.push(Number(value & 0xffn));
60231
+ else if (value >= -128n) {
60232
+ out.push(208);
60233
+ pushUint(out, value & 0xffn, 1);
60234
+ } else if (value >= -32768n) {
60235
+ out.push(209);
60236
+ pushUint(out, value & 0xffffn, 2);
60237
+ } else if (value >= -2147483648n) {
60238
+ out.push(210);
60239
+ pushUint(out, value & 0xffffffffn, 4);
60240
+ } else {
60241
+ out.push(211);
60242
+ pushUint(out, value & UINT64_MAX, 8);
60243
+ }
60244
+ }
60245
+ function packHeader(out, length, fixBase, fixMax, codes) {
60246
+ if (length <= fixMax) {
60247
+ out.push(fixBase | length);
60248
+ return;
60249
+ }
60250
+ const [c8, c16, c32] = codes;
60251
+ if (c8 !== undefined && c8 !== 0 && length <= 255) {
60252
+ out.push(c8);
60253
+ pushUint(out, BigInt(length), 1);
60254
+ } else if (length <= 65535) {
60255
+ out.push(c16);
60256
+ pushUint(out, BigInt(length), 2);
60257
+ } else {
60258
+ out.push(c32);
60259
+ pushUint(out, BigInt(length), 4);
60260
+ }
60261
+ }
60262
+ function packValue(out, value) {
60263
+ if (value === null)
60264
+ out.push(192);
60265
+ else if (value === false)
60266
+ out.push(194);
60267
+ else if (value === true)
60268
+ out.push(195);
60269
+ else if (typeof value === "bigint")
60270
+ packInteger(out, value);
60271
+ else if (typeof value === "number") {
60272
+ if (!Number.isSafeInteger(value))
60273
+ throw new Error(`msgpack: ${value} is not a safe integer`);
60274
+ packInteger(out, BigInt(value));
60275
+ } else if (typeof value === "string") {
60276
+ const bytes = new TextEncoder().encode(value);
60277
+ packHeader(out, bytes.length, 160, 31, [217, 218, 219]);
60278
+ for (const b of bytes)
60279
+ out.push(b);
60280
+ } else if (Array.isArray(value)) {
60281
+ packHeader(out, value.length, 144, 15, [0, 220, 221]);
60282
+ for (const item of value)
60283
+ packValue(out, item);
60284
+ } else if (typeof value === "object") {
60285
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
60286
+ for (const [key] of entries) {
60287
+ if (/^(0|[1-9][0-9]*)$/.test(key))
60288
+ throw new Error(`msgpack: integer-like key "${key}"`);
60289
+ }
60290
+ packHeader(out, entries.length, 128, 15, [0, 222, 223]);
60291
+ for (const [key, item] of entries) {
60292
+ packValue(out, key);
60293
+ packValue(out, item);
60294
+ }
60295
+ } else {
60296
+ throw new Error(`msgpack: cannot encode a ${typeof value}`);
60297
+ }
60298
+ }
60299
+ function msgpackEncode(value) {
60300
+ const out = [];
60301
+ packValue(out, value);
60302
+ return Uint8Array.from(out);
60303
+ }
60304
+ function toHex(bytes) {
60305
+ let out = "";
60306
+ for (const b of bytes)
60307
+ out += b.toString(16).padStart(2, "0");
60308
+ return out;
60309
+ }
60310
+ function hyperliquidActionHash(action, nonce) {
60311
+ const n = BigInt(nonce);
60312
+ if (n < 0n || n > UINT64_MAX)
60313
+ throw new Error("hyperliquidActionHash: nonce out of range");
60314
+ const out = Array.from(msgpackEncode(action));
60315
+ pushUint(out, n, 8);
60316
+ out.push(0);
60317
+ return `0x${toHex(keccak_256(Uint8Array.from(out)))}`;
60318
+ }
60319
+ function hyperliquidL1TypedData(connectionId, network) {
60320
+ return {
60321
+ domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: ZERO_ADDRESS },
60322
+ types: {
60323
+ Agent: [
60324
+ { name: "source", type: "string" },
60325
+ { name: "connectionId", type: "bytes32" }
60326
+ ]
60327
+ },
60328
+ primary_type: "Agent",
60329
+ message: { source: network === "mainnet" ? "a" : "b", connectionId }
60330
+ };
60331
+ }
60332
+ function hyperliquidApproveBuilderFeeTypedData(builder, nonce) {
60333
+ return {
60334
+ domain: { name: "HyperliquidSignTransaction", version: "1", chainId: 42161, verifyingContract: ZERO_ADDRESS },
60335
+ types: {
60336
+ "HyperliquidTransaction:ApproveBuilderFee": [
60337
+ { name: "hyperliquidChain", type: "string" },
60338
+ { name: "maxFeeRate", type: "string" },
60339
+ { name: "builder", type: "address" },
60340
+ { name: "nonce", type: "uint64" }
60341
+ ]
60342
+ },
60343
+ primary_type: "HyperliquidTransaction:ApproveBuilderFee",
60344
+ message: {
60345
+ hyperliquidChain: "Mainnet",
60346
+ maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
60347
+ builder: builder.toLowerCase(),
60348
+ nonce
60349
+ }
60350
+ };
60351
+ }
60352
+ function hyperliquidCanonicalJson(value) {
60353
+ return canonical(value);
60354
+ }
60355
+ function canonical(value) {
60356
+ if (value === null || typeof value !== "object")
60357
+ return JSON.stringify(value);
60358
+ if (Array.isArray(value))
60359
+ return `[${value.map(canonical).join(",")}]`;
60360
+ const record = value;
60361
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
60362
+ }
60363
+ function sameValue(a, b) {
60364
+ return canonical(a) === canonical(b);
60365
+ }
60366
+ var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
60367
+ function checkBuilderField(action, builder) {
60368
+ if (!("builder" in action) || action.builder === undefined)
60369
+ return null;
60370
+ const field = action.builder;
60371
+ if (typeof field !== "object" || field === null)
60372
+ return "builder field is not an object";
60373
+ if (typeof field.b !== "string" || field.b.toLowerCase() !== builder.toLowerCase()) {
60374
+ return "the order names a builder that is not Candle's";
60375
+ }
60376
+ if (typeof field.f !== "number" || !Number.isInteger(field.f) || field.f <= 0 || field.f > HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS) {
60377
+ return "the builder fee is outside Candle's 0.1% maximum";
60378
+ }
60379
+ return null;
60380
+ }
60381
+ function decimal(value) {
60382
+ if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
60383
+ return null;
60384
+ const negative = value.startsWith("-");
60385
+ const [whole = "0", frac = ""] = (negative ? value.slice(1) : value).split(".");
60386
+ const n = BigInt(whole) * 10n ** 18n + BigInt(frac.padEnd(18, "0"));
60387
+ return negative ? -n : n;
60388
+ }
60389
+ function checkActionIntent(action, intent) {
60390
+ const mainAsset = (a) => typeof a === "number" && Number.isInteger(a) && a >= 0 && a < 1e4;
60391
+ const orderList = action.type === "order" ? action.orders : action.type === "modify" ? [action.order] : [];
60392
+ if (!Array.isArray(orderList))
60393
+ return "invalid orders";
60394
+ for (const raw of orderList) {
60395
+ if (!raw || typeof raw !== "object")
60396
+ return "invalid order";
60397
+ const o = raw;
60398
+ if (!mainAsset(o.a))
60399
+ return "order asset is outside the main perp universe";
60400
+ if (typeof o.b !== "boolean" || typeof o.r !== "boolean" || (decimal(o.s) ?? 0n) <= 0n || (decimal(o.p) ?? 0n) <= 0n)
60401
+ return "invalid order side, size, price or reduce-only flag";
60402
+ }
60403
+ if (action.type === "cancel") {
60404
+ if (!Array.isArray(action.cancels) || action.cancels.length !== 1 || !mainAsset(action.cancels[0]?.a))
60405
+ return "invalid cancel asset";
60406
+ }
60407
+ if ((action.type === "updateLeverage" || action.type === "updateIsolatedMargin") && !mainAsset(action.asset))
60408
+ return "invalid action asset";
60409
+ if (!intent)
60410
+ return null;
60411
+ const { method, params: p } = intent;
60412
+ const types2 = {
60413
+ setup: "approveBuilderFee",
60414
+ open: "order",
60415
+ close: "order",
60416
+ cancel: "cancel",
60417
+ modify: "modify",
60418
+ leverage: "updateLeverage",
60419
+ margin: "updateIsolatedMargin"
60420
+ };
60421
+ if (action.type !== types2[method])
60422
+ return "action type does not match the requested method";
60423
+ if (method === "leverage") {
60424
+ if (action.leverage !== p.leverage || action.isCross !== (p.mode !== "isolated"))
60425
+ return "leverage or mode does not match the request";
60426
+ }
60427
+ if (method === "margin") {
60428
+ const amount = decimal(p.amount);
60429
+ if (amount === null || amount % 10n ** 12n !== 0n || !Number.isSafeInteger(action.ntli) || BigInt(action.ntli) !== amount / 10n ** 12n || action.isBuy !== true)
60430
+ return "isolated margin amount does not match the request";
60431
+ }
60432
+ if (method === "open" || method === "close" || method === "modify") {
60433
+ const list = orderList;
60434
+ const first = list[0];
60435
+ if (!first)
60436
+ return "missing requested order";
60437
+ const typ = first.t;
60438
+ if (!typ?.limit)
60439
+ return "requested order is not a limit/IOC order";
60440
+ if (method === "open") {
60441
+ const buy = p.side === "long" || p.side === "buy";
60442
+ if (first.b !== buy || first.r !== false || decimal(first.s) !== decimal(p.size))
60443
+ return "order side, size or reduce-only flag does not match the request";
60444
+ const tif = (p.type ?? (p.price === undefined ? "market" : "limit")) === "market" ? "Ioc" : p.tif ?? "Gtc";
60445
+ if (typ.limit.tif !== tif)
60446
+ return "order time in force does not match the request";
60447
+ const triggers = [
60448
+ ["takeProfit", "tp"],
60449
+ ["stopLoss", "sl"]
60450
+ ].filter(([field]) => p[field] != null);
60451
+ if (list.length !== 1 + triggers.length || action.grouping !== (triggers.length ? "normalTpsl" : "na"))
60452
+ return "unexpected extra orders or grouping";
60453
+ for (let i = 0;i < triggers.length; i++) {
60454
+ const [field, kind] = triggers[i];
60455
+ const leg = list[i + 1];
60456
+ if (!leg)
60457
+ return "missing trigger order";
60458
+ const trigger = leg.t?.trigger;
60459
+ if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal(leg.s) !== decimal(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal(trigger.triggerPx) !== decimal(p[field]))
60460
+ return "trigger side, size, reduce-only flag or price does not match the request";
60461
+ }
60462
+ } else {
60463
+ if (list.length !== 1)
60464
+ return "unexpected extra orders";
60465
+ if (method === "close" && (first.r !== true || typ.limit.tif !== "Ioc" || p.size != null && (decimal(first.s) ?? 0n) > (decimal(p.size) ?? 0n)))
60466
+ return "close size or reduce-only flag does not match the request";
60467
+ if (method === "modify" && p.size != null && decimal(first.s) !== decimal(p.size))
60468
+ return "replacement size does not match the request";
60469
+ }
60470
+ if (p.price != null && decimal(first.p) !== decimal(p.price))
60471
+ return "order price does not match the request";
60472
+ }
60473
+ return null;
60474
+ }
60475
+ async function hyperliquidCloseOrder(fetcher, network, address, params) {
60476
+ const read = async (body) => {
60477
+ const res = await fetcher(HYPERLIQUID_EXCHANGE_URLS[network].replace("/exchange", "/info"), {
60478
+ method: "POST",
60479
+ headers: { "content-type": "application/json" },
60480
+ body: JSON.stringify(body)
60481
+ });
60482
+ if (!res.ok)
60483
+ throw new Error("Cannot read the close position from Hyperliquid");
60484
+ return JSON.parse(await res.text());
60485
+ };
60486
+ const [meta, state] = await Promise.all([read({ type: "meta" }), read({ type: "clearinghouseState", user: address })]);
60487
+ const coin = String(params.coin).toUpperCase();
60488
+ const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
60489
+ const szi = state.assetPositions.find((p) => p.position.coin.toUpperCase() === coin)?.position.szi;
60490
+ const signed = decimal(szi);
60491
+ if (asset < 0 || asset >= 1e4 || signed === null || signed === 0n)
60492
+ throw new Error("No main-universe position to close");
60493
+ const held = signed < 0n ? -signed : signed;
60494
+ const asked = params.size == null ? held : decimal(params.size);
60495
+ if (asked === null || asked <= 0n)
60496
+ throw new Error("Invalid close size");
60497
+ const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
60498
+ return { asset, isBuy: signed < 0n, size };
60499
+ }
60500
+ function verifyPerpsBuild(build, opts) {
60501
+ try {
60502
+ const network = opts.network ?? "mainnet";
60503
+ if (!EVM_ADDRESS_RE.test(opts.builder))
60504
+ return { ok: false, reason: "no valid Candle builder address to check against" };
60505
+ if (build.network !== undefined && build.network !== network) {
60506
+ return { ok: false, reason: `the build is for ${build.network}, this client trades ${network}` };
60507
+ }
60508
+ if (opts.address && build.address && build.address.toLowerCase() !== opts.address.toLowerCase()) {
60509
+ return { ok: false, reason: "the build is for a different wallet" };
60510
+ }
60511
+ if (!Number.isSafeInteger(build.nonce) || build.nonce <= 0)
60512
+ return { ok: false, reason: "invalid nonce" };
60513
+ const typed = build.typedData;
60514
+ if (!typed || typeof typed !== "object")
60515
+ return { ok: false, reason: "no typed data" };
60516
+ const action = build.action;
60517
+ if (!action || typeof action !== "object")
60518
+ return { ok: false, reason: "no action" };
60519
+ const intentRefusal = checkActionIntent(action, opts.intent);
60520
+ if (intentRefusal)
60521
+ return { ok: false, reason: intentRefusal };
60522
+ if (opts.closeOrder) {
60523
+ const order = action.orders[0];
60524
+ if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
60525
+ return { ok: false, reason: "close side, asset or size does not match the requested position" };
60526
+ }
60527
+ if (typed.primary_type === "Agent") {
60528
+ const type = action.type;
60529
+ if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
60530
+ return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
60531
+ }
60532
+ if (type === "order") {
60533
+ const refusal = checkBuilderField(action, opts.builder);
60534
+ if (refusal)
60535
+ return { ok: false, reason: refusal };
60536
+ } else if ("builder" in action) {
60537
+ return { ok: false, reason: `a ${type} action carries no builder` };
60538
+ }
60539
+ if ("vaultAddress" in action)
60540
+ return { ok: false, reason: "the action names a vault" };
60541
+ const expected = hyperliquidL1TypedData(hyperliquidActionHash(action, build.nonce), network);
60542
+ if (!sameValue(typed, expected)) {
60543
+ return { ok: false, reason: "the typed data does not commit to this action and nonce" };
60544
+ }
60545
+ return { ok: true, kind: "l1" };
60546
+ }
60547
+ if (typed.primary_type === "HyperliquidTransaction:ApproveBuilderFee") {
60548
+ const expected = hyperliquidApproveBuilderFeeTypedData(opts.builder, build.nonce);
60549
+ if (!sameValue(typed, expected)) {
60550
+ return { ok: false, reason: "the builder approval is not Candle's builder at 0.1% on Mainnet" };
60551
+ }
60552
+ const expectedAction = {
60553
+ type: "approveBuilderFee",
60554
+ hyperliquidChain: "Mainnet",
60555
+ signatureChainId: HYPERLIQUID_SIGNATURE_CHAIN_ID,
60556
+ maxFeeRate: HYPERLIQUID_MAX_BUILDER_FEE_RATE,
60557
+ builder: opts.builder.toLowerCase(),
60558
+ nonce: build.nonce
60559
+ };
60560
+ if (!sameValue(action, expectedAction)) {
60561
+ return { ok: false, reason: "the approval action does not match its typed data" };
60562
+ }
60563
+ return { ok: true, kind: "approveBuilderFee" };
60564
+ }
60565
+ return { ok: false, reason: `primary type ${String(typed.primary_type)} is not one the relay signs` };
60566
+ } catch (err) {
60567
+ return { ok: false, reason: err instanceof Error ? err.message : "unreadable build" };
60568
+ }
60569
+ }
60570
+ function hyperliquidSplitSignature(signature) {
60571
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature))
60572
+ throw new Error("Not a 65-byte signature");
60573
+ let v = Number.parseInt(signature.slice(130, 132), 16);
60574
+ if (v < 27)
60575
+ v += 27;
60576
+ return { r: `0x${signature.slice(2, 66)}`, s: `0x${signature.slice(66, 130)}`, v };
60577
+ }
60578
+ function hyperliquidExchangeBody(action, nonce, signature) {
60579
+ return { action, nonce, signature: hyperliquidSplitSignature(signature), vaultAddress: null, expiresAfter: null };
60580
+ }
60581
+ function hyperliquidRelayBody(typedData) {
60582
+ return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
60583
+ }
60584
+
60585
+ // src/commands/perps.ts
60586
+ init_render();
60587
+ init_trading();
60588
+ var PERPS_SCOPE = "perps:write";
60589
+ var DEC_RE = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
60590
+ var CLOID_RE = /^0x[0-9a-fA-F]{32}$/;
60591
+ function usage4(ctx, line) {
60592
+ writeUsageFailure(ctx.deps, line, ctx.json);
60593
+ return 2;
60594
+ }
60595
+ async function perpsRequest(ctx, key, path, body) {
60596
+ const result = await apiRequest(path, {
60597
+ apiUrl: ctx.apiUrl,
60598
+ credentials: { apiKey: key },
60599
+ auth: "key",
60600
+ method: body ? "POST" : "GET",
60601
+ body,
60602
+ fetch: ctx.deps.fetch,
60603
+ env: ctx.deps.env
60604
+ });
60605
+ if (!result.ok) {
60606
+ if (result.status === 404 && !result.code)
60607
+ throw new TradingError("PERPS_NOT_ENABLED", "This Candle deployment does not serve perps routes (HYPERLIQUID_ENABLED is off there, or the API predates them).");
60608
+ throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
60609
+ }
60610
+ if (!result.body || typeof result.body !== "object")
60611
+ throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
60612
+ return result.body;
60613
+ }
60614
+ function network(ctx) {
60615
+ return ctx.deps.env.CANDLE_HYPERLIQUID_NETWORK === "testnet" ? "testnet" : "mainnet";
60616
+ }
60617
+ async function builderPin(ctx, key) {
60618
+ const pinned = ctx.deps.env.CANDLE_HYPERLIQUID_BUILDER ?? CANDLE_HYPERLIQUID_BUILDER_ADDRESS;
60619
+ if (pinned) {
60620
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pinned))
60621
+ throw new TradingError("INVALID_ADDRESS", "CANDLE_HYPERLIQUID_BUILDER must be an EVM address.");
60622
+ return pinned.toLowerCase();
60623
+ }
60624
+ const config = await perpsRequest(ctx, key, "/api/v1/agent/perps/config");
60625
+ if (typeof config.builder !== "string")
60626
+ throw new TradingError("PERPS_NOT_CONFIGURED", "The server reports no Hyperliquid builder address.");
60627
+ return config.builder.toLowerCase();
60628
+ }
60629
+ async function perpsWallet(ctx, key, name2, scope) {
60630
+ const { rows, appId, keyPrefix } = await listTradingWallets(ctx, key, scope);
60631
+ const evm = rows.filter((row2) => row2.chain === "evm");
60632
+ const matches = name2 === undefined ? evm : evm.filter((row2) => matchesName(row2, name2));
60633
+ if (matches.length !== 1) {
60634
+ throw new TradingError("TEE_WALLET_REQUIRED", evm.length === 0 ? "This key has no EVM TEE wallet bound to it. Hyperliquid perps sign only from one." : matches.length === 0 ? `No EVM TEE wallet on this key is called "${name2}". Bound to this key: ${evm.map(describeWallet).join("; ")}.` : `Name one EVM TEE wallet with --wallet: ${matches.map(describeWallet).join("; ")}.`);
60635
+ }
60636
+ const row = matches[0];
60637
+ if (scope === undefined)
60638
+ return { row };
60639
+ const wallet = await completeTradingWallet(ctx, row, appId, scope, "hood", {
60640
+ apiKey: key,
60641
+ ...keyPrefix !== undefined ? { keyPrefix } : {}
60642
+ });
60643
+ return { row, wallet };
60644
+ }
60645
+ function typedDataAuthorization(wallet, typedData) {
60646
+ const body = hyperliquidRelayBody(typedData);
60647
+ const payload = hyperliquidCanonicalJson({
60648
+ body,
60649
+ headers: { "privy-app-id": wallet.appId },
60650
+ method: "POST",
60651
+ url: `https://api.privy.io/v1/wallets/${wallet.privyWalletId}/rpc`,
60652
+ version: 1
60653
+ });
60654
+ return { body, authorizationSignature: sign3("sha256", Buffer.from(payload), wallet.signer).toString("base64") };
60655
+ }
60656
+ function previewLines(build) {
60657
+ const action = build.action;
60658
+ if (action.type === "approveBuilderFee")
60659
+ return [`Approve Candle's builder ${safeText(action.builder)} at ${safeText(action.maxFeeRate)} (once).`];
60660
+ if (action.type === "cancel")
60661
+ return action.cancels.map((o) => `Cancel asset ${o.a} order ${o.o}.`);
60662
+ if (action.type === "updateLeverage")
60663
+ return [
60664
+ `Set asset ${safeText(action.asset)} to ${safeText(action.leverage)}x ${action.isCross ? "cross" : "isolated"}.`
60665
+ ];
60666
+ const orders = action.type === "modify" ? [action.order] : action.orders;
60667
+ const lines = orders.map((o) => {
60668
+ const type = o.t;
60669
+ return `${action.type === "modify" ? "Modify to" : o.r ? "Close" : "Open"} ${o.b ? "buy" : "sell"} ${safeText(o.s)} asset ${safeText(o.a)} at ${safeText(o.p)}${type.limit ? ` (${safeText(type.limit.tif)})` : ` (${safeText(type.trigger?.tpsl)} trigger ${safeText(type.trigger?.triggerPx)})`}${o.r ? ", reduce-only" : ""}.`;
60670
+ });
60671
+ const builder = action.builder;
60672
+ lines.push(builder ? `Candle builder ${safeText(builder.b)} fee: ${builder.f / 10} bps.` : "Candle builder fee: none.");
60673
+ return lines;
60674
+ }
60675
+ async function confirm(ctx, build, address, yes) {
60676
+ const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
60677
+ output.write(`Hyperliquid account: ${safeText(address)} (${safeText(build.network)})
60678
+ `);
60679
+ for (const line of previewLines(build))
60680
+ output.write(`${line}
60681
+ `);
60682
+ if (yes)
60683
+ return true;
60684
+ if (!ctx.deps.isTTY.stdin)
60685
+ throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes.");
60686
+ return (await ctx.deps.promptLine("Sign and submit? [y/N] ")).trim().toLowerCase() === "y";
60687
+ }
60688
+ function venueRefusal(exchange) {
60689
+ if (!exchange || typeof exchange !== "object")
60690
+ return "Hyperliquid returned no answer.";
60691
+ const answer = exchange;
60692
+ if (answer.status !== "ok")
60693
+ return `Hyperliquid refused it: ${safeText(JSON.stringify(answer.response ?? answer))}`;
60694
+ const statuses = answer.response?.data?.statuses;
60695
+ for (const status of statuses ?? []) {
60696
+ if (status && typeof status === "object" && "error" in status)
60697
+ return `Hyperliquid refused the order: ${safeText(status.error)}`;
60698
+ }
60699
+ return null;
60700
+ }
60701
+ async function runPerpsWrite(ctx, opts) {
60702
+ const key = await tradingKey(ctx);
60703
+ const { wallet } = await perpsWallet(ctx, key, opts.wallet, PERPS_SCOPE);
60704
+ if (!wallet)
60705
+ throw new TradingError("TEE_WALLET_REQUIRED", "No EVM TEE wallet resolved.");
60706
+ const builder = await builderPin(ctx, key);
60707
+ const build = await perpsRequest(ctx, key, opts.path, { walletId: wallet.id, ...opts.body });
60708
+ if (build.ready === true)
60709
+ return printTradingResult(ctx, build);
60710
+ const net = network(ctx);
60711
+ const method = opts.path.split("/").at(-1);
60712
+ const check = verifyPerpsBuild(build, {
60713
+ builder,
60714
+ network: net,
60715
+ address: wallet.address,
60716
+ intent: { method, params: opts.body }
60717
+ });
60718
+ if (!check.ok)
60719
+ throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this build: ${check.reason}. Nothing was signed.`);
60720
+ if (method === "close") {
60721
+ const closeOrder = await hyperliquidCloseOrder(ctx.deps.fetch, net, wallet.address, opts.body);
60722
+ const closeCheck = verifyPerpsBuild(build, {
60723
+ builder,
60724
+ network: net,
60725
+ address: wallet.address,
60726
+ intent: { method, params: opts.body },
60727
+ closeOrder
60728
+ });
60729
+ if (!closeCheck.ok)
60730
+ throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
60731
+ }
60732
+ if (!opts.submit)
60733
+ return printTradingResult(ctx, { ...build, signed: false, submitted: false });
60734
+ if (!await confirm(ctx, build, wallet.address, opts.yes)) {
60735
+ throw new TradingError("CANCELLED", "Nothing was signed.");
60736
+ }
60737
+ const relay = await perpsRequest(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(wallet.id)}/sign`, typedDataAuthorization(wallet, build.typedData));
60738
+ const signature = relay.signature;
60739
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature))
60740
+ throw new TradingError("INVALID_RESPONSE", "The relay did not return a signature.");
60741
+ const exchangeBody = hyperliquidExchangeBody(build.action, build.nonce, signature);
60742
+ if (opts.signOnly)
60743
+ return printTradingResult(ctx, {
60744
+ success: true,
60745
+ perpOrderId: build.perpOrderId,
60746
+ kind: build.kind,
60747
+ network: build.network,
60748
+ address: wallet.address,
60749
+ nonce: build.nonce,
60750
+ signature,
60751
+ submitted: false,
60752
+ exchangeBody
60753
+ });
60754
+ let exchange;
60755
+ try {
60756
+ const res = await ctx.deps.fetch(HYPERLIQUID_EXCHANGE_URLS[net], {
60757
+ method: "POST",
60758
+ headers: { "content-type": "application/json" },
60759
+ body: JSON.stringify(exchangeBody)
60760
+ });
60761
+ const text = await res.text();
60762
+ if (!res.ok)
60763
+ throw new Error(`HTTP ${res.status}`);
60764
+ exchange = JSON.parse(text);
60765
+ } catch (error) {
60766
+ throw new TradingError("PERPS_SUBMIT_FAILED", `Signed, but the submit to Hyperliquid failed (${error instanceof Error ? error.message : "unknown"}). The signed action is in details.exchangeBody; nothing was lost.`, { exitCode: 3, details: { perpOrderId: build.perpOrderId, exchangeBody } });
60767
+ }
60768
+ const refusal = venueRefusal(exchange);
60769
+ if (refusal)
60770
+ throw new TradingError("PERPS_VENUE_REFUSED", refusal, {
60771
+ details: { perpOrderId: build.perpOrderId, exchange }
60772
+ });
60773
+ return printTradingResult(ctx, {
60774
+ success: true,
60775
+ perpOrderId: build.perpOrderId,
60776
+ kind: build.kind,
60777
+ network: build.network,
60778
+ address: wallet.address,
60779
+ ...build.cloid ? { cloid: build.cloid } : {},
60780
+ ...build.childCloids ? { childCloids: build.childCloids } : {},
60781
+ nonce: build.nonce,
60782
+ signature,
60783
+ exchange
60784
+ });
60785
+ }
60786
+ var WRITE_FLAGS = { valueFlags: ["--wallet"], booleanFlags: ["--yes", "--no-submit", "--sign-only"] };
60787
+ function sharedWrite(values, booleans) {
60788
+ return {
60789
+ ...values["--wallet"] !== undefined ? { wallet: values["--wallet"] } : {},
60790
+ yes: booleans.has("--yes"),
60791
+ submit: !booleans.has("--no-submit"),
60792
+ signOnly: booleans.has("--sign-only")
60793
+ };
60794
+ }
60795
+ function slippage(value) {
60796
+ if (value === undefined)
60797
+ return;
60798
+ const n = Number(value);
60799
+ return Number.isInteger(n) && n >= 1 ? n : null;
60800
+ }
60801
+ async function perpsSetup(args, ctx) {
60802
+ const parsed = parseArgs(args, WRITE_FLAGS);
60803
+ if ("error" in parsed)
60804
+ return usage4(ctx, parsed.error);
60805
+ if (parsed.positionals.length !== 0)
60806
+ return usage4(ctx, "Usage: candle perps setup [--wallet <tee>] [--yes]");
60807
+ try {
60808
+ return await runPerpsWrite(ctx, {
60809
+ path: "/api/v1/agent/perps/setup",
60810
+ body: {},
60811
+ ...sharedWrite(parsed.values, parsed.booleans)
60812
+ });
60813
+ } catch (error) {
60814
+ return tradingFailure(ctx, error);
60815
+ }
60816
+ }
60817
+ var OPEN_USAGE = "Usage: candle perps open <coin> <long|short> <size> [--price <px>] [--tif Gtc|Alo|Ioc] [--slippage-bps <n>] [--tp <px>] [--sl <px>] [--wallet <tee>] [--yes]";
60818
+ async function perpsOpen(args, ctx) {
60819
+ const parsed = parseArgs(args, {
60820
+ valueFlags: ["--wallet", "--price", "--tif", "--slippage-bps", "--tp", "--sl"],
60821
+ booleanFlags: ["--yes", "--no-submit", "--sign-only"]
60822
+ });
60823
+ if ("error" in parsed)
60824
+ return usage4(ctx, parsed.error);
60825
+ const [coin, side, size] = parsed.positionals;
60826
+ if (parsed.positionals.length !== 3 || !coin || !side || !size)
60827
+ return usage4(ctx, OPEN_USAGE);
60828
+ if (side !== "long" && side !== "short")
60829
+ return usage4(ctx, OPEN_USAGE);
60830
+ if (!DEC_RE.test(size))
60831
+ return usage4(ctx, "The size must be a plain decimal, e.g. 0.01.");
60832
+ const { values } = parsed;
60833
+ for (const flag of ["--price", "--tp", "--sl"]) {
60834
+ const value = values[flag];
60835
+ if (value !== undefined && !DEC_RE.test(value))
60836
+ return usage4(ctx, `${flag} must be a plain decimal.`);
60837
+ }
60838
+ const tif = values["--tif"];
60839
+ if (tif !== undefined && !["Gtc", "Alo", "Ioc"].includes(tif))
60840
+ return usage4(ctx, "--tif must be Gtc, Alo or Ioc.");
60841
+ if (tif !== undefined && tif !== "Ioc" && values["--price"] === undefined)
60842
+ return usage4(ctx, "--tif Gtc or Alo needs --price.");
60843
+ const slippageBps = slippage(values["--slippage-bps"]);
60844
+ if (slippageBps === null)
60845
+ return usage4(ctx, "--slippage-bps must be a whole number of at least 1.");
60846
+ const body = {
60847
+ coin,
60848
+ side,
60849
+ size,
60850
+ type: values["--price"] === undefined ? "market" : "limit",
60851
+ ...values["--price"] !== undefined ? { price: values["--price"] } : {},
60852
+ ...tif !== undefined && values["--price"] !== undefined ? { tif } : {},
60853
+ ...slippageBps !== undefined ? { slippageBps } : {},
60854
+ ...values["--tp"] !== undefined ? { takeProfit: values["--tp"] } : {},
60855
+ ...values["--sl"] !== undefined ? { stopLoss: values["--sl"] } : {}
60856
+ };
60857
+ try {
60858
+ return await runPerpsWrite(ctx, { path: "/api/v1/agent/perps/open", body, ...sharedWrite(values, parsed.booleans) });
60859
+ } catch (error) {
60860
+ return tradingFailure(ctx, error);
60861
+ }
60862
+ }
60863
+ async function perpsClose(args, ctx) {
60864
+ const parsed = parseArgs(args, {
60865
+ valueFlags: ["--wallet", "--size", "--slippage-bps"],
60866
+ booleanFlags: ["--yes", "--no-submit", "--sign-only"]
60867
+ });
60868
+ if ("error" in parsed)
60869
+ return usage4(ctx, parsed.error);
60870
+ const [coin] = parsed.positionals;
60871
+ if (parsed.positionals.length !== 1 || !coin)
60872
+ return usage4(ctx, "Usage: candle perps close <coin> [--size <n>] [--slippage-bps <n>] [--wallet <tee>] [--yes]");
60873
+ const size = parsed.values["--size"];
60874
+ if (size !== undefined && !DEC_RE.test(size))
60875
+ return usage4(ctx, "--size must be a plain decimal.");
60876
+ const slippageBps = slippage(parsed.values["--slippage-bps"]);
60877
+ if (slippageBps === null)
60878
+ return usage4(ctx, "--slippage-bps must be a whole number of at least 1.");
60879
+ const body = {
60880
+ coin,
60881
+ ...size !== undefined ? { size } : {},
60882
+ ...slippageBps !== undefined ? { slippageBps } : {}
60883
+ };
60884
+ try {
60885
+ return await runPerpsWrite(ctx, {
60886
+ path: "/api/v1/agent/perps/close",
60887
+ body,
60888
+ ...sharedWrite(parsed.values, parsed.booleans)
60889
+ });
60890
+ } catch (error) {
60891
+ return tradingFailure(ctx, error);
60892
+ }
60893
+ }
60894
+ async function perpsCancel(args, ctx) {
60895
+ const parsed = parseArgs(args, WRITE_FLAGS);
60896
+ if ("error" in parsed)
60897
+ return usage4(ctx, parsed.error);
60898
+ const [cloid] = parsed.positionals;
60899
+ if (parsed.positionals.length !== 1 || !cloid || !CLOID_RE.test(cloid))
60900
+ return usage4(ctx, "Usage: candle perps cancel <cloid> [--wallet <tee>] [--yes] (cloid: 0x + 32 hex)");
60901
+ try {
60902
+ return await runPerpsWrite(ctx, {
60903
+ path: "/api/v1/agent/perps/cancel",
60904
+ body: { cloid: cloid.toLowerCase() },
60905
+ ...sharedWrite(parsed.values, parsed.booleans)
60906
+ });
60907
+ } catch (error) {
60908
+ return tradingFailure(ctx, error);
60909
+ }
60910
+ }
60911
+ async function perpsLeverage(args, ctx) {
60912
+ const parsed = parseArgs(args, {
60913
+ valueFlags: ["--wallet"],
60914
+ booleanFlags: ["--yes", "--no-submit", "--sign-only", "--isolated"]
60915
+ });
60916
+ if ("error" in parsed)
60917
+ return usage4(ctx, parsed.error);
60918
+ const [coin, leverage] = parsed.positionals;
60919
+ const n = Number(leverage);
60920
+ if (parsed.positionals.length !== 2 || !coin || !Number.isInteger(n) || n < 1)
60921
+ return usage4(ctx, "Usage: candle perps leverage <coin> <x> [--isolated] [--wallet <tee>] [--yes]");
60922
+ try {
60923
+ return await runPerpsWrite(ctx, {
60924
+ path: "/api/v1/agent/perps/leverage",
60925
+ body: { coin, leverage: n, mode: parsed.booleans.has("--isolated") ? "isolated" : "cross" },
60926
+ ...sharedWrite(parsed.values, parsed.booleans)
60927
+ });
60928
+ } catch (error) {
60929
+ return tradingFailure(ctx, error);
60930
+ }
60931
+ }
60932
+ async function perpsPositions(args, ctx) {
60933
+ const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
60934
+ if ("error" in parsed)
60935
+ return usage4(ctx, parsed.error);
60936
+ if (parsed.positionals.length !== 0)
60937
+ return usage4(ctx, "Usage: candle perps positions [--wallet <tee>]");
60938
+ try {
60939
+ const key = await tradingKey(ctx);
60940
+ const { row } = await perpsWallet(ctx, key, parsed.values["--wallet"]);
60941
+ const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(row.id)}`);
60942
+ if (ctx.json)
60943
+ return printTradingResult(ctx, result);
60944
+ const out = ctx.deps.stdout;
60945
+ const account = result.account ?? {};
60946
+ out.write(`${safeText(result.address)} (${safeText(result.network)}, ${safeText(result.mode)} mode): account value ${safeText(account.accountValue)} USD, withdrawable ${safeText(result.withdrawable)} USD
60947
+ `);
60948
+ const positions = result.positions ?? [];
60949
+ if (positions.length === 0)
60950
+ out.write(`No open positions.
60951
+ `);
60952
+ for (const p of positions) {
60953
+ const leverage = p.leverage ?? {};
60954
+ out.write(` ${safeText(p.coin)} ${safeText(p.szi)} at ${safeText(p.entryPx)}, value ${safeText(p.positionValue)}, uPnL ${safeText(p.unrealizedPnl)}, ${safeText(leverage.value)}x ${safeText(leverage.type)}, liq ${safeText(p.liquidationPx ?? "none")}
60955
+ `);
60956
+ }
60957
+ return 0;
60958
+ } catch (error) {
60959
+ return tradingFailure(ctx, error);
60960
+ }
60961
+ }
60962
+ async function perpsOrders(args, ctx) {
60963
+ const parsed = parseArgs(args, { valueFlags: ["--wallet", "--limit"] });
60964
+ if ("error" in parsed)
60965
+ return usage4(ctx, parsed.error);
60966
+ const limit = parsed.values["--limit"];
60967
+ if (parsed.positionals.length !== 0 || limit !== undefined && !/^[1-9][0-9]{0,2}$/.test(limit))
60968
+ return usage4(ctx, "Usage: candle perps orders [--wallet <tee>] [--limit <n>]");
60969
+ try {
60970
+ const key = await tradingKey(ctx);
60971
+ const { row } = await perpsWallet(ctx, key, parsed.values["--wallet"]);
60972
+ const query = new URLSearchParams({ walletId: row.id });
60973
+ if (limit !== undefined)
60974
+ query.set("limit", limit);
60975
+ const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/orders?${query}`);
60976
+ if (ctx.json)
60977
+ return printTradingResult(ctx, result);
60978
+ const out = ctx.deps.stdout;
60979
+ const open4 = result.open ?? [];
60980
+ out.write(`Open on Hyperliquid (${open4.length}):
60981
+ `);
60982
+ for (const o of open4) {
60983
+ out.write(` ${safeText(o.coin)} ${o.side === "B" ? "buy" : "sell"} ${safeText(o.sz)} at ${safeText(o.limitPx)}${o.reduceOnly ? " reduce-only" : ""}${o.isTrigger ? ` trigger ${safeText(o.triggerPx)}` : ""}, cloid ${safeText(o.cloid ?? "none")}
60984
+ `);
60985
+ }
60986
+ const recorded = result.recorded ?? [];
60987
+ out.write(`Built by Candle (${recorded.length}, newest first):
60988
+ `);
60989
+ for (const r of recorded) {
60990
+ out.write(` ${safeText(r.kind)} ${safeText(r.coin ?? "")} ${safeText(r.status)}${r.venueStatus ? ` (${safeText(r.venueStatus)})` : ""}${r.cloid ? `, cloid ${safeText(r.cloid)}` : ""}${r.targetCloid ? `, target ${safeText(r.targetCloid)}` : ""}
60991
+ `);
60992
+ }
60993
+ return 0;
60994
+ } catch (error) {
60995
+ return tradingFailure(ctx, error);
60996
+ }
60997
+ }
60998
+
59081
60999
  // src/commands/plugins.ts
59082
61000
  init_args();
59083
61001
 
@@ -59271,20 +61189,20 @@ async function writeSecretNames(deps, profile, names) {
59271
61189
  else
59272
61190
  await deps.updateProfile(profile, { secretNames: sorted });
59273
61191
  }
59274
- function usage4(ctx, line) {
61192
+ function usage5(ctx, line) {
59275
61193
  writeUsageFailure(ctx.deps, line, ctx.json);
59276
61194
  return 2;
59277
61195
  }
59278
61196
  async function secretsSet(args, ctx) {
59279
61197
  const parsed = parseArgs(args, {});
59280
61198
  if ("error" in parsed)
59281
- return usage4(ctx, parsed.error);
61199
+ return usage5(ctx, parsed.error);
59282
61200
  const [raw, extra] = parsed.positionals;
59283
61201
  if (!raw || extra !== undefined)
59284
- return usage4(ctx, "Usage: candle secrets set <name>");
61202
+ return usage5(ctx, "Usage: candle secrets set <name>");
59285
61203
  const name2 = canonicalSecretName(raw);
59286
61204
  if (name2 === undefined) {
59287
- return usage4(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
61205
+ return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
59288
61206
  }
59289
61207
  if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
59290
61208
  writeLocalFailure(ctx.deps, {
@@ -59296,7 +61214,7 @@ async function secretsSet(args, ctx) {
59296
61214
  }
59297
61215
  const value = await ctx.deps.promptSecret(`Value for ${name2} (input hidden): `);
59298
61216
  if (value.length === 0)
59299
- return usage4(ctx, "An empty value was typed; nothing was stored.");
61217
+ return usage5(ctx, "An empty value was typed; nothing was stored.");
59300
61218
  try {
59301
61219
  await ctx.deps.secretsStore.set(secretRef(ctx.profile, name2), value);
59302
61220
  } catch (error) {
@@ -59316,9 +61234,9 @@ async function secretsSet(args, ctx) {
59316
61234
  async function secretsList(args, ctx) {
59317
61235
  const parsed = parseArgs(args, {});
59318
61236
  if ("error" in parsed)
59319
- return usage4(ctx, parsed.error);
61237
+ return usage5(ctx, parsed.error);
59320
61238
  if (parsed.positionals.length > 0)
59321
- return usage4(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
61239
+ return usage5(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
59322
61240
  const names = await storedSecretNames(ctx.deps, ctx.profile);
59323
61241
  if (ctx.json) {
59324
61242
  ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
@@ -59337,13 +61255,13 @@ async function secretsList(args, ctx) {
59337
61255
  async function secretsRemove(args, ctx) {
59338
61256
  const parsed = parseArgs(args, {});
59339
61257
  if ("error" in parsed)
59340
- return usage4(ctx, parsed.error);
61258
+ return usage5(ctx, parsed.error);
59341
61259
  const [raw, extra] = parsed.positionals;
59342
61260
  if (!raw || extra !== undefined)
59343
- return usage4(ctx, "Usage: candle secrets remove <name>");
61261
+ return usage5(ctx, "Usage: candle secrets remove <name>");
59344
61262
  const name2 = canonicalSecretName(raw);
59345
61263
  if (name2 === undefined) {
59346
- return usage4(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
61264
+ return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
59347
61265
  }
59348
61266
  await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name2));
59349
61267
  const names = await storedSecretNames(ctx.deps, ctx.profile);
@@ -59469,11 +61387,11 @@ init_render();
59469
61387
  function formatUsd(value) {
59470
61388
  if (!Number.isFinite(value))
59471
61389
  return "?";
59472
- const sign3 = value < 0 ? "-" : "";
61390
+ const sign4 = value < 0 ? "-" : "";
59473
61391
  const abs = Math.abs(value);
59474
61392
  if (abs > 0 && abs < 0.005)
59475
- return `${sign3}<$0.01`;
59476
- return `${sign3}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
61393
+ return `${sign4}<$0.01`;
61394
+ return `${sign4}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
59477
61395
  }
59478
61396
  function formatPrice(value) {
59479
61397
  if (!Number.isFinite(value) || value <= 0)
@@ -59500,6 +61418,8 @@ function shortAddress2(address) {
59500
61418
  }
59501
61419
 
59502
61420
  // src/commands/pnl.ts
61421
+ var CHAINS = ["solana", "hood"];
61422
+ var CHAIN_TITLES = { solana: "Solana", hood: "Hood" };
59503
61423
  var NO_API_KEY4 = {
59504
61424
  code: "NO_API_KEY",
59505
61425
  message: "No API key for this profile.",
@@ -59567,6 +61487,7 @@ async function pnl(args, ctx) {
59567
61487
  deps.stdout.write(`P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
59568
61488
 
59569
61489
  `);
61490
+ writeChainBlocks(ctx, p.byChain);
59570
61491
  writeSummary(ctx, {
59571
61492
  realizedNetUsd: p.realizedNetUsd,
59572
61493
  realizedGrossUsd: p.realizedGrossUsd,
@@ -59583,6 +61504,7 @@ async function pnl(args, ctx) {
59583
61504
  lp
59584
61505
  });
59585
61506
  writePositions(ctx, p.openPositions, false);
61507
+ writeWallets(ctx, p.byWallet);
59586
61508
  writeLpPositions(ctx, lp, false);
59587
61509
  return 0;
59588
61510
  }
@@ -59590,6 +61512,7 @@ async function pnl(args, ctx) {
59590
61512
  deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
59591
61513
 
59592
61514
  `);
61515
+ writeChainBlocks(ctx, { solana: books.solana, hood: books.hood });
59593
61516
  writeSummary(ctx, {
59594
61517
  ...books.all,
59595
61518
  positions: books.positions.length,
@@ -59603,6 +61526,68 @@ async function pnl(args, ctx) {
59603
61526
  writeLpPositions(ctx, books.lp, true);
59604
61527
  return 0;
59605
61528
  }
61529
+ function isChainSummary(value) {
61530
+ return typeof value === "object" && value !== null && typeof value.realizedNetUsd === "number";
61531
+ }
61532
+ function writeChainBlocks(ctx, byChain) {
61533
+ const blocks = CHAINS.flatMap((chain2) => {
61534
+ const summary = byChain?.[chain2];
61535
+ return isChainSummary(summary) ? [[chain2, summary]] : [];
61536
+ });
61537
+ if (blocks.length === 0)
61538
+ return;
61539
+ for (const [chain2, s] of blocks) {
61540
+ const unmarked = s.unmarked ?? s.unmarkedPositions ?? 0;
61541
+ const marked = s.openPositions - unmarked;
61542
+ ctx.deps.stdout.write(`${CHAIN_TITLES[chain2]}
61543
+ `);
61544
+ writeLines(ctx, [
61545
+ [
61546
+ "Realized net",
61547
+ formatUsd(s.realizedNetUsd),
61548
+ `gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
61549
+ ],
61550
+ [
61551
+ "Unrealized",
61552
+ formatUsd(s.unrealizedUsd),
61553
+ `${marked} of ${s.openPositions} open ${s.openPositions === 1 ? "position" : "positions"} marked${unmarked > 0 ? `; ${unmarked} unpriced, not counted` : ""}`
61554
+ ],
61555
+ ["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]
61556
+ ]);
61557
+ if (s.unvalued > 0) {
61558
+ ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
61559
+ `);
61560
+ }
61561
+ ctx.deps.stdout.write(`
61562
+ `);
61563
+ }
61564
+ ctx.deps.stdout.write(`All chains
61565
+ `);
61566
+ }
61567
+ function writeLines(ctx, lines) {
61568
+ const width = Math.max(...lines.map(([label]) => label.length));
61569
+ const valueWidth = Math.max(...lines.map(([, value]) => value.length));
61570
+ for (const [label, value, note] of lines) {
61571
+ ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
61572
+ `);
61573
+ }
61574
+ }
61575
+ function writeWallets(ctx, wallets2) {
61576
+ if (!wallets2 || wallets2.length === 0)
61577
+ return;
61578
+ const rows = wallets2.map((w) => [
61579
+ `${w.label?.trim() ? `${w.label.trim()} ` : w.payerType === "main" ? "main " : ""}(${shortAddress2(w.wallet)})`,
61580
+ w.chain ?? "-",
61581
+ String(w.tradesConsidered),
61582
+ formatUsd(w.realizedNetUsd),
61583
+ formatUsd(w.unrealizedUsd),
61584
+ `${w.openPositions.length}${w.unmarkedPositions > 0 ? ` (${w.unmarkedPositions} unpriced)` : ""}`
61585
+ ]);
61586
+ ctx.deps.stdout.write(`
61587
+ By wallet (each its own cost basis, so the rows need not sum to the total)
61588
+ ${renderTable(["WALLET", "CHAIN", "TRADES", "REALIZED NET", "UNREALIZED", "OPEN"], rows.map((row) => row.map(terminalText)))}
61589
+ `);
61590
+ }
59606
61591
  function writeSummary(ctx, s) {
59607
61592
  const marked = s.positions - s.unmarked;
59608
61593
  const lines = [
@@ -59642,12 +61627,7 @@ function writeSummary(ctx, s) {
59642
61627
  } else {
59643
61628
  lines.push(["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]);
59644
61629
  }
59645
- const width = Math.max(...lines.map(([label]) => label.length));
59646
- const valueWidth = Math.max(...lines.map(([, value]) => value.length));
59647
- for (const [label, value, note] of lines) {
59648
- ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
59649
- `);
59650
- }
61630
+ writeLines(ctx, lines);
59651
61631
  if (s.oldestMarkAt !== undefined) {
59652
61632
  ctx.deps.stdout.write(`Marks as old as ${new Date(s.oldestMarkAt).toISOString()}.
59653
61633
  `);
@@ -59708,12 +61688,13 @@ No open positions.
59708
61688
  `);
59709
61689
  return;
59710
61690
  }
59711
- const headers = ["TOKEN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
61691
+ const headers = ["TOKEN", "CHAIN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
59712
61692
  if (withBook)
59713
61693
  headers.push("BOOK");
59714
61694
  const rows = positions.map((p) => {
59715
61695
  const row = [
59716
61696
  p.symbol?.trim() || shortAddress2(p.mint),
61697
+ p.chain ?? "-",
59717
61698
  formatQuantity(p.quantity),
59718
61699
  formatPrice(p.avgEntryUsd),
59719
61700
  p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
@@ -59732,10 +61713,12 @@ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
59732
61713
  // src/commands/portfolio.ts
59733
61714
  init_args();
59734
61715
  init_deps();
61716
+ init_evm_lite();
59735
61717
  init_profiles();
59736
61718
  init_render();
59737
61719
  init_solana_endpoint();
59738
61720
  init_solana_lite();
61721
+ init_errors();
59739
61722
  init_store();
59740
61723
  init_vault_support();
59741
61724
  var SOL_MINT = "So11111111111111111111111111111111111111112";
@@ -59746,31 +61729,74 @@ var KNOWN_SYMBOLS = {
59746
61729
  };
59747
61730
  var CHUNK = 100;
59748
61731
  var TOKEN_READS_IN_FLIGHT = 8;
61732
+ var EVM_READS_IN_FLIGHT = 8;
61733
+ var CHAINS2 = ["solana", "hood"];
61734
+ var CHAIN_NAMES = { solana: "Solana", hood: "Hood" };
61735
+ var HOOD_NATIVE = "native";
61736
+ function hoodPriceKey(mint) {
61737
+ return mint === HOOD_NATIVE ? "hood:native" : `hood:${mint.toLowerCase()}`;
61738
+ }
61739
+ var HOOD_USDG = toChecksumAddress(HOOD_USDG_ADDRESS);
61740
+ var UNREAD_LABELS = {
61741
+ sol: "SOL",
61742
+ tokens: "tokens",
61743
+ eth: "ETH",
61744
+ usdg: "USDG",
61745
+ "hood-tokens": "tokens"
61746
+ };
59749
61747
  var NO_API_KEY5 = {
59750
61748
  code: "NO_API_KEY",
59751
61749
  message: "No API key for this profile.",
59752
61750
  suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
59753
61751
  };
59754
61752
  async function portfolio(args, ctx) {
59755
- const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
61753
+ const parsed = parseArgs(args, {
61754
+ valueFlags: ["--rpc-url", "--evm-rpc-url", "--keystore", "--chain"],
61755
+ pathFlags: ["--keystore"]
61756
+ });
59756
61757
  if ("error" in parsed)
59757
61758
  return usage(ctx, parsed.error);
59758
61759
  if (parsed.positionals.length > 0)
59759
61760
  return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
59760
61761
  const { deps } = ctx;
61762
+ const chainFlag = parsed.values["--chain"];
61763
+ if (chainFlag !== undefined && !CHAINS2.includes(chainFlag)) {
61764
+ return usage(ctx, `--chain must be solana or hood, not ${chainFlag}.`);
61765
+ }
61766
+ const chains = chainFlag === undefined ? [...CHAINS2] : [chainFlag];
61767
+ const showSolana = chains.includes("solana");
61768
+ const showHood = chains.includes("hood");
61769
+ if (!showSolana && parsed.values["--rpc-url"] !== undefined) {
61770
+ return usage(ctx, "--rpc-url is the Solana RPC and has no effect with --chain hood; use --evm-rpc-url.");
61771
+ }
61772
+ if (!showHood && parsed.values["--evm-rpc-url"] !== undefined) {
61773
+ return usage(ctx, "--evm-rpc-url is the Hood RPC and has no effect with --chain solana.");
61774
+ }
59761
61775
  const apiKey = await resolveApiKey(deps, ctx.profile);
59762
61776
  if (!apiKey) {
59763
61777
  writeLocalFailure(deps, NO_API_KEY5, ctx.json);
59764
61778
  return 1;
59765
61779
  }
59766
- const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
59767
- if ("error" in solana)
59768
- return usage(ctx, solana.error);
61780
+ let solana;
61781
+ if (showSolana) {
61782
+ const opened = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
61783
+ if ("error" in opened)
61784
+ return usage(ctx, opened.error);
61785
+ solana = opened;
61786
+ }
61787
+ let evmRpc;
61788
+ if (showHood) {
61789
+ const resolved = resolveEvmRpcUrl(parsed.values["--evm-rpc-url"], deps.env[EVM_RPC_URL_ENV], "--evm-rpc-url");
61790
+ if ("error" in resolved)
61791
+ return usage(ctx, resolved.error);
61792
+ evmRpc = resolved;
61793
+ }
59769
61794
  const resolvedVault = vaultPathFor(ctx, parsed);
59770
61795
  if ("error" in resolvedVault)
59771
61796
  return usage(ctx, resolvedVault.error);
59772
61797
  return runVaultCommand(ctx, async ({ hold }) => {
59773
61798
  let vaultEntries;
61799
+ let evmEntries;
59774
61800
  let vaultReason;
59775
61801
  const raw = await readVaultRaw(resolvedVault.path);
59776
61802
  if (raw === null) {
@@ -59781,17 +61807,26 @@ async function portfolio(args, ctx) {
59781
61807
  if (!requirePromptStreams(ctx, "portfolio"))
59782
61808
  return 1;
59783
61809
  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 }));
61810
+ vaultEntries = showSolana ? vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role })) : [];
61811
+ evmEntries = showHood ? vault.index.entries.filter((entry) => entry.chain === "evm" && entry.role === "vault").map((entry) => ({ address: entry.address, label: entry.label })) : [];
59785
61812
  }
59786
61813
  await printIdentity(ctx);
59787
- const rpcHost = solana.endpoint.host;
59788
- if (vaultEntries && vaultEntries.length > 0) {
61814
+ const rpcHost = solana?.endpoint.host;
61815
+ if (solana && vaultEntries && vaultEntries.length > 0) {
59789
61816
  await solana.disclose();
59790
61817
  const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
59791
61818
  deps.stderr.write(`Reading ${vaultEntries.length} vault ${vaultEntries.length === 1 ? "address" : "addresses"} from ${rpcHost} in ${requests} requests. That endpoint sees them together; Candle sees none of them.
59792
61819
  `);
59793
61820
  }
59794
- const [candle, vaultRead] = await Promise.all([
61821
+ let evmHost;
61822
+ if (evmRpc && evmEntries && evmEntries.length > 0) {
61823
+ evmHost = rpcHostOf2(evmRpc.url);
61824
+ const n = evmEntries.length;
61825
+ deps.stderr.write(`Reading ETH and USDG for ${n} EVM vault ${n === 1 ? "address" : "addresses"} from ${evmHost}${evmRpc.builtIn ? " (the built-in Hood RPC)" : ""} in ${1 + 2 * n} requests. That endpoint sees them together; Candle sees none of them.
61826
+ `);
61827
+ }
61828
+ const evmRead2 = evmRpc && evmEntries && evmEntries.length > 0 ? await startEvmRead(evmEntries.map((entry) => entry.address), evmRpc.url, deps.fetch) : undefined;
61829
+ const [candle, vaultRead, evmBalances] = await Promise.all([
59795
61830
  apiRequest("/api/v1/agent/portfolio", {
59796
61831
  auth: "key",
59797
61832
  credentials: { apiKey },
@@ -59799,11 +61834,17 @@ async function portfolio(args, ctx) {
59799
61834
  fetch: deps.fetch,
59800
61835
  env: deps.env
59801
61836
  }),
59802
- vaultEntries ? readOwnRpc(vaultEntries.map((entry) => entry.address), solana, ctx) : Promise.resolve(undefined)
61837
+ solana && vaultEntries && vaultEntries.length > 0 ? readOwnRpc(vaultEntries.map((entry) => entry.address), solana, ctx) : Promise.resolve(undefined),
61838
+ evmRead2 ? evmRead2.balances() : Promise.resolve(undefined)
59803
61839
  ]);
59804
61840
  if (vaultRead?.failure !== undefined) {
59805
61841
  const n = vaultRead.unavailable.length;
59806
61842
  deps.stderr.write(`${n} vault ${n === 1 ? "address" : "addresses"} could not be read: ${vaultRead.failure}.
61843
+ `);
61844
+ }
61845
+ if (evmBalances?.failure !== undefined) {
61846
+ const n = evmBalances.unavailable.length;
61847
+ deps.stderr.write(`${n} EVM vault ${n === 1 ? "address" : "addresses"} on Hood could not be read in full: ${terminalText(evmBalances.failure)}.
59807
61848
  `);
59808
61849
  }
59809
61850
  if (!candle.ok) {
@@ -59812,6 +61853,7 @@ async function portfolio(args, ctx) {
59812
61853
  }
59813
61854
  const fromCandle = candle.body;
59814
61855
  const prices = { ...fromCandle.prices ?? {} };
61856
+ const candleHood = showHood ? fromCandle.hood : undefined;
59815
61857
  let priceFailure;
59816
61858
  if (vaultRead) {
59817
61859
  const wanted = new Set;
@@ -59838,84 +61880,244 @@ async function portfolio(args, ctx) {
59838
61880
  priceFailure ??= priced.message;
59839
61881
  }
59840
61882
  }
61883
+ if (evmBalances) {
61884
+ const wanted = [];
61885
+ const held = [...evmBalances.byAddress.values()];
61886
+ if (held.some((b) => (b.wei ?? 0n) > 0n))
61887
+ wanted.push(HOOD_NATIVE);
61888
+ if (held.some((b) => (b.usdg ?? 0n) > 0n))
61889
+ wanted.push(HOOD_USDG);
61890
+ const missing = wanted.filter((asset) => prices[hoodPriceKey(asset)] === undefined);
61891
+ if (missing.length > 0) {
61892
+ const priced = await apiRequest("/api/v1/agent/prices", {
61893
+ method: "POST",
61894
+ body: { hood: missing },
61895
+ auth: "key",
61896
+ credentials: { apiKey },
61897
+ apiUrl: ctx.apiUrl,
61898
+ fetch: deps.fetch,
61899
+ env: deps.env
61900
+ });
61901
+ if (priced.ok)
61902
+ Object.assign(prices, priced.body.prices ?? {});
61903
+ else
61904
+ priceFailure ??= priced.message;
61905
+ }
61906
+ }
59841
61907
  if (priceFailure !== undefined) {
59842
61908
  deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
59843
61909
  `);
59844
61910
  }
61911
+ const lpSection = showSolana ? fromCandle.lp : undefined;
59845
61912
  const lpByWallet = new Map;
59846
61913
  const lpOf = (address) => {
59847
61914
  const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
59848
61915
  lpByWallet.set(address, entry);
59849
61916
  return entry;
59850
61917
  };
59851
- for (const position of fromCandle.lp?.positions ?? [])
61918
+ for (const position of lpSection?.positions ?? [])
59852
61919
  lpOf(position.wallet).positions.push(lpRow(position, prices));
59853
- for (const unread of fromCandle.lp?.unreadable ?? [])
61920
+ for (const unread of lpSection?.unreadable ?? [])
59854
61921
  lpOf(unread.wallet).unread.push(unread.position);
59855
- const wallet = (address, read, extra) => {
59856
- const holdings = read === undefined ? null : valueHoldings(read, prices);
59857
- const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
59858
- const lp2 = fromCandle.lp ? lpOf(address) : undefined;
61922
+ const wallet = (chain2, address, read, extra) => {
61923
+ const lp2 = chain2 === "solana" && lpSection ? lpOf(address) : undefined;
59859
61924
  return {
61925
+ chain: chain2,
59860
61926
  address,
59861
61927
  ...extra,
59862
- holdings,
59863
- ...unread.length > 0 ? { unread } : {},
61928
+ holdings: read.holdings,
61929
+ ...read.unread.length > 0 ? { unread: read.unread } : {},
61930
+ ...read.truncated ? { truncated: true } : {},
59864
61931
  ...lp2 ? { lpPositions: lp2.positions } : {},
59865
61932
  ...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
61933
+ valueUsd: (read.holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
61934
+ unpriced: (read.holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
59868
61935
  };
59869
61936
  };
61937
+ const subtotals = (wallets2) => Object.fromEntries(chains.map((chain2) => {
61938
+ const on = wallets2.filter((w) => w.chain === chain2);
61939
+ return [
61940
+ chain2,
61941
+ {
61942
+ valueUsd: on.reduce((sum, w) => sum + w.valueUsd, 0),
61943
+ unpriced: on.reduce((sum, w) => sum + w.unpriced, 0)
61944
+ }
61945
+ ];
61946
+ }));
59870
61947
  const group = (name2, wallets2, read, reason) => ({
59871
61948
  group: name2,
59872
61949
  read,
59873
61950
  ...reason !== undefined ? { reason } : {},
59874
61951
  wallets: wallets2,
59875
61952
  valueUsd: wallets2.reduce((sum, w) => sum + w.valueUsd, 0),
59876
- unpriced: wallets2.reduce((sum, w) => sum + w.unpriced, 0)
61953
+ unpriced: wallets2.reduce((sum, w) => sum + w.unpriced, 0),
61954
+ byChain: subtotals(wallets2)
59877
61955
  });
59878
- const orNull = (read) => read.lamports === null && read.tokens === null ? undefined : read;
61956
+ const solanaRows = (rows) => showSolana ? rows ?? [] : [];
59879
61957
  const groups = [
59880
- group("vault", (vaultEntries ?? []).map((entry) => {
59881
- const read = vaultRead?.byAddress.get(entry.address);
59882
- return wallet(entry.address, read ? orNull(read) : undefined, { label: entry.label, role: entry.role });
59883
- }), vaultEntries !== undefined, vaultReason),
59884
- group("tee", (fromCandle.tee ?? []).map((row) => wallet(row.address, orNull(row), {
59885
- id: row.id,
59886
- ...row.label ? { label: row.label } : {},
59887
- active: row.active
59888
- })), true),
59889
- group("embedded", (fromCandle.embedded ?? []).map((row) => wallet(row.address, orNull(row), {})), true)
61958
+ group("vault", [
61959
+ ...(vaultEntries ?? []).map((entry) => wallet("solana", entry.address, solanaHoldings(vaultRead?.byAddress.get(entry.address), prices), {
61960
+ label: entry.label,
61961
+ role: entry.role
61962
+ })),
61963
+ ...(evmEntries ?? []).map((entry) => {
61964
+ const read = evmBalances?.byAddress.get(entry.address);
61965
+ return wallet("hood", entry.address, hoodHoldings(read && {
61966
+ wei: read.wei === null ? null : read.wei.toString(),
61967
+ tokens: read.usdg === null ? null : read.usdg > 0n ? [
61968
+ {
61969
+ mint: HOOD_USDG,
61970
+ amountRaw: read.usdg.toString(),
61971
+ decimals: HOOD_USDG_DECIMALS,
61972
+ symbol: "USDG"
61973
+ }
61974
+ ] : []
61975
+ }, prices, "usdg"), { label: entry.label, role: "vault" });
61976
+ })
61977
+ ], vaultEntries !== undefined, vaultReason),
61978
+ group("tee", [
61979
+ ...solanaRows(fromCandle.tee).map((row) => wallet("solana", row.address, solanaHoldings(row, prices), {
61980
+ id: row.id,
61981
+ ...row.label ? { label: row.label } : {},
61982
+ active: row.active
61983
+ })),
61984
+ ...(candleHood?.tee ?? []).map((row) => wallet("hood", row.address, hoodHoldings(row, prices, "hood-tokens"), {
61985
+ id: row.id,
61986
+ ...row.label ? { label: row.label } : {},
61987
+ active: row.active
61988
+ }))
61989
+ ], true),
61990
+ group("embedded", [
61991
+ ...solanaRows(fromCandle.embedded).map((row) => wallet("solana", row.address, solanaHoldings(row, prices), {})),
61992
+ ...(candleHood?.embedded ?? []).map((row) => wallet("hood", row.address, hoodHoldings(row, prices, "hood-tokens"), {}))
61993
+ ], true)
59890
61994
  ];
59891
- const unavailable = [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []];
59892
- const lpUnread = (fromCandle.lp?.unreadable ?? []).length;
59893
- const complete = fromCandle.complete !== false && unavailable.length === 0 && lpUnread === 0;
61995
+ const unavailableByChain = {
61996
+ solana: showSolana ? [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []] : [],
61997
+ hood: [...evmBalances?.unavailable ?? [], ...candleHood?.unavailable ?? []]
61998
+ };
61999
+ const unavailable = chains.flatMap((chain2) => unavailableByChain[chain2]);
62000
+ const lpUnread = (lpSection?.unreadable ?? []).length;
62001
+ const candleExplained = (fromCandle.unavailable ?? []).length > 0 || (fromCandle.hood?.unavailable ?? []).length > 0 || (fromCandle.lp?.unreadable ?? []).length > 0;
62002
+ const listCutOff = fromCandle.complete === false && !candleExplained;
62003
+ const complete = !listCutOff && unavailable.length === 0 && lpUnread === 0;
59894
62004
  const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
59895
62005
  const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
59896
- const lp = fromCandle.lp ? {
59897
- positions: fromCandle.lp.positions.length,
59898
- unpriced: fromCandle.lp.positions.filter((p) => p.valueUsd === null).length,
62006
+ const lp = lpSection ? {
62007
+ positions: lpSection.positions.length,
62008
+ unpriced: lpSection.positions.filter((p) => p.valueUsd === null).length,
59899
62009
  unreadable: lpUnread,
59900
- valueUsd: fromCandle.lp.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
62010
+ valueUsd: lpSection.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
59901
62011
  } : undefined;
62012
+ const all = groups.flatMap((g) => g.wallets);
62013
+ const byChain = subtotals(all);
62014
+ for (const chain2 of chains) {
62015
+ const entry = byChain[chain2];
62016
+ if (!entry)
62017
+ continue;
62018
+ entry.wallets = all.filter((w) => w.chain === chain2).length;
62019
+ entry.unavailable = unavailableByChain[chain2].length;
62020
+ }
62021
+ const hoodTotals = byChain.hood;
62022
+ if (hoodTotals) {
62023
+ const reasons = { ...candleHood?.unpricedByReason ?? {} };
62024
+ let vaultUnpriced = 0;
62025
+ for (const w of groups[0]?.wallets ?? []) {
62026
+ if (w.chain !== "hood")
62027
+ continue;
62028
+ for (const h of w.holdings ?? []) {
62029
+ if (h.priceUsd !== null)
62030
+ continue;
62031
+ vaultUnpriced += 1;
62032
+ const reason = h.unpricedReason ?? "source-unavailable";
62033
+ reasons[reason] = (reasons[reason] ?? 0) + 1;
62034
+ }
62035
+ }
62036
+ if (candleHood)
62037
+ hoodTotals.unpriced = (candleHood.unpriced ?? 0) + vaultUnpriced;
62038
+ hoodTotals.unpricedByReason = reasons;
62039
+ }
59902
62040
  if (ctx.json) {
59903
62041
  writeJson(deps, {
59904
62042
  ok: true,
62043
+ chains,
59905
62044
  totalUsd,
59906
62045
  unpriced,
59907
62046
  complete,
59908
62047
  unavailable,
59909
- rpcHost,
62048
+ unavailableByChain: Object.fromEntries(chains.map((chain2) => [chain2, unavailableByChain[chain2]])),
62049
+ rpcHost: rpcHost ?? null,
62050
+ ...evmHost !== undefined ? { evmRpcHost: evmHost } : {},
59910
62051
  ...lp ? { lp } : {},
62052
+ byChain,
59911
62053
  groups
59912
62054
  });
59913
62055
  return complete ? 0 : 3;
59914
62056
  }
59915
- writeTable(ctx, groups, { totalUsd, unpriced, unavailable: unavailable.length, lp });
62057
+ writeTable(ctx, groups, {
62058
+ totalUsd,
62059
+ unpriced,
62060
+ chains,
62061
+ byChain,
62062
+ unavailableByChain,
62063
+ listCutOff,
62064
+ lp
62065
+ });
59916
62066
  return complete ? 0 : 3;
59917
62067
  });
59918
62068
  }
62069
+ async function startEvmRead(addresses, rpcUrl, fetchFn) {
62070
+ const rpc = createEvmRpc(rpcUrl, fetchFn);
62071
+ const host = rpcHostOf2(rpcUrl);
62072
+ const unique = [...new Set(addresses)];
62073
+ const message = (error) => error instanceof Error ? error.message : String(error);
62074
+ let chainId;
62075
+ try {
62076
+ chainId = await rpc.chainId();
62077
+ } catch (error) {
62078
+ const failure = message(error);
62079
+ return {
62080
+ balances: async () => ({
62081
+ byAddress: new Map(unique.map((address) => [address, { wei: null, usdg: null }])),
62082
+ unavailable: unique,
62083
+ failure
62084
+ })
62085
+ };
62086
+ }
62087
+ if (chainId !== BigInt(HOOD_CHAIN_ID)) {
62088
+ throw new VaultError("EVM_CHAIN_MISMATCH", `${host} answered chain id ${chainId}; candle portfolio reads EVM vault keys on Hood, chain id ${HOOD_CHAIN_ID}, only.`, {
62089
+ suggestion: "Nothing was read or priced. Point --evm-rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC, or pass --chain solana."
62090
+ });
62091
+ }
62092
+ return {
62093
+ balances: async () => {
62094
+ const byAddress = new Map;
62095
+ let failure;
62096
+ let next = 0;
62097
+ await Promise.all(Array.from({ length: Math.min(EVM_READS_IN_FLIGHT, unique.length) }, async () => {
62098
+ while (next < unique.length) {
62099
+ const address = unique[next++];
62100
+ const read = async (call) => {
62101
+ try {
62102
+ return await call();
62103
+ } catch (error) {
62104
+ failure ??= message(error);
62105
+ return null;
62106
+ }
62107
+ };
62108
+ const wei = await read(() => rpc.getBalance(address));
62109
+ const usdg = await read(() => rpc.erc20BalanceOf(HOOD_USDG, address));
62110
+ byAddress.set(address, { wei, usdg });
62111
+ }
62112
+ }));
62113
+ const unavailable = unique.filter((address) => {
62114
+ const read = byAddress.get(address);
62115
+ return read === undefined || read.wei === null || read.usdg === null;
62116
+ });
62117
+ return { byAddress, unavailable, ...failure === undefined ? {} : { failure } };
62118
+ }
62119
+ };
62120
+ }
59919
62121
  async function readOwnRpc(addresses, solana, ctx) {
59920
62122
  const { rpc } = solana;
59921
62123
  const unique = [...new Set(addresses)];
@@ -59991,6 +62193,14 @@ async function readOwnRpc(addresses, solana, ctx) {
59991
62193
  }
59992
62194
  return { byAddress, unavailable, ...failure === undefined ? {} : { failure } };
59993
62195
  }
62196
+ function solanaHoldings(read, prices) {
62197
+ if (read === undefined || read.lamports === null && read.tokens === null)
62198
+ return { holdings: null, unread: [] };
62199
+ return {
62200
+ holdings: valueHoldings(read, prices),
62201
+ unread: [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []]
62202
+ };
62203
+ }
59994
62204
  function valueHoldings(read, prices) {
59995
62205
  const raw = [];
59996
62206
  if (read.lamports !== null && BigInt(read.lamports) > 0n) {
@@ -60003,6 +62213,7 @@ function valueHoldings(read, prices) {
60003
62213
  const priceUsd = price?.priceUsd ?? null;
60004
62214
  const amount = formatAmount(h.amountRaw, h.decimals);
60005
62215
  return {
62216
+ chain: "solana",
60006
62217
  ...h,
60007
62218
  symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
60008
62219
  amount,
@@ -60012,6 +62223,46 @@ function valueHoldings(read, prices) {
60012
62223
  };
60013
62224
  });
60014
62225
  }
62226
+ function hoodHoldings(read, prices, tokensPart) {
62227
+ if (read === undefined || read.wei === null && read.tokens === null)
62228
+ return { holdings: null, unread: [] };
62229
+ const raw = [];
62230
+ if (read.wei !== null && BigInt(read.wei) > 0n) {
62231
+ raw.push({ mint: HOOD_NATIVE, amountRaw: read.wei, decimals: NATIVE_DECIMALS, program: "native", symbol: "ETH" });
62232
+ }
62233
+ for (const token of read.tokens ?? []) {
62234
+ if (!/^\d+$/.test(token.amountRaw) || BigInt(token.amountRaw) === 0n)
62235
+ continue;
62236
+ raw.push({
62237
+ mint: token.mint,
62238
+ amountRaw: token.amountRaw,
62239
+ decimals: token.decimals,
62240
+ program: "erc20",
62241
+ ...token.symbol ? { symbol: token.symbol } : {}
62242
+ });
62243
+ }
62244
+ const holdings = raw.map(({ symbol, ...h }) => {
62245
+ const price = prices[hoodPriceKey(h.mint)];
62246
+ const priceUsd = price?.priceUsd ?? null;
62247
+ const amount = formatAmount(h.amountRaw, h.decimals);
62248
+ const known = h.mint.toLowerCase() === HOOD_USDG_ADDRESS ? "USDG" : undefined;
62249
+ return {
62250
+ chain: "hood",
62251
+ ...h,
62252
+ symbol: known ?? symbol ?? price?.symbol ?? null,
62253
+ amount,
62254
+ priceUsd,
62255
+ valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
62256
+ priceSource: price?.source ?? null,
62257
+ ...priceUsd === null ? { unpricedReason: price?.unpricedReason ?? "source-unavailable" } : {}
62258
+ };
62259
+ });
62260
+ return {
62261
+ holdings,
62262
+ unread: [...read.wei === null ? ["eth"] : [], ...read.tokens === null ? [tokensPart] : []],
62263
+ ...read.truncated ? { truncated: true } : {}
62264
+ };
62265
+ }
60015
62266
  function lpRow(position, prices) {
60016
62267
  return {
60017
62268
  position: position.position,
@@ -60056,22 +62307,23 @@ function writeTable(ctx, groups, totals) {
60056
62307
  lpRows.push([g.group, name2, shortAddress2(position), "-", "not read", "-", "-"]);
60057
62308
  }
60058
62309
  if (w.holdings === null) {
60059
- rows.push([g.group, name2, "-", "not read", "-", "-"]);
62310
+ rows.push([g.group, w.chain, name2, "-", "not read", "-", "-"]);
60060
62311
  continue;
60061
62312
  }
60062
62313
  const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
60063
62314
  for (const h of sorted) {
60064
62315
  rows.push([
60065
62316
  g.group,
62317
+ w.chain,
60066
62318
  name2,
60067
62319
  h.symbol ?? shortAddress2(h.mint),
60068
62320
  h.amount,
60069
62321
  h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
60070
- h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
62322
+ h.valueUsd === null ? h.unpricedReason ?? "-" : formatUsd(h.valueUsd)
60071
62323
  ]);
60072
62324
  }
60073
62325
  for (const part of w.unread ?? [])
60074
- rows.push([g.group, name2, part === "sol" ? "SOL" : "tokens", "not read", "-", "-"]);
62326
+ rows.push([g.group, w.chain, name2, UNREAD_LABELS[part], "not read", "-", "-"]);
60075
62327
  }
60076
62328
  const empty = g.wallets.length - shown.length;
60077
62329
  const count = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
@@ -60083,7 +62335,7 @@ function writeTable(ctx, groups, totals) {
60083
62335
  }
60084
62336
  if (rows.length > 0)
60085
62337
  deps.stdout.write(`
60086
- ${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
62338
+ ${renderTable(["GROUP", "CHAIN", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
60087
62339
  `);
60088
62340
  else
60089
62341
  deps.stdout.write(`
@@ -60094,6 +62346,17 @@ Nothing held in any wallet read.
60094
62346
  LP positions
60095
62347
  ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
60096
62348
  `);
62349
+ for (const chain2 of totals.chains) {
62350
+ const sub = totals.byChain[chain2];
62351
+ if (!sub)
62352
+ continue;
62353
+ const reasons = Object.entries(sub.unpricedByReason ?? {}).filter(([, n]) => (n ?? 0) > 0).map(([reason, n]) => `${n} ${reason}`);
62354
+ notes.push([
62355
+ chain2,
62356
+ formatUsd(sub.valueUsd),
62357
+ `${sub.wallets} ${sub.wallets === 1 ? "wallet" : "wallets"}${sub.unpriced > 0 ? `, ${sub.unpriced} unpriced${reasons.length > 0 ? ` (${reasons.join(", ")})` : ""}` : ""}`
62358
+ ]);
62359
+ }
60097
62360
  notes.push([
60098
62361
  "total",
60099
62362
  formatUsd(totals.totalUsd),
@@ -60104,16 +62367,31 @@ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEE
60104
62367
  deps.stdout.write(`
60105
62368
  `);
60106
62369
  for (const [label, value, note] of notes) {
60107
- deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
62370
+ deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${terminalText(note)}
62371
+ `);
62372
+ }
62373
+ const unreadByChain = totals.chains.map((chain2) => [chain2, totals.unavailableByChain[chain2].length]).filter(([, n]) => n > 0);
62374
+ const unavailable = unreadByChain.reduce((sum, [, n]) => sum + n, 0);
62375
+ if (unavailable > 0) {
62376
+ deps.stdout.write(`${unavailable} ${unavailable === 1 ? "wallet" : "wallets"} could not be read in full (${unreadByChain.map(([chain2, n]) => `${n} on ${CHAIN_NAMES[chain2]}`).join(", ")}). What was not read is marked "not read" and is not in the total.
60108
62377
  `);
60109
62378
  }
60110
- if (totals.unavailable > 0) {
60111
- deps.stdout.write(`${totals.unavailable} ${totals.unavailable === 1 ? "wallet" : "wallets"} could not be read in full. What was not read is marked "not read" and is not in the total.
62379
+ if (totals.listCutOff) {
62380
+ deps.stdout.write(`Candle listed only part of this account's wallets. The total is partial.
62381
+ `);
62382
+ }
62383
+ const truncated = groups.flatMap((g) => g.wallets).filter((w) => w.truncated).length;
62384
+ if (truncated > 0) {
62385
+ deps.stdout.write(`${truncated} Hood ${truncated === 1 ? "wallet holds" : "wallets hold"} more tokens than Candle lists. The rest are not shown and not in the total.
60112
62386
  `);
60113
62387
  }
60114
62388
  if ((totals.lp?.unreadable ?? 0) > 0) {
60115
62389
  const n = totals.lp?.unreadable ?? 0;
60116
62390
  deps.stdout.write(`${n} LP ${n === 1 ? "position" : "positions"} could not be read (the pool did not answer). ${n === 1 ? "It is" : "They are"} marked "not read" and not in the total.
62391
+ `);
62392
+ }
62393
+ if (groups.some((g) => g.group === "vault" && g.wallets.some((w) => w.chain === "hood"))) {
62394
+ deps.stdout.write(`EVM vault keys are read for ETH and USDG only; other tokens they hold are not shown.
60117
62395
  `);
60118
62396
  }
60119
62397
  }
@@ -60641,7 +62919,7 @@ function displayLines(input) {
60641
62919
  lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
60642
62920
  return lines;
60643
62921
  }
60644
- async function sign3(args, ctx) {
62922
+ async function sign4(args, ctx) {
60645
62923
  const lifted = takeRepeatedFlag(args, "--wallet");
60646
62924
  if ("error" in lifted)
60647
62925
  return usage(ctx, lifted.error);
@@ -64149,10 +66427,10 @@ init_solana_lite();
64149
66427
  init_token_2022();
64150
66428
  init_errors();
64151
66429
  var USDC_MINT2 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
64152
- function decimalToRaw2(decimal, decimals) {
64153
- if (!/^\d+(\.\d+)?$/.test(decimal))
66430
+ function decimalToRaw2(decimal2, decimals) {
66431
+ if (!/^\d+(\.\d+)?$/.test(decimal2))
64154
66432
  return null;
64155
- const [whole, frac = ""] = decimal.split(".");
66433
+ const [whole, frac = ""] = decimal2.split(".");
64156
66434
  if (frac.length > decimals)
64157
66435
  return null;
64158
66436
  return BigInt((whole ?? "0") + frac.padEnd(decimals, "0"));
@@ -64720,11 +66998,11 @@ function mapLegacyTeeEntry(entry, keyId, grant, createdAtFallback) {
64720
66998
  }
64721
66999
  function buildTeeMeta(entry, lifecycle, grant) {
64722
67000
  const source = entry.tee;
64723
- const network = source?.network ?? "solana-mainnet";
64724
- if (network !== "solana-mainnet") {
64725
- throw new VaultError("VAULT_INDEX_INVALID", `Legacy entry ${entry.address} names network ${String(network)}; only solana-mainnet migrates.`);
67001
+ const network2 = source?.network ?? "solana-mainnet";
67002
+ if (network2 !== "solana-mainnet") {
67003
+ throw new VaultError("VAULT_INDEX_INVALID", `Legacy entry ${entry.address} names network ${String(network2)}; only solana-mainnet migrates.`);
64726
67004
  }
64727
- const meta = { network, lifecycle };
67005
+ const meta = { network: network2, lifecycle };
64728
67006
  carryOptional(meta, source, [
64729
67007
  "vaultDestination",
64730
67008
  "boundKeyPrefix",
@@ -64736,7 +67014,7 @@ function buildTeeMeta(entry, lifecycle, grant) {
64736
67014
  "sweptAt"
64737
67015
  ]);
64738
67016
  if (lifecycle === "local-candidate") {
64739
- return { network, lifecycle };
67017
+ return { network: network2, lifecycle };
64740
67018
  }
64741
67019
  if (lifecycle === "enabled") {
64742
67020
  if (meta.vaultDestination === undefined) {
@@ -68148,7 +70426,7 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
68148
70426
  await commitVault(vault, { index: { hd, entries }, addKeys: derived.entries.map((entry) => entry.blob) }, ctx.deps);
68149
70427
  return { entries, hd };
68150
70428
  }
68151
- function teeFieldsFor(row, ctx, network = "solana-mainnet") {
70429
+ function teeFieldsFor(row, ctx, network2 = "solana-mainnet") {
68152
70430
  const remoteState = row.sweptAt !== undefined ? "swept" : row.revokedAt !== undefined ? "quarantined" : "enabled";
68153
70431
  const grantIdentity = {
68154
70432
  account: "",
@@ -68156,7 +70434,7 @@ function teeFieldsFor(row, ctx, network = "solana-mainnet") {
68156
70434
  source: "recorded-at-operation"
68157
70435
  };
68158
70436
  const common = {
68159
- network,
70437
+ network: network2,
68160
70438
  ...row.vaultDestination !== undefined ? { vaultDestination: row.vaultDestination } : {},
68161
70439
  ...row.boundKeyPrefix !== undefined ? { boundKeyPrefix: row.boundKeyPrefix } : {},
68162
70440
  ...row.remoteAuthority !== undefined ? { remoteAuthority: row.remoteAuthority } : {},
@@ -69143,7 +71421,7 @@ function solOf(lamports) {
69143
71421
  const digits = lamports.padStart(10, "0");
69144
71422
  return `${digits.slice(0, -9)}.${digits.slice(-9)}`.replace(/\.?0+$/, "");
69145
71423
  }
69146
- function previewLines(preview) {
71424
+ function previewLines2(preview) {
69147
71425
  const lines = [
69148
71426
  `Close ${preview.accountCount} empty token account${preview.accountCount === 1 ? "" : "s"} on the embedded wallet ${safeText(preview.wallet)}`,
69149
71427
  `Rent returned to that same wallet: ${safeText(preview.totalSol)} SOL in ${preview.transactions} transaction${preview.transactions === 1 ? "" : "s"}, network fee about ${solOf(preview.estimatedFeeLamports)} SOL, net ${safeText(preview.netSol)} SOL`
@@ -69223,7 +71501,7 @@ async function walletsCloseEmpty(args, ctx) {
69223
71501
  const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
69224
71502
  output.write(`No empty token accounts to close on the embedded wallet ${safeText(wallet)}.
69225
71503
  `);
69226
- for (const line of previewLines(preview).slice(2))
71504
+ for (const line of previewLines2(preview).slice(2))
69227
71505
  output.write(`${line}
69228
71506
  `);
69229
71507
  return printTradingResult(ctx, {
@@ -69233,7 +71511,7 @@ async function walletsCloseEmpty(args, ctx) {
69233
71511
  preview
69234
71512
  });
69235
71513
  }
69236
- const confirmed = await confirmClose(ctx, previewLines(preview), parsed.booleans.has("--yes"));
71514
+ const confirmed = await confirmClose(ctx, previewLines2(preview), parsed.booleans.has("--yes"));
69237
71515
  if (!confirmed)
69238
71516
  return printTradingResult(ctx, {
69239
71517
  success: true,
@@ -69326,7 +71604,7 @@ function trustTable(rows) {
69326
71604
  }
69327
71605
  async function setTrust(args, ctx, trusted) {
69328
71606
  const { deps, apiUrl, json } = ctx;
69329
- const usage5 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
71607
+ const usage6 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
69330
71608
  const command = trusted ? "wallets trust" : "wallets untrust";
69331
71609
  const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
69332
71610
  if ("error" in parsed) {
@@ -69334,7 +71612,7 @@ async function setTrust(args, ctx, trusted) {
69334
71612
  return 2;
69335
71613
  }
69336
71614
  if (parsed.positionals.length === 0) {
69337
- writeUsageFailure(deps, `Name at least one wallet. ${usage5}`, json);
71615
+ writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
69338
71616
  return 2;
69339
71617
  }
69340
71618
  const skipConfirm = !trusted && parsed.booleans.has("--yes");
@@ -69606,6 +71884,17 @@ var COMMANDS = {
69606
71884
  pnl: { bare: pnl },
69607
71885
  portfolio: { bare: portfolio },
69608
71886
  lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
71887
+ perps: {
71888
+ subcommands: {
71889
+ setup: perpsSetup,
71890
+ open: perpsOpen,
71891
+ close: perpsClose,
71892
+ cancel: perpsCancel,
71893
+ orders: perpsOrders,
71894
+ positions: perpsPositions,
71895
+ leverage: perpsLeverage
71896
+ }
71897
+ },
69609
71898
  auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
69610
71899
  keys: {
69611
71900
  subcommands: {
@@ -69677,7 +71966,7 @@ var COMMANDS = {
69677
71966
  }
69678
71967
  },
69679
71968
  external: { subcommands: { new: externalNew, list: externalList, sweep: externalSweep } },
69680
- sign: { subcommands: { message: signMessage2 }, bare: sign3 },
71969
+ sign: { subcommands: { message: signMessage2 }, bare: sign4 },
69681
71970
  secrets: { subcommands: { set: secretsSet, list: secretsList, remove: secretsRemove } },
69682
71971
  plugins: { bare: plugins },
69683
71972
  doctor: { bare: doctor },
@@ -69981,8 +72270,8 @@ async function buildRealDeps() {
69981
72270
  const { runStdioServer: runStdioServer2 } = await Promise.resolve().then(() => (init_server2(), exports_server));
69982
72271
  await runStdioServer2(env);
69983
72272
  },
69984
- readFile: (path) => readFile9(path, "utf8"),
69985
- readBytes: (path) => readFile9(path),
72273
+ readFile: (path) => readFile10(path, "utf8"),
72274
+ readBytes: (path) => readFile10(path),
69986
72275
  readStdin: () => new Promise((resolve4, reject) => {
69987
72276
  const chunks = [];
69988
72277
  process.stdin.on("data", (chunk) => chunks.push(chunk));