@candledottv/cli 0.11.6 → 0.11.8

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 +15 -3
  2. package/dist/index.js +3156 -256
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -278,6 +278,16 @@ var init_args = __esm(() => {
278
278
  });
279
279
 
280
280
  // src/agent-key-access.ts
281
+ function scopesForPreset(preset) {
282
+ switch (preset) {
283
+ case "read":
284
+ return [...READ_SCOPES];
285
+ case "readwrite":
286
+ return [...READWRITE_SCOPES];
287
+ case "readwritetransfer":
288
+ return [...READWRITETRANSFER_SCOPES];
289
+ }
290
+ }
281
291
  function sameSet(scopes, preset) {
282
292
  const held = new Set(scopes);
283
293
  return held.size === preset.length && preset.every((scope) => held.has(scope));
@@ -287,6 +297,8 @@ function presetForScopes(scopes) {
287
297
  return "read";
288
298
  if (sameSet(scopes, READWRITE_SCOPES))
289
299
  return "readwrite";
300
+ if (sameSet(scopes, READWRITETRANSFER_SCOPES))
301
+ return "readwritetransfer";
290
302
  return null;
291
303
  }
292
304
  function agentKeyCapabilities(scopes) {
@@ -295,6 +307,7 @@ function agentKeyCapabilities(scopes) {
295
307
  launch: scopes.includes("launch:write"),
296
308
  trade: scopes.includes("swap:write"),
297
309
  transfer: scopes.includes("transfer:write"),
310
+ linkedTransfer: scopes.includes("transfer:bound"),
298
311
  report: scopes.includes("activity:write")
299
312
  };
300
313
  }
@@ -308,7 +321,7 @@ function agentKeyAccess(scopes) {
308
321
  function sortAgentKeyScopes(scopes) {
309
322
  return [...scopes].sort();
310
323
  }
311
- var READ_SCOPES, READWRITE_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
324
+ var READ_SCOPES, READWRITE_SCOPES, READWRITETRANSFER_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
312
325
  var init_agent_key_access = __esm(() => {
313
326
  READ_SCOPES = ["account:read"];
314
327
  READWRITE_SCOPES = [
@@ -319,18 +332,35 @@ var init_agent_key_access = __esm(() => {
319
332
  "transfer:write",
320
333
  "account:read"
321
334
  ];
322
- AGENT_KEY_PRESET_LABELS = { read: "Read", readwrite: "Read:Write" };
335
+ READWRITETRANSFER_SCOPES = [
336
+ "launch:write",
337
+ "launch:read",
338
+ "activity:write",
339
+ "swap:write",
340
+ "transfer:write",
341
+ "account:read",
342
+ "transfer:bound"
343
+ ];
344
+ AGENT_KEY_PRESET_LABELS = {
345
+ read: "Read",
346
+ readwrite: "Read:Write",
347
+ readwritetransfer: "Read:Write:Transfer"
348
+ };
323
349
  AGENT_KEY_CAPABILITY_CHIPS = [
324
350
  ["Launch", "launch"],
325
351
  ["Trade", "trade"],
326
352
  ["Transfer", "transfer"],
353
+ ["Linked transfer", "linkedTransfer"],
327
354
  ["Report", "report"]
328
355
  ];
329
356
  });
330
357
 
331
358
  // src/render.ts
332
359
  function formatScopesForSummary(scopes) {
333
- return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope).join(", ");
360
+ return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope === "transfer:bound" ? `${scope} (${TRANSFER_BOUND_NOTE})` : scope).join(", ");
361
+ }
362
+ function terminalText(value) {
363
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
334
364
  }
335
365
  function renderTable(headers, rows) {
336
366
  const widths = headers.map((header, col) => Math.max(header.length, ...rows.map((row) => (row[col] ?? "").length)));
@@ -433,7 +463,7 @@ function portalDeviceUrl(apiUrl, portalOrigin) {
433
463
  return `${apiUrl}/agents`;
434
464
  }
435
465
  }
436
- var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets";
466
+ var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets", TRANSFER_BOUND_NOTE = "moves funds -- this key can move funds out of the wallet it runs, to your linked wallets and its vault";
437
467
  var init_render = __esm(() => {
438
468
  init_agent_key_access();
439
469
  ALL_AGENT_SCOPES = [
@@ -442,9 +472,10 @@ var init_render = __esm(() => {
442
472
  "account:read",
443
473
  "activity:write",
444
474
  "swap:write",
445
- "transfer:write"
475
+ "transfer:write",
476
+ "transfer:bound"
446
477
  ];
447
- DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write");
478
+ DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write" && scope !== "transfer:bound");
448
479
  });
449
480
 
450
481
  // src/profiles.ts
@@ -2161,6 +2192,9 @@ function libraryInstallInstruction(platform) {
2161
2192
  }
2162
2193
 
2163
2194
  // src/fido2-helper/protocol.ts
2195
+ function excludeCapacity(device) {
2196
+ return device.maxCredentialCountInList ?? 1;
2197
+ }
2164
2198
  var HELPER_PROTOCOL = 1, RP_ID = "cli.candle.tv", PROBE_MAX_CREDENTIALS = 16, AUTHDATA_FLAG_UP = 1, AUTHDATA_FLAG_UV = 4, AUTHDATA_MIN_LENGTH = 37;
2165
2199
  var init_protocol = () => {};
2166
2200
 
@@ -3140,7 +3174,9 @@ var init_errors = __esm(() => {
3140
3174
  "WALLET_LIMIT_REACHED",
3141
3175
  "TIER_REQUIRED",
3142
3176
  "LINKED_WALLET_ROOM_UNREADABLE",
3143
- "PROMOTE_ACCOUNT_UNRESOLVED"
3177
+ "PROMOTE_ACCOUNT_UNRESOLVED",
3178
+ "TRANSFER_SWEEP_PENDING",
3179
+ "VAULT_KEY_ALREADY_ENROLLED"
3144
3180
  ];
3145
3181
  VaultError = class VaultError extends Error {
3146
3182
  code;
@@ -5953,6 +5989,9 @@ function translateHelperFailure(code, message, platform) {
5953
5989
  LIBRARY_MISSING: "VAULT_HELPER_MISSING"
5954
5990
  };
5955
5991
  const suggestion = "Nothing was derived and no other factor was tried.";
5992
+ if (code === "CREDENTIAL_EXCLUDED") {
5993
+ return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `This security key already holds one of this vault's credentials, so enrolling it again would not add a second key (${message}). Nothing was written.`, { suggestion: ALREADY_ENROLLED_SUGGESTION });
5994
+ }
5956
5995
  if (code === "DEVICE_NOT_READABLE") {
5957
5996
  return new VaultError("VAULT_AUTHENTICATOR_NOT_READABLE", platform === "linux" ? HIDRAW_MESSAGE : `A security key is attached but this user cannot open it: ${message}.`, { suggestion: "No other factor is substituted." });
5958
5997
  }
@@ -6013,6 +6052,51 @@ async function callHelper(deps, helperPath, request) {
6013
6052
  throw translateHelperFailure(String(response.code), String(response.message), deps.platform);
6014
6053
  return response;
6015
6054
  }
6055
+ function alreadyEnrolled(device, envelopeId) {
6056
+ const product = device.product || "This security key";
6057
+ return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `${product} (--device ${device.deviceId}) already holds this vault's security key credential${envelopeId !== undefined ? ` for envelope ${envelopeId}` : ""}, so enrolling it again would not add a second key. Nothing was written.`, {
6058
+ suggestion: ALREADY_ENROLLED_SUGGESTION,
6059
+ ...envelopeId !== undefined ? { details: { envelopeId } } : {}
6060
+ });
6061
+ }
6062
+ function helperPredatesExcludeList(helperPath, leftover) {
6063
+ return new VaultError("VAULT_HELPER_MISSING", `The security key helper at ${helperPath} predates the exclude list this command needs; reinstall the CLI so candle and candle-fido2 come from the same release.`, {
6064
+ suggestion: leftover ? "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again." : "Nothing was sent to any key and nothing was written. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again."
6065
+ });
6066
+ }
6067
+ function assertHelperExcludeList(info, helperPath, excludeCount) {
6068
+ if (excludeCount === 0)
6069
+ return;
6070
+ const features = Array.isArray(info.features) ? info.features : [];
6071
+ if (!features.includes(EXCLUDE_LIST_FEATURE))
6072
+ throw helperPredatesExcludeList(helperPath, false);
6073
+ }
6074
+ function assertExcludeListFits(device, excludeCount) {
6075
+ const capacity = excludeCapacity(device);
6076
+ if (excludeCount <= capacity)
6077
+ return;
6078
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${device.product || "This security key"} (--device ${device.deviceId}) takes at most ${capacity} credential${capacity === 1 ? "" : "s"} in one request${device.maxCredentialCountInList === null ? " (it reports no maxCredentialCountInList)" : ""}, and this vault has ${excludeCount} security key credentials to exclude, so it cannot enroll on this vault. Nothing was sent to it.`, { suggestion: "Nothing was written. Use a security key that reports a larger maxCredentialCountInList." });
6079
+ }
6080
+ function assertDeviceServesFactor(device) {
6081
+ if (!device.extensions.includes("hmac-secret")) {
6082
+ throw new VaultError("VAULT_PRF_UNSUPPORTED", `${device.product || "This security key"} does not support the hmac-secret extension, which this factor needs.`, { suggestion: "Use a key that supports hmac-secret (FIDO2 with PRF). No other derivation is substituted." });
6083
+ }
6084
+ if (device.options.clientPin !== true && device.options.uv !== true) {
6085
+ throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
6086
+ }
6087
+ }
6088
+ async function listSecurityKeys(deps, opts) {
6089
+ const location = await locateFido2Helper(deps);
6090
+ if (location.state === "absent")
6091
+ throw helperMissing(location);
6092
+ const info = await callHelper(deps, location.path, {
6093
+ op: "info",
6094
+ vaultId: opts.vaultId,
6095
+ envelopeId: opts.envelopeId,
6096
+ digest: base64.encode(operationDigest({ vaultId: opts.vaultId, envelopeId: opts.envelopeId, op: "info", nonce: b64u(randomBytes2(16)) }))
6097
+ });
6098
+ return { helperPath: location.path, info };
6099
+ }
6016
6100
  function describeDeviceForList(device) {
6017
6101
  const product = device.product || "security key";
6018
6102
  return `--device ${device.deviceId} ${product}${device.manufacturer ? ` (${device.manufacturer})` : ""}${device.readable ? "" : " [not readable by this user]"}`;
@@ -6095,17 +6179,12 @@ async function openSecurityKeySession(deps, opts) {
6095
6179
  deps.stderr.write(`Using the attached security key: ${device.product || "security key"} (--device ${device.deviceId})
6096
6180
  `);
6097
6181
  }
6098
- if (opts.requireFeatures) {
6099
- if (!device.extensions.includes("hmac-secret")) {
6100
- throw new VaultError("VAULT_PRF_UNSUPPORTED", `${device.product || "This security key"} does not support the hmac-secret extension, which this factor needs.`, { suggestion: "Use a key that supports hmac-secret (FIDO2 with PRF). No other derivation is substituted." });
6101
- }
6102
- if (device.options.clientPin !== true && device.options.uv !== true) {
6103
- throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
6104
- }
6105
- }
6182
+ if (opts.requireFeatures)
6183
+ assertDeviceServesFactor(device);
6184
+ opts.beforePin?.(device, info);
6106
6185
  const session = { helperPath: location.path, device, snapshotId: info.snapshotId };
6107
6186
  if (device.options.clientPin === true) {
6108
- const typed = await deps.promptSecret(`PIN for ${device.product || "the security key"} (input hidden): `);
6187
+ const typed = await deps.promptSecret(opts.pinPrompt?.(device) ?? `PIN for ${device.product || "the security key"} (input hidden): `);
6109
6188
  if (typed === "") {
6110
6189
  throw new VaultError("VAULT_PIN_REQUIRED", "This security key needs its PIN and none was typed; nothing was sent to it.");
6111
6190
  }
@@ -6159,7 +6238,7 @@ async function probeAttachedKeys(deps, opts) {
6159
6238
  if (opts.deviceFlag === undefined && allReadable && !uncertain)
6160
6239
  state.set(envelopeId, "not attached");
6161
6240
  }
6162
- return { state, holder };
6241
+ return { state, holder, devices, credentials: credentialOf };
6163
6242
  } catch {
6164
6243
  return;
6165
6244
  }
@@ -6167,20 +6246,37 @@ async function probeAttachedKeys(deps, opts) {
6167
6246
  async function registerCredential(deps, session, opts) {
6168
6247
  deps.stderr.write(`Touch ${session.device.product || "the security key"} to register the vault's credential on it.
6169
6248
  `);
6170
- const digest = operationDigest({ ...opts, op: "register", nonce: b64u(randomBytes2(16)) });
6171
- const response = await callHelper(deps, session.helperPath, {
6172
- op: "register",
6249
+ const digest = operationDigest({
6173
6250
  vaultId: opts.vaultId,
6174
6251
  envelopeId: opts.envelopeId,
6175
- digest: base64.encode(digest),
6176
- deviceId: session.device.deviceId,
6177
- expectSnapshot: session.snapshotId,
6178
- rpId: RP_ID,
6179
- userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
6180
- userName: userNameFor(opts.vaultId, opts.envelopeId),
6181
- clientDataHash: base64.encode(digest),
6182
- ...session.pin !== undefined ? { pin: session.pin } : {}
6252
+ op: "register",
6253
+ nonce: b64u(randomBytes2(16))
6183
6254
  });
6255
+ const exclude = opts.excludeCredentialIds ?? [];
6256
+ let response;
6257
+ try {
6258
+ response = await callHelper(deps, session.helperPath, {
6259
+ op: "register",
6260
+ vaultId: opts.vaultId,
6261
+ envelopeId: opts.envelopeId,
6262
+ digest: base64.encode(digest),
6263
+ deviceId: session.device.deviceId,
6264
+ expectSnapshot: session.snapshotId,
6265
+ rpId: RP_ID,
6266
+ userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
6267
+ userName: userNameFor(opts.vaultId, opts.envelopeId),
6268
+ clientDataHash: base64.encode(digest),
6269
+ ...session.pin !== undefined ? { pin: session.pin } : {},
6270
+ ...exclude.length > 0 ? { excludeCredentialIds: exclude } : {}
6271
+ });
6272
+ } catch (error) {
6273
+ if (error instanceof VaultError && error.code === "VAULT_KEY_ALREADY_ENROLLED")
6274
+ throw alreadyEnrolled(session.device);
6275
+ throw error;
6276
+ }
6277
+ if (exclude.length > 0 && response.excluded !== exclude.length) {
6278
+ throw helperPredatesExcludeList(session.helperPath, true);
6279
+ }
6184
6280
  const authData = base64.decode(response.authData);
6185
6281
  assertAuthenticatorData(authData, RP_ID, "registration");
6186
6282
  return {
@@ -6220,7 +6316,7 @@ async function assertPrf(deps, session, envelope, vaultId, purpose) {
6220
6316
  }
6221
6317
  return prfOutput;
6222
6318
  }
6223
- var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.";
6319
+ var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.", ALREADY_ENROLLED_SUGGESTION = "Insert a different security key, or name one with --device.", EXCLUDE_LIST_FEATURE = "exclude-list";
6224
6320
  var init_fido2 = __esm(() => {
6225
6321
  init_sha256();
6226
6322
  init_esm();
@@ -8344,6 +8440,23 @@ function retryAfterMs(header, now) {
8344
8440
  return;
8345
8441
  return Math.max(0, at - now);
8346
8442
  }
8443
+ function readProgramAccounts(method, entries, sliced) {
8444
+ const pubkeys = entries.map((entry) => {
8445
+ const pubkey = entry?.pubkey;
8446
+ if (typeof pubkey !== "string")
8447
+ throw new SolanaRpcError(`RPC ${method} answered an account without a pubkey`);
8448
+ return pubkey;
8449
+ });
8450
+ if (!sliced)
8451
+ return pubkeys;
8452
+ return entries.map((entry, i) => {
8453
+ const data = entry.account?.data;
8454
+ const encoded = Array.isArray(data) && data[1] === "base64" ? data[0] : undefined;
8455
+ if (typeof encoded !== "string")
8456
+ throw new SolanaRpcError(`RPC ${method} answered an account without base64 data`);
8457
+ return { pubkey: pubkeys[i], data: new Uint8Array(Buffer.from(encoded, "base64")) };
8458
+ });
8459
+ }
8347
8460
  function rawAccountView(value, address) {
8348
8461
  if (!value)
8349
8462
  return null;
@@ -8488,28 +8601,31 @@ function createSolanaRpc(url, fetchFn) {
8488
8601
  const r = await call("getSignaturesForAddress", [address, { limit: 1 }]);
8489
8602
  return Array.isArray(r) && r.length > 0;
8490
8603
  },
8491
- async isBlockhashValid(blockhash) {
8492
- const r = await call("isBlockhashValid", [blockhash, { commitment: "finalized" }]);
8604
+ async isBlockhashValid(blockhash, commitment = "finalized") {
8605
+ const r = await call("isBlockhashValid", [blockhash, { commitment }]);
8493
8606
  if (typeof r?.value !== "boolean")
8494
8607
  throw new Error("isBlockhashValid answered with a non-boolean value");
8495
8608
  return r.value;
8496
8609
  },
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);
8610
+ getProgramAccounts: async (programId, filters, opts) => {
8611
+ const r = await call("getProgramAccounts", [
8612
+ programId,
8613
+ {
8614
+ encoding: "base64",
8615
+ commitment: "finalized",
8616
+ dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
8617
+ filters
8618
+ }
8619
+ ], opts?.signal);
8499
8620
  if (!Array.isArray(r))
8500
8621
  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
- });
8622
+ return readProgramAccounts("getProgramAccounts", r, opts?.dataSlice !== undefined);
8507
8623
  },
8508
- async getProgramAccountsV2(programId, filters, opts) {
8624
+ getProgramAccountsV2: async (programId, filters, opts) => {
8509
8625
  const config = {
8510
8626
  encoding: "base64",
8511
8627
  commitment: "finalized",
8512
- dataSlice: { offset: 0, length: 0 },
8628
+ dataSlice: opts?.dataSlice ?? { offset: 0, length: 0 },
8513
8629
  filters,
8514
8630
  limit: opts?.limit ?? PROGRAM_ACCOUNTS_V2_LIMIT
8515
8631
  };
@@ -8520,19 +8636,17 @@ function createSolanaRpc(url, fetchFn) {
8520
8636
  throw new SolanaRpcError("RPC getProgramAccountsV2 answered without an account list");
8521
8637
  }
8522
8638
  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
- });
8639
+ const sliced = opts?.dataSlice !== undefined;
8640
+ const accounts = readProgramAccounts("getProgramAccountsV2", page.accounts, sliced);
8529
8641
  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");
8642
+ let paginationKey = null;
8643
+ if (cursor !== null && cursor !== undefined) {
8644
+ if (typeof cursor !== "string" || cursor.length === 0) {
8645
+ throw new SolanaRpcError("RPC getProgramAccountsV2 answered a paginationKey that is not a string");
8646
+ }
8647
+ paginationKey = cursor;
8534
8648
  }
8535
- return { pubkeys, paginationKey: cursor };
8649
+ return sliced ? { accounts, paginationKey } : { pubkeys: accounts, paginationKey };
8536
8650
  }
8537
8651
  };
8538
8652
  }
@@ -8647,7 +8761,7 @@ async function keysList(args, ctx) {
8647
8761
  async function keysCreate(args, ctx) {
8648
8762
  const { deps, apiUrl, json } = ctx;
8649
8763
  const parsed = parseArgs(args, {
8650
- valueFlags: ["--scopes", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
8764
+ valueFlags: ["--scopes", "--access", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
8651
8765
  });
8652
8766
  if ("error" in parsed) {
8653
8767
  writeUsageFailure(deps, parsed.error, json);
@@ -8657,7 +8771,20 @@ async function keysCreate(args, ctx) {
8657
8771
  writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}`, json);
8658
8772
  return 2;
8659
8773
  }
8660
- const requestedScopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
8774
+ if (parsed.values["--access"] !== undefined && parsed.values["--scopes"] !== undefined) {
8775
+ writeUsageFailure(deps, "--access and --scopes are mutually exclusive; pass one of them.", json);
8776
+ return 2;
8777
+ }
8778
+ let accessScopes;
8779
+ if (parsed.values["--access"] !== undefined) {
8780
+ const preset = ACCESS_LEVELS[parsed.values["--access"]];
8781
+ if (preset === undefined) {
8782
+ writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
8783
+ return 2;
8784
+ }
8785
+ accessScopes = scopesForPreset(preset);
8786
+ }
8787
+ const requestedScopes = accessScopes ?? (parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined);
8661
8788
  const environment = parsed.values["--environment"];
8662
8789
  const label = parsed.values["--label"]?.trim();
8663
8790
  if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > 64)) {
@@ -8753,7 +8880,7 @@ WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError
8753
8880
  `);
8754
8881
  }
8755
8882
  if (!requestedScopes) {
8756
- deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
8883
+ deps.stdout.write(`No --access or --scopes given: the server granted the default scopes (swap:write excluded).
8757
8884
  `);
8758
8885
  }
8759
8886
  if (storeError === undefined) {
@@ -8820,7 +8947,7 @@ async function keysRevoke(args, ctx) {
8820
8947
  }
8821
8948
  return 0;
8822
8949
  }
8823
- var KEYS_PATH = "/api/v1/agent/keys", NO_DEVICE_TOKEN;
8950
+ var KEYS_PATH = "/api/v1/agent/keys", ACCESS_LEVELS, NO_DEVICE_TOKEN;
8824
8951
  var init_keys = __esm(() => {
8825
8952
  init_agent_key_access();
8826
8953
  init_args();
@@ -8828,6 +8955,11 @@ var init_keys = __esm(() => {
8828
8955
  init_profiles();
8829
8956
  init_render();
8830
8957
  init_secret_store();
8958
+ ACCESS_LEVELS = {
8959
+ read: "read",
8960
+ "read-write": "readwrite",
8961
+ "read-write-transfer": "readwritetransfer"
8962
+ };
8831
8963
  NO_DEVICE_TOKEN = {
8832
8964
  code: "NO_DEVICE_TOKEN",
8833
8965
  message: "No device token available.",
@@ -8837,13 +8969,13 @@ var init_keys = __esm(() => {
8837
8969
 
8838
8970
  // src/vault/signer-roles.ts
8839
8971
  function tokenMintFilters(key) {
8840
- return [{ dataSize: 82 }, memcmp(0, concat2(COPTION_SOME_U32, key))];
8972
+ return [{ dataSize: 82 }, memcmp(4, key)];
8841
8973
  }
8842
8974
  function tokenFreezeFilters(key) {
8843
8975
  return [{ dataSize: 82 }, memcmp(46, concat2(COPTION_SOME_U32, key))];
8844
8976
  }
8845
8977
  function token2022MintFilters(key) {
8846
- const authority = memcmp(0, concat2(COPTION_SOME_U32, key));
8978
+ const authority = memcmp(4, key);
8847
8979
  return [
8848
8980
  [{ dataSize: 82 }, authority],
8849
8981
  [memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
@@ -8856,6 +8988,9 @@ function token2022FreezeFilters(key) {
8856
8988
  [memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
8857
8989
  ];
8858
8990
  }
8991
+ function keepSetAuthority(hits) {
8992
+ 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);
8993
+ }
8859
8994
  function programUpgradeFilters(key) {
8860
8995
  return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat2(COPTION_SOME_U8, key))];
8861
8996
  }
@@ -8950,19 +9085,31 @@ async function readSignerRoles(rpc, addresses, opts) {
8950
9085
  const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(Object.assign(new Error("timed out"), { name: "TimeoutError" })), timeoutMs) : undefined;
8951
9086
  try {
8952
9087
  requests += 1;
9088
+ const tagSlice = task.group.tagSlice;
8953
9089
  if (task.via === "v2") {
8954
9090
  if (rpc.getProgramAccountsV2 === undefined) {
8955
9091
  throw new SolanaRpcError("RPC getProgramAccountsV2 failed: -32601 Method not found", {
8956
9092
  rpcCode: RPC_METHOD_NOT_FOUND
8957
9093
  });
8958
9094
  }
8959
- return await rpc.getProgramAccountsV2(task.group.programId, filters, {
9095
+ const pageOpts = {
8960
9096
  signal: controller.signal,
8961
9097
  ...task.paginationKey !== undefined ? { paginationKey: task.paginationKey } : {}
8962
- });
9098
+ };
9099
+ if (tagSlice === undefined)
9100
+ return await rpc.getProgramAccountsV2(task.group.programId, filters, pageOpts);
9101
+ const page = await rpc.getProgramAccountsV2(task.group.programId, filters, { ...pageOpts, dataSlice: tagSlice });
9102
+ return { pubkeys: keepSetAuthority(page.accounts), paginationKey: page.paginationKey };
8963
9103
  }
8964
- const pubkeys = await rpc.getProgramAccounts(task.group.programId, filters, { signal: controller.signal });
8965
- return { pubkeys, paginationKey: null };
9104
+ if (tagSlice === undefined) {
9105
+ const pubkeys = await rpc.getProgramAccounts(task.group.programId, filters, { signal: controller.signal });
9106
+ return { pubkeys, paginationKey: null };
9107
+ }
9108
+ const hits = await rpc.getProgramAccounts(task.group.programId, filters, {
9109
+ signal: controller.signal,
9110
+ dataSlice: tagSlice
9111
+ });
9112
+ return { pubkeys: keepSetAuthority(hits), paginationKey: null };
8966
9113
  } finally {
8967
9114
  if (timer !== undefined)
8968
9115
  clearTimeout(timer);
@@ -9205,7 +9352,7 @@ var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", S
9205
9352
  return out;
9206
9353
  }, COPTION_SOME_U32, COPTION_SOME_U8, LOADER_PROGRAM_DATA_TAG, LOADER_PROGRAM_TAG, TOKEN_2022_MINT_ACCOUNT_TYPE, memcmp = (offset, bytes) => ({
9207
9354
  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");
9355
+ }), 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
9356
  var init_signer_roles = __esm(() => {
9210
9357
  init_esm();
9211
9358
  init_solana_lite();
@@ -9229,6 +9376,7 @@ var init_signer_roles = __esm(() => {
9229
9376
  LOADER_PROGRAM_DATA_TAG = new Uint8Array([3, 0, 0, 0]);
9230
9377
  LOADER_PROGRAM_TAG = new Uint8Array([2, 0, 0, 0]);
9231
9378
  TOKEN_2022_MINT_ACCOUNT_TYPE = new Uint8Array([1]);
9379
+ MINT_AUTHORITY_TAG_SLICE = { offset: 0, length: 4 };
9232
9380
  ROLE_GROUPS = [
9233
9381
  {
9234
9382
  id: "token-mint",
@@ -9236,7 +9384,8 @@ var init_signer_roles = __esm(() => {
9236
9384
  program: "token",
9237
9385
  programId: TOKEN_PROGRAM_ID,
9238
9386
  label: "token mint",
9239
- shapes: (key) => [tokenMintFilters(key)]
9387
+ shapes: (key) => [tokenMintFilters(key)],
9388
+ tagSlice: MINT_AUTHORITY_TAG_SLICE
9240
9389
  },
9241
9390
  {
9242
9391
  id: "token-freeze",
@@ -9252,7 +9401,8 @@ var init_signer_roles = __esm(() => {
9252
9401
  program: "token-2022",
9253
9402
  programId: TOKEN_2022_PROGRAM_ID,
9254
9403
  label: "Token-2022 mint",
9255
- shapes: token2022MintFilters
9404
+ shapes: token2022MintFilters,
9405
+ tagSlice: MINT_AUTHORITY_TAG_SLICE
9256
9406
  },
9257
9407
  {
9258
9408
  id: "token2022-freeze",
@@ -9298,6 +9448,7 @@ var init_signer_roles = __esm(() => {
9298
9448
  // src/vault/promote-support.ts
9299
9449
  var exports_promote_support = {};
9300
9450
  __export(exports_promote_support, {
9451
+ withToKey: () => withToKey,
9301
9452
  subjectPhrase: () => subjectPhrase,
9302
9453
  shortAddress: () => shortAddress,
9303
9454
  runRoleCheck: () => runRoleCheck,
@@ -9307,6 +9458,7 @@ __export(exports_promote_support, {
9307
9458
  promoteSentence: () => promoteSentence,
9308
9459
  printPromoteSentence: () => printPromoteSentence,
9309
9460
  findVaultRoleEntry: () => findVaultRoleEntry,
9461
+ findTransferSource: () => findTransferSource,
9310
9462
  findEntryByLabelOrAddress: () => findEntryByLabelOrAddress,
9311
9463
  controlledByJson: () => controlledByJson,
9312
9464
  confirmPrompt: () => confirmPrompt,
@@ -9352,6 +9504,10 @@ function findVaultRoleEntry(index, labelOrAddress) {
9352
9504
  return byLabel;
9353
9505
  return index.entries.find((entry) => entry.address === labelOrAddress);
9354
9506
  }
9507
+ function findTransferSource(index, labelOrAddress) {
9508
+ const byLabel = (role) => index.entries.find((entry) => entry.role === role && entry.label !== undefined && entry.label === labelOrAddress);
9509
+ return byLabel("vault") ?? byLabel("tee-wallet") ?? index.entries.find((entry) => entry.address === labelOrAddress);
9510
+ }
9355
9511
  function findEntryByLabelOrAddress(index, labelOrAddress) {
9356
9512
  const byLabel = index.entries.find((entry) => entry.label !== undefined && entry.label === labelOrAddress);
9357
9513
  if (byLabel !== undefined)
@@ -9472,6 +9628,9 @@ async function confirmPromotion(ctx, n) {
9472
9628
  function shortAddress(address) {
9473
9629
  return `${address.slice(0, 6)}…${address.slice(-4)}`;
9474
9630
  }
9631
+ function withToKey(controlledBy, toKey) {
9632
+ return { ...controlledBy, toKey };
9633
+ }
9475
9634
  function accountUnresolved(reason) {
9476
9635
  return new VaultError("PROMOTE_ACCOUNT_UNRESOLVED", `Could not confirm which Candle account this API key acts for (${reason}); nothing was written.`, {
9477
9636
  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 +9704,51 @@ function renderControlledBy(controlledBy, n) {
9545
9704
  const label = cleaned.length > 0 ? `(${cleaned}) ` : "";
9546
9705
  const environment = controlledBy.environment ?? "not a Candle host";
9547
9706
  const from = controlledBy.apiUrlFrom !== undefined ? `, from ${controlledBy.apiUrlFrom}` : "";
9707
+ const { toKey } = controlledBy;
9708
+ if (toKey === undefined) {
9709
+ return [
9710
+ `${subject} will be controlled by:`,
9711
+ ` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
9712
+ ` API key ${controlledBy.keyPrefix}… ${label}${source}`,
9713
+ ` API ${controlledBy.apiUrl} (${environment}${from})`
9714
+ ].join(`
9715
+ `);
9716
+ }
9717
+ const toLabel = toKey.label !== null && labelCell(toKey.label).length > 0 ? `(${labelCell(toKey.label)}) ` : "";
9548
9718
  return [
9549
9719
  `${subject} will be controlled by:`,
9550
9720
  ` Candle account ${controlledBy.username ?? "(no username)"} (${shortAddress(controlledBy.account)})`,
9551
- ` API key ${controlledBy.keyPrefix}… ${label}${source}`,
9552
- ` API ${controlledBy.apiUrl} (${environment}${from})`
9721
+ ` API key ${toKey.keyPrefix}… ${toLabel}--to-key, bound by a rebind after the import`,
9722
+ ` imported under ${controlledBy.keyPrefix}… ${label}${source}`,
9723
+ ` API ${controlledBy.apiUrl} (${environment}${from})`,
9724
+ ...toKey.warnings
9553
9725
  ].join(`
9554
9726
  `);
9555
9727
  }
9556
9728
  function controlledByJson(controlledBy) {
9729
+ const importedUnder = {
9730
+ keyPrefix: controlledBy.keyPrefix,
9731
+ keyLabel: controlledBy.keyLabel,
9732
+ keySource: controlledBy.keySource
9733
+ };
9734
+ if (controlledBy.toKey === undefined) {
9735
+ return {
9736
+ account: controlledBy.account,
9737
+ username: controlledBy.username,
9738
+ ...importedUnder,
9739
+ apiUrl: controlledBy.apiUrl,
9740
+ environment: controlledBy.environment
9741
+ };
9742
+ }
9557
9743
  return {
9558
9744
  account: controlledBy.account,
9559
9745
  username: controlledBy.username,
9560
- keyPrefix: controlledBy.keyPrefix,
9561
- keyLabel: controlledBy.keyLabel,
9562
- keySource: controlledBy.keySource,
9746
+ keyPrefix: controlledBy.toKey.keyPrefix,
9747
+ keyLabel: controlledBy.toKey.label,
9748
+ keySource: "--to-key",
9563
9749
  apiUrl: controlledBy.apiUrl,
9564
- environment: controlledBy.environment
9750
+ environment: controlledBy.environment,
9751
+ importedUnder
9565
9752
  };
9566
9753
  }
9567
9754
  async function runRoleCheck(ctx, rpc, addresses) {
@@ -13625,16 +13812,18 @@ async function request(ctx, key, path, body) {
13625
13812
  throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
13626
13813
  return result.body;
13627
13814
  }
13628
- async function teeWallets(ctx, key, scope) {
13815
+ async function listTradingWallets(ctx, key, scope) {
13629
13816
  let cursor;
13630
13817
  const rows = [];
13631
13818
  const cursors = new Set;
13632
13819
  let appId = "";
13820
+ let scopes = [];
13633
13821
  for (;; ) {
13634
13822
  const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
13635
- if (!response.scopes?.includes(scope))
13823
+ if (scope !== undefined && !response.scopes?.includes(scope))
13636
13824
  throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
13637
13825
  appId = response.privyAppId ?? "";
13826
+ scopes = response.scopes;
13638
13827
  if (!Array.isArray(response.page))
13639
13828
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
13640
13829
  rows.push(...response.page);
@@ -13645,7 +13834,7 @@ async function teeWallets(ctx, key, scope) {
13645
13834
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
13646
13835
  cursors.add(cursor);
13647
13836
  }
13648
- return { rows, appId };
13837
+ return { rows, appId, scopes };
13649
13838
  }
13650
13839
  function matchesName(row, name) {
13651
13840
  return row.id === name || row.address === name || row.label === name;
@@ -13675,32 +13864,31 @@ async function completeTradingWallet(ctx, row, appId, scope) {
13675
13864
  };
13676
13865
  }
13677
13866
  async function tradingWallet(ctx, key, name, scope) {
13678
- const { rows, appId } = await teeWallets(ctx, key, scope);
13867
+ const { rows, appId } = await listTradingWallets(ctx, key, scope);
13679
13868
  const matches = rows.filter((row) => matchesName(row, name));
13680
13869
  if (matches.length !== 1) {
13681
13870
  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)}.`);
13682
13871
  }
13683
13872
  return await completeTradingWallet(ctx, matches[0], appId, scope);
13684
13873
  }
13685
- async function tradingPayer(ctx, key, name) {
13686
- const scope = "swap:write";
13687
- const { rows, appId } = await teeWallets(ctx, key, scope);
13874
+ async function tradingPayer(ctx, key, name, scope = "swap:write") {
13875
+ const { rows, appId, scopes } = await listTradingWallets(ctx, key, scope);
13688
13876
  const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
13689
- const asEmbedded = () => ({ kind: "embedded", address: embedded });
13877
+ const asEmbedded = () => ({ kind: "embedded", address: embedded, scopes });
13690
13878
  const options = [
13691
13879
  ...rows.map((row) => `TEE ${describeWallet(row)}`),
13692
13880
  ...embedded ? [`embedded (${embedded})`] : []
13693
13881
  ].join("; ");
13694
13882
  if (name === undefined) {
13695
13883
  if (rows.length === 1 && !embedded)
13696
- return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope) };
13884
+ return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope), scopes };
13697
13885
  if (rows.length === 0 && embedded)
13698
13886
  return asEmbedded();
13699
13887
  throw new TradingError("PAYER_REQUIRED", options.length === 0 ? "This account has no wallet that can pay for a swap. Enrol a TEE wallet, or create an embedded wallet in the app." : `Name the payer with --wallet. This account can pay from: ${options}.`);
13700
13888
  }
13701
13889
  const matches = rows.filter((row) => matchesName(row, name));
13702
13890
  if (matches.length === 1)
13703
- return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope) };
13891
+ return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope), scopes };
13704
13892
  if (matches.length === 0 && embedded === name)
13705
13893
  return asEmbedded();
13706
13894
  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.` : options.length === 0 ? `"${name}" is not a wallet this account can pay from, and it has none: enrol a TEE wallet, or create an embedded wallet in the app.` : `"${name}" is not a wallet this account can pay from. It can pay from: ${options}.`);
@@ -13808,7 +13996,7 @@ Minimum received: ${safeText(quote.minimumReceived)}
13808
13996
  throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary trade prompt.");
13809
13997
  return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
13810
13998
  }
13811
- async function saveLaunchSignature(ctx, key, id, transaction) {
13999
+ async function saveOperationSignature(ctx, key, id, kind, transaction) {
13812
14000
  const bytes = Buffer.from(transaction, "base64");
13813
14001
  let offset = 0;
13814
14002
  let count = 0;
@@ -13825,7 +14013,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
13825
14013
  const signature = base58.encode(bytes.subarray(offset, offset + 64));
13826
14014
  const path = operationPath(ctx, key, id);
13827
14015
  const temporary = `${path}.${process.pid}.tmp`;
13828
- await writeFile4(temporary, JSON.stringify({ id, kind: "launch", signature }), { mode: 384 });
14016
+ await writeFile4(temporary, JSON.stringify({ id, kind, signature }), { mode: 384 });
13829
14017
  const file = await open3(temporary, "r");
13830
14018
  try {
13831
14019
  await file.sync();
@@ -13835,7 +14023,7 @@ async function saveLaunchSignature(ctx, key, id, transaction) {
13835
14023
  await rename3(temporary, path);
13836
14024
  return signature;
13837
14025
  }
13838
- var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, BASES;
14026
+ var TradingError, feeSchema, risksSchema, artifactSchema, swapBuildSchema, launchBuildSchema, walletSchema, walletPageSchema, embeddedSchema, operationSchema, lpAmountSchema, lpBuildSchema, lpPositionsSchema, lpPoolsSchema, BASES;
13839
14027
  var init_trading = __esm(() => {
13840
14028
  init_esm();
13841
14029
  init_zod();
@@ -13897,6 +14085,51 @@ var init_trading = __esm(() => {
13897
14085
  wallets: exports_external.object({ solana: exports_external.object({ address: exports_external.string() }).passthrough().nullable().optional() }).passthrough().optional()
13898
14086
  }).passthrough();
13899
14087
  operationSchema = exports_external.object({ job: exports_external.object({ status: exports_external.string() }).passthrough() }).passthrough();
14088
+ lpAmountSchema = exports_external.object({ mint: exports_external.string(), raw: exports_external.string().regex(/^\d+$/), decimals: exports_external.number().int() });
14089
+ lpBuildSchema = exports_external.object({
14090
+ build: exports_external.object({
14091
+ action: exports_external.enum(["add", "remove", "claim"]),
14092
+ pool: exports_external.string(),
14093
+ position: exports_external.string(),
14094
+ transaction: exports_external.string().min(1),
14095
+ walletAddress: exports_external.string(),
14096
+ signature: exports_external.string().optional()
14097
+ }).passthrough(),
14098
+ preview: exports_external.object({
14099
+ amounts: exports_external.array(lpAmountSchema).default([]),
14100
+ tokenRisks: risksSchema.default([]),
14101
+ warnings: exports_external.array(exports_external.string()).default([]),
14102
+ candleFeeBps: exports_external.number().optional()
14103
+ }).passthrough().nullable(),
14104
+ replay: exports_external.boolean().optional()
14105
+ });
14106
+ lpPositionsSchema = exports_external.object({
14107
+ positions: exports_external.array(exports_external.object({
14108
+ position: exports_external.string(),
14109
+ pool: exports_external.string(),
14110
+ tokens: exports_external.array(exports_external.object({
14111
+ mint: exports_external.string(),
14112
+ decimals: exports_external.number().int(),
14113
+ amountRaw: exports_external.string(),
14114
+ unclaimedFeesRaw: exports_external.string(),
14115
+ valueUsd: exports_external.number().nullable().optional()
14116
+ }).passthrough()),
14117
+ poolShare: exports_external.number().optional(),
14118
+ valueUsd: exports_external.number().nullable().optional()
14119
+ }).passthrough())
14120
+ });
14121
+ lpPoolsSchema = exports_external.object({
14122
+ page: exports_external.number().int(),
14123
+ pages: exports_external.number().int(),
14124
+ pools: exports_external.array(exports_external.object({
14125
+ pool: exports_external.string(),
14126
+ tokens: exports_external.array(exports_external.string()),
14127
+ liquidityUsd: exports_external.number().nullable().optional(),
14128
+ baseFeePercent: exports_external.number().nullable().optional(),
14129
+ volume24hUsd: exports_external.number().nullable().optional(),
14130
+ estimatedAprPercent: exports_external.number().nullable().optional()
14131
+ }).passthrough())
14132
+ });
13900
14133
  BASES = {
13901
14134
  SOL: { mint: "So11111111111111111111111111111111111111112", decimals: 9 },
13902
14135
  USDC: { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
@@ -22057,6 +22290,8 @@ __export(exports_vault_support, {
22057
22290
  menuSafeText: () => menuSafeText,
22058
22291
  findExternalEntry: () => findExternalEntry,
22059
22292
  factorMenu: () => factorMenu,
22293
+ factorAddPassphraseOnlyReason: () => factorAddPassphraseOnlyReason,
22294
+ factorAddAmong: () => factorAddAmong,
22060
22295
  describeRole: () => describeRole,
22061
22296
  describeEntry: () => describeEntry,
22062
22297
  derivationNotice: () => derivationNotice,
@@ -22155,6 +22390,20 @@ async function requireVaultRaw(ctx, resolved) {
22155
22390
  throw missingVault(ctx, resolved);
22156
22391
  return raw;
22157
22392
  }
22393
+ function factorAddAmong(envelopes) {
22394
+ return {
22395
+ envelopeIds: envelopes.filter((envelope) => isPassphraseEnvelope(envelope) || isCtap2Envelope(envelope)).map((envelope) => envelope.id),
22396
+ because: factorAddPassphraseOnlyReason,
22397
+ excludedBecause: "adding a factor opens the vault with the passphrase or a security key."
22398
+ };
22399
+ }
22400
+ function factorAddPassphraseOnlyReason(unusable) {
22401
+ if (unusable.length === 0) {
22402
+ return "adding a factor opens the vault with the passphrase or a security key, and this vault has no security key.";
22403
+ }
22404
+ const keys = unusable.map((entry) => `${entry.word} ${entry.id} cannot be used on this machine: ${entry.availability}`);
22405
+ return `adding a factor opens the vault with the passphrase or a security key, and ${keys.join("; ")}.`;
22406
+ }
22158
22407
  function derivationNotice(line, purpose) {
22159
22408
  if (purpose === undefined)
22160
22409
  return line;
@@ -22181,6 +22430,9 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22181
22430
  const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
22182
22431
  const notice = noticeFor(opts.purpose);
22183
22432
  if (choice.kind === "passphrase") {
22433
+ if (opts.onFactorChosen) {
22434
+ await opts.onFactorChosen(choice.envelopeId !== undefined ? { kind: "passphrase", envelopeId: choice.envelopeId } : { kind: "passphrase" });
22435
+ }
22184
22436
  if (choice.onlyBecause !== undefined)
22185
22437
  deps.stderr.write(`${PASSPHRASE_ONLY_PREFIX}${choice.onlyBecause}
22186
22438
  `);
@@ -22202,6 +22454,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22202
22454
  }
22203
22455
  if (choice.kind === "touch-id") {
22204
22456
  const envelope2 = choice.envelope;
22457
+ if (opts.onFactorChosen)
22458
+ await opts.onFactorChosen({ kind: "touch-id", envelope: envelope2 });
22205
22459
  const session2 = await openEnclaveSession(deps, envelope2.helper);
22206
22460
  const open5 = async (p, r, reason2) => {
22207
22461
  const current = parseVaultFile(r);
@@ -22230,6 +22484,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22230
22484
  }
22231
22485
  if (choice.kind === "passkey") {
22232
22486
  const envelope2 = choice.envelope;
22487
+ if (opts.onFactorChosen)
22488
+ await opts.onFactorChosen({ kind: "passkey", envelope: envelope2 });
22233
22489
  const session2 = await openPasskeySession(deps, envelope2.helper);
22234
22490
  const open5 = async (p, r, purpose) => {
22235
22491
  const current = parseVaultFile(r);
@@ -22257,11 +22513,14 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22257
22513
  };
22258
22514
  }
22259
22515
  const envelope = choice.envelope;
22516
+ const keyDevice = opts.onFactorChosen ? await opts.onFactorChosen({ kind: "security-key", envelope }) : undefined;
22260
22517
  const session = await openSecurityKeySession(deps, {
22261
22518
  vaultId: file.vaultId,
22262
22519
  envelopeId: envelope.id,
22263
- deviceFlag: ctx.vaultDevice,
22264
- ...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {},
22520
+ ...keyDevice !== undefined ? { preferDevice: keyDevice.preferDevice } : {
22521
+ deviceFlag: ctx.vaultDevice,
22522
+ ...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {}
22523
+ },
22265
22524
  requireFeatures: false
22266
22525
  });
22267
22526
  const open4 = async (p, r) => {
@@ -22355,7 +22614,7 @@ async function chooseFactor(ctx, vaultId, allEnvelopes, facts, requested, opts =
22355
22614
  }
22356
22615
  if (passphrases.length === 0 && drivable.length === 1)
22357
22616
  return choiceFor(drivable[0]);
22358
- const attached = ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
22617
+ const attached = "attached" in opts ? opts.attached : ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
22359
22618
  const notUsableHere = envelopes.filter((envelope) => !isPassphraseEnvelope(envelope) && !drivable.includes(envelope));
22360
22619
  const menu = factorMenu({
22361
22620
  keys: drivableKeys,
@@ -37604,7 +37863,7 @@ var init_update_notice = __esm(() => {
37604
37863
  });
37605
37864
 
37606
37865
  // ../mcp/src/orchestrate.ts
37607
- import { randomUUID as randomUUID3 } from "node:crypto";
37866
+ import { randomUUID as randomUUID4 } from "node:crypto";
37608
37867
  function requireApiKey(cfg) {
37609
37868
  if (!cfg.apiKey) {
37610
37869
  throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
@@ -37716,7 +37975,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
37716
37975
  }
37717
37976
  async function executeTrade(args, cfg, doFetch) {
37718
37977
  const apiKey = requireApiKey(cfg);
37719
- const clientTradeId = args.clientTradeId ?? randomUUID3();
37978
+ const clientTradeId = args.clientTradeId ?? randomUUID4();
37720
37979
  if (args.amount !== undefined && args.percent !== undefined) {
37721
37980
  return errText("pass exactly one of amount or percent, not both", { clientTradeId });
37722
37981
  }
@@ -37854,7 +38113,7 @@ async function executeTrade(args, cfg, doFetch) {
37854
38113
  }
37855
38114
  async function executeLaunchAndSeed(args, cfg, doFetch) {
37856
38115
  const apiKey = requireApiKey(cfg);
37857
- const clientLaunchId = args.clientLaunchId ?? randomUUID3();
38116
+ const clientLaunchId = args.clientLaunchId ?? randomUUID4();
37858
38117
  const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...launchFields } = args;
37859
38118
  let buyAmount;
37860
38119
  if (devBuy !== undefined) {
@@ -38263,8 +38522,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
38263
38522
  inputSchema: setProfileWalletsShape
38264
38523
  }, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
38265
38524
  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.",
38525
+ title: "Read an agent profile's P&L",
38526
+ 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
38527
  inputSchema: profilePnlShape
38269
38528
  }, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
38270
38529
  register("candle_get_profile_trades", {
@@ -38593,7 +38852,7 @@ init_render();
38593
38852
  init_secret_store();
38594
38853
 
38595
38854
  // src/version.ts
38596
- var CLI_VERSION = "0.11.6";
38855
+ var CLI_VERSION = "0.11.8";
38597
38856
 
38598
38857
  // src/commands/auth.ts
38599
38858
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -38615,6 +38874,10 @@ async function authLogin(args, ctx) {
38615
38874
  return 2;
38616
38875
  }
38617
38876
  const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
38877
+ if (scopes?.includes("transfer:bound")) {
38878
+ writeUsageFailure(deps, "transfer:bound is not available on a device login. Mint a Read:Write:Transfer key with: candle keys create --access read-write-transfer", json);
38879
+ return 2;
38880
+ }
38618
38881
  const label = parsed.values["--label"]?.trim();
38619
38882
  const noBrowser = parsed.booleans.has("--no-browser");
38620
38883
  if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > MAX_CLIENT_NAME_LENGTH)) {
@@ -39092,6 +39355,71 @@ var HELP = {
39092
39355
  ],
39093
39356
  env: ENV_API
39094
39357
  },
39358
+ lp: {
39359
+ group: "Trade",
39360
+ summary: "Meteora DAMM v2 liquidity from a TEE wallet: pools, add, positions, remove, claim",
39361
+ 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).",
39362
+ usage: ["candle lp <subcommand> [flags]"],
39363
+ rows: [
39364
+ {
39365
+ invocation: "pools <token> [--page <n>]",
39366
+ description: "Pools listing a token: liquidity, fee, 24h volume, estimated yield"
39367
+ },
39368
+ {
39369
+ invocation: "add <pool> --amount <n> <token> --wallet <tee> [--position <nft>]",
39370
+ description: "Open a position, or add to one; the other side follows the pool's ratio"
39371
+ },
39372
+ { invocation: "positions [--wallet <tee>]", description: "Every position across your TEE wallets" },
39373
+ {
39374
+ invocation: "remove <position> --percent <n> [--wallet <tee>]",
39375
+ description: "Withdraw; at 100% also claims fees and closes the position"
39376
+ },
39377
+ { invocation: "claim <position> [--wallet <tee>]", description: "Claim fees and any rewards" }
39378
+ ],
39379
+ flags: [
39380
+ { invocation: "--wallet <tee>", description: "The TEE wallet by id, address or unique label" },
39381
+ { invocation: "--rpc-url <url>", description: "Your Solana RPC, for mint reads and the broadcast" },
39382
+ {
39383
+ invocation: "--client-trade-id <id>",
39384
+ description: "Idempotency: the same id never deposits or withdraws twice"
39385
+ },
39386
+ { invocation: "--slippage-bps <n>", description: "add and remove: the tolerance, 0 to 1000 (default 100)" },
39387
+ { invocation: "--yes", description: "Skip the confirmation prompt (an ordinary prompt only)" }
39388
+ ],
39389
+ examples: [
39390
+ "candle lp pools So11111111111111111111111111111111111111112",
39391
+ "candle lp add <pool> --amount 0.5 SOL --wallet AgentOne --rpc-url https://<rpc>",
39392
+ "candle lp positions",
39393
+ "candle lp remove <position> --percent 100 --wallet AgentOne --rpc-url https://<rpc>"
39394
+ ],
39395
+ env: ENV_API
39396
+ },
39397
+ transfer: {
39398
+ group: "Trade",
39399
+ summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
39400
+ description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Read:Write:Transfer key (transfer:bound); from that wallet, Candle allows only its own pinned vault or another of the account's wallets you linked while signed in or marked trusted. To a linked wallet the amount counts against the key's spend caps and must be a base asset; to the vault any token and max are allowed. Candle builds the transaction, this machine approves the relay, Privy signs, Candle broadcasts. The destination and its kind are shown before you confirm.",
39401
+ usage: [
39402
+ "candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
39403
+ ],
39404
+ rows: [],
39405
+ flags: [
39406
+ {
39407
+ invocation: "--to <address|wallet name|vault>",
39408
+ description: "Where the funds go: vault is the wallet's own pin"
39409
+ },
39410
+ { invocation: "--asset <SOL|USDC|CNDL>", description: "A base asset (required for a linked-wallet destination)" },
39411
+ { invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
39412
+ { invocation: "--amount <decimal|max>", description: "How much, or max for the whole spendable balance" },
39413
+ { invocation: "--wallet <tee>", description: "The TEE wallet the funds leave; optional with one payer" },
39414
+ { invocation: "--rpc-url <url>", description: "Your own Solana RPC, to read a --mint's decimals" },
39415
+ { invocation: "--yes", description: "Skip the confirmation prompt (the destination is still printed)" }
39416
+ ],
39417
+ examples: [
39418
+ "candle transfer --to vault --asset SOL --amount max --wallet AgentOne",
39419
+ "candle transfer --to treasury --asset USDC --amount 250 --wallet AgentOne --yes --json"
39420
+ ],
39421
+ env: ENV_API
39422
+ },
39095
39423
  launch: {
39096
39424
  group: "Trade",
39097
39425
  summary: "Create a Solana token (the first buy is a separate swap)",
@@ -39107,6 +39435,31 @@ var HELP = {
39107
39435
  examples: ["candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne"],
39108
39436
  env: ENV_API
39109
39437
  },
39438
+ portfolio: {
39439
+ group: "Account",
39440
+ summary: "Every wallet's holdings, prices and value in one table (vault read over your own RPC)",
39441
+ 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.",
39442
+ usage: ["candle portfolio [--rpc-url <url>] [--json]"],
39443
+ rows: [],
39444
+ flags: [
39445
+ {
39446
+ invocation: "--rpc-url <url>",
39447
+ description: "Your own Solana RPC, for the vault. Without it (or CANDLE_SOLANA_RPC_URL) the vault is not read"
39448
+ },
39449
+ KEYSTORE_FLAG
39450
+ ],
39451
+ examples: ["candle portfolio", "candle portfolio --rpc-url https://your-rpc.example --json"],
39452
+ env: [...ENV_API, ...ENV_LOCAL_SIGNING]
39453
+ },
39454
+ pnl: {
39455
+ group: "Account",
39456
+ summary: "P&L: realized, fees, unrealized and open positions (--profile for one key's own)",
39457
+ 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.",
39458
+ usage: ["candle pnl [--profile <name>] [--json]"],
39459
+ rows: [],
39460
+ examples: ["candle pnl", "candle pnl --profile scalper --json"],
39461
+ env: ENV_API
39462
+ },
39110
39463
  keys: {
39111
39464
  group: "Account",
39112
39465
  summary: "API keys, and the wallets each key may use",
@@ -39115,11 +39468,11 @@ var HELP = {
39115
39468
  rows: [
39116
39469
  {
39117
39470
  invocation: "list [--scopes]",
39118
- description: "List API keys: name and Read or Read:Write access; --scopes adds the raw scopes"
39471
+ description: "List API keys: name and Read, Read:Write or Read:Write:Transfer access; --scopes adds the raw scopes"
39119
39472
  },
39120
39473
  {
39121
- invocation: "create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
39122
- description: "Create an API key"
39474
+ invocation: "create [--access read|read-write|read-write-transfer | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
39475
+ description: "Create an API key; --access mints one of the three levels (read-write-transfer can move funds out of the wallet it runs)"
39123
39476
  },
39124
39477
  { invocation: "revoke <prefix>", description: "Revoke an API key" },
39125
39478
  { invocation: "wallets <prefix>", description: "Wallets an agent profile can use" },
@@ -39131,6 +39484,7 @@ var HELP = {
39131
39484
  ],
39132
39485
  examples: [
39133
39486
  "candle keys list",
39487
+ "candle keys create --access read-write-transfer --label rebalancer",
39134
39488
  "candle keys create --scopes trade:write --label agent-one",
39135
39489
  "candle keys wallets ck_live_ab12",
39136
39490
  "candle keys revoke ck_live_ab12"
@@ -39141,19 +39495,28 @@ var HELP = {
39141
39495
  group: "Account",
39142
39496
  display: "wallet",
39143
39497
  summary: "Launch and linked wallets; import or revoke one (wallets is an alias)",
39144
- description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
39498
+ description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key and a Trusted column saying whether it is yours: linked while signed in, or marked with wallet trust. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
39145
39499
  usage: ["candle wallet [flags]", "candle wallet <subcommand> [flags]"],
39146
39500
  rows: [
39147
39501
  {
39148
39502
  invocation: "import --chain <solana|evm> [options]",
39149
39503
  description: "Import a wallet you own (key via --key-file or hidden prompt)"
39150
39504
  },
39151
- { invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" }
39505
+ { invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" },
39506
+ {
39507
+ invocation: "trust <label|address|id|prefix*>...",
39508
+ description: "Mark linked wallets yours, so agents can move funds into them (owner only; typed confirm)"
39509
+ },
39510
+ {
39511
+ invocation: "untrust <label|address|id|prefix*>... [--yes]",
39512
+ description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
39513
+ }
39152
39514
  ],
39153
39515
  examples: [
39154
39516
  "candle wallet",
39155
39517
  "candle wallet import --chain solana --key-file ./signer.json",
39156
- "candle wallet revoke wal_123"
39518
+ "candle wallet revoke wal_123",
39519
+ "candle wallet trust 'tr-*' 'dest-*'"
39157
39520
  ],
39158
39521
  env: ENV_API
39159
39522
  },
@@ -39228,14 +39591,14 @@ var HELP = {
39228
39591
  },
39229
39592
  {
39230
39593
  invocation: "transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>",
39231
- description: "Sign a vault-key transfer locally"
39594
+ description: "Sign a transfer locally from a vault key or a promoted TEE wallet"
39232
39595
  },
39233
39596
  {
39234
- invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>]",
39597
+ invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
39235
39598
  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
39599
  },
39237
39600
  {
39238
- invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings]",
39601
+ invocation: "promote-batch --pairs-from <file> --rpc-url <url> [--to-key <prefix|label>] [--token-holdings]",
39239
39602
  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
39603
  },
39241
39604
  {
@@ -39257,7 +39620,10 @@ var HELP = {
39257
39620
  invocation: "--factor <id|kind>",
39258
39621
  description: "Unlock with this envelope: an id from factor list, or passphrase, security-key, touch-id, passkey; without it, a terminal lists the factors to pick by number"
39259
39622
  },
39260
- { invocation: "--device <id>", description: "The security key to use when more than one is attached" },
39623
+ {
39624
+ invocation: "--device <id>",
39625
+ description: "The security key to use when more than one is attached; on factor add security-key, the key to add (the vault opens with the passphrase or another key)"
39626
+ },
39261
39627
  {
39262
39628
  invocation: "--balances",
39263
39629
  description: "list: SOL per matched key, read from your RPC (one request per 100 matched keys). Every matched address goes to that one endpoint together, which links them; tokens are never read"
@@ -39269,6 +39635,10 @@ var HELP = {
39269
39635
  {
39270
39636
  invocation: "--pairs-from <file>",
39271
39637
  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."
39638
+ },
39639
+ {
39640
+ invocation: "--to-key <prefix|label>",
39641
+ 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
39642
  }
39273
39643
  ],
39274
39644
  examples: [
@@ -39278,6 +39648,7 @@ var HELP = {
39278
39648
  "candle vault rename key-7 treasury-cold",
39279
39649
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
39280
39650
  "candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc>",
39651
+ "candle vault promote-batch --pairs-from ./promote-plan.csv --rpc-url https://<rpc> --to-key tr-01",
39281
39652
  "candle vault enroll security-key --label yubikey-a",
39282
39653
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
39283
39654
  "candle vault backup --to icloud",
@@ -39310,7 +39681,7 @@ var HELP = {
39310
39681
  },
39311
39682
  {
39312
39683
  invocation: "sweep <address> --rpc-url <url> [--emergency]",
39313
- description: "Sign locally and move everything to the pinned vault"
39684
+ 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
39685
  },
39315
39686
  {
39316
39687
  invocation: "rebind <wallet...> --to-key <prefix|label>",
@@ -43820,7 +44191,7 @@ function validClientId(id) {
43820
44191
  }
43821
44192
  async function lookupOperation(ctx, key, id, kind) {
43822
44193
  const local = await savedOperation(ctx, key, id);
43823
- const kinds = kind ? [kind] : local ? [local.kind] : ["trade", "swap", "launch"];
44194
+ const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
43824
44195
  const found = [];
43825
44196
  for (const candidate of kinds) {
43826
44197
  try {
@@ -44095,7 +44466,7 @@ async function launch(args, ctx) {
44095
44466
  if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
44096
44467
  throw new TradingError("QUOTE_EXPIRED", "The launch build expired before signing.");
44097
44468
  const signed = await relaySign(ctx, key, wallet, built.transaction);
44098
- const signature = await saveLaunchSignature(ctx, key, id, signed);
44469
+ const signature = await saveOperationSignature(ctx, key, id, "launch", signed);
44099
44470
  const broadcastSignature = await createSolanaRpc(url, ctx.deps.fetch).sendTransaction(signed);
44100
44471
  if (broadcastSignature !== signature)
44101
44472
  throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
@@ -44108,6 +44479,368 @@ async function launch(args, ctx) {
44108
44479
  }
44109
44480
  }
44110
44481
 
44482
+ // src/commands/lp.ts
44483
+ init_args();
44484
+ init_render();
44485
+ init_solana_lite();
44486
+ import { randomUUID as randomUUID3 } from "node:crypto";
44487
+ init_trading();
44488
+ var LP_SCOPE = "lp:write";
44489
+ var CONFIRM_POLL_MS2 = 2000;
44490
+ var CONFIRM_MAX_POLLS2 = 45;
44491
+ async function lpRequest(ctx, key, path, body) {
44492
+ const result = await apiRequest(path, {
44493
+ apiUrl: ctx.apiUrl,
44494
+ credentials: { apiKey: key },
44495
+ auth: "key",
44496
+ method: body ? "POST" : "GET",
44497
+ body,
44498
+ fetch: ctx.deps.fetch,
44499
+ env: ctx.deps.env
44500
+ });
44501
+ if (!result.ok) {
44502
+ if (result.status === 404 && !result.code)
44503
+ throw new TradingError("LP_NOT_ENABLED", "This Candle deployment does not serve LP routes (LP_ENABLED is off there, or the API predates them).");
44504
+ throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
44505
+ }
44506
+ if (!result.body || typeof result.body !== "object")
44507
+ throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
44508
+ return result.body;
44509
+ }
44510
+ function solanaAddress(value, what) {
44511
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value))
44512
+ throw new TradingError("INVALID_ADDRESS", `${what} must be a Solana address (base58).`);
44513
+ return value;
44514
+ }
44515
+ function poolToken(value) {
44516
+ const base = baseAsset(value);
44517
+ if (base)
44518
+ return BASES[base]?.mint;
44519
+ return solanaAddress(value, "The token");
44520
+ }
44521
+ function matchesWallet(row, name) {
44522
+ return row.id === name || row.address === name || row.label === name;
44523
+ }
44524
+ function usage2(ctx, line) {
44525
+ writeUsageFailure(ctx.deps, line, ctx.json);
44526
+ return 2;
44527
+ }
44528
+ function amountLine(amount) {
44529
+ const base = Object.entries(BASES).find(([, asset]) => asset.mint === amount.mint)?.[0];
44530
+ return `${decimalAmount(amount.raw, amount.decimals)} ${base ?? amount.mint}`;
44531
+ }
44532
+ async function lpPools(args, ctx) {
44533
+ const parsed = parseArgs(args, { valueFlags: ["--page"] });
44534
+ const page = Number(parsed && !("error" in parsed) ? parsed.values["--page"] ?? "1" : "1");
44535
+ if ("error" in parsed)
44536
+ return usage2(ctx, parsed.error);
44537
+ if (parsed.positionals.length !== 1 || !Number.isSafeInteger(page) || page < 1)
44538
+ return usage2(ctx, "Usage: candle lp pools <token> [--page <n>]");
44539
+ try {
44540
+ const token = poolToken(parsed.positionals[0]);
44541
+ const key = await tradingKey(ctx);
44542
+ const pools = lpPoolsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/pools/${encodeURIComponent(token)}?page=${page}`));
44543
+ if (ctx.json)
44544
+ return printTradingResult(ctx, { success: true, token, ...pools });
44545
+ const out = ctx.deps.stdout;
44546
+ if (pools.pools.length === 0) {
44547
+ out.write(`No DAMM v2 pool lists ${token} (page ${pools.page} of ${pools.pages}).
44548
+ `);
44549
+ return 0;
44550
+ }
44551
+ const usd = (value) => value === null || value === undefined ? "unavailable" : `$${value.toFixed(2)}`;
44552
+ const pct = (value) => value === null || value === undefined ? "unavailable" : `${value.toFixed(2)}%`;
44553
+ out.write(`DAMM v2 pools for ${token} (page ${pools.page} of ${pools.pages}):
44554
+ `);
44555
+ for (const pool of pools.pools) {
44556
+ out.write(` ${safeText(pool.pool)}
44557
+ pair ${pool.tokens.map(safeText).join(" / ")}
44558
+ liquidity ${usd(pool.liquidityUsd)}, fee ${pct(pool.baseFeePercent)}, 24h volume ${usd(pool.volume24hUsd)}, est. yield ${pct(pool.estimatedAprPercent)} APR (indexed, not a quote)
44559
+ `);
44560
+ }
44561
+ return 0;
44562
+ } catch (error) {
44563
+ return tradingFailure(ctx, error);
44564
+ }
44565
+ }
44566
+ async function lpPositions(args, ctx) {
44567
+ const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
44568
+ if ("error" in parsed)
44569
+ return usage2(ctx, parsed.error);
44570
+ if (parsed.positionals.length !== 0)
44571
+ return usage2(ctx, "Usage: candle lp positions [--wallet <tee>]");
44572
+ try {
44573
+ const key = await tradingKey(ctx);
44574
+ const { rows } = await listTradingWallets(ctx, key);
44575
+ const name = parsed.values["--wallet"];
44576
+ const wallets = rows.filter((row) => row.chain === "solana" && (name === undefined || matchesWallet(row, name)));
44577
+ if (name !== undefined && wallets.length === 0)
44578
+ throw new TradingError("TEE_WALLET_REQUIRED", `No TEE wallet on this key is called "${name}".`);
44579
+ const report = [];
44580
+ for (const row of wallets) {
44581
+ const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
44582
+ report.push({
44583
+ id: row.id,
44584
+ address: row.address,
44585
+ ...row.label ? { label: row.label } : {},
44586
+ positions: positions.positions
44587
+ });
44588
+ }
44589
+ if (ctx.json)
44590
+ return printTradingResult(ctx, { success: true, wallets: report });
44591
+ const out = ctx.deps.stdout;
44592
+ const total = report.reduce((n, wallet) => n + wallet.positions.length, 0);
44593
+ if (total === 0) {
44594
+ out.write(`No DAMM v2 positions across ${report.length} TEE wallet(s).
44595
+ `);
44596
+ return 0;
44597
+ }
44598
+ for (const wallet of report) {
44599
+ if (wallet.positions.length === 0)
44600
+ continue;
44601
+ out.write(`${wallet.label ? `${safeText(wallet.label)} ` : ""}(${wallet.id}, ${safeText(wallet.address)})
44602
+ `);
44603
+ for (const position of wallet.positions) {
44604
+ out.write(` position ${safeText(position.position)} in pool ${safeText(position.pool)}
44605
+ `);
44606
+ for (const token of position.tokens) {
44607
+ 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)})`}
44608
+ `);
44609
+ }
44610
+ 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`}
44611
+ `);
44612
+ }
44613
+ }
44614
+ return 0;
44615
+ } catch (error) {
44616
+ return tradingFailure(ctx, error);
44617
+ }
44618
+ }
44619
+ async function walletHolding(ctx, key, position, name) {
44620
+ if (name !== undefined)
44621
+ return tradingWallet(ctx, key, name, LP_SCOPE);
44622
+ const { rows, appId } = await listTradingWallets(ctx, key, LP_SCOPE);
44623
+ for (const row of rows) {
44624
+ if (row.chain !== "solana")
44625
+ continue;
44626
+ const positions = lpPositionsSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/positions?linkedWalletId=${encodeURIComponent(row.id)}`));
44627
+ if (positions.positions.some((held) => held.position === position))
44628
+ return completeTradingWallet(ctx, row, appId, LP_SCOPE);
44629
+ }
44630
+ 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.`);
44631
+ }
44632
+ async function waitConfirmed(ctx, rpc2, signature, id) {
44633
+ for (let i = 0;i < CONFIRM_MAX_POLLS2; i++) {
44634
+ const observed = classifyStatus(await rpc2.getSignatureStatus(signature));
44635
+ if (observed.kind === "finalized")
44636
+ return;
44637
+ if (observed.kind === "failed")
44638
+ throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}.`);
44639
+ if (observed.kind === "nonfinal") {
44640
+ if (observed.err !== null && observed.err !== undefined)
44641
+ throw new TradingError("LP_TRANSACTION_FAILED", `Transaction ${signature} failed: ${JSON.stringify(observed.err)}.`);
44642
+ if (observed.confirmationStatus === "confirmed")
44643
+ return;
44644
+ }
44645
+ await ctx.deps.sleep(CONFIRM_POLL_MS2);
44646
+ }
44647
+ 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.`);
44648
+ }
44649
+ async function confirmPreview(ctx, plan, built) {
44650
+ const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
44651
+ output.write(`${safeText(plan.intent)}
44652
+ Payer: ${safeText(plan.wallet.address)}
44653
+ `);
44654
+ output.write(`Pool: ${safeText(built.build.pool)}
44655
+ Position: ${safeText(built.build.position)}
44656
+ `);
44657
+ const preview = built.preview;
44658
+ const amounts = preview?.amounts ?? [];
44659
+ if (amounts.length > 0) {
44660
+ const verb = plan.action === "add" ? "Deposit (maximum, at the pool's ratio)" : plan.action === "remove" ? "Withdraw (minimum)" : "Claim";
44661
+ output.write(`${verb}: ${amounts.map(amountLine).join(" + ")}
44662
+ `);
44663
+ }
44664
+ output.write(`Candle LP fee: ${safeText(preview?.candleFeeBps ?? 0)} bps
44665
+ `);
44666
+ for (const warning of preview?.warnings ?? [])
44667
+ output.write(`Warning: ${safeText(warning)}
44668
+ `);
44669
+ for (const risk of preview?.tokenRisks ?? [])
44670
+ output.write(`Warning (${safeText(risk.mint)}): ${safeText(risk.message)}
44671
+ `);
44672
+ if (built.replay)
44673
+ output.write(`This client id was already built; the same artifact is shown again.
44674
+ `);
44675
+ if (plan.yes)
44676
+ return true;
44677
+ if (!ctx.deps.isTTY.stdin)
44678
+ throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary LP prompt.");
44679
+ return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
44680
+ }
44681
+ async function runLpOperation(ctx, plan) {
44682
+ const { id, key, wallet, action } = plan;
44683
+ const confirm = async (signature2, extra) => {
44684
+ const result = await lpRequest(ctx, key, "/api/v1/agent/lp/confirm", { clientTradeId: id, signature: signature2 });
44685
+ return printTradingResult(ctx, {
44686
+ ...result,
44687
+ clientTradeId: id,
44688
+ kind: "lp",
44689
+ action,
44690
+ signature: signature2,
44691
+ wallet: safeText(wallet.address),
44692
+ ...extra
44693
+ });
44694
+ };
44695
+ const local = await savedOperation(ctx, key, id);
44696
+ if (local?.signature)
44697
+ return confirm(local.signature, { resumed: true });
44698
+ if (!local && !await claimOperation(ctx, key, id, "lp"))
44699
+ throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
44700
+ ctx.deps.stderr.write(`Operation: ${id}
44701
+ `);
44702
+ const built = lpBuildSchema.parse(await lpRequest(ctx, key, `/api/v1/agent/lp/${action}/build`, {
44703
+ linkedWalletId: wallet.id,
44704
+ clientTradeId: id,
44705
+ ...plan.body
44706
+ }));
44707
+ if (built.build.walletAddress !== wallet.address || built.build.action !== action)
44708
+ throw new TradingError("INVALID_RESPONSE", "The LP build does not name the requested wallet and action; nothing was signed.");
44709
+ if (built.build.signature)
44710
+ return confirm(built.build.signature, { resumed: true });
44711
+ const preview = {
44712
+ pool: built.build.pool,
44713
+ position: built.build.position,
44714
+ amounts: built.preview?.amounts ?? [],
44715
+ warnings: built.preview?.warnings ?? [],
44716
+ tokenRisks: built.preview?.tokenRisks ?? [],
44717
+ candleFeeBps: built.preview?.candleFeeBps ?? 0
44718
+ };
44719
+ if (!await confirmPreview(ctx, plan, built))
44720
+ return printTradingResult(ctx, {
44721
+ success: true,
44722
+ status: "cancelled",
44723
+ clientTradeId: id,
44724
+ kind: "lp",
44725
+ action,
44726
+ preview
44727
+ });
44728
+ const signed = await relaySign(ctx, key, wallet, built.build.transaction);
44729
+ const signature = await saveOperationSignature(ctx, key, id, "lp", signed);
44730
+ const rpc2 = createSolanaRpc(plan.url, ctx.deps.fetch);
44731
+ const echoed = await rpc2.sendTransaction(signed);
44732
+ if (echoed !== signature)
44733
+ throw new TradingError("RPC_FAILED", "RPC returned a different transaction signature; check the saved operation.");
44734
+ ctx.deps.stderr.write(`LP ${action} signature: ${signature}
44735
+ `);
44736
+ await waitConfirmed(ctx, rpc2, signature, id);
44737
+ return confirm(signature, { preview });
44738
+ }
44739
+ function slippageOf(flag) {
44740
+ const slippage = Number(flag ?? "100");
44741
+ if (!Number.isInteger(slippage) || slippage < 0 || slippage > 1000)
44742
+ throw new TradingError("INVALID_AMOUNT", "--slippage-bps must be an integer from 0 to 1000.");
44743
+ return slippage;
44744
+ }
44745
+ async function lpAdd(args, ctx) {
44746
+ const parsed = parseArgs(args, {
44747
+ valueFlags: ["--amount", "--wallet", "--position", "--client-trade-id", "--slippage-bps", "--rpc-url"],
44748
+ booleanFlags: ["--yes"]
44749
+ });
44750
+ if ("error" in parsed)
44751
+ return usage2(ctx, parsed.error);
44752
+ const flags = parsed.values;
44753
+ const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
44754
+ if (parsed.positionals.length !== 2 || !flags["--amount"] || !flags["--wallet"] || !validClientId(id))
44755
+ 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]");
44756
+ try {
44757
+ const pool = solanaAddress(parsed.positionals[0], "The pool");
44758
+ const token = poolToken(parsed.positionals[1]);
44759
+ const position = flags["--position"] ? solanaAddress(flags["--position"], "--position") : undefined;
44760
+ rawAmount(flags["--amount"], 18);
44761
+ const slippageBps = slippageOf(flags["--slippage-bps"]);
44762
+ const url = rpcUrl(ctx, flags["--rpc-url"]);
44763
+ const key = await tradingKey(ctx);
44764
+ const wallet = await tradingWallet(ctx, key, flags["--wallet"], LP_SCOPE);
44765
+ const decimals = await decimalsFor(ctx, baseAsset(token) ?? token, flags["--rpc-url"]);
44766
+ const amountRaw = rawAmount(flags["--amount"], decimals);
44767
+ return await runLpOperation(ctx, {
44768
+ action: "add",
44769
+ id,
44770
+ key,
44771
+ wallet,
44772
+ url,
44773
+ yes: parsed.booleans.has("--yes"),
44774
+ intent: `Add ${decimalAmount(amountRaw, decimals)} ${baseAsset(token) ?? token} of liquidity to DAMM v2 pool ${pool}${position ? ` (position ${position})` : " (new position)"}`,
44775
+ body: { pool, token, amountRaw, slippageBps, ...position ? { position } : {} }
44776
+ });
44777
+ } catch (error) {
44778
+ return tradingFailure(ctx, error, id);
44779
+ }
44780
+ }
44781
+ async function lpRemove(args, ctx) {
44782
+ const parsed = parseArgs(args, {
44783
+ valueFlags: ["--percent", "--wallet", "--client-trade-id", "--slippage-bps", "--rpc-url"],
44784
+ booleanFlags: ["--yes"]
44785
+ });
44786
+ if ("error" in parsed)
44787
+ return usage2(ctx, parsed.error);
44788
+ const flags = parsed.values;
44789
+ const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
44790
+ const percent = Number(flags["--percent"]);
44791
+ if (parsed.positionals.length !== 1 || !flags["--percent"] || !/^\d+(\.\d{1,2})?$/.test(flags["--percent"]) || !(percent > 0 && percent <= 100) || !validClientId(id))
44792
+ 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]");
44793
+ try {
44794
+ const position = solanaAddress(parsed.positionals[0], "The position");
44795
+ const slippageBps = slippageOf(flags["--slippage-bps"]);
44796
+ const url = rpcUrl(ctx, flags["--rpc-url"]);
44797
+ const key = await tradingKey(ctx);
44798
+ const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
44799
+ return await runLpOperation(ctx, {
44800
+ action: "remove",
44801
+ id,
44802
+ key,
44803
+ wallet,
44804
+ url,
44805
+ yes: parsed.booleans.has("--yes"),
44806
+ 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}`,
44807
+ body: { position, percent, slippageBps }
44808
+ });
44809
+ } catch (error) {
44810
+ return tradingFailure(ctx, error, id);
44811
+ }
44812
+ }
44813
+ async function lpClaim(args, ctx) {
44814
+ const parsed = parseArgs(args, {
44815
+ valueFlags: ["--wallet", "--client-trade-id", "--rpc-url"],
44816
+ booleanFlags: ["--yes"]
44817
+ });
44818
+ if ("error" in parsed)
44819
+ return usage2(ctx, parsed.error);
44820
+ const flags = parsed.values;
44821
+ const id = flags["--client-trade-id"] ?? `lp-${randomUUID3()}`;
44822
+ if (parsed.positionals.length !== 1 || !validClientId(id))
44823
+ return usage2(ctx, "Usage: candle lp claim <position> [--wallet <tee>] [--client-trade-id <id>] [--rpc-url <url>] [--yes]");
44824
+ try {
44825
+ const position = solanaAddress(parsed.positionals[0], "The position");
44826
+ const url = rpcUrl(ctx, flags["--rpc-url"]);
44827
+ const key = await tradingKey(ctx);
44828
+ const wallet = await walletHolding(ctx, key, position, flags["--wallet"]);
44829
+ return await runLpOperation(ctx, {
44830
+ action: "claim",
44831
+ id,
44832
+ key,
44833
+ wallet,
44834
+ url,
44835
+ yes: parsed.booleans.has("--yes"),
44836
+ intent: `Claim the fees and any rewards of position ${position}`,
44837
+ body: { position }
44838
+ });
44839
+ } catch (error) {
44840
+ return tradingFailure(ctx, error, id);
44841
+ }
44842
+ }
44843
+
44111
44844
  // src/commands/mcp.ts
44112
44845
  init_args();
44113
44846
  init_deps();
@@ -44434,20 +45167,20 @@ async function writeSecretNames(deps, profile, names) {
44434
45167
  else
44435
45168
  await deps.updateProfile(profile, { secretNames: sorted });
44436
45169
  }
44437
- function usage2(ctx, line) {
45170
+ function usage3(ctx, line) {
44438
45171
  writeUsageFailure(ctx.deps, line, ctx.json);
44439
45172
  return 2;
44440
45173
  }
44441
45174
  async function secretsSet(args, ctx) {
44442
45175
  const parsed = parseArgs(args, {});
44443
45176
  if ("error" in parsed)
44444
- return usage2(ctx, parsed.error);
45177
+ return usage3(ctx, parsed.error);
44445
45178
  const [raw, extra] = parsed.positionals;
44446
45179
  if (!raw || extra !== undefined)
44447
- return usage2(ctx, "Usage: candle secrets set <name>");
45180
+ return usage3(ctx, "Usage: candle secrets set <name>");
44448
45181
  const name = canonicalSecretName(raw);
44449
45182
  if (name === undefined) {
44450
- return usage2(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
45183
+ return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
44451
45184
  }
44452
45185
  if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
44453
45186
  writeLocalFailure(ctx.deps, {
@@ -44459,7 +45192,7 @@ async function secretsSet(args, ctx) {
44459
45192
  }
44460
45193
  const value = await ctx.deps.promptSecret(`Value for ${name} (input hidden): `);
44461
45194
  if (value.length === 0)
44462
- return usage2(ctx, "An empty value was typed; nothing was stored.");
45195
+ return usage3(ctx, "An empty value was typed; nothing was stored.");
44463
45196
  try {
44464
45197
  await ctx.deps.secretsStore.set(secretRef(ctx.profile, name), value);
44465
45198
  } catch (error) {
@@ -44479,9 +45212,9 @@ async function secretsSet(args, ctx) {
44479
45212
  async function secretsList(args, ctx) {
44480
45213
  const parsed = parseArgs(args, {});
44481
45214
  if ("error" in parsed)
44482
- return usage2(ctx, parsed.error);
45215
+ return usage3(ctx, parsed.error);
44483
45216
  if (parsed.positionals.length > 0)
44484
- return usage2(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
45217
+ return usage3(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
44485
45218
  const names = await storedSecretNames(ctx.deps, ctx.profile);
44486
45219
  if (ctx.json) {
44487
45220
  ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
@@ -44500,13 +45233,13 @@ async function secretsList(args, ctx) {
44500
45233
  async function secretsRemove(args, ctx) {
44501
45234
  const parsed = parseArgs(args, {});
44502
45235
  if ("error" in parsed)
44503
- return usage2(ctx, parsed.error);
45236
+ return usage3(ctx, parsed.error);
44504
45237
  const [raw, extra] = parsed.positionals;
44505
45238
  if (!raw || extra !== undefined)
44506
- return usage2(ctx, "Usage: candle secrets remove <name>");
45239
+ return usage3(ctx, "Usage: candle secrets remove <name>");
44507
45240
  const name = canonicalSecretName(raw);
44508
45241
  if (name === undefined) {
44509
- return usage2(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
45242
+ return usage3(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
44510
45243
  }
44511
45244
  await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name));
44512
45245
  const names = await storedSecretNames(ctx.deps, ctx.profile);
@@ -44618,6 +45351,645 @@ async function runPlugin(name, rawArgs, ctx) {
44618
45351
  return ctx.deps.runPlugin(path, split2.passthrough, env);
44619
45352
  }
44620
45353
 
45354
+ // src/commands/pnl.ts
45355
+ init_args();
45356
+ init_deps();
45357
+ init_profiles();
45358
+ init_render();
45359
+
45360
+ // src/usd.ts
45361
+ function formatUsd(value) {
45362
+ if (!Number.isFinite(value))
45363
+ return "?";
45364
+ const sign2 = value < 0 ? "-" : "";
45365
+ const abs = Math.abs(value);
45366
+ if (abs > 0 && abs < 0.005)
45367
+ return `${sign2}<$0.01`;
45368
+ return `${sign2}$${abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
45369
+ }
45370
+ function formatPrice(value) {
45371
+ if (!Number.isFinite(value) || value <= 0)
45372
+ return "?";
45373
+ if (value >= 1)
45374
+ return formatUsd(value);
45375
+ return `$${Number(value.toPrecision(4)).toLocaleString("en-US", { maximumFractionDigits: 12 })}`;
45376
+ }
45377
+ function formatAmount(raw, decimals) {
45378
+ const digits = BigInt(raw).toString().padStart(decimals + 1, "0");
45379
+ if (decimals === 0)
45380
+ return digits;
45381
+ const whole = digits.slice(0, -decimals);
45382
+ const fraction = digits.slice(-decimals).replace(/0+$/, "");
45383
+ return fraction ? `${whole}.${fraction}` : whole;
45384
+ }
45385
+ function formatQuantity(value) {
45386
+ if (!Number.isFinite(value))
45387
+ return "?";
45388
+ return value.toLocaleString("en-US", { maximumFractionDigits: 6 });
45389
+ }
45390
+ function shortAddress2(address) {
45391
+ return address.length > 12 ? `${address.slice(0, 4)}…${address.slice(-4)}` : address;
45392
+ }
45393
+
45394
+ // src/commands/pnl.ts
45395
+ var NO_API_KEY2 = {
45396
+ code: "NO_API_KEY",
45397
+ message: "No API key for this profile.",
45398
+ suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
45399
+ };
45400
+ async function pnl(args, ctx) {
45401
+ const { deps, apiUrl, json } = ctx;
45402
+ const parsed = parseArgs(args, {});
45403
+ if ("error" in parsed) {
45404
+ writeUsageFailure(deps, parsed.error, json);
45405
+ return 2;
45406
+ }
45407
+ if (parsed.positionals.length > 0) {
45408
+ writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}. Usage: candle pnl [--profile <name>]`, json);
45409
+ return 2;
45410
+ }
45411
+ const apiKey = await resolveApiKey(deps, ctx.profile);
45412
+ if (!apiKey) {
45413
+ writeLocalFailure(deps, NO_API_KEY2, json);
45414
+ return 1;
45415
+ }
45416
+ const perProfile = ctx.profileFlag !== undefined;
45417
+ let keyPrefix;
45418
+ if (perProfile) {
45419
+ keyPrefix = apiKeyPrefix(apiKey);
45420
+ if (keyPrefix === undefined) {
45421
+ writeLocalFailure(deps, {
45422
+ code: "BAD_REQUEST",
45423
+ message: `The API key for profile ${ctx.profileFlag} is not a Candle agent key, so it names no profile to read.`,
45424
+ suggestion: "Run `candle auth login --profile <name>` to store a key for that profile."
45425
+ }, json);
45426
+ return 1;
45427
+ }
45428
+ }
45429
+ await printIdentity(ctx);
45430
+ const path = perProfile ? `/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl` : "/api/v1/agent/books";
45431
+ const result = await apiRequest(path, {
45432
+ auth: "key",
45433
+ credentials: { apiKey },
45434
+ apiUrl,
45435
+ fetch: deps.fetch,
45436
+ env: deps.env
45437
+ });
45438
+ if (!result.ok) {
45439
+ if (!perProfile && result.code === "SCOPE_MISSING") {
45440
+ writeLocalFailure(deps, {
45441
+ code: "SCOPE_MISSING",
45442
+ message: "The account's P&L needs a key with the Read scope (account:read); this profile's key has none.",
45443
+ suggestion: "Log in with a Read or Read:Write key, or read this key's own P&L: candle pnl --profile <name>"
45444
+ }, json);
45445
+ return 1;
45446
+ }
45447
+ writeFailure(deps, result, { apiUrl, authType: "key" }, json);
45448
+ return 1;
45449
+ }
45450
+ const body = result.body;
45451
+ if (json) {
45452
+ const { success: _success, ...rest } = body;
45453
+ deps.stdout.write(`${JSON.stringify({ ok: true, scope: perProfile ? "profile" : "account", ...rest })}
45454
+ `);
45455
+ return 0;
45456
+ }
45457
+ if (perProfile) {
45458
+ const { pnl: p, lp } = body;
45459
+ deps.stdout.write(`P&L for profile ${ctx.profileFlag} (key ${keyPrefix}): this key's own fills
45460
+
45461
+ `);
45462
+ writeSummary(ctx, {
45463
+ realizedNetUsd: p.realizedNetUsd,
45464
+ realizedGrossUsd: p.realizedGrossUsd,
45465
+ feesUsd: p.feesUsd,
45466
+ unrealizedUsd: p.unrealizedUsd,
45467
+ unmarked: p.unmarkedPositions,
45468
+ unvalued: p.unvalued,
45469
+ counted: p.counted,
45470
+ positions: p.openPositions.length,
45471
+ truncated: p.truncated,
45472
+ lookback: p.lookback,
45473
+ lookbackUnit: "trades",
45474
+ oldestMarkAt: p.oldestMarkAt,
45475
+ lp
45476
+ });
45477
+ writePositions(ctx, p.openPositions, false);
45478
+ writeLpPositions(ctx, lp, false);
45479
+ return 0;
45480
+ }
45481
+ const books = body;
45482
+ deps.stdout.write(`P&L for the account: every profile, the web app and the CLI, one ledger
45483
+
45484
+ `);
45485
+ writeSummary(ctx, {
45486
+ ...books.all,
45487
+ positions: books.positions.length,
45488
+ truncated: books.truncated,
45489
+ lookback: books.lookback,
45490
+ lookbackUnit: "ledger rows",
45491
+ oldestMarkAt: books.oldestMarkAt,
45492
+ lp: books.lp
45493
+ });
45494
+ writePositions(ctx, books.positions, true);
45495
+ writeLpPositions(ctx, books.lp, true);
45496
+ return 0;
45497
+ }
45498
+ function writeSummary(ctx, s) {
45499
+ const marked = s.positions - s.unmarked;
45500
+ const lines = [
45501
+ [
45502
+ "Realized net",
45503
+ formatUsd(s.realizedNetUsd),
45504
+ `gross ${formatUsd(s.realizedGrossUsd)}, fees ${formatUsd(s.feesUsd)}`
45505
+ ],
45506
+ [
45507
+ "Unrealized",
45508
+ formatUsd(s.unrealizedUsd),
45509
+ `${marked} of ${s.positions} open ${s.positions === 1 ? "position" : "positions"} marked${s.unmarked > 0 ? `; ${s.unmarked} unpriced, not counted` : ""}`
45510
+ ]
45511
+ ];
45512
+ const lp = s.lp;
45513
+ if (lp?.read) {
45514
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
45515
+ lines.push([
45516
+ "LP realized",
45517
+ formatUsd(lp.realizedUsd),
45518
+ `withdrawn ${formatUsd(lp.withdrawnUsd)} against ${formatUsd(lp.realizedBasisUsd)} of cost basis, plus ${formatUsd(lp.claimedFeesUsd)} claimed fees`
45519
+ ], [
45520
+ "LP unrealized",
45521
+ formatUsd(lp.unrealizedUsd),
45522
+ `${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` : ""}`
45523
+ ], [
45524
+ "LP vs holding",
45525
+ lp.vsHoldingPositions > 0 ? formatUsd(lp.vsHoldingUsd) : "-",
45526
+ 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"
45527
+ ], [
45528
+ "Total",
45529
+ formatUsd(s.realizedNetUsd + s.unrealizedUsd + lp.realizedUsd + lp.unrealizedUsd),
45530
+ "realized net plus unrealized, tokens and LP"
45531
+ ]);
45532
+ } else if (lp) {
45533
+ lines.push(["LP", "not read", `${terminalText(lp.reason)}; not in the total`], ["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized, tokens only"]);
45534
+ } else {
45535
+ lines.push(["Total", formatUsd(s.realizedNetUsd + s.unrealizedUsd), "realized net plus unrealized"]);
45536
+ }
45537
+ const width = Math.max(...lines.map(([label]) => label.length));
45538
+ const valueWidth = Math.max(...lines.map(([, value]) => value.length));
45539
+ for (const [label, value, note] of lines) {
45540
+ ctx.deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} (${note})
45541
+ `);
45542
+ }
45543
+ if (s.oldestMarkAt !== undefined) {
45544
+ ctx.deps.stdout.write(`Marks as old as ${new Date(s.oldestMarkAt).toISOString()}.
45545
+ `);
45546
+ }
45547
+ if (s.unvalued > 0 || (s.unresolved ?? 0) > 0) {
45548
+ ctx.deps.stdout.write(`${s.unvalued} ${s.unvalued === 1 ? "fill" : "fills"} could not be valued and are not in these figures.
45549
+ `);
45550
+ }
45551
+ if (s.truncated) {
45552
+ ctx.deps.stdout.write(`History is truncated: this covers the most recent ${s.lookback} ${s.lookbackUnit}, not the account's lifetime.
45553
+ `);
45554
+ }
45555
+ if (lp?.read) {
45556
+ if (lp.unvalued > 0) {
45557
+ 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.
45558
+ `);
45559
+ }
45560
+ if (lp.closedOutsideLedger > 0) {
45561
+ 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.
45562
+ `);
45563
+ }
45564
+ if (lp.truncated) {
45565
+ ctx.deps.stdout.write(`LP history is truncated: this covers the most recent ${lp.lookback} LP operations, not the account's lifetime.
45566
+ `);
45567
+ }
45568
+ }
45569
+ }
45570
+ function writeLpPositions(ctx, lp, withBook) {
45571
+ if (!lp?.read || lp.positions.length === 0)
45572
+ return;
45573
+ const headers = ["POSITION", "POOL", "WALLET", "VALUE", "COST BASIS", "UNREALIZED", "VS HOLDING"];
45574
+ if (withBook)
45575
+ headers.push("BOOK");
45576
+ 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";
45577
+ const rows = lp.positions.map((p) => {
45578
+ const row = [
45579
+ shortAddress2(p.position),
45580
+ shortAddress2(p.pool),
45581
+ shortAddress2(p.wallet),
45582
+ value(p),
45583
+ formatUsd(p.costBasisUsd),
45584
+ p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-",
45585
+ p.vsHoldingUsd !== undefined ? formatUsd(p.vsHoldingUsd) : "-"
45586
+ ];
45587
+ if (withBook)
45588
+ row.push(p.book ?? "-");
45589
+ return row;
45590
+ });
45591
+ ctx.deps.stdout.write(`
45592
+ Open LP positions
45593
+ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
45594
+ `);
45595
+ }
45596
+ function writePositions(ctx, positions, withBook) {
45597
+ if (positions.length === 0) {
45598
+ ctx.deps.stdout.write(`
45599
+ No open positions.
45600
+ `);
45601
+ return;
45602
+ }
45603
+ const headers = ["TOKEN", "QUANTITY", "AVG ENTRY", "MARK", "UNREALIZED"];
45604
+ if (withBook)
45605
+ headers.push("BOOK");
45606
+ const rows = positions.map((p) => {
45607
+ const row = [
45608
+ p.symbol?.trim() || shortAddress2(p.mint),
45609
+ formatQuantity(p.quantity),
45610
+ formatPrice(p.avgEntryUsd),
45611
+ p.markPriceUsd !== undefined ? formatPrice(p.markPriceUsd) : "unpriced",
45612
+ p.unrealizedUsd !== undefined ? formatUsd(p.unrealizedUsd) : "-"
45613
+ ];
45614
+ if (withBook)
45615
+ row.push(p.book ?? "-");
45616
+ return row;
45617
+ });
45618
+ ctx.deps.stdout.write(`
45619
+ Open positions
45620
+ ${renderTable(headers, rows.map((row) => row.map(terminalText)))}
45621
+ `);
45622
+ }
45623
+
45624
+ // src/commands/portfolio.ts
45625
+ init_args();
45626
+ init_deps();
45627
+ init_profiles();
45628
+ init_render();
45629
+ init_solana_lite();
45630
+ init_store();
45631
+ init_vault_support();
45632
+ var SOL_MINT = "So11111111111111111111111111111111111111112";
45633
+ var KNOWN_SYMBOLS = {
45634
+ [SOL_MINT]: "SOL",
45635
+ EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
45636
+ "9dXSV8VWuYvGfTzqvkBeoFwH9ihVTybDuWo5VaJPCNDL": "CNDL"
45637
+ };
45638
+ var CHUNK = 100;
45639
+ var TOKEN_READS_IN_FLIGHT = 8;
45640
+ var NO_API_KEY3 = {
45641
+ code: "NO_API_KEY",
45642
+ message: "No API key for this profile.",
45643
+ suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
45644
+ };
45645
+ async function portfolio(args, ctx) {
45646
+ const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
45647
+ if ("error" in parsed)
45648
+ return usage(ctx, parsed.error);
45649
+ if (parsed.positionals.length > 0)
45650
+ return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
45651
+ const { deps } = ctx;
45652
+ const apiKey = await resolveApiKey(deps, ctx.profile);
45653
+ if (!apiKey) {
45654
+ writeLocalFailure(deps, NO_API_KEY3, ctx.json);
45655
+ return 1;
45656
+ }
45657
+ const rpcGiven = parsed.values["--rpc-url"] !== undefined || Boolean(deps.env[RPC_URL_ENV]?.trim());
45658
+ let rpcUrl2;
45659
+ if (rpcGiven) {
45660
+ const resolved = rpcUrlFrom(ctx, parsed);
45661
+ if (typeof resolved !== "string")
45662
+ return usage(ctx, resolved.error);
45663
+ rpcUrl2 = resolved;
45664
+ }
45665
+ const resolvedVault = vaultPathFor(ctx, parsed);
45666
+ if ("error" in resolvedVault)
45667
+ return usage(ctx, resolvedVault.error);
45668
+ return runVaultCommand(ctx, async ({ hold }) => {
45669
+ let vaultEntries;
45670
+ let vaultReason;
45671
+ const raw = rpcUrl2 === undefined ? null : await readVaultRaw(resolvedVault.path);
45672
+ if (rpcUrl2 === undefined) {
45673
+ vaultReason = `not read: vault balances are read only over your own RPC; pass --rpc-url or set ${RPC_URL_ENV}`;
45674
+ } else if (raw === null) {
45675
+ vaultReason = `no vault at ${resolvedVault.path}`;
45676
+ } else {
45677
+ if (!refuseEnvPassphrase(ctx))
45678
+ return 1;
45679
+ if (!requirePromptStreams(ctx, "portfolio"))
45680
+ return 1;
45681
+ const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
45682
+ 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 }));
45683
+ }
45684
+ await printIdentity(ctx);
45685
+ const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
45686
+ if (vaultEntries && vaultEntries.length > 0 && rpcHost !== undefined) {
45687
+ const requests = Math.ceil(vaultEntries.length / CHUNK) + vaultEntries.length * 2;
45688
+ 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.
45689
+ `);
45690
+ }
45691
+ const [candle, vaultRead] = await Promise.all([
45692
+ apiRequest("/api/v1/agent/portfolio", {
45693
+ auth: "key",
45694
+ credentials: { apiKey },
45695
+ apiUrl: ctx.apiUrl,
45696
+ fetch: deps.fetch,
45697
+ env: deps.env
45698
+ }),
45699
+ vaultEntries && rpcUrl2 ? readOwnRpc(vaultEntries.map((entry) => entry.address), rpcUrl2, deps.fetch) : Promise.resolve(undefined)
45700
+ ]);
45701
+ if (!candle.ok) {
45702
+ writeFailure(deps, candle, { apiUrl: ctx.apiUrl, authType: "key" }, ctx.json);
45703
+ return 1;
45704
+ }
45705
+ const fromCandle = candle.body;
45706
+ const prices = { ...fromCandle.prices ?? {} };
45707
+ let priceFailure;
45708
+ if (vaultRead) {
45709
+ const wanted = new Set;
45710
+ for (const wallet2 of vaultRead.byAddress.values()) {
45711
+ if (wallet2.lamports !== null)
45712
+ wanted.add(SOL_MINT);
45713
+ for (const token of wallet2.tokens ?? [])
45714
+ wanted.add(token.mint);
45715
+ }
45716
+ const missing = [...wanted].filter((mint) => prices[mint] === undefined);
45717
+ for (let at = 0;at < missing.length; at += CHUNK) {
45718
+ const priced = await apiRequest("/api/v1/agent/prices", {
45719
+ method: "POST",
45720
+ body: { mints: missing.slice(at, at + CHUNK) },
45721
+ auth: "key",
45722
+ credentials: { apiKey },
45723
+ apiUrl: ctx.apiUrl,
45724
+ fetch: deps.fetch,
45725
+ env: deps.env
45726
+ });
45727
+ if (priced.ok)
45728
+ Object.assign(prices, priced.body.prices ?? {});
45729
+ else
45730
+ priceFailure ??= priced.message;
45731
+ }
45732
+ }
45733
+ if (priceFailure !== undefined) {
45734
+ deps.stderr.write(`Some vault holdings could not be priced: ${terminalText(priceFailure)}. They are shown as unpriced.
45735
+ `);
45736
+ }
45737
+ const lpByWallet = new Map;
45738
+ const lpOf = (address) => {
45739
+ const entry = lpByWallet.get(address) ?? { positions: [], unread: [] };
45740
+ lpByWallet.set(address, entry);
45741
+ return entry;
45742
+ };
45743
+ for (const position of fromCandle.lp?.positions ?? [])
45744
+ lpOf(position.wallet).positions.push(lpRow(position, prices));
45745
+ for (const unread of fromCandle.lp?.unreadable ?? [])
45746
+ lpOf(unread.wallet).unread.push(unread.position);
45747
+ const wallet = (address, read, extra) => {
45748
+ const holdings = read === undefined ? null : valueHoldings(read, prices);
45749
+ const unread = read === undefined ? [] : [...read.lamports === null ? ["sol"] : [], ...read.tokens === null ? ["tokens"] : []];
45750
+ const lp2 = fromCandle.lp ? lpOf(address) : undefined;
45751
+ return {
45752
+ address,
45753
+ ...extra,
45754
+ holdings,
45755
+ ...unread.length > 0 ? { unread } : {},
45756
+ ...lp2 ? { lpPositions: lp2.positions } : {},
45757
+ ...lp2 && lp2.unread.length > 0 ? { lpUnread: lp2.unread } : {},
45758
+ valueUsd: (holdings ?? []).reduce((sum, h) => sum + (h.valueUsd ?? 0), 0) + (lp2?.positions ?? []).reduce((sum, p) => sum + (p.valueUsd ?? 0), 0),
45759
+ unpriced: (holdings ?? []).filter((h) => h.priceUsd === null).length + (lp2?.positions ?? []).filter((p) => p.valueUsd === null).length
45760
+ };
45761
+ };
45762
+ const group = (name, wallets, read, reason) => ({
45763
+ group: name,
45764
+ read,
45765
+ ...reason !== undefined ? { reason } : {},
45766
+ wallets,
45767
+ valueUsd: wallets.reduce((sum, w) => sum + w.valueUsd, 0),
45768
+ unpriced: wallets.reduce((sum, w) => sum + w.unpriced, 0)
45769
+ });
45770
+ const orNull = (read) => read.lamports === null && read.tokens === null ? undefined : read;
45771
+ const groups = [
45772
+ group("vault", (vaultEntries ?? []).map((entry) => {
45773
+ const read = vaultRead?.byAddress.get(entry.address);
45774
+ return wallet(entry.address, read ? orNull(read) : undefined, { label: entry.label, role: entry.role });
45775
+ }), vaultEntries !== undefined, vaultReason),
45776
+ group("tee", (fromCandle.tee ?? []).map((row) => wallet(row.address, orNull(row), {
45777
+ id: row.id,
45778
+ ...row.label ? { label: row.label } : {},
45779
+ active: row.active
45780
+ })), true),
45781
+ group("embedded", (fromCandle.embedded ?? []).map((row) => wallet(row.address, orNull(row), {})), true)
45782
+ ];
45783
+ const unavailable = [...vaultRead?.unavailable ?? [], ...fromCandle.unavailable ?? []];
45784
+ const lpUnread = (fromCandle.lp?.unreadable ?? []).length;
45785
+ const complete = fromCandle.complete !== false && unavailable.length === 0 && lpUnread === 0;
45786
+ const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
45787
+ const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
45788
+ const lp = fromCandle.lp ? {
45789
+ positions: fromCandle.lp.positions.length,
45790
+ unpriced: fromCandle.lp.positions.filter((p) => p.valueUsd === null).length,
45791
+ unreadable: lpUnread,
45792
+ valueUsd: fromCandle.lp.positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
45793
+ } : undefined;
45794
+ if (ctx.json) {
45795
+ writeJson(deps, {
45796
+ ok: true,
45797
+ totalUsd,
45798
+ unpriced,
45799
+ complete,
45800
+ unavailable,
45801
+ ...rpcHost !== undefined ? { rpcHost } : {},
45802
+ ...lp ? { lp } : {},
45803
+ groups
45804
+ });
45805
+ return complete ? 0 : 3;
45806
+ }
45807
+ writeTable(ctx, groups, { totalUsd, unpriced, unavailable: unavailable.length, lp });
45808
+ return complete ? 0 : 3;
45809
+ });
45810
+ }
45811
+ async function readOwnRpc(addresses, rpcUrl2, fetchFn) {
45812
+ const rpc2 = createSolanaRpc(rpcUrl2, fetchFn);
45813
+ const unique = [...new Set(addresses)];
45814
+ const lamports = new Map;
45815
+ for (let at = 0;at < unique.length; at += CHUNK) {
45816
+ const chunk = unique.slice(at, at + CHUNK);
45817
+ try {
45818
+ const accounts = await rpc2.getMultipleAccounts(chunk);
45819
+ for (const [i, address] of chunk.entries())
45820
+ lamports.set(address, (accounts[i]?.lamports ?? 0n).toString());
45821
+ } catch {
45822
+ for (const address of chunk)
45823
+ lamports.set(address, null);
45824
+ }
45825
+ }
45826
+ const tokens = new Map;
45827
+ const failed = new Set;
45828
+ const reads = unique.flatMap((owner) => [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) => ({ owner, programId })));
45829
+ let next = 0;
45830
+ await Promise.all(Array.from({ length: Math.min(TOKEN_READS_IN_FLIGHT, reads.length) }, async () => {
45831
+ while (next < reads.length) {
45832
+ const { owner, programId } = reads[next++];
45833
+ try {
45834
+ const accounts = await rpc2.getTokenAccountsByOwner(owner, programId);
45835
+ const held = tokens.get(owner) ?? new Map;
45836
+ tokens.set(owner, held);
45837
+ const program = programId === TOKEN_PROGRAM_ID ? "token" : "token-2022";
45838
+ for (const account of accounts) {
45839
+ if (!/^\d+$/.test(account.amountRaw) || BigInt(account.amountRaw) === 0n)
45840
+ continue;
45841
+ const key = `${program}:${account.mint}`;
45842
+ const prior = held.get(key);
45843
+ held.set(key, {
45844
+ mint: account.mint,
45845
+ amountRaw: (BigInt(prior?.amountRaw ?? "0") + BigInt(account.amountRaw)).toString(),
45846
+ decimals: account.decimals,
45847
+ program
45848
+ });
45849
+ }
45850
+ } catch {
45851
+ failed.add(owner);
45852
+ }
45853
+ }
45854
+ }));
45855
+ const byAddress = new Map;
45856
+ const unavailable = [];
45857
+ for (const address of unique) {
45858
+ const sol = lamports.get(address) ?? null;
45859
+ const held = failed.has(address) ? null : [...tokens.get(address)?.values() ?? []];
45860
+ byAddress.set(address, { lamports: sol, tokens: held });
45861
+ if (sol === null || held === null)
45862
+ unavailable.push(address);
45863
+ }
45864
+ return { byAddress, unavailable };
45865
+ }
45866
+ function valueHoldings(read, prices) {
45867
+ const raw = [];
45868
+ if (read.lamports !== null && BigInt(read.lamports) > 0n) {
45869
+ raw.push({ mint: SOL_MINT, amountRaw: read.lamports, decimals: 9, program: "native" });
45870
+ }
45871
+ for (const token of read.tokens ?? [])
45872
+ raw.push(token);
45873
+ return raw.map((h) => {
45874
+ const price = prices[h.mint];
45875
+ const priceUsd = price?.priceUsd ?? null;
45876
+ const amount = formatAmount(h.amountRaw, h.decimals);
45877
+ return {
45878
+ ...h,
45879
+ symbol: KNOWN_SYMBOLS[h.mint] ?? price?.symbol ?? null,
45880
+ amount,
45881
+ priceUsd,
45882
+ valueUsd: priceUsd === null ? null : Number(amount) * priceUsd,
45883
+ priceSource: price?.source ?? null
45884
+ };
45885
+ });
45886
+ }
45887
+ function lpRow(position, prices) {
45888
+ return {
45889
+ position: position.position,
45890
+ pool: position.pool,
45891
+ tokens: position.tokens.map((token) => ({
45892
+ ...token,
45893
+ symbol: KNOWN_SYMBOLS[token.mint] ?? token.symbol ?? prices[token.mint]?.symbol ?? null,
45894
+ amount: formatAmount(token.amountRaw, token.decimals),
45895
+ unclaimedFees: formatAmount(token.unclaimedFeesRaw, token.decimals)
45896
+ })),
45897
+ ...position.poolShare !== undefined ? { poolShare: position.poolShare } : {},
45898
+ valueUsd: position.valueUsd,
45899
+ unpriced: position.unpriced
45900
+ };
45901
+ }
45902
+ function writeTable(ctx, groups, totals) {
45903
+ const { deps } = ctx;
45904
+ const rows = [];
45905
+ const lpRows = [];
45906
+ const notes = [];
45907
+ for (const g of groups) {
45908
+ 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);
45909
+ let lpCount = 0;
45910
+ let lpUnread = 0;
45911
+ for (const w of shown) {
45912
+ const name = `${w.label ? `${w.label} ` : ""}(${shortAddress2(w.address)})${w.role === "external" ? " external" : ""}`;
45913
+ const side = (t, amount) => `${amount} ${t.symbol ?? shortAddress2(t.mint)}`;
45914
+ for (const p of [...w.lpPositions ?? []].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1))) {
45915
+ lpCount += 1;
45916
+ lpRows.push([
45917
+ g.group,
45918
+ name,
45919
+ shortAddress2(p.position),
45920
+ shortAddress2(p.pool),
45921
+ p.tokens.map((t) => side(t, t.amount)).join(" + "),
45922
+ p.tokens.map((t) => side(t, t.unclaimedFees)).join(" + "),
45923
+ p.valueUsd === null ? "unpriced" : formatUsd(p.valueUsd)
45924
+ ]);
45925
+ }
45926
+ for (const position of w.lpUnread ?? []) {
45927
+ lpUnread += 1;
45928
+ lpRows.push([g.group, name, shortAddress2(position), "-", "not read", "-", "-"]);
45929
+ }
45930
+ if (w.holdings === null) {
45931
+ rows.push([g.group, name, "-", "not read", "-", "-"]);
45932
+ continue;
45933
+ }
45934
+ const sorted = [...w.holdings].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
45935
+ for (const h of sorted) {
45936
+ rows.push([
45937
+ g.group,
45938
+ name,
45939
+ h.symbol ?? shortAddress2(h.mint),
45940
+ h.amount,
45941
+ h.priceUsd === null ? "unpriced" : formatPrice(h.priceUsd),
45942
+ h.valueUsd === null ? "-" : formatUsd(h.valueUsd)
45943
+ ]);
45944
+ }
45945
+ for (const part of w.unread ?? [])
45946
+ rows.push([g.group, name, part === "sol" ? "SOL" : "tokens", "not read", "-", "-"]);
45947
+ }
45948
+ const empty = g.wallets.length - shown.length;
45949
+ const count = `${g.wallets.length} ${g.wallets.length === 1 ? "wallet" : "wallets"}`;
45950
+ notes.push([
45951
+ g.group,
45952
+ g.read ? formatUsd(g.valueUsd) : "-",
45953
+ 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"
45954
+ ]);
45955
+ }
45956
+ if (rows.length > 0)
45957
+ deps.stdout.write(`
45958
+ ${renderTable(["GROUP", "WALLET", "TOKEN", "AMOUNT", "PRICE", "VALUE"], rows.map((row) => row.map(terminalText)))}
45959
+ `);
45960
+ else
45961
+ deps.stdout.write(`
45962
+ Nothing held in any wallet read.
45963
+ `);
45964
+ if (lpRows.length > 0)
45965
+ deps.stdout.write(`
45966
+ LP positions
45967
+ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEES", "VALUE"], lpRows.map((row) => row.map(terminalText)))}
45968
+ `);
45969
+ notes.push([
45970
+ "total",
45971
+ formatUsd(totals.totalUsd),
45972
+ totals.unpriced > 0 ? `${totals.unpriced} unpriced ${totals.unpriced === 1 ? "holding" : "holdings"} not counted` : "every holding priced"
45973
+ ]);
45974
+ const width = Math.max(...notes.map(([label]) => label.length));
45975
+ const valueWidth = Math.max(...notes.map(([, value]) => value.length));
45976
+ deps.stdout.write(`
45977
+ `);
45978
+ for (const [label, value, note] of notes) {
45979
+ deps.stdout.write(`${label.padEnd(width)} ${value.padStart(valueWidth)} ${note}
45980
+ `);
45981
+ }
45982
+ if (totals.unavailable > 0) {
45983
+ 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.
45984
+ `);
45985
+ }
45986
+ if ((totals.lp?.unreadable ?? 0) > 0) {
45987
+ const n = totals.lp?.unreadable ?? 0;
45988
+ 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.
45989
+ `);
45990
+ }
45991
+ }
45992
+
44621
45993
  // src/commands/profile.ts
44622
45994
  init_args();
44623
45995
  init_profiles();
@@ -45353,7 +46725,7 @@ async function readMints(rpc2, mints) {
45353
46725
  }
45354
46726
  return out;
45355
46727
  }
45356
- function formatAmount(raw, decimals) {
46728
+ function formatAmount2(raw, decimals) {
45357
46729
  if (decimals === undefined)
45358
46730
  return `${raw} raw`;
45359
46731
  const negative = raw < 0n;
@@ -45363,7 +46735,7 @@ function formatAmount(raw, decimals) {
45363
46735
  return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
45364
46736
  }
45365
46737
  function formatSol(lamports) {
45366
- return `${formatAmount(lamports, 9)} SOL`;
46738
+ return `${formatAmount2(lamports, 9)} SOL`;
45367
46739
  }
45368
46740
  function displayLines(input) {
45369
46741
  const { tx, compiled, signers, simulation, mints } = input;
@@ -45386,7 +46758,7 @@ function displayLines(input) {
45386
46758
  }
45387
46759
  for (const token of deltas.tokens.filter((delta) => delta.owner === entry.address)) {
45388
46760
  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}`);
46761
+ 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
46762
  }
45391
46763
  }
45392
46764
  const others = [];
@@ -45401,7 +46773,7 @@ function displayLines(input) {
45401
46773
  if (signerAddresses.has(token.owner) || token.after <= token.before)
45402
46774
  continue;
45403
46775
  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})`);
46776
+ others.push(`${token.owner} receives +${formatAmount2(token.after - token.before, info?.decimals)} of ${token.mint} (account ${token.account})`);
45405
46777
  }
45406
46778
  if (others.length === 0)
45407
46779
  lines.push("others no other account gains a balance in the simulation");
@@ -45689,6 +47061,285 @@ async function signMessage2(args, ctx) {
45689
47061
  init_esm();
45690
47062
  init_args();
45691
47063
  init_deps();
47064
+
47065
+ // src/lp-close.ts
47066
+ init_solana_lite();
47067
+ var DAMM_V2_PROGRAM_ID = "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG";
47068
+ var CLOSE_PROGRAM_ALLOWLIST = new Set([
47069
+ DAMM_V2_PROGRAM_ID,
47070
+ TOKEN_PROGRAM_ID,
47071
+ TOKEN_2022_PROGRAM_ID,
47072
+ ASSOCIATED_TOKEN_PROGRAM_ID,
47073
+ COMPUTE_BUDGET_PROGRAM_ID,
47074
+ SYSTEM_PROGRAM_ID
47075
+ ]);
47076
+ var DAMM_POSITION_CLOSE_UNAVAILABLE = "DAMM_POSITION_CLOSE_UNAVAILABLE";
47077
+ var DAMM_CLOSE_TRANSACTION_REFUSED = "DAMM_CLOSE_TRANSACTION_REFUSED";
47078
+ function isPositionCandidate(account) {
47079
+ return account.programId === TOKEN_2022_PROGRAM_ID && account.amountRaw === "1" && account.decimals === 0;
47080
+ }
47081
+ function parseCloseArtifact(body) {
47082
+ if (!body || typeof body !== "object")
47083
+ return;
47084
+ const { transaction, accountKeys } = body;
47085
+ if (typeof transaction !== "string" || transaction.length === 0)
47086
+ return;
47087
+ if (!Array.isArray(accountKeys) || !accountKeys.every((key) => typeof key === "string" && key.length > 0))
47088
+ return;
47089
+ return { transaction, accountKeys };
47090
+ }
47091
+ var MINT_BASE_SIZE2 = 82;
47092
+ var ACCOUNT_TYPE_OFFSET2 = 165;
47093
+ var ACCOUNT_TYPE_MINT = 1;
47094
+ var MULTISIG_SIZE2 = 355;
47095
+ var TOKEN_ACCOUNT_SIZE2 = 165;
47096
+ var TOKEN_ACCOUNT_STATE_INITIALIZED = 1;
47097
+ var CONTROL_RANGES = [
47098
+ [0, 32, "mint"],
47099
+ [32, 64, "authority"],
47100
+ [72, 108, "delegate"],
47101
+ [108, 109, "state"],
47102
+ [121, 129, "delegated amount"],
47103
+ [129, 165, "close authority"]
47104
+ ];
47105
+ var TOKEN_IX_CLOSE_ACCOUNT = 9;
47106
+ var ATA_IX_CREATE = 0;
47107
+ var ATA_IX_CREATE_IDEMPOTENT = 1;
47108
+ function isTokenProgram(owner) {
47109
+ return owner === TOKEN_PROGRAM_ID || owner === TOKEN_2022_PROGRAM_ID;
47110
+ }
47111
+ function isMintAccount(view) {
47112
+ if (view === null || !isTokenProgram(view.owner))
47113
+ return false;
47114
+ if (view.data.length === MINT_BASE_SIZE2)
47115
+ return true;
47116
+ if (view.data.length === MULTISIG_SIZE2)
47117
+ return false;
47118
+ return view.data.length > ACCOUNT_TYPE_OFFSET2 && view.data[ACCOUNT_TYPE_OFFSET2] === ACCOUNT_TYPE_MINT;
47119
+ }
47120
+ function isUnsigned(tx) {
47121
+ return tx.signatures.every((slot) => slot.every((byte) => byte === 0));
47122
+ }
47123
+ function bytesEqual2(a, b) {
47124
+ return a.length === b.length && a.every((byte, i) => byte === b[i]);
47125
+ }
47126
+ function allZero(bytes) {
47127
+ return bytes.every((byte) => byte === 0);
47128
+ }
47129
+ async function verifyCloseArtifact(input) {
47130
+ const { artifact, rpc: rpc2, tee, vault, nftMint, nftAccount } = input;
47131
+ const refuse2 = (reason) => ({ ok: false, reason });
47132
+ const allowedOwner = (address) => address === tee || address === vault;
47133
+ let tx;
47134
+ try {
47135
+ tx = decodeTransaction(decodeStrictBase64(artifact.transaction));
47136
+ } catch (error) {
47137
+ if (error instanceof TransactionDecodeError)
47138
+ return refuse2(`the close transaction is undecodable: ${error.message}`);
47139
+ return refuse2(`the close transaction could not be read: ${error instanceof Error ? error.message : error}`);
47140
+ }
47141
+ if (tx.message.version !== 0)
47142
+ return refuse2("the close transaction is not a v0 message");
47143
+ if (!isUnsigned(tx))
47144
+ return refuse2("the close transaction already carries a signature; close-build must return it unsigned");
47145
+ if (tx.message.numRequiredSignatures !== 1)
47146
+ return refuse2(`the close transaction requires ${tx.message.numRequiredSignatures} signers; a position close needs only the TEE wallet`);
47147
+ if (tx.message.staticKeys[0] !== tee)
47148
+ return refuse2(`the fee payer is ${tx.message.staticKeys[0]}, not the TEE wallet`);
47149
+ let compiled;
47150
+ try {
47151
+ compiled = await resolveCompiledKeys(tx.message, rpc2);
47152
+ } catch (error) {
47153
+ if (error instanceof LookupTableError)
47154
+ return refuse2(`a lookup table could not be resolved: ${error.message}`);
47155
+ return refuse2(`the lookup tables could not be read: ${error instanceof Error ? error.message : error}`);
47156
+ }
47157
+ if (compiled.keys.length !== artifact.accountKeys.length)
47158
+ return refuse2(`the server listed ${artifact.accountKeys.length} account keys and the transaction resolves to ${compiled.keys.length}`);
47159
+ for (const [i, key] of compiled.keys.entries()) {
47160
+ if (artifact.accountKeys[i] !== key)
47161
+ return refuse2(`account key ${i} is ${key} in the transaction and ${artifact.accountKeys[i]} in the server's ordered array`);
47162
+ }
47163
+ let dammCalls = 0;
47164
+ for (const [i, ix] of tx.message.instructions.entries()) {
47165
+ const program = compiled.keys[ix.programIdIndex];
47166
+ const account = (at) => compiled.keys[ix.accountIndexes[at] ?? -1];
47167
+ if (program === undefined || !CLOSE_PROGRAM_ALLOWLIST.has(program))
47168
+ return refuse2(`instruction ${i} calls ${program ?? "an unknown program"}, which a position close never does`);
47169
+ switch (program) {
47170
+ case DAMM_V2_PROGRAM_ID:
47171
+ dammCalls += 1;
47172
+ break;
47173
+ case COMPUTE_BUDGET_PROGRAM_ID:
47174
+ break;
47175
+ case ASSOCIATED_TOKEN_PROGRAM_ID: {
47176
+ const kind = ix.data.length === 0 ? ATA_IX_CREATE : ix.data.length === 1 ? ix.data[0] : -1;
47177
+ if (kind !== ATA_IX_CREATE && kind !== ATA_IX_CREATE_IDEMPOTENT)
47178
+ return refuse2(`instruction ${i} is an Associated Token instruction other than Create, which a close never needs`);
47179
+ if (account(0) !== tee || !allowedOwner(account(2) ?? ""))
47180
+ return refuse2(`instruction ${i} creates a token account for ${account(2) ?? "?"}, not the TEE wallet or its vault`);
47181
+ break;
47182
+ }
47183
+ case TOKEN_PROGRAM_ID:
47184
+ case TOKEN_2022_PROGRAM_ID: {
47185
+ if (ix.data[0] !== TOKEN_IX_CLOSE_ACCOUNT || ix.data.length !== 1)
47186
+ return refuse2(`instruction ${i} is a token instruction other than CloseAccount, which a close never issues at the top level`);
47187
+ if (account(1) !== tee || account(2) !== tee)
47188
+ return refuse2(`instruction ${i} closes a token account to ${account(1) ?? "?"}, not the TEE wallet`);
47189
+ break;
47190
+ }
47191
+ default:
47192
+ return refuse2(`instruction ${i} is a top-level System instruction, which a close never issues`);
47193
+ }
47194
+ }
47195
+ if (dammCalls === 0)
47196
+ return refuse2("the transaction calls no DAMM v2 instruction, so it cannot close a position");
47197
+ let simulation;
47198
+ try {
47199
+ simulation = await simulateWithSnapshots(rpc2, artifact.transaction, compiled);
47200
+ } catch (error) {
47201
+ return refuse2(`the simulation could not be run: ${error instanceof Error ? error.message : error}`);
47202
+ }
47203
+ if (simulation.result.err !== null && simulation.result.err !== undefined)
47204
+ return refuse2(`the simulation failed: ${JSON.stringify(simulation.result.err)}`);
47205
+ const deltas = computeDeltas(simulation.snapshots);
47206
+ const postState = new Map(simulation.snapshots.map((snapshot) => [snapshot.address, snapshot.after]));
47207
+ for (const token of deltas.tokens) {
47208
+ if (token.after > token.before && !allowedOwner(token.owner))
47209
+ return refuse2(`${token.owner} would receive ${token.after - token.before} raw of ${token.mint} in account ${token.account}`);
47210
+ }
47211
+ for (const sol of deltas.sol) {
47212
+ if (sol.after <= sol.before)
47213
+ continue;
47214
+ if (allowedOwner(sol.address))
47215
+ continue;
47216
+ const balance = tokenBalanceOf(postState.get(sol.address) ?? null);
47217
+ if (balance !== undefined && allowedOwner(balance.owner))
47218
+ continue;
47219
+ return refuse2(`${sol.address} would receive ${sol.after - sol.before} lamports`);
47220
+ }
47221
+ const nftSnapshot = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
47222
+ if (nftSnapshot === undefined)
47223
+ return refuse2(`the position NFT account ${nftAccount} is not written by this transaction`);
47224
+ const nftAfter = tokenBalanceOf(nftSnapshot.after);
47225
+ if (nftAfter !== undefined && nftAfter.amount !== 0n)
47226
+ return refuse2(`the position NFT account ${nftAccount} still holds ${nftAfter.amount} after the simulation`);
47227
+ for (const { address, before, after } of simulation.snapshots) {
47228
+ if (allowedOwner(address)) {
47229
+ if (after === null || after.owner !== SYSTEM_PROGRAM_ID || after.data.length !== 0)
47230
+ return refuse2(`${address === tee ? "the TEE wallet" : "the vault"} would no longer be a plain System account after this transaction`);
47231
+ }
47232
+ if (after === null)
47233
+ continue;
47234
+ if (before !== null && after.owner !== before.owner)
47235
+ return refuse2(`writable account ${address} would change owner from ${before.owner} to ${after.owner}`);
47236
+ if (before !== null) {
47237
+ const controlled = tokenBalanceOf(before);
47238
+ if (controlled !== undefined && allowedOwner(controlled.owner)) {
47239
+ if (after.data.length < TOKEN_ACCOUNT_SIZE2)
47240
+ return refuse2(`token account ${address} would no longer decode as a token account`);
47241
+ for (const [start, end, what] of CONTROL_RANGES) {
47242
+ if (!bytesEqual2(before.data.subarray(start, end), after.data.subarray(start, end)))
47243
+ return refuse2(`token account ${address} would change its ${what}, which a close never does`);
47244
+ }
47245
+ continue;
47246
+ }
47247
+ }
47248
+ const created = before === null ? tokenBalanceOf(after) : undefined;
47249
+ if (created !== undefined) {
47250
+ if (!allowedOwner(created.owner))
47251
+ return refuse2(`created token account ${address} would be controlled by ${created.owner}`);
47252
+ 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)))
47253
+ return refuse2(`created token account ${address} would start with a delegate, a close authority, or a frozen state`);
47254
+ }
47255
+ }
47256
+ const infos = new Map;
47257
+ const infoOf = async (address) => {
47258
+ if (!infos.has(address))
47259
+ infos.set(address, await rpc2.getAccountInfo(address));
47260
+ return infos.get(address) ?? null;
47261
+ };
47262
+ const canonical = new Set;
47263
+ for (const key of compiled.keys) {
47264
+ const view = await infoOf(key);
47265
+ if (!isMintAccount(view) || view === null)
47266
+ continue;
47267
+ const mint = decodePubkey(key);
47268
+ const program = decodePubkey(view.owner);
47269
+ canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(tee), mint, program)));
47270
+ canonical.add(encodePubkey(associatedTokenAddress(decodePubkey(vault), mint, program)));
47271
+ }
47272
+ for (const [i, key] of compiled.keys.entries()) {
47273
+ if (!compiled.isWritable[i])
47274
+ continue;
47275
+ const view = await infoOf(key);
47276
+ if (view === null) {
47277
+ if (canonical.has(key))
47278
+ continue;
47279
+ return refuse2(`writable account ${key} does not exist and is not a TEE or vault associated token account for a mint this transaction names`);
47280
+ }
47281
+ if (isTokenProgram(view.owner)) {
47282
+ if (key === nftMint)
47283
+ continue;
47284
+ const balance = tokenBalanceOf(view);
47285
+ if (balance === undefined)
47286
+ return refuse2(`writable account ${key} is owned by a token program but is not a token account`);
47287
+ if (allowedOwner(balance.owner) || key === nftAccount)
47288
+ continue;
47289
+ const authority = await infoOf(balance.owner);
47290
+ if (authority !== null && authority.owner === DAMM_V2_PROGRAM_ID)
47291
+ continue;
47292
+ return refuse2(`writable token account ${key} is controlled by ${balance.owner}, which is neither this wallet, its vault, nor a DAMM v2 pool`);
47293
+ }
47294
+ if (view.owner === SYSTEM_PROGRAM_ID) {
47295
+ if (allowedOwner(key))
47296
+ continue;
47297
+ return refuse2(`writable system account ${key} is neither the TEE wallet nor its vault`);
47298
+ }
47299
+ if (view.owner !== DAMM_V2_PROGRAM_ID)
47300
+ return refuse2(`writable account ${key} is owned by ${view.owner}, not DAMM v2`);
47301
+ }
47302
+ return { ok: true, tx, compiled, simulation };
47303
+ }
47304
+ function formatSol2(lamports) {
47305
+ const negative = lamports < 0n;
47306
+ const abs = negative ? -lamports : lamports;
47307
+ const whole = abs / 1000000000n;
47308
+ const frac = (abs % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
47309
+ return `${negative ? "-" : ""}${whole}${frac ? `.${frac}` : ""} SOL`;
47310
+ }
47311
+ function describeClose(input) {
47312
+ const { verdict, tee, vault, nftMint, nftAccount } = input;
47313
+ const { tx, compiled, simulation } = verdict;
47314
+ const lines = [];
47315
+ 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}`);
47316
+ lines.push(`fee payer ${compiled.keys[0]} (this TEE wallet)`);
47317
+ lines.push(`position NFT mint ${nftMint}, account ${nftAccount}`);
47318
+ const programs = [...new Set(tx.message.instructions.map((ix) => compiled.keys[ix.programIdIndex] ?? "?"))];
47319
+ for (const program of programs)
47320
+ lines.push(`program ${program === DAMM_V2_PROGRAM_ID ? `Meteora DAMM v2 (${program})` : programNameOf(program)}`);
47321
+ const deltas = computeDeltas(simulation.snapshots);
47322
+ const who = (address) => address === tee ? "wallet" : address === vault ? "vault" : address;
47323
+ for (const address of [tee, vault]) {
47324
+ const sol = deltas.sol.find((delta) => delta.address === address);
47325
+ if (sol && sol.after !== sol.before)
47326
+ lines.push(`${who(address).padEnd(11)} ${formatSol2(sol.before)} -> ${formatSol2(sol.after)} (${sol.after >= sol.before ? "+" : ""}${formatSol2(sol.after - sol.before)})`);
47327
+ for (const token of deltas.tokens.filter((delta) => delta.owner === address && delta.after !== delta.before)) {
47328
+ 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)" : ""}`);
47329
+ }
47330
+ }
47331
+ const nft = simulation.snapshots.find((snapshot) => snapshot.address === nftAccount);
47332
+ if (nft && nft.after === null)
47333
+ lines.push(`position NFT account ${nftAccount} is closed by this transaction`);
47334
+ const others = deltas.tokens.filter((token) => token.owner !== tee && token.owner !== vault && token.after !== token.before);
47335
+ for (const token of others)
47336
+ lines.push(`${token.owner === undefined ? "?" : "pool"} ${token.mint}: ${token.before} -> ${token.after} raw in ${token.account} (authority ${token.owner})`);
47337
+ lines.push("checked ordered keys, programs, simulation, positive deltas, control, writable accounts: all passed");
47338
+ lines.push("note the simulation is evidence, not a guarantee: a program can behave differently once signed");
47339
+ return lines;
47340
+ }
47341
+
47342
+ // src/commands/tee.ts
45692
47343
  init_profiles();
45693
47344
  init_render();
45694
47345
  init_solana_lite();
@@ -48994,6 +50645,11 @@ init_deps();
48994
50645
  init_profiles();
48995
50646
  init_render();
48996
50647
  init_secret_store();
50648
+ function trustedCell(row) {
50649
+ if (row.revokedAt)
50650
+ return "-";
50651
+ return row.addedVia === "session" || typeof row.trustedAt === "number" ? "yes" : "no";
50652
+ }
48997
50653
  var TEE_PROFILES = new Set(["ember-tee", "ember-hot"]);
48998
50654
  function isTeeRow(row) {
48999
50655
  return row.profile !== undefined && TEE_PROFILES.has(row.profile);
@@ -49044,6 +50700,9 @@ var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade
49044
50700
  var TEE_HINT = `A wallet marked tee is a TEE trading wallet: pass its id, address or label to candle swap --wallet.
49045
50701
  ` + `This account's embedded wallet, shown above, can pay for a token trade too.
49046
50702
  `;
50703
+ var UNTRUSTED_HINT = `A wallet marked Trusted no was linked by an API key: agents can move funds into it only once you mark it.
50704
+ ` + `Run: candle wallets trust <label|address|prefix*> (the device token, never an API key)
50705
+ `;
49047
50706
  var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
49048
50707
  `;
49049
50708
  async function probeSignerStates(rows, store) {
@@ -49150,24 +50809,28 @@ Linked wallets (${linkedRows.length}):
49150
50809
  `);
49151
50810
  } else {
49152
50811
  const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
49153
- deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
50812
+ deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Trusted", "Signer"], linkedRows.map((wallet, index) => [
49154
50813
  wallet._id,
49155
50814
  wallet.chain,
49156
50815
  wallet.address,
49157
50816
  wallet.label ?? "-",
49158
50817
  isTeeRow(wallet) ? "tee" : "linked",
49159
50818
  wallet.revokedAt ? "yes" : "no",
50819
+ trustedCell(wallet),
49160
50820
  cells[index] ?? "-"
49161
50821
  ]))}
49162
50822
  `);
49163
50823
  const anyNone = cells.includes("none");
49164
50824
  const anyStale = cells.includes("stale");
49165
50825
  const anyTee = linkedRows.some(isTeeRow);
49166
- if (anyNone || anyStale || anyTee)
50826
+ const anyUntrusted = linkedRows.some((row) => trustedCell(row) === "no");
50827
+ if (anyNone || anyStale || anyTee || anyUntrusted)
49167
50828
  deps.stdout.write(`
49168
50829
  `);
49169
50830
  if (anyTee)
49170
50831
  deps.stdout.write(TEE_HINT);
50832
+ if (anyUntrusted)
50833
+ deps.stdout.write(UNTRUSTED_HINT);
49171
50834
  if (anyNone)
49172
50835
  deps.stdout.write(NONE_HINT);
49173
50836
  if (anyStale)
@@ -49429,8 +51092,8 @@ function readDisableOutcome(body) {
49429
51092
  var MIN_PASSPHRASE_LENGTH = 12;
49430
51093
  var USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
49431
51094
  var RPC_URL_ENV2 = "CANDLE_SOLANA_RPC_URL";
49432
- var CONFIRM_POLL_MS2 = 2000;
49433
- var CONFIRM_MAX_POLLS2 = 45;
51095
+ var CONFIRM_POLL_MS3 = 2000;
51096
+ var CONFIRM_MAX_POLLS3 = 45;
49434
51097
  function refuseEnvPassphrase2(ctx) {
49435
51098
  if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
49436
51099
  return true;
@@ -49441,7 +51104,7 @@ function refuseEnvPassphrase2(ctx) {
49441
51104
  }, ctx.json);
49442
51105
  return false;
49443
51106
  }
49444
- function usage3(ctx, line) {
51107
+ function usage4(ctx, line) {
49445
51108
  writeUsageFailure(ctx.deps, line, ctx.json);
49446
51109
  return 2;
49447
51110
  }
@@ -49661,7 +51324,7 @@ async function openExistingTeeStore(ctx, parsed) {
49661
51324
  const { deps, json } = ctx;
49662
51325
  const found = await readTeeStore(ctx, parsed);
49663
51326
  if ("usage" in found)
49664
- return { ok: false, code: usage3(ctx, found.usage) };
51327
+ return { ok: false, code: usage4(ctx, found.usage) };
49665
51328
  const { path } = found;
49666
51329
  if ("error" in found) {
49667
51330
  writeLocalFailure(deps, {
@@ -49758,12 +51421,12 @@ async function teeNew(args, ctx) {
49758
51421
  return 1;
49759
51422
  const parsed = parseArgs(args, { valueFlags: ["--label", "--keystore"], pathFlags: ["--keystore"] });
49760
51423
  if ("error" in parsed)
49761
- return usage3(ctx, parsed.error);
51424
+ return usage4(ctx, parsed.error);
49762
51425
  if (parsed.positionals.length > 0)
49763
- return usage3(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
51426
+ return usage4(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
49764
51427
  const found = await readTeeStore(ctx, parsed);
49765
51428
  if ("usage" in found)
49766
- return usage3(ctx, found.usage);
51429
+ return usage4(ctx, found.usage);
49767
51430
  const { path } = found;
49768
51431
  if ("error" in found) {
49769
51432
  writeLocalFailure(deps, {
@@ -49840,15 +51503,15 @@ async function teeEnable(args, ctx) {
49840
51503
  pathFlags: ["--keystore"]
49841
51504
  });
49842
51505
  if ("error" in parsed)
49843
- return usage3(ctx, parsed.error);
51506
+ return usage4(ctx, parsed.error);
49844
51507
  const [address, extra] = parsed.positionals;
49845
51508
  if (!address || extra !== undefined) {
49846
- return usage3(ctx, "Usage: candle tee enable <address> --vault <address> | --vault-key <label>");
51509
+ return usage4(ctx, "Usage: candle tee enable <address> --vault <address> | --vault-key <label>");
49847
51510
  }
49848
51511
  const vaultFlag = parsed.values["--vault"];
49849
51512
  const vaultKey = parsed.values["--vault-key"];
49850
51513
  if (vaultFlag !== undefined && vaultKey !== undefined) {
49851
- return usage3(ctx, "Use either --vault or --vault-key, not both.");
51514
+ return usage4(ctx, "Use either --vault or --vault-key, not both.");
49852
51515
  }
49853
51516
  let vault = vaultFlag;
49854
51517
  if (vaultKey !== undefined) {
@@ -49858,7 +51521,7 @@ async function teeEnable(args, ctx) {
49858
51521
  const { isVaultError: isVaultError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
49859
51522
  const resolvedVault = vaultPathFor2(ctx, parsed);
49860
51523
  if ("error" in resolvedVault)
49861
- return usage3(ctx, resolvedVault.error);
51524
+ return usage4(ctx, resolvedVault.error);
49862
51525
  const vaultPath = resolvedVault.path;
49863
51526
  const raw = await readVaultRaw2(vaultPath);
49864
51527
  if (raw === null) {
@@ -49896,11 +51559,11 @@ async function teeEnable(args, ctx) {
49896
51559
  }
49897
51560
  }
49898
51561
  if (!vault)
49899
- return usage3(ctx, "--vault <address> or --vault-key <label> is required: the destination every sweep sends to.");
51562
+ return usage4(ctx, "--vault <address> or --vault-key <label> is required: the destination every sweep sends to.");
49900
51563
  if (!isSolanaAddress(vault))
49901
- return usage3(ctx, "--vault is not a valid Solana address.");
51564
+ return usage4(ctx, "--vault is not a valid Solana address.");
49902
51565
  if (vault === address)
49903
- return usage3(ctx, "--vault must be a different address from the TEE wallet.");
51566
+ return usage4(ctx, "--vault must be a different address from the TEE wallet.");
49904
51567
  const vaultOwned = await addressOwnedByVault(ctx, address);
49905
51568
  if (vaultOwned === "usage")
49906
51569
  return 2;
@@ -50037,20 +51700,20 @@ async function teeFund(args, ctx) {
50037
51700
  return 1;
50038
51701
  const parsed = parseArgs(args, { valueFlags: ["--amount", "--asset", "--keystore"], pathFlags: ["--keystore"] });
50039
51702
  if ("error" in parsed)
50040
- return usage3(ctx, parsed.error);
51703
+ return usage4(ctx, parsed.error);
50041
51704
  const [address, extra] = parsed.positionals;
50042
51705
  if (!address || extra !== undefined)
50043
- return usage3(ctx, "Usage: candle tee fund <address> --amount <n> [--asset SOL|USDC]");
51706
+ return usage4(ctx, "Usage: candle tee fund <address> --amount <n> [--asset SOL|USDC]");
50044
51707
  const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
50045
51708
  if (asset !== "SOL" && asset !== "USDC")
50046
- return usage3(ctx, "--asset must be SOL or USDC.");
51709
+ return usage4(ctx, "--asset must be SOL or USDC.");
50047
51710
  const amount = parsed.values["--amount"];
50048
51711
  if (!amount)
50049
- return usage3(ctx, "--amount <n> is required.");
51712
+ return usage4(ctx, "--amount <n> is required.");
50050
51713
  const decimals = asset === "SOL" ? 9 : 6;
50051
51714
  const raw = decimalToRaw(amount, decimals);
50052
51715
  if (raw === null || raw === 0n)
50053
- return usage3(ctx, `--amount must be a positive decimal with at most ${decimals} decimal places.`);
51716
+ return usage4(ctx, `--amount must be a positive decimal with at most ${decimals} decimal places.`);
50054
51717
  const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
50055
51718
  if (!resolved.ok)
50056
51719
  return resolved.code;
@@ -50117,10 +51780,10 @@ async function teeStatus(args, ctx) {
50117
51780
  return 1;
50118
51781
  const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
50119
51782
  if ("error" in parsed)
50120
- return usage3(ctx, parsed.error);
51783
+ return usage4(ctx, parsed.error);
50121
51784
  const [address, extra] = parsed.positionals;
50122
51785
  if (!address || extra !== undefined)
50123
- return usage3(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
51786
+ return usage4(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
50124
51787
  const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
50125
51788
  if (!resolved.ok)
50126
51789
  return resolved.code;
@@ -50162,7 +51825,7 @@ async function teeStatus(args, ctx) {
50162
51825
  if (rpcUrl2) {
50163
51826
  const checked = rpcUrlFrom2(ctx, parsed);
50164
51827
  if (typeof checked !== "string")
50165
- return usage3(ctx, checked.error);
51828
+ return usage4(ctx, checked.error);
50166
51829
  const rpc2 = createSolanaRpc(checked, deps.fetch);
50167
51830
  try {
50168
51831
  const lamports = await rpc2.getBalance(address);
@@ -50243,10 +51906,10 @@ async function teeDisable(args, ctx) {
50243
51906
  return 1;
50244
51907
  const parsed = parseArgs(args, { valueFlags: ["--keystore"], pathFlags: ["--keystore"] });
50245
51908
  if ("error" in parsed)
50246
- return usage3(ctx, parsed.error);
51909
+ return usage4(ctx, parsed.error);
50247
51910
  const [address, extra] = parsed.positionals;
50248
51911
  if (!address || extra !== undefined)
50249
- return usage3(ctx, "Usage: candle tee disable <address>");
51912
+ return usage4(ctx, "Usage: candle tee disable <address>");
50250
51913
  await printIdentity(ctx);
50251
51914
  const openedActive = await openActiveTee(ctx, parsed, address, "read");
50252
51915
  if (!openedActive.ok)
@@ -50341,6 +52004,9 @@ function refusalState(raw) {
50341
52004
  async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions, pending, recordPending, clearPending) {
50342
52005
  const blockhash = await rpc2.getLatestBlockhash();
50343
52006
  const message = compileLegacyMessage({ feePayer, recentBlockhash: blockhash, instructions });
52007
+ return broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending);
52008
+ }
52009
+ async function broadcastMessage(rpc2, deps, secret, message, blockhash, pending, recordPending, clearPending) {
50344
52010
  const signatureBytes = signMessage(message, secret);
50345
52011
  const signature = base58.encode(signatureBytes);
50346
52012
  const wire = serializeSignedTransaction(message, signatureBytes);
@@ -50366,7 +52032,7 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
50366
52032
  error: `send did not answer cleanly (${error instanceof Error ? error.message : error}); it may still land`
50367
52033
  };
50368
52034
  }
50369
- for (let i = 0;i < CONFIRM_MAX_POLLS2; i++) {
52035
+ for (let i = 0;i < CONFIRM_MAX_POLLS3; i++) {
50370
52036
  let status;
50371
52037
  try {
50372
52038
  status = await rpc2.getSignatureStatus(signature);
@@ -50388,12 +52054,12 @@ async function broadcastAndFinalize2(rpc2, deps, secret, feePayer, instructions,
50388
52054
  error: `transaction ${signature} failed on chain: ${JSON.stringify(observed.err)}`
50389
52055
  };
50390
52056
  }
50391
- await deps.sleep(CONFIRM_POLL_MS2);
52057
+ await deps.sleep(CONFIRM_POLL_MS3);
50392
52058
  }
50393
52059
  return {
50394
52060
  status: "uncertain",
50395
52061
  signature,
50396
- error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS2 * CONFIRM_POLL_MS2 / 1000}s; it may still land${echoNote}`
52062
+ error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS3 * CONFIRM_POLL_MS3 / 1000}s; it may still land${echoNote}`
50397
52063
  };
50398
52064
  }
50399
52065
  async function teeSweep(args, ctx) {
@@ -50406,13 +52072,13 @@ async function teeSweep(args, ctx) {
50406
52072
  pathFlags: ["--keystore"]
50407
52073
  });
50408
52074
  if ("error" in parsed)
50409
- return usage3(ctx, parsed.error);
52075
+ return usage4(ctx, parsed.error);
50410
52076
  const [address, extra] = parsed.positionals;
50411
52077
  if (!address || extra !== undefined)
50412
- return usage3(ctx, "Usage: candle tee sweep <address> --rpc-url <url> [--emergency]");
52078
+ return usage4(ctx, "Usage: candle tee sweep <address> --rpc-url <url> [--emergency]");
50413
52079
  const rpcUrl2 = rpcUrlFrom2(ctx, parsed);
50414
52080
  if (typeof rpcUrl2 !== "string")
50415
- return usage3(ctx, rpcUrl2.error);
52081
+ return usage4(ctx, rpcUrl2.error);
50416
52082
  const emergency = parsed.booleans.has("--emergency");
50417
52083
  const openedActive = await openActiveTee(ctx, parsed, address, "sign");
50418
52084
  if (!openedActive.ok)
@@ -50615,6 +52281,116 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
50615
52281
  pendingStill.push(p);
50616
52282
  signingBlocked = true;
50617
52283
  }
52284
+ const candidateAccounts = new Set;
52285
+ const lpCloses = [];
52286
+ {
52287
+ let token2022Accounts = [];
52288
+ try {
52289
+ token2022Accounts = await rpc2.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID);
52290
+ } catch (error) {
52291
+ residuals.push({
52292
+ kind: "inventory",
52293
+ detail: `could not list Token-2022 accounts for position discovery: ${error instanceof Error ? error.message : error}`
52294
+ });
52295
+ }
52296
+ const candidates = token2022Accounts.filter(isPositionCandidate);
52297
+ for (const acct of candidates)
52298
+ candidateAccounts.add(acct.pubkey);
52299
+ const leftover = (kind, acct, detail) => residuals.push({ kind, detail, mint: acct.mint, account: acct.pubkey, amountRaw: acct.amountRaw });
52300
+ if (!emergency && candidates.length > 0) {
52301
+ if (!json)
52302
+ deps.stdout.write(` ${candidates.length} position NFT candidate(s) (Token-2022, amount 1, decimals 0)
52303
+ `);
52304
+ const verified = [];
52305
+ for (const acct of candidates) {
52306
+ if (signingBlocked)
52307
+ break;
52308
+ if (!apiKey || !entry.linkedWalletId) {
52309
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "no bound API key is available to build the close; the NFT was left in place");
52310
+ continue;
52311
+ }
52312
+ const result = await apiRequest(`/api/v1/agent/lp/positions/${encodeURIComponent(acct.mint)}/close-build`, {
52313
+ method: "POST",
52314
+ auth: "key",
52315
+ credentials: { apiKey },
52316
+ apiUrl,
52317
+ fetch: deps.fetch,
52318
+ env: deps.env,
52319
+ body: {}
52320
+ });
52321
+ if (!result.ok) {
52322
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `close-build ${result.code ?? `HTTP ${result.status}`}: ${result.message}; the NFT was left in place`);
52323
+ continue;
52324
+ }
52325
+ const artifact = parseCloseArtifact(result.body);
52326
+ if (artifact === undefined) {
52327
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "close-build answered without a transaction and an ordered account key array; the NFT was left in place");
52328
+ continue;
52329
+ }
52330
+ const verdict = await verifyCloseArtifact({
52331
+ artifact,
52332
+ rpc: rpc2,
52333
+ tee: address,
52334
+ vault,
52335
+ nftMint: acct.mint,
52336
+ nftAccount: acct.pubkey
52337
+ });
52338
+ if (!verdict.ok) {
52339
+ leftover(DAMM_CLOSE_TRANSACTION_REFUSED, acct, `${verdict.reason}; refused before signing`);
52340
+ continue;
52341
+ }
52342
+ verified.push({ acct, verdict });
52343
+ }
52344
+ for (const { acct, verdict } of verified) {
52345
+ if (signingBlocked) {
52346
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "not signed: an earlier transaction is still in flight");
52347
+ continue;
52348
+ }
52349
+ let blockhashValid;
52350
+ try {
52351
+ blockhashValid = await rpc2.isBlockhashValid(verdict.tx.message.recentBlockhash, "confirmed");
52352
+ } catch (error) {
52353
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, `could not check the close build's blockhash: ${error instanceof Error ? error.message : error}; not signed`);
52354
+ continue;
52355
+ }
52356
+ if (!blockhashValid) {
52357
+ leftover(DAMM_POSITION_CLOSE_UNAVAILABLE, acct, "the close build's blockhash has expired; not signed, re-run the sweep for a fresh close");
52358
+ continue;
52359
+ }
52360
+ const display = describeClose({ verdict, tee: address, vault, nftMint: acct.mint, nftAccount: acct.pubkey });
52361
+ lpCloses.push({ mint: acct.mint, account: acct.pubkey, display });
52362
+ if (!json) {
52363
+ deps.stdout.write(` Close for position ${acct.mint} (verified, unsigned; about to sign):
52364
+ `);
52365
+ for (const line of display)
52366
+ deps.stdout.write(` ${line}
52367
+ `);
52368
+ }
52369
+ const pending = {
52370
+ kind: "lp-close",
52371
+ mint: acct.mint,
52372
+ account: acct.pubkey,
52373
+ amountRaw: acct.amountRaw
52374
+ };
52375
+ const outcome = await broadcastMessage(rpc2, deps, secret, verdict.tx.message.bytes, verdict.tx.message.recentBlockhash, pending, recordPending, clearPending);
52376
+ if (outcome.status !== "finalized") {
52377
+ if (!settle2(outcome, pending, "lp-close"))
52378
+ signingBlocked = true;
52379
+ continue;
52380
+ }
52381
+ await retainReceipt({
52382
+ kind: "lp-close",
52383
+ mint: acct.mint,
52384
+ amountRaw: acct.amountRaw,
52385
+ signature: outcome.signature,
52386
+ finalizedAt: new Date(deps.now()).toISOString()
52387
+ });
52388
+ if (!json)
52389
+ deps.stdout.write(` closed LP position ${acct.mint} and withdrew its liquidity: ${outcome.signature}
52390
+ `);
52391
+ }
52392
+ }
52393
+ }
50618
52394
  const tokenAccounts = [];
50619
52395
  for (const programId of [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
50620
52396
  try {
@@ -50631,6 +52407,13 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
50631
52407
  if (signingBlocked)
50632
52408
  break;
50633
52409
  const token2022 = acct.programId === TOKEN_2022_PROGRAM_ID;
52410
+ if (candidateAccounts.has(acct.pubkey)) {
52411
+ if (!emergency)
52412
+ continue;
52413
+ if (!json)
52414
+ deps.stdout.write(` ${acct.mint}: position NFT moved as-is to the vault (emergency; unwind it from a fresh TEE wallet)
52415
+ `);
52416
+ }
50634
52417
  if (acct.state !== "initialized") {
50635
52418
  residuals.push({
50636
52419
  kind: token2022 && acct.state === "frozen" ? "TOKEN_2022_FROZEN" : "frozen-or-uninitialized",
@@ -50885,7 +52668,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
50885
52668
  signature: p.signature
50886
52669
  })),
50887
52670
  inventory,
50888
- recordedOnServer
52671
+ recordedOnServer,
52672
+ ...lpCloses.length > 0 ? { lpCloses } : {}
50889
52673
  })}
50890
52674
  `);
50891
52675
  return finalState === "swept" ? 0 : 3;
@@ -50968,8 +52752,7 @@ function launchWarning(keyPrefix, toKey, rows) {
50968
52752
  return `Warning: key ${keyPrefix} lacks launch:write; ${launchers.join(", ")} ${launchers.length === 1 ? "has" : "have"} allowLaunch but cannot launch under it.`;
50969
52753
  }
50970
52754
  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;
52755
+ function rebindFailureDetails(result, context) {
50973
52756
  let code = result.code;
50974
52757
  let message = result.message;
50975
52758
  let suggestion;
@@ -50989,31 +52772,61 @@ function writeRebindFailure(ctx, result, context) {
50989
52772
  } else if (result.code === "REBIND_STALE") {
50990
52773
  suggestion = "Run the command again; the preview will show the current binding.";
50991
52774
  }
52775
+ return {
52776
+ code: code ?? `HTTP ${result.status}`,
52777
+ message,
52778
+ ...suggestion !== undefined ? { suggestion } : {},
52779
+ status: result.status
52780
+ };
52781
+ }
52782
+ function writeRebindFailure(ctx, result, context) {
52783
+ const { deps, apiUrl, json } = ctx;
52784
+ const { code, message, suggestion } = rebindFailureDetails(result, context);
50992
52785
  const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
50993
52786
  if (json) {
50994
52787
  deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
50995
52788
  `);
50996
52789
  } else {
50997
- deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
52790
+ deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
50998
52791
  `);
50999
52792
  }
51000
52793
  return 1;
51001
52794
  }
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, {
52795
+ async function listAccountKeys(ctx, deviceToken) {
52796
+ const { deps, apiUrl } = ctx;
52797
+ return apiRequest(KEYS_PATH2, {
51007
52798
  auth: "device",
51008
52799
  credentials: { deviceToken },
51009
52800
  apiUrl,
51010
52801
  fetch: deps.fetch,
51011
52802
  env: deps.env
51012
52803
  });
51013
- if (!result.ok) {
51014
- return { ok: false, code: writeRebindFailure(ctx, result, {}) };
52804
+ }
52805
+ async function postRebind(ctx, deviceToken, body) {
52806
+ const { deps, apiUrl } = ctx;
52807
+ return apiRequest(REBIND_PATH, {
52808
+ method: "POST",
52809
+ body,
52810
+ auth: "device",
52811
+ credentials: { deviceToken },
52812
+ apiUrl,
52813
+ fetch: deps.fetch,
52814
+ env: deps.env
52815
+ });
52816
+ }
52817
+ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
52818
+ const { deps, json } = ctx;
52819
+ if (KEY_PREFIX_RE.test(raw))
52820
+ return { ok: true, keyPrefix: raw };
52821
+ let listed = opts.keys;
52822
+ if (listed === undefined) {
52823
+ const result = await listAccountKeys(ctx, deviceToken);
52824
+ if (!result.ok) {
52825
+ return { ok: false, code: writeRebindFailure(ctx, result, {}) };
52826
+ }
52827
+ listed = result.body?.keys ?? [];
51015
52828
  }
51016
- const keys = (result.body?.keys ?? []).filter((key) => !key.revokedAt);
52829
+ const keys = listed.filter((key) => !key.revokedAt);
51017
52830
  const matches = keys.filter((key) => key.label === raw);
51018
52831
  if (matches.length === 1)
51019
52832
  return { ok: true, keyPrefix: matches[0].keyPrefix };
@@ -51071,15 +52884,7 @@ async function teeRebind(args, ctx) {
51071
52884
  const target = await resolveTargetKey(ctx, deviceToken, toKey);
51072
52885
  if (!target.ok)
51073
52886
  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
- });
52887
+ const call = (body) => postRebind(ctx, deviceToken, body);
51083
52888
  const preview = await call({
51084
52889
  dryRun: true,
51085
52890
  toKeyPrefix: target.keyPrefix,
@@ -51221,6 +53026,150 @@ async function teeRebinds(args, ctx) {
51221
53026
  return 0;
51222
53027
  }
51223
53028
 
53029
+ // src/commands/transfer.ts
53030
+ init_args();
53031
+ init_render();
53032
+ init_trading();
53033
+ var USAGE2 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
53034
+ var TRANSFER_ASSETS = Object.keys(BASES);
53035
+ var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
53036
+ async function readLinkedWallets(ctx, key) {
53037
+ const rows = [];
53038
+ let cursor = null;
53039
+ const seen = new Set;
53040
+ for (let page = 0;page < 25; page++) {
53041
+ const query = cursor === null ? "?limit=100" : `?limit=100&cursor=${encodeURIComponent(cursor)}`;
53042
+ const body = await request(ctx, key, `/api/v1/agent/wallets${query}`);
53043
+ if (!Array.isArray(body.page))
53044
+ throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
53045
+ rows.push(...body.page);
53046
+ if (body.isDone !== false || typeof body.continueCursor !== "string" || seen.has(body.continueCursor))
53047
+ return rows;
53048
+ seen.add(body.continueCursor);
53049
+ cursor = body.continueCursor;
53050
+ }
53051
+ return rows;
53052
+ }
53053
+ async function resolveDestination(ctx, key, source, to) {
53054
+ if (to === "vault") {
53055
+ const lifecycle = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/lifecycle`);
53056
+ const vault = lifecycle.vaultDestination;
53057
+ if (typeof vault !== "string" || vault.length === 0)
53058
+ throw new TradingError("VAULT_NOT_PINNED", "This wallet has no pinned vault recorded on Candle, so --to vault names nothing. Pass the address instead.");
53059
+ return { kind: "vault", address: vault };
53060
+ }
53061
+ const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === "solana" && row.revokedAt === undefined && row.address !== source.address);
53062
+ const matches = rows.filter((row) => row.label === to || row._id === to || row.address === to);
53063
+ if (matches.length > 1)
53064
+ throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${matches.length} linked wallets: ${matches.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
53065
+ const match = matches[0];
53066
+ if (match)
53067
+ return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
53068
+ if (to === source.address)
53069
+ throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
53070
+ if (BASE58_ADDRESS.test(to))
53071
+ return { kind: "address", address: to };
53072
+ throw new TradingError("DESTINATION_UNKNOWN", rows.length === 0 ? `"${to}" is not vault, a Solana address, or a linked wallet on this account (it has no other active Solana linked wallets).` : `"${to}" is not vault, a Solana address, or a linked wallet on this account. Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`);
53073
+ }
53074
+ function describeDestination(destination) {
53075
+ switch (destination.kind) {
53076
+ case "vault":
53077
+ return `this wallet's pinned vault ${destination.address}`;
53078
+ case "linked":
53079
+ return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
53080
+ case "address":
53081
+ return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
53082
+ }
53083
+ }
53084
+ async function confirmTransfer(ctx, lines, yes) {
53085
+ const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
53086
+ for (const line of lines)
53087
+ output.write(`${safeText(line)}
53088
+ `);
53089
+ if (yes)
53090
+ return true;
53091
+ if (!ctx.deps.isTTY.stdin)
53092
+ throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary transfer prompt.");
53093
+ return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
53094
+ }
53095
+ async function transfer(args, ctx) {
53096
+ const parsed = parseArgs(args, {
53097
+ valueFlags: ["--wallet", "--to", "--asset", "--mint", "--amount", "--rpc-url"],
53098
+ booleanFlags: ["--yes"]
53099
+ });
53100
+ if ("error" in parsed) {
53101
+ writeUsageFailure(ctx.deps, parsed.error, ctx.json);
53102
+ return 2;
53103
+ }
53104
+ const flags = parsed.values;
53105
+ const asset = flags["--asset"]?.toUpperCase();
53106
+ if (parsed.positionals.length !== 0 || !flags["--to"] || !flags["--amount"] || Boolean(flags["--asset"]) === Boolean(flags["--mint"]) || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags["--mint"] !== undefined && !BASE58_ADDRESS.test(flags["--mint"])) {
53107
+ writeUsageFailure(ctx.deps, USAGE2, ctx.json);
53108
+ return 2;
53109
+ }
53110
+ const to = flags["--to"];
53111
+ const isMax = flags["--amount"] === "max";
53112
+ try {
53113
+ if (!isMax)
53114
+ rawAmount(flags["--amount"], 18);
53115
+ const key = await tradingKey(ctx);
53116
+ const payer = await tradingPayer(ctx, key, flags["--wallet"], "transfer:write");
53117
+ if (payer.kind === "embedded")
53118
+ throw new TradingError("PAYER_UNSUPPORTED", "This command moves a linked wallet this machine can sign for. The embedded wallet transfers through the agent transfer rail (MCP candle_transfer), not from here. Name a TEE wallet with --wallet.");
53119
+ if (!payer.scopes.includes("transfer:bound"))
53120
+ throw new TradingError("SCOPE_MISSING", "Moving funds out of a TEE wallet needs a Read:Write:Transfer key. Mint one with: candle keys create --access read-write-transfer, then bind the wallet to it with: candle tee rebind");
53121
+ const wallet = payer.wallet;
53122
+ const destination = await resolveDestination(ctx, key, wallet, to);
53123
+ const label = asset ?? flags["--mint"];
53124
+ let amountRaw;
53125
+ if (isMax)
53126
+ amountRaw = "max";
53127
+ else {
53128
+ const decimals = asset ? BASES[asset]?.decimals ?? 9 : await decimalsFor(ctx, flags["--mint"], flags["--rpc-url"]);
53129
+ amountRaw = rawAmount(flags["--amount"], decimals);
53130
+ }
53131
+ const amountText = isMax ? `the full spendable balance of ${label}` : `${flags["--amount"]} ${label}`;
53132
+ const confirmed = await confirmTransfer(ctx, [
53133
+ `Transfer ${amountText} from ${wallet.address} (${wallet.id}) to ${describeDestination(destination)}`,
53134
+ destination.kind === "vault" ? "Destination: this wallet's own vault, pinned when it was imported." : destination.kind === "linked" ? "Destination: a linked wallet on this account. Candle allows it only if you linked it while signed in or marked it trusted, and the amount counts against this key's spend caps." : "Destination: an address Candle will classify at build; from a TEE wallet only its vault or a trusted linked wallet is allowed."
53135
+ ], parsed.booleans.has("--yes"));
53136
+ if (!confirmed)
53137
+ return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet.id, destination });
53138
+ const built = await request(ctx, key, "/api/v1/agent/transfer/build", {
53139
+ walletId: wallet.id,
53140
+ chain: "solana",
53141
+ ...asset ? { asset } : { mint: flags["--mint"] },
53142
+ amountRaw,
53143
+ to: destination.address
53144
+ });
53145
+ const transferId = built.transferId;
53146
+ const unsigned = built.unsignedTransactionsBase64;
53147
+ if (typeof transferId !== "string" || !Array.isArray(unsigned) || unsigned.length !== 1 || typeof unsigned[0] !== "string")
53148
+ throw new TradingError("INVALID_RESPONSE", "Candle did not return one unsigned transfer transaction; nothing was signed.");
53149
+ if (typeof built.payerAddress === "string" && built.payerAddress !== wallet.address)
53150
+ throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the requested payer; nothing was signed.");
53151
+ const builtAmount = typeof built.amountRaw === "string" ? built.amountRaw : amountRaw;
53152
+ ctx.deps.stderr.write(`Built transfer ${transferId}: ${asset ? `${decimalAmount(builtAmount, BASES[asset]?.decimals ?? 0)} ${asset}` : `${builtAmount} raw units of ${label}`}${typeof built.destinationKind === "string" ? ` to ${built.destinationKind === "vault" ? "the vault" : "a linked wallet"}` : ""}.
53153
+ `);
53154
+ const signed = await relaySign(ctx, key, wallet, unsigned[0]);
53155
+ const result = await request(ctx, key, "/api/v1/agent/transfer/submit", {
53156
+ transferId,
53157
+ signedTransactionsBase64: [signed]
53158
+ });
53159
+ const receipt = {
53160
+ ...result,
53161
+ transferId,
53162
+ walletId: wallet.id,
53163
+ wallet: safeText(wallet.address),
53164
+ destination,
53165
+ ...typeof built.destinationKind === "string" ? { destinationKind: built.destinationKind } : {}
53166
+ };
53167
+ return printTradingResult(ctx, receipt);
53168
+ } catch (error) {
53169
+ return tradingFailure(ctx, error);
53170
+ }
53171
+ }
53172
+
51224
53173
  // src/commands/update.ts
51225
53174
  init_args();
51226
53175
  import { randomBytes as randomBytes3 } from "node:crypto";
@@ -52793,10 +54742,15 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
52793
54742
  const opened = await unlockInteractively(ctx, path, raw, {
52794
54743
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
52795
54744
  promptText: "Current vault passphrase, to unlock (input hidden): ",
52796
- factor: "passphrase",
52797
- passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
54745
+ among: factorAddAmong(parseVaultFile(raw).envelopes)
52798
54746
  });
52799
54747
  const vault = hold(opened.vault);
54748
+ const openedWith = {
54749
+ factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
54750
+ envelopeId: opened.factor.envelopeId
54751
+ };
54752
+ if (opened.factor.kind === "security-key")
54753
+ opened.factor.session.pin = undefined;
52800
54754
  const vaultId = vault.file.vaultId;
52801
54755
  let registered;
52802
54756
  try {
@@ -52894,7 +54848,8 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
52894
54848
  saltDerivation: "platform",
52895
54849
  helper: envelope.helper,
52896
54850
  recoverableFactors: recoverable,
52897
- verified: true
54851
+ verified: true,
54852
+ openedWith
52898
54853
  });
52899
54854
  return 0;
52900
54855
  }
@@ -52917,6 +54872,7 @@ ${PASSKEY_NOTE}
52917
54872
  }
52918
54873
 
52919
54874
  // src/commands/vault-factor-security-key.ts
54875
+ init_esm();
52920
54876
  init_crypto();
52921
54877
  init_errors();
52922
54878
  init_fido2();
@@ -53040,20 +54996,84 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
53040
54996
  const raw = await requireVaultRaw(ctx, resolvedVault);
53041
54997
  const file = parseVaultFile(raw);
53042
54998
  const envelopeId = freshEnvelopeId();
53043
- const session = await openSecurityKeySession(deps, {
53044
- vaultId: file.vaultId,
53045
- envelopeId,
53046
- deviceFlag: ctx.vaultDevice,
53047
- requireFeatures: true
53048
- });
54999
+ const vaultId = file.vaultId;
55000
+ const keysInHeader = file.envelopes.filter(isCtap2Envelope);
55001
+ const { helperPath, info } = await listSecurityKeys(deps, { vaultId, envelopeId });
55002
+ assertHelperExcludeList(info, helperPath, keysInHeader.length);
55003
+ const attached = keysInHeader.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: keysInHeader }) : undefined;
55004
+ const excludeCountNow = keysInHeader.length;
55005
+ const addingLine = (device) => deps.stderr.write(`Adding: ${device.product || "security key"} (--device ${device.deviceId}), a different key from the one that opened the vault.
55006
+ `);
55007
+ let session;
55008
+ let enrolling;
55009
+ let deferredAfter;
53049
55010
  const opened = await unlockInteractively(ctx, path, raw, {
53050
55011
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
53051
55012
  promptText: "Current vault passphrase, to unlock (input hidden): ",
53052
- factor: "passphrase",
53053
- passphraseOnlyBecause: "this command's security key session is for the key being added, so the vault opens with the passphrase here."
55013
+ among: factorAddAmong(file.envelopes),
55014
+ attached,
55015
+ onFactorChosen: async (chosen) => {
55016
+ if (chosen.kind === "passphrase") {
55017
+ session = await openSecurityKeySession(deps, {
55018
+ vaultId,
55019
+ envelopeId,
55020
+ deviceFlag: ctx.vaultDevice,
55021
+ requireFeatures: true,
55022
+ beforePin: (device) => {
55023
+ refuseIfEnrolled(device, attached);
55024
+ assertExcludeListFits(device, excludeCountNow);
55025
+ }
55026
+ });
55027
+ return;
55028
+ }
55029
+ if (chosen.kind !== "security-key")
55030
+ return;
55031
+ const plan = planDevices(ctx, chosen.envelope, info.devices, attached);
55032
+ if (plan.enrolling !== undefined) {
55033
+ refuseIfEnrolled(plan.enrolling, attached);
55034
+ assertDeviceServesFactor(plan.enrolling);
55035
+ assertExcludeListFits(plan.enrolling, excludeCountNow);
55036
+ enrolling = plan.enrolling;
55037
+ addingLine(plan.enrolling);
55038
+ } else {
55039
+ deferredAfter = plan.opening;
55040
+ }
55041
+ return { preferDevice: plan.opening.deviceId };
55042
+ }
53054
55043
  });
53055
55044
  const vault = hold(opened.vault);
53056
- const registered = await registerCredential(deps, session, { vaultId: vault.file.vaultId, envelopeId });
55045
+ const openedWith = {
55046
+ factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
55047
+ envelopeId: opened.factor.envelopeId
55048
+ };
55049
+ if (opened.factor.kind === "security-key")
55050
+ opened.factor.session.pin = undefined;
55051
+ const enrolledKeys = vault.file.envelopes.filter(isCtap2Envelope);
55052
+ const excludeCredentialIds = enrolledKeys.map((envelope2) => base64.encode(unb64u(envelope2.credentialId, "credentialId")));
55053
+ if (session === undefined) {
55054
+ if (deferredAfter !== undefined) {
55055
+ await deps.promptLine(`The vault is open. Unplug ${deferredAfter.product || "the security key that opened it"}, insert the security key to add, then press Enter.`);
55056
+ const again = await listSecurityKeys(deps, { vaultId, envelopeId });
55057
+ const probeAgain = enrolledKeys.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: enrolledKeys }) : undefined;
55058
+ const inserted = chooseAfterInsert(ctx, again.info.devices, probeAgain);
55059
+ refuseIfEnrolled(inserted, probeAgain);
55060
+ assertDeviceServesFactor(inserted);
55061
+ assertExcludeListFits(inserted, excludeCredentialIds.length);
55062
+ addingLine(inserted);
55063
+ enrolling = inserted;
55064
+ }
55065
+ if (enrolling === undefined)
55066
+ throw new Error("factor add security-key: no enrolling device was decided");
55067
+ const target = enrolling;
55068
+ session = await openSecurityKeySession(deps, {
55069
+ vaultId,
55070
+ envelopeId,
55071
+ deviceFlag: target.deviceId,
55072
+ requireFeatures: true,
55073
+ pinPrompt: (device) => `PIN for the key being added, ${device.product || "the security key"} (input hidden): `
55074
+ });
55075
+ }
55076
+ const registered = await registerCredential(deps, session, { vaultId, envelopeId, excludeCredentialIds });
53057
55077
  if (registered.backupEligible) {
53058
55078
  throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${session.device.product || "This authenticator"} reports the credential it created as backup-eligible (synced), so it is not a hardware-bound security key credential and cannot be recorded as a hardware-token factor.`, {
53059
55079
  suggestion: "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Use a hardware security key for this factor."
@@ -53110,7 +55130,8 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
53110
55130
  backupState: envelope.backupState,
53111
55131
  userVerification: "required",
53112
55132
  recoverableFactors: recoverable,
53113
- verified: true
55133
+ verified: true,
55134
+ openedWith
53114
55135
  });
53115
55136
  return 0;
53116
55137
  }
@@ -53129,6 +55150,76 @@ ${SECURITY_KEY_PAIR_NOTE}
53129
55150
  `);
53130
55151
  return 0;
53131
55152
  }
55153
+ function refuseIfEnrolled(device, attached) {
55154
+ const entry = attached?.devices?.find((candidate) => candidate.deviceId === device.deviceId);
55155
+ if (entry === undefined || attached?.credentials === undefined)
55156
+ return;
55157
+ for (const [envelopeId, credentialId] of attached.credentials) {
55158
+ if (entry.present.includes(credentialId))
55159
+ throw alreadyEnrolled(device, envelopeId);
55160
+ }
55161
+ }
55162
+ function planDevices(ctx, opener, devices, attached) {
55163
+ const platform = ctx.deps.platform;
55164
+ if (devices.length === 0) {
55165
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key is attached.", {
55166
+ suggestion: "Plug the key in and run the command again. No other factor is substituted."
55167
+ });
55168
+ }
55169
+ const named = ctx.vaultDevice;
55170
+ const openerCredential = attached?.credentials?.get(opener.id);
55171
+ const holders = openerCredential === undefined ? [] : (attached?.devices ?? []).filter((device) => device.present.includes(openerCredential));
55172
+ let opening;
55173
+ if (holders.length === 1) {
55174
+ opening = devices.find((device) => device.deviceId === holders[0].deviceId);
55175
+ }
55176
+ if (opening === undefined) {
55177
+ const candidates = devices.filter((device) => device.deviceId !== named);
55178
+ if (candidates.length === 1)
55179
+ opening = candidates[0];
55180
+ else if (devices.length === 1)
55181
+ opening = devices[0];
55182
+ }
55183
+ if (opening === undefined) {
55184
+ throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${devices.length} security keys are attached and the one that opens the vault could not be told apart. Leave only the key that opens the vault attached; this command asks for the key to add after the vault is open.`, { suggestion: "Nothing was sent to any key. Run again with only the key that opens the vault attached." });
55185
+ }
55186
+ if (!opening.readable)
55187
+ selectDevice([opening], undefined, platform);
55188
+ const others = devices.filter((device) => device.deviceId !== opening.deviceId);
55189
+ if (named !== undefined) {
55190
+ if (named === opening.deviceId)
55191
+ throw alreadyEnrolled(opening, opener.id);
55192
+ return { opening, enrolling: selectDevice(others, named, platform) };
55193
+ }
55194
+ if (others.length === 1)
55195
+ return { opening, enrolling: selectDevice(others, undefined, platform) };
55196
+ if (others.length > 1) {
55197
+ const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
55198
+ `);
55199
+ throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opens the vault, and none was named, so nothing was sent to any of them:
55200
+ ${listing}`, { suggestion: "Run again with --device <id> naming the key to add." });
55201
+ }
55202
+ return { opening, enrolling: undefined };
55203
+ }
55204
+ function chooseAfterInsert(ctx, devices, probe) {
55205
+ const platform = ctx.deps.platform;
55206
+ if (devices.length === 0) {
55207
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key other than the one that opened the vault is attached. Nothing was written.", { suggestion: "Run the command again with the key to add inserted when asked." });
55208
+ }
55209
+ if (devices.length === 1)
55210
+ return selectDevice(devices, undefined, platform);
55211
+ const holdsVault = (device) => (probe?.devices?.find((entry) => entry.deviceId === device.deviceId)?.present.length ?? 0) > 0;
55212
+ const others = devices.filter((device) => !holdsVault(device));
55213
+ if (others.length === 1)
55214
+ return selectDevice(others, undefined, platform);
55215
+ if (others.length === 0) {
55216
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "Every attached security key already holds one of this vault's credentials, so there is no key to add. Nothing was written.", { suggestion: "Run the command again with a security key this vault does not have inserted when asked." });
55217
+ }
55218
+ const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
55219
+ `);
55220
+ throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opened the vault, so nothing was sent to any of them:
55221
+ ${listing}`, { suggestion: "Run the command again with only the key to add inserted when asked. Nothing was written." });
55222
+ }
53132
55223
 
53133
55224
  // src/commands/vault-factor-touch-id.ts
53134
55225
  init_enclave();
@@ -53167,10 +55258,15 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
53167
55258
  const opened = await unlockInteractively(ctx, path, raw, {
53168
55259
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
53169
55260
  promptText: "Current vault passphrase, to unlock (input hidden): ",
53170
- factor: "passphrase",
53171
- passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
55261
+ among: factorAddAmong(parseVaultFile(raw).envelopes)
53172
55262
  });
53173
55263
  const vault = hold(opened.vault);
55264
+ const openedWith = {
55265
+ factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
55266
+ envelopeId: opened.factor.envelopeId
55267
+ };
55268
+ if (opened.factor.kind === "security-key")
55269
+ opened.factor.session.pin = undefined;
53174
55270
  const vaultId = vault.file.vaultId;
53175
55271
  const keyTag = keyTagFor(vaultId, envelopeId);
53176
55272
  let created;
@@ -53254,7 +55350,8 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
53254
55350
  helper: { teamId: session.identity.teamId, bundleId: session.identity.bundleId, minVersion: session.version },
53255
55351
  accessControl: "biometryCurrentSet",
53256
55352
  recoverableFactors: recoverable,
53257
- verified: true
55353
+ verified: true,
55354
+ openedWith
53258
55355
  });
53259
55356
  return 0;
53260
55357
  }
@@ -53960,9 +56057,18 @@ function decimalToRaw2(decimal, decimals) {
53960
56057
  }
53961
56058
  function assertVaultSigner(entry) {
53962
56059
  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).`);
56060
+ 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).`, {
56061
+ suggestion: `To move funds out of a promoted wallet, use: candle vault transfer <to> --from ${entry.label ?? entry.address}`
56062
+ });
53964
56063
  }
53965
56064
  }
56065
+ function assertTransferSigner(entry) {
56066
+ if (entry.role === "vault" || entry.role === "tee-wallet")
56067
+ return;
56068
+ throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${entry.label ?? entry.address} is an external wallet and cannot sign vault transfer shapes (ED-10).`, {
56069
+ suggestion: `Move funds out of an external wallet with: candle external sweep ${entry.label} --to <vault> --rpc-url <url>`
56070
+ });
56071
+ }
53966
56072
  function namedSendFailure(plan) {
53967
56073
  if (!plan.token)
53968
56074
  return;
@@ -54707,9 +56813,9 @@ init_render();
54707
56813
  init_solana_lite();
54708
56814
  init_store();
54709
56815
  init_vault_support();
54710
- var CHUNK = 100;
56816
+ var CHUNK2 = 100;
54711
56817
  var LAMPORTS_PER_SOL = 1000000000n;
54712
- function formatSol2(lamports) {
56818
+ function formatSol3(lamports) {
54713
56819
  const whole = lamports / LAMPORTS_PER_SOL;
54714
56820
  const fraction = (lamports % LAMPORTS_PER_SOL).toString().padStart(9, "0").replace(/0+$/, "");
54715
56821
  return fraction === "" ? whole.toString() : `${whole}.${fraction}`;
@@ -54719,8 +56825,8 @@ async function readLamports(addresses, rpcUrl2, fetchFn) {
54719
56825
  const lamports = new Map;
54720
56826
  const unavailable = [];
54721
56827
  let failure;
54722
- for (let at = 0;at < addresses.length; at += CHUNK) {
54723
- const chunk = addresses.slice(at, at + CHUNK);
56828
+ for (let at = 0;at < addresses.length; at += CHUNK2) {
56829
+ const chunk = addresses.slice(at, at + CHUNK2);
54724
56830
  try {
54725
56831
  const accounts = await rpc2.getMultipleAccounts(chunk);
54726
56832
  for (const [i, address] of chunk.entries())
@@ -54771,7 +56877,7 @@ async function vaultList(args, ctx) {
54771
56877
  const all = vault.index.entries;
54772
56878
  const matched = filter === undefined ? all : all.filter((entry) => matches(entry, filter));
54773
56879
  const solana = balances ? matched.filter((entry) => entry.chain === "solana") : [];
54774
- const requests = Math.ceil(solana.length / CHUNK);
56880
+ const requests = Math.ceil(solana.length / CHUNK2);
54775
56881
  const rpcHost = rpcUrl2 === undefined ? undefined : new URL(rpcUrl2).host;
54776
56882
  if (balances && solana.length > 0 && rpcHost !== undefined) {
54777
56883
  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 +56939,7 @@ async function vaultList(args, ctx) {
54833
56939
  const row = [entry.address, entry.label || "(none)", entry.role, entry.derivation?.path ?? "-"];
54834
56940
  if (balances) {
54835
56941
  const held = lamports.get(entry.address);
54836
- row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol2(held));
56942
+ row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol3(held));
54837
56943
  }
54838
56944
  return row;
54839
56945
  });
@@ -54842,9 +56948,9 @@ ${renderTable(headers, rows)}
54842
56948
  `);
54843
56949
  if (balances) {
54844
56950
  deps.stdout.write(complete ? `
54845
- total ${formatSol2(totalLamports)} SOL across ${solana.length} keys
56951
+ total ${formatSol3(totalLamports)} SOL across ${solana.length} keys
54846
56952
  ` : `
54847
- total ${formatSol2(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
56953
+ total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
54848
56954
  `);
54849
56955
  }
54850
56956
  return complete ? 0 : 3;
@@ -54870,6 +56976,7 @@ async function vaultPhrase(args, ctx) {
54870
56976
  init_esm();
54871
56977
  init_args();
54872
56978
  init_deps();
56979
+ init_profiles();
54873
56980
  init_render();
54874
56981
  init_solana_lite();
54875
56982
 
@@ -54976,12 +57083,299 @@ function restoreRefusedFailure(init, error) {
54976
57083
  // src/commands/vault-promote.ts
54977
57084
  init_errors();
54978
57085
  init_promote_support();
57086
+ // src/vault/promote-to-key.ts
57087
+ init_deps();
57088
+ init_render();
57089
+ var REBIND_CHUNK = 200;
57090
+ var SELECTED_SCOPE_LIMIT = 50;
57091
+ function targetKeyRefusal(row, keyPrefix, now) {
57092
+ const written = "Nothing was written.";
57093
+ if (row === undefined) {
57094
+ return {
57095
+ code: "REBIND_KEY_NOT_FOUND",
57096
+ message: `No key ${keyPrefix} on this account. ${written}`,
57097
+ suggestion: "The target must be a key on the same account: candle keys list"
57098
+ };
57099
+ }
57100
+ if (row.revokedAt !== undefined) {
57101
+ return {
57102
+ code: "REBIND_TARGET_INVALID",
57103
+ message: `The target key ${keyPrefix} is revoked. ${written}`,
57104
+ suggestion: "Pick an active key: candle keys list"
57105
+ };
57106
+ }
57107
+ if (row.expiresAt !== undefined && row.expiresAt !== null && row.expiresAt <= now) {
57108
+ return {
57109
+ code: "REBIND_TARGET_INVALID",
57110
+ message: `The target key ${keyPrefix} has expired. ${written}`,
57111
+ suggestion: "Pick an active key: candle keys list"
57112
+ };
57113
+ }
57114
+ if (row.environment !== "production") {
57115
+ return {
57116
+ code: "REBIND_TARGET_INVALID",
57117
+ message: `The target key ${keyPrefix} is a test key; the trade rail refuses test keys. ${written}`,
57118
+ suggestion: "Pick a production key: candle keys list"
57119
+ };
57120
+ }
57121
+ if (!row.scopes.includes("swap:write")) {
57122
+ return {
57123
+ code: "REBIND_TARGET_INVALID",
57124
+ message: `The target key ${keyPrefix} lacks swap:write, which a TEE wallet needs to trade. ${written}`,
57125
+ suggestion: "Pick a key with swap:write, or mint one: candle keys create"
57126
+ };
57127
+ }
57128
+ return null;
57129
+ }
57130
+ function tradeReadinessOf(row) {
57131
+ const hasTxLimit = row.txLimit !== null && row.txLimit !== undefined;
57132
+ const limits = row.spendLimits ?? [];
57133
+ const cap = (asset) => limits.some((limit) => limit.asset === asset.toLowerCase() || limit.asset === asset.toUpperCase());
57134
+ const sol = cap("sol");
57135
+ const usdc = cap("usdc");
57136
+ const missingCaps = [];
57137
+ if (!hasTxLimit)
57138
+ missingCaps.push("txLimit");
57139
+ if (!sol)
57140
+ missingCaps.push("spendLimits.sol");
57141
+ if (!usdc)
57142
+ missingCaps.push("spendLimits.usdc");
57143
+ return { tradeReady: { sol: hasTxLimit && sol, usdc: hasTxLimit && usdc }, missingCaps };
57144
+ }
57145
+ function targetWarnings(target) {
57146
+ return capWarnings(target.keyPrefix, {
57147
+ keyPrefix: target.keyPrefix,
57148
+ label: target.label,
57149
+ paused: target.paused,
57150
+ walletScope: target.walletScope,
57151
+ tradeReady: target.tradeReady,
57152
+ missingCaps: target.missingCaps,
57153
+ launchScope: target.launchScope
57154
+ });
57155
+ }
57156
+ async function preflightToKey(ctx, raw, opts) {
57157
+ const { deps, json } = ctx;
57158
+ const refuse2 = (failure) => {
57159
+ writeLocalFailure(deps, failure, json);
57160
+ return { ok: false, exit: 1 };
57161
+ };
57162
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
57163
+ if (!deviceToken)
57164
+ return refuse2(DEVICE_TOKEN_REQUIRED);
57165
+ const listing = await listAccountKeys(ctx, deviceToken);
57166
+ if (!listing.ok) {
57167
+ return refuse2({
57168
+ code: listing.code ?? `HTTP ${listing.status}`,
57169
+ message: `Could not read this account's keys to check --to-key (${listing.status === 0 ? listing.message : `HTTP ${listing.status}`}). Nothing was written.`,
57170
+ suggestion: "Check the device token with: candle auth status"
57171
+ });
57172
+ }
57173
+ const keys = listing.body?.keys ?? [];
57174
+ const resolved = await resolveTargetKey(ctx, deviceToken, raw, { keys });
57175
+ if (!resolved.ok)
57176
+ return { ok: false, exit: resolved.code };
57177
+ const keyPrefix = resolved.keyPrefix;
57178
+ const row = keys.find((key) => key.keyPrefix === keyPrefix);
57179
+ const refusal = targetKeyRefusal(row, keyPrefix, deps.now());
57180
+ if (refusal !== null)
57181
+ return refuse2(refusal);
57182
+ const target = row;
57183
+ const walletScope = target.walletScope === "selected" ? "selected" : "all";
57184
+ if (walletScope === "selected") {
57185
+ const room = await readSelectedScopeRoom(ctx, keyPrefix);
57186
+ if (room.ok) {
57187
+ const moving = opts.labels.filter((label) => !room.heldLabels.has(label)).length;
57188
+ if (room.held + moving > SELECTED_SCOPE_LIMIT) {
57189
+ return refuse2({
57190
+ code: "REBIND_SCOPE_FULL",
57191
+ 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.`,
57192
+ suggestion: "Widen the key's wallet scope from the portal, or name a key with room."
57193
+ });
57194
+ }
57195
+ } else {
57196
+ deps.stderr.write(`Could not read key ${keyPrefix}'s wallet set (${room.reason}); its selected-scope room is checked at the rebind.
57197
+ `);
57198
+ }
57199
+ }
57200
+ const readiness = tradeReadinessOf(target);
57201
+ return {
57202
+ ok: true,
57203
+ deviceToken,
57204
+ target: {
57205
+ keyPrefix,
57206
+ label: typeof target.label === "string" && target.label.length > 0 ? target.label : null,
57207
+ walletScope,
57208
+ paused: target.pausedAt !== undefined && target.pausedAt !== null,
57209
+ launchScope: target.scopes.includes("launch:write"),
57210
+ ...readiness
57211
+ }
57212
+ };
57213
+ }
57214
+ async function readSelectedScopeRoom(ctx, keyPrefix) {
57215
+ const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
57216
+ if (!apiKey)
57217
+ return { ok: false, reason: "no API key" };
57218
+ const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`, {
57219
+ auth: "key",
57220
+ credentials: { apiKey },
57221
+ apiUrl: ctx.apiUrl,
57222
+ fetch: ctx.deps.fetch,
57223
+ env: ctx.deps.env
57224
+ });
57225
+ if (!result.ok)
57226
+ return { ok: false, reason: result.status === 0 ? result.message : `HTTP ${result.status}` };
57227
+ const wallets2 = result.body?.wallets;
57228
+ if (!Array.isArray(wallets2))
57229
+ return { ok: false, reason: "no wallet list in the response" };
57230
+ const heldLabels = new Set;
57231
+ for (const wallet of wallets2)
57232
+ if (typeof wallet?.label === "string")
57233
+ heldLabels.add(wallet.label);
57234
+ return { ok: true, held: wallets2.length, heldLabels };
57235
+ }
57236
+ function chunkWallets(wallets2, size = REBIND_CHUNK) {
57237
+ const out = [];
57238
+ for (let at = 0;at < wallets2.length; at += size)
57239
+ out.push(wallets2.slice(at, at + size));
57240
+ return out;
57241
+ }
57242
+ async function rebindPromoted(ctx, deviceToken, toKeyPrefix, wallets2) {
57243
+ const run = { ok: true, requests: 0, outcomes: new Map };
57244
+ for (const wallet of wallets2)
57245
+ run.outcomes.set(wallet.id, { state: "not-reached" });
57246
+ const chunks = chunkWallets(wallets2);
57247
+ for (const [at, chunk] of chunks.entries()) {
57248
+ run.requests += 1;
57249
+ const preview = await postRebind(ctx, deviceToken, {
57250
+ dryRun: true,
57251
+ toKeyPrefix,
57252
+ wallets: chunk.map((wallet) => wallet.id)
57253
+ });
57254
+ if (!preview.ok) {
57255
+ run.ok = false;
57256
+ run.failure = { ...rebindFailureDetails(preview, {}), chunk: at + 1 };
57257
+ return run;
57258
+ }
57259
+ const shown = preview.body;
57260
+ if (run.toKey === undefined)
57261
+ run.toKey = shown.toKey;
57262
+ for (const row of shown.unchanged)
57263
+ run.outcomes.set(row.id, { state: "unchanged" });
57264
+ const moving = shown.rebound;
57265
+ if (moving.length === 0)
57266
+ continue;
57267
+ run.requests += 1;
57268
+ const committed = await postRebind(ctx, deviceToken, {
57269
+ toKeyPrefix,
57270
+ walletIds: moving.map((row) => row.id),
57271
+ expect: Object.fromEntries(moving.map((row) => [row.id, row.fromKeyPrefix]))
57272
+ });
57273
+ if (!committed.ok) {
57274
+ run.ok = false;
57275
+ run.failure = {
57276
+ ...rebindFailureDetails(committed, {
57277
+ fromKeyPrefixes: Array.from(new Set(moving.map((row) => row.fromKeyPrefix)))
57278
+ }),
57279
+ chunk: at + 1
57280
+ };
57281
+ for (const row of moving)
57282
+ run.outcomes.set(row.id, { state: "failed" });
57283
+ return run;
57284
+ }
57285
+ const result = committed.body;
57286
+ for (const row of result.rebound) {
57287
+ run.outcomes.set(row.id, { state: "rebound", ...row.auditId !== undefined ? { auditId: row.auditId } : {} });
57288
+ }
57289
+ for (const row of result.unchanged)
57290
+ run.outcomes.set(row.id, { state: "unchanged" });
57291
+ }
57292
+ return run;
57293
+ }
57294
+ function finishingCommands(addresses, toKeyPrefix) {
57295
+ return chunkWallets(addresses).map((chunk) => `candle tee rebind ${chunk.join(" ")} --to-key ${toKeyPrefix}`);
57296
+ }
57297
+ function pendingWallets(wallets2, run) {
57298
+ return wallets2.filter((wallet) => {
57299
+ const state = run?.outcomes.get(wallet.id)?.state;
57300
+ return state !== "rebound" && state !== "unchanged";
57301
+ });
57302
+ }
57303
+ function renderRebindReport(input) {
57304
+ const { target, wallets: wallets2, run, notRebindable } = input;
57305
+ const lines = [];
57306
+ const name = (wallet) => `${wallet.label} (${wallet.address})`;
57307
+ if (run !== undefined) {
57308
+ const rebound = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "rebound").length;
57309
+ const unchanged = wallets2.filter((wallet) => run.outcomes.get(wallet.id)?.state === "unchanged").length;
57310
+ if (rebound > 0 || unchanged > 0) {
57311
+ 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"}.`);
57312
+ }
57313
+ if (run.failure !== undefined) {
57314
+ lines.push(`Rebind ${run.failure.code}: ${run.failure.message}${run.failure.suggestion ? ` ${run.failure.suggestion}` : ""}`);
57315
+ }
57316
+ }
57317
+ const pending = pendingWallets(wallets2, run);
57318
+ if (pending.length > 0) {
57319
+ 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}`));
57320
+ }
57321
+ if (notRebindable.length > 0) {
57322
+ 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}`));
57323
+ }
57324
+ return lines.join(`
57325
+ `);
57326
+ }
57327
+ function walletRebindJson(wallet, run) {
57328
+ if (wallet.id === null || wallet.remoteAuthority !== "verified-active") {
57329
+ return {
57330
+ state: "not-rebindable",
57331
+ reason: wallet.id === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
57332
+ };
57333
+ }
57334
+ const outcome = run?.outcomes.get(wallet.id);
57335
+ if (outcome === undefined)
57336
+ return { state: "not-reached" };
57337
+ return { state: outcome.state, ...outcome.auditId !== undefined ? { auditId: outcome.auditId } : {} };
57338
+ }
57339
+ function finalBoundKey(wallet, run, toKeyPrefix) {
57340
+ const state = wallet.id === null ? undefined : run?.outcomes.get(wallet.id)?.state;
57341
+ return state === "rebound" || state === "unchanged" ? toKeyPrefix : wallet.importedTo;
57342
+ }
57343
+ function rebindJson(input) {
57344
+ const { target, wallets: wallets2, run } = input;
57345
+ const pending = pendingWallets(wallets2, run);
57346
+ const count = (state) => wallets2.filter((wallet) => run?.outcomes.get(wallet.id)?.state === state).length;
57347
+ return {
57348
+ toKey: {
57349
+ keyPrefix: target.keyPrefix,
57350
+ label: target.label,
57351
+ tradeReady: run?.toKey?.tradeReady ?? target.tradeReady,
57352
+ missingCaps: run?.toKey?.missingCaps ?? target.missingCaps
57353
+ },
57354
+ rebind: {
57355
+ ok: run !== undefined && run.ok && input.notRebindable.length === 0,
57356
+ reached: run !== undefined,
57357
+ requests: run?.requests ?? 0,
57358
+ rebound: count("rebound"),
57359
+ unchanged: count("unchanged"),
57360
+ pending: pending.map((wallet) => wallet.address),
57361
+ notRebindable: input.notRebindable.length,
57362
+ ...run?.failure !== undefined ? {
57363
+ code: run.failure.code,
57364
+ message: run.failure.message,
57365
+ ...run.failure.suggestion !== undefined ? { suggestion: run.failure.suggestion } : {}
57366
+ } : {},
57367
+ finishWith: finishingCommands(pending.map((wallet) => wallet.address), target.keyPrefix)
57368
+ }
57369
+ };
57370
+ }
57371
+
57372
+ // src/commands/vault-promote.ts
54979
57373
  init_signer_roles();
54980
57374
  init_store();
54981
57375
  init_vault_support();
54982
57376
  async function vaultPromote(args, ctx) {
54983
57377
  const parsed = parseArgs(args, {
54984
- valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore"],
57378
+ valueFlags: ["--from", "--in-place", "--sweep-to", "--label", "--rpc-url", "--keystore", "--to-key"],
54985
57379
  booleanFlags: ["--accept-unknown-exposure", "--accept-older-copy"],
54986
57380
  pathFlags: ["--keystore"]
54987
57381
  });
@@ -54995,21 +57389,73 @@ async function vaultPromote(args, ctx) {
54995
57389
  return usage(ctx, "Use either --from or --in-place, not both.");
54996
57390
  }
54997
57391
  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>");
57392
+ 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
57393
  }
57394
+ const toKeyRaw = parsed.values["--to-key"];
57395
+ if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
57396
+ return usage(ctx, "--to-key needs a label or a prefix.");
55000
57397
  if (!refuseEnvPassphrase(ctx))
55001
57398
  return 1;
55002
57399
  if (!requireTty(ctx, "vault promote"))
55003
57400
  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.");
57401
+ if (fromLabel !== undefined && parsed.values["--sweep-to"] !== undefined) {
57402
+ return usage(ctx, "Fresh-key promote takes --from, not --sweep-to.");
57403
+ }
57404
+ let toKey;
57405
+ if (toKeyRaw !== undefined) {
57406
+ const label = parsed.values["--label"] ?? inPlaceLabel ?? (fromLabel !== undefined ? "<fresh>" : undefined);
57407
+ const preflight = await preflightToKey(ctx, toKeyRaw, { labels: label !== undefined ? [label] : [] });
57408
+ if (!preflight.ok)
57409
+ return preflight.exit;
57410
+ toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
57411
+ }
57412
+ if (fromLabel !== undefined)
57413
+ return promoteFresh(ctx, parsed, fromLabel, toKey);
57414
+ return promoteInPlace(ctx, parsed, inPlaceLabel, toKey);
57415
+ }
57416
+ async function rebindAfterImport(ctx, toKey, wallet, callingKeyPrefix) {
57417
+ const rebindable = wallet.linkedWalletId !== null && wallet.remoteAuthority === "verified-active" ? [{ id: wallet.linkedWalletId, address: wallet.address, label: wallet.label }] : [];
57418
+ const notRebindable = rebindable.length === 0 ? [
57419
+ {
57420
+ address: wallet.address,
57421
+ label: wallet.label,
57422
+ reason: wallet.linkedWalletId === null ? "no linked wallet id" : `remote authority is ${wallet.remoteAuthority ?? "unknown"}, not verified-active`
57423
+ }
57424
+ ] : [];
57425
+ const run = rebindable.length > 0 ? await rebindPromoted(ctx, toKey.deviceToken, toKey.target.keyPrefix, rebindable) : undefined;
57426
+ const input = { target: toKey.target, callingKeyPrefix, wallets: rebindable, run, notRebindable };
57427
+ const report = renderRebindReport(input);
57428
+ if (report.length > 0)
57429
+ ctx.deps.stderr.write(`${report}
57430
+ `);
57431
+ const id = wallet.linkedWalletId;
57432
+ return {
57433
+ exit: run !== undefined && !run.ok ? 1 : 0,
57434
+ json: {
57435
+ ...rebindJson(input),
57436
+ boundKeyPrefix: finalBoundKey({ id, importedTo: wallet.importedTo }, run, toKey.target.keyPrefix),
57437
+ walletRebind: walletRebindJson({ id, remoteAuthority: wallet.remoteAuthority }, run)
55007
57438
  }
55008
- return promoteFresh(ctx, parsed, fromLabel);
57439
+ };
57440
+ }
57441
+ async function confirmResumeRebind(ctx, toKey) {
57442
+ const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
57443
+ ctx.deps.stderr.write(`${[
57444
+ `This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
57445
+ ...targetWarnings(toKey.target)
57446
+ ].join(`
57447
+ `)}
57448
+ `);
57449
+ const typed = await ctx.deps.promptLine(`Type ${CONFIRM_WORD} to move this wallet to ${toKey.target.keyPrefix}: `);
57450
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
57451
+ 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
57452
  }
55010
- return promoteInPlace(ctx, parsed, inPlaceLabel);
55011
57453
  }
55012
- async function promoteFresh(ctx, parsed, fromLabel) {
57454
+ async function callingKeyPrefixFor(ctx) {
57455
+ const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
57456
+ return (apiKey !== undefined ? apiKeyPrefix(apiKey) : undefined) ?? "(the calling key)";
57457
+ }
57458
+ async function promoteFresh(ctx, parsed, fromLabel, toKey) {
55013
57459
  if ("error" in parsed)
55014
57460
  return usage(ctx, parsed.error);
55015
57461
  const resolvedVault = vaultPathFor(ctx, parsed);
@@ -55088,10 +57534,19 @@ async function promoteFresh(ctx, parsed, fromLabel) {
55088
57534
  addKeys: [blob]
55089
57535
  }, ctx.deps));
55090
57536
  await confirmLastSix(ctx, destination.address, "the sweep vault destination");
57537
+ if (toKey !== undefined) {
57538
+ const name = toKey.target.label !== null ? ` (${toKey.target.label})` : "";
57539
+ ctx.deps.stderr.write(`${[
57540
+ `This wallet will be bound to key ${toKey.target.keyPrefix}${name} by a rebind after the import.`,
57541
+ ...targetWarnings(toKey.target)
57542
+ ].join(`
57543
+ `)}
57544
+ `);
57545
+ }
55091
57546
  const privateKey = base58.encode(secret64);
55092
57547
  wipe(secret64);
55093
57548
  const importCount = { n: 0 };
55094
- const { exit: code } = await runTeeImport(ctx, {
57549
+ const { exit: code, submitted } = await runTeeImport(ctx, {
55095
57550
  address,
55096
57551
  privateKey,
55097
57552
  label: entry.label,
@@ -55110,22 +57565,35 @@ async function promoteFresh(ctx, parsed, fromLabel) {
55110
57565
  if (target === undefined) {
55111
57566
  throw new VaultError("VAULT_INDEX_INVALID", `Entry ${keyId} missing after import.`);
55112
57567
  }
57568
+ let rebound;
57569
+ if (toKey !== undefined) {
57570
+ const importedTo = submitted?.boundKeyPrefix ?? null;
57571
+ rebound = await rebindAfterImport(ctx, toKey, {
57572
+ linkedWalletId: target.linkedWalletId ?? null,
57573
+ address,
57574
+ label: entry.label,
57575
+ remoteAuthority: target.tee?.remoteAuthority ?? submitted?.remoteAuthority ?? null,
57576
+ importedTo
57577
+ }, await callingKeyPrefixFor(ctx));
57578
+ }
57579
+ const exit = Math.max(code, rebound?.exit ?? 0);
55113
57580
  if (ctx.json) {
55114
57581
  writeJson(ctx.deps, {
55115
- ok: true,
57582
+ ok: rebound === undefined || rebound.exit === 0,
55116
57583
  mode: "fresh",
55117
57584
  address,
55118
57585
  label: entry.label,
55119
57586
  vaultDestination: destination.address,
55120
57587
  lifecycle: target.tee?.lifecycle,
55121
57588
  linkedWalletId: target.linkedWalletId ?? null,
55122
- importCalls: importCount.n
57589
+ importCalls: importCount.n,
57590
+ ...rebound?.json ?? {}
55123
57591
  });
55124
57592
  } else {
55125
57593
  ctx.deps.stdout.write(`Promoted fresh TEE wallet ${address} (sweep to ${destination.address}).
55126
57594
  `);
55127
57595
  }
55128
- return code;
57596
+ return exit;
55129
57597
  });
55130
57598
  }
55131
57599
  async function refuseWithoutRoom(ctx) {
@@ -55147,7 +57615,7 @@ async function reopenFromDisk(path, reopen, previous) {
55147
57615
  });
55148
57616
  return reopen(path, raw);
55149
57617
  }
55150
- async function promoteInPlace(ctx, parsed, subjectLabel) {
57618
+ async function promoteInPlace(ctx, parsed, subjectLabel, toKey) {
55151
57619
  if ("error" in parsed)
55152
57620
  return usage(ctx, parsed.error);
55153
57621
  const resolvedVault = vaultPathFor(ctx, parsed);
@@ -55174,7 +57642,9 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55174
57642
  if (sweepTo !== undefined) {
55175
57643
  return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
55176
57644
  }
55177
- const { exit } = await resumePromote(ctx, vault, existing, opened.reopen, path, hold, {
57645
+ if (toKey !== undefined)
57646
+ await confirmResumeRebind(ctx, toKey);
57647
+ const resumed = await resumePromote(ctx, vault, existing, opened.reopen, path, hold, {
55178
57648
  confirmAccount: (account) => confirmLastSix(ctx, account, "that account"),
55179
57649
  confirmDestination: async (destination2) => {
55180
57650
  try {
@@ -55183,9 +57653,36 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55183
57653
  } catch {
55184
57654
  return false;
55185
57655
  }
55186
- }
57656
+ },
57657
+ report: toKey !== undefined ? "return" : "write"
55187
57658
  });
55188
- return exit;
57659
+ if (toKey === undefined)
57660
+ return resumed.exit;
57661
+ if (resumed.failure !== undefined || resumed.exit !== 0) {
57662
+ writeLocalFailure(ctx.deps, resumed.failure ?? { code: "PROMOTE_OUTCOME_UNRESOLVED", message: "The resume did not complete." }, ctx.json);
57663
+ return resumed.exit;
57664
+ }
57665
+ const rebound2 = await rebindAfterImport(ctx, toKey, {
57666
+ linkedWalletId: resumed.adopted?.linkedWalletId ?? null,
57667
+ address: existing.address,
57668
+ label: existing.label,
57669
+ remoteAuthority: resumed.adopted?.remoteAuthority ?? null,
57670
+ importedTo: existing.tee?.boundKeyPrefix ?? null
57671
+ }, await callingKeyPrefixFor(ctx));
57672
+ if (ctx.json) {
57673
+ writeJson(ctx.deps, {
57674
+ ok: rebound2.exit === 0,
57675
+ mode: "resume",
57676
+ address: existing.address,
57677
+ lifecycle: "enabled",
57678
+ importCalls: 0,
57679
+ ...rebound2.json
57680
+ });
57681
+ } else {
57682
+ ctx.deps.stdout.write(`Resumed ${existing.address}: grant adopted; no re-import.
57683
+ `);
57684
+ }
57685
+ return rebound2.exit;
55189
57686
  }
55190
57687
  if (sweepTo === undefined || rpcUrl2 === undefined) {
55191
57688
  return usage(ctx, "Usage: candle vault promote --in-place <label> --sweep-to <label> --rpc-url <url> [--label] [--accept-unknown-exposure]");
@@ -55197,7 +57694,12 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55197
57694
  return usage(ctx, "A resume of promote takes no --sweep-to (exit 2).");
55198
57695
  }
55199
57696
  await refuseWithoutRoom(ctx);
55200
- const controlledBy = await readControlledBy(ctx);
57697
+ const live = await readControlledBy(ctx);
57698
+ const controlledBy = toKey === undefined ? live : withToKey(live, {
57699
+ keyPrefix: toKey.target.keyPrefix,
57700
+ label: toKey.target.label,
57701
+ warnings: targetWarnings(toKey.target)
57702
+ });
55201
57703
  const rpc2 = createSolanaRpc(rpcUrl2, ctx.deps.fetch);
55202
57704
  const host = new URL(rpcUrl2).host;
55203
57705
  await displayHoldings(ctx, rpc2, first.subject.address);
@@ -55267,24 +57769,37 @@ async function promoteInPlace(ctx, parsed, subjectLabel) {
55267
57769
  writeLocalFailure(ctx.deps, failure, ctx.json);
55268
57770
  return code;
55269
57771
  }
57772
+ let rebound;
57773
+ if (toKey !== undefined && imported.submitted !== undefined) {
57774
+ const importedTo = imported.submitted.boundKeyPrefix ?? null;
57775
+ rebound = await rebindAfterImport(ctx, toKey, {
57776
+ linkedWalletId: imported.submitted.id ?? null,
57777
+ address: subject.address,
57778
+ label: parsed.values["--label"] ?? subject.label,
57779
+ remoteAuthority: imported.submitted.remoteAuthority ?? null,
57780
+ importedTo
57781
+ }, controlledBy.keyPrefix);
57782
+ }
57783
+ const exit = Math.max(code, rebound?.exit ?? 0);
55270
57784
  if (ctx.json) {
55271
57785
  const reopened = hold(await reopenFromDisk(path, opened.reopen, vault));
55272
57786
  const updated = reopened.index.entries.find((e) => e.id === subject.id);
55273
57787
  writeJson(ctx.deps, {
55274
- ok: code === 0 || code === 3,
57788
+ ok: (code === 0 || code === 3) && (rebound === undefined || rebound.exit === 0),
55275
57789
  mode: "in-place",
55276
57790
  address: subject.address,
55277
57791
  vaultDestination: destination.address,
55278
57792
  lifecycle: updated?.tee?.lifecycle ?? null,
55279
57793
  linkedWalletId: updated?.linkedWalletId ?? null,
55280
57794
  controlledBy: controlledByJson(controlledBy),
55281
- authorities: authoritiesJson(roles)
57795
+ authorities: authoritiesJson(roles),
57796
+ ...rebound?.json ?? {}
55282
57797
  });
55283
57798
  } else if (code === 0 || code === 3) {
55284
57799
  ctx.deps.stdout.write(`Promoted ${subject.address} in place (sweep to ${destination.address}). This address never returns to cold.
55285
57800
  `);
55286
57801
  }
55287
- return code;
57802
+ return exit;
55288
57803
  });
55289
57804
  }
55290
57805
  async function resumePromote(ctx, vault, entry, reopen, path, hold, confirmations) {
@@ -55870,12 +58385,12 @@ function parseValueUsdCell(raw) {
55870
58385
  const value = Number(cleaned);
55871
58386
  return Number.isFinite(value) ? value : undefined;
55872
58387
  }
55873
- function formatUsd(value) {
58388
+ function formatUsd2(value) {
55874
58389
  const [whole, fraction] = value.toFixed(2).split(".");
55875
58390
  const grouped = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
55876
58391
  return `$${grouped}.${fraction ?? "00"}`;
55877
58392
  }
55878
- function formatSol3(lamports) {
58393
+ function formatSol4(lamports) {
55879
58394
  const whole = lamports / 1000000000n;
55880
58395
  const fraction = lamports % 1000000000n;
55881
58396
  return `${whole}.${fraction.toString().padStart(9, "0").slice(0, 6)}`;
@@ -55904,14 +58419,37 @@ init_promote_support();
55904
58419
  init_signer_roles();
55905
58420
  init_store();
55906
58421
  init_vault_support();
55907
- var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--token-holdings] [--accept-unknown-exposure]";
58422
+ var USAGE_LINE = "Usage: candle vault promote-batch --pairs-from <file> --rpc-url <url> [--to-key <label|prefix>] [--token-holdings] [--accept-unknown-exposure]";
55908
58423
  var rowObserver = null;
58424
+ function splitRebindable(results) {
58425
+ const wallets2 = [];
58426
+ const notRebindable = [];
58427
+ for (const row of results) {
58428
+ if (row.linkedWalletId !== null && row.remoteAuthority === "verified-active") {
58429
+ wallets2.push({ id: row.linkedWalletId, address: row.address, label: row.label });
58430
+ } else {
58431
+ notRebindable.push({
58432
+ address: row.address,
58433
+ label: row.label,
58434
+ reason: row.linkedWalletId === null ? `no linked wallet id (${row.lifecycle})` : `remote authority is ${row.remoteAuthority ?? "unknown"}, not verified-active`
58435
+ });
58436
+ }
58437
+ }
58438
+ return { wallets: wallets2, notRebindable };
58439
+ }
58440
+ function keysWithRebind(results, phase, run) {
58441
+ return results.map((row) => ({
58442
+ ...row,
58443
+ boundKeyPrefix: finalBoundKey({ id: row.linkedWalletId, importedTo: phase.importedTo.get(row.address) ?? null }, run, phase.toKey.target.keyPrefix),
58444
+ rebind: walletRebindJson({ id: row.linkedWalletId, remoteAuthority: row.remoteAuthority }, run)
58445
+ }));
58446
+ }
55909
58447
  async function vaultPromoteBatch(args, ctx) {
55910
58448
  if (args.some((arg) => arg === "--from" || arg.startsWith("--from="))) {
55911
58449
  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
58450
  }
55913
58451
  const parsed = parseArgs(args, {
55914
- valueFlags: ["--pairs-from", "--rpc-url", "--keystore"],
58452
+ valueFlags: ["--pairs-from", "--rpc-url", "--keystore", "--to-key"],
55915
58453
  booleanFlags: ["--token-holdings", "--accept-unknown-exposure", "--accept-older-copy"],
55916
58454
  pathFlags: ["--keystore", "--pairs-from"]
55917
58455
  });
@@ -55922,6 +58460,9 @@ async function vaultPromoteBatch(args, ctx) {
55922
58460
  const pairsFile = parsed.values["--pairs-from"];
55923
58461
  if (pairsFile === undefined)
55924
58462
  return usage(ctx, USAGE_LINE);
58463
+ const toKeyRaw = parsed.values["--to-key"];
58464
+ if (toKeyRaw !== undefined && toKeyRaw.trim().length === 0)
58465
+ return usage(ctx, "--to-key needs a label or a prefix.");
55925
58466
  if (!refuseEnvPassphrase(ctx))
55926
58467
  return 1;
55927
58468
  if (!requireTty(ctx, "vault promote-batch"))
@@ -55946,6 +58487,13 @@ async function vaultPromoteBatch(args, ctx) {
55946
58487
  if (!parsedFile.ok)
55947
58488
  return usage(ctx, renderPhaseAFindings(pairsFile, parsedFile.findings));
55948
58489
  const { rows, hasValueUsd } = parsedFile;
58490
+ let toKey;
58491
+ if (toKeyRaw !== undefined) {
58492
+ const preflight = await preflightToKey(ctx, toKeyRaw, { labels: rows.map((row) => row.label) });
58493
+ if (!preflight.ok)
58494
+ return preflight.exit;
58495
+ toKey = { target: preflight.target, deviceToken: preflight.deviceToken };
58496
+ }
55949
58497
  return runVaultCommand(ctx, async ({ hold }) => {
55950
58498
  const raw = await requireVaultRaw(ctx, resolvedVault);
55951
58499
  const opened = await unlockInteractively(ctx, path, raw, {
@@ -55981,7 +58529,17 @@ async function vaultPromoteBatch(args, ctx) {
55981
58529
  if (roomRefusal !== null)
55982
58530
  throw roomRefusal;
55983
58531
  const room = roomRead.room;
55984
- const controlledBy = await readControlledBy(ctx);
58532
+ const live = await readControlledBy(ctx);
58533
+ const controlledBy = toKey === undefined ? live : withToKey(live, {
58534
+ keyPrefix: toKey.target.keyPrefix,
58535
+ label: toKey.target.label,
58536
+ warnings: targetWarnings(toKey.target)
58537
+ });
58538
+ const rebindPhase = toKey === undefined ? undefined : { toKey, callingKeyPrefix: live.keyPrefix, importedTo: new Map };
58539
+ for (const item of planned) {
58540
+ if (item.kind === "skip")
58541
+ rebindPhase?.importedTo.set(item.subject.address, item.subject.tee?.boundKeyPrefix ?? null);
58542
+ }
55985
58543
  const addresses = planned.map((item) => item.subject.address);
55986
58544
  const rpc2 = createSolanaRpc(rpcUrl2, deps.fetch);
55987
58545
  const host = new URL(rpcUrl2).host;
@@ -56040,28 +58598,58 @@ ${footer}
56040
58598
  });
56041
58599
  }
56042
58600
  if (acting.length === 0) {
56043
- deps.stderr.write(`
58601
+ const roles2 = {
58602
+ checked: [...ROLE_GROUP_IDS],
58603
+ notChecked: [],
58604
+ found: [],
58605
+ requests: 0,
58606
+ planned: 0,
58607
+ rateLimited: 0,
58608
+ elapsedMs: 0
58609
+ };
58610
+ const skipped = planned.map((item) => skippedResult(item));
58611
+ const rebindable = rebindPhase === undefined ? undefined : splitRebindable(skipped);
58612
+ if (rebindPhase === undefined || rebindable === undefined || rebindable.wallets.length === 0) {
58613
+ deps.stderr.write(`
56044
58614
  ${table}
56045
58615
 
56046
58616
  ${footer}
56047
58617
  Nothing to do: every row already landed.
56048
58618
  `);
58619
+ return finish(ctx, {
58620
+ file: pairsFile,
58621
+ rows: rows.length,
58622
+ keys: skipped,
58623
+ destinations,
58624
+ exit: 0,
58625
+ controlledBy,
58626
+ roles: roles2
58627
+ });
58628
+ }
58629
+ const n = rebindable.wallets.length;
58630
+ deps.stderr.write(`
58631
+ ${table}
58632
+
58633
+ ${footer}
58634
+ Every row already landed; ${n} wallet${n === 1 ? "" : "s"} to rebind.
58635
+ `);
58636
+ deps.stderr.write(`
58637
+ ${renderControlledBy(controlledBy, n)}
58638
+ `);
58639
+ const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to move ${n === 1 ? "this wallet" : `these ${n} wallets`} to ${rebindPhase.toKey.target.keyPrefix}: `);
58640
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
58641
+ 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.` });
58642
+ }
58643
+ const rebound2 = await runRebindPhase(ctx, rebindPhase, skipped);
56049
58644
  return finish(ctx, {
56050
58645
  file: pairsFile,
56051
58646
  rows: rows.length,
56052
- keys: planned.map((item) => skippedResult(item)),
58647
+ keys: skipped,
56053
58648
  destinations,
56054
58649
  exit: 0,
56055
58650
  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
- }
58651
+ roles: roles2,
58652
+ rebound: rebound2
56065
58653
  });
56066
58654
  }
56067
58655
  const checked = roles;
@@ -56152,6 +58740,7 @@ ${renderControlledBy(controlledBy, acting.length)}
56152
58740
  worst = Math.max(worst, imported.exit);
56153
58741
  done += 1;
56154
58742
  last = { address: subject2.address, keyId: subject2.id };
58743
+ rebindPhase?.importedTo.set(subject2.address, imported.submitted.boundKeyPrefix ?? null);
56155
58744
  results.push({
56156
58745
  line: row2.line,
56157
58746
  label: subject2.label,
@@ -56191,6 +58780,7 @@ ${renderControlledBy(controlledBy, acting.length)}
56191
58780
  await rowObserver?.({ line: row.line, stage: "resumed", vault: current });
56192
58781
  done += 1;
56193
58782
  last = { address: subject.address, keyId: subject.id };
58783
+ rebindPhase?.importedTo.set(subject.address, subject.tee?.boundKeyPrefix ?? null);
56194
58784
  results.push({
56195
58785
  line: row.line,
56196
58786
  label: subject.label,
@@ -56216,6 +58806,20 @@ ${renderControlledBy(controlledBy, acting.length)}
56216
58806
  }
56217
58807
  if (stopped !== undefined) {
56218
58808
  reportPartial(ctx, { landed: done, acting: acting.length, failure: stopped });
58809
+ let notReached;
58810
+ if (rebindPhase !== undefined) {
58811
+ const { wallets: wallets2, notRebindable } = splitRebindable(results);
58812
+ notReached = {
58813
+ target: rebindPhase.toKey.target,
58814
+ callingKeyPrefix: rebindPhase.callingKeyPrefix,
58815
+ wallets: wallets2,
58816
+ notRebindable
58817
+ };
58818
+ const report = renderRebindReport(notReached);
58819
+ if (report.length > 0)
58820
+ deps.stderr.write(`${report}
58821
+ `);
58822
+ }
56219
58823
  if (ctx.json) {
56220
58824
  writeJson(deps, {
56221
58825
  ok: false,
@@ -56226,13 +58830,14 @@ ${renderControlledBy(controlledBy, acting.length)}
56226
58830
  resumed: results.filter((r) => r.state === "resumed").length,
56227
58831
  skipped: results.filter((r) => r.state === "skipped").length,
56228
58832
  destinations: destinations.map(({ label, address, keys }) => ({ label, address, keys })),
56229
- keys: results,
58833
+ keys: rebindPhase === undefined ? results : keysWithRebind(results, rebindPhase, undefined),
56230
58834
  failedLine: stopped.line,
56231
58835
  code: stopped.code,
56232
58836
  message: stopped.message,
56233
58837
  ...stopped.suggestion !== undefined ? { suggestion: stopped.suggestion } : {},
56234
58838
  controlledBy: controlledByJson(controlledBy),
56235
- authorities: authoritiesJson(checked)
58839
+ authorities: authoritiesJson(checked),
58840
+ ...notReached !== undefined ? rebindJson(notReached) : {}
56236
58841
  });
56237
58842
  } else {
56238
58843
  deps.stderr.write(`${stopped.message}${stopped.suggestion ? ` ${stopped.suggestion}` : ""}
@@ -56242,6 +58847,7 @@ ${renderControlledBy(controlledBy, acting.length)}
56242
58847
  }
56243
58848
  if (last !== undefined)
56244
58849
  await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx);
58850
+ const rebound = rebindPhase === undefined ? undefined : await runRebindPhase(ctx, rebindPhase, results);
56245
58851
  return finish(ctx, {
56246
58852
  file: pairsFile,
56247
58853
  rows: rows.length,
@@ -56249,10 +58855,27 @@ ${renderControlledBy(controlledBy, acting.length)}
56249
58855
  destinations,
56250
58856
  exit: worst,
56251
58857
  controlledBy,
56252
- roles: checked
58858
+ roles: checked,
58859
+ ...rebound !== undefined ? { rebound } : {}
56253
58860
  });
56254
58861
  });
56255
58862
  }
58863
+ async function runRebindPhase(ctx, phase, results) {
58864
+ const { wallets: wallets2, notRebindable } = splitRebindable(results);
58865
+ const run = wallets2.length > 0 ? await rebindPromoted(ctx, phase.toKey.deviceToken, phase.toKey.target.keyPrefix, wallets2) : undefined;
58866
+ const input = {
58867
+ target: phase.toKey.target,
58868
+ callingKeyPrefix: phase.callingKeyPrefix,
58869
+ wallets: wallets2,
58870
+ run,
58871
+ notRebindable
58872
+ };
58873
+ const report = renderRebindReport(input);
58874
+ if (report.length > 0)
58875
+ ctx.deps.stderr.write(`${report}
58876
+ `);
58877
+ return { phase, run, input };
58878
+ }
56256
58879
  async function verifySubjectSecret(vault, subject) {
56257
58880
  const secret = await decryptKey(vault, subject.id);
56258
58881
  try {
@@ -56375,7 +58998,7 @@ function renderBatchTable(planned, opts) {
56375
58998
  const authority = item.kind === "skip" ? "" : opts.roles === undefined ? "?" : authorityCell(address, opts.roles);
56376
58999
  const cells = [String(item.row.line), state, item.subject.label, address, destination, authority];
56377
59000
  const lamports = opts.lamports?.get(address);
56378
- cells.push(lamports === undefined ? "unread" : formatSol3(lamports));
59001
+ cells.push(lamports === undefined ? "unread" : formatSol4(lamports));
56379
59002
  if (opts.tokenCounts !== undefined)
56380
59003
  cells.push(String(opts.tokenCounts.get(address) ?? 0));
56381
59004
  if (opts.hasValueUsd)
@@ -56414,7 +59037,7 @@ function renderFooter(opts) {
56414
59037
  else
56415
59038
  total += value;
56416
59039
  }
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)` : ""}`);
59040
+ 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
59041
  }
56419
59042
  const accepted = opts.acting.filter((item) => item.kind === "promote" && item.acceptedUnknown);
56420
59043
  if (accepted.length > 0) {
@@ -56439,10 +59062,12 @@ function finish(ctx, opts) {
56439
59062
  const resumed = opts.keys.filter((r) => r.state === "resumed").length;
56440
59063
  const skipped = opts.keys.filter((r) => r.state === "skipped").length;
56441
59064
  const unverified = opts.keys.filter((r) => r.state !== "skipped" && r.remoteAuthority !== "verified-active");
56442
- const complete = opts.exit === 0;
59065
+ const rebindFailed = opts.rebound?.run !== undefined && !opts.rebound.run.ok;
59066
+ const exit = rebindFailed ? Math.max(opts.exit, 1) : opts.exit;
59067
+ const complete = exit === 0;
56443
59068
  if (ctx.json) {
56444
59069
  writeJson(ctx.deps, {
56445
- ok: true,
59070
+ ok: !rebindFailed,
56446
59071
  complete,
56447
59072
  file: opts.file,
56448
59073
  rows: opts.rows,
@@ -56450,11 +59075,12 @@ function finish(ctx, opts) {
56450
59075
  resumed,
56451
59076
  skipped,
56452
59077
  destinations: opts.destinations.map(({ label, address, keys }) => ({ label, address, keys })),
56453
- keys: opts.keys,
59078
+ keys: opts.rebound === undefined ? opts.keys : keysWithRebind(opts.keys, opts.rebound.phase, opts.rebound.run),
56454
59079
  controlledBy: controlledByJson(opts.controlledBy),
56455
- authorities: authoritiesJson(opts.roles)
59080
+ authorities: authoritiesJson(opts.roles),
59081
+ ...opts.rebound !== undefined ? rebindJson(opts.rebound.input) : {}
56456
59082
  });
56457
- return opts.exit;
59083
+ return exit;
56458
59084
  }
56459
59085
  const parts = [];
56460
59086
  if (promoted > 0)
@@ -56465,11 +59091,19 @@ function finish(ctx, opts) {
56465
59091
  parts.push(`${skipped} already promoted (skipped)`);
56466
59092
  ctx.deps.stdout.write(`${parts.join("; ")}.
56467
59093
  `);
56468
- if (!complete) {
59094
+ if (unverified.length > 0) {
56469
59095
  ctx.deps.stdout.write(`${unverified.length} import${unverified.length === 1 ? "" : "s"} did not report verified-active: ${unverified.map((r) => r.address).join(", ")}
56470
59096
  `);
56471
59097
  }
56472
- return opts.exit;
59098
+ if (opts.rebound !== undefined) {
59099
+ const { input, run } = opts.rebound;
59100
+ const moved = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "rebound").length;
59101
+ const already = input.wallets.filter((w) => run?.outcomes.get(w.id)?.state === "unchanged").length;
59102
+ const pending = input.wallets.length - moved - already;
59103
+ 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` : ""}.
59104
+ `);
59105
+ }
59106
+ return exit;
56473
59107
  }
56474
59108
 
56475
59109
  // src/commands/vault-rename.ts
@@ -57555,8 +60189,78 @@ function describeEnvelope(envelope, facts) {
57555
60189
 
57556
60190
  // src/commands/vault-transfer.ts
57557
60191
  init_args();
60192
+ init_deps();
57558
60193
  init_promote_support();
57559
60194
  init_store();
60195
+
60196
+ // src/vault/tee-transfer.ts
60197
+ init_errors();
60198
+ function assertNoPendingSweep(entry) {
60199
+ const pending = entry.tee?.sweepPending?.length ?? 0;
60200
+ if (pending === 0)
60201
+ return;
60202
+ 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>` });
60203
+ }
60204
+ async function readTeeServerState(ctx, entry, apiKey) {
60205
+ if (entry.linkedWalletId === undefined) {
60206
+ return { read: false, reason: "this wallet has no linked wallet id recorded" };
60207
+ }
60208
+ if (apiKey === undefined)
60209
+ return { read: false, reason: "no API key for this profile" };
60210
+ try {
60211
+ const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/lifecycle`, {
60212
+ auth: "key",
60213
+ credentials: { apiKey },
60214
+ apiUrl: ctx.apiUrl,
60215
+ fetch: ctx.deps.fetch,
60216
+ env: ctx.deps.env
60217
+ });
60218
+ if (!result.ok)
60219
+ return { read: false, reason: result.message };
60220
+ const state = result.body.state;
60221
+ if (typeof state !== "string")
60222
+ return { read: false, reason: "the lifecycle response carried no state" };
60223
+ return { read: true, state };
60224
+ } catch (error) {
60225
+ return { read: false, reason: error instanceof Error ? error.message : String(error) };
60226
+ }
60227
+ }
60228
+ function serverStateNotice(state) {
60229
+ if (!state.read) {
60230
+ return `Could not read this wallet's Candle state (${state.reason}); proceeding. An agent may be trading it.`;
60231
+ }
60232
+ if (state.state === "enabled") {
60233
+ return "Warning: this wallet is enabled, so an agent may be trading it now; a trade may fail if this transfer leaves too little.";
60234
+ }
60235
+ return;
60236
+ }
60237
+ async function reportTransferActivity(ctx, apiKey, signature) {
60238
+ const unseen = "Candle's history will not show this transfer.";
60239
+ if (apiKey === undefined)
60240
+ return { outcome: "no-api-key", line: `No API key for this profile, so ${unseen}` };
60241
+ try {
60242
+ const result = await apiRequest("/api/v1/activity/report", {
60243
+ method: "POST",
60244
+ body: { chain: "solana", signature },
60245
+ auth: "key",
60246
+ credentials: { apiKey },
60247
+ apiUrl: ctx.apiUrl,
60248
+ fetch: ctx.deps.fetch,
60249
+ env: ctx.deps.env
60250
+ });
60251
+ if (result.ok)
60252
+ return { outcome: "reported", line: "Reported to Candle's history." };
60253
+ if (result.code === "SCOPE_MISSING") {
60254
+ return { outcome: "scope-missing", line: `This profile's API key lacks activity:write, so ${unseen}` };
60255
+ }
60256
+ return { outcome: "failed", line: `Could not report this transfer (${result.message}); ${unseen}` };
60257
+ } catch (error) {
60258
+ const reason = error instanceof Error ? error.message : String(error);
60259
+ return { outcome: "failed", line: `Could not report this transfer (${reason}); ${unseen}` };
60260
+ }
60261
+ }
60262
+
60263
+ // src/commands/vault-transfer.ts
57560
60264
  init_vault_support();
57561
60265
  async function vaultTransfer(args, ctx) {
57562
60266
  const parsed = parseArgs(args, {
@@ -57597,11 +60301,17 @@ async function vaultTransfer(args, ctx) {
57597
60301
  promptText: "Vault passphrase (input hidden): "
57598
60302
  });
57599
60303
  const vault = hold(opened.vault);
57600
- const fromEntry = findVaultRoleEntry(vault.index, fromLabel);
60304
+ const fromEntry = findTransferSource(vault.index, fromLabel);
57601
60305
  if (fromEntry === undefined) {
57602
- return usage(ctx, `No vault key matches --from ${fromLabel}.`);
60306
+ return usage(ctx, `No vault key or promoted wallet matches --from ${fromLabel}.`);
57603
60307
  }
57604
- assertVaultSigner(fromEntry);
60308
+ assertTransferSigner(fromEntry);
60309
+ const promoted = fromEntry.role === "tee-wallet";
60310
+ if (promoted)
60311
+ assertNoPendingSweep(fromEntry);
60312
+ const note = (line) => (ctx.json ? ctx.deps.stderr : ctx.deps.stdout).write(`${line}
60313
+ `);
60314
+ const apiKey = promoted ? await resolveApiKey(ctx.deps, ctx.profile) : undefined;
57605
60315
  const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], rpcUrl2, ctx);
57606
60316
  if (reconciled !== null)
57607
60317
  return reconciled;
@@ -57615,11 +60325,25 @@ async function vaultTransfer(args, ctx) {
57615
60325
  });
57616
60326
  const feeQuote = await quoteTransferFee(rpcUrl2, ctx.deps.fetch, plan.from, plan.instructions);
57617
60327
  displayTransferPlan(ctx, plan, feeQuote);
60328
+ if (promoted) {
60329
+ const notice = serverStateNotice(await readTeeServerState(ctx, fromEntry, apiKey));
60330
+ if (notice !== undefined)
60331
+ note(notice);
60332
+ }
57618
60333
  await confirmLastSix(ctx, to, "the destination");
57619
60334
  await opened.confirm(`sign transfer of ${plan.amount} ${plan.asset} to ${to}`);
57620
60335
  const secret = await decryptKey(vault, fromEntry.id);
57621
60336
  try {
57622
60337
  const result = await signAndBroadcastTransfer({ ctx, rpcUrl: rpcUrl2, secret64: secret, plan });
60338
+ let activityReport;
60339
+ if (promoted && result.finalized) {
60340
+ const report = await reportTransferActivity(ctx, apiKey, result.signature);
60341
+ activityReport = report.outcome;
60342
+ note(report.line);
60343
+ } else if (promoted) {
60344
+ activityReport = "not-finalized";
60345
+ note("Not reported to Candle's history: finality is not yet confirmed.");
60346
+ }
57623
60347
  if (ctx.json) {
57624
60348
  writeJson(ctx.deps, {
57625
60349
  ok: result.finalized,
@@ -57629,7 +60353,8 @@ async function vaultTransfer(args, ctx) {
57629
60353
  amount: plan.amount,
57630
60354
  asset: plan.asset,
57631
60355
  amountRaw: plan.amountRaw.toString(),
57632
- finalized: result.finalized
60356
+ finalized: result.finalized,
60357
+ ...activityReport !== undefined ? { activityReport } : {}
57633
60358
  });
57634
60359
  } else {
57635
60360
  ctx.deps.stdout.write(result.finalized ? `Transferred ${plan.amount} ${plan.asset} to ${to}: ${result.signature}
@@ -57648,7 +60373,7 @@ init_args();
57648
60373
  init_release();
57649
60374
  import { dirname as dirname10, join as join11 } from "node:path";
57650
60375
  init_render();
57651
- var USAGE2 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
60376
+ var USAGE3 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
57652
60377
  async function resolveIdentity(deps, bundlePath, flag) {
57653
60378
  if (flag)
57654
60379
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -57680,19 +60405,19 @@ async function verify(args, ctx) {
57680
60405
  });
57681
60406
  if ("error" in parsed) {
57682
60407
  writeUsageFailure(deps, `${parsed.error}
57683
- ${USAGE2}`, json);
60408
+ ${USAGE3}`, json);
57684
60409
  return 2;
57685
60410
  }
57686
60411
  const file = parsed.positionals[0];
57687
60412
  if (parsed.positionals.length !== 1 || file === undefined) {
57688
60413
  writeUsageFailure(deps, `verify takes exactly one file.
57689
- ${USAGE2}`, json);
60414
+ ${USAGE3}`, json);
57690
60415
  return 2;
57691
60416
  }
57692
60417
  const bundlePath = parsed.values["--bundle"];
57693
60418
  if (!bundlePath) {
57694
60419
  writeUsageFailure(deps, `--bundle is required.
57695
- ${USAGE2}`, json);
60420
+ ${USAGE3}`, json);
57696
60421
  return 2;
57697
60422
  }
57698
60423
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -57706,7 +60431,7 @@ ${USAGE2}`, json);
57706
60431
  }
57707
60432
  if (resolved.kind === "absent") {
57708
60433
  writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
57709
- ${USAGE2}`, json);
60434
+ ${USAGE3}`, json);
57710
60435
  return 2;
57711
60436
  }
57712
60437
  const identity = resolved.uri;
@@ -57747,6 +60472,177 @@ function messageOf2(error) {
57747
60472
  return error instanceof Error ? error.message : String(error);
57748
60473
  }
57749
60474
 
60475
+ // src/commands/wallets-trust.ts
60476
+ init_args();
60477
+ init_deps();
60478
+ init_profiles();
60479
+ init_render();
60480
+ init_promote_support();
60481
+ var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
60482
+ var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
60483
+ var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
60484
+ var DEVICE_TOKEN_REQUIRED2 = {
60485
+ code: "DEVICE_TOKEN_REQUIRED",
60486
+ message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
60487
+ suggestion: "Run: candle auth login"
60488
+ };
60489
+ function sessionLinkedLine(rows) {
60490
+ const n = rows.length;
60491
+ const it = n === 1 ? "it" : "them";
60492
+ const names = rows.map((row) => row.label ?? row.address).join(", ");
60493
+ return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
60494
+ }
60495
+ function nothingToChangeLine(shown, trusted) {
60496
+ const session = shown.sessionLinked ?? [];
60497
+ const unchanged = shown.unchanged;
60498
+ if (session.length > 0 && unchanged.length === 0) {
60499
+ const one = session.length === 1;
60500
+ return `Nothing to change: ${one ? "that wallet is" : `those ${session.length} wallets are`} always yours while linked. Revoke ${one ? "it" : "them"} to remove ${one ? "it" : "them"}.
60501
+ `;
60502
+ }
60503
+ const bits = [];
60504
+ if (session.length > 0)
60505
+ bits.push(sessionLinkedLine(session).replace(/\.$/, ""));
60506
+ if (unchanged.length > 0) {
60507
+ bits.push(unchanged.length === 1 ? `that wallet is already ${trusted ? "trusted" : "untrusted"}` : `all ${unchanged.length} wallets are already ${trusted ? "trusted" : "untrusted"}`);
60508
+ }
60509
+ if (bits.length === 0)
60510
+ return `Nothing to change.
60511
+ `;
60512
+ return `Nothing to change: ${bits.join("; ")}.
60513
+ `;
60514
+ }
60515
+ function writeTrustFailure(ctx, result) {
60516
+ const { deps, apiUrl, json } = ctx;
60517
+ let code = result.code;
60518
+ let message = result.message;
60519
+ let suggestion;
60520
+ if (result.status === 404) {
60521
+ code = "TRUST_UNSUPPORTED";
60522
+ message = "This Candle API does not support trusting wallets yet; nothing changed.";
60523
+ } else {
60524
+ const error = result.raw?.error;
60525
+ if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
60526
+ const ids = error.matches.map((m) => String(m.id)).join(", ");
60527
+ suggestion = `Name one of them by id or address: ${ids}`;
60528
+ } else if (error?.reason === "not_found") {
60529
+ suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
60530
+ }
60531
+ }
60532
+ const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
60533
+ if (json) {
60534
+ deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
60535
+ `);
60536
+ } else {
60537
+ deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
60538
+ `);
60539
+ }
60540
+ return 1;
60541
+ }
60542
+ function trustTable(rows) {
60543
+ return renderTable(["line", "label", "wallet", "address"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address]));
60544
+ }
60545
+ async function setTrust(args, ctx, trusted) {
60546
+ const { deps, apiUrl, json } = ctx;
60547
+ const usage5 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
60548
+ const command = trusted ? "wallets trust" : "wallets untrust";
60549
+ const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
60550
+ if ("error" in parsed) {
60551
+ writeUsageFailure(deps, parsed.error, json);
60552
+ return 2;
60553
+ }
60554
+ if (parsed.positionals.length === 0) {
60555
+ writeUsageFailure(deps, `Name at least one wallet. ${usage5}`, json);
60556
+ return 2;
60557
+ }
60558
+ const skipConfirm = !trusted && parsed.booleans.has("--yes");
60559
+ await printIdentity(ctx);
60560
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
60561
+ if (!deviceToken) {
60562
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
60563
+ return 1;
60564
+ }
60565
+ if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
60566
+ writeLocalFailure(deps, {
60567
+ code: "TRUST_REQUIRES_TTY",
60568
+ message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
60569
+ suggestion: trusted ? "Run it in an interactive shell; there is no flag and no environment variable for confirm." : "Run it in an interactive shell, or pass --yes to untrust without the prompt."
60570
+ }, json);
60571
+ return 1;
60572
+ }
60573
+ const call = (body) => apiRequest(TRUST_PATH, {
60574
+ method: "POST",
60575
+ body,
60576
+ auth: "device",
60577
+ credentials: { deviceToken },
60578
+ apiUrl,
60579
+ fetch: deps.fetch,
60580
+ env: deps.env
60581
+ });
60582
+ const preview = await call({ dryRun: true, wallets: parsed.positionals, trusted });
60583
+ if (!preview.ok)
60584
+ return writeTrustFailure(ctx, preview);
60585
+ const shown = preview.body;
60586
+ const changing = shown.changed;
60587
+ if (changing.length === 0) {
60588
+ if (json) {
60589
+ deps.stdout.write(`${JSON.stringify({ ...shown, command })}
60590
+ `);
60591
+ } else {
60592
+ deps.stdout.write(nothingToChangeLine(shown, trusted));
60593
+ }
60594
+ return 0;
60595
+ }
60596
+ const n = changing.length;
60597
+ const screen = [
60598
+ trustTable(changing),
60599
+ ...shown.unchanged.length > 0 ? [
60600
+ `${shown.unchanged.length} already ${trusted ? "trusted" : "untrusted"}: ${shown.unchanged.map((row) => row.label ?? row.address).join(", ")}`
60601
+ ] : [],
60602
+ ...(shown.sessionLinked?.length ?? 0) > 0 ? [sessionLinkedLine(shown.sessionLinked ?? [])] : [],
60603
+ "",
60604
+ trusted ? `${n === 1 ? "This wallet" : `These ${n} wallets`} will be trusted: your agents can move funds into ${n === 1 ? "it" : "them"} with no cap, like a wallet you linked while signed in.` : `${n === 1 ? "This wallet" : `These ${n} wallets`} will no longer be trusted: moving funds into ${n === 1 ? "it" : "them"} will need the withdrawal allowlist again.`,
60605
+ ""
60606
+ ];
60607
+ deps.stderr.write(`${screen.join(`
60608
+ `)}
60609
+ `);
60610
+ if (!skipConfirm) {
60611
+ const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${trusted ? "trust" : "untrust"} ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
60612
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
60613
+ writeLocalFailure(deps, {
60614
+ code: "TRUST_NOT_ACKNOWLEDGED",
60615
+ message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
60616
+ suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
60617
+ }, json);
60618
+ return 1;
60619
+ }
60620
+ }
60621
+ const committed = await call({ walletIds: changing.map((row) => row.id), trusted });
60622
+ if (!committed.ok)
60623
+ return writeTrustFailure(ctx, committed);
60624
+ const result = committed.body;
60625
+ if (json) {
60626
+ deps.stdout.write(`${JSON.stringify({ ...result, command })}
60627
+ `);
60628
+ return result.skipped.length > 0 ? 1 : 0;
60629
+ }
60630
+ const done = result.changed.length;
60631
+ deps.stdout.write(`${trusted ? "Trusted" : "Untrusted"} ${done} wallet${done === 1 ? "" : "s"}.
60632
+ `);
60633
+ for (const skip of result.skipped) {
60634
+ deps.stderr.write(`Skipped ${skip.walletId}: ${skip.reason === "revoked" ? "revoked" : "not on this account"}.
60635
+ `);
60636
+ }
60637
+ return result.skipped.length > 0 ? 1 : 0;
60638
+ }
60639
+ function walletsTrust(args, ctx) {
60640
+ return setTrust(args, ctx, true);
60641
+ }
60642
+ function walletsUntrust(args, ctx) {
60643
+ return setTrust(args, ctx, false);
60644
+ }
60645
+
57750
60646
  // src/config.ts
57751
60647
  import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
57752
60648
  import { homedir as homedir5 } from "node:os";
@@ -58062,11 +60958,15 @@ function extractGlobalFlags(argv) {
58062
60958
  }
58063
60959
  var COMMANDS = {
58064
60960
  swap: { bare: swap, subcommands: { status: swapStatus } },
60961
+ transfer: { bare: transfer },
58065
60962
  launch: { bare: launch },
60963
+ pnl: { bare: pnl },
60964
+ portfolio: { bare: portfolio },
60965
+ lp: { subcommands: { pools: lpPools, add: lpAdd, positions: lpPositions, remove: lpRemove, claim: lpClaim } },
58066
60966
  auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
58067
60967
  keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
58068
60968
  wallets: {
58069
- subcommands: { import: walletsImport, revoke: walletsRevoke },
60969
+ subcommands: { import: walletsImport, revoke: walletsRevoke, trust: walletsTrust, untrust: walletsUntrust },
58070
60970
  bare: wallets
58071
60971
  },
58072
60972
  vault: {