@candledottv/cli 0.13.9 → 0.13.10

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 +878 -94
  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.10";
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",
@@ -16872,12 +16885,48 @@ function describeSwapEvidence(ctx, details, terminal) {
16872
16885
  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
16886
  `.trim());
16874
16887
  }
16888
+ function tradeSignatures(result) {
16889
+ const nested = (name) => result[name] && typeof result[name] === "object" ? result[name] : {};
16890
+ const payload = nested("payload");
16891
+ const job = nested("job");
16892
+ const sources = [result, payload, job];
16893
+ const operation = job.operation && typeof job.operation === "object" ? job.operation : {};
16894
+ const legs = [...sources.map((source) => source.landedLegs), operation.legs].flatMap((value) => Array.isArray(value) ? value.filter((leg) => leg !== null && typeof leg === "object") : []);
16895
+ const primary = sources.map((source) => source.signature).find(isText) ?? legs.find((leg) => (leg.kind === "trade" || leg.kind === "bridgeDeposit") && isText(leg.hash))?.hash;
16896
+ return [
16897
+ ...new Set([
16898
+ ...isText(primary) ? [primary] : [],
16899
+ ...sources.flatMap((source) => [
16900
+ ...isText(source.signature) ? [source.signature] : [],
16901
+ ...textsOf(source.signatures),
16902
+ ...textsOf(source.hashes),
16903
+ ...isText(source.feeSignature) ? [source.feeSignature] : []
16904
+ ]),
16905
+ ...legs.map((leg) => leg.hash).filter(isText)
16906
+ ])
16907
+ ];
16908
+ }
16909
+ function swapJsonResult(result) {
16910
+ const signatures = tradeSignatures(result);
16911
+ const inner = (name) => result[name] && typeof result[name] === "object" ? result[name].status : undefined;
16912
+ const status = [result.status, inner("job"), inner("payload")].find(isText) ?? null;
16913
+ return {
16914
+ ...result,
16915
+ kind: result.kind,
16916
+ status,
16917
+ clientTradeId: result.clientTradeId ?? null,
16918
+ signature: signatures[0] ?? null,
16919
+ signatures,
16920
+ raw: result
16921
+ };
16922
+ }
16875
16923
  function printTradingResult(ctx, result) {
16876
16924
  if (result.kind === "swap" && result.job && typeof result.job === "object") {
16877
16925
  const job = result.job;
16878
16926
  describeSwapEvidence(ctx, job, job.status === "failed");
16879
16927
  }
16880
- ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
16928
+ const printed = ctx.json && (result.kind === "swap" || result.kind === "trade") ? swapJsonResult(result) : result;
16929
+ ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(printed)}
16881
16930
  ` : `${JSON.stringify(result, null, 2)}
16882
16931
  `);
16883
16932
  return 0;
@@ -17684,7 +17733,7 @@ async function bridgeSwap(ctx, args) {
17684
17733
  return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
17685
17734
  }
17686
17735
  }
17687
- var AGENT_MAX_SLIPPAGE_BPS = 5000, SLIPPAGE_RANGE_MESSAGE, settlementSchema, bridgeFactsSchema, HOOD_TRADE_LEGS, bridgeBuildSchema;
17736
+ 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
17737
  var init_swap = __esm(() => {
17689
17738
  init_zod();
17690
17739
  init_args();
@@ -28872,7 +28921,7 @@ __export(exports_vault_support, {
28872
28921
  askForOwnPassphrase: () => askForOwnPassphrase,
28873
28922
  PASSPHRASE_ONLY_PREFIX: () => PASSPHRASE_ONLY_PREFIX
28874
28923
  });
28875
- import { dirname as dirname6 } from "node:path";
28924
+ import { dirname as dirname6, resolve as resolvePath } from "node:path";
28876
28925
  function refuseEnvPassphrase(ctx) {
28877
28926
  if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
28878
28927
  return true;
@@ -29002,6 +29051,9 @@ function assertVaultHelperIdentities(deps, envelopes) {
29002
29051
  }
29003
29052
  async function unlockInteractively(ctx, path, raw, opts = {}) {
29004
29053
  await assertNotOlderCopy(ctx, path, raw, opts.acceptOlderCopy ?? false);
29054
+ const held = ctx.heldUnlock;
29055
+ if (held !== undefined && resolvePath(held.path) === resolvePath(path))
29056
+ return held.open(raw);
29005
29057
  const { deps } = ctx;
29006
29058
  const file = parseVaultFile(raw);
29007
29059
  assertVaultHelperIdentities(deps, file.envelopes);
@@ -37251,9 +37303,11 @@ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
37251
37303
  return { ok: false, code: 1 };
37252
37304
  }
37253
37305
  if (byPrefix)
37254
- return { ok: true, keyPrefix: byPrefix.keyPrefix };
37255
- if (byLabel.length === 1)
37256
- return { ok: true, keyPrefix: byLabel[0].keyPrefix };
37306
+ return { ok: true, keyPrefix: byPrefix.keyPrefix, embeddedWallet: byPrefix.embeddedWallet };
37307
+ if (byLabel.length === 1) {
37308
+ const only = byLabel[0];
37309
+ return { ok: true, keyPrefix: only.keyPrefix, embeddedWallet: only.embeddedWallet };
37310
+ }
37257
37311
  if (byLabel.length > 1) {
37258
37312
  writeLocalFailure(deps, {
37259
37313
  code: `${codePrefix}_KEY_AMBIGUOUS`,
@@ -37265,8 +37319,10 @@ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
37265
37319
  return { ok: true, keyPrefix: raw };
37266
37320
  }
37267
37321
  const matches = keys.filter((key) => key.label === raw);
37268
- if (matches.length === 1)
37269
- return { ok: true, keyPrefix: matches[0].keyPrefix };
37322
+ if (matches.length === 1) {
37323
+ const only = matches[0];
37324
+ return { ok: true, keyPrefix: only.keyPrefix, embeddedWallet: only.embeddedWallet };
37325
+ }
37270
37326
  if (matches.length === 0) {
37271
37327
  const labelled = keys.filter((key) => key.label).map((key) => `${key.keyPrefix} ${labelCell(key.label)}`);
37272
37328
  writeLocalFailure(deps, {
@@ -37581,6 +37637,11 @@ var init_tee_rebind = __esm(() => {
37581
37637
  function effectiveEmbeddedWallet(value) {
37582
37638
  return value === "denied" ? "denied" : "allowed";
37583
37639
  }
37640
+ function withdrawNeedsEmbeddedWalletLine(keyPrefix) {
37641
+ 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.
37642
+ ` + `Allow it with: candle keys update ${keyPrefix} --embedded-wallet allow
37643
+ ` + "Transfers out of the key's own TEE wallets do not need it.";
37644
+ }
37584
37645
  function keyName(keyPrefix, label) {
37585
37646
  const cleaned = labelCell(label ?? undefined);
37586
37647
  return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
@@ -38094,6 +38155,10 @@ async function keysCreate(args, ctx) {
38094
38155
  } catch (error) {
38095
38156
  storeError = error instanceof Error ? error.message : String(error);
38096
38157
  }
38158
+ if (presetForScopes(body.scopes) === "readwritetransfer" && body.embeddedWallet === "denied") {
38159
+ notice(deps, "stderr", "warning", `${withdrawNeedsEmbeddedWalletLine(body.keyPrefix)}
38160
+ Or mint it that way: candle keys create --access withdraw --embedded-wallet allow`);
38161
+ }
38097
38162
  if (json) {
38098
38163
  deps.stdout.write(`${JSON.stringify({ ...body, stored, ...storeError ? { storeError } : {} })}
38099
38164
  `);
@@ -45295,7 +45360,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
45295
45360
  }, async (args) => callAndRelay("candle_get_fees", args, getConfig(), doFetch));
45296
45361
  register("candle_claim_fees", {
45297
45362
  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.",
45363
+ 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
45364
  inputSchema: claimFeesShape
45300
45365
  }, async (args) => callAndRelay("candle_claim_fees", args, getConfig(), doFetch));
45301
45366
  register("candle_get_wallets", {
@@ -76159,7 +76224,7 @@ var LAUNCH_USAGE = `candle launch ${launchFlagFields.map((field) => {
76159
76224
  if (field.key === "imageUrl")
76160
76225
  return `--image <path>|${invocation}`;
76161
76226
  return "required" in field && field.required ? invocation : `[${invocation}]`;
76162
- }).join(" ")} [--check] [--yes] [--rpc-url <url>] [--json]`;
76227
+ }).join(" ")} [--check] [--yes] [--require-fee-wallet] [--rpc-url <url>] [--json]`;
76163
76228
  function launchFieldProblem(flags) {
76164
76229
  for (const field of launchFlagFields) {
76165
76230
  const value = flags[field.cli.flag];
@@ -76344,7 +76409,7 @@ var HELP = {
76344
76409
  group: "Trade",
76345
76410
  intro: "Quote, swap and bridge on Solana or Hood; read an operation",
76346
76411
  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.",
76412
+ 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
76413
  usage: ["candle swap <from> <to> [flags]", "candle swap status <id>"],
76349
76414
  rows: [
76350
76415
  {
@@ -76657,19 +76722,19 @@ var HELP = {
76657
76722
  group: "Trade",
76658
76723
  intro: "Move funds out of a TEE wallet to your wallets or its vault",
76659
76724
  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.",
76725
+ 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
76726
  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]"
76727
+ "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
76728
  ],
76664
76729
  rows: [],
76665
76730
  flags: [
76666
76731
  {
76667
- invocation: "--to <address|wallet name|vault>",
76668
- description: "Where the funds go: vault is the wallet's own pin"
76732
+ invocation: "--to <address|wallet name|allowlist label|vault>",
76733
+ description: "Where the funds go: vault is the wallet's own pin; a linked wallet's name wins over an allowlist label"
76669
76734
  },
76670
76735
  {
76671
76736
  invocation: "--asset <SOL|USDC|CNDL|ETH|USDG>",
76672
- description: "A base asset (required for a linked-wallet destination)"
76737
+ description: "A base asset (required for a linked-wallet or approved-address destination)"
76673
76738
  },
76674
76739
  { invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
76675
76740
  { invocation: "--token <0x...>", description: "Any Hood ERC-20, instead of --asset (vault destinations only)" },
@@ -76691,9 +76756,14 @@ var HELP = {
76691
76756
  launch: {
76692
76757
  group: "Trade",
76693
76758
  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: [],
76759
+ 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; 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.",
76760
+ usage: [LAUNCH_USAGE, "candle launch status <mint-or-client-launch-id> [--wait]"],
76761
+ rows: [
76762
+ {
76763
+ invocation: "status <mint-or-client-launch-id> [--wait]",
76764
+ 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."
76765
+ }
76766
+ ],
76697
76767
  flags: [
76698
76768
  ...launchFlagRows,
76699
76769
  {
@@ -76705,6 +76775,10 @@ var HELP = {
76705
76775
  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
76776
  },
76707
76777
  { invocation: "--yes", description: "Confirm the launch without an interactive prompt." },
76778
+ {
76779
+ invocation: "--require-fee-wallet",
76780
+ description: "Refuse the launch when no --fee-wallet is given, rather than letting the fees earn to the account wallet."
76781
+ },
76708
76782
  {
76709
76783
  invocation: "--rpc-url <url>",
76710
76784
  description: "The Solana RPC for a TEE launch with no first buy, or the Hood RPC for receipt reads."
@@ -76718,7 +76792,8 @@ var HELP = {
76718
76792
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
76719
76793
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1",
76720
76794
  "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"
76795
+ "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",
76796
+ "candle launch status <mint-or-id> --wait"
76722
76797
  ],
76723
76798
  env: ENV_API
76724
76799
  },
@@ -77363,7 +77438,7 @@ var HELP = {
77363
77438
  },
77364
77439
  {
77365
77440
  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"
77441
+ 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
77442
  },
77368
77443
  {
77369
77444
  invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
@@ -77378,8 +77453,8 @@ var HELP = {
77378
77453
  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
77454
  },
77380
77455
  {
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"
77456
+ invocation: "demote <tee-address>... | --all --empty [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--yes] [--token <0x...>] [--from-block <n>]",
77457
+ 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
77458
  },
77384
77459
  {
77385
77460
  invocation: "export-key <label> --to <new-file>",
@@ -77398,7 +77473,15 @@ var HELP = {
77398
77473
  },
77399
77474
  {
77400
77475
  invocation: "--all",
77401
- description: "list: include retired TEE wallets (demoted and swept), which are hidden by default"
77476
+ 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)"
77477
+ },
77478
+ {
77479
+ invocation: "--empty",
77480
+ 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"
77481
+ },
77482
+ {
77483
+ invocation: "--yes",
77484
+ description: "demote with several wallets: skip the one typed confirmation for the listed set"
77402
77485
  },
77403
77486
  {
77404
77487
  invocation: "--balances",
@@ -77436,6 +77519,7 @@ var HELP = {
77436
77519
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
77437
77520
  "candle vault promote-batch --pairs-from ./promote-plan.csv",
77438
77521
  "candle vault promote-batch --pairs-from ./promote-plan.csv --to-key tr-01",
77522
+ "candle vault demote --all --empty",
77439
77523
  "candle vault enroll security-key --label yubikey-a",
77440
77524
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
77441
77525
  "candle vault backup --to icloud",
@@ -77496,8 +77580,12 @@ var HELP = {
77496
77580
  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
77581
  },
77498
77582
  {
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"
77583
+ invocation: "signer new --key <prefix|label> [--out <pem>] [--force] [--no-wait]",
77584
+ 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"
77585
+ },
77586
+ {
77587
+ invocation: " signer status --key <prefix|label>",
77588
+ 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
77589
  },
77502
77590
  {
77503
77591
  invocation: " signer remove --key <prefix|label>",
@@ -82610,14 +82698,69 @@ async function tokenAccountFrozen(rpc, address) {
82610
82698
  return { exists: false, frozen: false };
82611
82699
  return { exists: true, frozen: account.data[TOKEN_ACCOUNT_STATE_OFFSET2] === TOKEN_ACCOUNT_STATE_FROZEN2 };
82612
82700
  }
82701
+ function rawToDecimal2(raw, decimals) {
82702
+ if (decimals === 0)
82703
+ return raw.toString();
82704
+ const digits = raw.toString().padStart(decimals + 1, "0");
82705
+ return `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}`.replace(/\.?0+$/, "");
82706
+ }
82707
+ async function planSolMax(rpc, from, to) {
82708
+ if (from === to)
82709
+ throw new VaultError("VAULT_INDEX_INVALID", "SOL max needs a destination different from the source.");
82710
+ const account = await rpc.getAccountInfo(from);
82711
+ if (account && (account.owner !== SYSTEM_PROGRAM_ID || account.data.length !== 0)) {
82712
+ 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.");
82713
+ }
82714
+ const balance = account?.lamports ?? 0n;
82715
+ const reserve = 0n;
82716
+ const blockhash = await rpc.getLatestBlockhash();
82717
+ let raw = balance;
82718
+ for (let attempt = 0;attempt < 3; attempt++) {
82719
+ const instructions = [systemTransfer(decodePubkey(from), decodePubkey(to), raw)];
82720
+ const message = compileLegacyMessage({ feePayer: decodePubkey(from), recentBlockhash: blockhash, instructions });
82721
+ const fee = await rpc.getFeeForMessage(toBase642(message));
82722
+ if (fee === null) {
82723
+ 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.");
82724
+ }
82725
+ const available = balance - fee - reserve;
82726
+ if (available <= 0n) {
82727
+ 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." });
82728
+ }
82729
+ if (raw === available) {
82730
+ const amount = rawToDecimal2(raw, 9);
82731
+ return {
82732
+ asset: "SOL",
82733
+ amount,
82734
+ amountRaw: raw,
82735
+ decimals: 9,
82736
+ from,
82737
+ to,
82738
+ instructions,
82739
+ solMax: { blockhash, fee, reserve },
82740
+ displayLines: [
82741
+ `fee payer ${from}`,
82742
+ `destination ${to}`,
82743
+ `amount ${amount} SOL = ${raw} lamports (max)`,
82744
+ "mint (native SOL)",
82745
+ `source reserve ${reserve} lamports (source emptied)`
82746
+ ]
82747
+ };
82748
+ }
82749
+ raw = available;
82750
+ }
82751
+ throw new VaultError("VAULT_UNREADABLE", "The SOL max fee quote kept changing. Nothing was signed or sent; retry when the RPC is stable.");
82752
+ }
82613
82753
  async function planTransfer(input) {
82614
82754
  const asset = input.asset.toUpperCase();
82615
82755
  const fromKey = decodePubkey(input.from);
82616
82756
  const toKey = decodePubkey(input.to);
82757
+ const max = input.amount.toLowerCase() === "max";
82617
82758
  if (asset === "SOL") {
82759
+ if (max)
82760
+ return planSolMax(input.rpc, input.from, input.to);
82618
82761
  const raw2 = decimalToRaw4(input.amount, 9);
82619
82762
  if (raw2 === null || raw2 === 0n) {
82620
- throw new VaultError("VAULT_INDEX_INVALID", "--amount must be a positive SOL decimal with at most 9 places.");
82763
+ throw new VaultError("VAULT_INDEX_INVALID", "--amount must be max or a positive SOL decimal with at most 9 places.");
82621
82764
  }
82622
82765
  return {
82623
82766
  asset: "SOL",
@@ -82648,13 +82791,16 @@ async function planTransfer(input) {
82648
82791
  throw new VaultError("VAULT_UNREADABLE", `Could not read mint ${mintAddress}: ${asMessage(error)}`);
82649
82792
  }
82650
82793
  const decimals = profile.decimals;
82651
- const raw = decimalToRaw4(input.amount, decimals);
82794
+ const source = ataFor(profile, fromKey);
82795
+ const raw = max ? tokenAccountAmount((await rpc.getAccountInfo(encodePubkey(source)))?.data ?? new Uint8Array) : decimalToRaw4(input.amount, decimals);
82796
+ if (max && raw === 0n) {
82797
+ 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.`);
82798
+ }
82652
82799
  if (raw === null || raw === 0n) {
82653
- throw new VaultError("VAULT_INDEX_INVALID", `--amount must be a positive decimal with at most ${decimals} decimal places.`);
82800
+ throw new VaultError("VAULT_INDEX_INVALID", `--amount must be max or a positive decimal with at most ${decimals} decimal places.`);
82654
82801
  }
82655
82802
  const mint = decodePubkey(mintAddress);
82656
82803
  const tokenProgram = decodePubkey(profile.tokenProgram);
82657
- const source = ataFor(profile, fromKey);
82658
82804
  const destination = ataFor(profile, toKey);
82659
82805
  const instructions = [];
82660
82806
  const accountCreationLines = [];
@@ -82691,7 +82837,7 @@ async function planTransfer(input) {
82691
82837
  }));
82692
82838
  return {
82693
82839
  asset: asset === "USDC" ? "USDC" : mintAddress,
82694
- amount: input.amount,
82840
+ amount: max ? rawToDecimal2(raw, decimals) : input.amount,
82695
82841
  amountRaw: raw,
82696
82842
  decimals,
82697
82843
  mint: mintAddress,
@@ -82708,7 +82854,7 @@ async function planTransfer(input) {
82708
82854
  displayLines: [
82709
82855
  `fee payer ${input.from}`,
82710
82856
  `destination ${input.to}`,
82711
- `amount ${input.amount} (${raw} raw, ${decimals} dp)`,
82857
+ `amount ${max ? rawToDecimal2(raw, decimals) : input.amount} (${raw} raw, ${decimals} dp)${max ? " (max)" : ""}`,
82712
82858
  `mint ${mintAddress}`,
82713
82859
  `program ${profile.tokenProgram}${profile.token2022 ? " (Token-2022)" : ""}`,
82714
82860
  ...accountCreationLines,
@@ -82788,7 +82934,7 @@ async function signAndBroadcastTransfer(input) {
82788
82934
  if (encodePubkey(feePayer) !== input.plan.from) {
82789
82935
  throw new VaultError("VAULT_VERIFY_FAILED", "The decrypted key does not match the planned fee payer.");
82790
82936
  }
82791
- const blockhash = await input.solana.read(() => rpc.getLatestBlockhash());
82937
+ const blockhash = input.plan.solMax?.blockhash ?? await input.solana.read(() => rpc.getLatestBlockhash());
82792
82938
  const message = compileLegacyMessage({
82793
82939
  feePayer,
82794
82940
  recentBlockhash: blockhash,
@@ -84791,7 +84937,7 @@ function rebindJson(input, chain2) {
84791
84937
  init_key_output();
84792
84938
  init_keys();
84793
84939
  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]";
84940
+ 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
84941
  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
84942
  var SIGNER_POLL_MS = 5000;
84797
84943
  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 +84971,8 @@ async function teeSigner(args, ctx) {
84825
84971
  return teeSignerNew(rest, ctx);
84826
84972
  if (sub === "remove")
84827
84973
  return teeSignerRemove(rest, ctx);
84974
+ if (sub === "status")
84975
+ return teeSignerStatus(rest, ctx);
84828
84976
  writeScreenUsageFailure(ctx.deps, sub === undefined ? USAGE_TEE_SIGNER : `Unknown subcommand: ${sub}. ${USAGE_TEE_SIGNER}`, ctx.json);
84829
84977
  return 2;
84830
84978
  }
@@ -84837,7 +84985,11 @@ function generatePair() {
84837
84985
  }
84838
84986
  async function teeSignerNew(args, ctx) {
84839
84987
  const { deps, json } = ctx;
84840
- const parsed = parseArgs(args, { valueFlags: ["--key", "--out"], booleanFlags: ["--force"], pathFlags: ["--out"] });
84988
+ const parsed = parseArgs(args, {
84989
+ valueFlags: ["--key", "--out"],
84990
+ booleanFlags: ["--force", "--no-wait"],
84991
+ pathFlags: ["--out"]
84992
+ });
84841
84993
  if ("error" in parsed) {
84842
84994
  writeScreenUsageFailure(deps, `${parsed.error}. ${USAGE_TEE_SIGNER}`, json);
84843
84995
  return 2;
@@ -84853,6 +85005,7 @@ async function teeSignerNew(args, ctx) {
84853
85005
  }
84854
85006
  const out = parsed.values["--out"];
84855
85007
  const force = parsed.booleans.has("--force");
85008
+ const noWait = parsed.booleans.has("--no-wait");
84856
85009
  await printIdentity(ctx);
84857
85010
  const apiKey = await resolveApiKey(deps, ctx.profile);
84858
85011
  if (!apiKey) {
@@ -84891,7 +85044,7 @@ async function teeSignerNew(args, ctx) {
84891
85044
  entry = undefined;
84892
85045
  }
84893
85046
  if (entry !== undefined && active !== null && active.spkiSha256 === entry.spkiSha256) {
84894
- const recorded = { ...entry, signerQuorumId: active.signerQuorumId };
85047
+ const recorded = { ...entry, signerQuorumId: active.signerQuorumId, pendingRequest: undefined };
84895
85048
  await saveKeySignerEntry(deps, recorded);
84896
85049
  await writeOut(ctx, out, recorded);
84897
85050
  return reportApproved(ctx, view, recorded, { resumed: true, requested: false });
@@ -84940,12 +85093,17 @@ async function teeSignerNew(args, ctx) {
84940
85093
  resumed = false;
84941
85094
  }
84942
85095
  await writeOut(ctx, out, entry);
84943
- const requested = await requestSigner(ctx, apiKey, entry.publicKeyDer);
85096
+ const cached = matchingPendingRequest(ctx, view, entry);
85097
+ const requested = cached ? {
85098
+ ok: true,
85099
+ status: 200,
85100
+ body: { ...cached, spkiSha256: entry.spkiSha256, fingerprint: entry.fingerprint }
85101
+ } : await requestSigner(ctx, apiKey, entry.publicKeyDer);
84944
85102
  if (!requested.ok) {
84945
85103
  if (requested.code === "KEY_SIGNER_ALREADY_ACTIVE") {
84946
85104
  const quorum = errorDetails(requested).keySigner?.signerQuorumId;
84947
85105
  if (typeof quorum === "string") {
84948
- const recorded = { ...entry, signerQuorumId: quorum };
85106
+ const recorded = { ...entry, signerQuorumId: quorum, pendingRequest: undefined };
84949
85107
  await saveKeySignerEntry(deps, recorded);
84950
85108
  const reread = await readSigner(ctx, "self", { apiKey });
84951
85109
  return reportApproved(ctx, reread.ok ? reread.body : view, recorded, {
@@ -84982,6 +85140,27 @@ async function teeSignerNew(args, ctx) {
84982
85140
  }, json);
84983
85141
  return 1;
84984
85142
  }
85143
+ entry = {
85144
+ ...entry,
85145
+ pendingRequest: {
85146
+ apiUrl: ctx.apiUrl,
85147
+ userCode: body.userCode,
85148
+ verificationUri: body.verificationUri,
85149
+ verificationUriComplete: body.verificationUriComplete,
85150
+ expiresAt: body.expiresAt
85151
+ }
85152
+ };
85153
+ await saveKeySignerEntry(deps, entry);
85154
+ if (json) {
85155
+ deps.stdout.write(`${JSON.stringify({
85156
+ state: "pending",
85157
+ userCode: body.userCode,
85158
+ verificationUriComplete: body.verificationUriComplete,
85159
+ expiresAt: body.expiresAt,
85160
+ fingerprint: body.fingerprint
85161
+ })}
85162
+ `);
85163
+ }
84985
85164
  if (json) {
84986
85165
  writeFields(deps, "stderr", [
84987
85166
  ["Key", keyPrefix],
@@ -84992,7 +85171,7 @@ async function teeSignerNew(args, ctx) {
84992
85171
  ["Approve on owner machine", `candle keys signer approve ${body.userCode} --key ${keyPrefix}`],
84993
85172
  ["Expires", formatTime(body.expiresAt)]
84994
85173
  ], "Key signer request");
84995
- notice(deps, "stderr", "note", "The account owner approves it, typing this full fingerprint. Waiting for approval...");
85174
+ notice(deps, "stderr", "note", `The account owner approves it, typing this full fingerprint.${noWait ? "" : " Waiting for approval..."}`);
84996
85175
  } else {
84997
85176
  writeFields(deps, "stderr", [
84998
85177
  ["Key", keyPrefix],
@@ -85004,9 +85183,11 @@ async function teeSignerNew(args, ctx) {
85004
85183
  ], "Key signer request");
85005
85184
  notice(deps, "stderr", "warning", "The account owner approves it, typing this full fingerprint.");
85006
85185
  notice(deps, "stderr", "note", `Or, on the owner's machine:
85007
- candle keys signer approve ${body.userCode} --key ${keyPrefix}
85008
- Waiting for approval...`);
85186
+ candle keys signer approve ${body.userCode} --key ${keyPrefix}${noWait ? "" : `
85187
+ Waiting for approval...`}`);
85009
85188
  }
85189
+ if (noWait)
85190
+ return 0;
85010
85191
  while (deps.now() < body.expiresAt) {
85011
85192
  await deps.sleep(SIGNER_POLL_MS);
85012
85193
  const read = await readSigner(ctx, "self", { apiKey });
@@ -85015,9 +85196,9 @@ Waiting for approval...`);
85015
85196
  view = read.body;
85016
85197
  const now = activeSigner(view);
85017
85198
  if (now !== null && now.spkiSha256 === entry.spkiSha256) {
85018
- const recorded = { ...entry, signerQuorumId: now.signerQuorumId };
85199
+ const recorded = { ...entry, signerQuorumId: now.signerQuorumId, pendingRequest: undefined };
85019
85200
  await saveKeySignerEntry(deps, recorded);
85020
- return reportApproved(ctx, view, recorded, { resumed, requested: true, replaces });
85201
+ return reportApproved(ctx, view, recorded, { resumed, requested: cached === undefined, replaces });
85021
85202
  }
85022
85203
  if (view.pending === null || view.pending.spkiSha256 !== entry.spkiSha256) {
85023
85204
  writeScreenLocalFailure(deps, {
@@ -85037,6 +85218,58 @@ Waiting for approval...`);
85037
85218
  }, json);
85038
85219
  return 1;
85039
85220
  }
85221
+ function matchingPendingRequest(ctx, view, entry) {
85222
+ const pending = entry.pendingRequest;
85223
+ return pending?.apiUrl === ctx.apiUrl && pending.expiresAt > ctx.deps.now() && view.pending?.spkiSha256 === entry.spkiSha256 && view.pending.expiresAt === pending.expiresAt ? pending : undefined;
85224
+ }
85225
+ async function teeSignerStatus(args, ctx) {
85226
+ const { deps, json } = ctx;
85227
+ const parsed = parseArgs(args, { valueFlags: ["--key"] });
85228
+ if ("error" in parsed || !parsed.values["--key"] || parsed.positionals.length > 0) {
85229
+ writeScreenUsageFailure(deps, USAGE_TEE_SIGNER, json);
85230
+ return 2;
85231
+ }
85232
+ await printIdentity(ctx);
85233
+ const apiKey = await resolveApiKey(deps, ctx.profile);
85234
+ if (!apiKey) {
85235
+ writeScreenLocalFailure(deps, { code: "NO_API_KEY", message: "tee signer status needs this key's API key." }, json);
85236
+ return 1;
85237
+ }
85238
+ const read = await readSigner(ctx, "self", { apiKey });
85239
+ if (!read.ok)
85240
+ return writeSignerReadFailure(ctx, read);
85241
+ const view = read.body;
85242
+ const keyRaw = parsed.values["--key"];
85243
+ if (keyRaw !== view.keyPrefix && keyRaw !== view.keyLabel) {
85244
+ writeScreenLocalFailure(deps, { code: "KEY_SIGNER_KEY_MISMATCH", message: `This API key is ${view.keyPrefix}, not ${keyRaw}.` }, json);
85245
+ return 1;
85246
+ }
85247
+ const locals = await localKeySigners(deps, view.keyPrefix);
85248
+ const entry = locals.find((local) => local.spkiSha256 === view.pending?.spkiSha256);
85249
+ const pending = entry ? matchingPendingRequest(ctx, view, entry) : undefined;
85250
+ if (json) {
85251
+ deps.stdout.write(`${JSON.stringify({
85252
+ ok: true,
85253
+ command: "tee signer status",
85254
+ ...view,
85255
+ ...pending ? {
85256
+ userCode: pending.userCode,
85257
+ verificationUriComplete: pending.verificationUriComplete,
85258
+ expiresAt: pending.expiresAt
85259
+ } : {}
85260
+ })}
85261
+ `);
85262
+ } else {
85263
+ writeFields(deps, "stdout", [
85264
+ ["Key", view.keyPrefix],
85265
+ ["State", view.state],
85266
+ ["Fingerprint", view.pending?.fingerprint ?? view.fingerprint],
85267
+ ["Approve at", pending?.verificationUriComplete],
85268
+ ["Expires", view.pending?.expiresAt ? formatTime(view.pending.expiresAt, { now: deps.now() }) : undefined]
85269
+ ], "Key signer status");
85270
+ }
85271
+ return 0;
85272
+ }
85040
85273
  async function writeOut(ctx, out, entry) {
85041
85274
  if (out === undefined)
85042
85275
  return;
@@ -89541,6 +89774,7 @@ var REASON_WORDS = {
89541
89774
  delegation_off: "the embedded wallet is not delegated for agent access",
89542
89775
  embedded_wallet_not_permitted: "this key may not use the embedded wallet",
89543
89776
  not_recipient: "the fees go to someone else, or the token has no fee vault",
89777
+ fee_wallet_designated: "the fees go to this launch's designated fee wallet, which claims them itself",
89544
89778
  insufficient_gas: "the recipient wallet cannot cover the network fee"
89545
89779
  };
89546
89780
  function reasonText(reason, missing) {
@@ -89612,7 +89846,53 @@ async function feesClaim(args, ctx) {
89612
89846
  const dryRun = parsed.booleans.has("--dry-run");
89613
89847
  try {
89614
89848
  const key = await tradingKey(ctx);
89615
- const body = await request(ctx, key, `/api/v1/agent/fees/${encodeURIComponent(mint)}/claim`, { dryRun });
89849
+ let body = await request(ctx, key, `/api/v1/agent/fees/${encodeURIComponent(mint)}/claim`, {
89850
+ dryRun,
89851
+ teeSigning: true
89852
+ });
89853
+ if (!dryRun && Array.isArray(body.signing) && body.signing.length > 0) {
89854
+ if (body.signing.length > 2)
89855
+ throw new TradingError("INVALID_RESPONSE", "Too many fee claims to sign");
89856
+ const { rows, appId, keyPrefix } = await listTradingWallets(ctx, key, "launch:write");
89857
+ const signedClaims = [];
89858
+ for (const signing of body.signing) {
89859
+ const row = rows.find((row2) => row2.id === signing.walletId && row2.address === signing.wallet);
89860
+ if (!row || row.chain !== "solana")
89861
+ throw new TradingError("TEE_WALLET_REQUIRED", "The claim's wallet is not bound to this key");
89862
+ const wallet3 = await completeTradingWallet(ctx, row, appId, "account:read", "solana", {
89863
+ apiKey: key,
89864
+ keyPrefix
89865
+ });
89866
+ const rpc = signing.body;
89867
+ const transaction = rpc?.params?.transaction;
89868
+ if (typeof transaction !== "string" || !["streamer", "creator"].includes(String(signing.leg)))
89869
+ throw new TradingError("INVALID_RESPONSE", "The fee claim has no Solana transaction to sign");
89870
+ const approval = authorizationSignature(wallet3, transaction);
89871
+ if (JSON.stringify(approval.body) !== JSON.stringify(signing.body))
89872
+ throw new TradingError("INVALID_RESPONSE", "The fee claim's RPC body is not the exact Solana sign request");
89873
+ const signed = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(wallet3.id)}/sign`, {
89874
+ ...approval,
89875
+ feeClaim: true
89876
+ });
89877
+ if (typeof signed.signedTransaction !== "string" || signed.encoding !== "base64")
89878
+ throw new TradingError("INVALID_RESPONSE", "The relay returned no base64 signed fee claim");
89879
+ signedClaims.push({ walletId: wallet3.id, leg: signing.leg, signedTransaction: signed.signedTransaction });
89880
+ }
89881
+ const submitted = await request(ctx, key, `/api/v1/agent/fees/${encodeURIComponent(mint)}/claim/submit`, {
89882
+ signedClaims
89883
+ });
89884
+ body = {
89885
+ ...submitted,
89886
+ claimed: [
89887
+ ...Array.isArray(body.claimed) ? body.claimed : [],
89888
+ ...Array.isArray(submitted.claimed) ? submitted.claimed : []
89889
+ ],
89890
+ warnings: [
89891
+ ...Array.isArray(body.warnings) ? body.warnings : [],
89892
+ ...Array.isArray(submitted.warnings) ? submitted.warnings : []
89893
+ ]
89894
+ };
89895
+ }
89616
89896
  if (ctx.json)
89617
89897
  return printTradingResult(ctx, body);
89618
89898
  const style = terminalStyle(ctx.deps, "stdout");
@@ -89689,6 +89969,7 @@ init_render();
89689
89969
  init_promote_support();
89690
89970
  init_key_output();
89691
89971
  init_keys();
89972
+ init_keys_embedded_wallet();
89692
89973
  init_tee_rebind();
89693
89974
  var KEYS_PATH4 = "/api/v1/agent/keys";
89694
89975
  var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|write|withdraw> [--yes] [--json]
@@ -89852,6 +90133,7 @@ async function keysAccess(args, ctx) {
89852
90133
  let path;
89853
90134
  let auth;
89854
90135
  let credentials;
90136
+ let embeddedWallet;
89855
90137
  if (self) {
89856
90138
  const apiKey = await resolveApiKey(deps, ctx.profile);
89857
90139
  if (!apiKey) {
@@ -89873,6 +90155,7 @@ async function keysAccess(args, ctx) {
89873
90155
  });
89874
90156
  if (!resolved.ok)
89875
90157
  return resolved.code;
90158
+ embeddedWallet = resolved.embeddedWallet;
89876
90159
  path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
89877
90160
  auth = "device";
89878
90161
  credentials = { deviceToken };
@@ -89911,6 +90194,9 @@ async function keysAccess(args, ctx) {
89911
90194
  const account = fields2.account ? shortAddress2(fields2.account) : json ? "unknown" : symbols(terminalStyle(deps, "stderr")).missing;
89912
90195
  deps.stderr.write(`${accessScreen(shown, { account: fields2.username ? `${fields2.username} (${account})` : account, apiUrl }, json ? undefined : terminalStyle(deps, "stderr"))}
89913
90196
  `);
90197
+ if (shown.to.access === "readwritetransfer" && embeddedWallet === "denied") {
90198
+ notice(deps, "stderr", "warning", withdrawNeedsEmbeddedWalletLine(shown.keyPrefix));
90199
+ }
89914
90200
  if (widen) {
89915
90201
  const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
89916
90202
  if (typed.trim() !== shown.keyPrefix) {
@@ -90754,6 +91040,7 @@ function feeWalletLines(selection, echo) {
90754
91040
  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)."
90755
91041
  ];
90756
91042
  }
91043
+ var FEE_WALLET_DEFAULT_WARNING = "No --fee-wallet given: this launch's curve and pool fees earn to the account wallet. Pass --fee-wallet <address> to send them to another wallet.";
90757
91044
  function feeWalletQuoteLine(selection) {
90758
91045
  if (!selection)
90759
91046
  return "defaults to the account wallet";
@@ -91109,7 +91396,8 @@ var LAUNCH_ARG_SPEC = {
91109
91396
  booleanFlags: [
91110
91397
  ...launchFlagFields.filter((field) => field.type === "boolean").map((field) => field.cli.flag),
91111
91398
  "--check",
91112
- "--yes"
91399
+ "--yes",
91400
+ "--require-fee-wallet"
91113
91401
  ],
91114
91402
  pathFlags: ["--image", "--banner"]
91115
91403
  };
@@ -91251,6 +91539,10 @@ async function launch(args, ctx) {
91251
91539
  writeUsageFailure(ctx.deps, `${error.message}. ${USAGE9}`, ctx.json);
91252
91540
  return 2;
91253
91541
  }
91542
+ if (parsed.booleans.has("--require-fee-wallet") && !feeWallet) {
91543
+ writeUsageFailure(ctx.deps, `--require-fee-wallet needs --fee-wallet <address>: pass the wallet this launch's fees earn to. ${USAGE9}`, ctx.json);
91544
+ return 2;
91545
+ }
91254
91546
  try {
91255
91547
  if (flags["--buy"]?.trim() && !/^0+(\.0+)?$/.test(flags["--buy"]))
91256
91548
  launchBuyRaw(flags["--buy"], 18);
@@ -91318,6 +91610,8 @@ async function launch(args, ctx) {
91318
91610
  const chain2 = wallet3.chain === "evm" ? "hood" : "solana";
91319
91611
  if (chain2 === "hood" && flags["--dex-version"] === undefined)
91320
91612
  throw new TradingUsage(`${wallet3.address} is a Hood TEE wallet, and a Hood launch needs --dex-version v3|v4.`);
91613
+ if (chain2 === "solana" && !dryRun && !feeWallet)
91614
+ notice(ctx.deps, "stderr", "warning", FEE_WALLET_DEFAULT_WARNING);
91321
91615
  const buy = launchBuy(flags, chain2);
91322
91616
  if (chain2 === "hood" && (Object.keys(poolRequestFields(flags)).length || feeWalletGiven(flags)))
91323
91617
  throw new TradingUsage("Pool fee and pool shape choices are Solana only, and so is --fee-wallet; a Hood launch cannot take them.");
@@ -91767,6 +92061,8 @@ async function embeddedLaunch(ctx, key, args) {
91767
92061
  firstBuyCeiling(ctx, flags, buy);
91768
92062
  if (chain2 === "hood" && (Object.keys(poolRequestFields(flags)).length || feeWalletGiven(flags)))
91769
92063
  throw new TradingUsage("Pool fee and pool shape choices are Solana only, and so is --fee-wallet; a Hood launch cannot take them.");
92064
+ if (chain2 === "solana" && !args.dryRun && !feeWalletGiven(flags))
92065
+ notice(ctx.deps, "stderr", "warning", FEE_WALLET_DEFAULT_WARNING);
91770
92066
  const wallet3 = await embeddedLaunchWallet(ctx, key, chain2);
91771
92067
  const uploaded = await uploadLaunchImages(ctx, key, args);
91772
92068
  if (typeof uploaded === "number")
@@ -92023,6 +92319,92 @@ async function resumeHoodLaunch(ctx, key, id, operationId, prior) {
92023
92319
  return printTradingResult(ctx, { ...result, clientTradeId: id, kind: "launch", chain: "hood", operationId });
92024
92320
  }
92025
92321
 
92322
+ // src/commands/launch-status.ts
92323
+ init_args();
92324
+ init_presentation();
92325
+ init_render();
92326
+ init_trading();
92327
+ init_swap();
92328
+ var USAGE10 = "Usage: candle launch status <mint-or-client-launch-id> [--wait]";
92329
+ var LAUNCH_WAIT_MS = 10 * 60 * 1000;
92330
+ var LAUNCH_WAIT_POLL_MS = 1e4;
92331
+ var LIVE = new Set(["trading", "bonded", "migrated"]);
92332
+ function statusOf(body) {
92333
+ const status = body.status;
92334
+ return status !== null && typeof status === "object" && !Array.isArray(status) ? status : null;
92335
+ }
92336
+ function isFinal(status) {
92337
+ if (!status)
92338
+ return false;
92339
+ if (status.failed === true)
92340
+ return true;
92341
+ return typeof status.state === "string" && LIVE.has(status.state);
92342
+ }
92343
+ function humanLines(status) {
92344
+ const lines = [`state: ${safeText(String(status.state ?? ""))}`];
92345
+ if (status.failed === true)
92346
+ lines.push("failed: true");
92347
+ if (typeof status.errorCode === "string")
92348
+ lines.push(`error: ${safeText(status.errorCode)}`);
92349
+ if (typeof status.chain === "string")
92350
+ lines.push(`chain: ${safeText(status.chain)}`);
92351
+ if (typeof status.mint === "string")
92352
+ lines.push(`mint: ${safeText(status.mint)}`);
92353
+ if (typeof status.clientLaunchId === "string")
92354
+ lines.push(`clientLaunchId: ${safeText(status.clientLaunchId)}`);
92355
+ if (typeof status.lifecycle === "string")
92356
+ lines.push(`lifecycle: ${safeText(status.lifecycle)}`);
92357
+ const migration = status.migration;
92358
+ if (migration !== null && typeof migration === "object" && !Array.isArray(migration)) {
92359
+ const block2 = migration;
92360
+ const progress = typeof block2.progressBps === "number" ? ` ${String(block2.progressBps)} bps` : "";
92361
+ if (typeof block2.status === "string")
92362
+ lines.push(`migration: ${safeText(block2.status)}${progress}`);
92363
+ }
92364
+ if (typeof status.signature === "string")
92365
+ lines.push(`signature: ${safeText(status.signature)}`);
92366
+ return lines;
92367
+ }
92368
+ async function readStatus(ctx, key, id) {
92369
+ return request(ctx, key, `/api/v1/launch/status/${encodeURIComponent(id)}`);
92370
+ }
92371
+ async function launchStatus(args, ctx) {
92372
+ const parsed = parseArgs(args, { booleanFlags: ["--wait"] });
92373
+ if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "")) {
92374
+ writeUsageFailure(ctx.deps, USAGE10, ctx.json);
92375
+ return 2;
92376
+ }
92377
+ const id = parsed.positionals[0];
92378
+ const wait = parsed.booleans.has("--wait");
92379
+ try {
92380
+ const key = await tradingKey(ctx);
92381
+ let body = await readStatus(ctx, key, id);
92382
+ if (wait) {
92383
+ const deadline = ctx.deps.now() + LAUNCH_WAIT_MS;
92384
+ while (!isFinal(statusOf(body)) && ctx.deps.now() < deadline) {
92385
+ await ctx.deps.sleep(LAUNCH_WAIT_POLL_MS);
92386
+ body = await readStatus(ctx, key, id);
92387
+ }
92388
+ const final = isFinal(statusOf(body));
92389
+ if (!final) {
92390
+ notice(ctx.deps, "stderr", "note", `Still not trading after ten minutes. Re-check with candle launch status ${safeText(id)} --wait.`);
92391
+ }
92392
+ body = { ...body, waited: { final } };
92393
+ }
92394
+ if (ctx.json)
92395
+ return printTradingResult(ctx, body);
92396
+ const status = statusOf(body);
92397
+ if (!status)
92398
+ return printTradingResult(ctx, body);
92399
+ ctx.deps.stdout.write(`${humanLines(status).join(`
92400
+ `)}
92401
+ `);
92402
+ return 0;
92403
+ } catch (error) {
92404
+ return tradingFailure(ctx, error);
92405
+ }
92406
+ }
92407
+
92026
92408
  // src/commands/lp.ts
92027
92409
  init_args();
92028
92410
  init_client();
@@ -97883,7 +98265,7 @@ init_swap();
97883
98265
  init_vault_support();
97884
98266
  var WITHDRAW_SCOPE = "perps:withdraw";
97885
98267
  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>";
98268
+ var USAGE11 = "Usage: candle perps withdraw <amount> --to solana|hood [--wallet <evm tee>] [--id <id>] [--keystore <path>] | candle perps withdraw status <id>";
97887
98269
  var WITHDRAW_POLL_MS = 10 * 60 * 1000;
97888
98270
  var WITHDRAW_POLL_INTERVAL_MS = 5000;
97889
98271
  var TERMINAL = new Set(["settled", "refunded", "failed"]);
@@ -97934,7 +98316,7 @@ async function withdrawStatus(ctx, id) {
97934
98316
  async function perpsWithdraw(args, ctx) {
97935
98317
  if (args[0] === "status") {
97936
98318
  if (args.length !== 2 || !ID_RE.test(args[1]))
97937
- return usage10(ctx, USAGE10);
98319
+ return usage10(ctx, USAGE11);
97938
98320
  try {
97939
98321
  return await withdrawStatus(ctx, args[1]);
97940
98322
  } catch (error) {
@@ -97953,7 +98335,7 @@ async function perpsWithdraw(args, ctx) {
97953
98335
  const [amount] = parsed.positionals;
97954
98336
  const to = parsed.values["--to"];
97955
98337
  if (parsed.positionals.length !== 1 || !amount || to !== "solana" && to !== "hood")
97956
- return usage10(ctx, USAGE10);
98338
+ return usage10(ctx, USAGE11);
97957
98339
  const amountMicros = parseWithdrawAmount(amount);
97958
98340
  if (amountMicros === null)
97959
98341
  return usage10(ctx, "The amount must be a positive USDC decimal with at most 6 decimals.");
@@ -99841,12 +100223,12 @@ function planPromotionLine2(table) {
99841
100223
  init_presentation();
99842
100224
  init_render();
99843
100225
  init_key_output();
99844
- var USAGE11 = "Usage: candle plans";
100226
+ var USAGE12 = "Usage: candle plans";
99845
100227
  async function plans(args, ctx) {
99846
100228
  const parsed = parseArgs(args, {});
99847
100229
  if ("error" in parsed || parsed.positionals.length > 0) {
99848
100230
  writeScreenUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
99849
- ${USAGE11}` : USAGE11, ctx.json);
100231
+ ${USAGE12}` : USAGE12, ctx.json);
99850
100232
  return 2;
99851
100233
  }
99852
100234
  const result = await apiRequest("/api/v1/agent/plans", {
@@ -102120,7 +102502,7 @@ init_client();
102120
102502
  init_human_output();
102121
102503
  init_presentation();
102122
102504
  init_render();
102123
- var USAGE12 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
102505
+ var USAGE13 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
102124
102506
  var OTHER_CHAIN = "Candle trades Solana and Hood today; that is not a Solana or Hood contract address.";
102125
102507
  var HOOD_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
102126
102508
  var SOLANA_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
@@ -102220,20 +102602,20 @@ async function scan(args, ctx) {
102220
102602
  }
102221
102603
  const named = parsed.values["--chain"];
102222
102604
  if (parsed.positionals.length !== 1 || named !== undefined && named !== "solana" && named !== "hood") {
102223
- writeUsageFailure(ctx.deps, USAGE12, ctx.json);
102605
+ writeUsageFailure(ctx.deps, USAGE13, ctx.json);
102224
102606
  return 2;
102225
102607
  }
102226
102608
  const address = parsed.positionals[0].trim();
102227
102609
  const found = chainOfAddress(address);
102228
102610
  if (found === null) {
102229
102611
  writeUsageFailure(ctx.deps, `${OTHER_CHAIN}
102230
- ${USAGE12}`, ctx.json);
102612
+ ${USAGE13}`, ctx.json);
102231
102613
  return 2;
102232
102614
  }
102233
102615
  if (named !== undefined && named !== found) {
102234
102616
  const label = (c) => c === "hood" ? "Hood" : "Solana";
102235
102617
  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);
102618
+ ${USAGE13}`, ctx.json);
102237
102619
  return 2;
102238
102620
  }
102239
102621
  const chain2 = found;
@@ -104111,7 +104493,7 @@ init_deps();
104111
104493
  init_human_output();
104112
104494
  init_render();
104113
104495
  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]";
104496
+ var USAGE14 = "Usage: candle token events <mint> [--chain solana|hood] [--kind trade|signal|callout] [--since <iso-8601>] [--limit <n>] [--before <cursor>] [--json]";
104115
104497
  function readEventsBody(body) {
104116
104498
  if (body === null || typeof body !== "object" || !Array.isArray(body.events))
104117
104499
  return null;
@@ -104169,11 +104551,11 @@ async function tokenEvents(args, ctx) {
104169
104551
  const since = parsed.values["--since"];
104170
104552
  const before = parsed.values["--before"];
104171
104553
  if (parsed.positionals.length !== 1 || chain2 !== "solana" && chain2 !== "hood") {
104172
- writeUsageFailure(ctx.deps, USAGE13, ctx.json);
104554
+ writeUsageFailure(ctx.deps, USAGE14, ctx.json);
104173
104555
  return 2;
104174
104556
  }
104175
104557
  if (kind !== undefined && !KINDS.includes(kind)) {
104176
- writeUsageFailure(ctx.deps, USAGE13, ctx.json);
104558
+ writeUsageFailure(ctx.deps, USAGE14, ctx.json);
104177
104559
  return 2;
104178
104560
  }
104179
104561
  if (limit !== undefined && !/^[1-9][0-9]{0,3}$/.test(limit) || Number(limit) > 1000) {
@@ -104270,7 +104652,7 @@ var TOKEN_PAGE_BODY_CAP_BYTES = 32 * 1024;
104270
104652
  var TOKEN_PAGE_SECTIONS = ["thesis", "tokenomics", "lore"];
104271
104653
  var FLAG_REASONS = ["spam", "scam", "abuse", "misleading", "unsafe_link", "other"];
104272
104654
  var FLAG_DETAIL_LIMIT = 500;
104273
- var USAGE14 = {
104655
+ var USAGE15 = {
104274
104656
  about: "Usage: candle token about <address> [--chain solana|hood] [--capabilities]",
104275
104657
  claim: "Usage: candle token claim <address> [--chain solana|hood] [--creator [--challenge [--message-out <path>] | --challenge-id <id> --signature <sig>]] [--idempotency-key <key>]",
104276
104658
  section: `Usage: candle token section set <address> <thesis|tokenomics|lore> --file <path> --base-revision <n> [--clear] [--chain solana|hood] [--idempotency-key <key>]
@@ -104530,7 +104912,7 @@ function terminalTextKeepingLines(text4) {
104530
104912
  `);
104531
104913
  }
104532
104914
  async function tokenAbout(args, ctx) {
104533
- const usage12 = usageFor(ctx, USAGE14.about);
104915
+ const usage12 = usageFor(ctx, USAGE15.about);
104534
104916
  const parsed = parseArgs(args, { valueFlags: ["--chain"], booleanFlags: ["--capabilities"] });
104535
104917
  if ("error" in parsed)
104536
104918
  return usage12(parsed.error);
@@ -104576,7 +104958,7 @@ async function tokenAbout(args, ctx) {
104576
104958
  return 0;
104577
104959
  }
104578
104960
  async function tokenClaim(args, ctx) {
104579
- const usage12 = usageFor(ctx, USAGE14.claim);
104961
+ const usage12 = usageFor(ctx, USAGE15.claim);
104580
104962
  const parsed = parseArgs(args, {
104581
104963
  valueFlags: ["--chain", "--challenge-id", "--signature", "--message-out", "--idempotency-key"],
104582
104964
  booleanFlags: ["--creator", "--challenge"],
@@ -104703,7 +105085,7 @@ var SECTION_FLAGS = {
104703
105085
  restore: { valueFlags: ["--chain", "--version-id", "--base-revision", "--idempotency-key"], booleanFlags: [] }
104704
105086
  };
104705
105087
  async function tokenSection(args, ctx) {
104706
- const usage12 = usageFor(ctx, USAGE14.section);
105088
+ const usage12 = usageFor(ctx, USAGE15.section);
104707
105089
  const action2 = args[0];
104708
105090
  if (action2 !== "set" && action2 !== "restore")
104709
105091
  return usage12();
@@ -104796,7 +105178,7 @@ async function tokenSection(args, ctx) {
104796
105178
  return 0;
104797
105179
  }
104798
105180
  async function tokenHistory(args, ctx) {
104799
- const usage12 = usageFor(ctx, USAGE14.history);
105181
+ const usage12 = usageFor(ctx, USAGE15.history);
104800
105182
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
104801
105183
  if ("error" in parsed)
104802
105184
  return usage12(parsed.error);
@@ -104851,7 +105233,7 @@ More: --cursor ${data.nextCursor}
104851
105233
  return 0;
104852
105234
  }
104853
105235
  async function tokenFlag(args, ctx) {
104854
- const usage12 = usageFor(ctx, USAGE14.flag);
105236
+ const usage12 = usageFor(ctx, USAGE15.flag);
104855
105237
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--content", "--reason", "--detail", "--idempotency-key"] });
104856
105238
  if ("error" in parsed)
104857
105239
  return usage12(parsed.error);
@@ -104946,7 +105328,7 @@ async function tokenUpdate(args, ctx) {
104946
105328
  return 0;
104947
105329
  }
104948
105330
  async function tokenComment(args, ctx) {
104949
- const usage12 = usageFor(ctx, USAGE14.comment);
105331
+ const usage12 = usageFor(ctx, USAGE15.comment);
104950
105332
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--file", "--idempotency-key"], pathFlags: ["--file"] });
104951
105333
  if ("error" in parsed)
104952
105334
  return usage12(parsed.error);
@@ -104991,7 +105373,7 @@ async function tokenComment(args, ctx) {
104991
105373
  return 0;
104992
105374
  }
104993
105375
  async function tokenComments(args, ctx) {
104994
- const usage12 = usageFor(ctx, USAGE14.comments);
105376
+ const usage12 = usageFor(ctx, USAGE15.comments);
104995
105377
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
104996
105378
  if ("error" in parsed)
104997
105379
  return usage12(parsed.error);
@@ -105337,7 +105719,7 @@ init_render();
105337
105719
  init_solana_verify();
105338
105720
  init_trading();
105339
105721
  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]";
105722
+ 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
105723
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
105342
105724
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
105343
105725
  async function readLinkedWallets(ctx, key) {
@@ -105357,6 +105739,12 @@ async function readLinkedWallets(ctx, key) {
105357
105739
  }
105358
105740
  return rows;
105359
105741
  }
105742
+ async function readWithdrawalAddresses(ctx, key) {
105743
+ const body = await request(ctx, key, "/api/v1/agent/withdrawal-addresses");
105744
+ if (!Array.isArray(body.addresses))
105745
+ throw new TradingError("INVALID_RESPONSE", "The withdrawal allowlist read did not return a list.");
105746
+ return body.addresses;
105747
+ }
105360
105748
  async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
105361
105749
  const hood = chain2 === "hood";
105362
105750
  const same = (a, b) => hood ? sameEvmAddress(a, b) : a === b;
@@ -105382,13 +105770,35 @@ async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
105382
105770
  return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
105383
105771
  if (same(to, source.address))
105384
105772
  throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
105773
+ const allowlist = (await readWithdrawalAddresses(ctx, key)).filter((row) => row.chain === chain2 && row.revokedAt === undefined && !same(row.address, source.address));
105774
+ const approved = allowlist.filter((row) => row.label === to || row.id === to || same(row.address, to));
105775
+ if (approved.length > 1)
105776
+ 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.`);
105777
+ const listed = approved[0];
105778
+ if (listed) {
105779
+ const address = hood ? checkEvmAddress(listed.address) : undefined;
105780
+ if (address !== undefined && !address.ok)
105781
+ throw new TradingError("INVALID_RESPONSE", "Candle listed an approved Hood withdrawal address that is not an EVM address; nothing was built.");
105782
+ return {
105783
+ kind: "approved",
105784
+ address: address?.ok ? address.address : listed.address,
105785
+ id: listed.id,
105786
+ ...listed.label ? { label: listed.label } : {}
105787
+ };
105788
+ }
105385
105789
  if (!hood && BASE58_ADDRESS.test(to))
105386
105790
  return { kind: "address", address: to };
105387
105791
  const evm = hood ? checkEvmAddress(to) : undefined;
105388
105792
  if (evm?.ok)
105389
105793
  return { kind: "address", address: evm.address };
105390
105794
  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("; ")}.`);
105795
+ const known = [
105796
+ ...rows.length > 0 ? [`Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`] : [],
105797
+ ...allowlist.length > 0 ? [
105798
+ `Approved withdrawal addresses: ${allowlist.map((row) => `${row.label ?? ""} (${row.id}, ${row.address})`.trim()).join("; ")}.`
105799
+ ] : []
105800
+ ];
105801
+ 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
105802
  }
105393
105803
  function describeDestination(destination) {
105394
105804
  switch (destination.kind) {
@@ -105396,10 +105806,31 @@ function describeDestination(destination) {
105396
105806
  return `this wallet's pinned vault ${destination.address}`;
105397
105807
  case "linked":
105398
105808
  return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
105809
+ case "approved":
105810
+ return `approved withdrawal address ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
105399
105811
  case "address":
105400
- return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
105812
+ return `address ${destination.address} (not one of this account's linked wallets or approved withdrawal addresses; Candle decides whether it is allowed)`;
105401
105813
  }
105402
105814
  }
105815
+ function destinationNote(destination, hood) {
105816
+ switch (destination.kind) {
105817
+ case "vault":
105818
+ return "Destination: this wallet's own vault, pinned when it was imported.";
105819
+ case "linked":
105820
+ 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.`;
105821
+ case "approved":
105822
+ 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.`;
105823
+ case "address":
105824
+ 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.";
105825
+ }
105826
+ }
105827
+ function builtDestination(built) {
105828
+ if (built.destinationClass === "approved")
105829
+ return [["To", "an approved withdrawal address"]];
105830
+ if (typeof built.destinationKind === "string")
105831
+ return [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]];
105832
+ return [];
105833
+ }
105403
105834
  function writePreview(ctx, title2, rows, note) {
105404
105835
  const style = terminalStyle(ctx.deps, "stderr");
105405
105836
  ctx.deps.stderr.write(`${heading(title2, style)}
@@ -105433,7 +105864,7 @@ async function transfer(args, ctx) {
105433
105864
  const flags2 = parsed.values;
105434
105865
  const asset = flags2["--asset"]?.toUpperCase();
105435
105866
  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);
105867
+ writeUsageFailure(ctx.deps, USAGE16, ctx.json);
105437
105868
  return 2;
105438
105869
  }
105439
105870
  const to = flags2["--to"];
@@ -105470,7 +105901,7 @@ async function transfer(args, ctx) {
105470
105901
  const amountText2 = isMax ? `the full spendable balance of ${label}` : `${flags2["--amount"]} ${label}`;
105471
105902
  const confirmed = await confirmTransfer(ctx, [
105472
105903
  `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."
105904
+ destinationNote(destination, false)
105474
105905
  ], parsed.booleans.has("--yes"));
105475
105906
  if (!confirmed)
105476
105907
  return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet3.id, destination });
@@ -105491,11 +105922,7 @@ async function transfer(args, ctx) {
105491
105922
  if (builtAmount === null || !isMax && builtAmount !== amountRaw)
105492
105923
  throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the confirmed amount; nothing was signed.");
105493
105924
  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
- ]);
105925
+ writePreview(ctx, "Built", [["Transfer", transferId], ["Amount", amountShown], ...builtDestination(built)]);
105499
105926
  const reader = await solana();
105500
105927
  const problem = await tradingRead(ctx, reader, () => solanaSpendProblem(unsigned[0], wallet3.address, reader.rpc, {
105501
105928
  kind: "transfer",
@@ -105557,7 +105984,7 @@ async function hoodTransfer(ctx, args) {
105557
105984
  const amountText2 = isMax ? `the full spendable balance of ${label}` : `${flags2["--amount"]} ${label}`;
105558
105985
  const confirmed = await confirmTransfer(ctx, [
105559
105986
  `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.",
105987
+ destinationNote(destination, true),
105561
105988
  ...asset === "ETH" ? [
105562
105989
  "ETH keeps this send's gas and the sweep-home gas reserve in the wallet; Candle refuses an amount that would spend them."
105563
105990
  ] : []
@@ -105592,7 +106019,7 @@ async function hoodTransfer(ctx, args) {
105592
106019
  writePreview(ctx, "Built", [
105593
106020
  ["Transfer", safeText(transferId)],
105594
106021
  ["Amount", `${decimalAmount(amountRaw, decimals)} ${safeText(label)}`],
105595
- ...typeof built.destinationKind === "string" ? [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]] : [],
106022
+ ...builtDestination(built),
105596
106023
  [
105597
106024
  "Gas",
105598
106025
  `up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)`
@@ -107037,8 +107464,332 @@ init_reconcile_grant();
107037
107464
  init_tee_resolve();
107038
107465
  init_tee();
107039
107466
  init_tee_evm();
107467
+
107468
+ // src/commands/vault-demote-batch.ts
107469
+ init_args();
107470
+ init_human_output();
107471
+ init_presentation();
107472
+ init_solana_endpoint();
107473
+ init_errors2();
107474
+ init_promote_support();
107475
+ init_store();
107476
+ init_tee();
107477
+ init_vault_support();
107478
+ var BATCH_USAGE = "Usage: candle vault demote <tee-address> <tee-address>... | --all --empty [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--yes]";
107479
+ var VALUE_FLAGS = new Set(["--rpc-url", "--keystore", "-k", "--sweep-to", "--from-block", "--token"]);
107480
+ var BATCH_FLAGS = new Set(["--all", "--empty", "--yes"]);
107481
+ var SINGLE_ONLY_FLAGS = ["--token", "--from-block", "--accept-unknown-exposure"];
107482
+ function isBatchDemote(args) {
107483
+ let addresses = 0;
107484
+ for (let i = 0;i < args.length; i++) {
107485
+ const arg = args[i];
107486
+ if (VALUE_FLAGS.has(arg)) {
107487
+ i += 1;
107488
+ continue;
107489
+ }
107490
+ if (BATCH_FLAGS.has(arg))
107491
+ return true;
107492
+ if (!arg.startsWith("-"))
107493
+ addresses += 1;
107494
+ }
107495
+ return addresses > 1;
107496
+ }
107497
+ var isEvmAddress = (value) => /^0x[0-9a-fA-F]{40}$/.test(value);
107498
+ var sameAddress5 = (chain2, a, b) => chain2 === "evm" ? a.toLowerCase() === b.toLowerCase() : a === b;
107499
+ async function vaultDemoteBatch(args, ctx) {
107500
+ const single = SINGLE_ONLY_FLAGS.find((flag) => args.includes(flag));
107501
+ if (single !== undefined) {
107502
+ return usage(ctx, `${single} names one Hood wallet's sweep; pass it to a single demote: candle vault demote <0x address> ${single} ...`);
107503
+ }
107504
+ const parsed = parseArgs(args, {
107505
+ valueFlags: ["--rpc-url", "--sweep-to", "--keystore"],
107506
+ booleanFlags: ["--emergency", "--accept-older-copy", "--all", "--empty", "--yes"],
107507
+ pathFlags: ["--keystore"]
107508
+ });
107509
+ if ("error" in parsed)
107510
+ return usage(ctx, parsed.error);
107511
+ const all = parsed.booleans.has("--all");
107512
+ const empty = parsed.booleans.has("--empty");
107513
+ const yes = parsed.booleans.has("--yes");
107514
+ if (all && parsed.positionals.length > 0)
107515
+ return usage(ctx, `--all takes no addresses. ${BATCH_USAGE}`);
107516
+ if (all && !empty) {
107517
+ return usage(ctx, `--all needs a filter: --empty (wallets with nothing left to sweep). ${BATCH_USAGE}`);
107518
+ }
107519
+ if (empty && !all)
107520
+ return usage(ctx, `--empty filters --all. ${BATCH_USAGE}`);
107521
+ if (!all && parsed.positionals.length === 0)
107522
+ return usage(ctx, BATCH_USAGE);
107523
+ const rpcUrl = parsed.values["--rpc-url"];
107524
+ const sweepTo = parsed.values["--sweep-to"];
107525
+ const named = parsed.positionals;
107526
+ if (rpcUrl !== undefined && named.some(isEvmAddress) && named.some((a) => !isEvmAddress(a))) {
107527
+ return usage(ctx, "--rpc-url names one chain's node; demote Solana and Hood wallets in separate batches.");
107528
+ }
107529
+ if (!refuseEnvPassphrase(ctx))
107530
+ return 1;
107531
+ if (!requireTty(ctx, "vault demote"))
107532
+ return 1;
107533
+ const resolvedVault = vaultPathFor(ctx, parsed);
107534
+ if ("error" in resolvedVault)
107535
+ return usage(ctx, resolvedVault.error);
107536
+ let solana;
107537
+ if (empty) {
107538
+ const opened = await openSolanaClient(ctx, rpcUrl);
107539
+ if ("error" in opened)
107540
+ return usage(ctx, opened.error);
107541
+ solana = opened;
107542
+ }
107543
+ return runVaultCommand(ctx, async ({ hold }) => {
107544
+ const raw = await requireVaultRaw(ctx, resolvedVault);
107545
+ const opened = await unlockInteractively(ctx, resolvedVault.path, raw, {
107546
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
107547
+ });
107548
+ const held = hold(opened.vault);
107549
+ const outcomes = [];
107550
+ const planned = [];
107551
+ const plan2 = (entry) => {
107552
+ const chain2 = entry.chain === "evm" ? "evm" : "solana";
107553
+ const row = { address: entry.address, label: entry.label, chain: chain2, destination: null };
107554
+ const pinned = entry.tee?.vaultDestination;
107555
+ if (pinned !== undefined)
107556
+ row.destination = pinned;
107557
+ else if (sweepTo === undefined)
107558
+ row.destinationNote = "no pinned destination; pass --sweep-to";
107559
+ else {
107560
+ try {
107561
+ row.destination = assertColdVaultDestination(held.index, sweepTo, {
107562
+ chain: chain2,
107563
+ subjectAddress: entry.address
107564
+ }).address;
107565
+ } catch (error) {
107566
+ if (!isVaultError(error))
107567
+ throw error;
107568
+ row.destinationNote = `--sweep-to ${sweepTo}: ${error.message}`;
107569
+ }
107570
+ }
107571
+ planned.push(row);
107572
+ };
107573
+ const tees = held.index.entries.filter((entry) => entry.role === "tee-wallet");
107574
+ if (all) {
107575
+ for (const entry of tees) {
107576
+ if (entry.tee?.lifecycle === "retired")
107577
+ continue;
107578
+ const base2 = {
107579
+ address: entry.address,
107580
+ label: entry.label,
107581
+ chain: entry.chain === "evm" ? "evm" : "solana"
107582
+ };
107583
+ if (entry.chain === "evm") {
107584
+ outcomes.push({
107585
+ ...base2,
107586
+ status: "skipped",
107587
+ reason: "--empty reads Solana balances only; demote a Hood wallet by its address"
107588
+ });
107589
+ continue;
107590
+ }
107591
+ if (!await nothingLeftToSweep(solana, entry)) {
107592
+ outcomes.push({
107593
+ ...base2,
107594
+ status: "skipped",
107595
+ reason: "not empty: it holds SOL or token accounts, or an earlier sweep is unsettled"
107596
+ });
107597
+ continue;
107598
+ }
107599
+ plan2(entry);
107600
+ }
107601
+ } else {
107602
+ const seen = [];
107603
+ for (const address of named) {
107604
+ const chain2 = isEvmAddress(address) ? "evm" : "solana";
107605
+ if (seen.some((other) => sameAddress5(chain2, other, address))) {
107606
+ outcomes.push({ address, label: null, chain: chain2, status: "skipped", reason: "listed more than once" });
107607
+ continue;
107608
+ }
107609
+ seen.push(address);
107610
+ const entry = tees.find((candidate) => sameAddress5(chain2, candidate.address, address));
107611
+ if (entry === undefined) {
107612
+ outcomes.push({
107613
+ address,
107614
+ label: null,
107615
+ chain: chain2,
107616
+ status: "failed",
107617
+ code: "TEE_WALLET_UNKNOWN",
107618
+ reason: `${address} is not a TEE wallet in the vault`
107619
+ });
107620
+ continue;
107621
+ }
107622
+ if (entry.tee?.lifecycle === "retired") {
107623
+ outcomes.push({
107624
+ address,
107625
+ label: entry.label,
107626
+ chain: chain2,
107627
+ status: "skipped",
107628
+ reason: "already retired (demoted and swept)"
107629
+ });
107630
+ continue;
107631
+ }
107632
+ plan2(entry);
107633
+ }
107634
+ }
107635
+ if (rpcUrl !== undefined && new Set(planned.map((row) => row.chain)).size > 1) {
107636
+ return usage(ctx, "--rpc-url names one chain's node; demote Solana and Hood wallets in separate batches.");
107637
+ }
107638
+ if (planned.length > 0) {
107639
+ const display = ctx.json ? "stderr" : "stdout";
107640
+ ctx.deps[display].write(`Demote ${planned.length} TEE wallet${planned.length === 1 ? "" : "s"}, one at a time: disable, then sweep to the destination shown.
107641
+ `);
107642
+ writeTable(ctx.deps, display, ["#", "TEE wallet", "Label", "Sweeps to"], planned.map((row, i) => [
107643
+ String(i + 1),
107644
+ row.address,
107645
+ row.label,
107646
+ row.destination ?? `none (${row.destinationNote})`
107647
+ ]), []);
107648
+ for (const skipped of outcomes) {
107649
+ ctx.deps[display].write(`Not demoted: ${skipped.address}: ${skipped.reason}
107650
+ `);
107651
+ }
107652
+ if (!yes) {
107653
+ const typed = await ctx.deps.promptLine(`Type confirm to demote these ${planned.length} wallets: `);
107654
+ if (typed.trim().toLowerCase() !== "confirm") {
107655
+ throw new VaultError("DESTINATION_NOT_CONFIRMED", "The batch was not confirmed; nothing was done.", {
107656
+ suggestion: "Nothing was disabled or swept. Run it again and type confirm at the prompt."
107657
+ });
107658
+ }
107659
+ }
107660
+ }
107661
+ const session = { path: held.path, open: (bytes) => openHeld(opened, held, bytes) };
107662
+ const width = String(planned.length).length;
107663
+ for (const [i, row] of planned.entries()) {
107664
+ if (!ctx.json) {
107665
+ notice(ctx.deps, "stdout", "note", `[${String(i + 1).padStart(width)}/${planned.length}] Demoting ${row.address} (${row.label})`);
107666
+ }
107667
+ const outcome = await demoteOne(ctx, session, row, parsed.values, parsed.booleans);
107668
+ outcomes.push(outcome);
107669
+ if (!ctx.json)
107670
+ writeResultLine(ctx.deps, outcome);
107671
+ }
107672
+ const count3 = (status) => outcomes.filter((o) => o.status === status).length;
107673
+ const summary = {
107674
+ total: outcomes.length,
107675
+ done: count3("done"),
107676
+ incomplete: count3("incomplete"),
107677
+ failed: count3("failed"),
107678
+ skipped: count3("skipped")
107679
+ };
107680
+ const exit = summary.failed > 0 ? 1 : summary.incomplete > 0 ? 3 : 0;
107681
+ if (ctx.json) {
107682
+ writeJson(ctx.deps, { ok: exit === 0, wallets: outcomes, summary });
107683
+ return exit;
107684
+ }
107685
+ ctx.deps.stdout.write(`
107686
+ Summary: ${summary.done} done, ${summary.incomplete} incomplete, ${summary.failed} failed, ${summary.skipped} skipped (of ${summary.total}).
107687
+ `);
107688
+ for (const outcome of outcomes) {
107689
+ if (outcome.status === "done")
107690
+ continue;
107691
+ ctx.deps.stdout.write(` ${outcome.status.padEnd(10)} ${outcome.address}${outcome.reason ? `: ${outcome.reason}` : ""}
107692
+ `);
107693
+ }
107694
+ return exit;
107695
+ });
107696
+ }
107697
+ async function openHeld(opened, held, raw) {
107698
+ const vault = await reopenWithHeldDek(held.path, raw, held);
107699
+ return {
107700
+ vault,
107701
+ factor: opened.factor,
107702
+ reopen: (path, bytes) => reopenWithHeldDek(path, bytes, held),
107703
+ confirm: opened.confirm
107704
+ };
107705
+ }
107706
+ async function demoteOne(ctx, session, row, values, booleans) {
107707
+ const base2 = { address: row.address, label: row.label, chain: row.chain };
107708
+ const args = [row.address];
107709
+ for (const flag of ["--rpc-url", "--sweep-to", "--keystore"]) {
107710
+ const value = values[flag];
107711
+ if (value !== undefined)
107712
+ args.push(flag, value);
107713
+ }
107714
+ for (const flag of ["--emergency", "--accept-older-copy"])
107715
+ if (booleans.has(flag))
107716
+ args.push(flag);
107717
+ const covered = row.destination === null ? null : `Type confirm to sweep everything to ${row.destination}: `;
107718
+ const promptLine = async (text4) => text4 === covered ? "confirm" : ctx.deps.promptLine(text4);
107719
+ const captured = [];
107720
+ const errLines = [];
107721
+ const stdout = ctx.json ? { write: (chunk) => void captured.push(chunk) } : ctx.deps.stdout;
107722
+ const stderr = {
107723
+ write: (chunk) => {
107724
+ errLines.push(chunk);
107725
+ ctx.deps.stderr.write(chunk);
107726
+ }
107727
+ };
107728
+ const deps = { ...ctx.deps, stdout, stderr, promptLine };
107729
+ const step = { ...ctx, deps, heldUnlock: session };
107730
+ let exitCode;
107731
+ let thrown;
107732
+ try {
107733
+ exitCode = await vaultDemote(args, step);
107734
+ } catch (error) {
107735
+ exitCode = 1;
107736
+ thrown = error instanceof Error ? error.message : String(error);
107737
+ }
107738
+ const outcome = {
107739
+ ...base2,
107740
+ status: exitCode === 0 ? "done" : exitCode === 3 ? "incomplete" : "failed",
107741
+ exitCode
107742
+ };
107743
+ if (ctx.json) {
107744
+ const results = [];
107745
+ for (const line of captured.join("").split(`
107746
+ `)) {
107747
+ if (line.trim() === "")
107748
+ continue;
107749
+ try {
107750
+ results.push(JSON.parse(line));
107751
+ } catch {
107752
+ ctx.deps.stderr.write(`${line}
107753
+ `);
107754
+ }
107755
+ }
107756
+ outcome.results = results;
107757
+ const failure3 = [...results].reverse().find((doc) => typeof doc === "object" && doc !== null && doc.ok === false);
107758
+ if (failure3 !== undefined && exitCode !== 0) {
107759
+ if (typeof failure3.code === "string")
107760
+ outcome.code = failure3.code;
107761
+ if (typeof failure3.message === "string")
107762
+ outcome.reason = failure3.message;
107763
+ }
107764
+ }
107765
+ if (thrown !== undefined)
107766
+ outcome.reason = thrown;
107767
+ if (outcome.reason === undefined && exitCode === 3) {
107768
+ outcome.reason = "the disable is not verified or funds remain; remote authority may still be open";
107769
+ } else if (outcome.reason === undefined && exitCode !== 0) {
107770
+ const first = errLines.find((chunk) => chunk.trim() !== "" && !chunk.includes("vault.state.json"));
107771
+ outcome.reason = firstSentence(first ?? "") ?? `exit ${exitCode}`;
107772
+ }
107773
+ return outcome;
107774
+ }
107775
+ function firstSentence(chunk) {
107776
+ const text4 = chunk.replace(/\s+/g, " ").replace(/^[^\p{L}\p{N}]+/u, "").trim();
107777
+ if (text4 === "")
107778
+ return;
107779
+ const end = text4.search(/\.(\s|$)/);
107780
+ return end < 0 ? text4 : text4.slice(0, end + 1);
107781
+ }
107782
+ function writeResultLine(deps, outcome) {
107783
+ const word3 = { done: "done", incomplete: "incomplete", failed: "FAILED", skipped: "skipped" }[outcome.status];
107784
+ deps.stdout.write(`Result ${outcome.address}: ${word3} (exit ${outcome.exitCode})${outcome.reason && outcome.status !== "done" ? `: ${outcome.reason}` : ""}
107785
+ `);
107786
+ }
107787
+
107788
+ // src/commands/vault-demote.ts
107040
107789
  init_vault_support();
107041
107790
  async function vaultDemote(args, ctx) {
107791
+ if (ctx.heldUnlock === undefined && isBatchDemote(args))
107792
+ return vaultDemoteBatch(args, ctx);
107042
107793
  if (namesEvmWallet(args))
107043
107794
  return vaultDemoteEvm(args, ctx);
107044
107795
  const parsed = parseArgs(args, {
@@ -108804,6 +109555,8 @@ async function vaultFund(args, ctx) {
108804
109555
  const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
108805
109556
  if (!amount)
108806
109557
  return usage(ctx, "--amount <n> is required.");
109558
+ if (amount.toLowerCase() === "max")
109559
+ return usage(ctx, "--amount must be a number; funding a wallet takes no max.");
108807
109560
  if (asset !== "SOL" && asset !== "USDC")
108808
109561
  return usage(ctx, "--asset must be SOL or USDC.");
108809
109562
  const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
@@ -113027,6 +113780,7 @@ init_args();
113027
113780
  init_deps();
113028
113781
  init_evm_lite();
113029
113782
  init_solana_endpoint();
113783
+ init_errors2();
113030
113784
  init_evm_tee();
113031
113785
  init_evm_transfer();
113032
113786
  init_promote_support();
@@ -113049,7 +113803,7 @@ async function vaultTransfer(args, ctx) {
113049
113803
  const fromLabel = parsed.values["--from"];
113050
113804
  const rpcUrlFlag = parsed.values["--rpc-url"];
113051
113805
  if (!amount)
113052
- return usage(ctx, "--amount <n> is required.");
113806
+ return usage(ctx, "--amount <n|max> is required.");
113053
113807
  if (!asset)
113054
113808
  return usage(ctx, "--asset SOL|<mint>|ETH|USDG|<0x token> is required.");
113055
113809
  if (!fromLabel)
@@ -113145,14 +113899,15 @@ async function vaultTransfer(args, ctx) {
113145
113899
  const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], solana.rpc, ctx);
113146
113900
  if (reconciled !== null)
113147
113901
  return reconciled;
113148
- const plan2 = await solana.read(() => planTransfer({
113902
+ const buildPlan = () => solana.read(() => planTransfer({
113149
113903
  from: fromEntry.address,
113150
113904
  to,
113151
113905
  amount,
113152
113906
  asset,
113153
113907
  rpc: solana.rpc
113154
113908
  }));
113155
- const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan2.from, plan2.instructions));
113909
+ let plan2 = await buildPlan();
113910
+ let feeQuote = plan2.solMax?.fee ?? await solana.read(() => quoteTransferFee(solana.rpc, plan2.from, plan2.instructions));
113156
113911
  displayTransferPlan(ctx, plan2, feeQuote);
113157
113912
  await assertTransferAffordable(solana, plan2, feeQuote);
113158
113913
  if (promoted) {
@@ -113160,7 +113915,23 @@ async function vaultTransfer(args, ctx) {
113160
113915
  if (notice2 !== undefined)
113161
113916
  note(notice2);
113162
113917
  }
113163
- await confirmSend(ctx, { amount: plan2.amount, asset: plan2.asset, address: to });
113918
+ for (let attempt = 0;attempt < 3; attempt++) {
113919
+ await confirmSend(ctx, { amount: plan2.amount, asset: plan2.asset, address: to });
113920
+ if (!plan2.solMax)
113921
+ break;
113922
+ const refreshed = await buildPlan();
113923
+ const unchanged = refreshed.amountRaw === plan2.amountRaw && refreshed.solMax?.fee === plan2.solMax.fee;
113924
+ plan2 = refreshed;
113925
+ feeQuote = refreshed.solMax?.fee ?? null;
113926
+ if (unchanged)
113927
+ break;
113928
+ if (attempt === 2) {
113929
+ 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.");
113930
+ }
113931
+ note("SOL max balance or fee changed; review and confirm the refreshed amount.");
113932
+ displayTransferPlan(ctx, plan2, feeQuote);
113933
+ await assertTransferAffordable(solana, plan2, feeQuote);
113934
+ }
113164
113935
  const secret = await decryptKey(vault, fromEntry.id);
113165
113936
  try {
113166
113937
  const result = await signAndBroadcastTransfer({ ctx, solana, secret64: secret, plan: plan2 });
@@ -113203,7 +113974,7 @@ init_human_output();
113203
113974
  init_release();
113204
113975
  import { dirname as dirname11, join as join15 } from "node:path";
113205
113976
  init_key_output();
113206
- var USAGE16 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
113977
+ var USAGE17 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
113207
113978
  async function resolveIdentity(deps, bundlePath, flag) {
113208
113979
  if (flag)
113209
113980
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -113235,19 +114006,19 @@ async function verify(args, ctx) {
113235
114006
  });
113236
114007
  if ("error" in parsed) {
113237
114008
  writeScreenUsageFailure(deps, `${parsed.error}
113238
- ${USAGE16}`, json);
114009
+ ${USAGE17}`, json);
113239
114010
  return 2;
113240
114011
  }
113241
114012
  const file = parsed.positionals[0];
113242
114013
  if (parsed.positionals.length !== 1 || file === undefined) {
113243
114014
  writeScreenUsageFailure(deps, `verify takes exactly one file.
113244
- ${USAGE16}`, json);
114015
+ ${USAGE17}`, json);
113245
114016
  return 2;
113246
114017
  }
113247
114018
  const bundlePath = parsed.values["--bundle"];
113248
114019
  if (!bundlePath) {
113249
114020
  writeScreenUsageFailure(deps, `--bundle is required.
113250
- ${USAGE16}`, json);
114021
+ ${USAGE17}`, json);
113251
114022
  return 2;
113252
114023
  }
113253
114024
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -113261,7 +114032,7 @@ ${USAGE16}`, json);
113261
114032
  }
113262
114033
  if (resolved.kind === "absent") {
113263
114034
  writeScreenUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
113264
- ${USAGE16}`, json);
114035
+ ${USAGE17}`, json);
113265
114036
  return 2;
113266
114037
  }
113267
114038
  const identity4 = resolved.uri;
@@ -113510,7 +114281,7 @@ init_solana_lite();
113510
114281
  init_trading();
113511
114282
  init_swap();
113512
114283
  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]";
114284
+ var USAGE18 = "Usage: candle wallet close-empty [--wallet embedded|<tee address|label>] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
113514
114285
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
113515
114286
  var LISTED_ROWS = 50;
113516
114287
  var TOKEN_IX_CLOSE_ACCOUNT3 = 9;
@@ -113773,7 +114544,7 @@ async function closeTee(ctx, key, wallet3, name2, run2) {
113773
114544
  async function walletsCloseEmpty(args, ctx) {
113774
114545
  const lifted = splitKeep(args);
113775
114546
  if ("error" in lifted) {
113776
- writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE17}`, ctx.json);
114547
+ writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE18}`, ctx.json);
113777
114548
  return 2;
113778
114549
  }
113779
114550
  const parsed = parseArgs(lifted.rest, { valueFlags: ["--wallet", "--client-trade-id"], booleanFlags: ["--yes"] });
@@ -113784,7 +114555,7 @@ async function walletsCloseEmpty(args, ctx) {
113784
114555
  const named = parsed.values["--client-trade-id"];
113785
114556
  const id = named ?? `close-${randomUUID15()}`;
113786
114557
  if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
113787
- writeUsageFailure(ctx.deps, USAGE17, ctx.json);
114558
+ writeUsageFailure(ctx.deps, USAGE18, ctx.json);
113788
114559
  return 2;
113789
114560
  }
113790
114561
  const run2 = { id, named: named !== undefined, keep: lifted.keep, yes: parsed.booleans.has("--yes") };
@@ -114101,14 +114872,27 @@ async function maybeWriteUpdateNotice(deps, opts = {}) {
114101
114872
  return;
114102
114873
  if (opts.command === "update" || opts.command === "doctor")
114103
114874
  return;
114104
- const latest = latestCliVersionFromApi();
114105
- if (!latest || !isPlainVersion(latest) || compareVersions(latest, CLI_VERSION) <= 0)
114875
+ const seen = latestCliVersionFromApi();
114876
+ if (!seen || !isPlainVersion(seen) || compareVersions(seen, CLI_VERSION) <= 0)
114106
114877
  return;
114878
+ const realExec = await deps.realpath(deps.execPath).catch(() => deps.execPath);
114879
+ const method = detectInstall(deps.execPath, realExec);
114880
+ let latest = seen;
114881
+ let command = "npm i -g @candledottv/cli@latest";
114882
+ if (method !== "script") {
114883
+ command = method === "homebrew" ? "brew upgrade candle" : "candle update";
114884
+ const feed = await fetchLatest(deps, releaseBaseUrl(deps.env, updateHomeFor(CLI_VERSION)));
114885
+ if (!feed.ok || !isPlainVersion(feed.manifest.version))
114886
+ return;
114887
+ latest = feed.manifest.version;
114888
+ if (compareVersions(latest, CLI_VERSION) <= 0)
114889
+ return;
114890
+ }
114107
114891
  const config = await deps.readConfig();
114108
114892
  const prior = config.updateNotice;
114109
114893
  if (prior && prior.version === latest && deps.now() - prior.shownAt < DAY_MS2)
114110
114894
  return;
114111
- const line = `Update available: candle ${CLI_VERSION} -> ${latest}. Run: candle update`;
114895
+ const line = `Update available: candle ${CLI_VERSION} -> ${latest}. Run: ${command}`;
114112
114896
  const tty = terminalStyle(deps, "stderr", opts.json).color !== 0;
114113
114897
  deps.stderr.write(tty ? `\x1B[2m${line}\x1B[0m
114114
114898
  ` : `${line}
@@ -114189,7 +114973,7 @@ var COMMANDS = {
114189
114973
  stake: { bare: stake, subcommands: { add: stakeAdd, remove: stakeRemove, finish: stakeFinish, claim: stakeClaim } },
114190
114974
  swap: { bare: swap, subcommands: { status: swapStatus } },
114191
114975
  transfer: { bare: transfer },
114192
- launch: { bare: launch },
114976
+ launch: { bare: launch, subcommands: { status: launchStatus } },
114193
114977
  image: { subcommands: { upload: imageUpload } },
114194
114978
  pnl: { bare: pnl },
114195
114979
  portfolio: { bare: portfolio },