@candledottv/cli 0.13.9 → 0.13.11

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 +6 -2
  2. package/dist/index.js +955 -114
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3476,7 +3476,7 @@ var init_terminal_theme = __esm(() => {
3476
3476
  });
3477
3477
 
3478
3478
  // src/version.ts
3479
- var CLI_VERSION = "0.13.9";
3479
+ var CLI_VERSION = "0.13.11";
3480
3480
 
3481
3481
  // src/presentation.ts
3482
3482
  function terminalStyle(deps, stream, machine = false) {
@@ -14140,6 +14140,7 @@ __export(exports_store, {
14140
14140
  serializeVault: () => serializeVault,
14141
14141
  sealKeyBlob: () => sealKeyBlob,
14142
14142
  sealIndex: () => sealIndex,
14143
+ reopenWithHeldDek: () => reopenWithHeldDek,
14143
14144
  readVaultRaw: () => readVaultRaw,
14144
14145
  ownSecret: () => ownSecret,
14145
14146
  legacyWalletsPath: () => legacyWalletsPath,
@@ -14203,6 +14204,17 @@ async function unlockVault(path, raw, request, opts = {}) {
14203
14204
  wipe(dek);
14204
14205
  throw new VaultError("VAULT_UNLOCK_FAILED", "The unwrapped key is the wrong length; this file is corrupt.");
14205
14206
  }
14207
+ return openWithDek(path, raw, file, envelope, dek);
14208
+ }
14209
+ async function reopenWithHeldDek(path, raw, held) {
14210
+ const file = parseVaultFile(raw);
14211
+ if (file.vaultId !== held.file.vaultId) {
14212
+ throw new VaultError("VAULT_UNLOCK_FAILED", `${path} is not the vault this command unlocked.`);
14213
+ }
14214
+ const envelope = file.envelopes.find((candidate) => candidate.id === held.envelope.id) ?? held.envelope;
14215
+ return openWithDek(path, raw, file, envelope, new Uint8Array(held.dek));
14216
+ }
14217
+ async function openWithDek(path, raw, file, envelope, dek) {
14206
14218
  try {
14207
14219
  const payloadKey = await derivePayloadKey(dek, unb64u(file.vaultId, "vaultId"));
14208
14220
  const indexBytes = await open2(payloadKey, file.index, canonicalHeader(file), {
@@ -14548,6 +14560,7 @@ var init_ownership = __esm(() => {
14548
14560
  "token clusters",
14549
14561
  "wallets",
14550
14562
  "swap status",
14563
+ "launch status",
14551
14564
  "fees",
14552
14565
  "lp pools",
14553
14566
  "lp positions",
@@ -15758,7 +15771,9 @@ var init_trading = __esm(() => {
15758
15771
  transaction: exports_external.string().min(1),
15759
15772
  maxDebitLamports: exports_external.string().regex(/^\d+$/),
15760
15773
  expiresAt: exports_external.number().finite(),
15761
- fee: feeSchema.optional()
15774
+ fee: feeSchema.optional(),
15775
+ feeWallet: exports_external.string().optional(),
15776
+ feeWalletSource: exports_external.string().optional()
15762
15777
  });
15763
15778
  walletSchema = exports_external.object({
15764
15779
  id: exports_external.string(),
@@ -16872,12 +16887,48 @@ function describeSwapEvidence(ctx, details, terminal) {
16872
16887
  notice(ctx.deps, "stderr", "warning", `The trade landed${typeof details.signature === "string" ? ` (${safeText(details.signature)})` : ""} and a later step failed. Do not retry or use a new id.
16873
16888
  `.trim());
16874
16889
  }
16890
+ function tradeSignatures(result) {
16891
+ const nested = (name) => result[name] && typeof result[name] === "object" ? result[name] : {};
16892
+ const payload = nested("payload");
16893
+ const job = nested("job");
16894
+ const sources = [result, payload, job];
16895
+ const operation = job.operation && typeof job.operation === "object" ? job.operation : {};
16896
+ const legs = [...sources.map((source) => source.landedLegs), operation.legs].flatMap((value) => Array.isArray(value) ? value.filter((leg) => leg !== null && typeof leg === "object") : []);
16897
+ const primary = sources.map((source) => source.signature).find(isText) ?? legs.find((leg) => (leg.kind === "trade" || leg.kind === "bridgeDeposit") && isText(leg.hash))?.hash;
16898
+ return [
16899
+ ...new Set([
16900
+ ...isText(primary) ? [primary] : [],
16901
+ ...sources.flatMap((source) => [
16902
+ ...isText(source.signature) ? [source.signature] : [],
16903
+ ...textsOf(source.signatures),
16904
+ ...textsOf(source.hashes),
16905
+ ...isText(source.feeSignature) ? [source.feeSignature] : []
16906
+ ]),
16907
+ ...legs.map((leg) => leg.hash).filter(isText)
16908
+ ])
16909
+ ];
16910
+ }
16911
+ function swapJsonResult(result) {
16912
+ const signatures = tradeSignatures(result);
16913
+ const inner = (name) => result[name] && typeof result[name] === "object" ? result[name].status : undefined;
16914
+ const status = [result.status, inner("job"), inner("payload")].find(isText) ?? null;
16915
+ return {
16916
+ ...result,
16917
+ kind: result.kind,
16918
+ status,
16919
+ clientTradeId: result.clientTradeId ?? null,
16920
+ signature: signatures[0] ?? null,
16921
+ signatures,
16922
+ raw: result
16923
+ };
16924
+ }
16875
16925
  function printTradingResult(ctx, result) {
16876
16926
  if (result.kind === "swap" && result.job && typeof result.job === "object") {
16877
16927
  const job = result.job;
16878
16928
  describeSwapEvidence(ctx, job, job.status === "failed");
16879
16929
  }
16880
- ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
16930
+ const printed = ctx.json && (result.kind === "swap" || result.kind === "trade") ? swapJsonResult(result) : result;
16931
+ ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(printed)}
16881
16932
  ` : `${JSON.stringify(result, null, 2)}
16882
16933
  `);
16883
16934
  return 0;
@@ -17684,7 +17735,7 @@ async function bridgeSwap(ctx, args) {
17684
17735
  return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
17685
17736
  }
17686
17737
  }
17687
- var AGENT_MAX_SLIPPAGE_BPS = 5000, SLIPPAGE_RANGE_MESSAGE, settlementSchema, bridgeFactsSchema, HOOD_TRADE_LEGS, bridgeBuildSchema;
17738
+ var isText = (value) => typeof value === "string" && value.length > 0, textsOf = (value) => Array.isArray(value) ? value.filter(isText) : [], AGENT_MAX_SLIPPAGE_BPS = 5000, SLIPPAGE_RANGE_MESSAGE, settlementSchema, bridgeFactsSchema, HOOD_TRADE_LEGS, bridgeBuildSchema;
17688
17739
  var init_swap = __esm(() => {
17689
17740
  init_zod();
17690
17741
  init_args();
@@ -28872,7 +28923,7 @@ __export(exports_vault_support, {
28872
28923
  askForOwnPassphrase: () => askForOwnPassphrase,
28873
28924
  PASSPHRASE_ONLY_PREFIX: () => PASSPHRASE_ONLY_PREFIX
28874
28925
  });
28875
- import { dirname as dirname6 } from "node:path";
28926
+ import { dirname as dirname6, resolve as resolvePath } from "node:path";
28876
28927
  function refuseEnvPassphrase(ctx) {
28877
28928
  if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
28878
28929
  return true;
@@ -29002,6 +29053,9 @@ function assertVaultHelperIdentities(deps, envelopes) {
29002
29053
  }
29003
29054
  async function unlockInteractively(ctx, path, raw, opts = {}) {
29004
29055
  await assertNotOlderCopy(ctx, path, raw, opts.acceptOlderCopy ?? false);
29056
+ const held = ctx.heldUnlock;
29057
+ if (held !== undefined && resolvePath(held.path) === resolvePath(path))
29058
+ return held.open(raw);
29005
29059
  const { deps } = ctx;
29006
29060
  const file = parseVaultFile(raw);
29007
29061
  assertVaultHelperIdentities(deps, file.envelopes);
@@ -37251,9 +37305,11 @@ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
37251
37305
  return { ok: false, code: 1 };
37252
37306
  }
37253
37307
  if (byPrefix)
37254
- return { ok: true, keyPrefix: byPrefix.keyPrefix };
37255
- if (byLabel.length === 1)
37256
- return { ok: true, keyPrefix: byLabel[0].keyPrefix };
37308
+ return { ok: true, keyPrefix: byPrefix.keyPrefix, embeddedWallet: byPrefix.embeddedWallet };
37309
+ if (byLabel.length === 1) {
37310
+ const only = byLabel[0];
37311
+ return { ok: true, keyPrefix: only.keyPrefix, embeddedWallet: only.embeddedWallet };
37312
+ }
37257
37313
  if (byLabel.length > 1) {
37258
37314
  writeLocalFailure(deps, {
37259
37315
  code: `${codePrefix}_KEY_AMBIGUOUS`,
@@ -37265,8 +37321,10 @@ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
37265
37321
  return { ok: true, keyPrefix: raw };
37266
37322
  }
37267
37323
  const matches = keys.filter((key) => key.label === raw);
37268
- if (matches.length === 1)
37269
- return { ok: true, keyPrefix: matches[0].keyPrefix };
37324
+ if (matches.length === 1) {
37325
+ const only = matches[0];
37326
+ return { ok: true, keyPrefix: only.keyPrefix, embeddedWallet: only.embeddedWallet };
37327
+ }
37270
37328
  if (matches.length === 0) {
37271
37329
  const labelled = keys.filter((key) => key.label).map((key) => `${key.keyPrefix} ${labelCell(key.label)}`);
37272
37330
  writeLocalFailure(deps, {
@@ -37581,6 +37639,11 @@ var init_tee_rebind = __esm(() => {
37581
37639
  function effectiveEmbeddedWallet(value) {
37582
37640
  return value === "denied" ? "denied" : "allowed";
37583
37641
  }
37642
+ function withdrawNeedsEmbeddedWalletLine(keyPrefix) {
37643
+ return `Withdrawals to your approved addresses leave from the embedded wallet, so a Withdraw key needs the embedded wallet allowed; it is denied on this key, and those withdrawals will be refused.
37644
+ ` + `Allow it with: candle keys update ${keyPrefix} --embedded-wallet allow
37645
+ ` + "Transfers out of the key's own TEE wallets do not need it.";
37646
+ }
37584
37647
  function keyName(keyPrefix, label) {
37585
37648
  const cleaned = labelCell(label ?? undefined);
37586
37649
  return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
@@ -38094,6 +38157,10 @@ async function keysCreate(args, ctx) {
38094
38157
  } catch (error) {
38095
38158
  storeError = error instanceof Error ? error.message : String(error);
38096
38159
  }
38160
+ if (presetForScopes(body.scopes) === "readwritetransfer" && body.embeddedWallet === "denied") {
38161
+ notice(deps, "stderr", "warning", `${withdrawNeedsEmbeddedWalletLine(body.keyPrefix)}
38162
+ Or mint it that way: candle keys create --access withdraw --embedded-wallet allow`);
38163
+ }
38097
38164
  if (json) {
38098
38165
  deps.stdout.write(`${JSON.stringify({ ...body, stored, ...storeError ? { storeError } : {} })}
38099
38166
  `);
@@ -38399,7 +38466,7 @@ var init_token_clusters = __esm(() => {
38399
38466
  });
38400
38467
 
38401
38468
  // ../mcp/src/version.ts
38402
- var SERVER_VERSION = "0.10.28";
38469
+ var SERVER_VERSION = "0.10.29";
38403
38470
 
38404
38471
  // ../mcp/src/update-notice.ts
38405
38472
  function newer(a, b) {
@@ -39657,7 +39724,7 @@ var init_launch_definition = __esm(() => {
39657
39724
  {
39658
39725
  key: "feeWallet",
39659
39726
  type: "string",
39660
- help: "Solana only: the wallet this launch's curve and pool fees earn to, an on-curve wallet or a Squads v4 vault; defaults to the account wallet.",
39727
+ help: "Solana only: the wallet this launch's curve and pool fees earn to, an on-curve wallet or a Squads v4 vault; defaults to the wallet that launches the token (the account wallet on a server without LAUNCH_FEE_WALLET_FROM_LAUNCHER, except an embedded launch).",
39661
39728
  chains: [
39662
39729
  "solana"
39663
39730
  ],
@@ -44618,6 +44685,18 @@ function buildRequest(name, args, cfg) {
44618
44685
  init: { method: "POST", headers: jsonHeaders(apiKey), body: JSON.stringify(body) }
44619
44686
  };
44620
44687
  }
44688
+ case "candle_atomic_launch_dry_run": {
44689
+ const apiKey = requireApiKey2(cfg);
44690
+ const { payer, chain: _chain, buyAmount: _buyAmount, ...rest } = args;
44691
+ return {
44692
+ url: `${base2}/api/v1/launch/atomic/dry-run`,
44693
+ init: {
44694
+ method: "POST",
44695
+ headers: jsonHeaders(apiKey),
44696
+ body: JSON.stringify({ ...rest, chain: "solana", payer: payer ?? { type: "main" } })
44697
+ }
44698
+ };
44699
+ }
44621
44700
  case "candle_get_market": {
44622
44701
  const { chain: chain2, mint } = args;
44623
44702
  return {
@@ -45290,12 +45369,12 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
45290
45369
  }, async (args) => callAndRelay("candle_claim_staking_reward", args, getConfig(), doFetch));
45291
45370
  register("candle_get_fees", {
45292
45371
  title: "Read the creator fees this account's tokens can pay",
45293
- description: "The tokens launched by the account's own wallets, with what each could pay now in creator fees " + "(raw units from direct chain reads) and `claimable: true` or the `reason` it is not. Reads only; " + "moves nothing. It returns no earned or paid-out totals. An unreadable token is marked, never " + "shown as zero. Paged: pass `continueCursor` back as `cursor`.",
45372
+ description: "The tokens launched by the account's own wallets, with what each could pay now in creator fees " + "(raw units from direct chain reads) and a `reason` saying who can claim: `claimable_by_key` on a " + "claimable Solana row, and otherwise why this key cannot (including `claimable_via_web_session` " + "and `fee_wallet_designated`, which name whose authority collects). Reads only; " + "moves nothing. It returns no earned or paid-out totals. An unreadable token is marked, never " + "shown as zero. Paged: pass `continueCursor` back as `cursor`.",
45294
45373
  inputSchema: getFeesShape
45295
45374
  }, async (args) => callAndRelay("candle_get_fees", args, getConfig(), doFetch));
45296
45375
  register("candle_claim_fees", {
45297
45376
  title: "Claim a token's creator fees",
45298
- description: "Collect one token's creator fees and pay them to the wallet fixed at launch. There is no destination " + "argument and the tool cannot send funds anywhere else. Candle signs with the embedded wallet, which " + "pays the network fee. Needs launch:write. " + "Call it with dryRun: true first; a real claim moves real funds into the recipient wallet, which may be " + "the wallet this agent trades from. Not idempotent by id: if the result is lost, read candle_get_fees " + "before claiming again.",
45377
+ description: "Collect one token's creator fees and pay them to the wallet fixed at launch. There is no destination " + "argument and the tool cannot send funds anywhere else. Candle signs with the embedded wallet, which " + "pays the network fee. A fee recipient that is a TEE wallet is refused here: claim it with the candle CLI " + "(`candle fees claim`). The dry run reports the same refusal (`claimable: false`, `reason: " + "wallet_not_signable`) and counts only what this tool could claim. Needs launch:write. " + "Call it with dryRun: true first; a real claim moves real funds into the recipient wallet, which may be " + "the wallet this agent trades from. Not idempotent by id: if the result is lost, read candle_get_fees " + "before claiming again.",
45299
45378
  inputSchema: claimFeesShape
45300
45379
  }, async (args) => callAndRelay("candle_claim_fees", args, getConfig(), doFetch));
45301
45380
  register("candle_get_wallets", {
@@ -45403,6 +45482,12 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
45403
45482
  const result = await executeTrade(args, getConfig(), doFetch);
45404
45483
  return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
45405
45484
  });
45485
+ register("candle_atomic_launch_dry_run", {
45486
+ title: "Dry-run an atomic Solana launch",
45487
+ description: "Check a Solana launch bundled with first buys and price each buy, without building anything. Runs the " + "same checks as the atomic build, in the same order (payers, Believer check, fee and spend limits, the " + "key's transaction cap), then returns the launch preview (curveTerms) and one fill per first buy: the " + "tokens it would receive and the floor the build would sign it at. Reads only; moves nothing, reserves " + "nothing, and the clientLaunchId stays unused. It does not check balances, the daily cap, or the " + "transaction's size, so the real launch can still refuse. Needs launch:write, swap:write and Pro or Max.",
45488
+ inputSchema: atomicLaunchDryRunShape,
45489
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
45490
+ }, async (args) => callAndRelay("candle_atomic_launch_dry_run", args, getConfig(), doFetch));
45406
45491
  register("candle_launch_and_seed", {
45407
45492
  title: "Launch a token and seed it",
45408
45493
  description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed, then " + "return the fresh market state and token links in one result. On solana a seed makes this " + (options.headlessSeed === true ? "the seed is a separate first-buy transaction in the same Jito bundle as the launch, so " + "both land or neither does. That works on every plan, Free included, and needs no " + "swap:write. On hood the seed is " : "the atomic launch: the launch and the seed are two transactions Candle signs and sends as " + "one Jito bundle, so both land or neither does. That needs Pro or Max (TIER_REQUIRED " + "otherwise) and swap:write on the key, and the result carries bundleId. On hood the seed is ") + "a best-effort follow-up. A launch with no seed works on every plan, Free included. MOVES " + "REAL FUNDS unless dryRun; dry-run first, which spends nothing. Seeds above the platform " + "dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH): at most 0.5 SOL / 500 USDC / 2,000,000 " + "CNDL, or on an exp- curve 10% of the threshold when that is lower; the server holds the " + "creator's own seed to the effective ceiling, so launch with the largest devBuy it allows " + "and top up with candle_trade. " + LAUNCH_SHARED_SENTENCES + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
@@ -45605,7 +45690,7 @@ async function registerEligibleProfileTool(server, env = process.env, doFetch =
45605
45690
  }
45606
45691
  return eligible;
45607
45692
  }
45608
- var VAMP_CANDIDATES_DESCRIPTION, localProfileTools, STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.", stakingWriteShape, TOOL_NAMES, LAB_TOOL_NAMES, PROFILE_EDIT_TOOL_NAMES, isProfileEditTool = (name) => PROFILE_EDIT_TOOL_NAMES.includes(name), OPERATION_NOT_FOUND_RULE, getMarketShape, TOKEN_EVENTS_LIMIT_MESSAGE = "limit must be a whole number from 1 to 1000", tokenEventsLimit, TOKEN_EVENTS_KIND_NOTE = "kept back from the request and applied to the returned page by this server, so a filtered page can be shorter than limit while older events of other kinds remain: page with nextCursor as before until it is null", tokenCardShape, tokenEventsShape, tokenForensicsShape, FEED_LIMIT_MESSAGE = "limit must be a whole number from 1 to 200", feedLimit, getFeedShape, reportActivityShape, getAgentProfileShape, listAgentsShape, createPostShape, deletePostShape, listAgentPostsShape, getAgentPositionsShape, getOperationShape, vampCandidatesShape, getFeesShape, claimFeesShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, tokenImageUrl, tokenBannerUrl, localImageShape, localLaunchTokenShape, localLaunchAndSeedShape, IMAGE_PATH_DISCLOSURE, transferShape, sweepShape, PROFILE_PNL_HYPERLIQUID;
45693
+ var VAMP_CANDIDATES_DESCRIPTION, localProfileTools, STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.", stakingWriteShape, TOOL_NAMES, LAB_TOOL_NAMES, PROFILE_EDIT_TOOL_NAMES, isProfileEditTool = (name) => PROFILE_EDIT_TOOL_NAMES.includes(name), OPERATION_NOT_FOUND_RULE, getMarketShape, TOKEN_EVENTS_LIMIT_MESSAGE = "limit must be a whole number from 1 to 1000", tokenEventsLimit, TOKEN_EVENTS_KIND_NOTE = "kept back from the request and applied to the returned page by this server, so a filtered page can be shorter than limit while older events of other kinds remain: page with nextCursor as before until it is null", tokenCardShape, tokenEventsShape, tokenForensicsShape, FEED_LIMIT_MESSAGE = "limit must be a whole number from 1 to 200", feedLimit, getFeedShape, reportActivityShape, getAgentProfileShape, listAgentsShape, createPostShape, deletePostShape, listAgentPostsShape, getAgentPositionsShape, getOperationShape, vampCandidatesShape, getFeesShape, atomicPayerShape, atomicLaunchDryRunShape, claimFeesShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, tokenImageUrl, tokenBannerUrl, localImageShape, localLaunchTokenShape, localLaunchAndSeedShape, IMAGE_PATH_DISCLOSURE, transferShape, sweepShape, PROFILE_PNL_HYPERLIQUID;
45609
45694
  var init_tools = __esm(() => {
45610
45695
  init_zod();
45611
45696
  init_client2();
@@ -45670,6 +45755,7 @@ var init_tools = __esm(() => {
45670
45755
  "candle_get_plans",
45671
45756
  "candle_trade",
45672
45757
  "candle_launch_and_seed",
45758
+ "candle_atomic_launch_dry_run",
45673
45759
  "candle_swap",
45674
45760
  "candle_transfer",
45675
45761
  "candle_sweep",
@@ -45821,6 +45907,22 @@ var init_tools = __esm(() => {
45821
45907
  cursor: exports_external.string().optional().describe("The previous page's continueCursor, verbatim"),
45822
45908
  limit: exports_external.number().optional().describe("Tokens per page, 1 to 50 (default 20). Each one costs chain reads")
45823
45909
  };
45910
+ atomicPayerShape = exports_external.union([
45911
+ exports_external.object({ type: exports_external.literal("main") }),
45912
+ exports_external.object({ type: exports_external.literal("linked"), linkedWalletId: exports_external.string().min(1) })
45913
+ ]);
45914
+ atomicLaunchDryRunShape = (() => {
45915
+ const { buyAmount: _buyAmount, chain: _chain, ...launch } = launchTokenShape;
45916
+ return {
45917
+ ...launch,
45918
+ clientLaunchId: exports_external.string().optional().describe("Not used by a dry run; send the id you will launch with"),
45919
+ payer: atomicPayerShape.optional().describe("Who pays for the launch: {type:'main'} (default) or {type:'linked', linkedWalletId}"),
45920
+ firstBuys: exports_external.array(exports_external.object({
45921
+ payer: atomicPayerShape,
45922
+ amountRaw: exports_external.string().regex(/^[1-9][0-9]*$/).describe("Quote-asset raw units")
45923
+ })).min(1).max(4).describe("One to four first buys, each priced on the curve this launch would deploy")
45924
+ };
45925
+ })();
45824
45926
  claimFeesShape = {
45825
45927
  mint: exports_external.string().describe("The launched token: a Solana mint or a Hood token address, as candle_get_fees lists it"),
45826
45928
  dryRun: exports_external.boolean().optional().describe("Check and read only. Sends nothing and writes nothing. Use it first to see what would be claimed")
@@ -76035,7 +76137,7 @@ var LAUNCH_DEFINITION = {
76035
76137
  {
76036
76138
  key: "feeWallet",
76037
76139
  type: "string",
76038
- help: "Solana only: the wallet this launch's curve and pool fees earn to, an on-curve wallet or a Squads v4 vault; defaults to the account wallet.",
76140
+ help: "Solana only: the wallet this launch's curve and pool fees earn to, an on-curve wallet or a Squads v4 vault; defaults to the wallet that launches the token (the account wallet on a server without LAUNCH_FEE_WALLET_FROM_LAUNCHER, except an embedded launch).",
76039
76141
  chains: [
76040
76142
  "solana"
76041
76143
  ],
@@ -76159,7 +76261,7 @@ var LAUNCH_USAGE = `candle launch ${launchFlagFields.map((field) => {
76159
76261
  if (field.key === "imageUrl")
76160
76262
  return `--image <path>|${invocation}`;
76161
76263
  return "required" in field && field.required ? invocation : `[${invocation}]`;
76162
- }).join(" ")} [--check] [--yes] [--rpc-url <url>] [--json]`;
76264
+ }).join(" ")} [--check] [--yes] [--require-fee-wallet] [--rpc-url <url>] [--json]`;
76163
76265
  function launchFieldProblem(flags) {
76164
76266
  for (const field of launchFlagFields) {
76165
76267
  const value = flags[field.cli.flag];
@@ -76344,7 +76446,7 @@ var HELP = {
76344
76446
  group: "Trade",
76345
76447
  intro: "Quote, swap and bridge on Solana or Hood; read an operation",
76346
76448
  summary: "Quote, confirm and swap on Solana or Hood, bridge between them; read an operation by id",
76347
- description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent. The first buy after a launch is this command, unless the launch carried it with candle launch --buy. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, only configured raw caps and USD limits apply (unset means unlimited, zero still binds), and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and sell a token it holds that was not launched on Candle; buying such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force.",
76449
+ description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent. The first buy after a launch is this command, unless the launch carried it with candle launch --buy. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, only configured raw caps and USD limits apply (unset means unlimited, zero still binds), and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and sell a token it holds that was not launched on Candle; buying such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force. With --json, stdout is one JSON value for every swap, trade, replay and status read: kind, status, clientTradeId, signature (the primary transaction, or null), signatures (a list) and raw (the original response); every human line is on stderr. The keys older versions printed stay beside them.",
76348
76450
  usage: ["candle swap <from> <to> [flags]", "candle swap status <id>"],
76349
76451
  rows: [
76350
76452
  {
@@ -76657,19 +76759,19 @@ var HELP = {
76657
76759
  group: "Trade",
76658
76760
  intro: "Move funds out of a TEE wallet to your wallets or its vault",
76659
76761
  summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
76660
- description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Withdraw 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 only configured raw caps and USD limits apply (unset means unlimited, zero still binds), and the asset 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. The asset decides the chain: ETH, USDG or --token is Hood, from a Hood TEE wallet, where this machine checks the one leg moves exactly what you confirmed before it is signed, and ETH max leaves the gas reserve.",
76762
+ description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Withdraw 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, and, where the server has TEE_WITHDRAWALS_ENABLED on, an address on the account's withdrawal allowlist (named by its label, id or address). To a linked wallet or an approved address only configured raw caps and USD limits apply (unset means unlimited, zero still binds), and the asset 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. The asset decides the chain: ETH, USDG or --token is Hood, from a Hood TEE wallet, where this machine checks the one leg moves exactly what you confirmed before it is signed, and ETH max leaves the gas reserve.",
76661
76763
  usage: [
76662
- "candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
76764
+ "candle transfer --to <address|wallet name|allowlist label|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
76663
76765
  ],
76664
76766
  rows: [],
76665
76767
  flags: [
76666
76768
  {
76667
- invocation: "--to <address|wallet name|vault>",
76668
- description: "Where the funds go: vault is the wallet's own pin"
76769
+ invocation: "--to <address|wallet name|allowlist label|vault>",
76770
+ description: "Where the funds go: vault is the wallet's own pin; a linked wallet's name wins over an allowlist label"
76669
76771
  },
76670
76772
  {
76671
76773
  invocation: "--asset <SOL|USDC|CNDL|ETH|USDG>",
76672
- description: "A base asset (required for a linked-wallet destination)"
76774
+ description: "A base asset (required for a linked-wallet or approved-address destination)"
76673
76775
  },
76674
76776
  { invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
76675
76777
  { invocation: "--token <0x...>", description: "Any Hood ERC-20, instead of --asset (vault destinations only)" },
@@ -76691,9 +76793,14 @@ var HELP = {
76691
76793
  launch: {
76692
76794
  group: "Trade",
76693
76795
  summary: "Create a token on Solana or Hood, with an optional first buy",
76694
- description: "Creates a token from the wallet you name, with an optional first buy. Embedded launches use /launch/headless on every plan, with the key's embedded-wallet allowance and launch:write; no swap:write is needed. A Solana first buy is a creator bundle, signed by Candle. A Hood first buy is best effort after creation. TEE launches use /launch/self and require Pro or Max, allowLaunch, a bound key and verified authority. TEE buys need TEE_LAUNCH_BUYS_ENABLED on the server; Solana buys also need AGENT_TRADING_ENABLED and Jito. Existing server gates and ceilings apply. The embedded path previews the launch before confirmation; --dry-run stops at the preview. No buy creates only the token. Local image and banner uploads happen before confirmation and are public and permanent even if the launch is declined. Reuse --client-trade-id for recovery. On Solana, --fee-wallet <address> sets the wallet the launch's curve and pool fees earn to, an on-curve wallet by itself or a Squads v4 vault with --fee-wallet-multisig <address> and --fee-wallet-multisig-vault-index <n>; without it the fees earn to the account wallet, which the preview, the confirm and --check all say. --check answers first, before any of it: which launch path works on this key and server (embedded allowance, tier, allowLaunch, TEE_LAUNCH_BUYS_ENABLED) in one call, with nothing uploaded, built or sent; name a wallet with --wallet to narrow it, and it exits 1 when no path works.",
76695
- usage: [LAUNCH_USAGE],
76696
- rows: [],
76796
+ description: "Creates a token from the wallet you name, with an optional first buy. Embedded launches use /launch/headless on every plan, with the key's embedded-wallet allowance and launch:write; no swap:write is needed. A Solana first buy is a creator bundle, signed by Candle. A Hood first buy is best effort after creation. TEE launches use /launch/self and require Pro or Max, allowLaunch, a bound key and verified authority. TEE buys need TEE_LAUNCH_BUYS_ENABLED on the server; Solana buys also need AGENT_TRADING_ENABLED and Jito. Existing server gates and ceilings apply. The embedded path previews the launch before confirmation; --dry-run stops at the preview. No buy creates only the token. Local image and banner uploads happen before confirmation and are public and permanent even if the launch is declined. Reuse --client-trade-id for recovery. On Solana, --fee-wallet <address> sets the wallet the launch's curve and pool fees earn to, an on-curve wallet by itself or a Squads v4 vault with --fee-wallet-multisig <address> and --fee-wallet-multisig-vault-index <n>; without it the fees earn to the wallet that launches the token where the server has LAUNCH_FEE_WALLET_FROM_LAUNCHER on (an embedded launch already earns to its embedded wallet), otherwise to the account wallet, and the preview, the confirm and --check all say which; a live launch also warns on stderr when it defaults, and --require-fee-wallet refuses the launch without one. --check answers first, before any of it: which launch path works on this key and server (embedded allowance, tier, allowLaunch, TEE_LAUNCH_BUYS_ENABLED) in one call, with nothing uploaded, built or sent; name a wallet with --wallet to narrow it, and it exits 1 when no path works. status reads one launch by its mint or clientLaunchId: build, signed, confirmed, trading, bonded, then migrated, with the market's lifecycle and migration fields. --wait follows it until the token is trading, the launch has failed, or ten minutes pass.",
76797
+ usage: [LAUNCH_USAGE, "candle launch status <mint-or-client-launch-id> [--wait]"],
76798
+ rows: [
76799
+ {
76800
+ invocation: "status <mint-or-client-launch-id> [--wait]",
76801
+ description: "One state for a mint or a clientLaunchId: build, signed, confirmed, trading, bonded, migrated. Uses the market's lifecycle and migration fields. --wait follows until the token is trading, the launch has failed, or ten minutes pass. Reads only."
76802
+ }
76803
+ ],
76697
76804
  flags: [
76698
76805
  ...launchFlagRows,
76699
76806
  {
@@ -76705,6 +76812,10 @@ var HELP = {
76705
76812
  description: "Answer which launch path works (embedded allowance, tier, allowLaunch, TEE_LAUNCH_BUYS_ENABLED) in one call, before any image upload; nothing is sent. Exits 1 when no path works."
76706
76813
  },
76707
76814
  { invocation: "--yes", description: "Confirm the launch without an interactive prompt." },
76815
+ {
76816
+ invocation: "--require-fee-wallet",
76817
+ description: "Refuse the launch when no --fee-wallet is given, rather than letting the fees earn to the account wallet."
76818
+ },
76708
76819
  {
76709
76820
  invocation: "--rpc-url <url>",
76710
76821
  description: "The Solana RPC for a TEE launch with no first buy, or the Hood RPC for receipt reads."
@@ -76718,7 +76829,8 @@ var HELP = {
76718
76829
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
76719
76830
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1",
76720
76831
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet embedded --buy 0.1 --dry-run",
76721
- "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --mode exp-custom --curve-v 4 --curve-g 20 --creator-fee-bps 100 --dry-run"
76832
+ "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --mode exp-custom --curve-v 4 --curve-g 20 --creator-fee-bps 100 --dry-run",
76833
+ "candle launch status <mint-or-id> --wait"
76722
76834
  ],
76723
76835
  env: ENV_API
76724
76836
  },
@@ -77363,7 +77475,7 @@ var HELP = {
77363
77475
  },
77364
77476
  {
77365
77477
  invocation: "transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]",
77366
- description: "Sign a transfer locally from a vault key or a promoted TEE wallet (Solana, or Hood: refused while a sequenced trade holds the wallet's nonce, and when that cannot be read). From an EVM key: ETH or an ERC-20, on Hood by default (--rpc-url for any EVM chain; the chain id is read from the RPC), exit 0 means depth-confirmed (1 block on Hood, 2 elsewhere), not finalized. A Solana key reads and sends over --rpc-url, else CANDLE_SOLANA_RPC_URL, else the profile's RPC, else the public endpoint. One verification: the unlock names the send; after the decoded display you type confirm. No --yes"
77478
+ description: "Sign a transfer locally from a vault key or a promoted TEE wallet (Solana, or Hood: refused while a sequenced trade holds the wallet's nonce, and when that cannot be read). From an EVM key: ETH or an ERC-20, on Hood by default (--rpc-url for any EVM chain; the chain id is read from the RPC), exit 0 means depth-confirmed (1 block on Hood, 2 elsewhere), not finalized. A Solana key reads and sends over --rpc-url, else CANDLE_SOLANA_RPC_URL, else the profile's RPC, else the public endpoint. Max sends SOL minus the quoted fee (zero source reserve when emptied), or the whole token balance; EVM native max deducts the fee cap. SOL max refreshes before signing; a changed amount or fee requires a fresh confirm. One verification: the unlock names the send; after the decoded display you type confirm. No --yes"
77367
77479
  },
77368
77480
  {
77369
77481
  invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
@@ -77378,8 +77490,8 @@ var HELP = {
77378
77490
  description: "Fund a TEE or external wallet from a vault key. A Hood TEE wallet takes ETH or USDG from its pinned EVM vault key. One verification: the unlock names the send; after the decoded display you type confirm. No --yes"
77379
77491
  },
77380
77492
  {
77381
- invocation: "demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--token <0x...>] [--from-block <n>]",
77382
- description: "Disable then sweep a TEE wallet back to its pin. A Hood wallet takes tee sweep's --token and --from-block"
77493
+ invocation: "demote <tee-address>... | --all --empty [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--yes] [--token <0x...>] [--from-block <n>]",
77494
+ description: "Disable then sweep a TEE wallet back to its pin. A Hood wallet takes tee sweep's --token and --from-block (one wallet only). Several addresses, or --all --empty, demote one at a time under one unlock: every wallet and its sweep destination is listed, then you type confirm once (--yes skips it; it stands in for each sweep's confirm). A failure does not stop the rest; each wallet gets a result line and a summary counts done, incomplete, failed and skipped with reasons. --json writes one document: wallets[] and summary"
77383
77495
  },
77384
77496
  {
77385
77497
  invocation: "export-key <label> --to <new-file>",
@@ -77398,7 +77510,15 @@ var HELP = {
77398
77510
  },
77399
77511
  {
77400
77512
  invocation: "--all",
77401
- description: "list: include retired TEE wallets (demoted and swept), which are hidden by default"
77513
+ description: "list: include retired TEE wallets (demoted and swept), which are hidden by default. demote: every TEE wallet in the vault that is not retired, with a filter (--empty)"
77514
+ },
77515
+ {
77516
+ invocation: "--empty",
77517
+ description: "demote --all: only Solana TEE wallets with nothing left to sweep (0 SOL, no token accounts, no unsettled sweep); the rest are listed as skipped"
77518
+ },
77519
+ {
77520
+ invocation: "--yes",
77521
+ description: "demote with several wallets: skip the one typed confirmation for the listed set"
77402
77522
  },
77403
77523
  {
77404
77524
  invocation: "--balances",
@@ -77436,6 +77556,7 @@ var HELP = {
77436
77556
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
77437
77557
  "candle vault promote-batch --pairs-from ./promote-plan.csv",
77438
77558
  "candle vault promote-batch --pairs-from ./promote-plan.csv --to-key tr-01",
77559
+ "candle vault demote --all --empty",
77439
77560
  "candle vault enroll security-key --label yubikey-a",
77440
77561
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
77441
77562
  "candle vault backup --to icloud",
@@ -77496,8 +77617,12 @@ var HELP = {
77496
77617
  description: "Rename 1–256 TEE wallets with one server call, all or nothing, then one vault commit. Every finding refuses the whole file. The vault is unlocked once, only when a named TEE wallet may be in it. --dry-run prints the plan and changes nothing; if the vault write fails after the server accepted, it prints address-keyed rows to re-run"
77497
77618
  },
77498
77619
  {
77499
- invocation: "signer new --key <prefix|label> [--out <pem>] [--force]",
77500
- description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an older signer on THIS machine still owns wallets; it moves, revokes and deletes nothing"
77620
+ invocation: "signer new --key <prefix|label> [--out <pem>] [--force] [--no-wait]",
77621
+ description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext P-256 authorization key for Privy wallet RPC; it cannot sign Solana transactions (the TEE signs them). --no-wait exits 0 once pending. --json emits newline-delimited JSON: a pending event with userCode, verificationUriComplete, expiresAt and fingerprint immediately, then an active result or error if waiting. Retrying re-attaches to the same live request; --force adds a signer while an older signer on THIS machine still owns wallets; it moves, revokes and deletes nothing"
77622
+ },
77623
+ {
77624
+ invocation: " signer status --key <prefix|label>",
77625
+ description: "Read the server signer state once without generating a pair or requesting approval. --json prints one value; on the requesting machine it includes the cached code and URL while that request is live"
77501
77626
  },
77502
77627
  {
77503
77628
  invocation: " signer remove --key <prefix|label>",
@@ -78119,6 +78244,7 @@ var TOOL_CLASSES = {
78119
78244
  candle_execution_status: READ,
78120
78245
  candle_get_operation: READ,
78121
78246
  candle_get_fees: READ,
78247
+ candle_atomic_launch_dry_run: READ,
78122
78248
  candle_get_staking: READ,
78123
78249
  candle_perps_markets: READ,
78124
78250
  candle_perps_market: READ,
@@ -82610,14 +82736,69 @@ async function tokenAccountFrozen(rpc, address) {
82610
82736
  return { exists: false, frozen: false };
82611
82737
  return { exists: true, frozen: account.data[TOKEN_ACCOUNT_STATE_OFFSET2] === TOKEN_ACCOUNT_STATE_FROZEN2 };
82612
82738
  }
82739
+ function rawToDecimal2(raw, decimals) {
82740
+ if (decimals === 0)
82741
+ return raw.toString();
82742
+ const digits = raw.toString().padStart(decimals + 1, "0");
82743
+ return `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}`.replace(/\.?0+$/, "");
82744
+ }
82745
+ async function planSolMax(rpc, from, to) {
82746
+ if (from === to)
82747
+ throw new VaultError("VAULT_INDEX_INVALID", "SOL max needs a destination different from the source.");
82748
+ const account = await rpc.getAccountInfo(from);
82749
+ if (account && (account.owner !== SYSTEM_PROGRAM_ID || account.data.length !== 0)) {
82750
+ throw new VaultError("VAULT_INDEX_INVALID", "SOL max requires a system wallet with no account data; nonce and program accounts cannot use this transfer shape. Nothing was signed or sent.");
82751
+ }
82752
+ const balance = account?.lamports ?? 0n;
82753
+ const reserve = 0n;
82754
+ const blockhash = await rpc.getLatestBlockhash();
82755
+ let raw = balance;
82756
+ for (let attempt = 0;attempt < 3; attempt++) {
82757
+ const instructions = [systemTransfer(decodePubkey(from), decodePubkey(to), raw)];
82758
+ const message = compileLegacyMessage({ feePayer: decodePubkey(from), recentBlockhash: blockhash, instructions });
82759
+ const fee = await rpc.getFeeForMessage(toBase642(message));
82760
+ if (fee === null) {
82761
+ throw new VaultError("VAULT_UNREADABLE", "The RPC could not quote the SOL max fee. Nothing was signed or sent; retry with a fresh RPC quote.");
82762
+ }
82763
+ const available = balance - fee - reserve;
82764
+ if (available <= 0n) {
82765
+ throw new VaultError("TRANSFER_INSUFFICIENT_FUNDS", `${from} holds ${balance} lamports, not above the ${fee} lamport fee; max leaves nothing to send. Nothing was signed or sent.`, { suggestion: "Fund the key with SOL first." });
82766
+ }
82767
+ if (raw === available) {
82768
+ const amount = rawToDecimal2(raw, 9);
82769
+ return {
82770
+ asset: "SOL",
82771
+ amount,
82772
+ amountRaw: raw,
82773
+ decimals: 9,
82774
+ from,
82775
+ to,
82776
+ instructions,
82777
+ solMax: { blockhash, fee, reserve },
82778
+ displayLines: [
82779
+ `fee payer ${from}`,
82780
+ `destination ${to}`,
82781
+ `amount ${amount} SOL = ${raw} lamports (max)`,
82782
+ "mint (native SOL)",
82783
+ `source reserve ${reserve} lamports (source emptied)`
82784
+ ]
82785
+ };
82786
+ }
82787
+ raw = available;
82788
+ }
82789
+ throw new VaultError("VAULT_UNREADABLE", "The SOL max fee quote kept changing. Nothing was signed or sent; retry when the RPC is stable.");
82790
+ }
82613
82791
  async function planTransfer(input) {
82614
82792
  const asset = input.asset.toUpperCase();
82615
82793
  const fromKey = decodePubkey(input.from);
82616
82794
  const toKey = decodePubkey(input.to);
82795
+ const max = input.amount.toLowerCase() === "max";
82617
82796
  if (asset === "SOL") {
82797
+ if (max)
82798
+ return planSolMax(input.rpc, input.from, input.to);
82618
82799
  const raw2 = decimalToRaw4(input.amount, 9);
82619
82800
  if (raw2 === null || raw2 === 0n) {
82620
- throw new VaultError("VAULT_INDEX_INVALID", "--amount must be a positive SOL decimal with at most 9 places.");
82801
+ throw new VaultError("VAULT_INDEX_INVALID", "--amount must be max or a positive SOL decimal with at most 9 places.");
82621
82802
  }
82622
82803
  return {
82623
82804
  asset: "SOL",
@@ -82648,13 +82829,16 @@ async function planTransfer(input) {
82648
82829
  throw new VaultError("VAULT_UNREADABLE", `Could not read mint ${mintAddress}: ${asMessage(error)}`);
82649
82830
  }
82650
82831
  const decimals = profile.decimals;
82651
- const raw = decimalToRaw4(input.amount, decimals);
82832
+ const source = ataFor(profile, fromKey);
82833
+ const raw = max ? tokenAccountAmount((await rpc.getAccountInfo(encodePubkey(source)))?.data ?? new Uint8Array) : decimalToRaw4(input.amount, decimals);
82834
+ if (max && raw === 0n) {
82835
+ throw new VaultError("TRANSFER_INSUFFICIENT_FUNDS", `${input.from} holds no ${input.asset} in its source token account; max leaves nothing to send. Nothing was signed or sent.`);
82836
+ }
82652
82837
  if (raw === null || raw === 0n) {
82653
- throw new VaultError("VAULT_INDEX_INVALID", `--amount must be a positive decimal with at most ${decimals} decimal places.`);
82838
+ throw new VaultError("VAULT_INDEX_INVALID", `--amount must be max or a positive decimal with at most ${decimals} decimal places.`);
82654
82839
  }
82655
82840
  const mint = decodePubkey(mintAddress);
82656
82841
  const tokenProgram = decodePubkey(profile.tokenProgram);
82657
- const source = ataFor(profile, fromKey);
82658
82842
  const destination = ataFor(profile, toKey);
82659
82843
  const instructions = [];
82660
82844
  const accountCreationLines = [];
@@ -82691,7 +82875,7 @@ async function planTransfer(input) {
82691
82875
  }));
82692
82876
  return {
82693
82877
  asset: asset === "USDC" ? "USDC" : mintAddress,
82694
- amount: input.amount,
82878
+ amount: max ? rawToDecimal2(raw, decimals) : input.amount,
82695
82879
  amountRaw: raw,
82696
82880
  decimals,
82697
82881
  mint: mintAddress,
@@ -82708,7 +82892,7 @@ async function planTransfer(input) {
82708
82892
  displayLines: [
82709
82893
  `fee payer ${input.from}`,
82710
82894
  `destination ${input.to}`,
82711
- `amount ${input.amount} (${raw} raw, ${decimals} dp)`,
82895
+ `amount ${max ? rawToDecimal2(raw, decimals) : input.amount} (${raw} raw, ${decimals} dp)${max ? " (max)" : ""}`,
82712
82896
  `mint ${mintAddress}`,
82713
82897
  `program ${profile.tokenProgram}${profile.token2022 ? " (Token-2022)" : ""}`,
82714
82898
  ...accountCreationLines,
@@ -82788,7 +82972,7 @@ async function signAndBroadcastTransfer(input) {
82788
82972
  if (encodePubkey(feePayer) !== input.plan.from) {
82789
82973
  throw new VaultError("VAULT_VERIFY_FAILED", "The decrypted key does not match the planned fee payer.");
82790
82974
  }
82791
- const blockhash = await input.solana.read(() => rpc.getLatestBlockhash());
82975
+ const blockhash = input.plan.solMax?.blockhash ?? await input.solana.read(() => rpc.getLatestBlockhash());
82792
82976
  const message = compileLegacyMessage({
82793
82977
  feePayer,
82794
82978
  recentBlockhash: blockhash,
@@ -84791,7 +84975,7 @@ function rebindJson(input, chain2) {
84791
84975
  init_key_output();
84792
84976
  init_keys();
84793
84977
  init_tee_rebind();
84794
- var USAGE_TEE_SIGNER = "Usage: candle tee signer new --key <prefix|label> [--out <pem>] [--force] [--json] | candle tee signer remove --key <prefix|label> [--json]";
84978
+ var USAGE_TEE_SIGNER = "Usage: candle tee signer new --key <prefix|label> [--out <pem>] [--force] [--no-wait] [--json] | candle tee signer status --key <prefix|label> [--json] | candle tee signer remove --key <prefix|label> [--json]";
84795
84979
  var USAGE_KEYS_SIGNER = "Usage: candle keys signer approve <code> --key <prefix|label> [--reject] | candle keys signer move <prefix|label> [--to-key <prefix|label>] [--json]";
84796
84980
  var SIGNER_POLL_MS = 5000;
84797
84981
  var DEVICE_TOKEN_BESIDE_SIGNER_LINE = "Warning: this machine holds a device token as well as a key signer. A machine with both can approve its own signer request. Do not leave the trading machine logged in. If you signed in for a move, run candle auth logout --profile <that profile> here, and approve from the owner's machine.";
@@ -84825,6 +85009,8 @@ async function teeSigner(args, ctx) {
84825
85009
  return teeSignerNew(rest, ctx);
84826
85010
  if (sub === "remove")
84827
85011
  return teeSignerRemove(rest, ctx);
85012
+ if (sub === "status")
85013
+ return teeSignerStatus(rest, ctx);
84828
85014
  writeScreenUsageFailure(ctx.deps, sub === undefined ? USAGE_TEE_SIGNER : `Unknown subcommand: ${sub}. ${USAGE_TEE_SIGNER}`, ctx.json);
84829
85015
  return 2;
84830
85016
  }
@@ -84837,7 +85023,11 @@ function generatePair() {
84837
85023
  }
84838
85024
  async function teeSignerNew(args, ctx) {
84839
85025
  const { deps, json } = ctx;
84840
- const parsed = parseArgs(args, { valueFlags: ["--key", "--out"], booleanFlags: ["--force"], pathFlags: ["--out"] });
85026
+ const parsed = parseArgs(args, {
85027
+ valueFlags: ["--key", "--out"],
85028
+ booleanFlags: ["--force", "--no-wait"],
85029
+ pathFlags: ["--out"]
85030
+ });
84841
85031
  if ("error" in parsed) {
84842
85032
  writeScreenUsageFailure(deps, `${parsed.error}. ${USAGE_TEE_SIGNER}`, json);
84843
85033
  return 2;
@@ -84853,6 +85043,7 @@ async function teeSignerNew(args, ctx) {
84853
85043
  }
84854
85044
  const out = parsed.values["--out"];
84855
85045
  const force = parsed.booleans.has("--force");
85046
+ const noWait = parsed.booleans.has("--no-wait");
84856
85047
  await printIdentity(ctx);
84857
85048
  const apiKey = await resolveApiKey(deps, ctx.profile);
84858
85049
  if (!apiKey) {
@@ -84891,7 +85082,7 @@ async function teeSignerNew(args, ctx) {
84891
85082
  entry = undefined;
84892
85083
  }
84893
85084
  if (entry !== undefined && active !== null && active.spkiSha256 === entry.spkiSha256) {
84894
- const recorded = { ...entry, signerQuorumId: active.signerQuorumId };
85085
+ const recorded = { ...entry, signerQuorumId: active.signerQuorumId, pendingRequest: undefined };
84895
85086
  await saveKeySignerEntry(deps, recorded);
84896
85087
  await writeOut(ctx, out, recorded);
84897
85088
  return reportApproved(ctx, view, recorded, { resumed: true, requested: false });
@@ -84940,12 +85131,17 @@ async function teeSignerNew(args, ctx) {
84940
85131
  resumed = false;
84941
85132
  }
84942
85133
  await writeOut(ctx, out, entry);
84943
- const requested = await requestSigner(ctx, apiKey, entry.publicKeyDer);
85134
+ const cached = matchingPendingRequest(ctx, view, entry);
85135
+ const requested = cached ? {
85136
+ ok: true,
85137
+ status: 200,
85138
+ body: { ...cached, spkiSha256: entry.spkiSha256, fingerprint: entry.fingerprint }
85139
+ } : await requestSigner(ctx, apiKey, entry.publicKeyDer);
84944
85140
  if (!requested.ok) {
84945
85141
  if (requested.code === "KEY_SIGNER_ALREADY_ACTIVE") {
84946
85142
  const quorum = errorDetails(requested).keySigner?.signerQuorumId;
84947
85143
  if (typeof quorum === "string") {
84948
- const recorded = { ...entry, signerQuorumId: quorum };
85144
+ const recorded = { ...entry, signerQuorumId: quorum, pendingRequest: undefined };
84949
85145
  await saveKeySignerEntry(deps, recorded);
84950
85146
  const reread = await readSigner(ctx, "self", { apiKey });
84951
85147
  return reportApproved(ctx, reread.ok ? reread.body : view, recorded, {
@@ -84982,6 +85178,27 @@ async function teeSignerNew(args, ctx) {
84982
85178
  }, json);
84983
85179
  return 1;
84984
85180
  }
85181
+ entry = {
85182
+ ...entry,
85183
+ pendingRequest: {
85184
+ apiUrl: ctx.apiUrl,
85185
+ userCode: body.userCode,
85186
+ verificationUri: body.verificationUri,
85187
+ verificationUriComplete: body.verificationUriComplete,
85188
+ expiresAt: body.expiresAt
85189
+ }
85190
+ };
85191
+ await saveKeySignerEntry(deps, entry);
85192
+ if (json) {
85193
+ deps.stdout.write(`${JSON.stringify({
85194
+ state: "pending",
85195
+ userCode: body.userCode,
85196
+ verificationUriComplete: body.verificationUriComplete,
85197
+ expiresAt: body.expiresAt,
85198
+ fingerprint: body.fingerprint
85199
+ })}
85200
+ `);
85201
+ }
84985
85202
  if (json) {
84986
85203
  writeFields(deps, "stderr", [
84987
85204
  ["Key", keyPrefix],
@@ -84992,7 +85209,7 @@ async function teeSignerNew(args, ctx) {
84992
85209
  ["Approve on owner machine", `candle keys signer approve ${body.userCode} --key ${keyPrefix}`],
84993
85210
  ["Expires", formatTime(body.expiresAt)]
84994
85211
  ], "Key signer request");
84995
- notice(deps, "stderr", "note", "The account owner approves it, typing this full fingerprint. Waiting for approval...");
85212
+ notice(deps, "stderr", "note", `The account owner approves it, typing this full fingerprint.${noWait ? "" : " Waiting for approval..."}`);
84996
85213
  } else {
84997
85214
  writeFields(deps, "stderr", [
84998
85215
  ["Key", keyPrefix],
@@ -85004,9 +85221,11 @@ async function teeSignerNew(args, ctx) {
85004
85221
  ], "Key signer request");
85005
85222
  notice(deps, "stderr", "warning", "The account owner approves it, typing this full fingerprint.");
85006
85223
  notice(deps, "stderr", "note", `Or, on the owner's machine:
85007
- candle keys signer approve ${body.userCode} --key ${keyPrefix}
85008
- Waiting for approval...`);
85224
+ candle keys signer approve ${body.userCode} --key ${keyPrefix}${noWait ? "" : `
85225
+ Waiting for approval...`}`);
85009
85226
  }
85227
+ if (noWait)
85228
+ return 0;
85010
85229
  while (deps.now() < body.expiresAt) {
85011
85230
  await deps.sleep(SIGNER_POLL_MS);
85012
85231
  const read = await readSigner(ctx, "self", { apiKey });
@@ -85015,9 +85234,9 @@ Waiting for approval...`);
85015
85234
  view = read.body;
85016
85235
  const now = activeSigner(view);
85017
85236
  if (now !== null && now.spkiSha256 === entry.spkiSha256) {
85018
- const recorded = { ...entry, signerQuorumId: now.signerQuorumId };
85237
+ const recorded = { ...entry, signerQuorumId: now.signerQuorumId, pendingRequest: undefined };
85019
85238
  await saveKeySignerEntry(deps, recorded);
85020
- return reportApproved(ctx, view, recorded, { resumed, requested: true, replaces });
85239
+ return reportApproved(ctx, view, recorded, { resumed, requested: cached === undefined, replaces });
85021
85240
  }
85022
85241
  if (view.pending === null || view.pending.spkiSha256 !== entry.spkiSha256) {
85023
85242
  writeScreenLocalFailure(deps, {
@@ -85037,6 +85256,58 @@ Waiting for approval...`);
85037
85256
  }, json);
85038
85257
  return 1;
85039
85258
  }
85259
+ function matchingPendingRequest(ctx, view, entry) {
85260
+ const pending = entry.pendingRequest;
85261
+ return pending?.apiUrl === ctx.apiUrl && pending.expiresAt > ctx.deps.now() && view.pending?.spkiSha256 === entry.spkiSha256 && view.pending.expiresAt === pending.expiresAt ? pending : undefined;
85262
+ }
85263
+ async function teeSignerStatus(args, ctx) {
85264
+ const { deps, json } = ctx;
85265
+ const parsed = parseArgs(args, { valueFlags: ["--key"] });
85266
+ if ("error" in parsed || !parsed.values["--key"] || parsed.positionals.length > 0) {
85267
+ writeScreenUsageFailure(deps, USAGE_TEE_SIGNER, json);
85268
+ return 2;
85269
+ }
85270
+ await printIdentity(ctx);
85271
+ const apiKey = await resolveApiKey(deps, ctx.profile);
85272
+ if (!apiKey) {
85273
+ writeScreenLocalFailure(deps, { code: "NO_API_KEY", message: "tee signer status needs this key's API key." }, json);
85274
+ return 1;
85275
+ }
85276
+ const read = await readSigner(ctx, "self", { apiKey });
85277
+ if (!read.ok)
85278
+ return writeSignerReadFailure(ctx, read);
85279
+ const view = read.body;
85280
+ const keyRaw = parsed.values["--key"];
85281
+ if (keyRaw !== view.keyPrefix && keyRaw !== view.keyLabel) {
85282
+ writeScreenLocalFailure(deps, { code: "KEY_SIGNER_KEY_MISMATCH", message: `This API key is ${view.keyPrefix}, not ${keyRaw}.` }, json);
85283
+ return 1;
85284
+ }
85285
+ const locals = await localKeySigners(deps, view.keyPrefix);
85286
+ const entry = locals.find((local) => local.spkiSha256 === view.pending?.spkiSha256);
85287
+ const pending = entry ? matchingPendingRequest(ctx, view, entry) : undefined;
85288
+ if (json) {
85289
+ deps.stdout.write(`${JSON.stringify({
85290
+ ok: true,
85291
+ command: "tee signer status",
85292
+ ...view,
85293
+ ...pending ? {
85294
+ userCode: pending.userCode,
85295
+ verificationUriComplete: pending.verificationUriComplete,
85296
+ expiresAt: pending.expiresAt
85297
+ } : {}
85298
+ })}
85299
+ `);
85300
+ } else {
85301
+ writeFields(deps, "stdout", [
85302
+ ["Key", view.keyPrefix],
85303
+ ["State", view.state],
85304
+ ["Fingerprint", view.pending?.fingerprint ?? view.fingerprint],
85305
+ ["Approve at", pending?.verificationUriComplete],
85306
+ ["Expires", view.pending?.expiresAt ? formatTime(view.pending.expiresAt, { now: deps.now() }) : undefined]
85307
+ ], "Key signer status");
85308
+ }
85309
+ return 0;
85310
+ }
85040
85311
  async function writeOut(ctx, out, entry) {
85041
85312
  if (out === undefined)
85042
85313
  return;
@@ -89541,6 +89812,9 @@ var REASON_WORDS = {
89541
89812
  delegation_off: "the embedded wallet is not delegated for agent access",
89542
89813
  embedded_wallet_not_permitted: "this key may not use the embedded wallet",
89543
89814
  not_recipient: "the fees go to someone else, or the token has no fee vault",
89815
+ fee_wallet_designated: "the fees go to this launch's designated fee wallet, which claims them itself",
89816
+ claimable_by_key: "claimable by this key",
89817
+ claimable_via_web_session: "the fees go to this account's web wallet; a signed-in session claims them",
89544
89818
  insufficient_gas: "the recipient wallet cannot cover the network fee"
89545
89819
  };
89546
89820
  function reasonText(reason, missing) {
@@ -89575,7 +89849,7 @@ async function feesList(args, ctx) {
89575
89849
  notice(ctx.deps, "stdout", "note", "No launched tokens on this account.");
89576
89850
  else {
89577
89851
  const rows = page.map((row) => {
89578
- const status = row.unreadable ? "could not be read" : row.claimable ? `claimable: ${amountsText(row.claimableAmounts, missing)}` : `not claimable: ${reasonText(row.reason, missing)}`;
89852
+ const status = row.unreadable ? "could not be read" : row.claimable ? `${row.reason === "claimable_by_key" ? "claimable by this key" : "claimable"}: ${amountsText(row.claimableAmounts, missing)}` : `not claimable: ${reasonText(row.reason, missing)}`;
89579
89853
  return [
89580
89854
  safeText(String(row.symbol ?? row.name ?? row.mint)),
89581
89855
  safeText(String(row.chain)),
@@ -89612,7 +89886,53 @@ async function feesClaim(args, ctx) {
89612
89886
  const dryRun = parsed.booleans.has("--dry-run");
89613
89887
  try {
89614
89888
  const key = await tradingKey(ctx);
89615
- const body = await request(ctx, key, `/api/v1/agent/fees/${encodeURIComponent(mint)}/claim`, { dryRun });
89889
+ let body = await request(ctx, key, `/api/v1/agent/fees/${encodeURIComponent(mint)}/claim`, {
89890
+ dryRun,
89891
+ teeSigning: true
89892
+ });
89893
+ if (!dryRun && Array.isArray(body.signing) && body.signing.length > 0) {
89894
+ if (body.signing.length > 2)
89895
+ throw new TradingError("INVALID_RESPONSE", "Too many fee claims to sign");
89896
+ const { rows, appId, keyPrefix } = await listTradingWallets(ctx, key, "launch:write");
89897
+ const signedClaims = [];
89898
+ for (const signing of body.signing) {
89899
+ const row = rows.find((row2) => row2.id === signing.walletId && row2.address === signing.wallet);
89900
+ if (!row || row.chain !== "solana")
89901
+ throw new TradingError("TEE_WALLET_REQUIRED", "The claim's wallet is not bound to this key");
89902
+ const wallet3 = await completeTradingWallet(ctx, row, appId, "account:read", "solana", {
89903
+ apiKey: key,
89904
+ keyPrefix
89905
+ });
89906
+ const rpc = signing.body;
89907
+ const transaction = rpc?.params?.transaction;
89908
+ if (typeof transaction !== "string" || !["streamer", "creator"].includes(String(signing.leg)))
89909
+ throw new TradingError("INVALID_RESPONSE", "The fee claim has no Solana transaction to sign");
89910
+ const approval = authorizationSignature(wallet3, transaction);
89911
+ if (JSON.stringify(approval.body) !== JSON.stringify(signing.body))
89912
+ throw new TradingError("INVALID_RESPONSE", "The fee claim's RPC body is not the exact Solana sign request");
89913
+ const signed = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(wallet3.id)}/sign`, {
89914
+ ...approval,
89915
+ feeClaim: true
89916
+ });
89917
+ if (typeof signed.signedTransaction !== "string" || signed.encoding !== "base64")
89918
+ throw new TradingError("INVALID_RESPONSE", "The relay returned no base64 signed fee claim");
89919
+ signedClaims.push({ walletId: wallet3.id, leg: signing.leg, signedTransaction: signed.signedTransaction });
89920
+ }
89921
+ const submitted = await request(ctx, key, `/api/v1/agent/fees/${encodeURIComponent(mint)}/claim/submit`, {
89922
+ signedClaims
89923
+ });
89924
+ body = {
89925
+ ...submitted,
89926
+ claimed: [
89927
+ ...Array.isArray(body.claimed) ? body.claimed : [],
89928
+ ...Array.isArray(submitted.claimed) ? submitted.claimed : []
89929
+ ],
89930
+ warnings: [
89931
+ ...Array.isArray(body.warnings) ? body.warnings : [],
89932
+ ...Array.isArray(submitted.warnings) ? submitted.warnings : []
89933
+ ]
89934
+ };
89935
+ }
89616
89936
  if (ctx.json)
89617
89937
  return printTradingResult(ctx, body);
89618
89938
  const style = terminalStyle(ctx.deps, "stdout");
@@ -89689,6 +90009,7 @@ init_render();
89689
90009
  init_promote_support();
89690
90010
  init_key_output();
89691
90011
  init_keys();
90012
+ init_keys_embedded_wallet();
89692
90013
  init_tee_rebind();
89693
90014
  var KEYS_PATH4 = "/api/v1/agent/keys";
89694
90015
  var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|write|withdraw> [--yes] [--json]
@@ -89852,6 +90173,7 @@ async function keysAccess(args, ctx) {
89852
90173
  let path;
89853
90174
  let auth;
89854
90175
  let credentials;
90176
+ let embeddedWallet;
89855
90177
  if (self) {
89856
90178
  const apiKey = await resolveApiKey(deps, ctx.profile);
89857
90179
  if (!apiKey) {
@@ -89873,6 +90195,7 @@ async function keysAccess(args, ctx) {
89873
90195
  });
89874
90196
  if (!resolved.ok)
89875
90197
  return resolved.code;
90198
+ embeddedWallet = resolved.embeddedWallet;
89876
90199
  path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
89877
90200
  auth = "device";
89878
90201
  credentials = { deviceToken };
@@ -89911,6 +90234,9 @@ async function keysAccess(args, ctx) {
89911
90234
  const account = fields2.account ? shortAddress2(fields2.account) : json ? "unknown" : symbols(terminalStyle(deps, "stderr")).missing;
89912
90235
  deps.stderr.write(`${accessScreen(shown, { account: fields2.username ? `${fields2.username} (${account})` : account, apiUrl }, json ? undefined : terminalStyle(deps, "stderr"))}
89913
90236
  `);
90237
+ if (shown.to.access === "readwritetransfer" && embeddedWallet === "denied") {
90238
+ notice(deps, "stderr", "warning", withdrawNeedsEmbeddedWalletLine(shown.keyPrefix));
90239
+ }
89914
90240
  if (widen) {
89915
90241
  const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
89916
90242
  if (typed.trim() !== shown.keyPrefix) {
@@ -90742,7 +91068,8 @@ function decodeAddress(value, flag) {
90742
91068
  throw new Error(`${flag} takes a Solana address.`);
90743
91069
  }
90744
91070
  }
90745
- function feeWalletLines(selection, echo) {
91071
+ var fromLauncher = (echo) => echo?.feeWalletSource === "launching_wallet" && typeof echo.feeWallet === "string";
91072
+ function feeWalletLines(selection, echo, launcherDefault = false) {
90746
91073
  if (selection)
90747
91074
  return [
90748
91075
  `Fee wallet: ${selection.feeWallet} (earns this launch's fees; set by --fee-wallet)`,
@@ -90750,13 +91077,20 @@ function feeWalletLines(selection, echo) {
90750
91077
  `Fee wallet multisig: ${selection.feeWalletMultisig.address}, vault ${selection.feeWalletMultisig.vaultIndex} (the Squads v4 multisig behind the fee wallet)`
90751
91078
  ] : []
90752
91079
  ];
91080
+ if (fromLauncher(echo))
91081
+ return [
91082
+ `Fee wallet: ${echo.feeWallet} (defaults to the launching wallet; pass --fee-wallet <address> to change it)`
91083
+ ];
91084
+ if (launcherDefault)
91085
+ return ["Fee wallet: defaults to the launching wallet (pass --fee-wallet <address> to change it)."];
90753
91086
  return [
90754
- echo ? `Fee wallet: ${echo} (defaults to the account wallet; pass --fee-wallet <address> to change it)` : "Fee wallet: defaults to the account wallet (pass --fee-wallet <address> to change it)."
91087
+ echo?.feeWallet ? `Fee wallet: ${echo.feeWallet} (defaults to the account wallet; pass --fee-wallet <address> to change it)` : "Fee wallet: defaults to the account wallet (pass --fee-wallet <address> to change it)."
90755
91088
  ];
90756
91089
  }
90757
- function feeWalletQuoteLine(selection) {
91090
+ var FEE_WALLET_DEFAULT_WARNING = "No --fee-wallet given: this launch's curve and pool fees earn to the launching wallet, or to the account wallet on a server that does not record the launching wallet; the confirmation names which. Pass --fee-wallet <address> to send them to another wallet.";
91091
+ function feeWalletQuoteLine(selection, echo) {
90758
91092
  if (!selection)
90759
- return "defaults to the account wallet";
91093
+ return fromLauncher(echo) ? `${echo.feeWallet} (the launching wallet, by default)` : "defaults to the account wallet";
90760
91094
  return `${selection.feeWallet}${selection.feeWalletMultisig ? ` (Squads vault of ${selection.feeWalletMultisig.address}, vault ${selection.feeWalletMultisig.vaultIndex})` : ""}`;
90761
91095
  }
90762
91096
 
@@ -90903,7 +91237,8 @@ var checkSchema = exports_external.object({
90903
91237
  switches: exports_external.object({
90904
91238
  headlessLaunch: exports_external.boolean(),
90905
91239
  agentTrading: exports_external.boolean(),
90906
- teeLaunchBuys: exports_external.boolean()
91240
+ teeLaunchBuys: exports_external.boolean(),
91241
+ feeWalletFromLauncher: exports_external.boolean().optional()
90907
91242
  }),
90908
91243
  embedded: exports_external.object({
90909
91244
  solana: exports_external.object({ address: exports_external.string(), delegated: exports_external.boolean().optional() }).nullable().optional(),
@@ -91015,8 +91350,9 @@ async function launchCheck(ctx, key, walletFlag, requestedChain, feeWallet = und
91015
91350
  const lines = [`Launch paths for key ${answer2.key.keyPrefix} (tier ${answer2.key.tier}):`];
91016
91351
  if (requestedChain)
91017
91352
  lines.push(`Requested launch chain: ${chainName(requestedChain)}.`);
91353
+ const launcherDefault = answer2.switches.feeWalletFromLauncher === true;
91018
91354
  if (!requestedChain || requestedChain === "solana")
91019
- lines.push(...feeWalletLines(feeWallet));
91355
+ lines.push(...feeWalletLines(feeWallet, undefined, launcherDefault));
91020
91356
  if (walletFlag === undefined || embeddedOnly)
91021
91357
  lines.push(...embeddedLines(answer2, requestedChain));
91022
91358
  for (const row of shownWallets)
@@ -91030,7 +91366,7 @@ async function launchCheck(ctx, key, walletFlag, requestedChain, feeWallet = und
91030
91366
  ...requestedChain ? { requestedChain } : {},
91031
91367
  key: answer2.key,
91032
91368
  switches: answer2.switches,
91033
- ...feeWallet ? { feeWallet: { ...feeWallet, set: true } } : { feeWallet: { default: "account wallet", set: false } },
91369
+ ...feeWallet ? { feeWallet: { ...feeWallet, set: true } } : { feeWallet: { default: launcherDefault ? "launching wallet" : "account wallet", set: false } },
91034
91370
  ...answer2.embedded ? { embedded: answer2.embedded } : {},
91035
91371
  wallets: shownWallets.map((row) => ({
91036
91372
  ...row,
@@ -91109,7 +91445,8 @@ var LAUNCH_ARG_SPEC = {
91109
91445
  booleanFlags: [
91110
91446
  ...launchFlagFields.filter((field) => field.type === "boolean").map((field) => field.cli.flag),
91111
91447
  "--check",
91112
- "--yes"
91448
+ "--yes",
91449
+ "--require-fee-wallet"
91113
91450
  ],
91114
91451
  pathFlags: ["--image", "--banner"]
91115
91452
  };
@@ -91251,6 +91588,10 @@ async function launch(args, ctx) {
91251
91588
  writeUsageFailure(ctx.deps, `${error.message}. ${USAGE9}`, ctx.json);
91252
91589
  return 2;
91253
91590
  }
91591
+ if (parsed.booleans.has("--require-fee-wallet") && !feeWallet) {
91592
+ writeUsageFailure(ctx.deps, `--require-fee-wallet needs --fee-wallet <address>: pass the wallet this launch's fees earn to. ${USAGE9}`, ctx.json);
91593
+ return 2;
91594
+ }
91254
91595
  try {
91255
91596
  if (flags["--buy"]?.trim() && !/^0+(\.0+)?$/.test(flags["--buy"]))
91256
91597
  launchBuyRaw(flags["--buy"], 18);
@@ -91318,6 +91659,8 @@ async function launch(args, ctx) {
91318
91659
  const chain2 = wallet3.chain === "evm" ? "hood" : "solana";
91319
91660
  if (chain2 === "hood" && flags["--dex-version"] === undefined)
91320
91661
  throw new TradingUsage(`${wallet3.address} is a Hood TEE wallet, and a Hood launch needs --dex-version v3|v4.`);
91662
+ if (chain2 === "solana" && !dryRun && !feeWallet)
91663
+ notice(ctx.deps, "stderr", "warning", FEE_WALLET_DEFAULT_WARNING);
91321
91664
  const buy = launchBuy(flags, chain2);
91322
91665
  if (chain2 === "hood" && (Object.keys(poolRequestFields(flags)).length || feeWalletGiven(flags)))
91323
91666
  throw new TradingUsage("Pool fee and pool shape choices are Solana only, and so is --fee-wallet; a Hood launch cannot take them.");
@@ -91370,7 +91713,7 @@ async function launch(args, ctx) {
91370
91713
  fee: built.fee ?? { bps: 0, feeRaw: "0" },
91371
91714
  minimumReceived: buy ? "not quoted (the first buy on the new curve)" : "0 tokens (no first buy)",
91372
91715
  maxDebitLamports: built.maxDebitLamports,
91373
- feeWallet: feeWalletQuoteLine(feeWallet),
91716
+ feeWallet: feeWalletQuoteLine(feeWallet, built),
91374
91717
  tokenRisks: []
91375
91718
  };
91376
91719
  notice(ctx.deps, "stderr", "note", buyNotice(buy, "solana").trim());
@@ -91500,14 +91843,19 @@ var previewSchema = exports_external.object({
91500
91843
  }).passthrough().optional(),
91501
91844
  devBuyMax: exports_external.string().nullable().optional(),
91502
91845
  feeWallet: exports_external.string().optional(),
91846
+ feeWalletSource: exports_external.string().optional(),
91503
91847
  fills: exports_external.array(exports_external.object({ index: exports_external.number(), amountRaw: exports_external.string(), supplyPct: exports_external.number() }).passthrough()).optional()
91504
91848
  }).passthrough();
91505
91849
  var percent = (value) => `${Number(value.toFixed(4))}%`;
91850
+ function feeWalletEchoOf(answer2) {
91851
+ const parsed = exports_external.object({ feeWallet: exports_external.string().optional(), feeWalletSource: exports_external.string().optional() }).safeParse(answer2);
91852
+ return parsed.success ? parsed.data : undefined;
91853
+ }
91506
91854
  function previewLines(answer2, chain2, feeWallet = undefined) {
91507
91855
  const parsed = previewSchema.safeParse(answer2);
91508
91856
  if (!parsed.success)
91509
91857
  return "";
91510
- const { resolved, curveTerms: terms, devBuyMax, feeWallet: echo, fills } = parsed.data;
91858
+ const { resolved, curveTerms: terms, devBuyMax, feeWallet: echo, feeWalletSource, fills } = parsed.data;
91511
91859
  const quote = (resolved?.quoteAsset ?? LAUNCH_DEFINITION.defaultQuote[chain2]).toUpperCase();
91512
91860
  const decimals = (chain2 === "hood" ? HOOD_BASES[quote] : BASES[quote])?.decimals;
91513
91861
  const whole = (raw) => decimals === undefined ? `${raw} raw` : `${shortAmount(raw, decimals)} ${quote}`;
@@ -91526,7 +91874,7 @@ function previewLines(answer2, chain2, feeWallet = undefined) {
91526
91874
  if (devBuyMax)
91527
91875
  lines.push(`Dev-buy ceiling: ${whole(devBuyMax)}.`);
91528
91876
  if (chain2 === "solana")
91529
- lines.push(...feeWalletLines(feeWallet, echo));
91877
+ lines.push(...feeWalletLines(feeWallet, { feeWallet: echo, feeWalletSource }));
91530
91878
  return lines.length ? `${lines.join(`
91531
91879
  `)}
91532
91880
  ` : "";
@@ -91592,7 +91940,9 @@ var atomicLegSchema = exports_external.object({
91592
91940
  var atomicBuildSchema = exports_external.object({
91593
91941
  bundleId: exports_external.string().min(1),
91594
91942
  legs: exports_external.array(atomicLegSchema),
91595
- expiresAt: exports_external.number().finite()
91943
+ expiresAt: exports_external.number().finite(),
91944
+ feeWallet: exports_external.string().optional(),
91945
+ feeWalletSource: exports_external.string().optional()
91596
91946
  });
91597
91947
  var atomicOutcomeSchema = exports_external.discriminatedUnion("status", [
91598
91948
  exports_external.object({ status: exports_external.literal("landed"), bundleId: exports_external.string(), mint: exports_external.string(), signatures: exports_external.array(exports_external.string()) }).passthrough(),
@@ -91636,7 +91986,7 @@ async function atomicLaunch(ctx, key, args) {
91636
91986
  candleFee: reading.feeRaw > 0n ? `${decimalAmount(reading.feeRaw.toString(), decimals)} ${quoteAsset} (the tier fee on the first buy, in its leg)` : "none on this first buy",
91637
91987
  minimumReceived: minOut !== undefined ? `${minOut} raw units of the new token (the first buy's floor)` : "not quoted",
91638
91988
  maxLoss: quoteAsset === "SOL" ? sol : `${sol} and ${decimalAmount(reading.quoteRaw.toString(), decimals)} ${quoteAsset}`,
91639
- feeWallet: feeWalletQuoteLine(feeWalletSelection(flags)),
91989
+ feeWallet: feeWalletQuoteLine(feeWalletSelection(flags), built),
91640
91990
  tokenRisks: [],
91641
91991
  bundleId: built.bundleId
91642
91992
  };
@@ -91767,6 +92117,8 @@ async function embeddedLaunch(ctx, key, args) {
91767
92117
  firstBuyCeiling(ctx, flags, buy);
91768
92118
  if (chain2 === "hood" && (Object.keys(poolRequestFields(flags)).length || feeWalletGiven(flags)))
91769
92119
  throw new TradingUsage("Pool fee and pool shape choices are Solana only, and so is --fee-wallet; a Hood launch cannot take them.");
92120
+ if (chain2 === "solana" && !args.dryRun && !feeWalletGiven(flags))
92121
+ notice(ctx.deps, "stderr", "warning", FEE_WALLET_DEFAULT_WARNING);
91770
92122
  const wallet3 = await embeddedLaunchWallet(ctx, key, chain2);
91771
92123
  const uploaded = await uploadLaunchImages(ctx, key, args);
91772
92124
  if (typeof uploaded === "number")
@@ -91796,7 +92148,7 @@ async function embeddedLaunch(ctx, key, args) {
91796
92148
  venue: "curve launch (signed by Candle)",
91797
92149
  priceImpactPct: null,
91798
92150
  minimumReceived: "0 tokens (no first buy)",
91799
- ...chain2 === "solana" ? { feeWallet: feeWalletQuoteLine(feeWalletSelection(flags)) } : {},
92151
+ ...chain2 === "solana" ? { feeWallet: feeWalletQuoteLine(feeWalletSelection(flags), feeWalletEchoOf(answer2)) } : {},
91800
92152
  tokenRisks: []
91801
92153
  };
91802
92154
  if (buy && chain2 === "solana") {
@@ -92023,6 +92375,92 @@ async function resumeHoodLaunch(ctx, key, id, operationId, prior) {
92023
92375
  return printTradingResult(ctx, { ...result, clientTradeId: id, kind: "launch", chain: "hood", operationId });
92024
92376
  }
92025
92377
 
92378
+ // src/commands/launch-status.ts
92379
+ init_args();
92380
+ init_presentation();
92381
+ init_render();
92382
+ init_trading();
92383
+ init_swap();
92384
+ var USAGE10 = "Usage: candle launch status <mint-or-client-launch-id> [--wait]";
92385
+ var LAUNCH_WAIT_MS = 10 * 60 * 1000;
92386
+ var LAUNCH_WAIT_POLL_MS = 1e4;
92387
+ var LIVE = new Set(["trading", "bonded", "migrated"]);
92388
+ function statusOf(body) {
92389
+ const status = body.status;
92390
+ return status !== null && typeof status === "object" && !Array.isArray(status) ? status : null;
92391
+ }
92392
+ function isFinal(status) {
92393
+ if (!status)
92394
+ return false;
92395
+ if (status.failed === true)
92396
+ return true;
92397
+ return typeof status.state === "string" && LIVE.has(status.state);
92398
+ }
92399
+ function humanLines(status) {
92400
+ const lines = [`state: ${safeText(String(status.state ?? ""))}`];
92401
+ if (status.failed === true)
92402
+ lines.push("failed: true");
92403
+ if (typeof status.errorCode === "string")
92404
+ lines.push(`error: ${safeText(status.errorCode)}`);
92405
+ if (typeof status.chain === "string")
92406
+ lines.push(`chain: ${safeText(status.chain)}`);
92407
+ if (typeof status.mint === "string")
92408
+ lines.push(`mint: ${safeText(status.mint)}`);
92409
+ if (typeof status.clientLaunchId === "string")
92410
+ lines.push(`clientLaunchId: ${safeText(status.clientLaunchId)}`);
92411
+ if (typeof status.lifecycle === "string")
92412
+ lines.push(`lifecycle: ${safeText(status.lifecycle)}`);
92413
+ const migration = status.migration;
92414
+ if (migration !== null && typeof migration === "object" && !Array.isArray(migration)) {
92415
+ const block2 = migration;
92416
+ const progress = typeof block2.progressBps === "number" ? ` ${String(block2.progressBps)} bps` : "";
92417
+ if (typeof block2.status === "string")
92418
+ lines.push(`migration: ${safeText(block2.status)}${progress}`);
92419
+ }
92420
+ if (typeof status.signature === "string")
92421
+ lines.push(`signature: ${safeText(status.signature)}`);
92422
+ return lines;
92423
+ }
92424
+ async function readStatus(ctx, key, id) {
92425
+ return request(ctx, key, `/api/v1/launch/status/${encodeURIComponent(id)}`);
92426
+ }
92427
+ async function launchStatus(args, ctx) {
92428
+ const parsed = parseArgs(args, { booleanFlags: ["--wait"] });
92429
+ if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "")) {
92430
+ writeUsageFailure(ctx.deps, USAGE10, ctx.json);
92431
+ return 2;
92432
+ }
92433
+ const id = parsed.positionals[0];
92434
+ const wait = parsed.booleans.has("--wait");
92435
+ try {
92436
+ const key = await tradingKey(ctx);
92437
+ let body = await readStatus(ctx, key, id);
92438
+ if (wait) {
92439
+ const deadline = ctx.deps.now() + LAUNCH_WAIT_MS;
92440
+ while (!isFinal(statusOf(body)) && ctx.deps.now() < deadline) {
92441
+ await ctx.deps.sleep(LAUNCH_WAIT_POLL_MS);
92442
+ body = await readStatus(ctx, key, id);
92443
+ }
92444
+ const final = isFinal(statusOf(body));
92445
+ if (!final) {
92446
+ notice(ctx.deps, "stderr", "note", `Still not trading after ten minutes. Re-check with candle launch status ${safeText(id)} --wait.`);
92447
+ }
92448
+ body = { ...body, waited: { final } };
92449
+ }
92450
+ if (ctx.json)
92451
+ return printTradingResult(ctx, body);
92452
+ const status = statusOf(body);
92453
+ if (!status)
92454
+ return printTradingResult(ctx, body);
92455
+ ctx.deps.stdout.write(`${humanLines(status).join(`
92456
+ `)}
92457
+ `);
92458
+ return 0;
92459
+ } catch (error) {
92460
+ return tradingFailure(ctx, error);
92461
+ }
92462
+ }
92463
+
92026
92464
  // src/commands/lp.ts
92027
92465
  init_args();
92028
92466
  init_client();
@@ -92485,6 +92923,7 @@ var MCP_TOOL_NAMES = [
92485
92923
  "candle_execution_status",
92486
92924
  "candle_get_operation",
92487
92925
  "candle_get_fees",
92926
+ "candle_atomic_launch_dry_run",
92488
92927
  "candle_claim_fees",
92489
92928
  "candle_get_staking",
92490
92929
  "candle_stake",
@@ -97883,7 +98322,7 @@ init_swap();
97883
98322
  init_vault_support();
97884
98323
  var WITHDRAW_SCOPE = "perps:withdraw";
97885
98324
  var ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
97886
- var USAGE10 = "Usage: candle perps withdraw <amount> --to solana|hood [--wallet <evm tee>] [--id <id>] [--keystore <path>] | candle perps withdraw status <id>";
98325
+ var USAGE11 = "Usage: candle perps withdraw <amount> --to solana|hood [--wallet <evm tee>] [--id <id>] [--keystore <path>] | candle perps withdraw status <id>";
97887
98326
  var WITHDRAW_POLL_MS = 10 * 60 * 1000;
97888
98327
  var WITHDRAW_POLL_INTERVAL_MS = 5000;
97889
98328
  var TERMINAL = new Set(["settled", "refunded", "failed"]);
@@ -97934,7 +98373,7 @@ async function withdrawStatus(ctx, id) {
97934
98373
  async function perpsWithdraw(args, ctx) {
97935
98374
  if (args[0] === "status") {
97936
98375
  if (args.length !== 2 || !ID_RE.test(args[1]))
97937
- return usage10(ctx, USAGE10);
98376
+ return usage10(ctx, USAGE11);
97938
98377
  try {
97939
98378
  return await withdrawStatus(ctx, args[1]);
97940
98379
  } catch (error) {
@@ -97953,7 +98392,7 @@ async function perpsWithdraw(args, ctx) {
97953
98392
  const [amount] = parsed.positionals;
97954
98393
  const to = parsed.values["--to"];
97955
98394
  if (parsed.positionals.length !== 1 || !amount || to !== "solana" && to !== "hood")
97956
- return usage10(ctx, USAGE10);
98395
+ return usage10(ctx, USAGE11);
97957
98396
  const amountMicros = parseWithdrawAmount(amount);
97958
98397
  if (amountMicros === null)
97959
98398
  return usage10(ctx, "The amount must be a positive USDC decimal with at most 6 decimals.");
@@ -99841,12 +100280,12 @@ function planPromotionLine2(table) {
99841
100280
  init_presentation();
99842
100281
  init_render();
99843
100282
  init_key_output();
99844
- var USAGE11 = "Usage: candle plans";
100283
+ var USAGE12 = "Usage: candle plans";
99845
100284
  async function plans(args, ctx) {
99846
100285
  const parsed = parseArgs(args, {});
99847
100286
  if ("error" in parsed || parsed.positionals.length > 0) {
99848
100287
  writeScreenUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
99849
- ${USAGE11}` : USAGE11, ctx.json);
100288
+ ${USAGE12}` : USAGE12, ctx.json);
99850
100289
  return 2;
99851
100290
  }
99852
100291
  const result = await apiRequest("/api/v1/agent/plans", {
@@ -102120,7 +102559,7 @@ init_client();
102120
102559
  init_human_output();
102121
102560
  init_presentation();
102122
102561
  init_render();
102123
- var USAGE12 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
102562
+ var USAGE13 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
102124
102563
  var OTHER_CHAIN = "Candle trades Solana and Hood today; that is not a Solana or Hood contract address.";
102125
102564
  var HOOD_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
102126
102565
  var SOLANA_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
@@ -102220,20 +102659,20 @@ async function scan(args, ctx) {
102220
102659
  }
102221
102660
  const named = parsed.values["--chain"];
102222
102661
  if (parsed.positionals.length !== 1 || named !== undefined && named !== "solana" && named !== "hood") {
102223
- writeUsageFailure(ctx.deps, USAGE12, ctx.json);
102662
+ writeUsageFailure(ctx.deps, USAGE13, ctx.json);
102224
102663
  return 2;
102225
102664
  }
102226
102665
  const address = parsed.positionals[0].trim();
102227
102666
  const found = chainOfAddress(address);
102228
102667
  if (found === null) {
102229
102668
  writeUsageFailure(ctx.deps, `${OTHER_CHAIN}
102230
- ${USAGE12}`, ctx.json);
102669
+ ${USAGE13}`, ctx.json);
102231
102670
  return 2;
102232
102671
  }
102233
102672
  if (named !== undefined && named !== found) {
102234
102673
  const label = (c) => c === "hood" ? "Hood" : "Solana";
102235
102674
  writeUsageFailure(ctx.deps, `That address is a ${label(found)} contract, not a ${label(named)} one. Candle trades Solana and Hood today.
102236
- ${USAGE12}`, ctx.json);
102675
+ ${USAGE13}`, ctx.json);
102237
102676
  return 2;
102238
102677
  }
102239
102678
  const chain2 = found;
@@ -104111,7 +104550,7 @@ init_deps();
104111
104550
  init_human_output();
104112
104551
  init_render();
104113
104552
  var KINDS = ["trade", "signal", "callout"];
104114
- var USAGE13 = "Usage: candle token events <mint> [--chain solana|hood] [--kind trade|signal|callout] [--since <iso-8601>] [--limit <n>] [--before <cursor>] [--json]";
104553
+ var USAGE14 = "Usage: candle token events <mint> [--chain solana|hood] [--kind trade|signal|callout] [--since <iso-8601>] [--limit <n>] [--before <cursor>] [--json]";
104115
104554
  function readEventsBody(body) {
104116
104555
  if (body === null || typeof body !== "object" || !Array.isArray(body.events))
104117
104556
  return null;
@@ -104169,11 +104608,11 @@ async function tokenEvents(args, ctx) {
104169
104608
  const since = parsed.values["--since"];
104170
104609
  const before = parsed.values["--before"];
104171
104610
  if (parsed.positionals.length !== 1 || chain2 !== "solana" && chain2 !== "hood") {
104172
- writeUsageFailure(ctx.deps, USAGE13, ctx.json);
104611
+ writeUsageFailure(ctx.deps, USAGE14, ctx.json);
104173
104612
  return 2;
104174
104613
  }
104175
104614
  if (kind !== undefined && !KINDS.includes(kind)) {
104176
- writeUsageFailure(ctx.deps, USAGE13, ctx.json);
104615
+ writeUsageFailure(ctx.deps, USAGE14, ctx.json);
104177
104616
  return 2;
104178
104617
  }
104179
104618
  if (limit !== undefined && !/^[1-9][0-9]{0,3}$/.test(limit) || Number(limit) > 1000) {
@@ -104270,7 +104709,7 @@ var TOKEN_PAGE_BODY_CAP_BYTES = 32 * 1024;
104270
104709
  var TOKEN_PAGE_SECTIONS = ["thesis", "tokenomics", "lore"];
104271
104710
  var FLAG_REASONS = ["spam", "scam", "abuse", "misleading", "unsafe_link", "other"];
104272
104711
  var FLAG_DETAIL_LIMIT = 500;
104273
- var USAGE14 = {
104712
+ var USAGE15 = {
104274
104713
  about: "Usage: candle token about <address> [--chain solana|hood] [--capabilities]",
104275
104714
  claim: "Usage: candle token claim <address> [--chain solana|hood] [--creator [--challenge [--message-out <path>] | --challenge-id <id> --signature <sig>]] [--idempotency-key <key>]",
104276
104715
  section: `Usage: candle token section set <address> <thesis|tokenomics|lore> --file <path> --base-revision <n> [--clear] [--chain solana|hood] [--idempotency-key <key>]
@@ -104530,7 +104969,7 @@ function terminalTextKeepingLines(text4) {
104530
104969
  `);
104531
104970
  }
104532
104971
  async function tokenAbout(args, ctx) {
104533
- const usage12 = usageFor(ctx, USAGE14.about);
104972
+ const usage12 = usageFor(ctx, USAGE15.about);
104534
104973
  const parsed = parseArgs(args, { valueFlags: ["--chain"], booleanFlags: ["--capabilities"] });
104535
104974
  if ("error" in parsed)
104536
104975
  return usage12(parsed.error);
@@ -104576,7 +105015,7 @@ async function tokenAbout(args, ctx) {
104576
105015
  return 0;
104577
105016
  }
104578
105017
  async function tokenClaim(args, ctx) {
104579
- const usage12 = usageFor(ctx, USAGE14.claim);
105018
+ const usage12 = usageFor(ctx, USAGE15.claim);
104580
105019
  const parsed = parseArgs(args, {
104581
105020
  valueFlags: ["--chain", "--challenge-id", "--signature", "--message-out", "--idempotency-key"],
104582
105021
  booleanFlags: ["--creator", "--challenge"],
@@ -104703,7 +105142,7 @@ var SECTION_FLAGS = {
104703
105142
  restore: { valueFlags: ["--chain", "--version-id", "--base-revision", "--idempotency-key"], booleanFlags: [] }
104704
105143
  };
104705
105144
  async function tokenSection(args, ctx) {
104706
- const usage12 = usageFor(ctx, USAGE14.section);
105145
+ const usage12 = usageFor(ctx, USAGE15.section);
104707
105146
  const action2 = args[0];
104708
105147
  if (action2 !== "set" && action2 !== "restore")
104709
105148
  return usage12();
@@ -104796,7 +105235,7 @@ async function tokenSection(args, ctx) {
104796
105235
  return 0;
104797
105236
  }
104798
105237
  async function tokenHistory(args, ctx) {
104799
- const usage12 = usageFor(ctx, USAGE14.history);
105238
+ const usage12 = usageFor(ctx, USAGE15.history);
104800
105239
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
104801
105240
  if ("error" in parsed)
104802
105241
  return usage12(parsed.error);
@@ -104851,7 +105290,7 @@ More: --cursor ${data.nextCursor}
104851
105290
  return 0;
104852
105291
  }
104853
105292
  async function tokenFlag(args, ctx) {
104854
- const usage12 = usageFor(ctx, USAGE14.flag);
105293
+ const usage12 = usageFor(ctx, USAGE15.flag);
104855
105294
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--content", "--reason", "--detail", "--idempotency-key"] });
104856
105295
  if ("error" in parsed)
104857
105296
  return usage12(parsed.error);
@@ -104946,7 +105385,7 @@ async function tokenUpdate(args, ctx) {
104946
105385
  return 0;
104947
105386
  }
104948
105387
  async function tokenComment(args, ctx) {
104949
- const usage12 = usageFor(ctx, USAGE14.comment);
105388
+ const usage12 = usageFor(ctx, USAGE15.comment);
104950
105389
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--file", "--idempotency-key"], pathFlags: ["--file"] });
104951
105390
  if ("error" in parsed)
104952
105391
  return usage12(parsed.error);
@@ -104991,7 +105430,7 @@ async function tokenComment(args, ctx) {
104991
105430
  return 0;
104992
105431
  }
104993
105432
  async function tokenComments(args, ctx) {
104994
- const usage12 = usageFor(ctx, USAGE14.comments);
105433
+ const usage12 = usageFor(ctx, USAGE15.comments);
104995
105434
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
104996
105435
  if ("error" in parsed)
104997
105436
  return usage12(parsed.error);
@@ -105337,7 +105776,7 @@ init_render();
105337
105776
  init_solana_verify();
105338
105777
  init_trading();
105339
105778
  init_swap();
105340
- var USAGE15 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
105779
+ var USAGE16 = "Usage: candle transfer --to <address|wallet name|allowlist label|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
105341
105780
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
105342
105781
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
105343
105782
  async function readLinkedWallets(ctx, key) {
@@ -105357,6 +105796,12 @@ async function readLinkedWallets(ctx, key) {
105357
105796
  }
105358
105797
  return rows;
105359
105798
  }
105799
+ async function readWithdrawalAddresses(ctx, key) {
105800
+ const body = await request(ctx, key, "/api/v1/agent/withdrawal-addresses");
105801
+ if (!Array.isArray(body.addresses))
105802
+ throw new TradingError("INVALID_RESPONSE", "The withdrawal allowlist read did not return a list.");
105803
+ return body.addresses;
105804
+ }
105360
105805
  async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
105361
105806
  const hood = chain2 === "hood";
105362
105807
  const same = (a, b) => hood ? sameEvmAddress(a, b) : a === b;
@@ -105382,13 +105827,35 @@ async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
105382
105827
  return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
105383
105828
  if (same(to, source.address))
105384
105829
  throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
105830
+ const allowlist = (await readWithdrawalAddresses(ctx, key)).filter((row) => row.chain === chain2 && row.revokedAt === undefined && !same(row.address, source.address));
105831
+ const approved = allowlist.filter((row) => row.label === to || row.id === to || same(row.address, to));
105832
+ if (approved.length > 1)
105833
+ throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${approved.length} approved withdrawal addresses: ${approved.map((row) => `${row.label ?? ""} (${row.id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
105834
+ const listed = approved[0];
105835
+ if (listed) {
105836
+ const address = hood ? checkEvmAddress(listed.address) : undefined;
105837
+ if (address !== undefined && !address.ok)
105838
+ throw new TradingError("INVALID_RESPONSE", "Candle listed an approved Hood withdrawal address that is not an EVM address; nothing was built.");
105839
+ return {
105840
+ kind: "approved",
105841
+ address: address?.ok ? address.address : listed.address,
105842
+ id: listed.id,
105843
+ ...listed.label ? { label: listed.label } : {}
105844
+ };
105845
+ }
105385
105846
  if (!hood && BASE58_ADDRESS.test(to))
105386
105847
  return { kind: "address", address: to };
105387
105848
  const evm = hood ? checkEvmAddress(to) : undefined;
105388
105849
  if (evm?.ok)
105389
105850
  return { kind: "address", address: evm.address };
105390
105851
  const name2 = hood ? "Hood" : "Solana";
105391
- throw new TradingError("DESTINATION_UNKNOWN", rows.length === 0 ? `"${to}" is not vault, a ${name2} address, or a linked wallet on this account (it has no other active ${name2} linked wallets).` : `"${to}" is not vault, a ${name2} address, or a linked wallet on this account. Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`);
105852
+ const known = [
105853
+ ...rows.length > 0 ? [`Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`] : [],
105854
+ ...allowlist.length > 0 ? [
105855
+ `Approved withdrawal addresses: ${allowlist.map((row) => `${row.label ?? ""} (${row.id}, ${row.address})`.trim()).join("; ")}.`
105856
+ ] : []
105857
+ ];
105858
+ throw new TradingError("DESTINATION_UNKNOWN", known.length === 0 ? `"${to}" is not vault, a ${name2} address, a linked wallet or an approved withdrawal address on this account (it has no other active ${name2} linked wallets or approved addresses).` : `"${to}" is not vault, a ${name2} address, a linked wallet or an approved withdrawal address on this account. ${known.join(" ")}`);
105392
105859
  }
105393
105860
  function describeDestination(destination) {
105394
105861
  switch (destination.kind) {
@@ -105396,10 +105863,31 @@ function describeDestination(destination) {
105396
105863
  return `this wallet's pinned vault ${destination.address}`;
105397
105864
  case "linked":
105398
105865
  return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
105866
+ case "approved":
105867
+ return `approved withdrawal address ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
105868
+ case "address":
105869
+ return `address ${destination.address} (not one of this account's linked wallets or approved withdrawal addresses; Candle decides whether it is allowed)`;
105870
+ }
105871
+ }
105872
+ function destinationNote(destination, hood) {
105873
+ switch (destination.kind) {
105874
+ case "vault":
105875
+ return "Destination: this wallet's own vault, pinned when it was imported.";
105876
+ case "linked":
105877
+ return `Destination: a linked wallet on this account. Candle allows it only if you linked it while signed in or marked it trusted,${hood ? " only for ETH or USDG," : ""} and the amount counts against this key's spend caps.`;
105878
+ case "approved":
105879
+ return `Destination: an address on this account's withdrawal allowlist, approved while signed in. Candle allows it from a TEE wallet only where TEE withdrawals are switched on,${hood ? " only for ETH or USDG," : " for base assets only,"} and the amount counts against this key's spend caps.`;
105399
105880
  case "address":
105400
- return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
105881
+ return "Destination: an address Candle will classify at build; from a TEE wallet only its vault, a trusted linked wallet or an approved withdrawal address is allowed.";
105401
105882
  }
105402
105883
  }
105884
+ function builtDestination(built) {
105885
+ if (built.destinationClass === "approved")
105886
+ return [["To", "an approved withdrawal address"]];
105887
+ if (typeof built.destinationKind === "string")
105888
+ return [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]];
105889
+ return [];
105890
+ }
105403
105891
  function writePreview(ctx, title2, rows, note) {
105404
105892
  const style = terminalStyle(ctx.deps, "stderr");
105405
105893
  ctx.deps.stderr.write(`${heading(title2, style)}
@@ -105433,7 +105921,7 @@ async function transfer(args, ctx) {
105433
105921
  const flags2 = parsed.values;
105434
105922
  const asset = flags2["--asset"]?.toUpperCase();
105435
105923
  if (parsed.positionals.length !== 0 || !flags2["--to"] || !flags2["--amount"] || [flags2["--asset"], flags2["--mint"], flags2["--token"]].filter(Boolean).length !== 1 || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags2["--mint"] !== undefined && !BASE58_ADDRESS.test(flags2["--mint"]) || flags2["--token"] !== undefined && !checkEvmAddress(flags2["--token"]).ok) {
105436
- writeUsageFailure(ctx.deps, USAGE15, ctx.json);
105924
+ writeUsageFailure(ctx.deps, USAGE16, ctx.json);
105437
105925
  return 2;
105438
105926
  }
105439
105927
  const to = flags2["--to"];
@@ -105470,7 +105958,7 @@ async function transfer(args, ctx) {
105470
105958
  const amountText2 = isMax ? `the full spendable balance of ${label}` : `${flags2["--amount"]} ${label}`;
105471
105959
  const confirmed = await confirmTransfer(ctx, [
105472
105960
  `Transfer ${amountText2} from ${wallet3.address} (${wallet3.id}) to ${describeDestination(destination)}`,
105473
- 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."
105961
+ destinationNote(destination, false)
105474
105962
  ], parsed.booleans.has("--yes"));
105475
105963
  if (!confirmed)
105476
105964
  return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet3.id, destination });
@@ -105491,11 +105979,7 @@ async function transfer(args, ctx) {
105491
105979
  if (builtAmount === null || !isMax && builtAmount !== amountRaw)
105492
105980
  throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the confirmed amount; nothing was signed.");
105493
105981
  const amountShown = asset ? `${decimalAmount(builtAmount, BASES[asset]?.decimals ?? 0)} ${asset}` : `${builtAmount} raw units of ${label}`;
105494
- writePreview(ctx, "Built", [
105495
- ["Transfer", transferId],
105496
- ["Amount", amountShown],
105497
- ...typeof built.destinationKind === "string" ? [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]] : []
105498
- ]);
105982
+ writePreview(ctx, "Built", [["Transfer", transferId], ["Amount", amountShown], ...builtDestination(built)]);
105499
105983
  const reader = await solana();
105500
105984
  const problem = await tradingRead(ctx, reader, () => solanaSpendProblem(unsigned[0], wallet3.address, reader.rpc, {
105501
105985
  kind: "transfer",
@@ -105557,7 +106041,7 @@ async function hoodTransfer(ctx, args) {
105557
106041
  const amountText2 = isMax ? `the full spendable balance of ${label}` : `${flags2["--amount"]} ${label}`;
105558
106042
  const confirmed = await confirmTransfer(ctx, [
105559
106043
  `Transfer ${amountText2} on Hood from ${wallet3.address} (${wallet3.id}) to ${describeDestination(destination)}`,
105560
- 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, only for ETH or USDG, 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.",
106044
+ destinationNote(destination, true),
105561
106045
  ...asset === "ETH" ? [
105562
106046
  "ETH keeps this send's gas and the sweep-home gas reserve in the wallet; Candle refuses an amount that would spend them."
105563
106047
  ] : []
@@ -105592,7 +106076,7 @@ async function hoodTransfer(ctx, args) {
105592
106076
  writePreview(ctx, "Built", [
105593
106077
  ["Transfer", safeText(transferId)],
105594
106078
  ["Amount", `${decimalAmount(amountRaw, decimals)} ${safeText(label)}`],
105595
- ...typeof built.destinationKind === "string" ? [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]] : [],
106079
+ ...builtDestination(built),
105596
106080
  [
105597
106081
  "Gas",
105598
106082
  `up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)`
@@ -107037,8 +107521,332 @@ init_reconcile_grant();
107037
107521
  init_tee_resolve();
107038
107522
  init_tee();
107039
107523
  init_tee_evm();
107524
+
107525
+ // src/commands/vault-demote-batch.ts
107526
+ init_args();
107527
+ init_human_output();
107528
+ init_presentation();
107529
+ init_solana_endpoint();
107530
+ init_errors2();
107531
+ init_promote_support();
107532
+ init_store();
107533
+ init_tee();
107534
+ init_vault_support();
107535
+ var BATCH_USAGE = "Usage: candle vault demote <tee-address> <tee-address>... | --all --empty [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--yes]";
107536
+ var VALUE_FLAGS = new Set(["--rpc-url", "--keystore", "-k", "--sweep-to", "--from-block", "--token"]);
107537
+ var BATCH_FLAGS = new Set(["--all", "--empty", "--yes"]);
107538
+ var SINGLE_ONLY_FLAGS = ["--token", "--from-block", "--accept-unknown-exposure"];
107539
+ function isBatchDemote(args) {
107540
+ let addresses = 0;
107541
+ for (let i = 0;i < args.length; i++) {
107542
+ const arg = args[i];
107543
+ if (VALUE_FLAGS.has(arg)) {
107544
+ i += 1;
107545
+ continue;
107546
+ }
107547
+ if (BATCH_FLAGS.has(arg))
107548
+ return true;
107549
+ if (!arg.startsWith("-"))
107550
+ addresses += 1;
107551
+ }
107552
+ return addresses > 1;
107553
+ }
107554
+ var isEvmAddress = (value) => /^0x[0-9a-fA-F]{40}$/.test(value);
107555
+ var sameAddress5 = (chain2, a, b) => chain2 === "evm" ? a.toLowerCase() === b.toLowerCase() : a === b;
107556
+ async function vaultDemoteBatch(args, ctx) {
107557
+ const single = SINGLE_ONLY_FLAGS.find((flag) => args.includes(flag));
107558
+ if (single !== undefined) {
107559
+ return usage(ctx, `${single} names one Hood wallet's sweep; pass it to a single demote: candle vault demote <0x address> ${single} ...`);
107560
+ }
107561
+ const parsed = parseArgs(args, {
107562
+ valueFlags: ["--rpc-url", "--sweep-to", "--keystore"],
107563
+ booleanFlags: ["--emergency", "--accept-older-copy", "--all", "--empty", "--yes"],
107564
+ pathFlags: ["--keystore"]
107565
+ });
107566
+ if ("error" in parsed)
107567
+ return usage(ctx, parsed.error);
107568
+ const all = parsed.booleans.has("--all");
107569
+ const empty = parsed.booleans.has("--empty");
107570
+ const yes = parsed.booleans.has("--yes");
107571
+ if (all && parsed.positionals.length > 0)
107572
+ return usage(ctx, `--all takes no addresses. ${BATCH_USAGE}`);
107573
+ if (all && !empty) {
107574
+ return usage(ctx, `--all needs a filter: --empty (wallets with nothing left to sweep). ${BATCH_USAGE}`);
107575
+ }
107576
+ if (empty && !all)
107577
+ return usage(ctx, `--empty filters --all. ${BATCH_USAGE}`);
107578
+ if (!all && parsed.positionals.length === 0)
107579
+ return usage(ctx, BATCH_USAGE);
107580
+ const rpcUrl = parsed.values["--rpc-url"];
107581
+ const sweepTo = parsed.values["--sweep-to"];
107582
+ const named = parsed.positionals;
107583
+ if (rpcUrl !== undefined && named.some(isEvmAddress) && named.some((a) => !isEvmAddress(a))) {
107584
+ return usage(ctx, "--rpc-url names one chain's node; demote Solana and Hood wallets in separate batches.");
107585
+ }
107586
+ if (!refuseEnvPassphrase(ctx))
107587
+ return 1;
107588
+ if (!requireTty(ctx, "vault demote"))
107589
+ return 1;
107590
+ const resolvedVault = vaultPathFor(ctx, parsed);
107591
+ if ("error" in resolvedVault)
107592
+ return usage(ctx, resolvedVault.error);
107593
+ let solana;
107594
+ if (empty) {
107595
+ const opened = await openSolanaClient(ctx, rpcUrl);
107596
+ if ("error" in opened)
107597
+ return usage(ctx, opened.error);
107598
+ solana = opened;
107599
+ }
107600
+ return runVaultCommand(ctx, async ({ hold }) => {
107601
+ const raw = await requireVaultRaw(ctx, resolvedVault);
107602
+ const opened = await unlockInteractively(ctx, resolvedVault.path, raw, {
107603
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
107604
+ });
107605
+ const held = hold(opened.vault);
107606
+ const outcomes = [];
107607
+ const planned = [];
107608
+ const plan2 = (entry) => {
107609
+ const chain2 = entry.chain === "evm" ? "evm" : "solana";
107610
+ const row = { address: entry.address, label: entry.label, chain: chain2, destination: null };
107611
+ const pinned = entry.tee?.vaultDestination;
107612
+ if (pinned !== undefined)
107613
+ row.destination = pinned;
107614
+ else if (sweepTo === undefined)
107615
+ row.destinationNote = "no pinned destination; pass --sweep-to";
107616
+ else {
107617
+ try {
107618
+ row.destination = assertColdVaultDestination(held.index, sweepTo, {
107619
+ chain: chain2,
107620
+ subjectAddress: entry.address
107621
+ }).address;
107622
+ } catch (error) {
107623
+ if (!isVaultError(error))
107624
+ throw error;
107625
+ row.destinationNote = `--sweep-to ${sweepTo}: ${error.message}`;
107626
+ }
107627
+ }
107628
+ planned.push(row);
107629
+ };
107630
+ const tees = held.index.entries.filter((entry) => entry.role === "tee-wallet");
107631
+ if (all) {
107632
+ for (const entry of tees) {
107633
+ if (entry.tee?.lifecycle === "retired")
107634
+ continue;
107635
+ const base2 = {
107636
+ address: entry.address,
107637
+ label: entry.label,
107638
+ chain: entry.chain === "evm" ? "evm" : "solana"
107639
+ };
107640
+ if (entry.chain === "evm") {
107641
+ outcomes.push({
107642
+ ...base2,
107643
+ status: "skipped",
107644
+ reason: "--empty reads Solana balances only; demote a Hood wallet by its address"
107645
+ });
107646
+ continue;
107647
+ }
107648
+ if (!await nothingLeftToSweep(solana, entry)) {
107649
+ outcomes.push({
107650
+ ...base2,
107651
+ status: "skipped",
107652
+ reason: "not empty: it holds SOL or token accounts, or an earlier sweep is unsettled"
107653
+ });
107654
+ continue;
107655
+ }
107656
+ plan2(entry);
107657
+ }
107658
+ } else {
107659
+ const seen = [];
107660
+ for (const address of named) {
107661
+ const chain2 = isEvmAddress(address) ? "evm" : "solana";
107662
+ if (seen.some((other) => sameAddress5(chain2, other, address))) {
107663
+ outcomes.push({ address, label: null, chain: chain2, status: "skipped", reason: "listed more than once" });
107664
+ continue;
107665
+ }
107666
+ seen.push(address);
107667
+ const entry = tees.find((candidate) => sameAddress5(chain2, candidate.address, address));
107668
+ if (entry === undefined) {
107669
+ outcomes.push({
107670
+ address,
107671
+ label: null,
107672
+ chain: chain2,
107673
+ status: "failed",
107674
+ code: "TEE_WALLET_UNKNOWN",
107675
+ reason: `${address} is not a TEE wallet in the vault`
107676
+ });
107677
+ continue;
107678
+ }
107679
+ if (entry.tee?.lifecycle === "retired") {
107680
+ outcomes.push({
107681
+ address,
107682
+ label: entry.label,
107683
+ chain: chain2,
107684
+ status: "skipped",
107685
+ reason: "already retired (demoted and swept)"
107686
+ });
107687
+ continue;
107688
+ }
107689
+ plan2(entry);
107690
+ }
107691
+ }
107692
+ if (rpcUrl !== undefined && new Set(planned.map((row) => row.chain)).size > 1) {
107693
+ return usage(ctx, "--rpc-url names one chain's node; demote Solana and Hood wallets in separate batches.");
107694
+ }
107695
+ if (planned.length > 0) {
107696
+ const display = ctx.json ? "stderr" : "stdout";
107697
+ ctx.deps[display].write(`Demote ${planned.length} TEE wallet${planned.length === 1 ? "" : "s"}, one at a time: disable, then sweep to the destination shown.
107698
+ `);
107699
+ writeTable(ctx.deps, display, ["#", "TEE wallet", "Label", "Sweeps to"], planned.map((row, i) => [
107700
+ String(i + 1),
107701
+ row.address,
107702
+ row.label,
107703
+ row.destination ?? `none (${row.destinationNote})`
107704
+ ]), []);
107705
+ for (const skipped of outcomes) {
107706
+ ctx.deps[display].write(`Not demoted: ${skipped.address}: ${skipped.reason}
107707
+ `);
107708
+ }
107709
+ if (!yes) {
107710
+ const typed = await ctx.deps.promptLine(`Type confirm to demote these ${planned.length} wallets: `);
107711
+ if (typed.trim().toLowerCase() !== "confirm") {
107712
+ throw new VaultError("DESTINATION_NOT_CONFIRMED", "The batch was not confirmed; nothing was done.", {
107713
+ suggestion: "Nothing was disabled or swept. Run it again and type confirm at the prompt."
107714
+ });
107715
+ }
107716
+ }
107717
+ }
107718
+ const session = { path: held.path, open: (bytes) => openHeld(opened, held, bytes) };
107719
+ const width = String(planned.length).length;
107720
+ for (const [i, row] of planned.entries()) {
107721
+ if (!ctx.json) {
107722
+ notice(ctx.deps, "stdout", "note", `[${String(i + 1).padStart(width)}/${planned.length}] Demoting ${row.address} (${row.label})`);
107723
+ }
107724
+ const outcome = await demoteOne(ctx, session, row, parsed.values, parsed.booleans);
107725
+ outcomes.push(outcome);
107726
+ if (!ctx.json)
107727
+ writeResultLine(ctx.deps, outcome);
107728
+ }
107729
+ const count3 = (status) => outcomes.filter((o) => o.status === status).length;
107730
+ const summary = {
107731
+ total: outcomes.length,
107732
+ done: count3("done"),
107733
+ incomplete: count3("incomplete"),
107734
+ failed: count3("failed"),
107735
+ skipped: count3("skipped")
107736
+ };
107737
+ const exit = summary.failed > 0 ? 1 : summary.incomplete > 0 ? 3 : 0;
107738
+ if (ctx.json) {
107739
+ writeJson(ctx.deps, { ok: exit === 0, wallets: outcomes, summary });
107740
+ return exit;
107741
+ }
107742
+ ctx.deps.stdout.write(`
107743
+ Summary: ${summary.done} done, ${summary.incomplete} incomplete, ${summary.failed} failed, ${summary.skipped} skipped (of ${summary.total}).
107744
+ `);
107745
+ for (const outcome of outcomes) {
107746
+ if (outcome.status === "done")
107747
+ continue;
107748
+ ctx.deps.stdout.write(` ${outcome.status.padEnd(10)} ${outcome.address}${outcome.reason ? `: ${outcome.reason}` : ""}
107749
+ `);
107750
+ }
107751
+ return exit;
107752
+ });
107753
+ }
107754
+ async function openHeld(opened, held, raw) {
107755
+ const vault = await reopenWithHeldDek(held.path, raw, held);
107756
+ return {
107757
+ vault,
107758
+ factor: opened.factor,
107759
+ reopen: (path, bytes) => reopenWithHeldDek(path, bytes, held),
107760
+ confirm: opened.confirm
107761
+ };
107762
+ }
107763
+ async function demoteOne(ctx, session, row, values, booleans) {
107764
+ const base2 = { address: row.address, label: row.label, chain: row.chain };
107765
+ const args = [row.address];
107766
+ for (const flag of ["--rpc-url", "--sweep-to", "--keystore"]) {
107767
+ const value = values[flag];
107768
+ if (value !== undefined)
107769
+ args.push(flag, value);
107770
+ }
107771
+ for (const flag of ["--emergency", "--accept-older-copy"])
107772
+ if (booleans.has(flag))
107773
+ args.push(flag);
107774
+ const covered = row.destination === null ? null : `Type confirm to sweep everything to ${row.destination}: `;
107775
+ const promptLine = async (text4) => text4 === covered ? "confirm" : ctx.deps.promptLine(text4);
107776
+ const captured = [];
107777
+ const errLines = [];
107778
+ const stdout = ctx.json ? { write: (chunk) => void captured.push(chunk) } : ctx.deps.stdout;
107779
+ const stderr = {
107780
+ write: (chunk) => {
107781
+ errLines.push(chunk);
107782
+ ctx.deps.stderr.write(chunk);
107783
+ }
107784
+ };
107785
+ const deps = { ...ctx.deps, stdout, stderr, promptLine };
107786
+ const step = { ...ctx, deps, heldUnlock: session };
107787
+ let exitCode;
107788
+ let thrown;
107789
+ try {
107790
+ exitCode = await vaultDemote(args, step);
107791
+ } catch (error) {
107792
+ exitCode = 1;
107793
+ thrown = error instanceof Error ? error.message : String(error);
107794
+ }
107795
+ const outcome = {
107796
+ ...base2,
107797
+ status: exitCode === 0 ? "done" : exitCode === 3 ? "incomplete" : "failed",
107798
+ exitCode
107799
+ };
107800
+ if (ctx.json) {
107801
+ const results = [];
107802
+ for (const line of captured.join("").split(`
107803
+ `)) {
107804
+ if (line.trim() === "")
107805
+ continue;
107806
+ try {
107807
+ results.push(JSON.parse(line));
107808
+ } catch {
107809
+ ctx.deps.stderr.write(`${line}
107810
+ `);
107811
+ }
107812
+ }
107813
+ outcome.results = results;
107814
+ const failure3 = [...results].reverse().find((doc) => typeof doc === "object" && doc !== null && doc.ok === false);
107815
+ if (failure3 !== undefined && exitCode !== 0) {
107816
+ if (typeof failure3.code === "string")
107817
+ outcome.code = failure3.code;
107818
+ if (typeof failure3.message === "string")
107819
+ outcome.reason = failure3.message;
107820
+ }
107821
+ }
107822
+ if (thrown !== undefined)
107823
+ outcome.reason = thrown;
107824
+ if (outcome.reason === undefined && exitCode === 3) {
107825
+ outcome.reason = "the disable is not verified or funds remain; remote authority may still be open";
107826
+ } else if (outcome.reason === undefined && exitCode !== 0) {
107827
+ const first = errLines.find((chunk) => chunk.trim() !== "" && !chunk.includes("vault.state.json"));
107828
+ outcome.reason = firstSentence(first ?? "") ?? `exit ${exitCode}`;
107829
+ }
107830
+ return outcome;
107831
+ }
107832
+ function firstSentence(chunk) {
107833
+ const text4 = chunk.replace(/\s+/g, " ").replace(/^[^\p{L}\p{N}]+/u, "").trim();
107834
+ if (text4 === "")
107835
+ return;
107836
+ const end = text4.search(/\.(\s|$)/);
107837
+ return end < 0 ? text4 : text4.slice(0, end + 1);
107838
+ }
107839
+ function writeResultLine(deps, outcome) {
107840
+ const word3 = { done: "done", incomplete: "incomplete", failed: "FAILED", skipped: "skipped" }[outcome.status];
107841
+ deps.stdout.write(`Result ${outcome.address}: ${word3} (exit ${outcome.exitCode})${outcome.reason && outcome.status !== "done" ? `: ${outcome.reason}` : ""}
107842
+ `);
107843
+ }
107844
+
107845
+ // src/commands/vault-demote.ts
107040
107846
  init_vault_support();
107041
107847
  async function vaultDemote(args, ctx) {
107848
+ if (ctx.heldUnlock === undefined && isBatchDemote(args))
107849
+ return vaultDemoteBatch(args, ctx);
107042
107850
  if (namesEvmWallet(args))
107043
107851
  return vaultDemoteEvm(args, ctx);
107044
107852
  const parsed = parseArgs(args, {
@@ -108804,6 +109612,8 @@ async function vaultFund(args, ctx) {
108804
109612
  const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
108805
109613
  if (!amount)
108806
109614
  return usage(ctx, "--amount <n> is required.");
109615
+ if (amount.toLowerCase() === "max")
109616
+ return usage(ctx, "--amount must be a number; funding a wallet takes no max.");
108807
109617
  if (asset !== "SOL" && asset !== "USDC")
108808
109618
  return usage(ctx, "--asset must be SOL or USDC.");
108809
109619
  const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
@@ -113027,6 +113837,7 @@ init_args();
113027
113837
  init_deps();
113028
113838
  init_evm_lite();
113029
113839
  init_solana_endpoint();
113840
+ init_errors2();
113030
113841
  init_evm_tee();
113031
113842
  init_evm_transfer();
113032
113843
  init_promote_support();
@@ -113049,7 +113860,7 @@ async function vaultTransfer(args, ctx) {
113049
113860
  const fromLabel = parsed.values["--from"];
113050
113861
  const rpcUrlFlag = parsed.values["--rpc-url"];
113051
113862
  if (!amount)
113052
- return usage(ctx, "--amount <n> is required.");
113863
+ return usage(ctx, "--amount <n|max> is required.");
113053
113864
  if (!asset)
113054
113865
  return usage(ctx, "--asset SOL|<mint>|ETH|USDG|<0x token> is required.");
113055
113866
  if (!fromLabel)
@@ -113145,14 +113956,15 @@ async function vaultTransfer(args, ctx) {
113145
113956
  const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], solana.rpc, ctx);
113146
113957
  if (reconciled !== null)
113147
113958
  return reconciled;
113148
- const plan2 = await solana.read(() => planTransfer({
113959
+ const buildPlan = () => solana.read(() => planTransfer({
113149
113960
  from: fromEntry.address,
113150
113961
  to,
113151
113962
  amount,
113152
113963
  asset,
113153
113964
  rpc: solana.rpc
113154
113965
  }));
113155
- const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan2.from, plan2.instructions));
113966
+ let plan2 = await buildPlan();
113967
+ let feeQuote = plan2.solMax?.fee ?? await solana.read(() => quoteTransferFee(solana.rpc, plan2.from, plan2.instructions));
113156
113968
  displayTransferPlan(ctx, plan2, feeQuote);
113157
113969
  await assertTransferAffordable(solana, plan2, feeQuote);
113158
113970
  if (promoted) {
@@ -113160,7 +113972,23 @@ async function vaultTransfer(args, ctx) {
113160
113972
  if (notice2 !== undefined)
113161
113973
  note(notice2);
113162
113974
  }
113163
- await confirmSend(ctx, { amount: plan2.amount, asset: plan2.asset, address: to });
113975
+ for (let attempt = 0;attempt < 3; attempt++) {
113976
+ await confirmSend(ctx, { amount: plan2.amount, asset: plan2.asset, address: to });
113977
+ if (!plan2.solMax)
113978
+ break;
113979
+ const refreshed = await buildPlan();
113980
+ const unchanged = refreshed.amountRaw === plan2.amountRaw && refreshed.solMax?.fee === plan2.solMax.fee;
113981
+ plan2 = refreshed;
113982
+ feeQuote = refreshed.solMax?.fee ?? null;
113983
+ if (unchanged)
113984
+ break;
113985
+ if (attempt === 2) {
113986
+ throw new VaultError("VAULT_UNREADABLE", "The SOL max balance or fee kept changing during confirmation. Nothing was signed or sent; retry when the wallet and RPC are stable.");
113987
+ }
113988
+ note("SOL max balance or fee changed; review and confirm the refreshed amount.");
113989
+ displayTransferPlan(ctx, plan2, feeQuote);
113990
+ await assertTransferAffordable(solana, plan2, feeQuote);
113991
+ }
113164
113992
  const secret = await decryptKey(vault, fromEntry.id);
113165
113993
  try {
113166
113994
  const result = await signAndBroadcastTransfer({ ctx, solana, secret64: secret, plan: plan2 });
@@ -113203,7 +114031,7 @@ init_human_output();
113203
114031
  init_release();
113204
114032
  import { dirname as dirname11, join as join15 } from "node:path";
113205
114033
  init_key_output();
113206
- var USAGE16 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
114034
+ var USAGE17 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
113207
114035
  async function resolveIdentity(deps, bundlePath, flag) {
113208
114036
  if (flag)
113209
114037
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -113235,19 +114063,19 @@ async function verify(args, ctx) {
113235
114063
  });
113236
114064
  if ("error" in parsed) {
113237
114065
  writeScreenUsageFailure(deps, `${parsed.error}
113238
- ${USAGE16}`, json);
114066
+ ${USAGE17}`, json);
113239
114067
  return 2;
113240
114068
  }
113241
114069
  const file = parsed.positionals[0];
113242
114070
  if (parsed.positionals.length !== 1 || file === undefined) {
113243
114071
  writeScreenUsageFailure(deps, `verify takes exactly one file.
113244
- ${USAGE16}`, json);
114072
+ ${USAGE17}`, json);
113245
114073
  return 2;
113246
114074
  }
113247
114075
  const bundlePath = parsed.values["--bundle"];
113248
114076
  if (!bundlePath) {
113249
114077
  writeScreenUsageFailure(deps, `--bundle is required.
113250
- ${USAGE16}`, json);
114078
+ ${USAGE17}`, json);
113251
114079
  return 2;
113252
114080
  }
113253
114081
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -113261,7 +114089,7 @@ ${USAGE16}`, json);
113261
114089
  }
113262
114090
  if (resolved.kind === "absent") {
113263
114091
  writeScreenUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
113264
- ${USAGE16}`, json);
114092
+ ${USAGE17}`, json);
113265
114093
  return 2;
113266
114094
  }
113267
114095
  const identity4 = resolved.uri;
@@ -113510,7 +114338,7 @@ init_solana_lite();
113510
114338
  init_trading();
113511
114339
  init_swap();
113512
114340
  import { randomUUID as randomUUID15 } from "node:crypto";
113513
- var USAGE17 = "Usage: candle wallet close-empty [--wallet embedded|<tee address|label>] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
114341
+ var USAGE18 = "Usage: candle wallet close-empty [--wallet embedded|<tee address|label>] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
113514
114342
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
113515
114343
  var LISTED_ROWS = 50;
113516
114344
  var TOKEN_IX_CLOSE_ACCOUNT3 = 9;
@@ -113773,7 +114601,7 @@ async function closeTee(ctx, key, wallet3, name2, run2) {
113773
114601
  async function walletsCloseEmpty(args, ctx) {
113774
114602
  const lifted = splitKeep(args);
113775
114603
  if ("error" in lifted) {
113776
- writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE17}`, ctx.json);
114604
+ writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE18}`, ctx.json);
113777
114605
  return 2;
113778
114606
  }
113779
114607
  const parsed = parseArgs(lifted.rest, { valueFlags: ["--wallet", "--client-trade-id"], booleanFlags: ["--yes"] });
@@ -113784,7 +114612,7 @@ async function walletsCloseEmpty(args, ctx) {
113784
114612
  const named = parsed.values["--client-trade-id"];
113785
114613
  const id = named ?? `close-${randomUUID15()}`;
113786
114614
  if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
113787
- writeUsageFailure(ctx.deps, USAGE17, ctx.json);
114615
+ writeUsageFailure(ctx.deps, USAGE18, ctx.json);
113788
114616
  return 2;
113789
114617
  }
113790
114618
  const run2 = { id, named: named !== undefined, keep: lifted.keep, yes: parsed.booleans.has("--yes") };
@@ -114101,14 +114929,27 @@ async function maybeWriteUpdateNotice(deps, opts = {}) {
114101
114929
  return;
114102
114930
  if (opts.command === "update" || opts.command === "doctor")
114103
114931
  return;
114104
- const latest = latestCliVersionFromApi();
114105
- if (!latest || !isPlainVersion(latest) || compareVersions(latest, CLI_VERSION) <= 0)
114932
+ const seen = latestCliVersionFromApi();
114933
+ if (!seen || !isPlainVersion(seen) || compareVersions(seen, CLI_VERSION) <= 0)
114106
114934
  return;
114935
+ const realExec = await deps.realpath(deps.execPath).catch(() => deps.execPath);
114936
+ const method = detectInstall(deps.execPath, realExec);
114937
+ let latest = seen;
114938
+ let command = "npm i -g @candledottv/cli@latest";
114939
+ if (method !== "script") {
114940
+ command = method === "homebrew" ? "brew upgrade candle" : "candle update";
114941
+ const feed = await fetchLatest(deps, releaseBaseUrl(deps.env, updateHomeFor(CLI_VERSION)));
114942
+ if (!feed.ok || !isPlainVersion(feed.manifest.version))
114943
+ return;
114944
+ latest = feed.manifest.version;
114945
+ if (compareVersions(latest, CLI_VERSION) <= 0)
114946
+ return;
114947
+ }
114107
114948
  const config = await deps.readConfig();
114108
114949
  const prior = config.updateNotice;
114109
114950
  if (prior && prior.version === latest && deps.now() - prior.shownAt < DAY_MS2)
114110
114951
  return;
114111
- const line = `Update available: candle ${CLI_VERSION} -> ${latest}. Run: candle update`;
114952
+ const line = `Update available: candle ${CLI_VERSION} -> ${latest}. Run: ${command}`;
114112
114953
  const tty = terminalStyle(deps, "stderr", opts.json).color !== 0;
114113
114954
  deps.stderr.write(tty ? `\x1B[2m${line}\x1B[0m
114114
114955
  ` : `${line}
@@ -114189,7 +115030,7 @@ var COMMANDS = {
114189
115030
  stake: { bare: stake, subcommands: { add: stakeAdd, remove: stakeRemove, finish: stakeFinish, claim: stakeClaim } },
114190
115031
  swap: { bare: swap, subcommands: { status: swapStatus } },
114191
115032
  transfer: { bare: transfer },
114192
- launch: { bare: launch },
115033
+ launch: { bare: launch, subcommands: { status: launchStatus } },
114193
115034
  image: { subcommands: { upload: imageUpload } },
114194
115035
  pnl: { bare: pnl },
114195
115036
  portfolio: { bare: portfolio },