@candledottv/cli 0.11.6 → 0.11.7

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 +12 -1
  2. package/dist/index.js +2428 -184
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -332,6 +332,9 @@ var init_agent_key_access = __esm(() => {
332
332
  function formatScopesForSummary(scopes) {
333
333
  return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope).join(", ");
334
334
  }
335
+ function terminalText(value) {
336
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
337
+ }
335
338
  function renderTable(headers, rows) {
336
339
  const widths = headers.map((header, col) => Math.max(header.length, ...rows.map((row) => (row[col] ?? "").length)));
337
340
  const line = (cells) => cells.map((cell, col) => col === cells.length - 1 ? cell ?? "" : (cell ?? "").padEnd(widths[col] ?? 0)).join(" ");
@@ -3140,7 +3143,8 @@ var init_errors = __esm(() => {
3140
3143
  "WALLET_LIMIT_REACHED",
3141
3144
  "TIER_REQUIRED",
3142
3145
  "LINKED_WALLET_ROOM_UNREADABLE",
3143
- "PROMOTE_ACCOUNT_UNRESOLVED"
3146
+ "PROMOTE_ACCOUNT_UNRESOLVED",
3147
+ "TRANSFER_SWEEP_PENDING"
3144
3148
  ];
3145
3149
  VaultError = class VaultError extends Error {
3146
3150
  code;
@@ -8344,6 +8348,23 @@ function retryAfterMs(header, now) {
8344
8348
  return;
8345
8349
  return Math.max(0, at - now);
8346
8350
  }
8351
+ function readProgramAccounts(method, entries, sliced) {
8352
+ const pubkeys = entries.map((entry) => {
8353
+ const pubkey = entry?.pubkey;
8354
+ if (typeof pubkey !== "string")
8355
+ throw new SolanaRpcError(`RPC ${method} answered an account without a pubkey`);
8356
+ return pubkey;
8357
+ });
8358
+ if (!sliced)
8359
+ return pubkeys;
8360
+ return entries.map((entry, i) => {
8361
+ const data = entry.account?.data;
8362
+ const encoded = Array.isArray(data) && data[1] === "base64" ? data[0] : undefined;
8363
+ if (typeof encoded !== "string")
8364
+ throw new SolanaRpcError(`RPC ${method} answered an account without base64 data`);
8365
+ return { pubkey: pubkeys[i], data: new Uint8Array(Buffer.from(encoded, "base64")) };
8366
+ });
8367
+ }
8347
8368
  function rawAccountView(value, address) {
8348
8369
  if (!value)
8349
8370
  return null;
@@ -8488,28 +8509,31 @@ function createSolanaRpc(url, fetchFn) {
8488
8509
  const r = await call("getSignaturesForAddress", [address, { limit: 1 }]);
8489
8510
  return Array.isArray(r) && r.length > 0;
8490
8511
  },
8491
- async isBlockhashValid(blockhash) {
8492
- const r = await call("isBlockhashValid", [blockhash, { commitment: "finalized" }]);
8512
+ async isBlockhashValid(blockhash, commitment = "finalized") {
8513
+ const r = await call("isBlockhashValid", [blockhash, { commitment }]);
8493
8514
  if (typeof r?.value !== "boolean")
8494
8515
  throw new Error("isBlockhashValid answered with a non-boolean value");
8495
8516
  return r.value;
8496
8517
  },
8497
- async getProgramAccounts(programId, filters, opts) {
8498
- const r = await call("getProgramAccounts", [programId, { encoding: "base64", commitment: "finalized", dataSlice: { offset: 0, length: 0 }, filters }], opts?.signal);
8518
+ getProgramAccounts: async (programId, filters, opts) => {
8519
+ const r = await call("getProgramAccounts", [
8520
+ programId,
8521
+ {
8522
+ encoding: "base64",
8523
+ commitment: "finalized",
8524
+ dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
8525
+ filters
8526
+ }
8527
+ ], opts?.signal);
8499
8528
  if (!Array.isArray(r))
8500
8529
  throw new SolanaRpcError("RPC getProgramAccounts answered without an account list");
8501
- return r.map((entry) => {
8502
- if (typeof entry?.pubkey !== "string") {
8503
- throw new SolanaRpcError("RPC getProgramAccounts answered an account without a pubkey");
8504
- }
8505
- return entry.pubkey;
8506
- });
8530
+ return readProgramAccounts("getProgramAccounts", r, opts?.dataSlice !== undefined);
8507
8531
  },
8508
- async getProgramAccountsV2(programId, filters, opts) {
8532
+ getProgramAccountsV2: async (programId, filters, opts) => {
8509
8533
  const config = {
8510
8534
  encoding: "base64",
8511
8535
  commitment: "finalized",
8512
- dataSlice: { offset: 0, length: 0 },
8536
+ dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
8513
8537
  filters,
8514
8538
  limit: opts?.limit ?? PROGRAM_ACCOUNTS_V2_LIMIT
8515
8539
  };
@@ -8520,19 +8544,17 @@ function createSolanaRpc(url, fetchFn) {
8520
8544
  throw new SolanaRpcError("RPC getProgramAccountsV2 answered without an account list");
8521
8545
  }
8522
8546
  const page = r;
8523
- const pubkeys = page.accounts.map((entry) => {
8524
- if (typeof entry?.pubkey !== "string") {
8525
- throw new SolanaRpcError("RPC getProgramAccountsV2 answered an account without a pubkey");
8526
- }
8527
- return entry.pubkey;
8528
- });
8547
+ const sliced = opts?.dataSlice !== undefined;
8548
+ const accounts = readProgramAccounts("getProgramAccountsV2", page.accounts, sliced);
8529
8549
  const cursor = page.paginationKey;
8530
- if (cursor === null || cursor === undefined)
8531
- return { pubkeys, paginationKey: null };
8532
- if (typeof cursor !== "string" || cursor.length === 0) {
8533
- throw new SolanaRpcError("RPC getProgramAccountsV2 answered a paginationKey that is not a string");
8550
+ let paginationKey = null;
8551
+ if (cursor !== null && cursor !== undefined) {
8552
+ if (typeof cursor !== "string" || cursor.length === 0) {
8553
+ throw new SolanaRpcError("RPC getProgramAccountsV2 answered a paginationKey that is not a string");
8554
+ }
8555
+ paginationKey = cursor;
8534
8556
  }
8535
- return { pubkeys, paginationKey: cursor };
8557
+ return sliced ? { accounts, paginationKey } : { pubkeys: accounts, paginationKey };
8536
8558
  }
8537
8559
  };
8538
8560
  }
@@ -8837,13 +8859,13 @@ var init_keys = __esm(() => {
8837
8859
 
8838
8860
  // src/vault/signer-roles.ts
8839
8861
  function tokenMintFilters(key) {
8840
- return [{ dataSize: 82 }, memcmp(0, concat2(COPTION_SOME_U32, key))];
8862
+ return [{ dataSize: 82 }, memcmp(4, key)];
8841
8863
  }
8842
8864
  function tokenFreezeFilters(key) {
8843
8865
  return [{ dataSize: 82 }, memcmp(46, concat2(COPTION_SOME_U32, key))];
8844
8866
  }
8845
8867
  function token2022MintFilters(key) {
8846
- const authority = memcmp(0, concat2(COPTION_SOME_U32, key));
8868
+ const authority = memcmp(4, key);
8847
8869
  return [
8848
8870
  [{ dataSize: 82 }, authority],
8849
8871
  [memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
@@ -8856,6 +8878,9 @@ function token2022FreezeFilters(key) {
8856
8878
  [memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
8857
8879
  ];
8858
8880
  }
8881
+ function keepSetAuthority(hits) {
8882
+ return hits.filter((hit) => hit.data.length === COPTION_SOME_U32.length && hit.data.every((byte, i) => byte === COPTION_SOME_U32[i])).map((hit) => hit.pubkey);
8883
+ }
8859
8884
  function programUpgradeFilters(key) {
8860
8885
  return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat2(COPTION_SOME_U8, key))];
8861
8886
  }
@@ -8950,19 +8975,31 @@ async function readSignerRoles(rpc, addresses, opts) {
8950
8975
  const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(Object.assign(new Error("timed out"), { name: "TimeoutError" })), timeoutMs) : undefined;
8951
8976
  try {
8952
8977
  requests += 1;
8978
+ const tagSlice = task.group.tagSlice;
8953
8979
  if (task.via === "v2") {
8954
8980
  if (rpc.getProgramAccountsV2 === undefined) {
8955
8981
  throw new SolanaRpcError("RPC getProgramAccountsV2 failed: -32601 Method not found", {
8956
8982
  rpcCode: RPC_METHOD_NOT_FOUND
8957
8983
  });
8958
8984
  }
8959
- return await rpc.getProgramAccountsV2(task.group.programId, filters, {
8985
+ const pageOpts = {
8960
8986
  signal: controller.signal,
8961
8987
  ...task.paginationKey !== undefined ? { paginationKey: task.paginationKey } : {}
8962
- });
8988
+ };
8989
+ if (tagSlice === undefined)
8990
+ return await rpc.getProgramAccountsV2(task.group.programId, filters, pageOpts);
8991
+ const page = await rpc.getProgramAccountsV2(task.group.programId, filters, { ...pageOpts, dataSlice: tagSlice });
8992
+ return { pubkeys: keepSetAuthority(page.accounts), paginationKey: page.paginationKey };
8963
8993
  }
8964
- const pubkeys = await rpc.getProgramAccounts(task.group.programId, filters, { signal: controller.signal });
8965
- return { pubkeys, paginationKey: null };
8994
+ if (tagSlice === undefined) {
8995
+ const pubkeys = await rpc.getProgramAccounts(task.group.programId, filters, { signal: controller.signal });
8996
+ return { pubkeys, paginationKey: null };
8997
+ }
8998
+ const hits = await rpc.getProgramAccounts(task.group.programId, filters, {
8999
+ signal: controller.signal,
9000
+ dataSlice: tagSlice
9001
+ });
9002
+ return { pubkeys: keepSetAuthority(hits), paginationKey: null };
8966
9003
  } finally {
8967
9004
  if (timer !== undefined)
8968
9005
  clearTimeout(timer);
@@ -9205,7 +9242,7 @@ var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", S
9205
9242
  return out;
9206
9243
  }, COPTION_SOME_U32, COPTION_SOME_U8, LOADER_PROGRAM_DATA_TAG, LOADER_PROGRAM_TAG, TOKEN_2022_MINT_ACCOUNT_TYPE, memcmp = (offset, bytes) => ({
9207
9244
  memcmp: { offset, bytes: b642(bytes), encoding: "base64" }
9208
- }), ROLE_GROUPS, PROGRAM_LABELS, NOT_CHECKED_LINE = "Not checked: multisig membership, Token-2022 extension authorities, metadata update authority.", thousands = (n) => n.toLocaleString("en-US");
9245
+ }), MINT_AUTHORITY_TAG_SLICE, ROLE_GROUPS, PROGRAM_LABELS, NOT_CHECKED_LINE = "Not checked: multisig membership, Token-2022 extension authorities, metadata update authority.", thousands = (n) => n.toLocaleString("en-US");
9209
9246
  var init_signer_roles = __esm(() => {
9210
9247
  init_esm();
9211
9248
  init_solana_lite();
@@ -9229,6 +9266,7 @@ var init_signer_roles = __esm(() => {
9229
9266
  LOADER_PROGRAM_DATA_TAG = new Uint8Array([3, 0, 0, 0]);
9230
9267
  LOADER_PROGRAM_TAG = new Uint8Array([2, 0, 0, 0]);
9231
9268
  TOKEN_2022_MINT_ACCOUNT_TYPE = new Uint8Array([1]);
9269
+ MINT_AUTHORITY_TAG_SLICE = { offset: 0, length: 4 };
9232
9270
  ROLE_GROUPS = [
9233
9271
  {
9234
9272
  id: "token-mint",
@@ -9236,7 +9274,8 @@ var init_signer_roles = __esm(() => {
9236
9274
  program: "token",
9237
9275
  programId: TOKEN_PROGRAM_ID,
9238
9276
  label: "token mint",
9239
- shapes: (key) => [tokenMintFilters(key)]
9277
+ shapes: (key) => [tokenMintFilters(key)],
9278
+ tagSlice: MINT_AUTHORITY_TAG_SLICE
9240
9279
  },
9241
9280
  {
9242
9281
  id: "token-freeze",
@@ -9252,7 +9291,8 @@ var init_signer_roles = __esm(() => {
9252
9291
  program: "token-2022",
9253
9292
  programId: TOKEN_2022_PROGRAM_ID,
9254
9293
  label: "Token-2022 mint",
9255
- shapes: token2022MintFilters
9294
+ shapes: token2022MintFilters,
9295
+ tagSlice: MINT_AUTHORITY_TAG_SLICE
9256
9296
  },
9257
9297
  {
9258
9298
  id: "token2022-freeze",
@@ -9298,6 +9338,7 @@ var init_signer_roles = __esm(() => {
9298
9338
  // src/vault/promote-support.ts
9299
9339
  var exports_promote_support = {};
9300
9340
  __export(exports_promote_support, {
9341
+ withToKey: () => withToKey,
9301
9342
  subjectPhrase: () => subjectPhrase,
9302
9343
  shortAddress: () => shortAddress,
9303
9344
  runRoleCheck: () => runRoleCheck,
@@ -9307,6 +9348,7 @@ __export(exports_promote_support, {
9307
9348
  promoteSentence: () => promoteSentence,
9308
9349
  printPromoteSentence: () => printPromoteSentence,
9309
9350
  findVaultRoleEntry: () => findVaultRoleEntry,
9351
+ findTransferSource: () => findTransferSource,
9310
9352
  findEntryByLabelOrAddress: () => findEntryByLabelOrAddress,
9311
9353
  controlledByJson: () => controlledByJson,
9312
9354
  confirmPrompt: () => confirmPrompt,
@@ -9352,6 +9394,10 @@ function findVaultRoleEntry(index, labelOrAddress) {
9352
9394
  return byLabel;
9353
9395
  return index.entries.find((entry) => entry.address === labelOrAddress);
9354
9396
  }
9397
+ function findTransferSource(index, labelOrAddress) {
9398
+ const byLabel = (role) => index.entries.find((entry) => entry.role === role && entry.label !== undefined && entry.label === labelOrAddress);
9399
+ return byLabel("vault") ?? byLabel("tee-wallet") ?? index.entries.find((entry) => entry.address === labelOrAddress);
9400
+ }
9355
9401
  function findEntryByLabelOrAddress(index, labelOrAddress) {
9356
9402
  const byLabel = index.entries.find((entry) => entry.label !== undefined && entry.label === labelOrAddress);
9357
9403
  if (byLabel !== undefined)
@@ -9472,6 +9518,9 @@ async function confirmPromotion(ctx, n) {
9472
9518
  function shortAddress(address) {
9473
9519
  return `${address.slice(0, 6)}…${address.slice(-4)}`;
9474
9520
  }
9521
+ function withToKey(controlledBy, toKey) {
9522
+ return { ...controlledBy, toKey };
9523
+ }
9475
9524
  function accountUnresolved(reason) {
9476
9525
  return new VaultError("PROMOTE_ACCOUNT_UNRESOLVED", `Could not confirm which Candle account this API key acts for (${reason}); nothing was written.`, {
9477
9526
  suggestion: "Check the key with: candle doctor. Promotion registers the keys to that account, so it does not proceed on a cached value."
@@ -9545,23 +9594,51 @@ function renderControlledBy(controlledBy, n) {
9545
9594
  const label = cleaned.length > 0 ? `(${cleaned}) ` : "";
9546
9595
  const environment = controlledBy.environment ?? "not a Candle host";
9547
9596
  const from = controlledBy.apiUrlFrom !== undefined ? `, from ${controlledBy.apiUrlFrom}` : "";
9597
+ const { toKey } = controlledBy;
9598
+ if (toKey === undefined) {
9599
+ return [
9600
+ `${subject} will be controlled by:`,
9601
+ ` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
9602
+ ` API key ${controlledBy.keyPrefix}… ${label}${source}`,
9603
+ ` API ${controlledBy.apiUrl} (${environment}${from})`
9604
+ ].join(`
9605
+ `);
9606
+ }
9607
+ const toLabel = toKey.label !== null && labelCell(toKey.label).length > 0 ? `(${labelCell(toKey.label)}) ` : "";
9548
9608
  return [
9549
9609
  `${subject} will be controlled by:`,
9550
9610
  ` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
9551
- ` API key ${controlledBy.keyPrefix}… ${label}${source}`,
9552
- ` API ${controlledBy.apiUrl} (${environment}${from})`
9611
+ ` API key ${toKey.keyPrefix}… ${toLabel}--to-key, bound by a rebind after the import`,
9612
+ ` imported under ${controlledBy.keyPrefix}… ${label}${source}`,
9613
+ ` API ${controlledBy.apiUrl} (${environment}${from})`,
9614
+ ...toKey.warnings
9553
9615
  ].join(`
9554
9616
  `);
9555
9617
  }
9556
9618
  function controlledByJson(controlledBy) {
9619
+ const importedUnder = {
9620
+ keyPrefix: controlledBy.keyPrefix,
9621
+ keyLabel: controlledBy.keyLabel,
9622
+ keySource: controlledBy.keySource
9623
+ };
9624
+ if (controlledBy.toKey === undefined) {
9625
+ return {
9626
+ account: controlledBy.account,
9627
+ username: controlledBy.username,
9628
+ ...importedUnder,
9629
+ apiUrl: controlledBy.apiUrl,
9630
+ environment: controlledBy.environment
9631
+ };
9632
+ }
9557
9633
  return {
9558
9634
  account: controlledBy.account,
9559
9635
  username: controlledBy.username,
9560
- keyPrefix: controlledBy.keyPrefix,
9561
- keyLabel: controlledBy.keyLabel,
9562
- keySource: controlledBy.keySource,
9636
+ keyPrefix: controlledBy.toKey.keyPrefix,
9637
+ keyLabel: controlledBy.toKey.label,
9638
+ keySource: "--to-key",
9563
9639
  apiUrl: controlledBy.apiUrl,
9564
- environment: controlledBy.environment
9640
+ environment: controlledBy.environment,
9641
+ importedUnder
9565
9642
  };
9566
9643
  }
9567
9644
  async function runRoleCheck(ctx, rpc, addresses) {
@@ -13625,14 +13702,14 @@ async function request(ctx, key, path, body) {
13625
13702
  throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
13626
13703
  return result.body;
13627
13704
  }
13628
- async function teeWallets(ctx, key, scope) {
13705
+ async function listTradingWallets(ctx, key, scope) {
13629
13706
  let cursor;
13630
13707
  const rows = [];
13631
13708
  const cursors = new Set;
13632
13709
  let appId = "";
13633
13710
  for (;; ) {
13634
13711
  const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
13635
- if (!response.scopes?.includes(scope))
13712
+ if (scope !== undefined && !response.scopes?.includes(scope))
13636
13713
  throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
13637
13714
  appId = response.privyAppId ?? "";
13638
13715
  if (!Array.isArray(response.page))
@@ -13675,7 +13752,7 @@ async function completeTradingWallet(ctx, row, appId, scope) {
13675
13752
  };
13676
13753
  }
13677
13754
  async function tradingWallet(ctx, key, name, scope) {
13678
- const { rows, appId } = await teeWallets(ctx, key, scope);
13755
+ const { rows, appId } = await listTradingWallets(ctx, key, scope);
13679
13756
  const matches = rows.filter((row) => matchesName(row, name));
13680
13757
  if (matches.length !== 1) {
13681
13758
  throw new TradingError("TEE_WALLET_REQUIRED", matches.length > 1 ? `"${name}" matches ${matches.length} TEE wallets on this key: ${teeWalletList(matches)}. Name one by id or address.` : rows.length === 0 ? "This key has no TEE wallets bound to it. Enrol one, or select the profile whose key holds it." : `No TEE wallet on this key is called "${name}". Bound to this key: ${teeWalletList(rows)}.`);
@@ -13684,7 +13761,7 @@ async function tradingWallet(ctx, key, name, scope) {
13684
13761
  }
13685
13762
  async function tradingPayer(ctx, key, name) {
13686
13763
  const scope = "swap:write";
13687
- const { rows, appId } = await teeWallets(ctx, key, scope);
13764
+ const { rows, appId } = await listTradingWallets(ctx, key, scope);
13688
13765
  const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
13689
13766
  const asEmbedded = () => ({ kind: "embedded", address: embedded });
13690
13767
  const options = [
@@ -13808,7 +13885,7 @@ Minimum received: ${safeText(quote.minimumReceived)}
13808
13885
  throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary trade prompt.");
13809
13886
  return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
13810
13887
  }
13811
- async function saveLaunchSignature(ctx, key, id, transaction) {
13888
+ async function saveOperationSignature(ctx, key, id, kind, transaction) {
13812
13889
  const bytes = Buffer.from(transaction, "base64");
13813
13890
  let offset = 0;
13814
13891
  let count = 0;
@@ -13825,7 +13902,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
13825
13902
  const signature = base58.encode(bytes.subarray(offset, offset + 64));
13826
13903
  const path = operationPath(ctx, key, id);
13827
13904
  const temporary = `${path}.${process.pid}.tmp`;
13828
- await writeFile4(temporary, JSON.stringify({ id, kind: "launch", signature }), { mode: 384 });
13905
+ await writeFile4(temporary, JSON.stringify({ id, kind, signature }), { mode: 384 });
13829
13906
  const file = await open3(temporary, "r");
13830
13907
  try {
13831
13908
  await file.sync();
@@ -13835,7 +13912,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
13835
13912
  await rename3(temporary, path);
13836
13913
  return signature;
13837
13914
  }
13838
- var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, BASES;
13915
+ var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, lpAmountSchema, lpBuildSchema, lpPositionsSchema, lpPoolsSchema, BASES;
13839
13916
  var init_trading = __esm(() => {
13840
13917
  init_esm();
13841
13918
  init_zod();
@@ -13897,6 +13974,51 @@ var init_trading = __esm(() => {
13897
13974
  wallets: exports_external.object({ solana: exports_external.object({ address: exports_external.string() }).passthrough().nullable().optional() }).passthrough().optional()
13898
13975
  }).passthrough();
13899
13976
  operationSchema = exports_external.object({ job: exports_external.object({ status: exports_external.string() }).passthrough() }).passthrough();
13977
+ lpAmountSchema = exports_external.object({ mint: exports_external.string(), raw: exports_external.string().regex(/^\d+$/), decimals: exports_external.number().int() });
13978
+ lpBuildSchema = exports_external.object({
13979
+ build: exports_external.object({
13980
+ action: exports_external.enum(["add", "remove", "claim"]),
13981
+ pool: exports_external.string(),
13982
+ position: exports_external.string(),
13983
+ transaction: exports_external.string().min(1),
13984
+ walletAddress: exports_external.string(),
13985
+ signature: exports_external.string().optional()
13986
+ }).passthrough(),
13987
+ preview: exports_external.object({
13988
+ amounts: exports_external.array(lpAmountSchema).default([]),
13989
+ tokenRisks: risksSchema.default([]),
13990
+ warnings: exports_external.array(exports_external.string()).default([]),
13991
+ candleFeeBps: exports_external.number().optional()
13992
+ }).passthrough().nullable(),
13993
+ replay: exports_external.boolean().optional()
13994
+ });
13995
+ lpPositionsSchema = exports_external.object({
13996
+ positions: exports_external.array(exports_external.object({
13997
+ position: exports_external.string(),
13998
+ pool: exports_external.string(),
13999
+ tokens: exports_external.array(exports_external.object({
14000
+ mint: exports_external.string(),
14001
+ decimals: exports_external.number().int(),
14002
+ amountRaw: exports_external.string(),
14003
+ unclaimedFeesRaw: exports_external.string(),
14004
+ valueUsd: exports_external.number().nullable().optional()
14005
+ }).passthrough()),
14006
+ poolShare: exports_external.number().optional(),
14007
+ valueUsd: exports_external.number().nullable().optional()
14008
+ }).passthrough())
14009
+ });
14010
+ lpPoolsSchema = exports_external.object({
14011
+ page: exports_external.number().int(),
14012
+ pages: exports_external.number().int(),
14013
+ pools: exports_external.array(exports_external.object({
14014
+ pool: exports_external.string(),
14015
+ tokens: exports_external.array(exports_external.string()),
14016
+ liquidityUsd: exports_external.number().nullable().optional(),
14017
+ baseFeePercent: exports_external.number().nullable().optional(),
14018
+ volume24hUsd: exports_external.number().nullable().optional(),
14019
+ estimatedAprPercent: exports_external.number().nullable().optional()
14020
+ }).passthrough())
14021
+ });
13900
14022
  BASES = {
13901
14023
  SOL: { mint: "So11111111111111111111111111111111111111112", decimals: 9 },
13902
14024
  USDC: { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
@@ -37604,7 +37726,7 @@ var init_update_notice = __esm(() => {
37604
37726
  });
37605
37727
 
37606
37728
  // ../mcp/src/orchestrate.ts
37607
- import { randomUUID as randomUUID3 } from "node:crypto";
37729
+ import { randomUUID as randomUUID4 } from "node:crypto";
37608
37730
  function requireApiKey(cfg) {
37609
37731
  if (!cfg.apiKey) {
37610
37732
  throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
@@ -37716,7 +37838,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
37716
37838
  }
37717
37839
  async function executeTrade(args, cfg, doFetch) {
37718
37840
  const apiKey = requireApiKey(cfg);
37719
- const clientTradeId = args.clientTradeId ?? randomUUID3();
37841
+ const clientTradeId = args.clientTradeId ?? randomUUID4();
37720
37842
  if (args.amount !== undefined && args.percent !== undefined) {
37721
37843
  return errText("pass exactly one of amount or percent, not both", { clientTradeId });
37722
37844
  }
@@ -37854,7 +37976,7 @@ async function executeTrade(args, cfg, doFetch) {
37854
37976
  }
37855
37977
  async function executeLaunchAndSeed(args, cfg, doFetch) {
37856
37978
  const apiKey = requireApiKey(cfg);
37857
- const clientLaunchId = args.clientLaunchId ?? randomUUID3();
37979
+ const clientLaunchId = args.clientLaunchId ?? randomUUID4();
37858
37980
  const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...launchFields } = args;
37859
37981
  let buyAmount;
37860
37982
  if (devBuy !== undefined) {
@@ -38263,8 +38385,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
38263
38385
  inputSchema: setProfileWalletsShape
38264
38386
  }, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
38265
38387
  register("candle_get_profile_pnl", {
38266
- title: "Read an agent profile's realized P&L",
38267
- description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their COST BASIS -- not their current value, which is not marked " + "here. Deposits, withdrawals and transfers are excluded: funding a wallet is not profit. Check " + "`unvalued` and `truncated` before quoting the number; they mean the total is partial. Reads only.",
38388
+ title: "Read an agent profile's P&L",
38389
+ description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their cost basis, each MARKED at Candle's current price where one " + "exists: `markPriceUsd`, `marketValueUsd` and `unrealizedUsd` per position, and `unrealizedUsd` " + "overall. A position with no price is counted in `unmarkedPositions` and left out of unrealized, " + "never valued at zero; `oldestMarkAt` says how old the marks are. Deposits, withdrawals and " + "transfers are excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before " + "quoting the number; they mean the total is partial. Reads only.",
38268
38390
  inputSchema: profilePnlShape
38269
38391
  }, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
38270
38392
  register("candle_get_profile_trades", {
@@ -38593,7 +38715,7 @@ init_render();
38593
38715
  init_secret_store();
38594
38716
 
38595
38717
  // src/version.ts
38596
- var CLI_VERSION = "0.11.6";
38718
+ var CLI_VERSION = "0.11.7";
38597
38719
 
38598
38720
  // src/commands/auth.ts
38599
38721
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -39092,6 +39214,45 @@ var HELP = {
39092
39214
  ],
39093
39215
  env: ENV_API
39094
39216
  },
39217
+ lp: {
39218
+ group: "Trade",
39219
+ summary: "Meteora DAMM v2 liquidity from a TEE wallet: pools, add, positions, remove, claim",
39220
+ description: "Provide liquidity on any Meteora DAMM v2 pool from a TEE wallet, through its bound key (scope lp:write, opt-in). Every write is quoted, shown with the pool's Token-2022 warnings, confirmed, signed by Candle's relay and broadcast over your RPC; no Candle fee. A sweep closes open positions itself (tee sweep).",
39221
+ usage: ["candle lp <subcommand> [flags]"],
39222
+ rows: [
39223
+ {
39224
+ invocation: "pools <token> [--page <n>]",
39225
+ description: "Pools listing a token: liquidity, fee, 24h volume, estimated yield"
39226
+ },
39227
+ {
39228
+ invocation: "add <pool> --amount <n> <token> --wallet <tee> [--position <nft>]",
39229
+ description: "Open a position, or add to one; the other side follows the pool's ratio"
39230
+ },
39231
+ { invocation: "positions [--wallet <tee>]", description: "Every position across your TEE wallets" },
39232
+ {
39233
+ invocation: "remove <position> --percent <n> [--wallet <tee>]",
39234
+ description: "Withdraw; at 100% also claims fees and closes the position"
39235
+ },
39236
+ { invocation: "claim <position> [--wallet <tee>]", description: "Claim fees and any rewards" }
39237
+ ],
39238
+ flags: [
39239
+ { invocation: "--wallet <tee>", description: "The TEE wallet by id, address or unique label" },
39240
+ { invocation: "--rpc-url <url>", description: "Your Solana RPC, for mint reads and the broadcast" },
39241
+ {
39242
+ invocation: "--client-trade-id <id>",
39243
+ description: "Idempotency: the same id never deposits or withdraws twice"
39244
+ },
39245
+ { invocation: "--slippage-bps <n>", description: "add and remove: the tolerance, 0 to 1000 (default 100)" },
39246
+ { invocation: "--yes", description: "Skip the confirmation prompt (an ordinary prompt only)" }
39247
+ ],
39248
+ examples: [
39249
+ "candle lp pools So11111111111111111111111111111111111111112",
39250
+ "candle lp add <pool> --amount 0.5 SOL --wallet AgentOne --rpc-url https://<rpc>",
39251
+ "candle lp positions",
39252
+ "candle lp remove <position> --percent 100 --wallet AgentOne --rpc-url https://<rpc>"
39253
+ ],
39254
+ env: ENV_API
39255
+ },
39095
39256
  launch: {
39096
39257
  group: "Trade",
39097
39258
  summary: "Create a Solana token (the first buy is a separate swap)",
@@ -39107,6 +39268,31 @@ var HELP = {
39107
39268
  examples: ["candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne"],
39108
39269
  env: ENV_API
39109
39270
  },
39271
+ portfolio: {
39272
+ group: "Account",
39273
+ summary: "Every wallet's holdings, prices and value in one table (vault read over your own RPC)",
39274
+ 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.",
39275
+ usage: ["candle portfolio [--rpc-url <url>] [--json]"],
39276
+ rows: [],
39277
+ flags: [
39278
+ {
39279
+ invocation: "--rpc-url <url>",
39280
+ description: "Your own Solana RPC, for the vault. Without it (or CANDLE_SOLANA_RPC_URL) the vault is not read"
39281
+ },
39282
+ KEYSTORE_FLAG
39283
+ ],
39284
+ examples: ["candle portfolio", "candle portfolio --rpc-url https://your-rpc.example --json"],
39285
+ env: [...ENV_API, ...ENV_LOCAL_SIGNING]
39286
+ },
39287
+ pnl: {
39288
+ group: "Account",
39289
+ summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's own)",
39290
+ 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.",
39291
+ usage: ["candle pnl [--profile <name>] [--json]"],
39292
+ rows: [],
39293
+ examples: ["candle pnl", "candle pnl --profile scalper --json"],
39294
+ env: ENV_API
39295
+ },
39110
39296
  keys: {
39111
39297
  group: "Account",
39112
39298
  summary: "API keys, and the wallets each key may use",
@@ -39228,14 +39414,14 @@ var HELP = {
39228
39414
  },
39229
39415
  {
39230
39416
  invocation: "transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>",
39231
- description: "Sign a vault-key transfer locally"
39417
+ description: "Sign a transfer locally from a vault key or a promoted TEE wallet"
39232
39418
  },
39233
39419
  {
39234
- invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>]",
39420
+ invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
39235
39421
  description: "Fresh TEE key, or promote one vault key in place. Reads, over your RPC, whether each key is a token mint, freeze, program upgrade or stake authority (9 requests per key; public endpoints refuse the token scans). Multisig membership is not checked."
39236
39422
  },
39237
39423
  {
39238
- invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings]",
39424
+ invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--to-key <prefix|label>] [--token-holdings]",
39239
39425
  description: "Promote many vault keys in place: one unlock, one reviewed acknowledgement. Reads, over your RPC, whether each key is a token mint, freeze, program upgrade or stake authority (9 requests per key; public endpoints refuse the token scans). Multisig membership is not checked."
39240
39426
  },
39241
39427
  {
@@ -39269,6 +39455,10 @@ var HELP = {
39269
39455
  {
39270
39456
  invocation: "--pairs-from <file>",
39271
39457
  description: "promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
39458
+ },
39459
+ {
39460
+ invocation: "--to-key <prefix|label>",
39461
+ description: "promote, promote-batch: bind the promoted wallets to this key instead of the calling one. The import runs under the calling key as before, then the wallets are moved to the named key by a rebind (device token; the target's secret never touches this machine). The target is checked before the unlock; a wallet whose rebind fails is named as still on the calling key, with the tee rebind command that finishes."
39272
39462
  }
39273
39463
  ],
39274
39464
  examples: [
@@ -39278,6 +39468,7 @@ var HELP = {
39278
39468
  "candle vault rename key-7 treasury-cold",
39279
39469
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
39280
39470
  "candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc>",
39471
+ "candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc> --to-key tr-01",
39281
39472
  "candle vault enroll security-key --label yubikey-a",
39282
39473
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
39283
39474
  "candle vault backup --to icloud",
@@ -39310,7 +39501,7 @@ var HELP = {
39310
39501
  },
39311
39502
  {
39312
39503
  invocation: "sweep <address> --rpc-url <url> [--emergency]",
39313
- description: "Sign locally and move everything to the pinned vault"
39504
+ description: "Sign locally and move everything to the pinned vault; closes DAMM v2 LP positions after verifying each server-built close (--emergency moves the position NFT instead, with no API)"
39314
39505
  },
39315
39506
  {
39316
39507
  invocation: "rebind <wallet...> --to-key <prefix|label>",
@@ -43820,7 +44011,7 @@ function validClientId(id) {
43820
44011
  }
43821
44012
  async function lookupOperation(ctx, key, id, kind) {
43822
44013
  const local = await savedOperation(ctx, key, id);
43823
- const kinds = kind ? [kind] : local ? [local.kind] : ["trade", "swap", "launch"];
44014
+ const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
43824
44015
  const found = [];
43825
44016
  for (const candidate of kinds) {
43826
44017
  try {
@@ -44095,7 +44286,7 @@ async function launch(args, ctx) {
44095
44286
  if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
44096
44287
  throw new TradingError("QUOTE_EXPIRED", "The launch build expired before signing.");
44097
44288
  const signed = await relaySign(ctx, key, wallet, built.transaction);
44098
- const signature = await saveLaunchSignature(ctx, key, id, signed);
44289
+ const signature = await saveOperationSignature(ctx, key, id, "launch", signed);
44099
44290
  const broadcastSignature = await createSolanaRpc(url, ctx.deps.fetch).sendTransaction(signed);
44100
44291
  if (broadcastSignature !== signature)
44101
44292
  throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
@@ -44108,6 +44299,368 @@ async function launch(args, ctx) {
44108
44299
  }
44109
44300
  }
44110
44301
 
44302
+ // src/commands/lp.ts
44303
+ init_args();
44304
+ init_render();
44305
+ init_solana_lite();
44306
+ import { randomUUID as randomUUID3 } from "node:crypto";
44307
+ init_trading();
44308
+ var LP_SCOPE = "lp:write";
44309
+ var CONFIRM_POLL_MS2 = 2000;
44310
+ var CONFIRM_MAX_POLLS2 = 45;
44311
+ async function lpRequest(ctx, key, path, body) {
44312
+ const result = await apiRequest(path, {
44313
+ apiUrl: ctx.apiUrl,
44314
+ credentials: { apiKey: key },
44315
+ auth: "key",
44316
+ method: body ? "POST" : "GET",
44317
+ body,
44318
+ fetch: ctx.deps.fetch,
44319
+ env: ctx.deps.env
44320
+ });
44321
+ if (!result.ok) {
44322
+ if (result.status === 404 && !result.code)
44323
+ throw new TradingError("LP_NOT_ENABLED", "This Candle deployment does not serve LP routes (LP_ENABLED is off there, or the API predates them).");
44324
+ throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
44325
+ }
44326
+ if (!result.body || typeof result.body !== "object")
44327
+ throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
44328
+ return result.body;
44329
+ }
44330
+ function solanaAddress(value, what) {
44331
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
44332
+ throw new TradingError("INVALID_ADDRESS", `${what} must be a Solana address (base58).`);
44333
+ return value;
44334
+ }
44335
+ function poolToken(value) {
44336
+ const base = baseAsset(value);
44337
+ if (base)
44338
+ return BASES[base]?.mint;
44339
+ return solanaAddress(value, "The token");
44340
+ }
44341
+ function matchesWallet(row, name) {
44342
+ return row.id === name || row.address === name || row.label === name;
44343
+ }
44344
+ function usage2(ctx, line) {
44345
+ writeUsageFailure(ctx.deps, line, ctx.json);
44346
+ return 2;
44347
+ }
44348
+ function amountLine(amount) {
44349
+ const base = Object.entries(BASES).find(([, asset]) => asset.mint === amount.mint)?.[0];
44350
+ return `${decimalAmount(amount.raw, amount.decimals)} ${base ?? amount.mint}`;
44351
+ }
44352
+ async function lpPools(args, ctx) {
44353
+ const parsed = parseArgs(args, { valueFlags: ["--page"] });
44354
+ const page = Number(parsed && !("error" in parsed) ? parsed.values["--page"] ?? "1" : "1");
44355
+ if ("error" in parsed)
44356
+ return usage2(ctx, parsed.error);
44357
+ if (parsed.positionals.length !== 1 || !Number.isSafeInteger(page) || page < 1)
44358
+ return usage2(ctx, "Usage: candle lp pools <token> [--page <n>]");
44359
+ try {
44360
+ const token = poolToken(parsed.positionals[0]);
44361
+ const key = await tradingKey(ctx);
44362
+ const pools = lpPoolsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/pools/${encodeURIComponent(token)}?page=${page}`));
44363
+ if (ctx.json)
44364
+ return printTradingResult(ctx, { success: true, token, ...pools });
44365
+ const out = ctx.deps.stdout;
44366
+ if (pools.pools.length === 0) {
44367
+ out.write(`No DAMM v2 pool lists ${token} (page ${pools.page} of ${pools.pages}).
44368
+ `);
44369
+ return 0;
44370
+ }
44371
+ const usd = (value) => value === null || value === undefined ? "unavailable" : `$${value.toFixed(2)}`;
44372
+ const pct = (value) => value === null || value === undefined ? "unavailable" : `${value.toFixed(2)}%`;
44373
+ out.write(`DAMM v2 pools for ${token} (page ${pools.page} of ${pools.pages}):
44374
+ `);
44375
+ for (const pool of pools.pools) {
44376
+ out.write(` ${safeText(pool.pool)}
44377
+ pair ${pool.tokens.map(safeText).join(" / ")}
44378
+ liquidity ${usd(pool.liquidityUsd)}, fee ${pct(pool.baseFeePercent)}, 24h volume ${usd(pool.volume24hUsd)}, est. yield ${pct(pool.estimatedAprPercent)} APR (indexed, not a quote)
44379
+ `);
44380
+ }
44381
+ return 0;
44382
+ } catch (error) {
44383
+ return tradingFailure(ctx, error);
44384
+ }
44385
+ }
44386
+ async function lpPositions(args, ctx) {
44387
+ const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
44388
+ if ("error" in parsed)
44389
+ return usage2(ctx, parsed.error);
44390
+ if (parsed.positionals.length !== 0)
44391
+ return usage2(ctx, "Usage: candle lp positions [--wallet <tee>]");
44392
+ try {
44393
+ const key = await tradingKey(ctx);
44394
+ const { rows } = await listTradingWallets(ctx, key);
44395
+ const name = parsed.values["--wallet"];
44396
+ const wallets = rows.filter((row) => row.chain === "solana" && (name === undefined || matchesWallet(row, name)));
44397
+ if (name !== undefined && wallets.length === 0)
44398
+ throw new TradingError("TEE_WALLET_REQUIRED", `No TEE wallet on this key is called "${name}".`);
44399
+ const report = [];
44400
+ for (const row of wallets) {
44401
+ const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
44402
+ report.push({
44403
+ id: row.id,
44404
+ address: row.address,
44405
+ ...row.label ? { label: row.label } : {},
44406
+ positions: positions.positions
44407
+ });
44408
+ }
44409
+ if (ctx.json)
44410
+ return printTradingResult(ctx, { success: true, wallets: report });
44411
+ const out = ctx.deps.stdout;
44412
+ const total = report.reduce((n, wallet) => n + wallet.positions.length, 0);
44413
+ if (total === 0) {
44414
+ out.write(`No DAMM v2 positions across ${report.length} TEE wallet(s).
44415
+ `);
44416
+ return 0;
44417
+ }
44418
+ for (const wallet of report) {
44419
+ if (wallet.positions.length === 0)
44420
+ continue;
44421
+ out.write(`${wallet.label ? `${safeText(wallet.label)} ` : ""}(${wallet.id}, ${safeText(wallet.address)})
44422
+ `);
44423
+ for (const position of wallet.positions) {
44424
+ out.write(` position ${safeText(position.position)} in pool ${safeText(position.pool)}
44425
+ `);
44426
+ for (const token of position.tokens) {
44427
+ out.write(` ${amountLine({ mint: token.mint, raw: token.amountRaw, decimals: token.decimals })}, unclaimed fees ${decimalAmount(token.unclaimedFeesRaw, token.decimals)}${token.valueUsd === null || token.valueUsd === undefined ? "" : ` ($${token.valueUsd.toFixed(2)})`}
44428
+ `);
44429
+ }
44430
+ out.write(` value ${position.valueUsd === null || position.valueUsd === undefined ? "unpriced" : `$${position.valueUsd.toFixed(2)}`}${position.poolShare === undefined ? "" : `, ${(position.poolShare * 100).toFixed(4)}% of the pool`}
44431
+ `);
44432
+ }
44433
+ }
44434
+ return 0;
44435
+ } catch (error) {
44436
+ return tradingFailure(ctx, error);
44437
+ }
44438
+ }
44439
+ async function walletHolding(ctx, key, position, name) {
44440
+ if (name !== undefined)
44441
+ return tradingWallet(ctx, key, name, LP_SCOPE);
44442
+ const { rows, appId } = await listTradingWallets(ctx, key, LP_SCOPE);
44443
+ for (const row of rows) {
44444
+ if (row.chain !== "solana")
44445
+ continue;
44446
+ const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
44447
+ if (positions.positions.some((held) => held.position === position))
44448
+ return completeTradingWallet(ctx, row, appId, LP_SCOPE);
44449
+ }
44450
+ throw new TradingError("LP_POSITION_NOT_FOUND", `No TEE wallet bound to this key holds position ${position}. See candle lp positions, or name the wallet with --wallet.`);
44451
+ }
44452
+ async function waitConfirmed(ctx, rpc2, signature, id) {
44453
+ for (let i = 0;i < CONFIRM_MAX_POLLS2; i++) {
44454
+ const observed = classifyStatus(await rpc2.getSignatureStatus(signature));
44455
+ if (observed.kind === "finalized")
44456
+ return;
44457
+ if (observed.kind === "failed")
44458
+ throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}.`);
44459
+ if (observed.kind === "nonfinal") {
44460
+ if (observed.err !== null && observed.err !== undefined)
44461
+ throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed: ${JSON.stringify(observed.err)}.`);
44462
+ if (observed.confirmationStatus === "confirmed")
44463
+ return;
44464
+ }
44465
+ await ctx.deps.sleep(CONFIRM_POLL_MS2);
44466
+ }
44467
+ throw new TradingError("LP_CONFIRM_PENDING", `Transaction ${signature} was sent but not confirmed within ${CONFIRM_MAX_POLLS2 * CONFIRM_POLL_MS2 / 1000}s. Re-run the same command with --client-trade-id ${id}: it confirms the saved signature and sends nothing new.`);
44468
+ }
44469
+ async function confirmPreview(ctx, plan, built) {
44470
+ const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
44471
+ output.write(`${safeText(plan.intent)}
44472
+ Payer: ${safeText(plan.wallet.address)}
44473
+ `);
44474
+ output.write(`Pool: ${safeText(built.build.pool)}
44475
+ Position: ${safeText(built.build.position)}
44476
+ `);
44477
+ const preview = built.preview;
44478
+ const amounts = preview?.amounts ?? [];
44479
+ if (amounts.length > 0) {
44480
+ const verb = plan.action === "add" ? "Deposit (maximum, at the pool's ratio)" : plan.action === "remove" ? "Withdraw (minimum)" : "Claim";
44481
+ output.write(`${verb}: ${amounts.map(amountLine).join(" + ")}
44482
+ `);
44483
+ }
44484
+ output.write(`Candle LP fee: ${safeText(preview?.candleFeeBps ?? 0)} bps
44485
+ `);
44486
+ for (const warning of preview?.warnings ?? [])
44487
+ output.write(`Warning: ${safeText(warning)}
44488
+ `);
44489
+ for (const risk of preview?.tokenRisks ?? [])
44490
+ output.write(`Warning (${safeText(risk.mint)}): ${safeText(risk.message)}
44491
+ `);
44492
+ if (built.replay)
44493
+ output.write(`This client id was already built; the same artifact is shown again.
44494
+ `);
44495
+ if (plan.yes)
44496
+ return true;
44497
+ if (!ctx.deps.isTTY.stdin)
44498
+ throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary LP prompt.");
44499
+ return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
44500
+ }
44501
+ async function runLpOperation(ctx, plan) {
44502
+ const { id, key, wallet, action } = plan;
44503
+ const confirm = async (signature2, extra) => {
44504
+ const result = await lpRequest(ctx, key, "/api/v1/agent/lp/confirm", { clientTradeId: id, signature: signature2 });
44505
+ return printTradingResult(ctx, {
44506
+ ...result,
44507
+ clientTradeId: id,
44508
+ kind: "lp",
44509
+ action,
44510
+ signature: signature2,
44511
+ wallet: safeText(wallet.address),
44512
+ ...extra
44513
+ });
44514
+ };
44515
+ const local = await savedOperation(ctx, key, id);
44516
+ if (local?.signature)
44517
+ return confirm(local.signature, { resumed: true });
44518
+ if (!local && !await claimOperation(ctx, key, id, "lp"))
44519
+ throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
44520
+ ctx.deps.stderr.write(`Operation: ${id}
44521
+ `);
44522
+ const built = lpBuildSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/${action}/build`, {
44523
+ linkedWalletId: wallet.id,
44524
+ clientTradeId: id,
44525
+ ...plan.body
44526
+ }));
44527
+ if (built.build.walletAddress !== wallet.address || built.build.action !== action)
44528
+ throw new TradingError("INVALID_RESPONSE", "The LP build does not name the requested wallet and action; nothing was signed.");
44529
+ if (built.build.signature)
44530
+ return confirm(built.build.signature, { resumed: true });
44531
+ const preview = {
44532
+ pool: built.build.pool,
44533
+ position: built.build.position,
44534
+ amounts: built.preview?.amounts ?? [],
44535
+ warnings: built.preview?.warnings ?? [],
44536
+ tokenRisks: built.preview?.tokenRisks ?? [],
44537
+ candleFeeBps: built.preview?.candleFeeBps ?? 0
44538
+ };
44539
+ if (!await confirmPreview(ctx, plan, built))
44540
+ return printTradingResult(ctx, {
44541
+ success: true,
44542
+ status: "cancelled",
44543
+ clientTradeId: id,
44544
+ kind: "lp",
44545
+ action,
44546
+ preview
44547
+ });
44548
+ const signed = await relaySign(ctx, key, wallet, built.build.transaction);
44549
+ const signature = await saveOperationSignature(ctx, key, id, "lp", signed);
44550
+ const rpc2 = createSolanaRpc(plan.url, ctx.deps.fetch);
44551
+ const echoed = await rpc2.sendTransaction(signed);
44552
+ if (echoed !== signature)
44553
+ throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
44554
+ ctx.deps.stderr.write(`LP ${action} signature: ${signature}
44555
+ `);
44556
+ await waitConfirmed(ctx, rpc2, signature, id);
44557
+ return confirm(signature, { preview });
44558
+ }
44559
+ function slippageOf(flag) {
44560
+ const slippage = Number(flag ?? "100");
44561
+ if (!Number.isInteger(slippage) || slippage < 0 || slippage > 1000)
44562
+ throw new TradingError("INVALID_AMOUNT", "--slippage-bps must be an integer from 0 to 1000.");
44563
+ return slippage;
44564
+ }
44565
+ async function lpAdd(args, ctx) {
44566
+ const parsed = parseArgs(args, {
44567
+ valueFlags: ["--amount", "--wallet", "--position", "--client-trade-id", "--slippage-bps", "--rpc-url"],
44568
+ booleanFlags: ["--yes"]
44569
+ });
44570
+ if ("error" in parsed)
44571
+ return usage2(ctx, parsed.error);
44572
+ const flags = parsed.values;
44573
+ const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
44574
+ if (parsed.positionals.length !== 2 || !flags["--amount"] || !flags["--wallet"] || !validClientId(id))
44575
+ return usage2(ctx, "Usage: candle lp add <pool> --amount <decimal> <token> --wallet <tee> [--position <nft-mint>] [--slippage-bps 100] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
44576
+ try {
44577
+ const pool = solanaAddress(parsed.positionals[0], "The pool");
44578
+ const token = poolToken(parsed.positionals[1]);
44579
+ const position = flags["--position"] ? solanaAddress(flags["--position"], "--position") : undefined;
44580
+ rawAmount(flags["--amount"], 18);
44581
+ const slippageBps = slippageOf(flags["--slippage-bps"]);
44582
+ const url = rpcUrl(ctx, flags["--rpc-url"]);
44583
+ const key = await tradingKey(ctx);
44584
+ const wallet = await tradingWallet(ctx, key, flags["--wallet"], LP_SCOPE);
44585
+ const decimals = await decimalsFor(ctx, baseAsset(token) ?? token, flags["--rpc-url"]);
44586
+ const amountRaw = rawAmount(flags["--amount"], decimals);
44587
+ return await runLpOperation(ctx, {
44588
+ action: "add",
44589
+ id,
44590
+ key,
44591
+ wallet,
44592
+ url,
44593
+ yes: parsed.booleans.has("--yes"),
44594
+ intent: `Add ${decimalAmount(amountRaw, decimals)} ${baseAsset(token) ?? token} of liquidity to DAMM v2 pool ${pool}${position ? ` (position ${position})` : " (new position)"}`,
44595
+ body: { pool, token, amountRaw, slippageBps, ...position ? { position } : {} }
44596
+ });
44597
+ } catch (error) {
44598
+ return tradingFailure(ctx, error, id);
44599
+ }
44600
+ }
44601
+ async function lpRemove(args, ctx) {
44602
+ const parsed = parseArgs(args, {
44603
+ valueFlags: ["--percent", "--wallet", "--client-trade-id", "--slippage-bps", "--rpc-url"],
44604
+ booleanFlags: ["--yes"]
44605
+ });
44606
+ if ("error" in parsed)
44607
+ return usage2(ctx, parsed.error);
44608
+ const flags = parsed.values;
44609
+ const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
44610
+ const percent = Number(flags["--percent"]);
44611
+ if (parsed.positionals.length !== 1 || !flags["--percent"] || !/^\d+(\.\d{1,2})?$/.test(flags["--percent"]) || !(percent > 0 && percent <= 100) || !validClientId(id))
44612
+ return usage2(ctx, "Usage: candle lp remove <position> --percent <0.01-100> [--wallet <tee>] [--slippage-bps 100] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
44613
+ try {
44614
+ const position = solanaAddress(parsed.positionals[0], "The position");
44615
+ const slippageBps = slippageOf(flags["--slippage-bps"]);
44616
+ const url = rpcUrl(ctx, flags["--rpc-url"]);
44617
+ const key = await tradingKey(ctx);
44618
+ const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
44619
+ return await runLpOperation(ctx, {
44620
+ action: "remove",
44621
+ id,
44622
+ key,
44623
+ wallet,
44624
+ url,
44625
+ yes: parsed.booleans.has("--yes"),
44626
+ intent: percent === 100 ? `Remove all liquidity from position ${position}, claim its fees and close it (rent returns to the wallet)` : `Remove ${percent}% of the liquidity in position ${position}`,
44627
+ body: { position, percent, slippageBps }
44628
+ });
44629
+ } catch (error) {
44630
+ return tradingFailure(ctx, error, id);
44631
+ }
44632
+ }
44633
+ async function lpClaim(args, ctx) {
44634
+ const parsed = parseArgs(args, {
44635
+ valueFlags: ["--wallet", "--client-trade-id", "--rpc-url"],
44636
+ booleanFlags: ["--yes"]
44637
+ });
44638
+ if ("error" in parsed)
44639
+ return usage2(ctx, parsed.error);
44640
+ const flags = parsed.values;
44641
+ const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
44642
+ if (parsed.positionals.length !== 1 || !validClientId(id))
44643
+ return usage2(ctx, "Usage: candle lp claim <position> [--wallet <tee>] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
44644
+ try {
44645
+ const position = solanaAddress(parsed.positionals[0], "The position");
44646
+ const url = rpcUrl(ctx, flags["--rpc-url"]);
44647
+ const key = await tradingKey(ctx);
44648
+ const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
44649
+ return await runLpOperation(ctx, {
44650
+ action: "claim",
44651
+ id,
44652
+ key,
44653
+ wallet,
44654
+ url,
44655
+ yes: parsed.booleans.has("--yes"),
44656
+ intent: `Claim the fees and any rewards of position ${position}`,
44657
+ body: { position }
44658
+ });
44659
+ } catch (error) {
44660
+ return tradingFailure(ctx, error, id);
44661
+ }
44662
+ }
44663
+
44111
44664
  // src/commands/mcp.ts
44112
44665
  init_args();
44113
44666
  init_deps();
@@ -44434,20 +44987,20 @@ async function writeSecretNames(deps, profile, names) {
44434
44987
  else
44435
44988
  await deps.updateProfile(profile, { secretNames: sorted });
44436
44989
  }
44437
- function usage2(ctx, line) {
44990
+ function usage3(ctx, line) {
44438
44991
  writeUsageFailure(ctx.deps, line, ctx.json);
44439
44992
  return 2;
44440
44993
  }
44441
44994
  async function secretsSet(args, ctx) {
44442
44995
  const parsed = parseArgs(args, {});
44443
44996
  if ("error" in parsed)
44444
- return usage2(ctx, parsed.error);
44997
+ return usage3(ctx, parsed.error);
44445
44998
  const [raw, extra] = parsed.positionals;
44446
44999
  if (!raw || extra !== undefined)
44447
- return usage2(ctx, "Usage: candle secrets set <name>");
45000
+ return usage3(ctx, "Usage: candle secrets set <name>");
44448
45001
  const name = canonicalSecretName(raw);
44449
45002
  if (name === undefined) {
44450
- return usage2(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
45003
+ return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
44451
45004
  }
44452
45005
  if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
44453
45006
  writeLocalFailure(ctx.deps, {
@@ -44459,7 +45012,7 @@ async function secretsSet(args, ctx) {
44459
45012
  }
44460
45013
  const value = await ctx.deps.promptSecret(`Value for ${name} (input hidden): `);
44461
45014
  if (value.length === 0)
44462
- return usage2(ctx, "An empty value was typed; nothing was stored.");
45015
+ return usage3(ctx, "An empty value was typed; nothing was stored.");
44463
45016
  try {
44464
45017
  await ctx.deps.secretsStore.set(secretRef(ctx.profile, name), value);
44465
45018
  } catch (error) {
@@ -44479,9 +45032,9 @@ async function secretsSet(args, ctx) {
44479
45032
  async function secretsList(args, ctx) {
44480
45033
  const parsed = parseArgs(args, {});
44481
45034
  if ("error" in parsed)
44482
- return usage2(ctx, parsed.error);
45035
+ return usage3(ctx, parsed.error);
44483
45036
  if (parsed.positionals.length > 0)
44484
- return usage2(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
45037
+ return usage3(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
44485
45038
  const names = await storedSecretNames(ctx.deps, ctx.profile);
44486
45039
  if (ctx.json) {
44487
45040
  ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
@@ -44500,13 +45053,13 @@ async function secretsList(args, ctx) {
44500
45053
  async function secretsRemove(args, ctx) {
44501
45054
  const parsed = parseArgs(args, {});
44502
45055
  if ("error" in parsed)
44503
- return usage2(ctx, parsed.error);
45056
+ return usage3(ctx, parsed.error);
44504
45057
  const [raw, extra] = parsed.positionals;
44505
45058
  if (!raw || extra !== undefined)
44506
- return usage2(ctx, "Usage: candle secrets remove <name>");
45059
+ return usage3(ctx, "Usage: candle secrets remove <name>");
44507
45060
  const name = canonicalSecretName(raw);
44508
45061
  if (name === undefined) {
44509
- return usage2(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
45062
+ return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
44510
45063
  }
44511
45064
  await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name));
44512
45065
  const names = await storedSecretNames(ctx.deps, ctx.profile);
@@ -44618,6 +45171,645 @@ async function runPlugin(name, rawArgs, ctx) {
44618
45171
  return ctx.deps.runPlugin(path, split2.passthrough, env);
44619
45172
  }
44620
45173
 
45174
+ // src/commands/pnl.ts
45175
+ init_args();
45176
+ init_deps();
45177
+ init_profiles();
45178
+ init_render();
45179
+
45180
+ // src/usd.ts
45181
+ function formatUsd(value) {
45182
+ if (!Number.isFinite(value))
45183
+ return "?";
45184
+ const sign2 = value < 0 ? "-" : "";
45185
+ const abs = Math.abs(value);
45186
+ if (abs > 0 && abs < 0.005)
45187
+ return `${sign2}<$0.01`;
45188
+ return `${sign2}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
45189
+ }
45190
+ function formatPrice(value) {
45191
+ if (!Number.isFinite(value) || value <= 0)
45192
+ return "?";
45193
+ if (value >= 1)
45194
+ return formatUsd(value);
45195
+ return `$${Number(value.toPrecision(4)).toLocaleString("en-US", { maximumFractionDigits: 12 })}`;
45196
+ }
45197
+ function formatAmount(raw, decimals) {
45198
+ const digits = BigInt(raw).toString().padStart(decimals + 1, "0");
45199
+ if (decimals === 0)
45200
+ return digits;
45201
+ const whole = digits.slice(0, -decimals);
45202
+ const fraction = digits.slice(-decimals).replace(/0+$/, "");
45203
+ return fraction ? `${whole}.${fraction}` : whole;
45204
+ }
45205
+ function formatQuantity(value) {
45206
+ if (!Number.isFinite(value))
45207
+ return "?";
45208
+ return value.toLocaleString("en-US", { maximumFractionDigits: 6 });
45209
+ }
45210
+ function shortAddress2(address) {
45211
+ return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address;
45212
+ }
45213
+
45214
+ // src/commands/pnl.ts
45215
+ var NO_API_KEY2 = {
45216
+ code: "NO_API_KEY",
45217
+ message: "No API key for this profile.",
45218
+ suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
45219
+ };
45220
+ async function pnl(args, ctx) {
45221
+ const { deps, apiUrl, json } = ctx;
45222
+ const parsed = parseArgs(args, {});
45223
+ if ("error" in parsed) {
45224
+ writeUsageFailure(deps, parsed.error, json);
45225
+ return 2;
45226
+ }
45227
+ if (parsed.positionals.length > 0) {
45228
+ writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}. Usage: candle pnl [--profile <name>]`, json);
45229
+ return 2;
45230
+ }
45231
+ const apiKey = await resolveApiKey(deps, ctx.profile);
45232
+ if (!apiKey) {
45233
+ writeLocalFailure(deps, NO_API_KEY2, json);
45234
+ return 1;
45235
+ }
45236
+ const perProfile = ctx.profileFlag !== undefined;
45237
+ let keyPrefix;
45238
+ if (perProfile) {
45239
+ keyPrefix = apiKeyPrefix(apiKey);
45240
+ if (keyPrefix === undefined) {
45241
+ writeLocalFailure(deps, {
45242
+ code: "BAD_REQUEST",
45243
+ message: `The API key for profile ${ctx.profileFlag} is not a Candle agent key, so it names no profile to read.`,
45244
+ suggestion: "Run `candle auth login --profile <name>` to store a key for that profile."
45245
+ }, json);
45246
+ return 1;
45247
+ }
45248
+ }
45249
+ await printIdentity(ctx);
45250
+ const path = perProfile ? `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl` : "/api/v1/agent/books";
45251
+ const result = await apiRequest(path, {
45252
+ auth: "key",
45253
+ credentials: { apiKey },
45254
+ apiUrl,
45255
+ fetch: deps.fetch,
45256
+ env: deps.env
45257
+ });
45258
+ if (!result.ok) {
45259
+ if (!perProfile && result.code === "SCOPE_MISSING") {
45260
+ writeLocalFailure(deps, {
45261
+ code: "SCOPE_MISSING",
45262
+ message: "The account's P&L needs a key with the Read scope (account:read); this profile's key has none.",
45263
+ suggestion: "Log in with a Read or Read:Write key, or read this key's own P&L: candle pnl --profile <name>"
45264
+ }, json);
45265
+ return 1;
45266
+ }
45267
+ writeFailure(deps, result, { apiUrl, authType: "key" }, json);
45268
+ return 1;
45269
+ }
45270
+ const body = result.body;
45271
+ if (json) {
45272
+ const { success: _success, ...rest } = body;
45273
+ deps.stdout.write(`${JSON.stringify({ ok: true, scope: perProfile ? "profile" : "account", ...rest })}
45274
+ `);
45275
+ return 0;
45276
+ }
45277
+ if (perProfile) {
45278
+ const { pnl: p, lp } = body;
45279
+ deps.stdout.write(`P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
45280
+
45281
+ `);
45282
+ writeSummary(ctx, {
45283
+ realizedNetUsd: p.realizedNetUsd,
45284
+ realizedGrossUsd: p.realizedGrossUsd,
45285
+ feesUsd: p.feesUsd,
45286
+ unrealizedUsd: p.unrealizedUsd,
45287
+ unmarked: p.unmarkedPositions,
45288
+ unvalued: p.unvalued,
45289
+ counted: p.counted,
45290
+ positions: p.openPositions.length,
45291
+ truncated: p.truncated,
45292
+ lookback: p.lookback,
45293
+ lookbackUnit: "trades",
45294
+ oldestMarkAt: p.oldestMarkAt,
45295
+ lp
45296
+ });
45297
+ writePositions(ctx, p.openPositions, false);
45298
+ writeLpPositions(ctx, lp, false);
45299
+ return 0;
45300
+ }
45301
+ const books = body;
45302
+ deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
45303
+
45304
+ `);
45305
+ writeSummary(ctx, {
45306
+ ...books.all,
45307
+ positions: books.positions.length,
45308
+ truncated: books.truncated,
45309
+ lookback: books.lookback,
45310
+ lookbackUnit: "ledger rows",
45311
+ oldestMarkAt: books.oldestMarkAt,
45312
+ lp: books.lp
45313
+ });
45314
+ writePositions(ctx, books.positions, true);
45315
+ writeLpPositions(ctx, books.lp, true);
45316
+ return 0;
45317
+ }
45318
+ function writeSummary(ctx, s) {
45319
+ const marked = s.positions - s.unmarked;
45320
+ const lines = [
45321
+ [
45322
+ "Realized net",
45323
+ formatUsd(s.realizedNetUsd),
45324
+ `gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
45325
+ ],
45326
+ [
45327
+ "Unrealized",
45328
+ formatUsd(s.unrealizedUsd),
45329
+ `${marked} of ${s.positions} open ${s.positions === 1 ? "position" : "positions"} marked${s.unmarked > 0 ? `; ${s.unmarked} unpriced, not counted` : ""}`
45330
+ ]
45331
+ ];
45332
+ const lp = s.lp;
45333
+ if (lp?.read) {
45334
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
45335
+ lines.push([
45336
+ "LP realized",
45337
+ formatUsd(lp.realizedUsd),
45338
+ `withdrawn ${formatUsd(lp.withdrawnUsd)} against ${formatUsd(lp.realizedBasisUsd)} of cost basis, plus ${formatUsd(lp.claimedFeesUsd)} claimed fees`
45339
+ ], [
45340
+ "LP unrealized",
45341
+ formatUsd(lp.unrealizedUsd),
45342
+ `${lp.valued} of ${plural(lp.openPositions, "open LP position", "open LP positions")} valued${lp.unpriced > 0 ? `; ${lp.unpriced} unpriced, not counted` : ""}${lp.unreadable > 0 ? `; ${lp.unreadable} not read, not counted` : ""}${lp.closedOutsideLedger > 0 ? `; ${lp.closedOutsideLedger} closed outside the ledger, not counted` : ""}`
45343
+ ], [
45344
+ "LP vs holding",
45345
+ lp.vsHoldingPositions > 0 ? formatUsd(lp.vsHoldingUsd) : "-",
45346
+ lp.vsHoldingPositions > 0 ? `${lp.vsHoldingPositions} of ${lp.valued} valued, against holding the deposited tokens (now ${formatUsd(lp.holdValueUsd)})` : "no valued LP position with every deposit priced"
45347
+ ], [
45348
+ "Total",
45349
+ formatUsd(s.realizedNetUsd + s.unrealizedUsd + lp.realizedUsd + lp.unrealizedUsd),
45350
+ "realized net plus unrealized, tokens and LP"
45351
+ ]);
45352
+ } else if (lp) {
45353
+ lines.push(["LP", "not read", `${terminalText(lp.reason)}; not in the total`], ["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized, tokens only"]);
45354
+ } else {
45355
+ lines.push(["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]);
45356
+ }
45357
+ const width = Math.max(...lines.map(([label]) => label.length));
45358
+ const valueWidth = Math.max(...lines.map(([, value]) => value.length));
45359
+ for (const [label, value, note] of lines) {
45360
+ ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
45361
+ `);
45362
+ }
45363
+ if (s.oldestMarkAt !== undefined) {
45364
+ ctx.deps.stdout.write(`Marks as old as ${new Date(s.oldestMarkAt).toISOString()}.
45365
+ `);
45366
+ }
45367
+ if (s.unvalued > 0 || (s.unresolved ?? 0) > 0) {
45368
+ ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
45369
+ `);
45370
+ }
45371
+ if (s.truncated) {
45372
+ ctx.deps.stdout.write(`History is truncated: this covers the most recent ${s.lookback} ${s.lookbackUnit}, not the account's lifetime.
45373
+ `);
45374
+ }
45375
+ if (lp?.read) {
45376
+ if (lp.unvalued > 0) {
45377
+ ctx.deps.stdout.write(`${lp.unvalued} LP ledger ${lp.unvalued === 1 ? "leg" : "legs"} could not be valued and ${lp.unvalued === 1 ? "is" : "are"} not in these figures.
45378
+ `);
45379
+ }
45380
+ if (lp.closedOutsideLedger > 0) {
45381
+ ctx.deps.stdout.write(`${lp.closedOutsideLedger} LP ${lp.closedOutsideLedger === 1 ? "position was" : "positions were"} closed outside the ledger (a sweep close writes no confirmation), so ${lp.closedOutsideLedger === 1 ? "its" : "their"} result is unknown and not in these figures.
45382
+ `);
45383
+ }
45384
+ if (lp.truncated) {
45385
+ ctx.deps.stdout.write(`LP history is truncated: this covers the most recent ${lp.lookback} LP operations, not the account's lifetime.
45386
+ `);
45387
+ }
45388
+ }
45389
+ }
45390
+ function writeLpPositions(ctx, lp, withBook) {
45391
+ if (!lp?.read || lp.positions.length === 0)
45392
+ return;
45393
+ const headers = ["POSITION", "POOL", "WALLET", "VALUE", "COST BASIS", "UNREALIZED", "VS HOLDING"];
45394
+ if (withBook)
45395
+ headers.push("BOOK");
45396
+ const value = (p) => p.status === "valued" && p.valueUsd !== undefined ? formatUsd(p.valueUsd) : p.status === "unpriced" ? "unpriced" : p.status === "unreadable" ? "not read" : "closed outside the ledger";
45397
+ const rows = lp.positions.map((p) => {
45398
+ const row = [
45399
+ shortAddress2(p.position),
45400
+ shortAddress2(p.pool),
45401
+ shortAddress2(p.wallet),
45402
+ value(p),
45403
+ formatUsd(p.costBasisUsd),
45404
+ p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-",
45405
+ p.vsHoldingUsd !== undefined ? formatUsd(p.vsHoldingUsd) : "-"
45406
+ ];
45407
+ if (withBook)
45408
+ row.push(p.book ?? "-");
45409
+ return row;
45410
+ });
45411
+ ctx.deps.stdout.write(`
45412
+ Open LP positions
45413
+ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
45414
+ `);
45415
+ }
45416
+ function writePositions(ctx, positions, withBook) {
45417
+ if (positions.length === 0) {
45418
+ ctx.deps.stdout.write(`
45419
+ No open positions.
45420
+ `);
45421
+ return;
45422
+ }
45423
+ const headers = ["TOKEN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
45424
+ if (withBook)
45425
+ headers.push("BOOK");
45426
+ const rows = positions.map((p) => {
45427
+ const row = [
45428
+ p.symbol?.trim() || shortAddress2(p.mint),
45429
+ formatQuantity(p.quantity),
45430
+ formatPrice(p.avgEntryUsd),
45431
+ p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
45432
+ p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-"
45433
+ ];
45434
+ if (withBook)
45435
+ row.push(p.book ?? "-");
45436
+ return row;
45437
+ });
45438
+ ctx.deps.stdout.write(`
45439
+ Open positions
45440
+ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
45441
+ `);
45442
+ }
45443
+
45444
+ // src/commands/portfolio.ts
45445
+ init_args();
45446
+ init_deps();
45447
+ init_profiles();
45448
+ init_render();
45449
+ init_solana_lite();
45450
+ init_store();
45451
+ init_vault_support();
45452
+ var SOL_MINT = "So11111111111111111111111111111111111111112";
45453
+ var KNOWN_SYMBOLS = {
45454
+ [SOL_MINT]: "SOL",
45455
+ EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
45456
+ "9dXSV8VWuYvGfTzqvkBeoFwH9ihVTybDuWo5VaJPCNDL": "CNDL"
45457
+ };
45458
+ var CHUNK = 100;
45459
+ var TOKEN_READS_IN_FLIGHT = 8;
45460
+ var NO_API_KEY3 = {
45461
+ code: "NO_API_KEY",
45462
+ message: "No API key for this profile.",
45463
+ suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
45464
+ };
45465
+ async function portfolio(args, ctx) {
45466
+ const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
45467
+ if ("error" in parsed)
45468
+ return usage(ctx, parsed.error);
45469
+ if (parsed.positionals.length > 0)
45470
+ return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
45471
+ const { deps } = ctx;
45472
+ const apiKey = await resolveApiKey(deps, ctx.profile);
45473
+ if (!apiKey) {
45474
+ writeLocalFailure(deps, NO_API_KEY3, ctx.json);
45475
+ return 1;
45476
+ }
45477
+ const rpcGiven = parsed.values["--rpc-url"] !== undefined || Boolean(deps.env[RPC_URL_ENV]?.trim());
45478
+ let rpcUrl2;
45479
+ if (rpcGiven) {
45480
+ const resolved = rpcUrlFrom(ctx, parsed);
45481
+ if (typeof resolved !== "string")
45482
+ return usage(ctx, resolved.error);
45483
+ rpcUrl2 = resolved;
45484
+ }
45485
+ const resolvedVault = vaultPathFor(ctx, parsed);
45486
+ if ("error" in resolvedVault)
45487
+ return usage(ctx, resolvedVault.error);
45488
+ return runVaultCommand(ctx, async ({ hold }) => {
45489
+ let vaultEntries;
45490
+ let vaultReason;
45491
+ const raw = rpcUrl2 === undefined ? null : await readVaultRaw(resolvedVault.path);
45492
+ if (rpcUrl2 === undefined) {
45493
+ vaultReason = `not read: vault balances are read only over your own RPC; pass --rpc-url or set ${RPC_URL_ENV}`;
45494
+ } else if (raw === null) {
45495
+ vaultReason = `no vault at ${resolvedVault.path}`;
45496
+ } else {
45497
+ if (!refuseEnvPassphrase(ctx))
45498
+ return 1;
45499
+ if (!requirePromptStreams(ctx, "portfolio"))
45500
+ return 1;
45501
+ const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
45502
+ 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 }));
45503
+ }
45504
+ await printIdentity(ctx);
45505
+ const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
45506
+ if (vaultEntries && vaultEntries.length > 0 && rpcHost !== undefined) {
45507
+ const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
45508
+ 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.
45509
+ `);
45510
+ }
45511
+ const [candle, vaultRead] = await Promise.all([
45512
+ apiRequest("/api/v1/agent/portfolio", {
45513
+ auth: "key",
45514
+ credentials: { apiKey },
45515
+ apiUrl: ctx.apiUrl,
45516
+ fetch: deps.fetch,
45517
+ env: deps.env
45518
+ }),
45519
+ vaultEntries && rpcUrl2 ? readOwnRpc(vaultEntries.map((entry) => entry.address), rpcUrl2, deps.fetch) : Promise.resolve(undefined)
45520
+ ]);
45521
+ if (!candle.ok) {
45522
+ writeFailure(deps, candle, { apiUrl: ctx.apiUrl, authType: "key" }, ctx.json);
45523
+ return 1;
45524
+ }
45525
+ const fromCandle = candle.body;
45526
+ const prices = { ...fromCandle.prices ?? {} };
45527
+ let priceFailure;
45528
+ if (vaultRead) {
45529
+ const wanted = new Set;
45530
+ for (const wallet2 of vaultRead.byAddress.values()) {
45531
+ if (wallet2.lamports !== null)
45532
+ wanted.add(SOL_MINT);
45533
+ for (const token of wallet2.tokens ?? [])
45534
+ wanted.add(token.mint);
45535
+ }
45536
+ const missing = [...wanted].filter((mint) => prices[mint] === undefined);
45537
+ for (let at = 0;at < missing.length; at += CHUNK) {
45538
+ const priced = await apiRequest("/api/v1/agent/prices", {
45539
+ method: "POST",
45540
+ body: { mints: missing.slice(at, at + CHUNK) },
45541
+ auth: "key",
45542
+ credentials: { apiKey },
45543
+ apiUrl: ctx.apiUrl,
45544
+ fetch: deps.fetch,
45545
+ env: deps.env
45546
+ });
45547
+ if (priced.ok)
45548
+ Object.assign(prices, priced.body.prices ?? {});
45549
+ else
45550
+ priceFailure ??= priced.message;
45551
+ }
45552
+ }
45553
+ if (priceFailure !== undefined) {
45554
+ deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
45555
+ `);
45556
+ }
45557
+ const lpByWallet = new Map;
45558
+ const lpOf = (address) => {
45559
+ const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
45560
+ lpByWallet.set(address, entry);
45561
+ return entry;
45562
+ };
45563
+ for (const position of fromCandle.lp?.positions ?? [])
45564
+ lpOf(position.wallet).positions.push(lpRow(position, prices));
45565
+ for (const unread of fromCandle.lp?.unreadable ?? [])
45566
+ lpOf(unread.wallet).unread.push(unread.position);
45567
+ const wallet = (address, read, extra) => {
45568
+ const holdings = read === undefined ? null : valueHoldings(read, prices);
45569
+ const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
45570
+ const lp2 = fromCandle.lp ? lpOf(address) : undefined;
45571
+ return {
45572
+ address,
45573
+ ...extra,
45574
+ holdings,
45575
+ ...unread.length > 0 ? { unread } : {},
45576
+ ...lp2 ? { lpPositions: lp2.positions } : {},
45577
+ ...lp2 && lp2.unread.length > 0 ? { lpUnread: lp2.unread } : {},
45578
+ valueUsd: (holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
45579
+ unpriced: (holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
45580
+ };
45581
+ };
45582
+ const group = (name, wallets, read, reason) => ({
45583
+ group: name,
45584
+ read,
45585
+ ...reason !== undefined ? { reason } : {},
45586
+ wallets,
45587
+ valueUsd: wallets.reduce((sum, w) => sum + w.valueUsd, 0),
45588
+ unpriced: wallets.reduce((sum, w) => sum + w.unpriced, 0)
45589
+ });
45590
+ const orNull = (read) => read.lamports === null && read.tokens === null ? undefined : read;
45591
+ const groups = [
45592
+ group("vault", (vaultEntries ?? []).map((entry) => {
45593
+ const read = vaultRead?.byAddress.get(entry.address);
45594
+ return wallet(entry.address, read ? orNull(read) : undefined, { label: entry.label, role: entry.role });
45595
+ }), vaultEntries !== undefined, vaultReason),
45596
+ group("tee", (fromCandle.tee ?? []).map((row) => wallet(row.address, orNull(row), {
45597
+ id: row.id,
45598
+ ...row.label ? { label: row.label } : {},
45599
+ active: row.active
45600
+ })), true),
45601
+ group("embedded", (fromCandle.embedded ?? []).map((row) => wallet(row.address, orNull(row), {})), true)
45602
+ ];
45603
+ const unavailable = [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []];
45604
+ const lpUnread = (fromCandle.lp?.unreadable ?? []).length;
45605
+ const complete = fromCandle.complete !== false && unavailable.length === 0 && lpUnread === 0;
45606
+ const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
45607
+ const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
45608
+ const lp = fromCandle.lp ? {
45609
+ positions: fromCandle.lp.positions.length,
45610
+ unpriced: fromCandle.lp.positions.filter((p) => p.valueUsd === null).length,
45611
+ unreadable: lpUnread,
45612
+ valueUsd: fromCandle.lp.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
45613
+ } : undefined;
45614
+ if (ctx.json) {
45615
+ writeJson(deps, {
45616
+ ok: true,
45617
+ totalUsd,
45618
+ unpriced,
45619
+ complete,
45620
+ unavailable,
45621
+ ...rpcHost !== undefined ? { rpcHost } : {},
45622
+ ...lp ? { lp } : {},
45623
+ groups
45624
+ });
45625
+ return complete ? 0 : 3;
45626
+ }
45627
+ writeTable(ctx, groups, { totalUsd, unpriced, unavailable: unavailable.length, lp });
45628
+ return complete ? 0 : 3;
45629
+ });
45630
+ }
45631
+ async function readOwnRpc(addresses, rpcUrl2, fetchFn) {
45632
+ const rpc2 = createSolanaRpc(rpcUrl2, fetchFn);
45633
+ const unique = [...new Set(addresses)];
45634
+ const lamports = new Map;
45635
+ for (let at = 0;at < unique.length; at += CHUNK) {
45636
+ const chunk = unique.slice(at, at + CHUNK);
45637
+ try {
45638
+ const accounts = await rpc2.getMultipleAccounts(chunk);
45639
+ for (const [i, address] of chunk.entries())
45640
+ lamports.set(address, (accounts[i]?.lamports ?? 0n).toString());
45641
+ } catch {
45642
+ for (const address of chunk)
45643
+ lamports.set(address, null);
45644
+ }
45645
+ }
45646
+ const tokens = new Map;
45647
+ const failed = new Set;
45648
+ const reads = unique.flatMap((owner) => [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) => ({ owner, programId })));
45649
+ let next = 0;
45650
+ await Promise.all(Array.from({ length: Math.min(TOKEN_READS_IN_FLIGHT, reads.length) }, async () => {
45651
+ while (next < reads.length) {
45652
+ const { owner, programId } = reads[next++];
45653
+ try {
45654
+ const accounts = await rpc2.getTokenAccountsByOwner(owner, programId);
45655
+ const held = tokens.get(owner) ?? new Map;
45656
+ tokens.set(owner, held);
45657
+ const program = programId === TOKEN_PROGRAM_ID ? "token" : "token-2022";
45658
+ for (const account of accounts) {
45659
+ if (!/^\d+$/.test(account.amountRaw) || BigInt(account.amountRaw) === 0n)
45660
+ continue;
45661
+ const key = `${program}:${account.mint}`;
45662
+ const prior = held.get(key);
45663
+ held.set(key, {
45664
+ mint: account.mint,
45665
+ amountRaw: (BigInt(prior?.amountRaw ?? "0") + BigInt(account.amountRaw)).toString(),
45666
+ decimals: account.decimals,
45667
+ program
45668
+ });
45669
+ }
45670
+ } catch {
45671
+ failed.add(owner);
45672
+ }
45673
+ }
45674
+ }));
45675
+ const byAddress = new Map;
45676
+ const unavailable = [];
45677
+ for (const address of unique) {
45678
+ const sol = lamports.get(address) ?? null;
45679
+ const held = failed.has(address) ? null : [...tokens.get(address)?.values() ?? []];
45680
+ byAddress.set(address, { lamports: sol, tokens: held });
45681
+ if (sol === null || held === null)
45682
+ unavailable.push(address);
45683
+ }
45684
+ return { byAddress, unavailable };
45685
+ }
45686
+ function valueHoldings(read, prices) {
45687
+ const raw = [];
45688
+ if (read.lamports !== null && BigInt(read.lamports) > 0n) {
45689
+ raw.push({ mint: SOL_MINT, amountRaw: read.lamports, decimals: 9, program: "native" });
45690
+ }
45691
+ for (const token of read.tokens ?? [])
45692
+ raw.push(token);
45693
+ return raw.map((h) => {
45694
+ const price = prices[h.mint];
45695
+ const priceUsd = price?.priceUsd ?? null;
45696
+ const amount = formatAmount(h.amountRaw, h.decimals);
45697
+ return {
45698
+ ...h,
45699
+ symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
45700
+ amount,
45701
+ priceUsd,
45702
+ valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
45703
+ priceSource: price?.source ?? null
45704
+ };
45705
+ });
45706
+ }
45707
+ function lpRow(position, prices) {
45708
+ return {
45709
+ position: position.position,
45710
+ pool: position.pool,
45711
+ tokens: position.tokens.map((token) => ({
45712
+ ...token,
45713
+ symbol: KNOWN_SYMBOLS[token.mint] ?? token.symbol ?? prices[token.mint]?.symbol ?? null,
45714
+ amount: formatAmount(token.amountRaw, token.decimals),
45715
+ unclaimedFees: formatAmount(token.unclaimedFeesRaw, token.decimals)
45716
+ })),
45717
+ ...position.poolShare !== undefined ? { poolShare: position.poolShare } : {},
45718
+ valueUsd: position.valueUsd,
45719
+ unpriced: position.unpriced
45720
+ };
45721
+ }
45722
+ function writeTable(ctx, groups, totals) {
45723
+ const { deps } = ctx;
45724
+ const rows = [];
45725
+ const lpRows = [];
45726
+ const notes = [];
45727
+ for (const g of groups) {
45728
+ const shown = g.wallets.filter((w) => w.holdings === null || w.holdings.length > 0 || (w.unread?.length ?? 0) > 0 || (w.lpPositions?.length ?? 0) > 0 || (w.lpUnread?.length ?? 0) > 0).sort((a, b) => b.valueUsd - a.valueUsd);
45729
+ let lpCount = 0;
45730
+ let lpUnread = 0;
45731
+ for (const w of shown) {
45732
+ const name = `${w.label ? `${w.label} ` : ""}(${shortAddress2(w.address)})${w.role === "external" ? " external" : ""}`;
45733
+ const side = (t, amount) => `${amount} ${t.symbol ?? shortAddress2(t.mint)}`;
45734
+ for (const p of [...w.lpPositions ?? []].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1))) {
45735
+ lpCount += 1;
45736
+ lpRows.push([
45737
+ g.group,
45738
+ name,
45739
+ shortAddress2(p.position),
45740
+ shortAddress2(p.pool),
45741
+ p.tokens.map((t) => side(t, t.amount)).join(" + "),
45742
+ p.tokens.map((t) => side(t, t.unclaimedFees)).join(" + "),
45743
+ p.valueUsd === null ? "unpriced" : formatUsd(p.valueUsd)
45744
+ ]);
45745
+ }
45746
+ for (const position of w.lpUnread ?? []) {
45747
+ lpUnread += 1;
45748
+ lpRows.push([g.group, name, shortAddress2(position), "-", "not read", "-", "-"]);
45749
+ }
45750
+ if (w.holdings === null) {
45751
+ rows.push([g.group, name, "-", "not read", "-", "-"]);
45752
+ continue;
45753
+ }
45754
+ const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
45755
+ for (const h of sorted) {
45756
+ rows.push([
45757
+ g.group,
45758
+ name,
45759
+ h.symbol ?? shortAddress2(h.mint),
45760
+ h.amount,
45761
+ h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
45762
+ h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
45763
+ ]);
45764
+ }
45765
+ for (const part of w.unread ?? [])
45766
+ rows.push([g.group, name, part === "sol" ? "SOL" : "tokens", "not read", "-", "-"]);
45767
+ }
45768
+ const empty = g.wallets.length - shown.length;
45769
+ const count = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
45770
+ notes.push([
45771
+ g.group,
45772
+ g.read ? formatUsd(g.valueUsd) : "-",
45773
+ g.read ? `${count}${empty > 0 ? `, ${empty} empty not shown` : ""}${lpCount > 0 ? `, ${lpCount} LP ${lpCount === 1 ? "position" : "positions"}` : ""}${lpUnread > 0 ? `, ${lpUnread} LP not read` : ""}${g.unpriced > 0 ? `, ${g.unpriced} unpriced` : ""}` : g.reason ?? "not read"
45774
+ ]);
45775
+ }
45776
+ if (rows.length > 0)
45777
+ deps.stdout.write(`
45778
+ ${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
45779
+ `);
45780
+ else
45781
+ deps.stdout.write(`
45782
+ Nothing held in any wallet read.
45783
+ `);
45784
+ if (lpRows.length > 0)
45785
+ deps.stdout.write(`
45786
+ LP positions
45787
+ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
45788
+ `);
45789
+ notes.push([
45790
+ "total",
45791
+ formatUsd(totals.totalUsd),
45792
+ totals.unpriced > 0 ? `${totals.unpriced} unpriced ${totals.unpriced === 1 ? "holding" : "holdings"} not counted` : "every holding priced"
45793
+ ]);
45794
+ const width = Math.max(...notes.map(([label]) => label.length));
45795
+ const valueWidth = Math.max(...notes.map(([, value]) => value.length));
45796
+ deps.stdout.write(`
45797
+ `);
45798
+ for (const [label, value, note] of notes) {
45799
+ deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
45800
+ `);
45801
+ }
45802
+ if (totals.unavailable > 0) {
45803
+ 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.
45804
+ `);
45805
+ }
45806
+ if ((totals.lp?.unreadable ?? 0) > 0) {
45807
+ const n = totals.lp?.unreadable ?? 0;
45808
+ 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.
45809
+ `);
45810
+ }
45811
+ }
45812
+
44621
45813
  // src/commands/profile.ts
44622
45814
  init_args();
44623
45815
  init_profiles();
@@ -45353,7 +46545,7 @@ async function readMints(rpc2, mints) {
45353
46545
  }
45354
46546
  return out;
45355
46547
  }
45356
- function formatAmount(raw, decimals) {
46548
+ function formatAmount2(raw, decimals) {
45357
46549
  if (decimals === undefined)
45358
46550
  return `${raw} raw`;
45359
46551
  const negative = raw < 0n;
@@ -45363,7 +46555,7 @@ function formatAmount(raw, decimals) {
45363
46555
  return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
45364
46556
  }
45365
46557
  function formatSol(lamports) {
45366
- return `${formatAmount(lamports, 9)} SOL`;
46558
+ return `${formatAmount2(lamports, 9)} SOL`;
45367
46559
  }
45368
46560
  function displayLines(input) {
45369
46561
  const { tx, compiled, signers, simulation, mints } = input;
@@ -45386,7 +46578,7 @@ function displayLines(input) {
45386
46578
  }
45387
46579
  for (const token of deltas.tokens.filter((delta) => delta.owner === entry.address)) {
45388
46580
  const info = mints.get(token.mint);
45389
- lines.push(` ${token.mint}: ${formatAmount(token.before, info?.decimals)} -> ${formatAmount(token.after, info?.decimals)} (${token.after >= token.before ? "+" : ""}${formatAmount(token.after - token.before, info?.decimals)}${info?.decimals === undefined ? "" : `, ${info.decimals} dp`}) in ${token.account}`);
46581
+ lines.push(` ${token.mint}: ${formatAmount2(token.before, info?.decimals)} -> ${formatAmount2(token.after, info?.decimals)} (${token.after >= token.before ? "+" : ""}${formatAmount2(token.after - token.before, info?.decimals)}${info?.decimals === undefined ? "" : `, ${info.decimals} dp`}) in ${token.account}`);
45390
46582
  }
45391
46583
  }
45392
46584
  const others = [];
@@ -45401,7 +46593,7 @@ function displayLines(input) {
45401
46593
  if (signerAddresses.has(token.owner) || token.after <= token.before)
45402
46594
  continue;
45403
46595
  const info = mints.get(token.mint);
45404
- others.push(`${token.owner} receives +${formatAmount(token.after - token.before, info?.decimals)} of ${token.mint} (account ${token.account})`);
46596
+ others.push(`${token.owner} receives +${formatAmount2(token.after - token.before, info?.decimals)} of ${token.mint} (account ${token.account})`);
45405
46597
  }
45406
46598
  if (others.length === 0)
45407
46599
  lines.push("others no other account gains a balance in the simulation");
@@ -45689,6 +46881,285 @@ async function signMessage2(args, ctx) {
45689
46881
  init_esm();
45690
46882
  init_args();
45691
46883
  init_deps();
46884
+
46885
+ // src/lp-close.ts
46886
+ init_solana_lite();
46887
+ var DAMM_V2_PROGRAM_ID = "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG";
46888
+ var CLOSE_PROGRAM_ALLOWLIST = new Set([
46889
+ DAMM_V2_PROGRAM_ID,
46890
+ TOKEN_PROGRAM_ID,
46891
+ TOKEN_2022_PROGRAM_ID,
46892
+ ASSOCIATED_TOKEN_PROGRAM_ID,
46893
+ COMPUTE_BUDGET_PROGRAM_ID,
46894
+ SYSTEM_PROGRAM_ID
46895
+ ]);
46896
+ var DAMM_POSITION_CLOSE_UNAVAILABLE = "DAMM_POSITION_CLOSE_UNAVAILABLE";
46897
+ var DAMM_CLOSE_TRANSACTION_REFUSED = "DAMM_CLOSE_TRANSACTION_REFUSED";
46898
+ function isPositionCandidate(account) {
46899
+ return account.programId === TOKEN_2022_PROGRAM_ID && account.amountRaw === "1" && account.decimals === 0;
46900
+ }
46901
+ function parseCloseArtifact(body) {
46902
+ if (!body || typeof body !== "object")
46903
+ return;
46904
+ const { transaction, accountKeys } = body;
46905
+ if (typeof transaction !== "string" || transaction.length === 0)
46906
+ return;
46907
+ if (!Array.isArray(accountKeys) || !accountKeys.every((key) => typeof key === "string" && key.length > 0))
46908
+ return;
46909
+ return { transaction, accountKeys };
46910
+ }
46911
+ var MINT_BASE_SIZE2 = 82;
46912
+ var ACCOUNT_TYPE_OFFSET2 = 165;
46913
+ var ACCOUNT_TYPE_MINT = 1;
46914
+ var MULTISIG_SIZE2 = 355;
46915
+ var TOKEN_ACCOUNT_SIZE2 = 165;
46916
+ var TOKEN_ACCOUNT_STATE_INITIALIZED = 1;
46917
+ var CONTROL_RANGES = [
46918
+ [0, 32, "mint"],
46919
+ [32, 64, "authority"],
46920
+ [72, 108, "delegate"],
46921
+ [108, 109, "state"],
46922
+ [121, 129, "delegated amount"],
46923
+ [129, 165, "close authority"]
46924
+ ];
46925
+ var TOKEN_IX_CLOSE_ACCOUNT = 9;
46926
+ var ATA_IX_CREATE = 0;
46927
+ var ATA_IX_CREATE_IDEMPOTENT = 1;
46928
+ function isTokenProgram(owner) {
46929
+ return owner === TOKEN_PROGRAM_ID || owner === TOKEN_2022_PROGRAM_ID;
46930
+ }
46931
+ function isMintAccount(view) {
46932
+ if (view === null || !isTokenProgram(view.owner))
46933
+ return false;
46934
+ if (view.data.length === MINT_BASE_SIZE2)
46935
+ return true;
46936
+ if (view.data.length === MULTISIG_SIZE2)
46937
+ return false;
46938
+ return view.data.length > ACCOUNT_TYPE_OFFSET2 && view.data[ACCOUNT_TYPE_OFFSET2] === ACCOUNT_TYPE_MINT;
46939
+ }
46940
+ function isUnsigned(tx) {
46941
+ return tx.signatures.every((slot) => slot.every((byte) => byte === 0));
46942
+ }
46943
+ function bytesEqual2(a, b) {
46944
+ return a.length === b.length && a.every((byte, i) => byte === b[i]);
46945
+ }
46946
+ function allZero(bytes) {
46947
+ return bytes.every((byte) => byte === 0);
46948
+ }
46949
+ async function verifyCloseArtifact(input) {
46950
+ const { artifact, rpc: rpc2, tee, vault, nftMint, nftAccount } = input;
46951
+ const refuse2 = (reason) => ({ ok: false, reason });
46952
+ const allowedOwner = (address) => address === tee || address === vault;
46953
+ let tx;
46954
+ try {
46955
+ tx = decodeTransaction(decodeStrictBase64(artifact.transaction));
46956
+ } catch (error) {
46957
+ if (error instanceof TransactionDecodeError)
46958
+ return refuse2(`the close transaction is undecodable: ${error.message}`);
46959
+ return refuse2(`the close transaction could not be read: ${error instanceof Error ? error.message : error}`);
46960
+ }
46961
+ if (tx.message.version !== 0)
46962
+ return refuse2("the close transaction is not a v0 message");
46963
+ if (!isUnsigned(tx))
46964
+ return refuse2("the close transaction already carries a signature; close-build must return it unsigned");
46965
+ if (tx.message.numRequiredSignatures !== 1)
46966
+ return refuse2(`the close transaction requires ${tx.message.numRequiredSignatures} signers; a position close needs only the TEE wallet`);
46967
+ if (tx.message.staticKeys[0] !== tee)
46968
+ return refuse2(`the fee payer is ${tx.message.staticKeys[0]}, not the TEE wallet`);
46969
+ let compiled;
46970
+ try {
46971
+ compiled = await resolveCompiledKeys(tx.message, rpc2);
46972
+ } catch (error) {
46973
+ if (error instanceof LookupTableError)
46974
+ return refuse2(`a lookup table could not be resolved: ${error.message}`);
46975
+ return refuse2(`the lookup tables could not be read: ${error instanceof Error ? error.message : error}`);
46976
+ }
46977
+ if (compiled.keys.length !== artifact.accountKeys.length)
46978
+ return refuse2(`the server listed ${artifact.accountKeys.length} account keys and the transaction resolves to ${compiled.keys.length}`);
46979
+ for (const [i, key] of compiled.keys.entries()) {
46980
+ if (artifact.accountKeys[i] !== key)
46981
+ return refuse2(`account key ${i} is ${key} in the transaction and ${artifact.accountKeys[i]} in the server's ordered array`);
46982
+ }
46983
+ let dammCalls = 0;
46984
+ for (const [i, ix] of tx.message.instructions.entries()) {
46985
+ const program = compiled.keys[ix.programIdIndex];
46986
+ const account = (at) => compiled.keys[ix.accountIndexes[at] ?? -1];
46987
+ if (program === undefined || !CLOSE_PROGRAM_ALLOWLIST.has(program))
46988
+ return refuse2(`instruction ${i} calls ${program ?? "an unknown program"}, which a position close never does`);
46989
+ switch (program) {
46990
+ case DAMM_V2_PROGRAM_ID:
46991
+ dammCalls += 1;
46992
+ break;
46993
+ case COMPUTE_BUDGET_PROGRAM_ID:
46994
+ break;
46995
+ case ASSOCIATED_TOKEN_PROGRAM_ID: {
46996
+ const kind = ix.data.length === 0 ? ATA_IX_CREATE : ix.data.length === 1 ? ix.data[0] : -1;
46997
+ if (kind !== ATA_IX_CREATE && kind !== ATA_IX_CREATE_IDEMPOTENT)
46998
+ return refuse2(`instruction ${i} is an Associated Token instruction other than Create, which a close never needs`);
46999
+ if (account(0) !== tee || !allowedOwner(account(2) ?? ""))
47000
+ return refuse2(`instruction ${i} creates a token account for ${account(2) ?? "?"}, not the TEE wallet or its vault`);
47001
+ break;
47002
+ }
47003
+ case TOKEN_PROGRAM_ID:
47004
+ case TOKEN_2022_PROGRAM_ID: {
47005
+ if (ix.data[0] !== TOKEN_IX_CLOSE_ACCOUNT || ix.data.length !== 1)
47006
+ return refuse2(`instruction ${i} is a token instruction other than CloseAccount, which a close never issues at the top level`);
47007
+ if (account(1) !== tee || account(2) !== tee)
47008
+ return refuse2(`instruction ${i} closes a token account to ${account(1) ?? "?"}, not the TEE wallet`);
47009
+ break;
47010
+ }
47011
+ default:
47012
+ return refuse2(`instruction ${i} is a top-level System instruction, which a close never issues`);
47013
+ }
47014
+ }
47015
+ if (dammCalls === 0)
47016
+ return refuse2("the transaction calls no DAMM v2 instruction, so it cannot close a position");
47017
+ let simulation;
47018
+ try {
47019
+ simulation = await simulateWithSnapshots(rpc2, artifact.transaction, compiled);
47020
+ } catch (error) {
47021
+ return refuse2(`the simulation could not be run: ${error instanceof Error ? error.message : error}`);
47022
+ }
47023
+ if (simulation.result.err !== null && simulation.result.err !== undefined)
47024
+ return refuse2(`the simulation failed: ${JSON.stringify(simulation.result.err)}`);
47025
+ const deltas = computeDeltas(simulation.snapshots);
47026
+ const postState = new Map(simulation.snapshots.map((snapshot) => [snapshot.address, snapshot.after]));
47027
+ for (const token of deltas.tokens) {
47028
+ if (token.after > token.before && !allowedOwner(token.owner))
47029
+ return refuse2(`${token.owner} would receive ${token.after - token.before} raw of ${token.mint} in account ${token.account}`);
47030
+ }
47031
+ for (const sol of deltas.sol) {
47032
+ if (sol.after <= sol.before)
47033
+ continue;
47034
+ if (allowedOwner(sol.address))
47035
+ continue;
47036
+ const balance = tokenBalanceOf(postState.get(sol.address) ?? null);
47037
+ if (balance !== undefined && allowedOwner(balance.owner))
47038
+ continue;
47039
+ return refuse2(`${sol.address} would receive ${sol.after - sol.before} lamports`);
47040
+ }
47041
+ const nftSnapshot = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
47042
+ if (nftSnapshot === undefined)
47043
+ return refuse2(`the position NFT account ${nftAccount} is not written by this transaction`);
47044
+ const nftAfter = tokenBalanceOf(nftSnapshot.after);
47045
+ if (nftAfter !== undefined && nftAfter.amount !== 0n)
47046
+ return refuse2(`the position NFT account ${nftAccount} still holds ${nftAfter.amount} after the simulation`);
47047
+ for (const { address, before, after } of simulation.snapshots) {
47048
+ if (allowedOwner(address)) {
47049
+ if (after === null || after.owner !== SYSTEM_PROGRAM_ID || after.data.length !== 0)
47050
+ return refuse2(`${address === tee ? "the TEE wallet" : "the vault"} would no longer be a plain System account after this transaction`);
47051
+ }
47052
+ if (after === null)
47053
+ continue;
47054
+ if (before !== null && after.owner !== before.owner)
47055
+ return refuse2(`writable account ${address} would change owner from ${before.owner} to ${after.owner}`);
47056
+ if (before !== null) {
47057
+ const controlled = tokenBalanceOf(before);
47058
+ if (controlled !== undefined && allowedOwner(controlled.owner)) {
47059
+ if (after.data.length < TOKEN_ACCOUNT_SIZE2)
47060
+ return refuse2(`token account ${address} would no longer decode as a token account`);
47061
+ for (const [start, end, what] of CONTROL_RANGES) {
47062
+ if (!bytesEqual2(before.data.subarray(start, end), after.data.subarray(start, end)))
47063
+ return refuse2(`token account ${address} would change its ${what}, which a close never does`);
47064
+ }
47065
+ continue;
47066
+ }
47067
+ }
47068
+ const created = before === null ? tokenBalanceOf(after) : undefined;
47069
+ if (created !== undefined) {
47070
+ if (!allowedOwner(created.owner))
47071
+ return refuse2(`created token account ${address} would be controlled by ${created.owner}`);
47072
+ if (!allZero(after.data.subarray(72, 108)) || after.data[108] !== TOKEN_ACCOUNT_STATE_INITIALIZED || !allZero(after.data.subarray(121, 129)) || !allZero(after.data.subarray(129, 165)))
47073
+ return refuse2(`created token account ${address} would start with a delegate, a close authority, or a frozen state`);
47074
+ }
47075
+ }
47076
+ const infos = new Map;
47077
+ const infoOf = async (address) => {
47078
+ if (!infos.has(address))
47079
+ infos.set(address, await rpc2.getAccountInfo(address));
47080
+ return infos.get(address) ?? null;
47081
+ };
47082
+ const canonical = new Set;
47083
+ for (const key of compiled.keys) {
47084
+ const view = await infoOf(key);
47085
+ if (!isMintAccount(view) || view === null)
47086
+ continue;
47087
+ const mint = decodePubkey(key);
47088
+ const program = decodePubkey(view.owner);
47089
+ canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(tee), mint, program)));
47090
+ canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(vault), mint, program)));
47091
+ }
47092
+ for (const [i, key] of compiled.keys.entries()) {
47093
+ if (!compiled.isWritable[i])
47094
+ continue;
47095
+ const view = await infoOf(key);
47096
+ if (view === null) {
47097
+ if (canonical.has(key))
47098
+ continue;
47099
+ return refuse2(`writable account ${key} does not exist and is not a TEE or vault associated token account for a mint this transaction names`);
47100
+ }
47101
+ if (isTokenProgram(view.owner)) {
47102
+ if (key === nftMint)
47103
+ continue;
47104
+ const balance = tokenBalanceOf(view);
47105
+ if (balance === undefined)
47106
+ return refuse2(`writable account ${key} is owned by a token program but is not a token account`);
47107
+ if (allowedOwner(balance.owner) || key === nftAccount)
47108
+ continue;
47109
+ const authority = await infoOf(balance.owner);
47110
+ if (authority !== null && authority.owner === DAMM_V2_PROGRAM_ID)
47111
+ continue;
47112
+ return refuse2(`writable token account ${key} is controlled by ${balance.owner}, which is neither this wallet, its vault, nor a DAMM v2 pool`);
47113
+ }
47114
+ if (view.owner === SYSTEM_PROGRAM_ID) {
47115
+ if (allowedOwner(key))
47116
+ continue;
47117
+ return refuse2(`writable system account ${key} is neither the TEE wallet nor its vault`);
47118
+ }
47119
+ if (view.owner !== DAMM_V2_PROGRAM_ID)
47120
+ return refuse2(`writable account ${key} is owned by ${view.owner}, not DAMM v2`);
47121
+ }
47122
+ return { ok: true, tx, compiled, simulation };
47123
+ }
47124
+ function formatSol2(lamports) {
47125
+ const negative = lamports < 0n;
47126
+ const abs = negative ? -lamports : lamports;
47127
+ const whole = abs / 1000000000n;
47128
+ const frac = (abs % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
47129
+ return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""} SOL`;
47130
+ }
47131
+ function describeClose(input) {
47132
+ const { verdict, tee, vault, nftMint, nftAccount } = input;
47133
+ const { tx, compiled, simulation } = verdict;
47134
+ const lines = [];
47135
+ lines.push(`message v0, ${tx.message.instructions.length} instruction(s), ${compiled.keys.length} account(s)${tx.message.lookups.length > 0 ? ` (${tx.message.lookups.length} lookup table(s) resolved)` : ""}, blockhash ${tx.message.recentBlockhash}`);
47136
+ lines.push(`fee payer ${compiled.keys[0]} (this TEE wallet)`);
47137
+ lines.push(`position NFT mint ${nftMint}, account ${nftAccount}`);
47138
+ const programs = [...new Set(tx.message.instructions.map((ix) => compiled.keys[ix.programIdIndex] ?? "?"))];
47139
+ for (const program of programs)
47140
+ lines.push(`program ${program === DAMM_V2_PROGRAM_ID ? `Meteora DAMM v2 (${program})` : programNameOf(program)}`);
47141
+ const deltas = computeDeltas(simulation.snapshots);
47142
+ const who = (address) => address === tee ? "wallet" : address === vault ? "vault" : address;
47143
+ for (const address of [tee, vault]) {
47144
+ const sol = deltas.sol.find((delta) => delta.address === address);
47145
+ if (sol && sol.after !== sol.before)
47146
+ lines.push(`${who(address).padEnd(11)} ${formatSol2(sol.before)} -> ${formatSol2(sol.after)} (${sol.after >= sol.before ? "+" : ""}${formatSol2(sol.after - sol.before)})`);
47147
+ for (const token of deltas.tokens.filter((delta) => delta.owner === address && delta.after !== delta.before)) {
47148
+ lines.push(`${who(address).padEnd(11)} ${token.mint}: ${token.before} -> ${token.after} raw (${token.after >= token.before ? "+" : ""}${token.after - token.before}) in ${token.account}${token.account === nftAccount ? " (position NFT, burned)" : ""}`);
47149
+ }
47150
+ }
47151
+ const nft = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
47152
+ if (nft && nft.after === null)
47153
+ lines.push(`position NFT account ${nftAccount} is closed by this transaction`);
47154
+ const others = deltas.tokens.filter((token) => token.owner !== tee && token.owner !== vault && token.after !== token.before);
47155
+ for (const token of others)
47156
+ lines.push(`${token.owner === undefined ? "?" : "pool"} ${token.mint}: ${token.before} -> ${token.after} raw in ${token.account} (authority ${token.owner})`);
47157
+ lines.push("checked ordered keys, programs, simulation, positive deltas, control, writable accounts: all passed");
47158
+ lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
47159
+ return lines;
47160
+ }
47161
+
47162
+ // src/commands/tee.ts
45692
47163
  init_profiles();
45693
47164
  init_render();
45694
47165
  init_solana_lite();
@@ -49429,8 +50900,8 @@ function readDisableOutcome(body) {
49429
50900
  var MIN_PASSPHRASE_LENGTH = 12;
49430
50901
  var USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
49431
50902
  var RPC_URL_ENV2 = "CANDLE_SOLANA_RPC_URL";
49432
- var CONFIRM_POLL_MS2 = 2000;
49433
- var CONFIRM_MAX_POLLS2 = 45;
50903
+ var CONFIRM_POLL_MS3 = 2000;
50904
+ var CONFIRM_MAX_POLLS3 = 45;
49434
50905
  function refuseEnvPassphrase2(ctx) {
49435
50906
  if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
49436
50907
  return true;
@@ -49441,7 +50912,7 @@ function refuseEnvPassphrase2(ctx) {
49441
50912
  }, ctx.json);
49442
50913
  return false;
49443
50914
  }
49444
- function usage3(ctx, line) {
50915
+ function usage4(ctx, line) {
49445
50916
  writeUsageFailure(ctx.deps, line, ctx.json);
49446
50917
  return 2;
49447
50918
  }
@@ -49661,7 +51132,7 @@ async function openExistingTeeStore(ctx, parsed) {
49661
51132
  const { deps, json } = ctx;
49662
51133
  const found = await readTeeStore(ctx, parsed);
49663
51134
  if ("usage" in found)
49664
- return { ok: false, code: usage3(ctx, found.usage) };
51135
+ return { ok: false, code: usage4(ctx, found.usage) };
49665
51136
  const { path } = found;
49666
51137
  if ("error" in found) {
49667
51138
  writeLocalFailure(deps, {
@@ -49758,12 +51229,12 @@ async function teeNew(args, ctx) {
49758
51229
  return 1;
49759
51230
  const parsed = parseArgs(args, { valueFlags: ["--label", "--keystore"], pathFlags: ["--keystore"] });
49760
51231
  if ("error" in parsed)
49761
- return usage3(ctx, parsed.error);
51232
+ return usage4(ctx, parsed.error);
49762
51233
  if (parsed.positionals.length > 0)
49763
- return usage3(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
51234
+ return usage4(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
49764
51235
  const found = await readTeeStore(ctx, parsed);
49765
51236
  if ("usage" in found)
49766
- return usage3(ctx, found.usage);
51237
+ return usage4(ctx, found.usage);
49767
51238
  const { path } = found;
49768
51239
  if ("error" in found) {
49769
51240
  writeLocalFailure(deps, {
@@ -49840,15 +51311,15 @@ async function teeEnable(args, ctx) {
49840
51311
  pathFlags: ["--keystore"]
49841
51312
  });
49842
51313
  if ("error" in parsed)
49843
- return usage3(ctx, parsed.error);
51314
+ return usage4(ctx, parsed.error);
49844
51315
  const [address, extra] = parsed.positionals;
49845
51316
  if (!address || extra !== undefined) {
49846
- return usage3(ctx, "Usage: candle tee enable <address> --vault <address> | --vault-key <label>");
51317
+ return usage4(ctx, "Usage: candle tee enable <address> --vault <address> | --vault-key <label>");
49847
51318
  }
49848
51319
  const vaultFlag = parsed.values["--vault"];
49849
51320
  const vaultKey = parsed.values["--vault-key"];
49850
51321
  if (vaultFlag !== undefined && vaultKey !== undefined) {
49851
- return usage3(ctx, "Use either --vault or --vault-key, not both.");
51322
+ return usage4(ctx, "Use either --vault or --vault-key, not both.");
49852
51323
  }
49853
51324
  let vault = vaultFlag;
49854
51325
  if (vaultKey !== undefined) {
@@ -49858,7 +51329,7 @@ async function teeEnable(args, ctx) {
49858
51329
  const { isVaultError: isVaultError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
49859
51330
  const resolvedVault = vaultPathFor2(ctx, parsed);
49860
51331
  if ("error" in resolvedVault)
49861
- return usage3(ctx, resolvedVault.error);
51332
+ return usage4(ctx, resolvedVault.error);
49862
51333
  const vaultPath = resolvedVault.path;
49863
51334
  const raw = await readVaultRaw2(vaultPath);
49864
51335
  if (raw === null) {
@@ -49896,11 +51367,11 @@ async function teeEnable(args, ctx) {
49896
51367
  }
49897
51368
  }
49898
51369
  if (!vault)
49899
- return usage3(ctx, "--vault <address> or --vault-key <label> is required: the destination every sweep sends to.");
51370
+ return usage4(ctx, "--vault <address> or --vault-key <label> is required: the destination every sweep sends to.");
49900
51371
  if (!isSolanaAddress(vault))
49901
- return usage3(ctx, "--vault is not a valid Solana address.");
51372
+ return usage4(ctx, "--vault is not a valid Solana address.");
49902
51373
  if (vault === address)
49903
- return usage3(ctx, "--vault must be a different address from the TEE wallet.");
51374
+ return usage4(ctx, "--vault must be a different address from the TEE wallet.");
49904
51375
  const vaultOwned = await addressOwnedByVault(ctx, address);
49905
51376
  if (vaultOwned === "usage")
49906
51377
  return 2;
@@ -50037,20 +51508,20 @@ async function teeFund(args, ctx) {
50037
51508
  return 1;
50038
51509
  const parsed = parseArgs(args, { valueFlags: ["--amount", "--asset", "--keystore"], pathFlags: ["--keystore"] });
50039
51510
  if ("error" in parsed)
50040
- return usage3(ctx, parsed.error);
51511
+ return usage4(ctx, parsed.error);
50041
51512
  const [address, extra] = parsed.positionals;
50042
51513
  if (!address || extra !== undefined)
50043
- return usage3(ctx, "Usage: candle tee fund <address> --amount <n> [--asset SOL|USDC]");
51514
+ return usage4(ctx, "Usage: candle tee fund <address> --amount <n> [--asset SOL|USDC]");
50044
51515
  const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
50045
51516
  if (asset !== "SOL" && asset !== "USDC")
50046
- return usage3(ctx, "--asset must be SOL or USDC.");
51517
+ return usage4(ctx, "--asset must be SOL or USDC.");
50047
51518
  const amount = parsed.values["--amount"];
50048
51519
  if (!amount)
50049
- return usage3(ctx, "--amount <n> is required.");
51520
+ return usage4(ctx, "--amount <n> is required.");
50050
51521
  const decimals = asset === "SOL" ? 9 : 6;
50051
51522
  const raw = decimalToRaw(amount, decimals);
50052
51523
  if (raw === null || raw === 0n)
50053
- return usage3(ctx, `--amount must be a positive decimal with at most ${decimals} decimal places.`);
51524
+ return usage4(ctx, `--amount must be a positive decimal with at most ${decimals} decimal places.`);
50054
51525
  const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
50055
51526
  if (!resolved.ok)
50056
51527
  return resolved.code;
@@ -50117,10 +51588,10 @@ async function teeStatus(args, ctx) {
50117
51588
  return 1;
50118
51589
  const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
50119
51590
  if ("error" in parsed)
50120
- return usage3(ctx, parsed.error);
51591
+ return usage4(ctx, parsed.error);
50121
51592
  const [address, extra] = parsed.positionals;
50122
51593
  if (!address || extra !== undefined)
50123
- return usage3(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
51594
+ return usage4(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
50124
51595
  const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
50125
51596
  if (!resolved.ok)
50126
51597
  return resolved.code;
@@ -50162,7 +51633,7 @@ async function teeStatus(args, ctx) {
50162
51633
  if (rpcUrl2) {
50163
51634
  const checked = rpcUrlFrom2(ctx, parsed);
50164
51635
  if (typeof checked !== "string")
50165
- return usage3(ctx, checked.error);
51636
+ return usage4(ctx, checked.error);
50166
51637
  const rpc2 = createSolanaRpc(checked, deps.fetch);
50167
51638
  try {
50168
51639
  const lamports = await rpc2.getBalance(address);
@@ -50243,10 +51714,10 @@ async function teeDisable(args, ctx) {
50243
51714
  return 1;
50244
51715
  const parsed = parseArgs(args, { valueFlags: ["--keystore"], pathFlags: ["--keystore"] });
50245
51716
  if ("error" in parsed)
50246
- return usage3(ctx, parsed.error);
51717
+ return usage4(ctx, parsed.error);
50247
51718
  const [address, extra] = parsed.positionals;
50248
51719
  if (!address || extra !== undefined)
50249
- return usage3(ctx, "Usage: candle tee disable <address>");
51720
+ return usage4(ctx, "Usage: candle tee disable <address>");
50250
51721
  await printIdentity(ctx);
50251
51722
  const openedActive = await openActiveTee(ctx, parsed, address, "read");
50252
51723
  if (!openedActive.ok)
@@ -50341,6 +51812,9 @@ function refusalState(raw) {
50341
51812
  async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions, pending, recordPending, clearPending) {
50342
51813
  const blockhash = await rpc2.getLatestBlockhash();
50343
51814
  const message = compileLegacyMessage({ feePayer, recentBlockhash: blockhash, instructions });
51815
+ return broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending);
51816
+ }
51817
+ async function broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending) {
50344
51818
  const signatureBytes = signMessage(message, secret);
50345
51819
  const signature = base58.encode(signatureBytes);
50346
51820
  const wire = serializeSignedTransaction(message, signatureBytes);
@@ -50366,7 +51840,7 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
50366
51840
  error: `send did not answer cleanly (${error instanceof Error ? error.message : error}); it may still land`
50367
51841
  };
50368
51842
  }
50369
- for (let i = 0;i < CONFIRM_MAX_POLLS2; i++) {
51843
+ for (let i = 0;i < CONFIRM_MAX_POLLS3; i++) {
50370
51844
  let status;
50371
51845
  try {
50372
51846
  status = await rpc2.getSignatureStatus(signature);
@@ -50388,12 +51862,12 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
50388
51862
  error: `transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}`
50389
51863
  };
50390
51864
  }
50391
- await deps.sleep(CONFIRM_POLL_MS2);
51865
+ await deps.sleep(CONFIRM_POLL_MS3);
50392
51866
  }
50393
51867
  return {
50394
51868
  status: "uncertain",
50395
51869
  signature,
50396
- error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS2 * CONFIRM_POLL_MS2 / 1000}s; it may still land${echoNote}`
51870
+ error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS3 * CONFIRM_POLL_MS3 / 1000}s; it may still land${echoNote}`
50397
51871
  };
50398
51872
  }
50399
51873
  async function teeSweep(args, ctx) {
@@ -50406,13 +51880,13 @@ async function teeSweep(args, ctx) {
50406
51880
  pathFlags: ["--keystore"]
50407
51881
  });
50408
51882
  if ("error" in parsed)
50409
- return usage3(ctx, parsed.error);
51883
+ return usage4(ctx, parsed.error);
50410
51884
  const [address, extra] = parsed.positionals;
50411
51885
  if (!address || extra !== undefined)
50412
- return usage3(ctx, "Usage: candle tee sweep <address> --rpc-url <url> [--emergency]");
51886
+ return usage4(ctx, "Usage: candle tee sweep <address> --rpc-url <url> [--emergency]");
50413
51887
  const rpcUrl2 = rpcUrlFrom2(ctx, parsed);
50414
51888
  if (typeof rpcUrl2 !== "string")
50415
- return usage3(ctx, rpcUrl2.error);
51889
+ return usage4(ctx, rpcUrl2.error);
50416
51890
  const emergency = parsed.booleans.has("--emergency");
50417
51891
  const openedActive = await openActiveTee(ctx, parsed, address, "sign");
50418
51892
  if (!openedActive.ok)
@@ -50615,6 +52089,116 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
50615
52089
  pendingStill.push(p);
50616
52090
  signingBlocked = true;
50617
52091
  }
52092
+ const candidateAccounts = new Set;
52093
+ const lpCloses = [];
52094
+ {
52095
+ let token2022Accounts = [];
52096
+ try {
52097
+ token2022Accounts = await rpc2.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID);
52098
+ } catch (error) {
52099
+ residuals.push({
52100
+ kind: "inventory",
52101
+ detail: `could not list Token-2022 accounts for position discovery: ${error instanceof Error ? error.message : error}`
52102
+ });
52103
+ }
52104
+ const candidates = token2022Accounts.filter(isPositionCandidate);
52105
+ for (const acct of candidates)
52106
+ candidateAccounts.add(acct.pubkey);
52107
+ const leftover = (kind, acct, detail) => residuals.push({ kind, detail, mint: acct.mint, account: acct.pubkey, amountRaw: acct.amountRaw });
52108
+ if (!emergency && candidates.length > 0) {
52109
+ if (!json)
52110
+ deps.stdout.write(` ${candidates.length} position NFT candidate(s) (Token-2022, amount 1, decimals 0)
52111
+ `);
52112
+ const verified = [];
52113
+ for (const acct of candidates) {
52114
+ if (signingBlocked)
52115
+ break;
52116
+ if (!apiKey || !entry.linkedWalletId) {
52117
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "no bound API key is available to build the close; the NFT was left in place");
52118
+ continue;
52119
+ }
52120
+ const result = await apiRequest(`/api/v1/agent/lp/positions/${encodeURIComponent(acct.mint)}/close-build`, {
52121
+ method: "POST",
52122
+ auth: "key",
52123
+ credentials: { apiKey },
52124
+ apiUrl,
52125
+ fetch: deps.fetch,
52126
+ env: deps.env,
52127
+ body: {}
52128
+ });
52129
+ if (!result.ok) {
52130
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `close-build ${result.code ?? `HTTP ${result.status}`}: ${result.message}; the NFT was left in place`);
52131
+ continue;
52132
+ }
52133
+ const artifact = parseCloseArtifact(result.body);
52134
+ if (artifact === undefined) {
52135
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "close-build answered without a transaction and an ordered account key array; the NFT was left in place");
52136
+ continue;
52137
+ }
52138
+ const verdict = await verifyCloseArtifact({
52139
+ artifact,
52140
+ rpc: rpc2,
52141
+ tee: address,
52142
+ vault,
52143
+ nftMint: acct.mint,
52144
+ nftAccount: acct.pubkey
52145
+ });
52146
+ if (!verdict.ok) {
52147
+ leftover(DAMM_CLOSE_TRANSACTION_REFUSED, acct, `${verdict.reason}; refused before signing`);
52148
+ continue;
52149
+ }
52150
+ verified.push({ acct, verdict });
52151
+ }
52152
+ for (const { acct, verdict } of verified) {
52153
+ if (signingBlocked) {
52154
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "not signed: an earlier transaction is still in flight");
52155
+ continue;
52156
+ }
52157
+ let blockhashValid;
52158
+ try {
52159
+ blockhashValid = await rpc2.isBlockhashValid(verdict.tx.message.recentBlockhash, "confirmed");
52160
+ } catch (error) {
52161
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `could not check the close build's blockhash: ${error instanceof Error ? error.message : error}; not signed`);
52162
+ continue;
52163
+ }
52164
+ if (!blockhashValid) {
52165
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "the close build's blockhash has expired; not signed, re-run the sweep for a fresh close");
52166
+ continue;
52167
+ }
52168
+ const display = describeClose({ verdict, tee: address, vault, nftMint: acct.mint, nftAccount: acct.pubkey });
52169
+ lpCloses.push({ mint: acct.mint, account: acct.pubkey, display });
52170
+ if (!json) {
52171
+ deps.stdout.write(` Close for position ${acct.mint} (verified, unsigned; about to sign):
52172
+ `);
52173
+ for (const line of display)
52174
+ deps.stdout.write(` ${line}
52175
+ `);
52176
+ }
52177
+ const pending = {
52178
+ kind: "lp-close",
52179
+ mint: acct.mint,
52180
+ account: acct.pubkey,
52181
+ amountRaw: acct.amountRaw
52182
+ };
52183
+ const outcome = await broadcastMessage(rpc2, deps, secret, verdict.tx.message.bytes, verdict.tx.message.recentBlockhash, pending, recordPending, clearPending);
52184
+ if (outcome.status !== "finalized") {
52185
+ if (!settle2(outcome, pending, "lp-close"))
52186
+ signingBlocked = true;
52187
+ continue;
52188
+ }
52189
+ await retainReceipt({
52190
+ kind: "lp-close",
52191
+ mint: acct.mint,
52192
+ amountRaw: acct.amountRaw,
52193
+ signature: outcome.signature,
52194
+ finalizedAt: new Date(deps.now()).toISOString()
52195
+ });
52196
+ if (!json)
52197
+ deps.stdout.write(` closed LP position ${acct.mint} and withdrew its liquidity: ${outcome.signature}
52198
+ `);
52199
+ }
52200
+ }
52201
+ }
50618
52202
  const tokenAccounts = [];
50619
52203
  for (const programId of [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
50620
52204
  try {
@@ -50631,6 +52215,13 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
50631
52215
  if (signingBlocked)
50632
52216
  break;
50633
52217
  const token2022 = acct.programId === TOKEN_2022_PROGRAM_ID;
52218
+ if (candidateAccounts.has(acct.pubkey)) {
52219
+ if (!emergency)
52220
+ continue;
52221
+ if (!json)
52222
+ deps.stdout.write(` ${acct.mint}: position NFT moved as-is to the vault (emergency; unwind it from a fresh TEE wallet)
52223
+ `);
52224
+ }
50634
52225
  if (acct.state !== "initialized") {
50635
52226
  residuals.push({
50636
52227
  kind: token2022 && acct.state === "frozen" ? "TOKEN_2022_FROZEN" : "frozen-or-uninitialized",
@@ -50885,7 +52476,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
50885
52476
  signature: p.signature
50886
52477
  })),
50887
52478
  inventory,
50888
- recordedOnServer
52479
+ recordedOnServer,
52480
+ ...lpCloses.length > 0 ? { lpCloses } : {}
50889
52481
  })}
50890
52482
  `);
50891
52483
  return finalState === "swept" ? 0 : 3;
@@ -50968,8 +52560,7 @@ function launchWarning(keyPrefix, toKey, rows) {
50968
52560
  return `Warning: key ${keyPrefix} lacks launch:write; ${launchers.join(", ")} ${launchers.length === 1 ? "has" : "have"} allowLaunch but cannot launch under it.`;
50969
52561
  }
50970
52562
  var RELAY_SIGNER_LINE = "The relay signer does not move: trade these wallets from the machine that promoted them.";
50971
- function writeRebindFailure(ctx, result, context) {
50972
- const { deps, apiUrl, json } = ctx;
52563
+ function rebindFailureDetails(result, context) {
50973
52564
  let code = result.code;
50974
52565
  let message = result.message;
50975
52566
  let suggestion;
@@ -50989,31 +52580,61 @@ function writeRebindFailure(ctx, result, context) {
50989
52580
  } else if (result.code === "REBIND_STALE") {
50990
52581
  suggestion = "Run the command again; the preview will show the current binding.";
50991
52582
  }
52583
+ return {
52584
+ code: code ?? `HTTP ${result.status}`,
52585
+ message,
52586
+ ...suggestion !== undefined ? { suggestion } : {},
52587
+ status: result.status
52588
+ };
52589
+ }
52590
+ function writeRebindFailure(ctx, result, context) {
52591
+ const { deps, apiUrl, json } = ctx;
52592
+ const { code, message, suggestion } = rebindFailureDetails(result, context);
50992
52593
  const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
50993
52594
  if (json) {
50994
52595
  deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
50995
52596
  `);
50996
52597
  } else {
50997
- deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
52598
+ deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
50998
52599
  `);
50999
52600
  }
51000
52601
  return 1;
51001
52602
  }
51002
- async function resolveTargetKey(ctx, deviceToken, raw) {
51003
- const { deps, apiUrl, json } = ctx;
51004
- if (KEY_PREFIX_RE.test(raw))
51005
- return { ok: true, keyPrefix: raw };
51006
- const result = await apiRequest(KEYS_PATH2, {
52603
+ async function listAccountKeys(ctx, deviceToken) {
52604
+ const { deps, apiUrl } = ctx;
52605
+ return apiRequest(KEYS_PATH2, {
51007
52606
  auth: "device",
51008
52607
  credentials: { deviceToken },
51009
52608
  apiUrl,
51010
52609
  fetch: deps.fetch,
51011
52610
  env: deps.env
51012
52611
  });
51013
- if (!result.ok) {
51014
- return { ok: false, code: writeRebindFailure(ctx, result, {}) };
52612
+ }
52613
+ async function postRebind(ctx, deviceToken, body) {
52614
+ const { deps, apiUrl } = ctx;
52615
+ return apiRequest(REBIND_PATH, {
52616
+ method: "POST",
52617
+ body,
52618
+ auth: "device",
52619
+ credentials: { deviceToken },
52620
+ apiUrl,
52621
+ fetch: deps.fetch,
52622
+ env: deps.env
52623
+ });
52624
+ }
52625
+ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
52626
+ const { deps, json } = ctx;
52627
+ if (KEY_PREFIX_RE.test(raw))
52628
+ return { ok: true, keyPrefix: raw };
52629
+ let listed = opts.keys;
52630
+ if (listed === undefined) {
52631
+ const result = await listAccountKeys(ctx, deviceToken);
52632
+ if (!result.ok) {
52633
+ return { ok: false, code: writeRebindFailure(ctx, result, {}) };
52634
+ }
52635
+ listed = result.body?.keys ?? [];
51015
52636
  }
51016
- const keys = (result.body?.keys ?? []).filter((key) => !key.revokedAt);
52637
+ const keys = listed.filter((key) => !key.revokedAt);
51017
52638
  const matches = keys.filter((key) => key.label === raw);
51018
52639
  if (matches.length === 1)
51019
52640
  return { ok: true, keyPrefix: matches[0].keyPrefix };
@@ -51071,15 +52692,7 @@ async function teeRebind(args, ctx) {
51071
52692
  const target = await resolveTargetKey(ctx, deviceToken, toKey);
51072
52693
  if (!target.ok)
51073
52694
  return target.code;
51074
- const call = (body) => apiRequest(REBIND_PATH, {
51075
- method: "POST",
51076
- body,
51077
- auth: "device",
51078
- credentials: { deviceToken },
51079
- apiUrl,
51080
- fetch: deps.fetch,
51081
- env: deps.env
51082
- });
52695
+ const call = (body) => postRebind(ctx, deviceToken, body);
51083
52696
  const preview = await call({
51084
52697
  dryRun: true,
51085
52698
  toKeyPrefix: target.keyPrefix,
@@ -53960,9 +55573,18 @@ function decimalToRaw2(decimal, decimals) {
53960
55573
  }
53961
55574
  function assertVaultSigner(entry) {
53962
55575
  if (entry.role !== "vault") {
53963
- throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is a TEE wallet entry and cannot sign vault transfer or fund shapes (ED-10 / N3).`);
55576
+ throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is not a vault key, and vault fund signs from vault keys only (ED-10).`, {
55577
+ suggestion: `To move funds out of a promoted wallet, use: candle vault transfer <to> --from ${entry.label ?? entry.address}`
55578
+ });
53964
55579
  }
53965
55580
  }
55581
+ function assertTransferSigner(entry) {
55582
+ if (entry.role === "vault" || entry.role === "tee-wallet")
55583
+ return;
55584
+ throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is an external wallet and cannot sign vault transfer shapes (ED-10).`, {
55585
+ suggestion: `Move funds out of an external wallet with: candle external sweep ${entry.label} --to <vault> --rpc-url <url>`
55586
+ });
55587
+ }
53966
55588
  function namedSendFailure(plan) {
53967
55589
  if (!plan.token)
53968
55590
  return;
@@ -54707,9 +56329,9 @@ init_render();
54707
56329
  init_solana_lite();
54708
56330
  init_store();
54709
56331
  init_vault_support();
54710
- var CHUNK = 100;
56332
+ var CHUNK2 = 100;
54711
56333
  var LAMPORTS_PER_SOL = 1000000000n;
54712
- function formatSol2(lamports) {
56334
+ function formatSol3(lamports) {
54713
56335
  const whole = lamports / LAMPORTS_PER_SOL;
54714
56336
  const fraction = (lamports % LAMPORTS_PER_SOL).toString().padStart(9, "0").replace(/0+$/, "");
54715
56337
  return fraction === "" ? whole.toString() : `${whole}.${fraction}`;
@@ -54719,8 +56341,8 @@ async function readLamports(addresses, rpcUrl2, fetchFn) {
54719
56341
  const lamports = new Map;
54720
56342
  const unavailable = [];
54721
56343
  let failure;
54722
- for (let at = 0;at < addresses.length; at += CHUNK) {
54723
- const chunk = addresses.slice(at, at + CHUNK);
56344
+ for (let at = 0;at < addresses.length; at += CHUNK2) {
56345
+ const chunk = addresses.slice(at, at + CHUNK2);
54724
56346
  try {
54725
56347
  const accounts = await rpc2.getMultipleAccounts(chunk);
54726
56348
  for (const [i, address] of chunk.entries())
@@ -54771,7 +56393,7 @@ async function vaultList(args, ctx) {
54771
56393
  const all = vault.index.entries;
54772
56394
  const matched = filter === undefined ? all : all.filter((entry) => matches(entry, filter));
54773
56395
  const solana = balances ? matched.filter((entry) => entry.chain === "solana") : [];
54774
- const requests = Math.ceil(solana.length / CHUNK);
56396
+ const requests = Math.ceil(solana.length / CHUNK2);
54775
56397
  const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
54776
56398
  if (balances && solana.length > 0 && rpcHost !== undefined) {
54777
56399
  deps.stderr.write(`Reading SOL for ${solana.length} addresses from ${rpcHost}, in ${requests} ${requests === 1 ? "request" : "requests"}. That endpoint sees all ${solana.length} together.
@@ -54833,7 +56455,7 @@ async function vaultList(args, ctx) {
54833
56455
  const row = [entry.address, entry.label || "(none)", entry.role, entry.derivation?.path ?? "-"];
54834
56456
  if (balances) {
54835
56457
  const held = lamports.get(entry.address);
54836
- row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol2(held));
56458
+ row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol3(held));
54837
56459
  }
54838
56460
  return row;
54839
56461
  });
@@ -54842,9 +56464,9 @@ ${renderTable(headers, rows)}
54842
56464
  `);
54843
56465
  if (balances) {
54844
56466
  deps.stdout.write(complete ? `
54845
- total ${formatSol2(totalLamports)} SOL across ${solana.length} keys
56467
+ total ${formatSol3(totalLamports)} SOL across ${solana.length} keys
54846
56468
  ` : `
54847
- total ${formatSol2(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
56469
+ total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
54848
56470
  `);
54849
56471
  }
54850
56472
  return complete ? 0 : 3;
@@ -54870,6 +56492,7 @@ async function vaultPhrase(args, ctx) {
54870
56492
  init_esm();
54871
56493
  init_args();
54872
56494
  init_deps();
56495
+ init_profiles();
54873
56496
  init_render();
54874
56497
  init_solana_lite();
54875
56498
 
@@ -54976,12 +56599,299 @@ function restoreRefusedFailure(init, error) {
54976
56599
  // src/commands/vault-promote.ts
54977
56600
  init_errors();
54978
56601
  init_promote_support();
56602
+ // src/vault/promote-to-key.ts
56603
+ init_deps();
56604
+ init_render();
56605
+ var REBIND_CHUNK = 200;
56606
+ var SELECTED_SCOPE_LIMIT = 50;
56607
+ function targetKeyRefusal(row, keyPrefix, now) {
56608
+ const written = "Nothing was written.";
56609
+ if (row === undefined) {
56610
+ return {
56611
+ code: "REBIND_KEY_NOT_FOUND",
56612
+ message: `No key ${keyPrefix} on this account. ${written}`,
56613
+ suggestion: "The target must be a key on the same account: candle keys list"
56614
+ };
56615
+ }
56616
+ if (row.revokedAt !== undefined) {
56617
+ return {
56618
+ code: "REBIND_TARGET_INVALID",
56619
+ message: `The target key ${keyPrefix} is revoked. ${written}`,
56620
+ suggestion: "Pick an active key: candle keys list"
56621
+ };
56622
+ }
56623
+ if (row.expiresAt !== undefined && row.expiresAt !== null && row.expiresAt <= now) {
56624
+ return {
56625
+ code: "REBIND_TARGET_INVALID",
56626
+ message: `The target key ${keyPrefix} has expired. ${written}`,
56627
+ suggestion: "Pick an active key: candle keys list"
56628
+ };
56629
+ }
56630
+ if (row.environment !== "production") {
56631
+ return {
56632
+ code: "REBIND_TARGET_INVALID",
56633
+ message: `The target key ${keyPrefix} is a test key; the trade rail refuses test keys. ${written}`,
56634
+ suggestion: "Pick a production key: candle keys list"
56635
+ };
56636
+ }
56637
+ if (!row.scopes.includes("swap:write")) {
56638
+ return {
56639
+ code: "REBIND_TARGET_INVALID",
56640
+ message: `The target key ${keyPrefix} lacks swap:write, which a TEE wallet needs to trade. ${written}`,
56641
+ suggestion: "Pick a key with swap:write, or mint one: candle keys create"
56642
+ };
56643
+ }
56644
+ return null;
56645
+ }
56646
+ function tradeReadinessOf(row) {
56647
+ const hasTxLimit = row.txLimit !== null && row.txLimit !== undefined;
56648
+ const limits = row.spendLimits ?? [];
56649
+ const cap = (asset) => limits.some((limit) => limit.asset === asset.toLowerCase() || limit.asset === asset.toUpperCase());
56650
+ const sol = cap("sol");
56651
+ const usdc = cap("usdc");
56652
+ const missingCaps = [];
56653
+ if (!hasTxLimit)
56654
+ missingCaps.push("txLimit");
56655
+ if (!sol)
56656
+ missingCaps.push("spendLimits.sol");
56657
+ if (!usdc)
56658
+ missingCaps.push("spendLimits.usdc");
56659
+ return { tradeReady: { sol: hasTxLimit && sol, usdc: hasTxLimit && usdc }, missingCaps };
56660
+ }
56661
+ function targetWarnings(target) {
56662
+ return capWarnings(target.keyPrefix, {
56663
+ keyPrefix: target.keyPrefix,
56664
+ label: target.label,
56665
+ paused: target.paused,
56666
+ walletScope: target.walletScope,
56667
+ tradeReady: target.tradeReady,
56668
+ missingCaps: target.missingCaps,
56669
+ launchScope: target.launchScope
56670
+ });
56671
+ }
56672
+ async function preflightToKey(ctx, raw, opts) {
56673
+ const { deps, json } = ctx;
56674
+ const refuse2 = (failure) => {
56675
+ writeLocalFailure(deps, failure, json);
56676
+ return { ok: false, exit: 1 };
56677
+ };
56678
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
56679
+ if (!deviceToken)
56680
+ return refuse2(DEVICE_TOKEN_REQUIRED);
56681
+ const listing = await listAccountKeys(ctx, deviceToken);
56682
+ if (!listing.ok) {
56683
+ return refuse2({
56684
+ code: listing.code ?? `HTTP ${listing.status}`,
56685
+ message: `Could not read this account's keys to check --to-key (${listing.status === 0 ? listing.message : `HTTP ${listing.status}`}). Nothing was written.`,
56686
+ suggestion: "Check the device token with: candle auth status"
56687
+ });
56688
+ }
56689
+ const keys = listing.body?.keys ?? [];
56690
+ const resolved = await resolveTargetKey(ctx, deviceToken, raw, { keys });
56691
+ if (!resolved.ok)
56692
+ return { ok: false, exit: resolved.code };
56693
+ const keyPrefix = resolved.keyPrefix;
56694
+ const row = keys.find((key) => key.keyPrefix === keyPrefix);
56695
+ const refusal = targetKeyRefusal(row, keyPrefix, deps.now());
56696
+ if (refusal !== null)
56697
+ return refuse2(refusal);
56698
+ const target = row;
56699
+ const walletScope = target.walletScope === "selected" ? "selected" : "all";
56700
+ if (walletScope === "selected") {
56701
+ const room = await readSelectedScopeRoom(ctx, keyPrefix);
56702
+ if (room.ok) {
56703
+ const moving = opts.labels.filter((label) => !room.heldLabels.has(label)).length;
56704
+ if (room.held + moving > SELECTED_SCOPE_LIMIT) {
56705
+ return refuse2({
56706
+ code: "REBIND_SCOPE_FULL",
56707
+ message: `Key ${keyPrefix} is scoped to selected wallets and holds ${room.held} of ${SELECTED_SCOPE_LIMIT}; the ${moving} this run would move do not fit. Nothing was written.`,
56708
+ suggestion: "Widen the key's wallet scope from the portal, or name a key with room."
56709
+ });
56710
+ }
56711
+ } else {
56712
+ deps.stderr.write(`Could not read key ${keyPrefix}'s wallet set (${room.reason}); its selected-scope room is checked at the rebind.
56713
+ `);
56714
+ }
56715
+ }
56716
+ const readiness = tradeReadinessOf(target);
56717
+ return {
56718
+ ok: true,
56719
+ deviceToken,
56720
+ target: {
56721
+ keyPrefix,
56722
+ label: typeof target.label === "string" && target.label.length > 0 ? target.label : null,
56723
+ walletScope,
56724
+ paused: target.pausedAt !== undefined && target.pausedAt !== null,
56725
+ launchScope: target.scopes.includes("launch:write"),
56726
+ ...readiness
56727
+ }
56728
+ };
56729
+ }
56730
+ async function readSelectedScopeRoom(ctx, keyPrefix) {
56731
+ const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
56732
+ if (!apiKey)
56733
+ return { ok: false, reason: "no API key" };
56734
+ const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`, {
56735
+ auth: "key",
56736
+ credentials: { apiKey },
56737
+ apiUrl: ctx.apiUrl,
56738
+ fetch: ctx.deps.fetch,
56739
+ env: ctx.deps.env
56740
+ });
56741
+ if (!result.ok)
56742
+ return { ok: false, reason: result.status === 0 ? result.message : `HTTP ${result.status}` };
56743
+ const wallets2 = result.body?.wallets;
56744
+ if (!Array.isArray(wallets2))
56745
+ return { ok: false, reason: "no wallet list in the response" };
56746
+ const heldLabels = new Set;
56747
+ for (const wallet of wallets2)
56748
+ if (typeof wallet?.label === "string")
56749
+ heldLabels.add(wallet.label);
56750
+ return { ok: true, held: wallets2.length, heldLabels };
56751
+ }
56752
+ function chunkWallets(wallets2, size = REBIND_CHUNK) {
56753
+ const out = [];
56754
+ for (let at = 0;at < wallets2.length; at += size)
56755
+ out.push(wallets2.slice(at, at + size));
56756
+ return out;
56757
+ }
56758
+ async function rebindPromoted(ctx, deviceToken, toKeyPrefix, wallets2) {
56759
+ const run = { ok: true, requests: 0, outcomes: new Map };
56760
+ for (const wallet of wallets2)
56761
+ run.outcomes.set(wallet.id, { state: "not-reached" });
56762
+ const chunks = chunkWallets(wallets2);
56763
+ for (const [at, chunk] of chunks.entries()) {
56764
+ run.requests += 1;
56765
+ const preview = await postRebind(ctx, deviceToken, {
56766
+ dryRun: true,
56767
+ toKeyPrefix,
56768
+ wallets: chunk.map((wallet) => wallet.id)
56769
+ });
56770
+ if (!preview.ok) {
56771
+ run.ok = false;
56772
+ run.failure = { ...rebindFailureDetails(preview, {}), chunk: at + 1 };
56773
+ return run;
56774
+ }
56775
+ const shown = preview.body;
56776
+ if (run.toKey === undefined)
56777
+ run.toKey = shown.toKey;
56778
+ for (const row of shown.unchanged)
56779
+ run.outcomes.set(row.id, { state: "unchanged" });
56780
+ const moving = shown.rebound;
56781
+ if (moving.length === 0)
56782
+ continue;
56783
+ run.requests += 1;
56784
+ const committed = await postRebind(ctx, deviceToken, {
56785
+ toKeyPrefix,
56786
+ walletIds: moving.map((row) => row.id),
56787
+ expect: Object.fromEntries(moving.map((row) => [row.id, row.fromKeyPrefix]))
56788
+ });
56789
+ if (!committed.ok) {
56790
+ run.ok = false;
56791
+ run.failure = {
56792
+ ...rebindFailureDetails(committed, {
56793
+ fromKeyPrefixes: Array.from(new Set(moving.map((row) => row.fromKeyPrefix)))
56794
+ }),
56795
+ chunk: at + 1
56796
+ };
56797
+ for (const row of moving)
56798
+ run.outcomes.set(row.id, { state: "failed" });
56799
+ return run;
56800
+ }
56801
+ const result = committed.body;
56802
+ for (const row of result.rebound) {
56803
+ run.outcomes.set(row.id, { state: "rebound", ...row.auditId !== undefined ? { auditId: row.auditId } : {} });
56804
+ }
56805
+ for (const row of result.unchanged)
56806
+ run.outcomes.set(row.id, { state: "unchanged" });
56807
+ }
56808
+ return run;
56809
+ }
56810
+ function finishingCommands(addresses, toKeyPrefix) {
56811
+ return chunkWallets(addresses).map((chunk) => `candle tee rebind ${chunk.join(" ")} --to-key ${toKeyPrefix}`);
56812
+ }
56813
+ function pendingWallets(wallets2, run) {
56814
+ return wallets2.filter((wallet) => {
56815
+ const state = run?.outcomes.get(wallet.id)?.state;
56816
+ return state !== "rebound" && state !== "unchanged";
56817
+ });
56818
+ }
56819
+ function renderRebindReport(input) {
56820
+ const { target, wallets: wallets2, run, notRebindable } = input;
56821
+ const lines = [];
56822
+ const name = (wallet) => `${wallet.label} (${wallet.address})`;
56823
+ if (run !== undefined) {
56824
+ const rebound = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "rebound").length;
56825
+ const unchanged = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "unchanged").length;
56826
+ if (rebound > 0 || unchanged > 0) {
56827
+ lines.push(`✓ ${rebound} wallet${rebound === 1 ? "" : "s"} moved to key ${target.keyPrefix}${unchanged > 0 ? ` (${unchanged} already there)` : ""} in ${run.requests} request${run.requests === 1 ? "" : "s"}.`);
56828
+ }
56829
+ if (run.failure !== undefined) {
56830
+ lines.push(`Rebind ${run.failure.code}: ${run.failure.message}${run.failure.suggestion ? ` ${run.failure.suggestion}` : ""}`);
56831
+ }
56832
+ }
56833
+ const pending = pendingWallets(wallets2, run);
56834
+ if (pending.length > 0) {
56835
+ lines.push(`${pending.length} promoted wallet${pending.length === 1 ? " is" : "s are"} still bound to the calling key ${input.callingKeyPrefix}, not ${target.keyPrefix}:`, ...pending.map((wallet) => ` ${name(wallet)}`), "Finish with:", ...finishingCommands(pending.map((wallet) => wallet.address), target.keyPrefix).map((command) => ` ${command}`));
56836
+ }
56837
+ if (notRebindable.length > 0) {
56838
+ lines.push(`${notRebindable.length} wallet${notRebindable.length === 1 ? "" : "s"} cannot be rebound yet (only a verified-active wallet moves):`, ...notRebindable.map((wallet) => ` ${name(wallet)}: ${wallet.reason}`));
56839
+ }
56840
+ return lines.join(`
56841
+ `);
56842
+ }
56843
+ function walletRebindJson(wallet, run) {
56844
+ if (wallet.id === null || wallet.remoteAuthority !== "verified-active") {
56845
+ return {
56846
+ state: "not-rebindable",
56847
+ reason: wallet.id === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
56848
+ };
56849
+ }
56850
+ const outcome = run?.outcomes.get(wallet.id);
56851
+ if (outcome === undefined)
56852
+ return { state: "not-reached" };
56853
+ return { state: outcome.state, ...outcome.auditId !== undefined ? { auditId: outcome.auditId } : {} };
56854
+ }
56855
+ function finalBoundKey(wallet, run, toKeyPrefix) {
56856
+ const state = wallet.id === null ? undefined : run?.outcomes.get(wallet.id)?.state;
56857
+ return state === "rebound" || state === "unchanged" ? toKeyPrefix : wallet.importedTo;
56858
+ }
56859
+ function rebindJson(input) {
56860
+ const { target, wallets: wallets2, run } = input;
56861
+ const pending = pendingWallets(wallets2, run);
56862
+ const count = (state) => wallets2.filter((wallet) => run?.outcomes.get(wallet.id)?.state === state).length;
56863
+ return {
56864
+ toKey: {
56865
+ keyPrefix: target.keyPrefix,
56866
+ label: target.label,
56867
+ tradeReady: run?.toKey?.tradeReady ?? target.tradeReady,
56868
+ missingCaps: run?.toKey?.missingCaps ?? target.missingCaps
56869
+ },
56870
+ rebind: {
56871
+ ok: run !== undefined && run.ok && input.notRebindable.length === 0,
56872
+ reached: run !== undefined,
56873
+ requests: run?.requests ?? 0,
56874
+ rebound: count("rebound"),
56875
+ unchanged: count("unchanged"),
56876
+ pending: pending.map((wallet) => wallet.address),
56877
+ notRebindable: input.notRebindable.length,
56878
+ ...run?.failure !== undefined ? {
56879
+ code: run.failure.code,
56880
+ message: run.failure.message,
56881
+ ...run.failure.suggestion !== undefined ? { suggestion: run.failure.suggestion } : {}
56882
+ } : {},
56883
+ finishWith: finishingCommands(pending.map((wallet) => wallet.address), target.keyPrefix)
56884
+ }
56885
+ };
56886
+ }
56887
+
56888
+ // src/commands/vault-promote.ts
54979
56889
  init_signer_roles();
54980
56890
  init_store();
54981
56891
  init_vault_support();
54982
56892
  async function vaultPromote(args, ctx) {
54983
56893
  const parsed = parseArgs(args, {
54984
- valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore"],
56894
+ valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore", "--to-key"],
54985
56895
  booleanFlags: ["--accept-unknown-exposure", "--accept-older-copy"],
54986
56896
  pathFlags: ["--keystore"]
54987
56897
  });
@@ -54995,21 +56905,73 @@ async function vaultPromote(args, ctx) {
54995
56905
  return usage(ctx, "Use either --from or --in-place, not both.");
54996
56906
  }
54997
56907
  if (fromLabel === undefined && inPlaceLabel === undefined) {
54998
- return usage(ctx, "Usage: candle vault promote --from <vault-key-label> | --in-place <vault-key-label> --sweep-to <label> --rpc-url <url>");
56908
+ return usage(ctx, "Usage: candle vault promote --from <vault-key-label> | --in-place <vault-key-label> --sweep-to <label> --rpc-url <url> [--to-key <label|prefix>]");
54999
56909
  }
56910
+ const toKeyRaw = parsed.values["--to-key"];
56911
+ if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
56912
+ return usage(ctx, "--to-key needs a label or a prefix.");
55000
56913
  if (!refuseEnvPassphrase(ctx))
55001
56914
  return 1;
55002
56915
  if (!requireTty(ctx, "vault promote"))
55003
56916
  return 1;
55004
- if (fromLabel !== undefined) {
55005
- if (parsed.values["--sweep-to"] !== undefined) {
55006
- return usage(ctx, "Fresh-key promote takes --from, not --sweep-to.");
56917
+ if (fromLabel !== undefined && parsed.values["--sweep-to"] !== undefined) {
56918
+ return usage(ctx, "Fresh-key promote takes --from, not --sweep-to.");
56919
+ }
56920
+ let toKey;
56921
+ if (toKeyRaw !== undefined) {
56922
+ const label = parsed.values["--label"] ?? inPlaceLabel ?? (fromLabel !== undefined ? "<fresh>" : undefined);
56923
+ const preflight = await preflightToKey(ctx, toKeyRaw, { labels: label !== undefined ? [label] : [] });
56924
+ if (!preflight.ok)
56925
+ return preflight.exit;
56926
+ toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
56927
+ }
56928
+ if (fromLabel !== undefined)
56929
+ return promoteFresh(ctx, parsed, fromLabel, toKey);
56930
+ return promoteInPlace(ctx, parsed, inPlaceLabel, toKey);
56931
+ }
56932
+ async function rebindAfterImport(ctx, toKey, wallet, callingKeyPrefix) {
56933
+ const rebindable = wallet.linkedWalletId !== null && wallet.remoteAuthority === "verified-active" ? [{ id: wallet.linkedWalletId, address: wallet.address, label: wallet.label }] : [];
56934
+ const notRebindable = rebindable.length === 0 ? [
56935
+ {
56936
+ address: wallet.address,
56937
+ label: wallet.label,
56938
+ reason: wallet.linkedWalletId === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
56939
+ }
56940
+ ] : [];
56941
+ const run = rebindable.length > 0 ? await rebindPromoted(ctx, toKey.deviceToken, toKey.target.keyPrefix, rebindable) : undefined;
56942
+ const input = { target: toKey.target, callingKeyPrefix, wallets: rebindable, run, notRebindable };
56943
+ const report = renderRebindReport(input);
56944
+ if (report.length > 0)
56945
+ ctx.deps.stderr.write(`${report}
56946
+ `);
56947
+ const id = wallet.linkedWalletId;
56948
+ return {
56949
+ exit: run !== undefined && !run.ok ? 1 : 0,
56950
+ json: {
56951
+ ...rebindJson(input),
56952
+ boundKeyPrefix: finalBoundKey({ id, importedTo: wallet.importedTo }, run, toKey.target.keyPrefix),
56953
+ walletRebind: walletRebindJson({ id, remoteAuthority: wallet.remoteAuthority }, run)
55007
56954
  }
55008
- return promoteFresh(ctx, parsed, fromLabel);
56955
+ };
56956
+ }
56957
+ async function confirmResumeRebind(ctx, toKey) {
56958
+ const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
56959
+ ctx.deps.stderr.write(`${[
56960
+ `This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
56961
+ ...targetWarnings(toKey.target)
56962
+ ].join(`
56963
+ `)}
56964
+ `);
56965
+ const typed = await ctx.deps.promptLine(`Type ${CONFIRM_WORD} to move this wallet to ${toKey.target.keyPrefix}: `);
56966
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
56967
+ throw new VaultError("PROMOTE_NOT_ACKNOWLEDGED", `The acknowledgement is the word ${CONFIRM_WORD}; nothing was moved, and nothing was written.`, { suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.` });
55009
56968
  }
55010
- return promoteInPlace(ctx, parsed, inPlaceLabel);
55011
56969
  }
55012
- async function promoteFresh(ctx, parsed, fromLabel) {
56970
+ async function callingKeyPrefixFor(ctx) {
56971
+ const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
56972
+ return (apiKey !== undefined ? apiKeyPrefix(apiKey) : undefined) ?? "(the calling key)";
56973
+ }
56974
+ async function promoteFresh(ctx, parsed, fromLabel, toKey) {
55013
56975
  if ("error" in parsed)
55014
56976
  return usage(ctx, parsed.error);
55015
56977
  const resolvedVault = vaultPathFor(ctx, parsed);
@@ -55088,10 +57050,19 @@ async function promoteFresh(ctx, parsed, fromLabel) {
55088
57050
  addKeys: [blob]
55089
57051
  }, ctx.deps));
55090
57052
  await confirmLastSix(ctx, destination.address, "the sweep vault destination");
57053
+ if (toKey !== undefined) {
57054
+ const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
57055
+ ctx.deps.stderr.write(`${[
57056
+ `This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
57057
+ ...targetWarnings(toKey.target)
57058
+ ].join(`
57059
+ `)}
57060
+ `);
57061
+ }
55091
57062
  const privateKey = base58.encode(secret64);
55092
57063
  wipe(secret64);
55093
57064
  const importCount = { n: 0 };
55094
- const { exit: code } = await runTeeImport(ctx, {
57065
+ const { exit: code, submitted } = await runTeeImport(ctx, {
55095
57066
  address,
55096
57067
  privateKey,
55097
57068
  label: entry.label,
@@ -55110,22 +57081,35 @@ async function promoteFresh(ctx, parsed, fromLabel) {
55110
57081
  if (target === undefined) {
55111
57082
  throw new VaultError("VAULT_INDEX_INVALID", `Entry ${keyId} missing after import.`);
55112
57083
  }
57084
+ let rebound;
57085
+ if (toKey !== undefined) {
57086
+ const importedTo = submitted?.boundKeyPrefix ?? null;
57087
+ rebound = await rebindAfterImport(ctx, toKey, {
57088
+ linkedWalletId: target.linkedWalletId ?? null,
57089
+ address,
57090
+ label: entry.label,
57091
+ remoteAuthority: target.tee?.remoteAuthority ?? submitted?.remoteAuthority ?? null,
57092
+ importedTo
57093
+ }, await callingKeyPrefixFor(ctx));
57094
+ }
57095
+ const exit = Math.max(code, rebound?.exit ?? 0);
55113
57096
  if (ctx.json) {
55114
57097
  writeJson(ctx.deps, {
55115
- ok: true,
57098
+ ok: rebound === undefined || rebound.exit === 0,
55116
57099
  mode: "fresh",
55117
57100
  address,
55118
57101
  label: entry.label,
55119
57102
  vaultDestination: destination.address,
55120
57103
  lifecycle: target.tee?.lifecycle,
55121
57104
  linkedWalletId: target.linkedWalletId ?? null,
55122
- importCalls: importCount.n
57105
+ importCalls: importCount.n,
57106
+ ...rebound?.json ?? {}
55123
57107
  });
55124
57108
  } else {
55125
57109
  ctx.deps.stdout.write(`Promoted fresh TEE wallet ${address} (sweep to ${destination.address}).
55126
57110
  `);
55127
57111
  }
55128
- return code;
57112
+ return exit;
55129
57113
  });
55130
57114
  }
55131
57115
  async function refuseWithoutRoom(ctx) {
@@ -55147,7 +57131,7 @@ async function reopenFromDisk(path, reopen, previous) {
55147
57131
  });
55148
57132
  return reopen(path, raw);
55149
57133
  }
55150
- async function promoteInPlace(ctx, parsed, subjectLabel) {
57134
+ async function promoteInPlace(ctx, parsed, subjectLabel, toKey) {
55151
57135
  if ("error" in parsed)
55152
57136
  return usage(ctx, parsed.error);
55153
57137
  const resolvedVault = vaultPathFor(ctx, parsed);
@@ -55174,7 +57158,9 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55174
57158
  if (sweepTo !== undefined) {
55175
57159
  return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
55176
57160
  }
55177
- const { exit } = await resumePromote(ctx, vault, existing, opened.reopen, path, hold, {
57161
+ if (toKey !== undefined)
57162
+ await confirmResumeRebind(ctx, toKey);
57163
+ const resumed = await resumePromote(ctx, vault, existing, opened.reopen, path, hold, {
55178
57164
  confirmAccount: (account) => confirmLastSix(ctx, account, "that account"),
55179
57165
  confirmDestination: async (destination2) => {
55180
57166
  try {
@@ -55183,9 +57169,36 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55183
57169
  } catch {
55184
57170
  return false;
55185
57171
  }
55186
- }
57172
+ },
57173
+ report: toKey !== undefined ? "return" : "write"
55187
57174
  });
55188
- return exit;
57175
+ if (toKey === undefined)
57176
+ return resumed.exit;
57177
+ if (resumed.failure !== undefined || resumed.exit !== 0) {
57178
+ writeLocalFailure(ctx.deps, resumed.failure ?? { code: "PROMOTE_OUTCOME_UNRESOLVED", message: "The resume did not complete." }, ctx.json);
57179
+ return resumed.exit;
57180
+ }
57181
+ const rebound2 = await rebindAfterImport(ctx, toKey, {
57182
+ linkedWalletId: resumed.adopted?.linkedWalletId ?? null,
57183
+ address: existing.address,
57184
+ label: existing.label,
57185
+ remoteAuthority: resumed.adopted?.remoteAuthority ?? null,
57186
+ importedTo: existing.tee?.boundKeyPrefix ?? null
57187
+ }, await callingKeyPrefixFor(ctx));
57188
+ if (ctx.json) {
57189
+ writeJson(ctx.deps, {
57190
+ ok: rebound2.exit === 0,
57191
+ mode: "resume",
57192
+ address: existing.address,
57193
+ lifecycle: "enabled",
57194
+ importCalls: 0,
57195
+ ...rebound2.json
57196
+ });
57197
+ } else {
57198
+ ctx.deps.stdout.write(`Resumed ${existing.address}: grant adopted; no re-import.
57199
+ `);
57200
+ }
57201
+ return rebound2.exit;
55189
57202
  }
55190
57203
  if (sweepTo === undefined || rpcUrl2 === undefined) {
55191
57204
  return usage(ctx, "Usage: candle vault promote --in-place <label> --sweep-to <label> --rpc-url <url> [--label] [--accept-unknown-exposure]");
@@ -55197,7 +57210,12 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55197
57210
  return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
55198
57211
  }
55199
57212
  await refuseWithoutRoom(ctx);
55200
- const controlledBy = await readControlledBy(ctx);
57213
+ const live = await readControlledBy(ctx);
57214
+ const controlledBy = toKey === undefined ? live : withToKey(live, {
57215
+ keyPrefix: toKey.target.keyPrefix,
57216
+ label: toKey.target.label,
57217
+ warnings: targetWarnings(toKey.target)
57218
+ });
55201
57219
  const rpc2 = createSolanaRpc(rpcUrl2, ctx.deps.fetch);
55202
57220
  const host = new URL(rpcUrl2).host;
55203
57221
  await displayHoldings(ctx, rpc2, first.subject.address);
@@ -55267,24 +57285,37 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55267
57285
  writeLocalFailure(ctx.deps, failure, ctx.json);
55268
57286
  return code;
55269
57287
  }
57288
+ let rebound;
57289
+ if (toKey !== undefined && imported.submitted !== undefined) {
57290
+ const importedTo = imported.submitted.boundKeyPrefix ?? null;
57291
+ rebound = await rebindAfterImport(ctx, toKey, {
57292
+ linkedWalletId: imported.submitted.id ?? null,
57293
+ address: subject.address,
57294
+ label: parsed.values["--label"] ?? subject.label,
57295
+ remoteAuthority: imported.submitted.remoteAuthority ?? null,
57296
+ importedTo
57297
+ }, controlledBy.keyPrefix);
57298
+ }
57299
+ const exit = Math.max(code, rebound?.exit ?? 0);
55270
57300
  if (ctx.json) {
55271
57301
  const reopened = hold(await reopenFromDisk(path, opened.reopen, vault));
55272
57302
  const updated = reopened.index.entries.find((e) => e.id === subject.id);
55273
57303
  writeJson(ctx.deps, {
55274
- ok: code === 0 || code === 3,
57304
+ ok: (code === 0 || code === 3) && (rebound === undefined || rebound.exit === 0),
55275
57305
  mode: "in-place",
55276
57306
  address: subject.address,
55277
57307
  vaultDestination: destination.address,
55278
57308
  lifecycle: updated?.tee?.lifecycle ?? null,
55279
57309
  linkedWalletId: updated?.linkedWalletId ?? null,
55280
57310
  controlledBy: controlledByJson(controlledBy),
55281
- authorities: authoritiesJson(roles)
57311
+ authorities: authoritiesJson(roles),
57312
+ ...rebound?.json ?? {}
55282
57313
  });
55283
57314
  } else if (code === 0 || code === 3) {
55284
57315
  ctx.deps.stdout.write(`Promoted ${subject.address} in place (sweep to ${destination.address}). This address never returns to cold.
55285
57316
  `);
55286
57317
  }
55287
- return code;
57318
+ return exit;
55288
57319
  });
55289
57320
  }
55290
57321
  async function resumePromote(ctx, vault, entry, reopen, path, hold, confirmations) {
@@ -55870,12 +57901,12 @@ function parseValueUsdCell(raw) {
55870
57901
  const value = Number(cleaned);
55871
57902
  return Number.isFinite(value) ? value : undefined;
55872
57903
  }
55873
- function formatUsd(value) {
57904
+ function formatUsd2(value) {
55874
57905
  const [whole, fraction] = value.toFixed(2).split(".");
55875
57906
  const grouped = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
55876
57907
  return `$${grouped}.${fraction ?? "00"}`;
55877
57908
  }
55878
- function formatSol3(lamports) {
57909
+ function formatSol4(lamports) {
55879
57910
  const whole = lamports / 1000000000n;
55880
57911
  const fraction = lamports % 1000000000n;
55881
57912
  return `${whole}.${fraction.toString().padStart(9, "0").slice(0, 6)}`;
@@ -55904,14 +57935,37 @@ init_promote_support();
55904
57935
  init_signer_roles();
55905
57936
  init_store();
55906
57937
  init_vault_support();
55907
- var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings] [--accept-unknown-exposure]";
57938
+ var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--to-key <label|prefix>] [--token-holdings] [--accept-unknown-exposure]";
55908
57939
  var rowObserver = null;
57940
+ function splitRebindable(results) {
57941
+ const wallets2 = [];
57942
+ const notRebindable = [];
57943
+ for (const row of results) {
57944
+ if (row.linkedWalletId !== null && row.remoteAuthority === "verified-active") {
57945
+ wallets2.push({ id: row.linkedWalletId, address: row.address, label: row.label });
57946
+ } else {
57947
+ notRebindable.push({
57948
+ address: row.address,
57949
+ label: row.label,
57950
+ reason: row.linkedWalletId === null ? `no linked wallet id (${row.lifecycle})` : `remote authority is ${row.remoteAuthority ?? "unknown"}, not verified-active`
57951
+ });
57952
+ }
57953
+ }
57954
+ return { wallets: wallets2, notRebindable };
57955
+ }
57956
+ function keysWithRebind(results, phase, run) {
57957
+ return results.map((row) => ({
57958
+ ...row,
57959
+ boundKeyPrefix: finalBoundKey({ id: row.linkedWalletId, importedTo: phase.importedTo.get(row.address) ?? null }, run, phase.toKey.target.keyPrefix),
57960
+ rebind: walletRebindJson({ id: row.linkedWalletId, remoteAuthority: row.remoteAuthority }, run)
57961
+ }));
57962
+ }
55909
57963
  async function vaultPromoteBatch(args, ctx) {
55910
57964
  if (args.some((arg) => arg === "--from" || arg.startsWith("--from="))) {
55911
57965
  return usage(ctx, "promote-batch promotes existing vault keys in place and takes no --from. For fresh keys: candle vault new-key --chain solana --labels-from <file>, then candle vault promote --from <label> for each.");
55912
57966
  }
55913
57967
  const parsed = parseArgs(args, {
55914
- valueFlags: ["--pairs-from", "--rpc-url", "--keystore"],
57968
+ valueFlags: ["--pairs-from", "--rpc-url", "--keystore", "--to-key"],
55915
57969
  booleanFlags: ["--token-holdings", "--accept-unknown-exposure", "--accept-older-copy"],
55916
57970
  pathFlags: ["--keystore", "--pairs-from"]
55917
57971
  });
@@ -55922,6 +57976,9 @@ async function vaultPromoteBatch(args, ctx) {
55922
57976
  const pairsFile = parsed.values["--pairs-from"];
55923
57977
  if (pairsFile === undefined)
55924
57978
  return usage(ctx, USAGE_LINE);
57979
+ const toKeyRaw = parsed.values["--to-key"];
57980
+ if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
57981
+ return usage(ctx, "--to-key needs a label or a prefix.");
55925
57982
  if (!refuseEnvPassphrase(ctx))
55926
57983
  return 1;
55927
57984
  if (!requireTty(ctx, "vault promote-batch"))
@@ -55946,6 +58003,13 @@ async function vaultPromoteBatch(args, ctx) {
55946
58003
  if (!parsedFile.ok)
55947
58004
  return usage(ctx, renderPhaseAFindings(pairsFile, parsedFile.findings));
55948
58005
  const { rows, hasValueUsd } = parsedFile;
58006
+ let toKey;
58007
+ if (toKeyRaw !== undefined) {
58008
+ const preflight = await preflightToKey(ctx, toKeyRaw, { labels: rows.map((row) => row.label) });
58009
+ if (!preflight.ok)
58010
+ return preflight.exit;
58011
+ toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
58012
+ }
55949
58013
  return runVaultCommand(ctx, async ({ hold }) => {
55950
58014
  const raw = await requireVaultRaw(ctx, resolvedVault);
55951
58015
  const opened = await unlockInteractively(ctx, path, raw, {
@@ -55981,7 +58045,17 @@ async function vaultPromoteBatch(args, ctx) {
55981
58045
  if (roomRefusal !== null)
55982
58046
  throw roomRefusal;
55983
58047
  const room = roomRead.room;
55984
- const controlledBy = await readControlledBy(ctx);
58048
+ const live = await readControlledBy(ctx);
58049
+ const controlledBy = toKey === undefined ? live : withToKey(live, {
58050
+ keyPrefix: toKey.target.keyPrefix,
58051
+ label: toKey.target.label,
58052
+ warnings: targetWarnings(toKey.target)
58053
+ });
58054
+ const rebindPhase = toKey === undefined ? undefined : { toKey, callingKeyPrefix: live.keyPrefix, importedTo: new Map };
58055
+ for (const item of planned) {
58056
+ if (item.kind === "skip")
58057
+ rebindPhase?.importedTo.set(item.subject.address, item.subject.tee?.boundKeyPrefix ?? null);
58058
+ }
55985
58059
  const addresses = planned.map((item) => item.subject.address);
55986
58060
  const rpc2 = createSolanaRpc(rpcUrl2, deps.fetch);
55987
58061
  const host = new URL(rpcUrl2).host;
@@ -56040,28 +58114,58 @@ ${footer}
56040
58114
  });
56041
58115
  }
56042
58116
  if (acting.length === 0) {
56043
- deps.stderr.write(`
58117
+ const roles2 = {
58118
+ checked: [...ROLE_GROUP_IDS],
58119
+ notChecked: [],
58120
+ found: [],
58121
+ requests: 0,
58122
+ planned: 0,
58123
+ rateLimited: 0,
58124
+ elapsedMs: 0
58125
+ };
58126
+ const skipped = planned.map((item) => skippedResult(item));
58127
+ const rebindable = rebindPhase === undefined ? undefined : splitRebindable(skipped);
58128
+ if (rebindPhase === undefined || rebindable === undefined || rebindable.wallets.length === 0) {
58129
+ deps.stderr.write(`
56044
58130
  ${table}
56045
58131
 
56046
58132
  ${footer}
56047
58133
  Nothing to do: every row already landed.
56048
58134
  `);
58135
+ return finish(ctx, {
58136
+ file: pairsFile,
58137
+ rows: rows.length,
58138
+ keys: skipped,
58139
+ destinations,
58140
+ exit: 0,
58141
+ controlledBy,
58142
+ roles: roles2
58143
+ });
58144
+ }
58145
+ const n = rebindable.wallets.length;
58146
+ deps.stderr.write(`
58147
+ ${table}
58148
+
58149
+ ${footer}
58150
+ Every row already landed; ${n} wallet${n === 1 ? "" : "s"} to rebind.
58151
+ `);
58152
+ deps.stderr.write(`
58153
+ ${renderControlledBy(controlledBy, n)}
58154
+ `);
58155
+ const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to move ${n === 1 ? "this wallet" : `these ${n} wallets`} to ${rebindPhase.toKey.target.keyPrefix}: `);
58156
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
58157
+ throw new VaultError("PROMOTE_NOT_ACKNOWLEDGED", `The acknowledgement is the word ${CONFIRM_WORD}; nothing was moved, and nothing was written.`, { suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.` });
58158
+ }
58159
+ const rebound2 = await runRebindPhase(ctx, rebindPhase, skipped);
56049
58160
  return finish(ctx, {
56050
58161
  file: pairsFile,
56051
58162
  rows: rows.length,
56052
- keys: planned.map((item) => skippedResult(item)),
58163
+ keys: skipped,
56053
58164
  destinations,
56054
58165
  exit: 0,
56055
58166
  controlledBy,
56056
- roles: {
56057
- checked: [...ROLE_GROUP_IDS],
56058
- notChecked: [],
56059
- found: [],
56060
- requests: 0,
56061
- planned: 0,
56062
- rateLimited: 0,
56063
- elapsedMs: 0
56064
- }
58167
+ roles: roles2,
58168
+ rebound: rebound2
56065
58169
  });
56066
58170
  }
56067
58171
  const checked = roles;
@@ -56152,6 +58256,7 @@ ${renderControlledBy(controlledBy, acting.length)}
56152
58256
  worst = Math.max(worst, imported.exit);
56153
58257
  done += 1;
56154
58258
  last = { address: subject2.address, keyId: subject2.id };
58259
+ rebindPhase?.importedTo.set(subject2.address, imported.submitted.boundKeyPrefix ?? null);
56155
58260
  results.push({
56156
58261
  line: row2.line,
56157
58262
  label: subject2.label,
@@ -56191,6 +58296,7 @@ ${renderControlledBy(controlledBy, acting.length)}
56191
58296
  await rowObserver?.({ line: row.line, stage: "resumed", vault: current });
56192
58297
  done += 1;
56193
58298
  last = { address: subject.address, keyId: subject.id };
58299
+ rebindPhase?.importedTo.set(subject.address, subject.tee?.boundKeyPrefix ?? null);
56194
58300
  results.push({
56195
58301
  line: row.line,
56196
58302
  label: subject.label,
@@ -56216,6 +58322,20 @@ ${renderControlledBy(controlledBy, acting.length)}
56216
58322
  }
56217
58323
  if (stopped !== undefined) {
56218
58324
  reportPartial(ctx, { landed: done, acting: acting.length, failure: stopped });
58325
+ let notReached;
58326
+ if (rebindPhase !== undefined) {
58327
+ const { wallets: wallets2, notRebindable } = splitRebindable(results);
58328
+ notReached = {
58329
+ target: rebindPhase.toKey.target,
58330
+ callingKeyPrefix: rebindPhase.callingKeyPrefix,
58331
+ wallets: wallets2,
58332
+ notRebindable
58333
+ };
58334
+ const report = renderRebindReport(notReached);
58335
+ if (report.length > 0)
58336
+ deps.stderr.write(`${report}
58337
+ `);
58338
+ }
56219
58339
  if (ctx.json) {
56220
58340
  writeJson(deps, {
56221
58341
  ok: false,
@@ -56226,13 +58346,14 @@ ${renderControlledBy(controlledBy, acting.length)}
56226
58346
  resumed: results.filter((r) => r.state === "resumed").length,
56227
58347
  skipped: results.filter((r) => r.state === "skipped").length,
56228
58348
  destinations: destinations.map(({ label, address, keys }) => ({ label, address, keys })),
56229
- keys: results,
58349
+ keys: rebindPhase === undefined ? results : keysWithRebind(results, rebindPhase, undefined),
56230
58350
  failedLine: stopped.line,
56231
58351
  code: stopped.code,
56232
58352
  message: stopped.message,
56233
58353
  ...stopped.suggestion !== undefined ? { suggestion: stopped.suggestion } : {},
56234
58354
  controlledBy: controlledByJson(controlledBy),
56235
- authorities: authoritiesJson(checked)
58355
+ authorities: authoritiesJson(checked),
58356
+ ...notReached !== undefined ? rebindJson(notReached) : {}
56236
58357
  });
56237
58358
  } else {
56238
58359
  deps.stderr.write(`${stopped.message}${stopped.suggestion ? ` ${stopped.suggestion}` : ""}
@@ -56242,6 +58363,7 @@ ${renderControlledBy(controlledBy, acting.length)}
56242
58363
  }
56243
58364
  if (last !== undefined)
56244
58365
  await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx);
58366
+ const rebound = rebindPhase === undefined ? undefined : await runRebindPhase(ctx, rebindPhase, results);
56245
58367
  return finish(ctx, {
56246
58368
  file: pairsFile,
56247
58369
  rows: rows.length,
@@ -56249,10 +58371,27 @@ ${renderControlledBy(controlledBy, acting.length)}
56249
58371
  destinations,
56250
58372
  exit: worst,
56251
58373
  controlledBy,
56252
- roles: checked
58374
+ roles: checked,
58375
+ ...rebound !== undefined ? { rebound } : {}
56253
58376
  });
56254
58377
  });
56255
58378
  }
58379
+ async function runRebindPhase(ctx, phase, results) {
58380
+ const { wallets: wallets2, notRebindable } = splitRebindable(results);
58381
+ const run = wallets2.length > 0 ? await rebindPromoted(ctx, phase.toKey.deviceToken, phase.toKey.target.keyPrefix, wallets2) : undefined;
58382
+ const input = {
58383
+ target: phase.toKey.target,
58384
+ callingKeyPrefix: phase.callingKeyPrefix,
58385
+ wallets: wallets2,
58386
+ run,
58387
+ notRebindable
58388
+ };
58389
+ const report = renderRebindReport(input);
58390
+ if (report.length > 0)
58391
+ ctx.deps.stderr.write(`${report}
58392
+ `);
58393
+ return { phase, run, input };
58394
+ }
56256
58395
  async function verifySubjectSecret(vault, subject) {
56257
58396
  const secret = await decryptKey(vault, subject.id);
56258
58397
  try {
@@ -56375,7 +58514,7 @@ function renderBatchTable(planned, opts) {
56375
58514
  const authority = item.kind === "skip" ? "" : opts.roles === undefined ? "?" : authorityCell(address, opts.roles);
56376
58515
  const cells = [String(item.row.line), state, item.subject.label, address, destination, authority];
56377
58516
  const lamports = opts.lamports?.get(address);
56378
- cells.push(lamports === undefined ? "unread" : formatSol3(lamports));
58517
+ cells.push(lamports === undefined ? "unread" : formatSol4(lamports));
56379
58518
  if (opts.tokenCounts !== undefined)
56380
58519
  cells.push(String(opts.tokenCounts.get(address) ?? 0));
56381
58520
  if (opts.hasValueUsd)
@@ -56414,7 +58553,7 @@ function renderFooter(opts) {
56414
58553
  else
56415
58554
  total += value;
56416
58555
  }
56417
- lines.push(`value_usd totals your file's own column; this CLI reads no price. Total for the ${opts.acting.length} rows this run will act on: ${formatUsd(total)}${excluded > 0 ? ` (${excluded} row${excluded === 1 ? "" : "s"} excluded: the cell did not parse)` : ""}`);
58556
+ lines.push(`value_usd totals your file's own column; this CLI reads no price. Total for the ${opts.acting.length} rows this run will act on: ${formatUsd2(total)}${excluded > 0 ? ` (${excluded} row${excluded === 1 ? "" : "s"} excluded: the cell did not parse)` : ""}`);
56418
58557
  }
56419
58558
  const accepted = opts.acting.filter((item) => item.kind === "promote" && item.acceptedUnknown);
56420
58559
  if (accepted.length > 0) {
@@ -56439,10 +58578,12 @@ function finish(ctx, opts) {
56439
58578
  const resumed = opts.keys.filter((r) => r.state === "resumed").length;
56440
58579
  const skipped = opts.keys.filter((r) => r.state === "skipped").length;
56441
58580
  const unverified = opts.keys.filter((r) => r.state !== "skipped" && r.remoteAuthority !== "verified-active");
56442
- const complete = opts.exit === 0;
58581
+ const rebindFailed = opts.rebound?.run !== undefined && !opts.rebound.run.ok;
58582
+ const exit = rebindFailed ? Math.max(opts.exit, 1) : opts.exit;
58583
+ const complete = exit === 0;
56443
58584
  if (ctx.json) {
56444
58585
  writeJson(ctx.deps, {
56445
- ok: true,
58586
+ ok: !rebindFailed,
56446
58587
  complete,
56447
58588
  file: opts.file,
56448
58589
  rows: opts.rows,
@@ -56450,11 +58591,12 @@ function finish(ctx, opts) {
56450
58591
  resumed,
56451
58592
  skipped,
56452
58593
  destinations: opts.destinations.map(({ label, address, keys }) => ({ label, address, keys })),
56453
- keys: opts.keys,
58594
+ keys: opts.rebound === undefined ? opts.keys : keysWithRebind(opts.keys, opts.rebound.phase, opts.rebound.run),
56454
58595
  controlledBy: controlledByJson(opts.controlledBy),
56455
- authorities: authoritiesJson(opts.roles)
58596
+ authorities: authoritiesJson(opts.roles),
58597
+ ...opts.rebound !== undefined ? rebindJson(opts.rebound.input) : {}
56456
58598
  });
56457
- return opts.exit;
58599
+ return exit;
56458
58600
  }
56459
58601
  const parts = [];
56460
58602
  if (promoted > 0)
@@ -56465,11 +58607,19 @@ function finish(ctx, opts) {
56465
58607
  parts.push(`${skipped} already promoted (skipped)`);
56466
58608
  ctx.deps.stdout.write(`${parts.join("; ")}.
56467
58609
  `);
56468
- if (!complete) {
58610
+ if (unverified.length > 0) {
56469
58611
  ctx.deps.stdout.write(`${unverified.length} import${unverified.length === 1 ? "" : "s"} did not report verified-active: ${unverified.map((r) => r.address).join(", ")}
56470
58612
  `);
56471
58613
  }
56472
- return opts.exit;
58614
+ if (opts.rebound !== undefined) {
58615
+ const { input, run } = opts.rebound;
58616
+ const moved = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "rebound").length;
58617
+ const already = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "unchanged").length;
58618
+ const pending = input.wallets.length - moved - already;
58619
+ ctx.deps.stdout.write(`${moved + already} of ${input.wallets.length + input.notRebindable.length} wallets bound to ${input.target.keyPrefix}${pending > 0 ? `; ${pending} still on ${input.callingKeyPrefix}` : ""}${input.notRebindable.length > 0 ? `; ${input.notRebindable.length} not yet rebindable` : ""}.
58620
+ `);
58621
+ }
58622
+ return exit;
56473
58623
  }
56474
58624
 
56475
58625
  // src/commands/vault-rename.ts
@@ -57555,8 +59705,78 @@ function describeEnvelope(envelope, facts) {
57555
59705
 
57556
59706
  // src/commands/vault-transfer.ts
57557
59707
  init_args();
59708
+ init_deps();
57558
59709
  init_promote_support();
57559
59710
  init_store();
59711
+
59712
+ // src/vault/tee-transfer.ts
59713
+ init_errors();
59714
+ function assertNoPendingSweep(entry) {
59715
+ const pending = entry.tee?.sweepPending?.length ?? 0;
59716
+ if (pending === 0)
59717
+ return;
59718
+ throw new VaultError("TRANSFER_SWEEP_PENDING", `${entry.label} (${entry.address}) has ${pending} pending sweep transaction(s); a transfer would interleave with that sweep.`, { suggestion: `Finish the sweep first: candle tee sweep ${entry.address} --rpc-url <url>` });
59719
+ }
59720
+ async function readTeeServerState(ctx, entry, apiKey) {
59721
+ if (entry.linkedWalletId === undefined) {
59722
+ return { read: false, reason: "this wallet has no linked wallet id recorded" };
59723
+ }
59724
+ if (apiKey === undefined)
59725
+ return { read: false, reason: "no API key for this profile" };
59726
+ try {
59727
+ const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/lifecycle`, {
59728
+ auth: "key",
59729
+ credentials: { apiKey },
59730
+ apiUrl: ctx.apiUrl,
59731
+ fetch: ctx.deps.fetch,
59732
+ env: ctx.deps.env
59733
+ });
59734
+ if (!result.ok)
59735
+ return { read: false, reason: result.message };
59736
+ const state = result.body.state;
59737
+ if (typeof state !== "string")
59738
+ return { read: false, reason: "the lifecycle response carried no state" };
59739
+ return { read: true, state };
59740
+ } catch (error) {
59741
+ return { read: false, reason: error instanceof Error ? error.message : String(error) };
59742
+ }
59743
+ }
59744
+ function serverStateNotice(state) {
59745
+ if (!state.read) {
59746
+ return `Could not read this wallet's Candle state (${state.reason}); proceeding. An agent may be trading it.`;
59747
+ }
59748
+ if (state.state === "enabled") {
59749
+ return "Warning: this wallet is enabled, so an agent may be trading it now; a trade may fail if this transfer leaves too little.";
59750
+ }
59751
+ return;
59752
+ }
59753
+ async function reportTransferActivity(ctx, apiKey, signature) {
59754
+ const unseen = "Candle's history will not show this transfer.";
59755
+ if (apiKey === undefined)
59756
+ return { outcome: "no-api-key", line: `No API key for this profile, so ${unseen}` };
59757
+ try {
59758
+ const result = await apiRequest("/api/v1/activity/report", {
59759
+ method: "POST",
59760
+ body: { chain: "solana", signature },
59761
+ auth: "key",
59762
+ credentials: { apiKey },
59763
+ apiUrl: ctx.apiUrl,
59764
+ fetch: ctx.deps.fetch,
59765
+ env: ctx.deps.env
59766
+ });
59767
+ if (result.ok)
59768
+ return { outcome: "reported", line: "Reported to Candle's history." };
59769
+ if (result.code === "SCOPE_MISSING") {
59770
+ return { outcome: "scope-missing", line: `This profile's API key lacks activity:write, so ${unseen}` };
59771
+ }
59772
+ return { outcome: "failed", line: `Could not report this transfer (${result.message}); ${unseen}` };
59773
+ } catch (error) {
59774
+ const reason = error instanceof Error ? error.message : String(error);
59775
+ return { outcome: "failed", line: `Could not report this transfer (${reason}); ${unseen}` };
59776
+ }
59777
+ }
59778
+
59779
+ // src/commands/vault-transfer.ts
57560
59780
  init_vault_support();
57561
59781
  async function vaultTransfer(args, ctx) {
57562
59782
  const parsed = parseArgs(args, {
@@ -57597,11 +59817,17 @@ async function vaultTransfer(args, ctx) {
57597
59817
  promptText: "Vault passphrase (input hidden): "
57598
59818
  });
57599
59819
  const vault = hold(opened.vault);
57600
- const fromEntry = findVaultRoleEntry(vault.index, fromLabel);
59820
+ const fromEntry = findTransferSource(vault.index, fromLabel);
57601
59821
  if (fromEntry === undefined) {
57602
- return usage(ctx, `No vault key matches --from ${fromLabel}.`);
59822
+ return usage(ctx, `No vault key or promoted wallet matches --from ${fromLabel}.`);
57603
59823
  }
57604
- assertVaultSigner(fromEntry);
59824
+ assertTransferSigner(fromEntry);
59825
+ const promoted = fromEntry.role === "tee-wallet";
59826
+ if (promoted)
59827
+ assertNoPendingSweep(fromEntry);
59828
+ const note = (line) => (ctx.json ? ctx.deps.stderr : ctx.deps.stdout).write(`${line}
59829
+ `);
59830
+ const apiKey = promoted ? await resolveApiKey(ctx.deps, ctx.profile) : undefined;
57605
59831
  const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], rpcUrl2, ctx);
57606
59832
  if (reconciled !== null)
57607
59833
  return reconciled;
@@ -57615,11 +59841,25 @@ async function vaultTransfer(args, ctx) {
57615
59841
  });
57616
59842
  const feeQuote = await quoteTransferFee(rpcUrl2, ctx.deps.fetch, plan.from, plan.instructions);
57617
59843
  displayTransferPlan(ctx, plan, feeQuote);
59844
+ if (promoted) {
59845
+ const notice = serverStateNotice(await readTeeServerState(ctx, fromEntry, apiKey));
59846
+ if (notice !== undefined)
59847
+ note(notice);
59848
+ }
57618
59849
  await confirmLastSix(ctx, to, "the destination");
57619
59850
  await opened.confirm(`sign transfer of ${plan.amount} ${plan.asset} to ${to}`);
57620
59851
  const secret = await decryptKey(vault, fromEntry.id);
57621
59852
  try {
57622
59853
  const result = await signAndBroadcastTransfer({ ctx, rpcUrl: rpcUrl2, secret64: secret, plan });
59854
+ let activityReport;
59855
+ if (promoted && result.finalized) {
59856
+ const report = await reportTransferActivity(ctx, apiKey, result.signature);
59857
+ activityReport = report.outcome;
59858
+ note(report.line);
59859
+ } else if (promoted) {
59860
+ activityReport = "not-finalized";
59861
+ note("Not reported to Candle's history: finality is not yet confirmed.");
59862
+ }
57623
59863
  if (ctx.json) {
57624
59864
  writeJson(ctx.deps, {
57625
59865
  ok: result.finalized,
@@ -57629,7 +59869,8 @@ async function vaultTransfer(args, ctx) {
57629
59869
  amount: plan.amount,
57630
59870
  asset: plan.asset,
57631
59871
  amountRaw: plan.amountRaw.toString(),
57632
- finalized: result.finalized
59872
+ finalized: result.finalized,
59873
+ ...activityReport !== undefined ? { activityReport } : {}
57633
59874
  });
57634
59875
  } else {
57635
59876
  ctx.deps.stdout.write(result.finalized ? `Transferred ${plan.amount} ${plan.asset} to ${to}: ${result.signature}
@@ -58063,6 +60304,9 @@ function extractGlobalFlags(argv) {
58063
60304
  var COMMANDS = {
58064
60305
  swap: { bare: swap, subcommands: { status: swapStatus } },
58065
60306
  launch: { bare: launch },
60307
+ pnl: { bare: pnl },
60308
+ portfolio: { bare: portfolio },
60309
+ lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
58066
60310
  auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
58067
60311
  keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
58068
60312
  wallets: {