@candledottv/cli 0.13.8 → 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 +1131 -108
  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.8";
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",
@@ -15613,6 +15626,9 @@ ${quote.fee ? `Tier fee: ${safeText(quote.fee.bps)} bps (${safeText(quote.fee.fe
15613
15626
  `);
15614
15627
  if (quote.maxLoss)
15615
15628
  output.write(`Most this wallet can lose: ${safeText(quote.maxLoss)}
15629
+ `);
15630
+ if (quote.feeWallet)
15631
+ output.write(`Fee wallet: ${safeText(quote.feeWallet)}
15616
15632
  `);
15617
15633
  if (quote.legs)
15618
15634
  output.write(`Legs, signed one at a time: ${quote.legs.map(safeText).join(", ")}
@@ -16869,12 +16885,48 @@ function describeSwapEvidence(ctx, details, terminal) {
16869
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.
16870
16886
  `.trim());
16871
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
+ }
16872
16923
  function printTradingResult(ctx, result) {
16873
16924
  if (result.kind === "swap" && result.job && typeof result.job === "object") {
16874
16925
  const job = result.job;
16875
16926
  describeSwapEvidence(ctx, job, job.status === "failed");
16876
16927
  }
16877
- 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)}
16878
16930
  ` : `${JSON.stringify(result, null, 2)}
16879
16931
  `);
16880
16932
  return 0;
@@ -17681,7 +17733,7 @@ async function bridgeSwap(ctx, args) {
17681
17733
  return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
17682
17734
  }
17683
17735
  }
17684
- 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;
17685
17737
  var init_swap = __esm(() => {
17686
17738
  init_zod();
17687
17739
  init_args();
@@ -28869,7 +28921,7 @@ __export(exports_vault_support, {
28869
28921
  askForOwnPassphrase: () => askForOwnPassphrase,
28870
28922
  PASSPHRASE_ONLY_PREFIX: () => PASSPHRASE_ONLY_PREFIX
28871
28923
  });
28872
- import { dirname as dirname6 } from "node:path";
28924
+ import { dirname as dirname6, resolve as resolvePath } from "node:path";
28873
28925
  function refuseEnvPassphrase(ctx) {
28874
28926
  if (ctx.deps.env.CANDLE_KEYSTORE_PASSPHRASE === undefined)
28875
28927
  return true;
@@ -28999,6 +29051,9 @@ function assertVaultHelperIdentities(deps, envelopes) {
28999
29051
  }
29000
29052
  async function unlockInteractively(ctx, path, raw, opts = {}) {
29001
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);
29002
29057
  const { deps } = ctx;
29003
29058
  const file = parseVaultFile(raw);
29004
29059
  assertVaultHelperIdentities(deps, file.envelopes);
@@ -37248,9 +37303,11 @@ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
37248
37303
  return { ok: false, code: 1 };
37249
37304
  }
37250
37305
  if (byPrefix)
37251
- return { ok: true, keyPrefix: byPrefix.keyPrefix };
37252
- if (byLabel.length === 1)
37253
- 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
+ }
37254
37311
  if (byLabel.length > 1) {
37255
37312
  writeLocalFailure(deps, {
37256
37313
  code: `${codePrefix}_KEY_AMBIGUOUS`,
@@ -37262,8 +37319,10 @@ async function resolveTargetKey(ctx, deviceToken, raw, opts = {}) {
37262
37319
  return { ok: true, keyPrefix: raw };
37263
37320
  }
37264
37321
  const matches = keys.filter((key) => key.label === raw);
37265
- if (matches.length === 1)
37266
- 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
+ }
37267
37326
  if (matches.length === 0) {
37268
37327
  const labelled = keys.filter((key) => key.label).map((key) => `${key.keyPrefix} ${labelCell(key.label)}`);
37269
37328
  writeLocalFailure(deps, {
@@ -37578,6 +37637,11 @@ var init_tee_rebind = __esm(() => {
37578
37637
  function effectiveEmbeddedWallet(value) {
37579
37638
  return value === "denied" ? "denied" : "allowed";
37580
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
+ }
37581
37645
  function keyName(keyPrefix, label) {
37582
37646
  const cleaned = labelCell(label ?? undefined);
37583
37647
  return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
@@ -38091,6 +38155,10 @@ async function keysCreate(args, ctx) {
38091
38155
  } catch (error) {
38092
38156
  storeError = error instanceof Error ? error.message : String(error);
38093
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
+ }
38094
38162
  if (json) {
38095
38163
  deps.stdout.write(`${JSON.stringify({ ...body, stored, ...storeError ? { storeError } : {} })}
38096
38164
  `);
@@ -38396,7 +38464,7 @@ var init_token_clusters = __esm(() => {
38396
38464
  });
38397
38465
 
38398
38466
  // ../mcp/src/version.ts
38399
- var SERVER_VERSION = "0.10.27";
38467
+ var SERVER_VERSION = "0.10.28";
38400
38468
 
38401
38469
  // ../mcp/src/update-notice.ts
38402
38470
  function newer(a, b) {
@@ -39651,6 +39719,77 @@ var init_launch_definition = __esm(() => {
39651
39719
  }
39652
39720
  }
39653
39721
  },
39722
+ {
39723
+ key: "feeWallet",
39724
+ type: "string",
39725
+ help: "Solana only: the wallet this launch's curve and pool fees earn to, an on-curve wallet or a Squads v4 vault; defaults to the account wallet.",
39726
+ chains: [
39727
+ "solana"
39728
+ ],
39729
+ surfaces: {
39730
+ cli: {
39731
+ flag: "--fee-wallet",
39732
+ value: "<address>"
39733
+ },
39734
+ mcp: {
39735
+ key: "feeWallet"
39736
+ },
39737
+ sdk: {
39738
+ key: "feeWallet"
39739
+ }
39740
+ }
39741
+ },
39742
+ {
39743
+ key: "feeWalletMultisig.address",
39744
+ type: "string",
39745
+ help: "The Squads v4 multisig behind an off-curve --fee-wallet vault; takes --fee-wallet-multisig-vault-index.",
39746
+ chains: [
39747
+ "solana"
39748
+ ],
39749
+ requires: [
39750
+ "feeWallet"
39751
+ ],
39752
+ surfaces: {
39753
+ cli: {
39754
+ flag: "--fee-wallet-multisig",
39755
+ value: "<address>"
39756
+ },
39757
+ mcp: {
39758
+ key: "feeWalletMultisig.address"
39759
+ },
39760
+ sdk: {
39761
+ key: "feeWalletMultisig.address"
39762
+ }
39763
+ }
39764
+ },
39765
+ {
39766
+ key: "feeWalletMultisig.vaultIndex",
39767
+ type: "integer",
39768
+ help: "The Squads vault index, 0 to 255, that with the multisig derives an off-curve --fee-wallet.",
39769
+ chains: [
39770
+ "solana"
39771
+ ],
39772
+ range: {
39773
+ min: "0",
39774
+ max: "255",
39775
+ unit: "vault index"
39776
+ },
39777
+ requires: [
39778
+ "feeWalletMultisig.address"
39779
+ ],
39780
+ surfaces: {
39781
+ cli: {
39782
+ flag: "--fee-wallet-multisig-vault-index",
39783
+ value: "<n>"
39784
+ },
39785
+ mcp: {
39786
+ key: "feeWalletMultisig.vaultIndex"
39787
+ },
39788
+ sdk: {
39789
+ key: "feeWalletMultisig.vaultIndex"
39790
+ }
39791
+ }
39792
+ },
39654
39793
  {
39655
39794
  key: "buy",
39656
39795
  type: "decimal",
@@ -39850,7 +39989,7 @@ var M2_TOP_KEYS, GROUPED_TOP_KEYS, LAUNCH_SHARED_SENTENCES, SOCIAL_KEYS, SOCIALS
39850
39989
  var init_launch_schema = __esm(() => {
39851
39990
  init_zod();
39852
39991
  init_launch_definition();
39853
- M2_TOP_KEYS = new Set(["wallet", "buy"]);
39992
+ M2_TOP_KEYS = new Set(["wallet", "buy", "feeWallet", "feeWalletMultisig"]);
39854
39993
  GROUPED_TOP_KEYS = new Set(["socials", "curve", "poolFee"]);
39855
39994
  LAUNCH_SHARED_SENTENCES = mcpLaunchFields().map((field) => field.help).join(" ");
39856
39995
  SOCIAL_KEYS = mcpLaunchFields().flatMap((field) => {
@@ -45221,7 +45360,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
45221
45360
  }, async (args) => callAndRelay("candle_get_fees", args, getConfig(), doFetch));
45222
45361
  register("candle_claim_fees", {
45223
45362
  title: "Claim a token's creator fees",
45224
- 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.",
45225
45364
  inputSchema: claimFeesShape
45226
45365
  }, async (args) => callAndRelay("candle_claim_fees", args, getConfig(), doFetch));
45227
45366
  register("candle_get_wallets", {
@@ -75958,6 +76097,77 @@ var LAUNCH_DEFINITION = {
75958
76097
  }
75959
76098
  }
75960
76099
  },
76100
+ {
76101
+ key: "feeWallet",
76102
+ type: "string",
76103
+ help: "Solana only: the wallet this launch's curve and pool fees earn to, an on-curve wallet or a Squads v4 vault; defaults to the account wallet.",
76104
+ chains: [
76105
+ "solana"
76106
+ ],
76107
+ surfaces: {
76108
+ cli: {
76109
+ flag: "--fee-wallet",
76110
+ value: "<address>"
76111
+ },
76112
+ mcp: {
76113
+ key: "feeWallet"
76114
+ },
76115
+ sdk: {
76116
+ key: "feeWallet"
76117
+ }
76118
+ }
76119
+ },
76120
+ {
76121
+ key: "feeWalletMultisig.address",
76122
+ type: "string",
76123
+ help: "The Squads v4 multisig behind an off-curve --fee-wallet vault; takes --fee-wallet-multisig-vault-index.",
76124
+ chains: [
76125
+ "solana"
76126
+ ],
76127
+ requires: [
76128
+ "feeWallet"
76129
+ ],
76130
+ surfaces: {
76131
+ cli: {
76132
+ flag: "--fee-wallet-multisig",
76133
+ value: "<address>"
76134
+ },
76135
+ mcp: {
76136
+ key: "feeWalletMultisig.address"
76137
+ },
76138
+ sdk: {
76139
+ key: "feeWalletMultisig.address"
76140
+ }
76141
+ }
76142
+ },
76143
+ {
76144
+ key: "feeWalletMultisig.vaultIndex",
76145
+ type: "integer",
76146
+ help: "The Squads vault index, 0 to 255, that with the multisig derives an off-curve --fee-wallet.",
76147
+ chains: [
76148
+ "solana"
76149
+ ],
76150
+ range: {
76151
+ min: "0",
76152
+ max: "255",
76153
+ unit: "vault index"
76154
+ },
76155
+ requires: [
76156
+ "feeWalletMultisig.address"
76157
+ ],
76158
+ surfaces: {
76159
+ cli: {
76160
+ flag: "--fee-wallet-multisig-vault-index",
76161
+ value: "<n>"
76162
+ },
76163
+ mcp: {
76164
+ key: "feeWalletMultisig.vaultIndex"
76165
+ },
76166
+ sdk: {
76167
+ key: "feeWalletMultisig.vaultIndex"
76168
+ }
76169
+ }
76170
+ },
75961
76171
  {
75962
76172
  key: "buy",
75963
76173
  type: "decimal",
@@ -76014,7 +76224,7 @@ var LAUNCH_USAGE = `candle launch ${launchFlagFields.map((field) => {
76014
76224
  if (field.key === "imageUrl")
76015
76225
  return `--image <path>|${invocation}`;
76016
76226
  return "required" in field && field.required ? invocation : `[${invocation}]`;
76017
- }).join(" ")} [--check] [--yes] [--rpc-url <url>] [--json]`;
76227
+ }).join(" ")} [--check] [--yes] [--require-fee-wallet] [--rpc-url <url>] [--json]`;
76018
76228
  function launchFieldProblem(flags) {
76019
76229
  for (const field of launchFlagFields) {
76020
76230
  const value = flags[field.cli.flag];
@@ -76199,7 +76409,7 @@ var HELP = {
76199
76409
  group: "Trade",
76200
76410
  intro: "Quote, swap and bridge on Solana or Hood; read an operation",
76201
76411
  summary: "Quote, confirm and swap on Solana or Hood, bridge between them; read an operation by id",
76202
- 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.",
76203
76413
  usage: ["candle swap <from> <to> [flags]", "candle swap status <id>"],
76204
76414
  rows: [
76205
76415
  {
@@ -76512,19 +76722,19 @@ var HELP = {
76512
76722
  group: "Trade",
76513
76723
  intro: "Move funds out of a TEE wallet to your wallets or its vault",
76514
76724
  summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
76515
- 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.",
76516
76726
  usage: [
76517
- "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]"
76518
76728
  ],
76519
76729
  rows: [],
76520
76730
  flags: [
76521
76731
  {
76522
- invocation: "--to <address|wallet name|vault>",
76523
- 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"
76524
76734
  },
76525
76735
  {
76526
76736
  invocation: "--asset <SOL|USDC|CNDL|ETH|USDG>",
76527
- description: "A base asset (required for a linked-wallet destination)"
76737
+ description: "A base asset (required for a linked-wallet or approved-address destination)"
76528
76738
  },
76529
76739
  { invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
76530
76740
  { invocation: "--token <0x...>", description: "Any Hood ERC-20, instead of --asset (vault destinations only)" },
@@ -76546,9 +76756,14 @@ var HELP = {
76546
76756
  launch: {
76547
76757
  group: "Trade",
76548
76758
  summary: "Create a token on Solana or Hood, with an optional first buy",
76549
- 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. --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.",
76550
- usage: [LAUNCH_USAGE],
76551
- 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
+ ],
76552
76767
  flags: [
76553
76768
  ...launchFlagRows,
76554
76769
  {
@@ -76560,6 +76775,10 @@ var HELP = {
76560
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."
76561
76776
  },
76562
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
+ },
76563
76782
  {
76564
76783
  invocation: "--rpc-url <url>",
76565
76784
  description: "The Solana RPC for a TEE launch with no first buy, or the Hood RPC for receipt reads."
@@ -76573,7 +76792,8 @@ var HELP = {
76573
76792
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
76574
76793
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1",
76575
76794
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet embedded --buy 0.1 --dry-run",
76576
- "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"
76577
76797
  ],
76578
76798
  env: ENV_API
76579
76799
  },
@@ -77218,7 +77438,7 @@ var HELP = {
77218
77438
  },
77219
77439
  {
77220
77440
  invocation: "transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]",
77221
- 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"
77222
77442
  },
77223
77443
  {
77224
77444
  invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
@@ -77233,8 +77453,8 @@ var HELP = {
77233
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"
77234
77454
  },
77235
77455
  {
77236
- invocation: "demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>] [--token <0x...>] [--from-block <n>]",
77237
- 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"
77238
77458
  },
77239
77459
  {
77240
77460
  invocation: "export-key <label> --to <new-file>",
@@ -77253,7 +77473,15 @@ var HELP = {
77253
77473
  },
77254
77474
  {
77255
77475
  invocation: "--all",
77256
- 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"
77257
77485
  },
77258
77486
  {
77259
77487
  invocation: "--balances",
@@ -77291,6 +77519,7 @@ var HELP = {
77291
77519
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
77292
77520
  "candle vault promote-batch --pairs-from ./promote-plan.csv",
77293
77521
  "candle vault promote-batch --pairs-from ./promote-plan.csv --to-key tr-01",
77522
+ "candle vault demote --all --empty",
77294
77523
  "candle vault enroll security-key --label yubikey-a",
77295
77524
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
77296
77525
  "candle vault backup --to icloud",
@@ -77351,8 +77580,12 @@ var HELP = {
77351
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"
77352
77581
  },
77353
77582
  {
77354
- invocation: "signer new --key <prefix|label> [--out <pem>] [--force]",
77355
- 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"
77356
77589
  },
77357
77590
  {
77358
77591
  invocation: " signer remove --key <prefix|label>",
@@ -82465,14 +82698,69 @@ async function tokenAccountFrozen(rpc, address) {
82465
82698
  return { exists: false, frozen: false };
82466
82699
  return { exists: true, frozen: account.data[TOKEN_ACCOUNT_STATE_OFFSET2] === TOKEN_ACCOUNT_STATE_FROZEN2 };
82467
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
+ }
82468
82753
  async function planTransfer(input) {
82469
82754
  const asset = input.asset.toUpperCase();
82470
82755
  const fromKey = decodePubkey(input.from);
82471
82756
  const toKey = decodePubkey(input.to);
82757
+ const max = input.amount.toLowerCase() === "max";
82472
82758
  if (asset === "SOL") {
82759
+ if (max)
82760
+ return planSolMax(input.rpc, input.from, input.to);
82473
82761
  const raw2 = decimalToRaw4(input.amount, 9);
82474
82762
  if (raw2 === null || raw2 === 0n) {
82475
- 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.");
82476
82764
  }
82477
82765
  return {
82478
82766
  asset: "SOL",
@@ -82503,13 +82791,16 @@ async function planTransfer(input) {
82503
82791
  throw new VaultError("VAULT_UNREADABLE", `Could not read mint ${mintAddress}: ${asMessage(error)}`);
82504
82792
  }
82505
82793
  const decimals = profile.decimals;
82506
- 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
+ }
82507
82799
  if (raw === null || raw === 0n) {
82508
- 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.`);
82509
82801
  }
82510
82802
  const mint = decodePubkey(mintAddress);
82511
82803
  const tokenProgram = decodePubkey(profile.tokenProgram);
82512
- const source = ataFor(profile, fromKey);
82513
82804
  const destination = ataFor(profile, toKey);
82514
82805
  const instructions = [];
82515
82806
  const accountCreationLines = [];
@@ -82546,7 +82837,7 @@ async function planTransfer(input) {
82546
82837
  }));
82547
82838
  return {
82548
82839
  asset: asset === "USDC" ? "USDC" : mintAddress,
82549
- amount: input.amount,
82840
+ amount: max ? rawToDecimal2(raw, decimals) : input.amount,
82550
82841
  amountRaw: raw,
82551
82842
  decimals,
82552
82843
  mint: mintAddress,
@@ -82563,7 +82854,7 @@ async function planTransfer(input) {
82563
82854
  displayLines: [
82564
82855
  `fee payer ${input.from}`,
82565
82856
  `destination ${input.to}`,
82566
- `amount ${input.amount} (${raw} raw, ${decimals} dp)`,
82857
+ `amount ${max ? rawToDecimal2(raw, decimals) : input.amount} (${raw} raw, ${decimals} dp)${max ? " (max)" : ""}`,
82567
82858
  `mint ${mintAddress}`,
82568
82859
  `program ${profile.tokenProgram}${profile.token2022 ? " (Token-2022)" : ""}`,
82569
82860
  ...accountCreationLines,
@@ -82643,7 +82934,7 @@ async function signAndBroadcastTransfer(input) {
82643
82934
  if (encodePubkey(feePayer) !== input.plan.from) {
82644
82935
  throw new VaultError("VAULT_VERIFY_FAILED", "The decrypted key does not match the planned fee payer.");
82645
82936
  }
82646
- const blockhash = await input.solana.read(() => rpc.getLatestBlockhash());
82937
+ const blockhash = input.plan.solMax?.blockhash ?? await input.solana.read(() => rpc.getLatestBlockhash());
82647
82938
  const message = compileLegacyMessage({
82648
82939
  feePayer,
82649
82940
  recentBlockhash: blockhash,
@@ -84646,7 +84937,7 @@ function rebindJson(input, chain2) {
84646
84937
  init_key_output();
84647
84938
  init_keys();
84648
84939
  init_tee_rebind();
84649
- 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]";
84650
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]";
84651
84942
  var SIGNER_POLL_MS = 5000;
84652
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.";
@@ -84680,6 +84971,8 @@ async function teeSigner(args, ctx) {
84680
84971
  return teeSignerNew(rest, ctx);
84681
84972
  if (sub === "remove")
84682
84973
  return teeSignerRemove(rest, ctx);
84974
+ if (sub === "status")
84975
+ return teeSignerStatus(rest, ctx);
84683
84976
  writeScreenUsageFailure(ctx.deps, sub === undefined ? USAGE_TEE_SIGNER : `Unknown subcommand: ${sub}. ${USAGE_TEE_SIGNER}`, ctx.json);
84684
84977
  return 2;
84685
84978
  }
@@ -84692,7 +84985,11 @@ function generatePair() {
84692
84985
  }
84693
84986
  async function teeSignerNew(args, ctx) {
84694
84987
  const { deps, json } = ctx;
84695
- 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
+ });
84696
84993
  if ("error" in parsed) {
84697
84994
  writeScreenUsageFailure(deps, `${parsed.error}. ${USAGE_TEE_SIGNER}`, json);
84698
84995
  return 2;
@@ -84708,6 +85005,7 @@ async function teeSignerNew(args, ctx) {
84708
85005
  }
84709
85006
  const out = parsed.values["--out"];
84710
85007
  const force = parsed.booleans.has("--force");
85008
+ const noWait = parsed.booleans.has("--no-wait");
84711
85009
  await printIdentity(ctx);
84712
85010
  const apiKey = await resolveApiKey(deps, ctx.profile);
84713
85011
  if (!apiKey) {
@@ -84746,7 +85044,7 @@ async function teeSignerNew(args, ctx) {
84746
85044
  entry = undefined;
84747
85045
  }
84748
85046
  if (entry !== undefined && active !== null && active.spkiSha256 === entry.spkiSha256) {
84749
- const recorded = { ...entry, signerQuorumId: active.signerQuorumId };
85047
+ const recorded = { ...entry, signerQuorumId: active.signerQuorumId, pendingRequest: undefined };
84750
85048
  await saveKeySignerEntry(deps, recorded);
84751
85049
  await writeOut(ctx, out, recorded);
84752
85050
  return reportApproved(ctx, view, recorded, { resumed: true, requested: false });
@@ -84795,12 +85093,17 @@ async function teeSignerNew(args, ctx) {
84795
85093
  resumed = false;
84796
85094
  }
84797
85095
  await writeOut(ctx, out, entry);
84798
- 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);
84799
85102
  if (!requested.ok) {
84800
85103
  if (requested.code === "KEY_SIGNER_ALREADY_ACTIVE") {
84801
85104
  const quorum = errorDetails(requested).keySigner?.signerQuorumId;
84802
85105
  if (typeof quorum === "string") {
84803
- const recorded = { ...entry, signerQuorumId: quorum };
85106
+ const recorded = { ...entry, signerQuorumId: quorum, pendingRequest: undefined };
84804
85107
  await saveKeySignerEntry(deps, recorded);
84805
85108
  const reread = await readSigner(ctx, "self", { apiKey });
84806
85109
  return reportApproved(ctx, reread.ok ? reread.body : view, recorded, {
@@ -84837,6 +85140,27 @@ async function teeSignerNew(args, ctx) {
84837
85140
  }, json);
84838
85141
  return 1;
84839
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
+ }
84840
85164
  if (json) {
84841
85165
  writeFields(deps, "stderr", [
84842
85166
  ["Key", keyPrefix],
@@ -84847,7 +85171,7 @@ async function teeSignerNew(args, ctx) {
84847
85171
  ["Approve on owner machine", `candle keys signer approve ${body.userCode} --key ${keyPrefix}`],
84848
85172
  ["Expires", formatTime(body.expiresAt)]
84849
85173
  ], "Key signer request");
84850
- 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..."}`);
84851
85175
  } else {
84852
85176
  writeFields(deps, "stderr", [
84853
85177
  ["Key", keyPrefix],
@@ -84859,9 +85183,11 @@ async function teeSignerNew(args, ctx) {
84859
85183
  ], "Key signer request");
84860
85184
  notice(deps, "stderr", "warning", "The account owner approves it, typing this full fingerprint.");
84861
85185
  notice(deps, "stderr", "note", `Or, on the owner's machine:
84862
- candle keys signer approve ${body.userCode} --key ${keyPrefix}
84863
- Waiting for approval...`);
85186
+ candle keys signer approve ${body.userCode} --key ${keyPrefix}${noWait ? "" : `
85187
+ Waiting for approval...`}`);
84864
85188
  }
85189
+ if (noWait)
85190
+ return 0;
84865
85191
  while (deps.now() < body.expiresAt) {
84866
85192
  await deps.sleep(SIGNER_POLL_MS);
84867
85193
  const read = await readSigner(ctx, "self", { apiKey });
@@ -84870,9 +85196,9 @@ Waiting for approval...`);
84870
85196
  view = read.body;
84871
85197
  const now = activeSigner(view);
84872
85198
  if (now !== null && now.spkiSha256 === entry.spkiSha256) {
84873
- const recorded = { ...entry, signerQuorumId: now.signerQuorumId };
85199
+ const recorded = { ...entry, signerQuorumId: now.signerQuorumId, pendingRequest: undefined };
84874
85200
  await saveKeySignerEntry(deps, recorded);
84875
- return reportApproved(ctx, view, recorded, { resumed, requested: true, replaces });
85201
+ return reportApproved(ctx, view, recorded, { resumed, requested: cached === undefined, replaces });
84876
85202
  }
84877
85203
  if (view.pending === null || view.pending.spkiSha256 !== entry.spkiSha256) {
84878
85204
  writeScreenLocalFailure(deps, {
@@ -84892,6 +85218,58 @@ Waiting for approval...`);
84892
85218
  }, json);
84893
85219
  return 1;
84894
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
+ }
84895
85273
  async function writeOut(ctx, out, entry) {
84896
85274
  if (out === undefined)
84897
85275
  return;
@@ -89396,6 +89774,7 @@ var REASON_WORDS = {
89396
89774
  delegation_off: "the embedded wallet is not delegated for agent access",
89397
89775
  embedded_wallet_not_permitted: "this key may not use the embedded wallet",
89398
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",
89399
89778
  insufficient_gas: "the recipient wallet cannot cover the network fee"
89400
89779
  };
89401
89780
  function reasonText(reason, missing) {
@@ -89467,7 +89846,53 @@ async function feesClaim(args, ctx) {
89467
89846
  const dryRun = parsed.booleans.has("--dry-run");
89468
89847
  try {
89469
89848
  const key = await tradingKey(ctx);
89470
- 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
+ }
89471
89896
  if (ctx.json)
89472
89897
  return printTradingResult(ctx, body);
89473
89898
  const style = terminalStyle(ctx.deps, "stdout");
@@ -89544,6 +89969,7 @@ init_render();
89544
89969
  init_promote_support();
89545
89970
  init_key_output();
89546
89971
  init_keys();
89972
+ init_keys_embedded_wallet();
89547
89973
  init_tee_rebind();
89548
89974
  var KEYS_PATH4 = "/api/v1/agent/keys";
89549
89975
  var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|write|withdraw> [--yes] [--json]
@@ -89707,6 +90133,7 @@ async function keysAccess(args, ctx) {
89707
90133
  let path;
89708
90134
  let auth;
89709
90135
  let credentials;
90136
+ let embeddedWallet;
89710
90137
  if (self) {
89711
90138
  const apiKey = await resolveApiKey(deps, ctx.profile);
89712
90139
  if (!apiKey) {
@@ -89728,6 +90155,7 @@ async function keysAccess(args, ctx) {
89728
90155
  });
89729
90156
  if (!resolved.ok)
89730
90157
  return resolved.code;
90158
+ embeddedWallet = resolved.embeddedWallet;
89731
90159
  path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
89732
90160
  auth = "device";
89733
90161
  credentials = { deviceToken };
@@ -89766,6 +90194,9 @@ async function keysAccess(args, ctx) {
89766
90194
  const account = fields2.account ? shortAddress2(fields2.account) : json ? "unknown" : symbols(terminalStyle(deps, "stderr")).missing;
89767
90195
  deps.stderr.write(`${accessScreen(shown, { account: fields2.username ? `${fields2.username} (${account})` : account, apiUrl }, json ? undefined : terminalStyle(deps, "stderr"))}
89768
90196
  `);
90197
+ if (shown.to.access === "readwritetransfer" && embeddedWallet === "denied") {
90198
+ notice(deps, "stderr", "warning", withdrawNeedsEmbeddedWalletLine(shown.keyPrefix));
90199
+ }
89769
90200
  if (widen) {
89770
90201
  const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
89771
90202
  if (typed.trim() !== shown.keyPrefix) {
@@ -90543,9 +90974,82 @@ init_zod();
90543
90974
  init_args();
90544
90975
  init_client();
90545
90976
  init_evm_lite();
90977
+ import { randomUUID as randomUUID6 } from "node:crypto";
90978
+
90979
+ // src/fee-wallet.ts
90980
+ init_solana_lite();
90981
+ var FEE_WALLET_FLAGS = ["--fee-wallet", "--fee-wallet-multisig", "--fee-wallet-multisig-vault-index"];
90982
+ var SQUADS_V4_PROGRAM_ID = decodePubkey("SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf");
90983
+ var UNSUPPORTED = "Fee wallet must be an on-curve Solana wallet or a verified Squads v4 vault when multisig fees are enabled";
90984
+ function feeWalletGiven(flags) {
90985
+ return FEE_WALLET_FLAGS.some((flag) => flags[flag] !== undefined);
90986
+ }
90987
+ function feeWalletSelection(flags) {
90988
+ if (!feeWalletGiven(flags))
90989
+ return;
90990
+ const raw = flags["--fee-wallet"];
90991
+ if (raw === undefined)
90992
+ throw new Error("--fee-wallet-multisig needs --fee-wallet <address>, the vault fees earn to");
90993
+ const bytes = decodeAddress(raw, "--fee-wallet");
90994
+ const feeWallet = encodePubkey(bytes);
90995
+ if (isOnCurve(bytes)) {
90996
+ if (flags["--fee-wallet-multisig"] !== undefined || flags["--fee-wallet-multisig-vault-index"] !== undefined)
90997
+ throw new Error(`--fee-wallet is an on-curve wallet and takes no --fee-wallet-multisig. ${UNSUPPORTED}`);
90998
+ return { feeWallet };
90999
+ }
91000
+ const multisig = flags["--fee-wallet-multisig"];
91001
+ const index = flags["--fee-wallet-multisig-vault-index"];
91002
+ if (multisig === undefined || index === undefined)
91003
+ throw new Error(`--fee-wallet is off-curve, a Squads v4 vault, and names its multisig with --fee-wallet-multisig <address> and --fee-wallet-multisig-vault-index <n>. ${UNSUPPORTED}`);
91004
+ if (!/^\d+$/.test(index) || Number(index) < 0 || Number(index) > 255)
91005
+ throw new Error("--fee-wallet-multisig-vault-index takes a whole number from 0 to 255");
91006
+ const multisigBytes = decodeAddress(multisig, "--fee-wallet-multisig");
91007
+ const vault = findProgramAddress([utf8("multisig"), multisigBytes, utf8("vault"), Uint8Array.of(Number(index))], SQUADS_V4_PROGRAM_ID).address;
91008
+ if (encodePubkey(vault) !== feeWallet)
91009
+ throw new Error(UNSUPPORTED);
91010
+ return { feeWallet, feeWalletMultisig: { address: encodePubkey(multisigBytes), vaultIndex: Number(index) } };
91011
+ }
91012
+ function feeWalletRequestFields(flags) {
91013
+ const selection = feeWalletSelection(flags);
91014
+ if (!selection)
91015
+ return {};
91016
+ return {
91017
+ feeWallet: selection.feeWallet,
91018
+ ...selection.feeWalletMultisig ? { feeWalletMultisig: selection.feeWalletMultisig } : {}
91019
+ };
91020
+ }
91021
+ function utf8(text4) {
91022
+ return new TextEncoder().encode(text4);
91023
+ }
91024
+ function decodeAddress(value, flag) {
91025
+ try {
91026
+ return decodePubkey(value);
91027
+ } catch {
91028
+ throw new Error(`${flag} takes a Solana address.`);
91029
+ }
91030
+ }
91031
+ function feeWalletLines(selection, echo) {
91032
+ if (selection)
91033
+ return [
91034
+ `Fee wallet: ${selection.feeWallet} (earns this launch's fees; set by --fee-wallet)`,
91035
+ ...selection.feeWalletMultisig ? [
91036
+ `Fee wallet multisig: ${selection.feeWalletMultisig.address}, vault ${selection.feeWalletMultisig.vaultIndex} (the Squads v4 multisig behind the fee wallet)`
91037
+ ] : []
91038
+ ];
91039
+ return [
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)."
91041
+ ];
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.";
91044
+ function feeWalletQuoteLine(selection) {
91045
+ if (!selection)
91046
+ return "defaults to the account wallet";
91047
+ return `${selection.feeWallet}${selection.feeWalletMultisig ? ` (Squads vault of ${selection.feeWalletMultisig.address}, vault ${selection.feeWalletMultisig.vaultIndex})` : ""}`;
91048
+ }
91049
+
91050
+ // src/commands/launch.ts
90546
91051
  init_hood_legs();
90547
91052
  init_human_output();
90548
- import { randomUUID as randomUUID6 } from "node:crypto";
90549
91053
 
90550
91054
  // src/launch-bundle.ts
90551
91055
  init_solana_alt();
@@ -90788,7 +91292,7 @@ function embeddedWorks(answer2, requestedChain) {
90788
91292
  function describeRow(row) {
90789
91293
  return `${row.label ? `${row.label} ` : ""}(${row.id}, ${row.address})`;
90790
91294
  }
90791
- async function launchCheck(ctx, key, walletFlag, requestedChain) {
91295
+ async function launchCheck(ctx, key, walletFlag, requestedChain, feeWallet = undefined) {
90792
91296
  const answer2 = checkSchema.parse(await request(ctx, key, "/api/v1/launch/preflight"));
90793
91297
  const embeddedOnly = walletFlag !== undefined && EMBEDDED_NAMES.has(walletFlag);
90794
91298
  const named = walletFlag === undefined || embeddedOnly ? [] : answer2.wallets.filter((row) => row.id === walletFlag || row.address === walletFlag || row.label === walletFlag);
@@ -90798,6 +91302,8 @@ async function launchCheck(ctx, key, walletFlag, requestedChain) {
90798
91302
  const lines = [`Launch paths for key ${answer2.key.keyPrefix} (tier ${answer2.key.tier}):`];
90799
91303
  if (requestedChain)
90800
91304
  lines.push(`Requested launch chain: ${chainName(requestedChain)}.`);
91305
+ if (!requestedChain || requestedChain === "solana")
91306
+ lines.push(...feeWalletLines(feeWallet));
90801
91307
  if (walletFlag === undefined || embeddedOnly)
90802
91308
  lines.push(...embeddedLines(answer2, requestedChain));
90803
91309
  for (const row of shownWallets)
@@ -90811,6 +91317,7 @@ async function launchCheck(ctx, key, walletFlag, requestedChain) {
90811
91317
  ...requestedChain ? { requestedChain } : {},
90812
91318
  key: answer2.key,
90813
91319
  switches: answer2.switches,
91320
+ ...feeWallet ? { feeWallet: { ...feeWallet, set: true } } : { feeWallet: { default: "account wallet", set: false } },
90814
91321
  ...answer2.embedded ? { embedded: answer2.embedded } : {},
90815
91322
  wallets: shownWallets.map((row) => ({
90816
91323
  ...row,
@@ -90889,7 +91396,8 @@ var LAUNCH_ARG_SPEC = {
90889
91396
  booleanFlags: [
90890
91397
  ...launchFlagFields.filter((field) => field.type === "boolean").map((field) => field.cli.flag),
90891
91398
  "--check",
90892
- "--yes"
91399
+ "--yes",
91400
+ "--require-fee-wallet"
90893
91401
  ],
90894
91402
  pathFlags: ["--image", "--banner"]
90895
91403
  };
@@ -91024,10 +91532,25 @@ async function launch(args, ctx) {
91024
91532
  writeUsageFailure(ctx.deps, `${error.message}. ${USAGE9}`, ctx.json);
91025
91533
  return 2;
91026
91534
  }
91535
+ let feeWallet;
91536
+ try {
91537
+ feeWallet = feeWalletSelection(flags);
91538
+ } catch (error) {
91539
+ writeUsageFailure(ctx.deps, `${error.message}. ${USAGE9}`, ctx.json);
91540
+ return 2;
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
+ }
91027
91546
  try {
91028
91547
  if (flags["--buy"]?.trim() && !/^0+(\.0+)?$/.test(flags["--buy"]))
91029
91548
  launchBuyRaw(flags["--buy"], 18);
91030
91549
  const hinted = flagChain(flags);
91550
+ if (hinted === "hood" && feeWalletGiven(flags)) {
91551
+ writeUsageFailure(ctx.deps, `--fee-wallet is Solana only; a Hood launch cannot take it. ${USAGE9}`, ctx.json);
91552
+ return 2;
91553
+ }
91031
91554
  if (!check && hinted === "hood" && flags["--dex-version"] === undefined) {
91032
91555
  writeUsageFailure(ctx.deps, `A Hood launch needs --dex-version v3|v4. ${USAGE9}`, ctx.json);
91033
91556
  return 2;
@@ -91046,7 +91569,7 @@ async function launch(args, ctx) {
91046
91569
  }
91047
91570
  const key = await tradingKey(ctx);
91048
91571
  if (check)
91049
- return await launchCheck(ctx, key, flags["--wallet"], hinted);
91572
+ return await launchCheck(ctx, key, flags["--wallet"], hinted, feeWallet);
91050
91573
  const prior = dryRun ? null : await lookupOperation(ctx, key, id, "launch");
91051
91574
  let retry;
91052
91575
  if (prior) {
@@ -91087,9 +91610,11 @@ async function launch(args, ctx) {
91087
91610
  const chain2 = wallet3.chain === "evm" ? "hood" : "solana";
91088
91611
  if (chain2 === "hood" && flags["--dex-version"] === undefined)
91089
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);
91090
91615
  const buy = launchBuy(flags, chain2);
91091
- if (chain2 === "hood" && Object.keys(poolRequestFields(flags)).length)
91092
- throw new TradingUsage("Pool fee and pool shape choices are Solana only; a Hood launch cannot take them.");
91616
+ if (chain2 === "hood" && (Object.keys(poolRequestFields(flags)).length || feeWalletGiven(flags)))
91617
+ throw new TradingUsage("Pool fee and pool shape choices are Solana only, and so is --fee-wallet; a Hood launch cannot take them.");
91093
91618
  const atomic = chain2 === "solana" && buy !== undefined;
91094
91619
  if (atomic)
91095
91620
  firstBuyCeiling(ctx, flags, buy);
@@ -91139,6 +91664,7 @@ async function launch(args, ctx) {
91139
91664
  fee: built.fee ?? { bps: 0, feeRaw: "0" },
91140
91665
  minimumReceived: buy ? "not quoted (the first buy on the new curve)" : "0 tokens (no first buy)",
91141
91666
  maxDebitLamports: built.maxDebitLamports,
91667
+ feeWallet: feeWalletQuoteLine(feeWallet),
91142
91668
  tokenRisks: []
91143
91669
  };
91144
91670
  notice(ctx.deps, "stderr", "note", buyNotice(buy, "solana").trim());
@@ -91239,7 +91765,8 @@ function solanaSelfBody(args) {
91239
91765
  chain: "solana",
91240
91766
  buyAmount: args.buy ? args.buy.raw : 0,
91241
91767
  linkedWalletId: args.wallet.id,
91242
- ...quoteAsset ? { quoteAsset } : {}
91768
+ ...quoteAsset ? { quoteAsset } : {},
91769
+ ...feeWalletRequestFields(args.flags)
91243
91770
  };
91244
91771
  }
91245
91772
  function hoodBody(args) {
@@ -91266,14 +91793,15 @@ var previewSchema = exports_external.object({
91266
91793
  migration: exports_external.object({ venue: exports_external.string(), poolShape: exports_external.string(), openingPriceStepPct: exports_external.number() }).passthrough().optional()
91267
91794
  }).passthrough().optional(),
91268
91795
  devBuyMax: exports_external.string().nullable().optional(),
91796
+ feeWallet: exports_external.string().optional(),
91269
91797
  fills: exports_external.array(exports_external.object({ index: exports_external.number(), amountRaw: exports_external.string(), supplyPct: exports_external.number() }).passthrough()).optional()
91270
91798
  }).passthrough();
91271
91799
  var percent = (value) => `${Number(value.toFixed(4))}%`;
91272
- function previewLines(answer2, chain2) {
91800
+ function previewLines(answer2, chain2, feeWallet = undefined) {
91273
91801
  const parsed = previewSchema.safeParse(answer2);
91274
91802
  if (!parsed.success)
91275
91803
  return "";
91276
- const { resolved, curveTerms: terms, devBuyMax, fills } = parsed.data;
91804
+ const { resolved, curveTerms: terms, devBuyMax, feeWallet: echo, fills } = parsed.data;
91277
91805
  const quote = (resolved?.quoteAsset ?? LAUNCH_DEFINITION.defaultQuote[chain2]).toUpperCase();
91278
91806
  const decimals = (chain2 === "hood" ? HOOD_BASES[quote] : BASES[quote])?.decimals;
91279
91807
  const whole = (raw) => decimals === undefined ? `${raw} raw` : `${shortAmount(raw, decimals)} ${quote}`;
@@ -91291,6 +91819,8 @@ function previewLines(answer2, chain2) {
91291
91819
  lines.push(`First buy ${fill.index}: ${whole(fill.amountRaw)} takes ${percent(fill.supplyPct)} of the supply.`);
91292
91820
  if (devBuyMax)
91293
91821
  lines.push(`Dev-buy ceiling: ${whole(devBuyMax)}.`);
91822
+ if (chain2 === "solana")
91823
+ lines.push(...feeWalletLines(feeWallet, echo));
91294
91824
  return lines.length ? `${lines.join(`
91295
91825
  `)}
91296
91826
  ` : "";
@@ -91303,7 +91833,7 @@ async function dryRunLaunch(ctx, key, args) {
91303
91833
  throw buyRefusal(error);
91304
91834
  });
91305
91835
  ctx.deps.stderr.write(`Dry run: nothing was built, signed or recorded, and launch id ${args.id} is unused.
91306
- ${previewLines(answer2, chain2)}`);
91836
+ ${previewLines(answer2, chain2, feeWalletSelection(args.flags))}`);
91307
91837
  return printTradingResult(ctx, { ...answer2, clientTradeId: args.id, kind: "launch", ...args.used });
91308
91838
  }
91309
91839
  var DEV_BUY_CEILINGS = LAUNCH_DEFINITION.buyLimits.defaults;
@@ -91400,6 +91930,7 @@ async function atomicLaunch(ctx, key, args) {
91400
91930
  candleFee: reading.feeRaw > 0n ? `${decimalAmount(reading.feeRaw.toString(), decimals)} ${quoteAsset} (the tier fee on the first buy, in its leg)` : "none on this first buy",
91401
91931
  minimumReceived: minOut !== undefined ? `${minOut} raw units of the new token (the first buy's floor)` : "not quoted",
91402
91932
  maxLoss: quoteAsset === "SOL" ? sol : `${sol} and ${decimalAmount(reading.quoteRaw.toString(), decimals)} ${quoteAsset}`,
91933
+ feeWallet: feeWalletQuoteLine(feeWalletSelection(flags)),
91403
91934
  tokenRisks: [],
91404
91935
  bundleId: built.bundleId
91405
91936
  };
@@ -91528,8 +92059,10 @@ async function embeddedLaunch(ctx, key, args) {
91528
92059
  const buy = launchBuy(flags, chain2);
91529
92060
  if (buy && chain2 === "solana")
91530
92061
  firstBuyCeiling(ctx, flags, buy);
91531
- if (chain2 === "hood" && Object.keys(poolRequestFields(flags)).length)
91532
- throw new TradingUsage("Pool fee and pool shape choices are Solana only; a Hood launch cannot take them.");
92062
+ if (chain2 === "hood" && (Object.keys(poolRequestFields(flags)).length || feeWalletGiven(flags)))
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);
91533
92066
  const wallet3 = await embeddedLaunchWallet(ctx, key, chain2);
91534
92067
  const uploaded = await uploadLaunchImages(ctx, key, args);
91535
92068
  if (typeof uploaded === "number")
@@ -91549,6 +92082,7 @@ async function embeddedLaunch(ctx, key, args) {
91549
92082
  ...launchCurveFields(flags),
91550
92083
  ...launchMetadataFields(flags),
91551
92084
  ...poolRequestFields(flags),
92085
+ ...feeWalletRequestFields(flags),
91552
92086
  ...buy ? { buyAmount: chain2 === "hood" || BigInt(buy.raw) > BigInt(Number.MAX_SAFE_INTEGER) ? buy.raw : Number(buy.raw) } : {}
91553
92087
  };
91554
92088
  const answer2 = await request(ctx, key, "/api/v1/launch/headless/dry-run", body);
@@ -91558,6 +92092,7 @@ async function embeddedLaunch(ctx, key, args) {
91558
92092
  venue: "curve launch (signed by Candle)",
91559
92093
  priceImpactPct: null,
91560
92094
  minimumReceived: "0 tokens (no first buy)",
92095
+ ...chain2 === "solana" ? { feeWallet: feeWalletQuoteLine(feeWalletSelection(flags)) } : {},
91561
92096
  tokenRisks: []
91562
92097
  };
91563
92098
  if (buy && chain2 === "solana") {
@@ -91581,7 +92116,7 @@ async function embeddedLaunch(ctx, key, args) {
91581
92116
  if (buy)
91582
92117
  quote.minimumReceived = "not quoted (best-effort first buy after creation)";
91583
92118
  }
91584
- ctx.deps.stderr.write(previewLines(answer2, chain2));
92119
+ ctx.deps.stderr.write(previewLines(answer2, chain2, feeWalletSelection(flags)));
91585
92120
  if (args.dryRun) {
91586
92121
  ctx.deps.stderr.write(`Dry run: nothing was built, signed or recorded, and launch id ${id} is unused.
91587
92122
  `);
@@ -91784,6 +92319,92 @@ async function resumeHoodLaunch(ctx, key, id, operationId, prior) {
91784
92319
  return printTradingResult(ctx, { ...result, clientTradeId: id, kind: "launch", chain: "hood", operationId });
91785
92320
  }
91786
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
+
91787
92408
  // src/commands/lp.ts
91788
92409
  init_args();
91789
92410
  init_client();
@@ -97644,7 +98265,7 @@ init_swap();
97644
98265
  init_vault_support();
97645
98266
  var WITHDRAW_SCOPE = "perps:withdraw";
97646
98267
  var ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
97647
- 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>";
97648
98269
  var WITHDRAW_POLL_MS = 10 * 60 * 1000;
97649
98270
  var WITHDRAW_POLL_INTERVAL_MS = 5000;
97650
98271
  var TERMINAL = new Set(["settled", "refunded", "failed"]);
@@ -97695,7 +98316,7 @@ async function withdrawStatus(ctx, id) {
97695
98316
  async function perpsWithdraw(args, ctx) {
97696
98317
  if (args[0] === "status") {
97697
98318
  if (args.length !== 2 || !ID_RE.test(args[1]))
97698
- return usage10(ctx, USAGE10);
98319
+ return usage10(ctx, USAGE11);
97699
98320
  try {
97700
98321
  return await withdrawStatus(ctx, args[1]);
97701
98322
  } catch (error) {
@@ -97714,7 +98335,7 @@ async function perpsWithdraw(args, ctx) {
97714
98335
  const [amount] = parsed.positionals;
97715
98336
  const to = parsed.values["--to"];
97716
98337
  if (parsed.positionals.length !== 1 || !amount || to !== "solana" && to !== "hood")
97717
- return usage10(ctx, USAGE10);
98338
+ return usage10(ctx, USAGE11);
97718
98339
  const amountMicros = parseWithdrawAmount(amount);
97719
98340
  if (amountMicros === null)
97720
98341
  return usage10(ctx, "The amount must be a positive USDC decimal with at most 6 decimals.");
@@ -99602,12 +100223,12 @@ function planPromotionLine2(table) {
99602
100223
  init_presentation();
99603
100224
  init_render();
99604
100225
  init_key_output();
99605
- var USAGE11 = "Usage: candle plans";
100226
+ var USAGE12 = "Usage: candle plans";
99606
100227
  async function plans(args, ctx) {
99607
100228
  const parsed = parseArgs(args, {});
99608
100229
  if ("error" in parsed || parsed.positionals.length > 0) {
99609
100230
  writeScreenUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
99610
- ${USAGE11}` : USAGE11, ctx.json);
100231
+ ${USAGE12}` : USAGE12, ctx.json);
99611
100232
  return 2;
99612
100233
  }
99613
100234
  const result = await apiRequest("/api/v1/agent/plans", {
@@ -101881,7 +102502,7 @@ init_client();
101881
102502
  init_human_output();
101882
102503
  init_presentation();
101883
102504
  init_render();
101884
- var USAGE12 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
102505
+ var USAGE13 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
101885
102506
  var OTHER_CHAIN = "Candle trades Solana and Hood today; that is not a Solana or Hood contract address.";
101886
102507
  var HOOD_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
101887
102508
  var SOLANA_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
@@ -101981,20 +102602,20 @@ async function scan(args, ctx) {
101981
102602
  }
101982
102603
  const named = parsed.values["--chain"];
101983
102604
  if (parsed.positionals.length !== 1 || named !== undefined && named !== "solana" && named !== "hood") {
101984
- writeUsageFailure(ctx.deps, USAGE12, ctx.json);
102605
+ writeUsageFailure(ctx.deps, USAGE13, ctx.json);
101985
102606
  return 2;
101986
102607
  }
101987
102608
  const address = parsed.positionals[0].trim();
101988
102609
  const found = chainOfAddress(address);
101989
102610
  if (found === null) {
101990
102611
  writeUsageFailure(ctx.deps, `${OTHER_CHAIN}
101991
- ${USAGE12}`, ctx.json);
102612
+ ${USAGE13}`, ctx.json);
101992
102613
  return 2;
101993
102614
  }
101994
102615
  if (named !== undefined && named !== found) {
101995
102616
  const label = (c) => c === "hood" ? "Hood" : "Solana";
101996
102617
  writeUsageFailure(ctx.deps, `That address is a ${label(found)} contract, not a ${label(named)} one. Candle trades Solana and Hood today.
101997
- ${USAGE12}`, ctx.json);
102618
+ ${USAGE13}`, ctx.json);
101998
102619
  return 2;
101999
102620
  }
102000
102621
  const chain2 = found;
@@ -103872,7 +104493,7 @@ init_deps();
103872
104493
  init_human_output();
103873
104494
  init_render();
103874
104495
  var KINDS = ["trade", "signal", "callout"];
103875
- 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]";
103876
104497
  function readEventsBody(body) {
103877
104498
  if (body === null || typeof body !== "object" || !Array.isArray(body.events))
103878
104499
  return null;
@@ -103930,11 +104551,11 @@ async function tokenEvents(args, ctx) {
103930
104551
  const since = parsed.values["--since"];
103931
104552
  const before = parsed.values["--before"];
103932
104553
  if (parsed.positionals.length !== 1 || chain2 !== "solana" && chain2 !== "hood") {
103933
- writeUsageFailure(ctx.deps, USAGE13, ctx.json);
104554
+ writeUsageFailure(ctx.deps, USAGE14, ctx.json);
103934
104555
  return 2;
103935
104556
  }
103936
104557
  if (kind !== undefined && !KINDS.includes(kind)) {
103937
- writeUsageFailure(ctx.deps, USAGE13, ctx.json);
104558
+ writeUsageFailure(ctx.deps, USAGE14, ctx.json);
103938
104559
  return 2;
103939
104560
  }
103940
104561
  if (limit !== undefined && !/^[1-9][0-9]{0,3}$/.test(limit) || Number(limit) > 1000) {
@@ -104031,7 +104652,7 @@ var TOKEN_PAGE_BODY_CAP_BYTES = 32 * 1024;
104031
104652
  var TOKEN_PAGE_SECTIONS = ["thesis", "tokenomics", "lore"];
104032
104653
  var FLAG_REASONS = ["spam", "scam", "abuse", "misleading", "unsafe_link", "other"];
104033
104654
  var FLAG_DETAIL_LIMIT = 500;
104034
- var USAGE14 = {
104655
+ var USAGE15 = {
104035
104656
  about: "Usage: candle token about <address> [--chain solana|hood] [--capabilities]",
104036
104657
  claim: "Usage: candle token claim <address> [--chain solana|hood] [--creator [--challenge [--message-out <path>] | --challenge-id <id> --signature <sig>]] [--idempotency-key <key>]",
104037
104658
  section: `Usage: candle token section set <address> <thesis|tokenomics|lore> --file <path> --base-revision <n> [--clear] [--chain solana|hood] [--idempotency-key <key>]
@@ -104291,7 +104912,7 @@ function terminalTextKeepingLines(text4) {
104291
104912
  `);
104292
104913
  }
104293
104914
  async function tokenAbout(args, ctx) {
104294
- const usage12 = usageFor(ctx, USAGE14.about);
104915
+ const usage12 = usageFor(ctx, USAGE15.about);
104295
104916
  const parsed = parseArgs(args, { valueFlags: ["--chain"], booleanFlags: ["--capabilities"] });
104296
104917
  if ("error" in parsed)
104297
104918
  return usage12(parsed.error);
@@ -104337,7 +104958,7 @@ async function tokenAbout(args, ctx) {
104337
104958
  return 0;
104338
104959
  }
104339
104960
  async function tokenClaim(args, ctx) {
104340
- const usage12 = usageFor(ctx, USAGE14.claim);
104961
+ const usage12 = usageFor(ctx, USAGE15.claim);
104341
104962
  const parsed = parseArgs(args, {
104342
104963
  valueFlags: ["--chain", "--challenge-id", "--signature", "--message-out", "--idempotency-key"],
104343
104964
  booleanFlags: ["--creator", "--challenge"],
@@ -104464,7 +105085,7 @@ var SECTION_FLAGS = {
104464
105085
  restore: { valueFlags: ["--chain", "--version-id", "--base-revision", "--idempotency-key"], booleanFlags: [] }
104465
105086
  };
104466
105087
  async function tokenSection(args, ctx) {
104467
- const usage12 = usageFor(ctx, USAGE14.section);
105088
+ const usage12 = usageFor(ctx, USAGE15.section);
104468
105089
  const action2 = args[0];
104469
105090
  if (action2 !== "set" && action2 !== "restore")
104470
105091
  return usage12();
@@ -104557,7 +105178,7 @@ async function tokenSection(args, ctx) {
104557
105178
  return 0;
104558
105179
  }
104559
105180
  async function tokenHistory(args, ctx) {
104560
- const usage12 = usageFor(ctx, USAGE14.history);
105181
+ const usage12 = usageFor(ctx, USAGE15.history);
104561
105182
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
104562
105183
  if ("error" in parsed)
104563
105184
  return usage12(parsed.error);
@@ -104612,7 +105233,7 @@ More: --cursor ${data.nextCursor}
104612
105233
  return 0;
104613
105234
  }
104614
105235
  async function tokenFlag(args, ctx) {
104615
- const usage12 = usageFor(ctx, USAGE14.flag);
105236
+ const usage12 = usageFor(ctx, USAGE15.flag);
104616
105237
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--content", "--reason", "--detail", "--idempotency-key"] });
104617
105238
  if ("error" in parsed)
104618
105239
  return usage12(parsed.error);
@@ -104707,7 +105328,7 @@ async function tokenUpdate(args, ctx) {
104707
105328
  return 0;
104708
105329
  }
104709
105330
  async function tokenComment(args, ctx) {
104710
- const usage12 = usageFor(ctx, USAGE14.comment);
105331
+ const usage12 = usageFor(ctx, USAGE15.comment);
104711
105332
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--file", "--idempotency-key"], pathFlags: ["--file"] });
104712
105333
  if ("error" in parsed)
104713
105334
  return usage12(parsed.error);
@@ -104752,7 +105373,7 @@ async function tokenComment(args, ctx) {
104752
105373
  return 0;
104753
105374
  }
104754
105375
  async function tokenComments(args, ctx) {
104755
- const usage12 = usageFor(ctx, USAGE14.comments);
105376
+ const usage12 = usageFor(ctx, USAGE15.comments);
104756
105377
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
104757
105378
  if ("error" in parsed)
104758
105379
  return usage12(parsed.error);
@@ -105098,7 +105719,7 @@ init_render();
105098
105719
  init_solana_verify();
105099
105720
  init_trading();
105100
105721
  init_swap();
105101
- 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]";
105102
105723
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
105103
105724
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
105104
105725
  async function readLinkedWallets(ctx, key) {
@@ -105118,6 +105739,12 @@ async function readLinkedWallets(ctx, key) {
105118
105739
  }
105119
105740
  return rows;
105120
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
+ }
105121
105748
  async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
105122
105749
  const hood = chain2 === "hood";
105123
105750
  const same = (a, b) => hood ? sameEvmAddress(a, b) : a === b;
@@ -105143,13 +105770,35 @@ async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
105143
105770
  return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
105144
105771
  if (same(to, source.address))
105145
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
+ }
105146
105789
  if (!hood && BASE58_ADDRESS.test(to))
105147
105790
  return { kind: "address", address: to };
105148
105791
  const evm = hood ? checkEvmAddress(to) : undefined;
105149
105792
  if (evm?.ok)
105150
105793
  return { kind: "address", address: evm.address };
105151
105794
  const name2 = hood ? "Hood" : "Solana";
105152
- 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(" ")}`);
105153
105802
  }
105154
105803
  function describeDestination(destination) {
105155
105804
  switch (destination.kind) {
@@ -105157,10 +105806,31 @@ function describeDestination(destination) {
105157
105806
  return `this wallet's pinned vault ${destination.address}`;
105158
105807
  case "linked":
105159
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})`;
105160
105811
  case "address":
105161
- 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)`;
105162
105813
  }
105163
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
+ }
105164
105834
  function writePreview(ctx, title2, rows, note) {
105165
105835
  const style = terminalStyle(ctx.deps, "stderr");
105166
105836
  ctx.deps.stderr.write(`${heading(title2, style)}
@@ -105194,7 +105864,7 @@ async function transfer(args, ctx) {
105194
105864
  const flags2 = parsed.values;
105195
105865
  const asset = flags2["--asset"]?.toUpperCase();
105196
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) {
105197
- writeUsageFailure(ctx.deps, USAGE15, ctx.json);
105867
+ writeUsageFailure(ctx.deps, USAGE16, ctx.json);
105198
105868
  return 2;
105199
105869
  }
105200
105870
  const to = flags2["--to"];
@@ -105231,7 +105901,7 @@ async function transfer(args, ctx) {
105231
105901
  const amountText2 = isMax ? `the full spendable balance of ${label}` : `${flags2["--amount"]} ${label}`;
105232
105902
  const confirmed = await confirmTransfer(ctx, [
105233
105903
  `Transfer ${amountText2} from ${wallet3.address} (${wallet3.id}) to ${describeDestination(destination)}`,
105234
- 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)
105235
105905
  ], parsed.booleans.has("--yes"));
105236
105906
  if (!confirmed)
105237
105907
  return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet3.id, destination });
@@ -105252,11 +105922,7 @@ async function transfer(args, ctx) {
105252
105922
  if (builtAmount === null || !isMax && builtAmount !== amountRaw)
105253
105923
  throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the confirmed amount; nothing was signed.");
105254
105924
  const amountShown = asset ? `${decimalAmount(builtAmount, BASES[asset]?.decimals ?? 0)} ${asset}` : `${builtAmount} raw units of ${label}`;
105255
- writePreview(ctx, "Built", [
105256
- ["Transfer", transferId],
105257
- ["Amount", amountShown],
105258
- ...typeof built.destinationKind === "string" ? [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]] : []
105259
- ]);
105925
+ writePreview(ctx, "Built", [["Transfer", transferId], ["Amount", amountShown], ...builtDestination(built)]);
105260
105926
  const reader = await solana();
105261
105927
  const problem = await tradingRead(ctx, reader, () => solanaSpendProblem(unsigned[0], wallet3.address, reader.rpc, {
105262
105928
  kind: "transfer",
@@ -105318,7 +105984,7 @@ async function hoodTransfer(ctx, args) {
105318
105984
  const amountText2 = isMax ? `the full spendable balance of ${label}` : `${flags2["--amount"]} ${label}`;
105319
105985
  const confirmed = await confirmTransfer(ctx, [
105320
105986
  `Transfer ${amountText2} on Hood from ${wallet3.address} (${wallet3.id}) to ${describeDestination(destination)}`,
105321
- 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),
105322
105988
  ...asset === "ETH" ? [
105323
105989
  "ETH keeps this send's gas and the sweep-home gas reserve in the wallet; Candle refuses an amount that would spend them."
105324
105990
  ] : []
@@ -105353,7 +106019,7 @@ async function hoodTransfer(ctx, args) {
105353
106019
  writePreview(ctx, "Built", [
105354
106020
  ["Transfer", safeText(transferId)],
105355
106021
  ["Amount", `${decimalAmount(amountRaw, decimals)} ${safeText(label)}`],
105356
- ...typeof built.destinationKind === "string" ? [["To", built.destinationKind === "vault" ? "the vault" : "a linked wallet"]] : [],
106022
+ ...builtDestination(built),
105357
106023
  [
105358
106024
  "Gas",
105359
106025
  `up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)`
@@ -106798,8 +107464,332 @@ init_reconcile_grant();
106798
107464
  init_tee_resolve();
106799
107465
  init_tee();
106800
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
106801
107789
  init_vault_support();
106802
107790
  async function vaultDemote(args, ctx) {
107791
+ if (ctx.heldUnlock === undefined && isBatchDemote(args))
107792
+ return vaultDemoteBatch(args, ctx);
106803
107793
  if (namesEvmWallet(args))
106804
107794
  return vaultDemoteEvm(args, ctx);
106805
107795
  const parsed = parseArgs(args, {
@@ -108565,6 +109555,8 @@ async function vaultFund(args, ctx) {
108565
109555
  const asset = (parsed.values["--asset"] ?? "SOL").toUpperCase();
108566
109556
  if (!amount)
108567
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.");
108568
109560
  if (asset !== "SOL" && asset !== "USDC")
108569
109561
  return usage(ctx, "--asset must be SOL or USDC.");
108570
109562
  const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
@@ -112788,6 +113780,7 @@ init_args();
112788
113780
  init_deps();
112789
113781
  init_evm_lite();
112790
113782
  init_solana_endpoint();
113783
+ init_errors2();
112791
113784
  init_evm_tee();
112792
113785
  init_evm_transfer();
112793
113786
  init_promote_support();
@@ -112810,7 +113803,7 @@ async function vaultTransfer(args, ctx) {
112810
113803
  const fromLabel = parsed.values["--from"];
112811
113804
  const rpcUrlFlag = parsed.values["--rpc-url"];
112812
113805
  if (!amount)
112813
- return usage(ctx, "--amount <n> is required.");
113806
+ return usage(ctx, "--amount <n|max> is required.");
112814
113807
  if (!asset)
112815
113808
  return usage(ctx, "--asset SOL|<mint>|ETH|USDG|<0x token> is required.");
112816
113809
  if (!fromLabel)
@@ -112906,14 +113899,15 @@ async function vaultTransfer(args, ctx) {
112906
113899
  const reconciled = await reconcileFundingReceipts(vault, [fromEntry.address, to], solana.rpc, ctx);
112907
113900
  if (reconciled !== null)
112908
113901
  return reconciled;
112909
- const plan2 = await solana.read(() => planTransfer({
113902
+ const buildPlan = () => solana.read(() => planTransfer({
112910
113903
  from: fromEntry.address,
112911
113904
  to,
112912
113905
  amount,
112913
113906
  asset,
112914
113907
  rpc: solana.rpc
112915
113908
  }));
112916
- 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));
112917
113911
  displayTransferPlan(ctx, plan2, feeQuote);
112918
113912
  await assertTransferAffordable(solana, plan2, feeQuote);
112919
113913
  if (promoted) {
@@ -112921,7 +113915,23 @@ async function vaultTransfer(args, ctx) {
112921
113915
  if (notice2 !== undefined)
112922
113916
  note(notice2);
112923
113917
  }
112924
- 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
+ }
112925
113935
  const secret = await decryptKey(vault, fromEntry.id);
112926
113936
  try {
112927
113937
  const result = await signAndBroadcastTransfer({ ctx, solana, secret64: secret, plan: plan2 });
@@ -112964,7 +113974,7 @@ init_human_output();
112964
113974
  init_release();
112965
113975
  import { dirname as dirname11, join as join15 } from "node:path";
112966
113976
  init_key_output();
112967
- 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>]";
112968
113978
  async function resolveIdentity(deps, bundlePath, flag) {
112969
113979
  if (flag)
112970
113980
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -112996,19 +114006,19 @@ async function verify(args, ctx) {
112996
114006
  });
112997
114007
  if ("error" in parsed) {
112998
114008
  writeScreenUsageFailure(deps, `${parsed.error}
112999
- ${USAGE16}`, json);
114009
+ ${USAGE17}`, json);
113000
114010
  return 2;
113001
114011
  }
113002
114012
  const file = parsed.positionals[0];
113003
114013
  if (parsed.positionals.length !== 1 || file === undefined) {
113004
114014
  writeScreenUsageFailure(deps, `verify takes exactly one file.
113005
- ${USAGE16}`, json);
114015
+ ${USAGE17}`, json);
113006
114016
  return 2;
113007
114017
  }
113008
114018
  const bundlePath = parsed.values["--bundle"];
113009
114019
  if (!bundlePath) {
113010
114020
  writeScreenUsageFailure(deps, `--bundle is required.
113011
- ${USAGE16}`, json);
114021
+ ${USAGE17}`, json);
113012
114022
  return 2;
113013
114023
  }
113014
114024
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -113022,7 +114032,7 @@ ${USAGE16}`, json);
113022
114032
  }
113023
114033
  if (resolved.kind === "absent") {
113024
114034
  writeScreenUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
113025
- ${USAGE16}`, json);
114035
+ ${USAGE17}`, json);
113026
114036
  return 2;
113027
114037
  }
113028
114038
  const identity4 = resolved.uri;
@@ -113271,7 +114281,7 @@ init_solana_lite();
113271
114281
  init_trading();
113272
114282
  init_swap();
113273
114283
  import { randomUUID as randomUUID15 } from "node:crypto";
113274
- 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]";
113275
114285
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
113276
114286
  var LISTED_ROWS = 50;
113277
114287
  var TOKEN_IX_CLOSE_ACCOUNT3 = 9;
@@ -113534,7 +114544,7 @@ async function closeTee(ctx, key, wallet3, name2, run2) {
113534
114544
  async function walletsCloseEmpty(args, ctx) {
113535
114545
  const lifted = splitKeep(args);
113536
114546
  if ("error" in lifted) {
113537
- writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE17}`, ctx.json);
114547
+ writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE18}`, ctx.json);
113538
114548
  return 2;
113539
114549
  }
113540
114550
  const parsed = parseArgs(lifted.rest, { valueFlags: ["--wallet", "--client-trade-id"], booleanFlags: ["--yes"] });
@@ -113545,7 +114555,7 @@ async function walletsCloseEmpty(args, ctx) {
113545
114555
  const named = parsed.values["--client-trade-id"];
113546
114556
  const id = named ?? `close-${randomUUID15()}`;
113547
114557
  if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
113548
- writeUsageFailure(ctx.deps, USAGE17, ctx.json);
114558
+ writeUsageFailure(ctx.deps, USAGE18, ctx.json);
113549
114559
  return 2;
113550
114560
  }
113551
114561
  const run2 = { id, named: named !== undefined, keep: lifted.keep, yes: parsed.booleans.has("--yes") };
@@ -113862,14 +114872,27 @@ async function maybeWriteUpdateNotice(deps, opts = {}) {
113862
114872
  return;
113863
114873
  if (opts.command === "update" || opts.command === "doctor")
113864
114874
  return;
113865
- const latest = latestCliVersionFromApi();
113866
- if (!latest || !isPlainVersion(latest) || compareVersions(latest, CLI_VERSION) <= 0)
114875
+ const seen = latestCliVersionFromApi();
114876
+ if (!seen || !isPlainVersion(seen) || compareVersions(seen, CLI_VERSION) <= 0)
113867
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
+ }
113868
114891
  const config = await deps.readConfig();
113869
114892
  const prior = config.updateNotice;
113870
114893
  if (prior && prior.version === latest && deps.now() - prior.shownAt < DAY_MS2)
113871
114894
  return;
113872
- const line = `Update available: candle ${CLI_VERSION} -> ${latest}. Run: candle update`;
114895
+ const line = `Update available: candle ${CLI_VERSION} -> ${latest}. Run: ${command}`;
113873
114896
  const tty = terminalStyle(deps, "stderr", opts.json).color !== 0;
113874
114897
  deps.stderr.write(tty ? `\x1B[2m${line}\x1B[0m
113875
114898
  ` : `${line}
@@ -113950,7 +114973,7 @@ var COMMANDS = {
113950
114973
  stake: { bare: stake, subcommands: { add: stakeAdd, remove: stakeRemove, finish: stakeFinish, claim: stakeClaim } },
113951
114974
  swap: { bare: swap, subcommands: { status: swapStatus } },
113952
114975
  transfer: { bare: transfer },
113953
- launch: { bare: launch },
114976
+ launch: { bare: launch, subcommands: { status: launchStatus } },
113954
114977
  image: { subcommands: { upload: imageUpload } },
113955
114978
  pnl: { bare: pnl },
113956
114979
  portfolio: { bare: portfolio },