@candledottv/cli 0.11.16 → 0.11.18

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 +24 -15
  2. package/dist/index.js +592 -232
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -343,14 +343,14 @@ var init_agent_key_access = __esm(() => {
343
343
  ];
344
344
  AGENT_KEY_PRESET_LABELS = {
345
345
  read: "Read",
346
- readwrite: "Read:Write",
347
- readwritetransfer: "Read:Write:Transfer"
346
+ readwrite: "Write",
347
+ readwritetransfer: "Withdraw"
348
348
  };
349
349
  AGENT_KEY_CAPABILITY_CHIPS = [
350
350
  ["Launch", "launch"],
351
351
  ["Trade", "trade"],
352
352
  ["Transfer", "transfer"],
353
- ["Linked transfer", "linkedTransfer"],
353
+ ["Withdraw", "linkedTransfer"],
354
354
  ["Report", "report"]
355
355
  ];
356
356
  });
@@ -1028,7 +1028,7 @@ async function keysCreate(args, ctx) {
1028
1028
  if (parsed.values["--access"] !== undefined) {
1029
1029
  const preset = ACCESS_LEVELS[parsed.values["--access"]];
1030
1030
  if (preset === undefined) {
1031
- writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
1031
+ writeUsageFailure(deps, "--access must be one of: read, write, withdraw.", json);
1032
1032
  return 2;
1033
1033
  }
1034
1034
  accessScopes = scopesForPreset(preset);
@@ -1226,6 +1226,9 @@ var init_keys = __esm(() => {
1226
1226
  init_keys_embedded_wallet();
1227
1227
  ACCESS_LEVELS = {
1228
1228
  read: "read",
1229
+ write: "readwrite",
1230
+ withdraw: "readwritetransfer",
1231
+ transfer: "readwritetransfer",
1229
1232
  "read-write": "readwrite",
1230
1233
  "read-write-transfer": "readwritetransfer"
1231
1234
  };
@@ -14506,8 +14509,29 @@ async function request(ctx, key, path, body) {
14506
14509
  fetch: ctx.deps.fetch,
14507
14510
  env: ctx.deps.env
14508
14511
  });
14509
- if (!result.ok)
14510
- throw new TradingError(result.code ?? "REQUEST_FAILED", result.message);
14512
+ if (!result.ok) {
14513
+ const raw = result.raw;
14514
+ const error = raw?.error;
14515
+ const details = {};
14516
+ for (const field of [
14517
+ "field",
14518
+ "stage",
14519
+ "signature",
14520
+ "leg",
14521
+ "legs",
14522
+ "hashes",
14523
+ "retryable",
14524
+ "evidenceStored",
14525
+ "terminal"
14526
+ ]) {
14527
+ if (error?.[field] !== undefined && (field === "field" || path === "/api/v1/agent/swap/submit"))
14528
+ details[field] = error[field];
14529
+ }
14530
+ throw new TradingError(result.code ?? "REQUEST_FAILED", result.message, {
14531
+ ...Object.keys(details).length ? { details } : {},
14532
+ ...path === "/api/v1/agent/swap/submit" && error?.stage === "unconfirmed" ? { exitCode: 3 } : {}
14533
+ });
14534
+ }
14511
14535
  if (!result.body || typeof result.body !== "object")
14512
14536
  throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
14513
14537
  return result.body;
@@ -14553,7 +14577,7 @@ async function completeTradingWallet(ctx, row, appId, scope, chain2 = "solana",
14553
14577
  if (!row.active)
14554
14578
  throw new TradingError("TEE_WALLET_INACTIVE", `The payer must be a verified-active ${chainName(chain2)} TEE wallet.`);
14555
14579
  if (scope === "launch:write" && row.allowLaunch !== true)
14556
- throw new TradingError("LAUNCH_NOT_ALLOWED", `The operator must enable allowLaunch for wallet ${row.id} through PUT /api/v1/agent/wallets/${row.id}/capabilities using device/session authentication.`);
14580
+ throw new TradingError("LAUNCH_NOT_ALLOWED", `The account owner must turn on allowLaunch for wallet ${row.id}: run candle wallets allow-launch ${row.id} on a profile signed in with candle auth login. An agent key cannot.`);
14557
14581
  if (!appId || !row.privyWalletId)
14558
14582
  throw new TradingError("SIGNER_UNAVAILABLE", "The server did not return its relay public identifiers.");
14559
14583
  const signer = await localSignerFor(ctx.deps, row.id, row.signerQuorumId);
@@ -27434,6 +27458,20 @@ async function resolveTeeAddress(ctx, _parsed, address, openLegacy, access3 = "r
27434
27458
  }
27435
27459
  };
27436
27460
  }
27461
+ async function grantSignAccess(resolved) {
27462
+ if (resolved.privateKeyBase58 === null) {
27463
+ const secret = await decryptKey(resolved.vault, resolved.entry.id);
27464
+ try {
27465
+ if (addressFromSecret64(secret) !== resolved.entry.address) {
27466
+ throw new VaultError("VAULT_VERIFY_FAILED", `${resolved.entry.address} in the vault does not re-derive from its stored secret.`);
27467
+ }
27468
+ resolved.privateKeyBase58 = base58.encode(secret);
27469
+ } finally {
27470
+ wipe(secret);
27471
+ }
27472
+ }
27473
+ resolved.legacyView = keyEntryAsKeystore(resolved.entry, resolved.privateKeyBase58);
27474
+ }
27437
27475
  function releaseResolvedTee(resolved) {
27438
27476
  if (resolved.source === "vault")
27439
27477
  closeVault(resolved.vault);
@@ -30311,6 +30349,8 @@ function tradingFailure(ctx, error, id) {
30311
30349
  writeUsageFailure(ctx.deps, error.message, ctx.json);
30312
30350
  return 2;
30313
30351
  }
30352
+ if (error instanceof TradingError && error.details)
30353
+ describeSwapEvidence(ctx, error.details, error.details.terminal === true);
30314
30354
  writeLocalFailure(ctx.deps, {
30315
30355
  code: error instanceof TradingError ? error.code : "TRADING_FAILED",
30316
30356
  message: `${error instanceof Error ? describeRpcFailure(error) : "Trading failed."}${id ? ` Operation ${id}; use candle swap status ${id} before another attempt.` : ""}`,
@@ -30335,7 +30375,31 @@ async function tradingRead(ctx, client, read) {
30335
30375
  throw error;
30336
30376
  }
30337
30377
  }
30378
+ async function requestSwapBuild(ctx, key, body) {
30379
+ try {
30380
+ return await request(ctx, key, "/api/v1/agent/swap/build", body);
30381
+ } catch (error) {
30382
+ if (body.payer?.type === "main" && error instanceof TradingError && error.code === "VALIDATION_FAILED" && error.details?.field === "payer") {
30383
+ throw new TradingError("EMBEDDED_PAYER_UNSUPPORTED", "This Candle deployment cannot hold an embedded-wallet swap back for confirmation. Nothing was built. Name a TEE wallet with --wallet, or point at a deployment that supports embedded swap builds.");
30384
+ }
30385
+ throw error;
30386
+ }
30387
+ }
30388
+ function describeSwapEvidence(ctx, details, terminal) {
30389
+ if (details.stage === "not_broadcast" && terminal)
30390
+ ctx.deps.stderr.write(`Nothing was sent. This swap job is terminal; retry with a new id.
30391
+ `);
30392
+ if (details.stage === "unconfirmed") {
30393
+ const hashes = Array.isArray(details.hashes) ? details.hashes : typeof details.signature === "string" ? [details.signature] : [];
30394
+ ctx.deps.stderr.write(`Swap outcome needs verification${details.leg !== undefined ? ` (leg ${safeText(String(details.leg))})` : ""}. Known hashes: ${hashes.length ? hashes.map((hash) => safeText(String(hash))).join(", ") : "unknown"}. Do not automatically retry or use a new id.
30395
+ `);
30396
+ }
30397
+ }
30338
30398
  function printTradingResult(ctx, result) {
30399
+ if (result.kind === "swap" && result.job && typeof result.job === "object") {
30400
+ const job = result.job;
30401
+ describeSwapEvidence(ctx, job, job.status === "failed");
30402
+ }
30339
30403
  ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
30340
30404
  ` : `${JSON.stringify(result, null, 2)}
30341
30405
  `);
@@ -30533,8 +30597,6 @@ async function swap(args, ctx) {
30533
30597
  if (prior)
30534
30598
  return printTradingResult(ctx, prior);
30535
30599
  const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
30536
- if (payerWallet.kind === "embedded" && kind === "swap")
30537
- throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
30538
30600
  const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
30539
30601
  const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
30540
30602
  const decimals = await decimalsFor(ctx, from, solana);
@@ -30560,14 +30622,14 @@ async function swap(args, ctx) {
30560
30622
  amountRaw = rawAmount(flags["--amount"], decimals);
30561
30623
  if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
30562
30624
  throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
30563
- if (payerWallet.kind === "embedded")
30625
+ if (payerWallet.kind === "embedded" && kind === "trade")
30564
30626
  await assertDeferredExecuteSupported(ctx, key, id);
30565
30627
  if (!await claimOperation(ctx, key, id, kind))
30566
30628
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30567
30629
  ctx.deps.stderr.write(`Operation: ${id}
30568
30630
  `);
30569
30631
  const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
30570
- const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
30632
+ const built = kind === "swap" ? await requestSwapBuild(ctx, key, {
30571
30633
  clientTradeId: id,
30572
30634
  from,
30573
30635
  to,
@@ -30589,7 +30651,7 @@ async function swap(args, ctx) {
30589
30651
  return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
30590
30652
  const data = swapBuildSchema.parse(kind === "swap" ? built.payload : built);
30591
30653
  if (kind === "swap" && (data.venue !== "jupiter" || data.recipient !== wallet.address))
30592
- throw new TradingError("INVALID_RESPONSE", "A TEE base swap must use Jupiter and return to its payer.");
30654
+ throw new TradingError("INVALID_RESPONSE", "A base swap must use Jupiter and return to its payer.");
30593
30655
  if (kind === "trade" && (built.chain !== "solana" || built.walletAddress !== wallet.address))
30594
30656
  throw new TradingError("INVALID_RESPONSE", "The token build does not name the requested Solana payer.");
30595
30657
  const artifacts = kind === "swap" ? { ...data, transactionBase64: undefined, quoteSource: undefined, quoteAsset: undefined } : data.artifacts;
@@ -30616,9 +30678,11 @@ async function swap(args, ctx) {
30616
30678
  if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
30617
30679
  throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
30618
30680
  if (payerWallet.kind === "embedded") {
30619
- const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30681
+ const executed = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", { clientTradeId: id, swapId: data.swapId }) : await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30682
+ const settled2 = kind === "swap" ? reportSettlement(ctx, executed.payload?.settlement, { asset: to, decimals: outDecimals }, id) : {};
30620
30683
  return printTradingResult(ctx, {
30621
30684
  ...executed,
30685
+ ...settled2,
30622
30686
  clientTradeId: id,
30623
30687
  kind,
30624
30688
  quote,
@@ -30728,8 +30792,6 @@ async function hoodSwap(ctx, args) {
30728
30792
  if (prior)
30729
30793
  return printTradingResult(ctx, prior);
30730
30794
  const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
30731
- if (payer.kind === "embedded" && kind === "swap")
30732
- throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap ETH and USDG from this command: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a Hood TEE wallet.");
30733
30795
  const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
30734
30796
  const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
30735
30797
  const decimals = await hoodDecimals(from, rpc);
@@ -30742,7 +30804,7 @@ async function hoodSwap(ctx, args) {
30742
30804
  throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
30743
30805
  } else
30744
30806
  amountRaw = rawAmount(flags["--amount"], decimals);
30745
- if (payer.kind === "embedded")
30807
+ if (payer.kind === "embedded" && kind === "trade")
30746
30808
  await assertDeferredExecuteSupported(ctx, key, id);
30747
30809
  if (!await claimOperation(ctx, key, id, kind))
30748
30810
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
@@ -30751,7 +30813,7 @@ async function hoodSwap(ctx, args) {
30751
30813
  const payerBody = payer.kind === "tee" ? { type: "linked", linkedWalletId: payer.wallet.id } : { type: "main" };
30752
30814
  const base = from.base ?? to.base;
30753
30815
  const token = from.base ? to.asset : from.asset;
30754
- const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
30816
+ const built = kind === "swap" ? await requestSwapBuild(ctx, key, {
30755
30817
  clientTradeId: id,
30756
30818
  from: from.asset,
30757
30819
  to: to.asset,
@@ -30825,8 +30887,16 @@ async function hoodSwap(ctx, args) {
30825
30887
  if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
30826
30888
  throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
30827
30889
  if (payer.kind === "embedded") {
30828
- const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30829
- return printTradingResult(ctx, { ...executed, clientTradeId: id, kind, quote, wallet: safeText(payerAddress) });
30890
+ const executed = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", { clientTradeId: id, swapId: data.swapId }) : await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30891
+ const settled2 = kind === "swap" ? reportSettlement(ctx, executed.payload?.settlement, { asset: to.asset, decimals: outDecimals }, id) : {};
30892
+ return printTradingResult(ctx, {
30893
+ ...executed,
30894
+ ...settled2,
30895
+ clientTradeId: id,
30896
+ kind,
30897
+ quote,
30898
+ wallet: safeText(payerAddress)
30899
+ });
30830
30900
  }
30831
30901
  const wallet = payer.wallet;
30832
30902
  if (wallet.chain !== "evm" || !sequenced)
@@ -30935,7 +31005,7 @@ async function bridgeSwap(ctx, args) {
30935
31005
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30936
31006
  ctx.deps.stderr.write(`Operation: ${id}
30937
31007
  `);
30938
- const built = await request(ctx, key, "/api/v1/agent/swap/build", {
31008
+ const built = await requestSwapBuild(ctx, key, {
30939
31009
  clientTradeId: id,
30940
31010
  from,
30941
31011
  to,
@@ -32319,23 +32389,10 @@ async function teeSweepEvm(args, ctx) {
32319
32389
  if (!address || extra !== undefined) {
32320
32390
  return usage(ctx, "Usage: candle tee sweep <0x address> [--rpc-url <url>] [--emergency] [--token <0x...>]... [--from-block <n>]");
32321
32391
  }
32322
- const tokens = [];
32323
- for (const raw of tokenFlags) {
32324
- const checked = checkEvmAddress(raw);
32325
- if (!checked.ok)
32326
- return usage(ctx, `--token must be an ERC-20 contract address: ${raw || "(empty)"} is ${checked.reason}.`);
32327
- tokens.push(checked.address);
32328
- }
32329
- let fromBlock;
32330
- const fromBlockRaw = parsed.values["--from-block"];
32331
- if (fromBlockRaw !== undefined) {
32332
- if (!/^\d+$/.test(fromBlockRaw))
32333
- return usage(ctx, `--from-block must be a Hood block number: ${fromBlockRaw} is not.`);
32334
- fromBlock = BigInt(fromBlockRaw);
32335
- }
32336
- const client = resolveHoodClient(ctx, parsed.values["--rpc-url"]);
32337
- if ("error" in client)
32338
- return usage(ctx, client.error);
32392
+ const options = sweepOptions(ctx, parsed, tokenFlags);
32393
+ if (typeof options === "number")
32394
+ return options;
32395
+ const { tokens, fromBlock, client } = options;
32339
32396
  if (!refuseEnvPassphrase(ctx))
32340
32397
  return 1;
32341
32398
  if (!requireTty(ctx, "tee sweep"))
@@ -32354,6 +32411,26 @@ async function teeSweepEvm(args, ctx) {
32354
32411
  return sweepEvmWallet({ ctx, vault, hold, entry, client, emergency, tokenFlags: tokens, fromBlock });
32355
32412
  });
32356
32413
  }
32414
+ function sweepOptions(ctx, parsed, tokenFlags) {
32415
+ const tokens = [];
32416
+ for (const raw of tokenFlags) {
32417
+ const checked = checkEvmAddress(raw);
32418
+ if (!checked.ok)
32419
+ return usage(ctx, `--token must be an ERC-20 contract address: ${raw || "(empty)"} is ${checked.reason}.`);
32420
+ tokens.push(checked.address);
32421
+ }
32422
+ let fromBlock;
32423
+ const fromBlockRaw = parsed.values["--from-block"];
32424
+ if (fromBlockRaw !== undefined) {
32425
+ if (!/^\d+$/.test(fromBlockRaw))
32426
+ return usage(ctx, `--from-block must be a Hood block number: ${fromBlockRaw} is not.`);
32427
+ fromBlock = BigInt(fromBlockRaw);
32428
+ }
32429
+ const client = resolveHoodClient(ctx, parsed.values["--rpc-url"]);
32430
+ if ("error" in client)
32431
+ return usage(ctx, client.error);
32432
+ return { tokens, fromBlock, client };
32433
+ }
32357
32434
  async function sweepEvmWallet(input) {
32358
32435
  const { ctx, client, emergency, hold } = input;
32359
32436
  const { deps, json } = ctx;
@@ -32931,78 +33008,83 @@ async function teeDisableEvm(args, ctx) {
32931
33008
  const resolvedVault = vaultPathFor(ctx, parsed);
32932
33009
  if ("error" in resolvedVault)
32933
33010
  return usage(ctx, resolvedVault.error);
32934
- const { deps, json } = ctx;
32935
33011
  await printIdentity(ctx);
32936
33012
  return runVaultCommand(ctx, async ({ hold }) => {
32937
33013
  const raw = await requireVaultRaw(ctx, resolvedVault);
32938
33014
  const opened = await unlockInteractively(ctx, resolvedVault.path, raw, { acceptOlderCopy: true });
32939
- let vault = hold(opened.vault);
33015
+ const vault = hold(opened.vault);
32940
33016
  const entry = findEvmTee(vault, address);
32941
33017
  if (entry === undefined)
32942
33018
  return unknownWallet(ctx, address);
32943
- if (entry.linkedWalletId === undefined) {
32944
- writeLocalFailure(deps, { code: "TEE_WALLET_NOT_ENABLED", message: `${entry.address} was never enabled; there is nothing to stop.` }, json);
32945
- return 1;
33019
+ return (await disableEvmEntry(ctx, vault, hold, entry, false)).code;
33020
+ });
33021
+ }
33022
+ async function disableEvmEntry(ctx, opened, hold, entry, inDemote) {
33023
+ const { deps, json } = ctx;
33024
+ let vault = opened;
33025
+ if (entry.linkedWalletId === undefined) {
33026
+ writeLocalFailure(deps, { code: "TEE_WALLET_NOT_ENABLED", message: `${entry.address} was never enabled; there is nothing to stop.` }, json);
33027
+ return { code: 1, vault };
33028
+ }
33029
+ const linkedWalletId = entry.linkedWalletId;
33030
+ const stopRequestedAt = entry.tee?.stopRequestedAt ?? new Date(deps.now()).toISOString();
33031
+ vault = hold(await patchTee(ctx, vault, entry.id, (tee) => ({ ...tee, stopRequestedAt })));
33032
+ const unconfirmed = (detail, suggestion) => {
33033
+ if (json) {
33034
+ writeJson(deps, {
33035
+ ok: false,
33036
+ code: "STOP_UNCONFIRMED",
33037
+ message: detail,
33038
+ address: entry.address,
33039
+ linkedWalletId,
33040
+ stopRequestedAt,
33041
+ remoteEnforcement: "unconfirmed",
33042
+ suggestion
33043
+ });
33044
+ return { code: 1, vault };
32946
33045
  }
32947
- const linkedWalletId = entry.linkedWalletId;
32948
- const stopRequestedAt = entry.tee?.stopRequestedAt ?? new Date(deps.now()).toISOString();
32949
- vault = hold(await patchTee(ctx, vault, entry.id, (tee) => ({ ...tee, stopRequestedAt })));
32950
- const unconfirmed = (detail, suggestion) => {
32951
- if (json) {
32952
- writeJson(deps, {
32953
- ok: false,
32954
- code: "STOP_UNCONFIRMED",
32955
- message: detail,
32956
- address: entry.address,
32957
- linkedWalletId,
32958
- stopRequestedAt,
32959
- remoteEnforcement: "unconfirmed",
32960
- suggestion
32961
- });
32962
- return 1;
32963
- }
32964
- deps.stderr.write(`${detail}
33046
+ deps.stderr.write(`${detail}
32965
33047
  Stop intent recorded locally at ${stopRequestedAt}: this CLI will not fund ${entry.address} again. Remote enforcement is UNCONFIRMED: the agent may still trade until the server acknowledges the stop.
32966
33048
  ${suggestion}
32967
33049
  `);
32968
- return 1;
32969
- };
32970
- const apiKey = await resolveApiKey(deps, ctx.profile);
32971
- if (!apiKey) {
32972
- return unconfirmed("No API key available, so the server was not asked to stop the agent.", `Stop it from your Candle session (revoke linked wallet ${linkedWalletId}), or restore the key and re-run: candle tee disable ${entry.address}`);
32973
- }
32974
- const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(linkedWalletId)}`, {
32975
- method: "DELETE",
32976
- auth: "key",
32977
- credentials: { apiKey },
32978
- apiUrl: ctx.apiUrl,
32979
- fetch: deps.fetch,
32980
- env: deps.env
32981
- });
32982
- if (!result.ok) {
32983
- return unconfirmed(`The stop request failed: ${result.message ?? `HTTP ${result.status}`}.`, `Re-run: candle tee disable ${entry.address}. If the key is lost or revoked, stop it from your Candle session (revoke linked wallet ${linkedWalletId}).`);
32984
- }
32985
- const outcome = readDisableOutcome(result.body);
32986
- for (const warning of bridgeDisableWarnings(entry.address, result.body))
32987
- deps.stderr.write(`${warning}
33050
+ return { code: 1, vault };
33051
+ };
33052
+ const apiKey = await resolveApiKey(deps, ctx.profile);
33053
+ if (!apiKey) {
33054
+ return unconfirmed("No API key available, so the server was not asked to stop the agent.", `Stop it from your Candle session (revoke linked wallet ${linkedWalletId}), or restore the key and re-run: candle tee disable ${entry.address}`);
33055
+ }
33056
+ const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(linkedWalletId)}`, {
33057
+ method: "DELETE",
33058
+ auth: "key",
33059
+ credentials: { apiKey },
33060
+ apiUrl: ctx.apiUrl,
33061
+ fetch: deps.fetch,
33062
+ env: deps.env
33063
+ });
33064
+ if (!result.ok) {
33065
+ return unconfirmed(`The stop request failed: ${result.message ?? `HTTP ${result.status}`}.`, `Re-run: candle tee disable ${entry.address}. If the key is lost or revoked, stop it from your Candle session (revoke linked wallet ${linkedWalletId}).`);
33066
+ }
33067
+ const outcome = readDisableOutcome(result.body);
33068
+ for (const warning of bridgeDisableWarnings(entry.address, result.body))
33069
+ deps.stderr.write(`${warning}
32988
33070
  `);
32989
- if (json) {
32990
- writeJson(deps, { address: entry.address, linkedWalletId, ...result.body });
32991
- return outcome.complete ? 0 : 3;
32992
- }
32993
- if (outcome.complete) {
32994
- deps.stdout.write(`Stopped ${entry.address}. Remote signing denial verified; the wallet is quarantined.
33071
+ if (json) {
33072
+ writeJson(deps, { address: entry.address, linkedWalletId, ...result.body });
33073
+ return { code: outcome.complete ? 0 : 3, vault };
33074
+ }
33075
+ if (outcome.complete) {
33076
+ deps.stdout.write(`Stopped ${entry.address}. Remote signing denial verified; the wallet is quarantined.
32995
33077
  `);
33078
+ if (!inDemote)
32996
33079
  deps.stdout.write(`Recover the funds: candle tee sweep ${entry.address}
32997
33080
  `);
32998
- return 0;
32999
- }
33000
- deps.stdout.write(`Agent trading stopped at Candle for ${entry.address}. Remote policy verification is pending${outcome.reasonCode ? ` (${outcome.reasonCode})` : ""}.
33081
+ return { code: 0, vault };
33082
+ }
33083
+ deps.stdout.write(`Agent trading stopped at Candle for ${entry.address}. Remote policy verification is pending${outcome.reasonCode ? ` (${outcome.reasonCode})` : ""}.
33001
33084
  Funds remain in the TEE wallet and its TEE signing authority may still be active. Re-run: candle tee disable ${entry.address}
33002
- If the provider is down or theft is suspected: candle tee sweep ${entry.address} --emergency
33003
- `);
33004
- return 3;
33005
- });
33085
+ ${inDemote ? "" : `If the provider is down or theft is suspected: candle tee sweep ${entry.address} --emergency
33086
+ `}`);
33087
+ return { code: 3, vault };
33006
33088
  }
33007
33089
  async function vaultDemoteEvm(args, ctx) {
33008
33090
  const { rest, values: tokenFlags } = extractRepeated(args, "--token");
@@ -33017,6 +33099,9 @@ async function vaultDemoteEvm(args, ctx) {
33017
33099
  if (!address || extra !== undefined) {
33018
33100
  return usage(ctx, "Usage: candle vault demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>]");
33019
33101
  }
33102
+ const options = sweepOptions(ctx, parsed, tokenFlags);
33103
+ if (typeof options === "number")
33104
+ return options;
33020
33105
  if (!refuseEnvPassphrase(ctx))
33021
33106
  return 1;
33022
33107
  if (!requireTty(ctx, "vault demote"))
@@ -33026,13 +33111,13 @@ async function vaultDemoteEvm(args, ctx) {
33026
33111
  return usage(ctx, resolvedVault.error);
33027
33112
  const sweepTo = parsed.values["--sweep-to"];
33028
33113
  let emergency = parsed.booleans.has("--emergency");
33029
- const snapshot = await runVaultCommand(ctx, async ({ hold }) => {
33114
+ return runVaultCommand(ctx, async ({ hold }) => {
33030
33115
  const raw = await requireVaultRaw(ctx, resolvedVault);
33031
33116
  const opened = await unlockInteractively(ctx, resolvedVault.path, raw, {
33032
33117
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
33033
33118
  });
33034
- const vault = hold(opened.vault);
33035
- const entry = findEvmTee(vault, address);
33119
+ let vault = hold(opened.vault);
33120
+ let entry = findEvmTee(vault, address);
33036
33121
  if (entry === undefined)
33037
33122
  return unknownWallet(ctx, address);
33038
33123
  if (entry.tee?.vaultDestination === undefined) {
@@ -33045,39 +33130,40 @@ async function vaultDemoteEvm(args, ctx) {
33045
33130
  chain: "evm",
33046
33131
  acceptUnknownExposure: parsed.booleans.has("--accept-unknown-exposure")
33047
33132
  });
33048
- hold(await patchTee(ctx, vault, entry.id, (tee) => ({ ...tee, vaultDestination: destination.address })));
33133
+ vault = hold(await patchTee(ctx, vault, entry.id, (tee) => ({ ...tee, vaultDestination: destination.address })));
33134
+ entry = findEvmTee(vault, address);
33049
33135
  }
33050
33136
  if (entry.linkedWalletId === undefined) {
33051
33137
  ctx.deps.stdout.write(`No linkedWalletId is recorded for ${entry.address}; disable was not called and remote authority stays unknown.
33052
33138
  `);
33053
33139
  emergency = true;
33054
33140
  }
33055
- return 0;
33056
- });
33057
- if (snapshot !== 0)
33058
- return snapshot;
33059
- if (!emergency) {
33060
- const disableCode = await teeDisableEvm([address, ...keystoreArgs(parsed)], ctx);
33061
- if (disableCode !== 0 && disableCode !== 3)
33062
- return disableCode;
33063
- if (disableCode === 3) {
33064
- ctx.deps.stdout.write(`The disable is not yet verified; the sweep below will wait for quarantine. If the provider is down or theft is suspected, run: candle vault demote ${address} --emergency
33141
+ if (!emergency) {
33142
+ await printIdentity(ctx);
33143
+ const disabled = await disableEvmEntry(ctx, vault, hold, entry, true);
33144
+ if (disabled.code !== 0 && disabled.code !== 3)
33145
+ return disabled.code;
33146
+ vault = disabled.vault;
33147
+ entry = findEvmTee(vault, address);
33148
+ if (disabled.code === 3) {
33149
+ ctx.deps.stdout.write(`The disable is not yet verified; the sweep below will wait for quarantine. If the provider is down or theft is suspected, run: candle vault demote ${address} --emergency
33065
33150
  `);
33151
+ }
33066
33152
  }
33067
- }
33068
- const sweepArgs = [address, ...keystoreArgs(parsed)];
33069
- if (parsed.values["--rpc-url"] !== undefined)
33070
- sweepArgs.push("--rpc-url", parsed.values["--rpc-url"]);
33071
- if (parsed.values["--from-block"] !== undefined)
33072
- sweepArgs.push("--from-block", parsed.values["--from-block"]);
33073
- for (const token of tokenFlags)
33074
- sweepArgs.push("--token", token);
33075
- if (emergency)
33076
- sweepArgs.push("--emergency");
33077
- return teeSweepEvm(sweepArgs, ctx);
33078
- }
33079
- function keystoreArgs(parsed) {
33080
- return parsed.values["--keystore"] !== undefined ? ["--keystore", parsed.values["--keystore"]] : [];
33153
+ if (!ctx.json)
33154
+ ctx.deps.stdout.write(`Sweeping remaining funds to ${entry.tee?.vaultDestination}...
33155
+ `);
33156
+ return sweepEvmWallet({
33157
+ ctx,
33158
+ vault,
33159
+ hold,
33160
+ entry,
33161
+ client: options.client,
33162
+ emergency,
33163
+ tokenFlags: options.tokens,
33164
+ fromBlock: options.fromBlock
33165
+ });
33166
+ });
33081
33167
  }
33082
33168
  var MAX_TOKEN_TRANSFER_GAS = 1000000n;
33083
33169
  var init_tee_evm = __esm(() => {
@@ -33445,6 +33531,9 @@ async function commitTee(deps, opened, mutate, opts = {}) {
33445
33531
  };
33446
33532
  }
33447
33533
  }
33534
+ function vaultActiveTee(ctx, resolved) {
33535
+ return { source: "vault", resolved, entry: resolved.legacyView, path: "vault", ctx };
33536
+ }
33448
33537
  function applyKeystoreViewToVaultEntry(entry, view) {
33449
33538
  if (view.linkedWalletId !== undefined)
33450
33539
  entry.linkedWalletId = view.linkedWalletId;
@@ -33499,18 +33588,8 @@ async function openActiveTee(ctx, parsed, address, access3) {
33499
33588
  const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed), access3);
33500
33589
  if (!resolved.ok)
33501
33590
  return { ok: false, code: resolved.code };
33502
- if (resolved.resolved.source === "vault") {
33503
- return {
33504
- ok: true,
33505
- active: {
33506
- source: "vault",
33507
- resolved: resolved.resolved,
33508
- entry: resolved.resolved.legacyView,
33509
- path: "vault",
33510
- ctx
33511
- }
33512
- };
33513
- }
33591
+ if (resolved.resolved.source === "vault")
33592
+ return { ok: true, active: vaultActiveTee(ctx, resolved.resolved) };
33514
33593
  return {
33515
33594
  ok: true,
33516
33595
  active: {
@@ -34120,7 +34199,7 @@ async function teeStatus(args, ctx) {
34120
34199
  releaseResolvedTee(local);
34121
34200
  }
34122
34201
  }
34123
- async function teeDisable(args, ctx) {
34202
+ async function teeDisable(args, ctx, demote) {
34124
34203
  const { deps, apiUrl, json } = ctx;
34125
34204
  if (namesEvmWallet(args))
34126
34205
  return teeDisableEvm(args, ctx);
@@ -34133,7 +34212,7 @@ async function teeDisable(args, ctx) {
34133
34212
  if (!address || extra !== undefined)
34134
34213
  return usage2(ctx, "Usage: candle tee disable <address>");
34135
34214
  await printIdentity(ctx);
34136
- const openedActive = await openActiveTee(ctx, parsed, address, "read");
34215
+ const openedActive = demote ? { ok: true, active: vaultActiveTee(ctx, demote.resolved) } : await openActiveTee(ctx, parsed, address, "read");
34137
34216
  if (!openedActive.ok)
34138
34217
  return openedActive.code;
34139
34218
  const { active } = openedActive;
@@ -34202,17 +34281,37 @@ ${suggestion}
34202
34281
  if (outcome.complete) {
34203
34282
  deps.stdout.write(`Stopped ${address}. Remote signing denial verified; the wallet is quarantined.
34204
34283
  `);
34205
- deps.stdout.write(`Recover the funds: candle tee sweep ${address}
34284
+ if (!demote)
34285
+ deps.stdout.write(`Recover the funds: candle tee sweep ${address}
34206
34286
  `);
34207
34287
  return 0;
34208
34288
  }
34209
34289
  deps.stdout.write(`Agent trading stopped at Candle for ${address}. Remote policy verification is pending${outcome.reasonCode ? ` (${outcome.reasonCode})` : ""}.
34210
34290
  Funds remain in the TEE wallet and its TEE signing authority may still be active. Re-run: candle tee disable ${address}
34211
- If the provider is down or theft is suspected: candle tee sweep ${address} --emergency
34291
+ If the provider is down or theft is suspected: ${demote ? `candle vault demote ${address}` : `candle tee sweep ${address}`} --emergency
34212
34292
  `);
34213
34293
  return 3;
34214
34294
  } finally {
34215
- releaseActiveTee(active);
34295
+ if (!demote)
34296
+ releaseActiveTee(active);
34297
+ }
34298
+ }
34299
+ async function nothingLeftToSweep(solana, entry) {
34300
+ const tee = entry.tee;
34301
+ if ((tee?.sweepPending?.length ?? 0) > 0)
34302
+ return false;
34303
+ if ((tee?.sweepReceipts?.length ?? 0) > 0 && tee?.sweptAt === undefined)
34304
+ return false;
34305
+ try {
34306
+ if (await solana.rpc.getBalance(entry.address) > 0n)
34307
+ return false;
34308
+ for (const programId of [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]) {
34309
+ if ((await solana.rpc.getTokenAccountsByOwner(entry.address, programId)).length > 0)
34310
+ return false;
34311
+ }
34312
+ return true;
34313
+ } catch {
34314
+ return false;
34216
34315
  }
34217
34316
  }
34218
34317
  function refusalState(raw) {
@@ -34320,7 +34419,7 @@ async function broadcastMessage(rpc, ctx, secret, message, blockhash, pending, r
34320
34419
  error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS * CONFIRM_POLL_MS / 1000}s; it may still land${echoNote}`
34321
34420
  };
34322
34421
  }
34323
- async function teeSweep(args, ctx) {
34422
+ async function teeSweep(args, ctx, demote) {
34324
34423
  const { deps, apiUrl, json } = ctx;
34325
34424
  if (namesEvmWallet(args))
34326
34425
  return teeSweepEvm(args, ctx);
@@ -34336,11 +34435,24 @@ async function teeSweep(args, ctx) {
34336
34435
  const [address, extra] = parsed.positionals;
34337
34436
  if (!address || extra !== undefined)
34338
34437
  return usage2(ctx, "Usage: candle tee sweep <address> [--rpc-url <url>] [--emergency]");
34339
- const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
34438
+ const solana = demote ? demote.solana : await openSolanaClient(ctx, parsed.values["--rpc-url"]);
34340
34439
  if ("error" in solana)
34341
34440
  return usage2(ctx, solana.error);
34342
34441
  const emergency = parsed.booleans.has("--emergency");
34343
- const openedActive = await openActiveTee(ctx, parsed, address, "sign");
34442
+ let openedActive;
34443
+ if (demote) {
34444
+ try {
34445
+ await grantSignAccess(demote.resolved);
34446
+ } catch (error) {
34447
+ if (!isVaultError(error))
34448
+ throw error;
34449
+ writeLocalFailure(deps, { code: error.code, message: error.message }, json);
34450
+ return error.exitCode;
34451
+ }
34452
+ openedActive = { ok: true, active: vaultActiveTee(ctx, demote.resolved) };
34453
+ } else {
34454
+ openedActive = await openActiveTee(ctx, parsed, address, "sign");
34455
+ }
34344
34456
  if (!openedActive.ok)
34345
34457
  return openedActive.code;
34346
34458
  const { active } = openedActive;
@@ -35124,7 +35236,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
35124
35236
  `);
35125
35237
  return 3;
35126
35238
  } finally {
35127
- releaseActiveTee(active);
35239
+ if (!demote)
35240
+ releaseActiveTee(active);
35128
35241
  }
35129
35242
  }
35130
35243
  var MIN_PASSPHRASE_LENGTH = 12, USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", CONFIRM_POLL_MS = 2000, CONFIRM_MAX_POLLS = 45, StoreChangedError, TOKEN_ACCOUNT_STATE_OFFSET = 108, TOKEN_ACCOUNT_STATE_FROZEN = 2;
@@ -35139,6 +35252,7 @@ var init_tee = __esm(() => {
35139
35252
  init_solana_endpoint();
35140
35253
  init_solana_lite();
35141
35254
  init_token_2022();
35255
+ init_errors();
35142
35256
  init_store();
35143
35257
  init_tee_resolve();
35144
35258
  init_wallet_import_flow();
@@ -50758,7 +50872,7 @@ var init_convert = __esm(() => {
50758
50872
  });
50759
50873
 
50760
50874
  // ../mcp/src/version.ts
50761
- var SERVER_VERSION = "0.10.6";
50875
+ var SERVER_VERSION = "0.10.7";
50762
50876
 
50763
50877
  // ../mcp/src/update-notice.ts
50764
50878
  function newer(a, b) {
@@ -52063,7 +52177,7 @@ function planTableMarkdown(table) {
52063
52177
  return [line(headers2), line(headers2.map(() => "---")), ...rows.map(line)].join(`
52064
52178
  `);
52065
52179
  }
52066
- var PLAN_CAPABILITY_NOTE2 = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.", PLAN_CAPABILITY_LABELS2, PLAN_LABELS2;
52180
+ var PLAN_CAPABILITY_NOTE2 = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs. Every plan can launch a token from its embedded wallet, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the headless launch; the two launch rows are additional routes, not the only ones.", PLAN_CAPABILITY_LABELS2, PLAN_LABELS2;
52067
52181
  var init_plans = __esm(() => {
52068
52182
  PLAN_CAPABILITY_LABELS2 = {
52069
52183
  tradeCandleTokens: "Trade Candle-launched tokens",
@@ -52072,8 +52186,8 @@ var init_plans = __esm(() => {
52072
52186
  sellExternalTokens: "Sell tokens not launched on Candle",
52073
52187
  buyExternalTokens: "Buy tokens not launched on Candle",
52074
52188
  hyperliquidPerps: "Hyperliquid perps (when enabled)",
52075
- selfLaunch: "Self-launch from a linked wallet",
52076
- atomicLaunch: "Atomic launch with first buys",
52189
+ selfLaunch: "Launch from a linked or TEE wallet (self-signed)",
52190
+ atomicLaunch: "Atomic launch: launch + 1–4 first buys in one bundle",
52077
52191
  createLinkedWallets: "Create linked wallets",
52078
52192
  importLinkedWallets: "Import linked wallets",
52079
52193
  limitOrders: "Limit orders",
@@ -52288,7 +52402,7 @@ function registerTools(server, env = process.env) {
52288
52402
  };
52289
52403
  register("candle_launch_token", {
52290
52404
  title: "Launch a token on Candle",
52291
- description: "Launch a new token via the Candle headless launch API. Set dryRun: true to validate without spending anything.",
52405
+ description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything.",
52292
52406
  inputSchema: launchTokenShape
52293
52407
  }, async (args) => callAndRelay("candle_launch_token", args, cfg));
52294
52408
  register("candle_get_market", {
@@ -52454,7 +52568,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
52454
52568
  });
52455
52569
  register("candle_launch_and_seed", {
52456
52570
  title: "Launch a token and seed it",
52457
- description: "Launch a new token with an optional dev-buy seed bundled into the launch itself, then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade.",
52571
+ description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed (in the " + "launch transaction on solana, a best-effort follow-up on hood). Works on every plan, Free included (this is not the atomic launch). Then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade.",
52458
52572
  inputSchema: launchAndSeedShape
52459
52573
  }, async (args) => {
52460
52574
  const result = await executeLaunchAndSeed(args, cfg, fetch);
@@ -52631,7 +52745,7 @@ var init_tools = __esm(() => {
52631
52745
  launchAndSeedShape = {
52632
52746
  ...seedableLaunchShape,
52633
52747
  clientLaunchId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
52634
- devBuy: exports_external.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
52748
+ devBuy: exports_external.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself on solana and sent as a follow-up " + "transaction on hood. Paid from the account's embedded wallet. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
52635
52749
  };
52636
52750
  transferShape = {
52637
52751
  chain: exports_external.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
@@ -52770,7 +52884,7 @@ init_render();
52770
52884
  init_secret_store();
52771
52885
 
52772
52886
  // src/version.ts
52773
- var CLI_VERSION = "0.11.16";
52887
+ var CLI_VERSION = "0.11.18";
52774
52888
 
52775
52889
  // src/commands/auth.ts
52776
52890
  init_keys_embedded_wallet();
@@ -52794,7 +52908,7 @@ async function authLogin(args, ctx) {
52794
52908
  }
52795
52909
  const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
52796
52910
  if (scopes?.includes("transfer:bound")) {
52797
- writeUsageFailure(deps, "transfer:bound is not available on a device login. Mint a Read:Write:Transfer key with: candle keys create --access read-write-transfer, or widen an existing key with: candle keys access <prefix> --access read-write-transfer", json);
52911
+ writeUsageFailure(deps, "transfer:bound is not available on a device login. Mint a Withdraw key with: candle keys create --access withdraw, or widen an existing key with: candle keys access <prefix> --access withdraw", json);
52798
52912
  return 2;
52799
52913
  }
52800
52914
  const label = parsed.values["--label"]?.trim();
@@ -53282,7 +53396,7 @@ var HELP = {
53282
53396
  swap: {
53283
53397
  group: "Trade",
53284
53398
  summary: "Quote, confirm and swap on Solana or Hood, bridge between them; read an operation by id",
53285
- description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent, and the first buy after a launch is this command rather than part of the launch. 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, the key needs a raw cap on the origin asset, 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.",
53399
+ 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, the key needs a raw cap on the origin asset, 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.",
53286
53400
  usage: ["candle swap <from> <to> [flags]", "candle swap status <id>"],
53287
53401
  rows: [
53288
53402
  {
@@ -53407,7 +53521,7 @@ var HELP = {
53407
53521
  transfer: {
53408
53522
  group: "Trade",
53409
53523
  summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
53410
- description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Read:Write:Transfer key (transfer:bound); from that wallet, Candle allows only its own pinned vault or another of the account's wallets you linked while signed in or marked trusted. To a linked wallet the amount counts against the key's spend caps and must be a base asset; to the vault any token and max are allowed. Candle builds the transaction, this machine approves the relay, Privy signs, Candle broadcasts. The destination and its kind are shown before you confirm. 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.",
53524
+ 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 the amount counts against the key's spend caps and must be a base asset; to the vault any token and max are allowed. Candle builds the transaction, this machine approves the relay, Privy signs, Candle broadcasts. The destination and its kind are shown before you confirm. 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.",
53411
53525
  usage: [
53412
53526
  "candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
53413
53527
  ],
@@ -53440,10 +53554,10 @@ var HELP = {
53440
53554
  },
53441
53555
  launch: {
53442
53556
  group: "Trade",
53443
- summary: "Create a token on Solana or Hood (the first buy is a separate swap)",
53444
- description: "Creates a token with no first buy, so the launch and the position are two decisions rather than one. Needs the launch:write scope and an operator-enabled allowLaunch. The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans).",
53557
+ summary: "Create a token on Solana or Hood, with an optional first buy",
53558
+ description: "Creates a token from a TEE wallet. Without --buy it makes no first buy, so the launch and the position are two decisions, and the buy is a separate candle swap. --buy <amount> adds a first buy from the same wallet, in whole units of the quote asset (SOL, USDC or CNDL on Solana, ETH on Hood; not USDG), within the dev-buy ceilings: on Solana it rides in the launch transaction, and on Hood it is a last leg after the curve and the fee. Candle reserves the buy against the key's spend window before anything is signed. --buy needs a server with TEE_LAUNCH_BUYS_ENABLED on, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs the launch:write scope and allowLaunch on the wallet, which only the account owner can turn on (candle wallets allow-launch <wallet>, over the device token; an agent key cannot). The wallet decides the chain: a Hood TEE wallet launches on Hood, needs --dex-version, and signs one leg at a time (the curve, then the fee, then the buy with --buy), each only after the one before it landed. Launching from a TEE wallet needs Pro or Max (TIER_REQUIRED otherwise; candle plans). Every plan, Free included, can launch from its embedded wallet with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the MCP tool candle_launch_and_seed or the SDK's launch(); this command does not use that path.",
53445
53559
  usage: [
53446
- "candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--quote-asset <asset>] [--dex-version v3|v4]"
53560
+ "candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3|v4]"
53447
53561
  ],
53448
53562
  rows: [],
53449
53563
  flags: [
@@ -53451,6 +53565,10 @@ var HELP = {
53451
53565
  { invocation: "--symbol <symbol>", description: "The token's ticker" },
53452
53566
  { invocation: "--image-url <url>", description: "The token image, already hosted" },
53453
53567
  { invocation: "--wallet <tee>", description: "The TEE wallet that creates it" },
53568
+ {
53569
+ invocation: "--buy <amount>",
53570
+ description: "A first buy from the same wallet, in whole units of the quote asset; needs TEE_LAUNCH_BUYS_ENABLED on the server"
53571
+ },
53454
53572
  { invocation: "--quote-asset <asset>", description: "sol, usdc or cndl on Solana; eth or usdg on Hood" },
53455
53573
  {
53456
53574
  invocation: "--dex-version v3|v4",
@@ -53459,7 +53577,8 @@ var HELP = {
53459
53577
  ],
53460
53578
  examples: [
53461
53579
  "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne",
53462
- "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4"
53580
+ "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet HoodAgent --dex-version v4",
53581
+ "candle launch --name Demo --symbol DEMO --image-url https://example.com/d.png --wallet AgentOne --buy 0.1"
53463
53582
  ],
53464
53583
  env: ENV_API
53465
53584
  },
@@ -53520,14 +53639,14 @@ var HELP = {
53520
53639
  rows: [
53521
53640
  {
53522
53641
  invocation: "list [--scopes]",
53523
- description: "List API keys: name and Read, Read:Write or Read:Write:Transfer access; --scopes adds the raw scopes"
53642
+ description: "List API keys: name and Read, Write or Withdraw access; --scopes adds the raw scopes"
53524
53643
  },
53525
53644
  {
53526
- invocation: "create [--access read|read-write|read-write-transfer | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]] [--embedded-wallet deny|allow]",
53527
- description: "Create an API key; --access mints one of the three levels (read-write-transfer can move funds out of the wallet it runs). --embedded-wallet defaults to deny; allow lets the key spend the account's embedded wallet and is confirmed at a terminal"
53645
+ invocation: "create [--access read|write|withdraw | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]] [--embedded-wallet deny|allow]",
53646
+ description: "Create an API key; --access mints one of the three levels (withdraw can move funds out of the wallet it runs). --embedded-wallet defaults to deny; allow lets the key spend the account's embedded wallet and is confirmed at a terminal"
53528
53647
  },
53529
53648
  {
53530
- invocation: "access <prefix|label|self> (--access read|read-write|read-write-transfer [--yes] | --history)",
53649
+ invocation: "access <prefix|label|self> (--access read|write|withdraw [--yes] | --history)",
53531
53650
  description: "Change an existing key's level in place (same key, wallets and caps). Widening needs the device token and the prefix typed back at a terminal; --yes skips the prompt when narrowing; self narrows the profile's own key. --history lists the key's changes and who made them"
53532
53651
  },
53533
53652
  {
@@ -53562,9 +53681,9 @@ var HELP = {
53562
53681
  ],
53563
53682
  examples: [
53564
53683
  "candle keys list",
53565
- "candle keys create --access read-write-transfer --label rebalancer",
53684
+ "candle keys create --access withdraw --label rebalancer",
53566
53685
  "candle keys create --scopes trade:write --label agent-one",
53567
- "candle keys access cndl --access read-write-transfer",
53686
+ "candle keys access cndl --access withdraw",
53568
53687
  "candle keys access self --access read --yes",
53569
53688
  "candle keys update agent-one --embedded-wallet deny --yes",
53570
53689
  "candle keys self embedded-wallet deny",
@@ -53596,6 +53715,14 @@ var HELP = {
53596
53715
  invocation: "untrust <label|address|id|prefix*>... [--yes]",
53597
53716
  description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
53598
53717
  },
53718
+ {
53719
+ invocation: "allow-launch <label|address|id|prefix*>...",
53720
+ description: "Let TEE wallets pay for a launch (candle launch), after a screen of label, address and bound key (owner only; typed confirm)"
53721
+ },
53722
+ {
53723
+ invocation: "disallow-launch <label|address|id|prefix*>... [--yes]",
53724
+ description: "Stop TEE wallets paying for a launch; trading is unchanged (owner only)"
53725
+ },
53599
53726
  {
53600
53727
  invocation: "close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes]",
53601
53728
  description: "Close the embedded wallet's empty token accounts and return their rent to it, after a preview (transfer:write)"
@@ -53606,6 +53733,7 @@ var HELP = {
53606
53733
  "candle wallet import --chain solana --key-file ./signer.json",
53607
53734
  "candle wallet revoke wal_123",
53608
53735
  "candle wallet trust 'tr-*' 'dest-*'",
53736
+ "candle wallet allow-launch launcher-1",
53609
53737
  "candle wallet close-empty --keep EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
53610
53738
  ],
53611
53739
  env: ENV_API
@@ -59348,10 +59476,14 @@ init_promote_support();
59348
59476
  init_keys();
59349
59477
  init_tee_rebind();
59350
59478
  var KEYS_PATH4 = "/api/v1/agent/keys";
59351
- var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|read-write|read-write-transfer> [--yes] [--json]
59479
+ var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|write|withdraw> [--yes] [--json]
59352
59480
  ` + " candle keys access <prefix|label> --history [--json]";
59353
59481
  var SELF = "self";
59354
- var CLI_SPELLING = Object.fromEntries(Object.entries(ACCESS_LEVELS).map(([spelling, preset]) => [preset, spelling]));
59482
+ var CLI_SPELLING = {
59483
+ read: "read",
59484
+ readwrite: "write",
59485
+ readwritetransfer: "withdraw"
59486
+ };
59355
59487
  var NO_API_KEY2 = {
59356
59488
  code: "API_KEY_REQUIRED",
59357
59489
  message: "candle keys access self changes the profile's own API key, and this profile holds none.",
@@ -59496,7 +59628,7 @@ async function keysAccess(args, ctx) {
59496
59628
  }
59497
59629
  const preset = ACCESS_LEVELS[accessFlag];
59498
59630
  if (preset === undefined) {
59499
- writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
59631
+ writeUsageFailure(deps, "--access must be one of: read, write, withdraw.", json);
59500
59632
  return 2;
59501
59633
  }
59502
59634
  await printIdentity(ctx);
@@ -59851,7 +59983,7 @@ init_solana_lite();
59851
59983
  init_trading();
59852
59984
  init_swap();
59853
59985
  import { randomUUID as randomUUID2 } from "node:crypto";
59854
- var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image-url <https-url> --wallet <tee> [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
59986
+ var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image-url <https-url> --wallet <tee> [--buy <amount>] [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
59855
59987
  var QUOTE_CHAIN = {
59856
59988
  sol: "solana",
59857
59989
  usdc: "solana",
@@ -59860,6 +59992,34 @@ var QUOTE_CHAIN = {
59860
59992
  usdg: "hood"
59861
59993
  };
59862
59994
  var HOOD_LAUNCH_LEGS = ["createCurve", "feeTransfer"];
59995
+ var HOOD_LAUNCH_BUY_LEGS = ["createCurve", "feeTransfer", "trade"];
59996
+ function launchBuy(flags, chain2) {
59997
+ const amount = flags["--buy"];
59998
+ if (amount === undefined)
59999
+ return;
60000
+ const quote = (flags["--quote-asset"] ?? (chain2 === "hood" ? "eth" : "sol")).toUpperCase();
60001
+ const decimals = chain2 === "hood" ? HOOD_BASES[quote]?.decimals : BASES[quote]?.decimals;
60002
+ if (decimals === undefined)
60003
+ throw new TradingError("INVALID_AMOUNT", `--buy is in the launch's quote asset, and ${safeText(quote)} is not one on ${chainName(chain2)}. Nothing was built.`);
60004
+ const raw = rawAmount(amount, decimals);
60005
+ return { raw, display: `${decimalAmount(raw, decimals)} ${quote}` };
60006
+ }
60007
+ function buyRefusal(error) {
60008
+ if (!(error instanceof TradingError) || error.code !== "VALIDATION_FAILED" || error.details?.field !== "buyAmount")
60009
+ return error;
60010
+ return new TradingError("VALIDATION_FAILED", `Candle refused the first buy on this TEE launch: ${safeText(error.message)}. A TEE wallet's launch takes --buy only where the server has TEE_LAUNCH_BUYS_ENABLED on, and it is off on this deployment. Nothing was built or signed.`, {
60011
+ suggestion: "Launch without --buy under a new --client-trade-id, then make the first buy with candle swap once the token lands.",
60012
+ details: error.details
60013
+ });
60014
+ }
60015
+ function buyNotice(buy, chain2) {
60016
+ if (!buy)
60017
+ return `Launch creates the token only. Make the first buy with a separate candle swap. Price impact does not apply to creation.
60018
+ `;
60019
+ return chain2 === "hood" ? `Launch creates the token, then spends ${buy.display} on a first buy on the new curve, as the last leg. Candle reserves the buy against this key's spend window before any leg is signed. If the buy leg never lands, the token exists with no first buy. Price impact does not apply to creation.
60020
+ ` : `Launch creates the token and spends ${buy.display} on a first buy in the same transaction. Candle reserves the whole debit against this key's spend window before anything is signed. Price impact does not apply to creation.
60021
+ `;
60022
+ }
59863
60023
  function flagChain(flags) {
59864
60024
  const said = [];
59865
60025
  const named = walletNameChain(flags["--wallet"] ?? "");
@@ -59889,7 +60049,8 @@ async function launch(args, ctx) {
59889
60049
  "--rpc-url",
59890
60050
  "--quote-asset",
59891
60051
  "--mode",
59892
- "--dex-version"
60052
+ "--dex-version",
60053
+ "--buy"
59893
60054
  ],
59894
60055
  booleanFlags: ["--yes"]
59895
60056
  });
@@ -59904,6 +60065,8 @@ async function launch(args, ctx) {
59904
60065
  return 2;
59905
60066
  }
59906
60067
  try {
60068
+ if (flags["--buy"] !== undefined)
60069
+ rawAmount(flags["--buy"], 18);
59907
60070
  const hinted = flagChain(flags);
59908
60071
  if (hinted === "hood" && flags["--dex-version"] === undefined) {
59909
60072
  writeUsageFailure(ctx.deps, `A Hood launch needs --dex-version v3|v4. ${USAGE2}`, ctx.json);
@@ -59929,8 +60092,10 @@ async function launch(args, ctx) {
59929
60092
  if (wallet.chain === "evm") {
59930
60093
  if (flags["--dex-version"] === undefined)
59931
60094
  throw new TradingUsage(`${wallet.address} is a Hood TEE wallet, and a Hood launch needs --dex-version v3|v4.`);
59932
- return await hoodLaunch(ctx, key, { flags, id, wallet, yes: parsed.booleans.has("--yes") });
60095
+ const buy2 = launchBuy(flags, "hood");
60096
+ return await hoodLaunch(ctx, key, { flags, id, wallet, buy: buy2, yes: parsed.booleans.has("--yes") });
59933
60097
  }
60098
+ const buy = launchBuy(flags, "solana");
59934
60099
  const solana = await tradingSolanaClient(ctx, flags["--rpc-url"]);
59935
60100
  if (!await claimOperation(ctx, key, id, "launch"))
59936
60101
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this launch id; no write was resent.");
@@ -59939,7 +60104,7 @@ async function launch(args, ctx) {
59939
60104
  const built = launchBuildSchema.parse(await request(ctx, key, "/api/v1/launch/self/build", {
59940
60105
  clientLaunchId: id,
59941
60106
  chain: "solana",
59942
- buyAmount: 0,
60107
+ buyAmount: buy ? buy.raw : 0,
59943
60108
  name: flags["--name"],
59944
60109
  symbol: flags["--symbol"],
59945
60110
  imageUrl: flags["--image-url"],
@@ -59947,21 +60112,22 @@ async function launch(args, ctx) {
59947
60112
  ...flags["--description"] ? { description: flags["--description"] } : {},
59948
60113
  ...flags["--quote-asset"] ? { quoteAsset: flags["--quote-asset"] } : {},
59949
60114
  ...flags["--mode"] ? { mode: flags["--mode"] } : {}
60115
+ }).catch((error) => {
60116
+ throw buyRefusal(error);
59950
60117
  }));
59951
60118
  if (!built.transaction || !/^\d+$/.test(built.maxDebitLamports ?? ""))
59952
60119
  throw new TradingError("INVALID_RESPONSE", "Candle did not return a launch transaction and maximum debit; nothing was signed.");
59953
60120
  const quote = {
59954
- intent: `Launch ${flags["--name"]} (${flags["--symbol"]})`,
60121
+ intent: `Launch ${flags["--name"]} (${flags["--symbol"]})${buy ? ` with a first buy of ${buy.display}` : ""}`,
59955
60122
  wallet: wallet.address,
59956
60123
  venue: "curve launch",
59957
60124
  priceImpactPct: null,
59958
60125
  fee: built.fee ?? { bps: 0, feeRaw: "0" },
59959
- minimumReceived: "0 tokens (no first buy)",
60126
+ minimumReceived: buy ? "not quoted (the first buy on the new curve)" : "0 tokens (no first buy)",
59960
60127
  maxDebitLamports: built.maxDebitLamports,
59961
60128
  tokenRisks: []
59962
60129
  };
59963
- ctx.deps.stderr.write(`Launch creates the token only. Make the first buy with a separate candle swap. Price impact does not apply to creation.
59964
- `);
60130
+ ctx.deps.stderr.write(buyNotice(buy, "solana"));
59965
60131
  if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
59966
60132
  return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind: "launch", quote });
59967
60133
  if (!Number.isFinite(built.expiresAt) || built.expiresAt <= ctx.deps.now())
@@ -59987,7 +60153,8 @@ async function launch(args, ctx) {
59987
60153
  }
59988
60154
  }
59989
60155
  async function hoodLaunch(ctx, key, args) {
59990
- const { flags, id, wallet } = args;
60156
+ const { flags, id, wallet, buy } = args;
60157
+ const allowedLegs = buy ? HOOD_LAUNCH_BUY_LEGS : HOOD_LAUNCH_LEGS;
59991
60158
  if (!await claimOperation(ctx, key, id, "launch"))
59992
60159
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this launch id; no write was resent.");
59993
60160
  ctx.deps.stderr.write(`Operation: ${id}
@@ -59995,7 +60162,7 @@ async function hoodLaunch(ctx, key, args) {
59995
60162
  const built = await request(ctx, key, "/api/v1/launch/self/build", {
59996
60163
  clientLaunchId: id,
59997
60164
  chain: "hood",
59998
- buyAmount: 0,
60165
+ buyAmount: buy ? buy.raw : 0,
59999
60166
  name: flags["--name"],
60000
60167
  symbol: flags["--symbol"],
60001
60168
  imageUrl: flags["--image-url"],
@@ -60004,6 +60171,8 @@ async function hoodLaunch(ctx, key, args) {
60004
60171
  ...flags["--description"] ? { description: flags["--description"] } : {},
60005
60172
  ...flags["--quote-asset"] ? { quoteAsset: flags["--quote-asset"].toLowerCase() } : {},
60006
60173
  ...flags["--mode"] ? { mode: flags["--mode"] } : {}
60174
+ }).catch((error) => {
60175
+ throw buyRefusal(error);
60007
60176
  });
60008
60177
  const parsed = sequencedSchema.safeParse(built);
60009
60178
  if (!parsed.success)
@@ -60012,26 +60181,25 @@ async function hoodLaunch(ctx, key, args) {
60012
60181
  const curve = typeof built.curveAddress === "string" ? checkEvmAddress(built.curveAddress) : undefined;
60013
60182
  if (built.chain !== "hood" || built.clientLaunchId !== id || typeof built.walletAddress !== "string" || !sameEvmAddress(built.walletAddress, wallet.address) || !curve?.ok)
60014
60183
  throw new TradingError("INVALID_RESPONSE", "The Hood launch build does not name this launch and payer; nothing was signed.");
60015
- if (first.legKind !== "createCurve" || first.plannedLegCount > HOOD_LAUNCH_LEGS.length)
60184
+ if (first.legKind !== "createCurve" || first.plannedLegCount > allowedLegs.length || buy !== undefined && first.plannedLegCount < 2)
60016
60185
  throw new TradingError("INVALID_RESPONSE", `A Hood launch starts with its createCurve leg, and Candle offered ${safeText(first.legKind)} of ${first.plannedLegCount}; nothing was signed.`);
60017
60186
  const leg = first.nextLeg;
60018
60187
  const maxFee = BigInt(leg.maxFeePerGas);
60019
60188
  const reserve = sweepReserveFloor(maxFee);
60020
60189
  const quote = {
60021
- intent: `Launch ${flags["--name"]} (${flags["--symbol"]}) on Hood`,
60190
+ intent: `Launch ${flags["--name"]} (${flags["--symbol"]}) on Hood${buy ? ` with a first buy of ${buy.display}` : ""}`,
60022
60191
  wallet: wallet.address,
60023
60192
  venue: "curve launch",
60024
60193
  priceImpactPct: null,
60025
- minimumReceived: "0 tokens (no first buy)",
60194
+ minimumReceived: buy ? "not quoted (Candle's buy leg sets its floor on the new curve)" : "0 tokens (no first buy)",
60026
60195
  tokenRisks: [],
60027
- legs: first.plannedLegCount === 2 ? ["create curve", "fee"] : ["create curve"],
60028
- gas: `the createCurve leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)${first.plannedLegCount === 2 ? "; the fee leg is priced by Candle when it becomes next" : ""}`,
60196
+ legs: hoodLaunchLegNames(first.plannedLegCount, buy !== undefined),
60197
+ gas: `the createCurve leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei)${hoodLaterLegsGas(first.plannedLegCount, buy !== undefined)}`,
60029
60198
  reserve: `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee)`,
60030
60199
  curveAddress: curve.address,
60031
60200
  operationId: first.operationId
60032
60201
  };
60033
- ctx.deps.stderr.write(`Launch creates the token only. Make the first buy with a separate candle swap. Price impact does not apply to creation.
60034
- `);
60202
+ ctx.deps.stderr.write(buyNotice(buy, "hood"));
60035
60203
  if (!await confirmQuote(ctx, quote, args.yes)) {
60036
60204
  ctx.deps.stderr.write(`Nothing was signed. Operation ${first.operationId} holds this wallet until ${new Date(first.expiresAt).toISOString()}; a new Hood operation from it is refused as WALLET_BUSY until then.
60037
60205
  `);
@@ -60046,6 +60214,8 @@ async function hoodLaunch(ctx, key, args) {
60046
60214
  walletHeldUntil: first.expiresAt
60047
60215
  });
60048
60216
  }
60217
+ const confirmedPlan = buy ? first.plannedLegCount === 3 ? ["createCurve", "feeTransfer", "trade"] : ["createCurve", "trade"] : undefined;
60218
+ let position = 0;
60049
60219
  const run2 = await runSequencedLegs(ctx, key, {
60050
60220
  wallet,
60051
60221
  first,
@@ -60055,10 +60225,16 @@ async function hoodLaunch(ctx, key, args) {
60055
60225
  clientId: id,
60056
60226
  kind: "launch",
60057
60227
  primaryLeg: "createCurve",
60058
- allowedLegs: HOOD_LAUNCH_LEGS,
60059
- checkLeg: (kind, leg2) => kind !== "createCurve" || leg2.value === "0",
60228
+ allowedLegs,
60229
+ checkLeg: (kind, leg2) => {
60230
+ if (confirmedPlan && confirmedPlan[position++] !== kind)
60231
+ return false;
60232
+ return kind === "createCurve" ? leg2.value === "0" : kind !== "trade" || buy !== undefined && leg2.value === buy.raw && sameEvmAddress(leg2.to, curve.address);
60233
+ },
60060
60234
  onLanded: async () => {}
60061
60235
  });
60236
+ if (confirmedPlan && run2.landed.map((done) => done.kind).join() !== confirmedPlan.join())
60237
+ throw new TradingError("INVALID_RESPONSE", `Candle ended the launch before every confirmed leg landed (${confirmedPlan.join(", ")}).${run2.landed.length === 0 ? " No leg landed." : ` Landed: ${run2.landed.map((done) => `${done.kind} ${safeText(done.hash)}`).join(", ")}.`}`, { details: { operationId: first.operationId, landedLegs: run2.landed } });
60062
60238
  return printTradingResult(ctx, {
60063
60239
  ...run2.final,
60064
60240
  ...await recordLaunchedToken(ctx, wallet, run2.final),
@@ -60071,6 +60247,16 @@ async function hoodLaunch(ctx, key, args) {
60071
60247
  landedLegs: run2.landed
60072
60248
  });
60073
60249
  }
60250
+ function hoodLaterLegsGas(count, buys) {
60251
+ if (count < 2)
60252
+ return "";
60253
+ return buys ? "; the later legs are priced by Candle when each becomes next" : "; the fee leg is priced by Candle when it becomes next";
60254
+ }
60255
+ function hoodLaunchLegNames(count, buys) {
60256
+ if (!buys)
60257
+ return count === 2 ? ["create curve", "fee"] : ["create curve"];
60258
+ return count === 3 ? ["create curve", "fee", "first buy"] : ["create curve", "first buy"];
60259
+ }
60074
60260
  async function recordLaunchedToken(ctx, wallet, final) {
60075
60261
  const mint = typeof final.mint === "string" ? checkEvmAddress(final.mint) : undefined;
60076
60262
  if (!mint?.ok) {
@@ -61633,7 +61819,7 @@ async function perpsDeposit(args, ctx) {
61633
61819
  init_args();
61634
61820
 
61635
61821
  // src/plans.ts
61636
- var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs.";
61822
+ var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs. Every plan can launch a token from its embedded wallet, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the headless launch; the two launch rows are additional routes, not the only ones.";
61637
61823
  var PLAN_CAPABILITY_LABELS = {
61638
61824
  tradeCandleTokens: "Trade Candle-launched tokens",
61639
61825
  tradeBaseAssets: "Trade base assets",
@@ -61641,8 +61827,8 @@ var PLAN_CAPABILITY_LABELS = {
61641
61827
  sellExternalTokens: "Sell tokens not launched on Candle",
61642
61828
  buyExternalTokens: "Buy tokens not launched on Candle",
61643
61829
  hyperliquidPerps: "Hyperliquid perps (when enabled)",
61644
- selfLaunch: "Self-launch from a linked wallet",
61645
- atomicLaunch: "Atomic launch with first buys",
61830
+ selfLaunch: "Launch from a linked or TEE wallet (self-signed)",
61831
+ atomicLaunch: "Atomic launch: launch + 1–4 first buys in one bundle",
61646
61832
  createLinkedWallets: "Create linked wallets",
61647
61833
  importLinkedWallets: "Import linked wallets",
61648
61834
  limitOrders: "Limit orders",
@@ -62263,7 +62449,7 @@ async function pnl(args, ctx) {
62263
62449
  writeLocalFailure(deps, {
62264
62450
  code: "SCOPE_MISSING",
62265
62451
  message: "The account's P&L needs a key with the Read scope (account:read); this profile's key has none.",
62266
- suggestion: "Log in with a Read or Read:Write key, or read this key's own P&L: candle pnl --profile <name>"
62452
+ suggestion: "Log in with a Read or Write key, or read this key's own P&L: candle pnl --profile <name>"
62267
62453
  }, json);
62268
62454
  return 1;
62269
62455
  }
@@ -64260,7 +64446,7 @@ async function transfer(args, ctx) {
64260
64446
  if (payer.kind === "embedded")
64261
64447
  throw new TradingError("PAYER_UNSUPPORTED", "This command moves a linked wallet this machine can sign for. The embedded wallet transfers through the agent transfer rail (MCP candle_transfer), not from here. Name a TEE wallet with --wallet.");
64262
64448
  if (!payer.scopes.includes("transfer:bound"))
64263
- throw new TradingError("SCOPE_MISSING", `Moving funds out of a TEE wallet needs a Read:Write:Transfer key. Widen the bound key with: candle keys access ${apiKeyPrefix(key) ?? "<bound prefix>"} --access read-write-transfer. Or mint one with: candle keys create --access read-write-transfer, then bind the wallet to it with: candle tee rebind`);
64449
+ throw new TradingError("SCOPE_MISSING", `Moving funds out of a TEE wallet needs a Withdraw key. Widen the bound key with: candle keys access ${apiKeyPrefix(key) ?? "<bound prefix>"} --access withdraw. Or mint one with: candle keys create --access withdraw, then bind the wallet to it with: candle tee rebind`);
64264
64450
  const wallet = payer.wallet;
64265
64451
  const destination = await resolveDestination(ctx, key, wallet, to);
64266
64452
  const label = asset ?? flags["--mint"];
@@ -64332,7 +64518,7 @@ async function hoodTransfer(ctx, args) {
64332
64518
  if (payer.kind === "embedded")
64333
64519
  throw new TradingError("PAYER_UNSUPPORTED", "This command moves a linked wallet this machine can sign for. The embedded wallet transfers through the agent transfer rail (MCP candle_transfer), not from here. Name a Hood TEE wallet with --wallet.");
64334
64520
  if (!payer.scopes.includes("transfer:bound"))
64335
- throw new TradingError("SCOPE_MISSING", `Moving funds out of a TEE wallet needs a Read:Write:Transfer key. Widen the bound key with: candle keys access ${apiKeyPrefix(key) ?? "<bound prefix>"} --access read-write-transfer. Or mint one with: candle keys create --access read-write-transfer, then bind the wallet to it with: candle tee rebind`);
64521
+ throw new TradingError("SCOPE_MISSING", `Moving funds out of a TEE wallet needs a Withdraw key. Widen the bound key with: candle keys access ${apiKeyPrefix(key) ?? "<bound prefix>"} --access withdraw. Or mint one with: candle keys create --access withdraw, then bind the wallet to it with: candle tee rebind`);
64336
64522
  const wallet = payer.wallet;
64337
64523
  const destination = await resolveDestination(ctx, key, wallet, to, "hood");
64338
64524
  const decimals = asset ? HOOD_BASES[asset]?.decimals ?? 18 : await hoodDecimals({ chain: "hood", asset: tokenAddress }, lazyEvmRpc(ctx, flags["--rpc-url"]));
@@ -65696,10 +65882,10 @@ function recordCopyLine(outcome) {
65696
65882
  // src/commands/vault-demote.ts
65697
65883
  init_args();
65698
65884
  init_render();
65885
+ init_solana_endpoint();
65699
65886
  init_errors();
65700
65887
  init_promote_support();
65701
65888
  init_reconcile_grant();
65702
- init_store();
65703
65889
  init_tee_resolve();
65704
65890
  init_tee();
65705
65891
  init_tee_evm();
@@ -65718,7 +65904,6 @@ async function vaultDemote(args, ctx) {
65718
65904
  if (!address || extra !== undefined) {
65719
65905
  return usage(ctx, "Usage: candle vault demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>]");
65720
65906
  }
65721
- const rpcUrl = parsed.values["--rpc-url"];
65722
65907
  if (!refuseEnvPassphrase(ctx))
65723
65908
  return 1;
65724
65909
  if (!requireTty(ctx, "vault demote"))
@@ -65729,6 +65914,9 @@ async function vaultDemote(args, ctx) {
65729
65914
  const path = resolvedVault.path;
65730
65915
  const emergency = parsed.booleans.has("--emergency");
65731
65916
  const sweepTo = parsed.values["--sweep-to"];
65917
+ const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
65918
+ if ("error" in solana)
65919
+ return usage(ctx, solana.error);
65732
65920
  const resolved = await resolveTeeAddress(ctx, parsed, address, async () => ({
65733
65921
  ok: false,
65734
65922
  code: 1
@@ -65751,49 +65939,33 @@ async function vaultDemote(args, ctx) {
65751
65939
  }, ctx.json);
65752
65940
  return 1;
65753
65941
  }
65754
- let released = false;
65942
+ const vaultResolved = resolved.resolved;
65755
65943
  try {
65756
- const vaultResolved = resolved.resolved;
65757
65944
  const reconciled = await maybeReconcileVaultTee(ctx, vaultResolved, "demote");
65758
65945
  if (reconciled.code !== null)
65759
65946
  return reconciled.code;
65760
65947
  const entry = reconciled.entry;
65948
+ const step = { resolved: vaultResolved, solana };
65761
65949
  if (entry.tee?.vaultDestination === undefined) {
65762
65950
  if (sweepTo === undefined) {
65763
65951
  throw new VaultError("GRANT_DESTINATION_UNRESOLVED", `${address} has no pinned vault destination.`, {
65764
65952
  suggestion: "Pass --sweep-to <vault-key-label> (subject to the recovery destination rule), or adopt the server's pin."
65765
65953
  });
65766
65954
  }
65767
- releaseResolvedTee(resolved.resolved);
65768
- released = true;
65769
- return await runVaultCommand(ctx, async ({ hold }) => {
65770
- const raw = await requireVaultRaw(ctx, resolvedVault);
65771
- const opened = await unlockInteractively(ctx, path, raw, {
65772
- acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
65773
- });
65774
- const vault = hold(opened.vault);
65775
- const destination = assertColdVaultDestination(vault.index, sweepTo, {});
65776
- await commitVault(vault, {
65777
- index: {
65778
- hd: vault.index.hd,
65779
- entries: vault.index.entries.map((e) => e.address === address ? {
65780
- ...e,
65781
- tee: {
65782
- ...e.tee ?? { network: "solana-mainnet", lifecycle: "local-candidate" },
65783
- vaultDestination: destination.address
65784
- }
65785
- } : e)
65786
- }
65787
- }, ctx.deps);
65788
- return demoteWithAdapter(ctx, { linkedWalletId: entry.linkedWalletId }, address, rpcUrl, true);
65955
+ await assertNotOlderCopy(ctx, path, vaultResolved.vault.raw, parsed.booleans.has("--accept-older-copy"));
65956
+ const destination = assertColdVaultDestination(vaultResolved.vault.index, sweepTo, {});
65957
+ const next = await commitVaultTeeEntry(ctx, vaultResolved.vault, entry.id, (target) => {
65958
+ target.tee = {
65959
+ ...target.tee ?? { network: "solana-mainnet", lifecycle: "local-candidate" },
65960
+ vaultDestination: destination.address
65961
+ };
65789
65962
  });
65963
+ applyVault(vaultResolved, next);
65964
+ return await demoteWithAdapter(ctx, step, address, parsed.values["--rpc-url"], true);
65790
65965
  }
65791
65966
  requireTeeDestination(entry);
65792
65967
  const needsEmergency = emergency || entry.linkedWalletId === undefined || entry.tee?.lifecycle === "stranded";
65793
- const snapshot = { linkedWalletId: entry.linkedWalletId };
65794
- releaseResolvedTee(resolved.resolved);
65795
- released = true;
65796
- return demoteWithAdapter(ctx, snapshot, address, rpcUrl, needsEmergency);
65968
+ return await demoteWithAdapter(ctx, step, address, parsed.values["--rpc-url"], needsEmergency);
65797
65969
  } catch (error) {
65798
65970
  if (error instanceof VaultError) {
65799
65971
  writeLocalFailure(ctx.deps, {
@@ -65805,23 +65977,39 @@ async function vaultDemote(args, ctx) {
65805
65977
  }
65806
65978
  throw error;
65807
65979
  } finally {
65808
- if (!released)
65809
- releaseResolvedTee(resolved.resolved);
65980
+ releaseResolvedTee(vaultResolved);
65810
65981
  }
65811
65982
  }
65812
- async function demoteWithAdapter(ctx, entry, address, rpcUrl, emergency) {
65813
- if (entry.linkedWalletId !== undefined) {
65814
- const disableCode = await teeDisable([address], ctx);
65983
+ async function demoteWithAdapter(ctx, step, address, rpcUrl, emergency) {
65984
+ const linkedWalletId = step.resolved.entry.linkedWalletId;
65985
+ let disableCode = null;
65986
+ if (linkedWalletId !== undefined) {
65987
+ disableCode = await teeDisable([address], ctx, step);
65815
65988
  if (disableCode !== 0 && disableCode !== 3 && !emergency)
65816
65989
  return disableCode;
65817
65990
  } else {
65818
65991
  ctx.deps.stdout.write(`No linkedWalletId is recorded for ${address}; the grant could not be identified, so disable was not called and remote authority stays unknown.
65819
65992
  `);
65820
65993
  }
65994
+ const destination = step.resolved.entry.tee?.vaultDestination;
65995
+ if (await nothingLeftToSweep(step.solana, step.resolved.entry)) {
65996
+ const code = disableCode === 0 ? 0 : 3;
65997
+ if (ctx.json) {
65998
+ ctx.deps.stdout.write(`${JSON.stringify({ address, vaultDestination: destination, swept: false, nothingToSweep: true, lamports: "0", tokenAccounts: 0 })}
65999
+ `);
66000
+ } else {
66001
+ ctx.deps.stdout.write(`Nothing to sweep: 0 SOL, 0 token accounts.
66002
+ `);
66003
+ }
66004
+ return code;
66005
+ }
66006
+ if (!ctx.json)
66007
+ ctx.deps.stdout.write(`Sweeping remaining funds to ${destination}...
66008
+ `);
65821
66009
  const sweepArgs = rpcUrl === undefined ? [address] : [address, "--rpc-url", rpcUrl];
65822
- if (emergency || entry.linkedWalletId === undefined)
66010
+ if (emergency || linkedWalletId === undefined)
65823
66011
  sweepArgs.push("--emergency");
65824
- return teeSweep(sweepArgs, ctx);
66012
+ return teeSweep(sweepArgs, ctx, step);
65825
66013
  }
65826
66014
 
65827
66015
  // src/commands/vault-export-key.ts
@@ -72327,6 +72515,176 @@ function messageOf3(error) {
72327
72515
  // src/index.ts
72328
72516
  init_wallets();
72329
72517
 
72518
+ // src/commands/wallets-allow-launch.ts
72519
+ init_args();
72520
+ init_deps();
72521
+ init_profiles();
72522
+ init_render();
72523
+ init_promote_support();
72524
+ var PREVIEW_PATH = "/api/v1/agent/tee-wallets/allow-launch/preview";
72525
+ var capabilityPath = (id) => `/api/v1/agent/wallets/${encodeURIComponent(id)}/capabilities`;
72526
+ var USAGE_ALLOW = "Usage: candle wallets allow-launch <label|address|id|prefix*>... [--json]";
72527
+ var USAGE_DISALLOW = "Usage: candle wallets disallow-launch <label|address|id|prefix*>... [--yes] [--json]";
72528
+ var DEVICE_TOKEN_REQUIRED2 = {
72529
+ code: "DEVICE_TOKEN_REQUIRED",
72530
+ message: "Changing whether a TEE wallet may launch needs the device token, the owner's credential; an API key cannot do it.",
72531
+ suggestion: "Run: candle auth login"
72532
+ };
72533
+ var nameOf = (row) => row.label ?? row.address;
72534
+ function allowLaunchTable(rows) {
72535
+ return renderTable(["line", "label", "wallet", "address", "bound key"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address, row.boundKeyPrefix ?? "-"]));
72536
+ }
72537
+ function nothingToChangeLine(shown, enabled) {
72538
+ const bits = [];
72539
+ if (shown.unchanged.length === 1)
72540
+ bits.push(`that wallet already has allowLaunch ${enabled ? "on" : "off"}`);
72541
+ else if (shown.unchanged.length > 1) {
72542
+ bits.push(`all ${shown.unchanged.length} wallets already have allowLaunch ${enabled ? "on" : "off"}`);
72543
+ }
72544
+ if (shown.notTee.length > 0) {
72545
+ bits.push(`${shown.notTee.map(nameOf).join(", ")} ${shown.notTee.length === 1 ? "is" : "are"} not a TEE wallet`);
72546
+ }
72547
+ if (bits.length === 0)
72548
+ return `Nothing to change.
72549
+ `;
72550
+ return `Nothing to change: ${bits.join("; ")}.
72551
+ `;
72552
+ }
72553
+ function writeFailure2(ctx, result) {
72554
+ const { deps, apiUrl, json } = ctx;
72555
+ let code = result.code;
72556
+ let message = result.message;
72557
+ let suggestion;
72558
+ if (result.status === 404) {
72559
+ code = "ALLOW_LAUNCH_UNSUPPORTED";
72560
+ message = "This Candle API does not offer allowLaunch on TEE wallets (an older API, or TEE launch is off); nothing changed.";
72561
+ } else {
72562
+ const error = result.raw?.error;
72563
+ if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
72564
+ const ids = error.matches.map((m) => String(m.id)).join(", ");
72565
+ suggestion = `Name one of them by id or address: ${ids}`;
72566
+ } else if (error?.reason === "not_found") {
72567
+ suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
72568
+ }
72569
+ }
72570
+ const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
72571
+ if (json) {
72572
+ deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
72573
+ `);
72574
+ } else {
72575
+ deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
72576
+ `);
72577
+ }
72578
+ return 1;
72579
+ }
72580
+ async function setAllowLaunch(args, ctx, enabled) {
72581
+ const { deps, apiUrl, json } = ctx;
72582
+ const usage6 = enabled ? USAGE_ALLOW : USAGE_DISALLOW;
72583
+ const command = enabled ? "wallets allow-launch" : "wallets disallow-launch";
72584
+ const parsed = parseArgs(args, enabled ? {} : { booleanFlags: ["--yes"] });
72585
+ if ("error" in parsed) {
72586
+ writeUsageFailure(deps, parsed.error, json);
72587
+ return 2;
72588
+ }
72589
+ if (parsed.positionals.length === 0) {
72590
+ writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
72591
+ return 2;
72592
+ }
72593
+ const skipConfirm = !enabled && parsed.booleans.has("--yes");
72594
+ await printIdentity(ctx);
72595
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
72596
+ if (!deviceToken) {
72597
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
72598
+ return 1;
72599
+ }
72600
+ if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
72601
+ writeLocalFailure(deps, {
72602
+ code: "ALLOW_LAUNCH_REQUIRES_TTY",
72603
+ message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
72604
+ suggestion: enabled ? "Run it in an interactive shell; there is no flag and no environment variable for confirm." : "Run it in an interactive shell, or pass --yes to turn it off without the prompt."
72605
+ }, json);
72606
+ return 1;
72607
+ }
72608
+ const request2 = (path, method, body) => apiRequest(path, {
72609
+ method,
72610
+ body,
72611
+ auth: "device",
72612
+ credentials: { deviceToken },
72613
+ apiUrl,
72614
+ fetch: deps.fetch,
72615
+ env: deps.env
72616
+ });
72617
+ const preview = await request2(PREVIEW_PATH, "POST", { wallets: parsed.positionals, enabled });
72618
+ if (!preview.ok)
72619
+ return writeFailure2(ctx, preview);
72620
+ const shown = preview.body;
72621
+ const changing = shown.changed;
72622
+ if (changing.length === 0) {
72623
+ if (json)
72624
+ deps.stdout.write(`${JSON.stringify({ ...shown, command })}
72625
+ `);
72626
+ else
72627
+ deps.stdout.write(nothingToChangeLine(shown, enabled));
72628
+ return shown.unchanged.length === 0 && shown.notTee.length > 0 ? 1 : 0;
72629
+ }
72630
+ const n = changing.length;
72631
+ const screen = [
72632
+ allowLaunchTable(changing),
72633
+ ...shown.unchanged.length > 0 ? [`${shown.unchanged.length} already ${enabled ? "on" : "off"}: ${shown.unchanged.map(nameOf).join(", ")}`] : [],
72634
+ ...shown.notTee.length > 0 ? [`Not a TEE wallet, left alone: ${shown.notTee.map(nameOf).join(", ")}`] : [],
72635
+ "",
72636
+ enabled ? `${n === 1 ? "This wallet" : `These ${n} wallets`} will be allowed to pay for a launch: the bound key, with launch:write, can create tokens from ${n === 1 ? "it" : "them"} (candle launch).` : `${n === 1 ? "This wallet" : `These ${n} wallets`} will no longer be able to pay for a launch. Trading is unchanged.`,
72637
+ ""
72638
+ ];
72639
+ deps.stderr.write(`${screen.join(`
72640
+ `)}
72641
+ `);
72642
+ if (!skipConfirm) {
72643
+ const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${enabled ? "allow" : "stop"} launches from ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
72644
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
72645
+ writeLocalFailure(deps, {
72646
+ code: "ALLOW_LAUNCH_NOT_ACKNOWLEDGED",
72647
+ message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
72648
+ suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
72649
+ }, json);
72650
+ return 1;
72651
+ }
72652
+ }
72653
+ const done = [];
72654
+ const failed = [];
72655
+ for (const row of changing) {
72656
+ const result = await request2(capabilityPath(row.id), "PUT", { capability: "allowLaunch", enabled });
72657
+ if (result.ok)
72658
+ done.push({ ...row, allowLaunch: enabled });
72659
+ else {
72660
+ failed.push({
72661
+ id: row.id,
72662
+ code: result.status === 404 ? "WALLET_NOT_FOUND" : result.code ?? null,
72663
+ message: result.status === 404 ? "no longer an active TEE wallet on this account" : result.message
72664
+ });
72665
+ }
72666
+ }
72667
+ if (json) {
72668
+ deps.stdout.write(`${JSON.stringify({ success: failed.length === 0, command, enabled, changed: done, unchanged: shown.unchanged, notTee: shown.notTee, failed })}
72669
+ `);
72670
+ return failed.length > 0 ? 1 : 0;
72671
+ }
72672
+ const verb = enabled ? "Allowed launches from" : "Stopped launches from";
72673
+ deps.stdout.write(`${verb} ${done.length} wallet${done.length === 1 ? "" : "s"}.
72674
+ `);
72675
+ for (const failure of failed) {
72676
+ deps.stderr.write(`Not changed: ${failure.id}: ${failure.message}${failure.code ? ` (${failure.code})` : ""}.
72677
+ `);
72678
+ }
72679
+ return failed.length > 0 ? 1 : 0;
72680
+ }
72681
+ function walletsAllowLaunch(args, ctx) {
72682
+ return setAllowLaunch(args, ctx, true);
72683
+ }
72684
+ function walletsDisallowLaunch(args, ctx) {
72685
+ return setAllowLaunch(args, ctx, false);
72686
+ }
72687
+
72330
72688
  // src/commands/wallets-close-empty.ts
72331
72689
  init_args();
72332
72690
  init_render();
@@ -72484,7 +72842,7 @@ init_promote_support();
72484
72842
  var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
72485
72843
  var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
72486
72844
  var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
72487
- var DEVICE_TOKEN_REQUIRED2 = {
72845
+ var DEVICE_TOKEN_REQUIRED3 = {
72488
72846
  code: "DEVICE_TOKEN_REQUIRED",
72489
72847
  message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
72490
72848
  suggestion: "Run: candle auth login"
@@ -72495,7 +72853,7 @@ function sessionLinkedLine(rows) {
72495
72853
  const names = rows.map((row) => row.label ?? row.address).join(", ");
72496
72854
  return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
72497
72855
  }
72498
- function nothingToChangeLine(shown, trusted) {
72856
+ function nothingToChangeLine2(shown, trusted) {
72499
72857
  const session = shown.sessionLinked ?? [];
72500
72858
  const unchanged = shown.unchanged;
72501
72859
  if (session.length > 0 && unchanged.length === 0) {
@@ -72562,7 +72920,7 @@ async function setTrust(args, ctx, trusted) {
72562
72920
  await printIdentity(ctx);
72563
72921
  const deviceToken = await resolveDeviceToken(deps, ctx.profile);
72564
72922
  if (!deviceToken) {
72565
- writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
72923
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED3, json);
72566
72924
  return 1;
72567
72925
  }
72568
72926
  if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
@@ -72592,7 +72950,7 @@ async function setTrust(args, ctx, trusted) {
72592
72950
  deps.stdout.write(`${JSON.stringify({ ...shown, command })}
72593
72951
  `);
72594
72952
  } else {
72595
- deps.stdout.write(nothingToChangeLine(shown, trusted));
72953
+ deps.stdout.write(nothingToChangeLine2(shown, trusted));
72596
72954
  }
72597
72955
  return 0;
72598
72956
  }
@@ -72859,6 +73217,8 @@ var COMMANDS = {
72859
73217
  revoke: walletsRevoke,
72860
73218
  trust: walletsTrust,
72861
73219
  untrust: walletsUntrust,
73220
+ "allow-launch": walletsAllowLaunch,
73221
+ "disallow-launch": walletsDisallowLaunch,
72862
73222
  "close-empty": walletsCloseEmpty
72863
73223
  },
72864
73224
  bare: wallets