@candledottv/cli 0.11.17 → 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 +8 -6
  2. package/dist/index.js +245 -178
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @candledottv/cli
2
2
 
3
- The Candle CLI (current version 0.11.17): authorize a device from your browser, keep your own keys
3
+ The Candle CLI (current version 0.11.18): authorize a device from your browser, keep your own keys
4
4
  in an encrypted vault on this machine, hand an agent a TEE wallet it can trade, and manage API
5
5
  keys, wallets, and setup health from the terminal. Zero runtime dependencies; the whole thing is
6
6
  one self-contained `dist/index.js` that runs under plain Node.
@@ -70,16 +70,16 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
70
70
  | `candle auth login [--profile <name>] [--scopes <a,b,c>] [--label <name>] [--no-browser]` | Authorizes this device: prints a code, opens (or prints) an approval URL, polls until approved, then stores the resulting device token and API key. `--label` names both the device and the key. |
71
71
  | `candle auth status` | Shows which storage backend is in use, both credential prefixes, the config file path, and a live validity check for each credential. |
72
72
  | `candle auth logout [--keep-key]` | Revokes the stored API key (skipped with `--keep-key`), clears local credentials and config, and prints the portal URL for revoking the device itself. |
73
- | `candle keys list [--scopes]` | Lists this account's API keys: prefix, name, Read, Read:Write or Read:Write:Transfer access, environment, timestamps, and which device minted each one. `--scopes` adds the raw scopes, sorted. |
74
- | `candle keys 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]` | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. `--access` mints one of the portal's three levels (`read-write-transfer` is Read:Write plus `transfer:bound`: the key can move funds out of the wallet it runs, to wallets you linked while signed in or marked trusted and to that wallet's vault); `--scopes` names raw scopes instead, never both. `--embedded-wallet` defaults to `deny`; `allow` lets the key spend the account's embedded wallet and is confirmed at a terminal. Stored locally only if the CLI does not already hold a working key. An account holds at most 12 active keys. |
75
- | `candle keys access <prefix\|label\|self> (--access read\|read-write\|read-write-transfer [--yes] \| --history)` | Changes 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 and can never widen it. `--history` lists the key's changes and who made them. |
73
+ | `candle keys list [--scopes]` | Lists this account's API keys: prefix, name, Read, Write or Withdraw access, environment, timestamps, and which device minted each one. `--scopes` adds the raw scopes, sorted. |
74
+ | `candle keys 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]` | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. `--access` mints one of the portal's three levels (`withdraw` is Write plus `transfer:bound`: the key can move funds out of the wallet it runs, to wallets you linked while signed in or marked trusted and to that wallet's vault); `--scopes` names raw scopes instead, never both. `--embedded-wallet` defaults to `deny`; `allow` lets the key spend the account's embedded wallet and is confirmed at a terminal. Stored locally only if the CLI does not already hold a working key. An account holds at most 12 active keys. |
75
+ | `candle keys access <prefix\|label\|self> (--access read\|write\|withdraw [--yes] \| --history)` | Changes 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 and can never widen it. `--history` lists the key's changes and who made them. |
76
76
  | `candle keys update <prefix\|label> --embedded-wallet allow\|deny [--yes]`, `candle keys self embedded-wallet deny` | Allow or deny a key the account's embedded wallet, with the device token (allowing needs the prefix typed back at a terminal). `self embedded-wallet deny` lets the profile's own key give it up with no prompt; a key can never allow itself. |
77
77
  | `candle keys signer approve <code> --key <prefix\|label> [--reject]`, `candle keys signer move <prefix\|label> [--to-key <prefix\|label>]` | Key signers for bots on a different machine from the vault. `approve` accepts (or rejects) a key's signer request with the device token, after you type the full fingerprint the trading machine printed. `move`, on the machine holding a wallet's current signer, moves every wallet on the key onto its active signer; `--to-key` moves a revoked key's wallets to a live key. |
78
78
  | `candle keys revoke <prefix>` | Revokes an API key by prefix. Revoking the CLI's own stored key also clears it locally. |
79
79
  | `candle keys wallets <prefix>`, `keys wallets set <prefix> --wallets <id,id>`, `keys wallets scope <prefix> --scope all\|selected` | Each key is an agent profile with its own wallet set: list it, replace it, or limit the profile to its assigned wallets. |
80
80
  | `candle wallets` (alias `candle wallet`) | Shows the account's embedded (launch) wallets and any linked wallets, using the API key, with a `Signer` column saying whether this machine holds each linked wallet's signing key and a `Trusted` column saying whether it is yours (linked while signed in, or marked with `trust`). It links and revokes; it never generates or prints a key, which the vault now derives. |
81
81
  | `candle wallets import --chain solana\|evm`, `wallets revoke <wallet-id>` | Link a wallet you own (key from `--key-file` or a hidden prompt, sealed to Privy before it leaves the machine), or unlink one. |
82
- | `candle wallets trust\|untrust <label\|address\|id\|prefix*>...` | Mark linked wallets as yours so an agent's Read:Write:Transfer key can move funds into them, or clear the mark. Owner only, over the device token, with a typed confirm; a trailing `*`, as in `'tr-*'`, selects by prefix. |
82
+ | `candle wallets trust\|untrust <label\|address\|id\|prefix*>...` | Mark linked wallets as yours so an agent's Withdraw key can move funds into them, or clear the mark. Owner only, over the device token, with a typed confirm; a trailing `*`, as in `'tr-*'`, selects by prefix. |
83
83
  | `candle wallets allow-launch\|disallow-launch <label\|address\|id\|prefix*>...` | Let TEE wallets pay for a launch (`candle launch`), or stop them. Owner only, over the device token; the screen shows each wallet's label, address and bound key before a typed confirm. `disallow-launch --yes` skips the confirm. |
84
84
  | `candle wallets close-empty [--wallet embedded] [--keep <mint>]... [--yes]` | Closes the embedded wallet's empty token accounts and returns their rent to it, after a preview (`transfer:write`). |
85
85
  | `candle vault init\|status\|list\|new-key\|rename\|phrase show\|restore\|reconcile-exposure` | The encrypted local vault, `vault.enc` (Tier 1). `init` creates it with a passphrase factor and a 24-word recovery phrase (and no key), `new-key` derives a key inside it (`--count`, `--labels-from <file>` for many under one unlock), `list [filter] [--balances]` shows one line per key, `rename` changes a label only, `phrase show` displays the phrase on a terminal (never in `--json`, a log or a pipe), and `restore --phrase` rebuilds the derived keys on another machine with a new passphrase (`--count`, `--tee-count`, `--external-count`, `--evm-count` and `--evm-tee-count` for EVM keys and Hood TEE wallets, or a Solana gap scan with `--rpc-url`); a restored vault never allocates new keys. `reconcile-exposure` re-reads the account and adds exposure records, clearing nothing. No key ever leaves the vault to reach Candle. See [CLI custody](https://docs.candle.tv/developers/cli-custody). |
@@ -96,7 +96,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
96
96
  | `candle tee new\|enable` | The legacy path: `new` seals a fresh key in its own `tee-wallets.enc` and `enable <address> --vault <address>` delegates it with a pinned sweep vault. New setups use `vault promote`; `vault import-legacy --tee` migrates an existing store. |
97
97
  | `candle swap <from> <to> --amount <n>\|--percent <n> --wallet <tee> [--to <tee>] [--wait]` | Quote, confirm and swap through the TEE wallet's bound key; the first buy after a launch is this command, unless `candle launch --buy` made it. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana). SOL or USDC to ETH or USDG, or back, is a bridge through Relay into this key's own TEE wallet on the other chain (`--to` names it when there are several); `--wait` follows it for up to ten minutes. |
98
98
  | `candle swap status <id> [--kind trade\|swap\|launch] [--wait]` | Read an operation without resending it; `--wait` follows a bridge until it ends. |
99
- | `candle transfer --to <address\|wallet name\|vault> --asset <SOL\|USDC\|CNDL\|ETH\|USDG>\|--mint <mint>\|--token <0x...> --amount <decimal\|max> [--wallet <tee>] [--rpc-url <url>] [--yes]` | Move funds out of a TEE wallet through its bound Read:Write:Transfer key, to the wallet's own pinned vault (`--to vault`, any token, `max` allowed, one SOL transfer at most the server's backstop ceiling, 50 SOL by default) or to another of the account's wallets you linked while signed in or marked trusted (a base asset, counted against the key's caps, which must include a per-asset cap and a USD `--tx-limit`). The asset decides the chain: ETH, USDG or `--token` moves from a Hood TEE wallet. The destination and its kind are shown before you confirm; Candle builds, the relay signs, Candle broadcasts. |
99
+ | `candle transfer --to <address\|wallet name\|vault> --asset <SOL\|USDC\|CNDL\|ETH\|USDG>\|--mint <mint>\|--token <0x...> --amount <decimal\|max> [--wallet <tee>] [--rpc-url <url>] [--yes]` | Move funds out of a TEE wallet through its bound Withdraw key, to the wallet's own pinned vault (`--to vault`, any token, `max` allowed, one SOL transfer at most the server's backstop ceiling, 50 SOL by default) or to another of the account's wallets you linked while signed in or marked trusted (a base asset, counted against the key's caps, which must include a per-asset cap and a USD `--tx-limit`). The asset decides the chain: ETH, USDG or `--token` moves from a Hood TEE wallet. The destination and its kind are shown before you confirm; Candle builds, the relay signs, Candle broadcasts. |
100
100
  | `candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee> [--buy <amount>] [--quote-asset <asset>] [--dex-version v3\|v4]` | Create a token from a TEE wallet. Without `--buy` it makes no first buy. `--buy <amount>` adds one from the same wallet, in whole units of the quote asset, reserved against the key's spend window before signing: in the launch transaction on Solana, as a last leg on Hood. It needs the server's `TEE_LAUNCH_BUYS_ENABLED`, which is off by default; while it is off Candle refuses the buy and nothing is built. Needs `launch:write`, `allowLaunch` turned on for the wallet by the account owner, and the Pro or Max plan. On Free, launch from the embedded wallet instead, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood): the MCP tool `candle_launch_and_seed` or the SDK's `launch()`. The wallet decides the chain: a Hood TEE wallet launches on Hood and needs `--dex-version`. `--quote-asset` is sol, usdc or cndl on Solana, eth or usdg on Hood. |
101
101
  | `candle pnl [--profile <name>]` | P&L: realized net, fees, unrealized and total, then open positions, the same figures the console shows. Without `--profile`, the account's books (every profile, the web app and the CLI, one ledger; needs the Read scope), with a By agent table (each key, then Manual, then the account total). With `--profile <name>`, that profile's key's share of the same P&L: realized on its sales and the open positions that belong to it, with a row per wallet that sums to the total. Each wallet is its own average-cost pool, and a recorded move between the account's wallets carries its cost. Both show a Solana block, a Hood block, then the combined figure, the closed positions as money made, minus money lost, plus partial sells, and a `CHAIN` column (and `WALLET`, where the API names it) on positions. Unpriced positions are never valued at zero; dust (worth under one cent) is marked and still counted. A bridge between Solana and Hood is not a trade, and bridge and Relay costs are not netted. DAMM v2 LP positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis) and vs holding (what the deposited tokens would be worth held); `Total` stays the console's token figure and `Total with LP` covers tokens and LP. |
102
102
  | `candle portfolio [<filter>] [--chain solana\|hood] [--rpc-url <url>] [--evm-rpc-url <url>]` | Vault, TEE and embedded wallets on Solana and Hood in one table with chain, amount, price and value, subtotals per group and per chain, and a total. Solana vault and external balances are read over your own Solana RPC; Candle is sent only the mints, for prices. EVM vault keys are read over your own Hood RPC (`--evm-rpc-url`, else `CANDLE_EVM_RPC_URL`, else the built-in Hood RPC) for ETH and USDG only, priced by asset, never by address; an RPC on another chain is refused. An unpriced Hood token shows why (`no-market-row`, `stale-mark`, ...). `--chain` shows one chain. Each TEE wallet's DAMM v2 LP positions follow the tokens, valued by Candle at the same marks, and count in the total; one whose pool could not be read is shown as not read and the total says it is partial. |
@@ -116,6 +116,8 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
116
116
  | `candle completion <zsh\|bash\|fish>` | Prints a shell completion script to stdout, generated from the same data `candle help` prints, so it offers exactly the commands, subcommands and flags the help documents. Install it by redirecting: `candle completion zsh > "${fpath[1]}/_candle"`, `candle completion bash > ~/.local/share/bash-completion/completions/candle`, `candle completion fish > ~/.config/fish/completions/candle.fish`. Static: profile names, key labels and factor ids are not completed, because that would mean reading config or opening the vault at tab time. Regenerate after every `candle update`. |
117
117
  | `candle help [<command>]` | The top-level screen (six groups of command words, the global flags, and every environment variable the CLI reads), or one command's own: its subcommands, the flags they share, examples, and the environment it reads. `candle <command> --help` is the same screen. Reads no config and makes no request, so it answers before any profile is selected. |
118
118
 
119
+ The documented `--access` values are `read`, `write` and `withdraw`. The older `read-write`, `transfer` and `read-write-transfer` values remain accepted as silent, permanent aliases.
120
+
119
121
  Every command accepts these global options:
120
122
 
121
123
  | Flag | Effect |
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
  };
@@ -27455,6 +27458,20 @@ async function resolveTeeAddress(ctx, _parsed, address, openLegacy, access3 = "r
27455
27458
  }
27456
27459
  };
27457
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
+ }
27458
27475
  function releaseResolvedTee(resolved) {
27459
27476
  if (resolved.source === "vault")
27460
27477
  closeVault(resolved.vault);
@@ -32372,23 +32389,10 @@ async function teeSweepEvm(args, ctx) {
32372
32389
  if (!address || extra !== undefined) {
32373
32390
  return usage(ctx, "Usage: candle tee sweep <0x address> [--rpc-url <url>] [--emergency] [--token <0x...>]... [--from-block <n>]");
32374
32391
  }
32375
- const tokens = [];
32376
- for (const raw of tokenFlags) {
32377
- const checked = checkEvmAddress(raw);
32378
- if (!checked.ok)
32379
- return usage(ctx, `--token must be an ERC-20 contract address: ${raw || "(empty)"} is ${checked.reason}.`);
32380
- tokens.push(checked.address);
32381
- }
32382
- let fromBlock;
32383
- const fromBlockRaw = parsed.values["--from-block"];
32384
- if (fromBlockRaw !== undefined) {
32385
- if (!/^\d+$/.test(fromBlockRaw))
32386
- return usage(ctx, `--from-block must be a Hood block number: ${fromBlockRaw} is not.`);
32387
- fromBlock = BigInt(fromBlockRaw);
32388
- }
32389
- const client = resolveHoodClient(ctx, parsed.values["--rpc-url"]);
32390
- if ("error" in client)
32391
- 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;
32392
32396
  if (!refuseEnvPassphrase(ctx))
32393
32397
  return 1;
32394
32398
  if (!requireTty(ctx, "tee sweep"))
@@ -32407,6 +32411,26 @@ async function teeSweepEvm(args, ctx) {
32407
32411
  return sweepEvmWallet({ ctx, vault, hold, entry, client, emergency, tokenFlags: tokens, fromBlock });
32408
32412
  });
32409
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
+ }
32410
32434
  async function sweepEvmWallet(input) {
32411
32435
  const { ctx, client, emergency, hold } = input;
32412
32436
  const { deps, json } = ctx;
@@ -32984,78 +33008,83 @@ async function teeDisableEvm(args, ctx) {
32984
33008
  const resolvedVault = vaultPathFor(ctx, parsed);
32985
33009
  if ("error" in resolvedVault)
32986
33010
  return usage(ctx, resolvedVault.error);
32987
- const { deps, json } = ctx;
32988
33011
  await printIdentity(ctx);
32989
33012
  return runVaultCommand(ctx, async ({ hold }) => {
32990
33013
  const raw = await requireVaultRaw(ctx, resolvedVault);
32991
33014
  const opened = await unlockInteractively(ctx, resolvedVault.path, raw, { acceptOlderCopy: true });
32992
- let vault = hold(opened.vault);
33015
+ const vault = hold(opened.vault);
32993
33016
  const entry = findEvmTee(vault, address);
32994
33017
  if (entry === undefined)
32995
33018
  return unknownWallet(ctx, address);
32996
- if (entry.linkedWalletId === undefined) {
32997
- writeLocalFailure(deps, { code: "TEE_WALLET_NOT_ENABLED", message: `${entry.address} was never enabled; there is nothing to stop.` }, json);
32998
- 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 };
32999
33045
  }
33000
- const linkedWalletId = entry.linkedWalletId;
33001
- const stopRequestedAt = entry.tee?.stopRequestedAt ?? new Date(deps.now()).toISOString();
33002
- vault = hold(await patchTee(ctx, vault, entry.id, (tee) => ({ ...tee, stopRequestedAt })));
33003
- const unconfirmed = (detail, suggestion) => {
33004
- if (json) {
33005
- writeJson(deps, {
33006
- ok: false,
33007
- code: "STOP_UNCONFIRMED",
33008
- message: detail,
33009
- address: entry.address,
33010
- linkedWalletId,
33011
- stopRequestedAt,
33012
- remoteEnforcement: "unconfirmed",
33013
- suggestion
33014
- });
33015
- return 1;
33016
- }
33017
- deps.stderr.write(`${detail}
33046
+ deps.stderr.write(`${detail}
33018
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.
33019
33048
  ${suggestion}
33020
33049
  `);
33021
- return 1;
33022
- };
33023
- const apiKey = await resolveApiKey(deps, ctx.profile);
33024
- if (!apiKey) {
33025
- 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}`);
33026
- }
33027
- const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(linkedWalletId)}`, {
33028
- method: "DELETE",
33029
- auth: "key",
33030
- credentials: { apiKey },
33031
- apiUrl: ctx.apiUrl,
33032
- fetch: deps.fetch,
33033
- env: deps.env
33034
- });
33035
- if (!result.ok) {
33036
- 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}).`);
33037
- }
33038
- const outcome = readDisableOutcome(result.body);
33039
- for (const warning of bridgeDisableWarnings(entry.address, result.body))
33040
- 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}
33041
33070
  `);
33042
- if (json) {
33043
- writeJson(deps, { address: entry.address, linkedWalletId, ...result.body });
33044
- return outcome.complete ? 0 : 3;
33045
- }
33046
- if (outcome.complete) {
33047
- 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.
33048
33077
  `);
33078
+ if (!inDemote)
33049
33079
  deps.stdout.write(`Recover the funds: candle tee sweep ${entry.address}
33050
33080
  `);
33051
- return 0;
33052
- }
33053
- 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})` : ""}.
33054
33084
  Funds remain in the TEE wallet and its TEE signing authority may still be active. Re-run: candle tee disable ${entry.address}
33055
- If the provider is down or theft is suspected: candle tee sweep ${entry.address} --emergency
33056
- `);
33057
- return 3;
33058
- });
33085
+ ${inDemote ? "" : `If the provider is down or theft is suspected: candle tee sweep ${entry.address} --emergency
33086
+ `}`);
33087
+ return { code: 3, vault };
33059
33088
  }
33060
33089
  async function vaultDemoteEvm(args, ctx) {
33061
33090
  const { rest, values: tokenFlags } = extractRepeated(args, "--token");
@@ -33070,6 +33099,9 @@ async function vaultDemoteEvm(args, ctx) {
33070
33099
  if (!address || extra !== undefined) {
33071
33100
  return usage(ctx, "Usage: candle vault demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>]");
33072
33101
  }
33102
+ const options = sweepOptions(ctx, parsed, tokenFlags);
33103
+ if (typeof options === "number")
33104
+ return options;
33073
33105
  if (!refuseEnvPassphrase(ctx))
33074
33106
  return 1;
33075
33107
  if (!requireTty(ctx, "vault demote"))
@@ -33079,13 +33111,13 @@ async function vaultDemoteEvm(args, ctx) {
33079
33111
  return usage(ctx, resolvedVault.error);
33080
33112
  const sweepTo = parsed.values["--sweep-to"];
33081
33113
  let emergency = parsed.booleans.has("--emergency");
33082
- const snapshot = await runVaultCommand(ctx, async ({ hold }) => {
33114
+ return runVaultCommand(ctx, async ({ hold }) => {
33083
33115
  const raw = await requireVaultRaw(ctx, resolvedVault);
33084
33116
  const opened = await unlockInteractively(ctx, resolvedVault.path, raw, {
33085
33117
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
33086
33118
  });
33087
- const vault = hold(opened.vault);
33088
- const entry = findEvmTee(vault, address);
33119
+ let vault = hold(opened.vault);
33120
+ let entry = findEvmTee(vault, address);
33089
33121
  if (entry === undefined)
33090
33122
  return unknownWallet(ctx, address);
33091
33123
  if (entry.tee?.vaultDestination === undefined) {
@@ -33098,39 +33130,40 @@ async function vaultDemoteEvm(args, ctx) {
33098
33130
  chain: "evm",
33099
33131
  acceptUnknownExposure: parsed.booleans.has("--accept-unknown-exposure")
33100
33132
  });
33101
- 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);
33102
33135
  }
33103
33136
  if (entry.linkedWalletId === undefined) {
33104
33137
  ctx.deps.stdout.write(`No linkedWalletId is recorded for ${entry.address}; disable was not called and remote authority stays unknown.
33105
33138
  `);
33106
33139
  emergency = true;
33107
33140
  }
33108
- return 0;
33109
- });
33110
- if (snapshot !== 0)
33111
- return snapshot;
33112
- if (!emergency) {
33113
- const disableCode = await teeDisableEvm([address, ...keystoreArgs(parsed)], ctx);
33114
- if (disableCode !== 0 && disableCode !== 3)
33115
- return disableCode;
33116
- if (disableCode === 3) {
33117
- 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
33118
33150
  `);
33151
+ }
33119
33152
  }
33120
- }
33121
- const sweepArgs = [address, ...keystoreArgs(parsed)];
33122
- if (parsed.values["--rpc-url"] !== undefined)
33123
- sweepArgs.push("--rpc-url", parsed.values["--rpc-url"]);
33124
- if (parsed.values["--from-block"] !== undefined)
33125
- sweepArgs.push("--from-block", parsed.values["--from-block"]);
33126
- for (const token of tokenFlags)
33127
- sweepArgs.push("--token", token);
33128
- if (emergency)
33129
- sweepArgs.push("--emergency");
33130
- return teeSweepEvm(sweepArgs, ctx);
33131
- }
33132
- function keystoreArgs(parsed) {
33133
- 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
+ });
33134
33167
  }
33135
33168
  var MAX_TOKEN_TRANSFER_GAS = 1000000n;
33136
33169
  var init_tee_evm = __esm(() => {
@@ -33498,6 +33531,9 @@ async function commitTee(deps, opened, mutate, opts = {}) {
33498
33531
  };
33499
33532
  }
33500
33533
  }
33534
+ function vaultActiveTee(ctx, resolved) {
33535
+ return { source: "vault", resolved, entry: resolved.legacyView, path: "vault", ctx };
33536
+ }
33501
33537
  function applyKeystoreViewToVaultEntry(entry, view) {
33502
33538
  if (view.linkedWalletId !== undefined)
33503
33539
  entry.linkedWalletId = view.linkedWalletId;
@@ -33552,18 +33588,8 @@ async function openActiveTee(ctx, parsed, address, access3) {
33552
33588
  const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed), access3);
33553
33589
  if (!resolved.ok)
33554
33590
  return { ok: false, code: resolved.code };
33555
- if (resolved.resolved.source === "vault") {
33556
- return {
33557
- ok: true,
33558
- active: {
33559
- source: "vault",
33560
- resolved: resolved.resolved,
33561
- entry: resolved.resolved.legacyView,
33562
- path: "vault",
33563
- ctx
33564
- }
33565
- };
33566
- }
33591
+ if (resolved.resolved.source === "vault")
33592
+ return { ok: true, active: vaultActiveTee(ctx, resolved.resolved) };
33567
33593
  return {
33568
33594
  ok: true,
33569
33595
  active: {
@@ -34173,7 +34199,7 @@ async function teeStatus(args, ctx) {
34173
34199
  releaseResolvedTee(local);
34174
34200
  }
34175
34201
  }
34176
- async function teeDisable(args, ctx) {
34202
+ async function teeDisable(args, ctx, demote) {
34177
34203
  const { deps, apiUrl, json } = ctx;
34178
34204
  if (namesEvmWallet(args))
34179
34205
  return teeDisableEvm(args, ctx);
@@ -34186,7 +34212,7 @@ async function teeDisable(args, ctx) {
34186
34212
  if (!address || extra !== undefined)
34187
34213
  return usage2(ctx, "Usage: candle tee disable <address>");
34188
34214
  await printIdentity(ctx);
34189
- 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");
34190
34216
  if (!openedActive.ok)
34191
34217
  return openedActive.code;
34192
34218
  const { active } = openedActive;
@@ -34255,17 +34281,37 @@ ${suggestion}
34255
34281
  if (outcome.complete) {
34256
34282
  deps.stdout.write(`Stopped ${address}. Remote signing denial verified; the wallet is quarantined.
34257
34283
  `);
34258
- deps.stdout.write(`Recover the funds: candle tee sweep ${address}
34284
+ if (!demote)
34285
+ deps.stdout.write(`Recover the funds: candle tee sweep ${address}
34259
34286
  `);
34260
34287
  return 0;
34261
34288
  }
34262
34289
  deps.stdout.write(`Agent trading stopped at Candle for ${address}. Remote policy verification is pending${outcome.reasonCode ? ` (${outcome.reasonCode})` : ""}.
34263
34290
  Funds remain in the TEE wallet and its TEE signing authority may still be active. Re-run: candle tee disable ${address}
34264
- 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
34265
34292
  `);
34266
34293
  return 3;
34267
34294
  } finally {
34268
- 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;
34269
34315
  }
34270
34316
  }
34271
34317
  function refusalState(raw) {
@@ -34373,7 +34419,7 @@ async function broadcastMessage(rpc, ctx, secret, message, blockhash, pending, r
34373
34419
  error: `transaction ${signature} was not finalized within ${CONFIRM_MAX_POLLS * CONFIRM_POLL_MS / 1000}s; it may still land${echoNote}`
34374
34420
  };
34375
34421
  }
34376
- async function teeSweep(args, ctx) {
34422
+ async function teeSweep(args, ctx, demote) {
34377
34423
  const { deps, apiUrl, json } = ctx;
34378
34424
  if (namesEvmWallet(args))
34379
34425
  return teeSweepEvm(args, ctx);
@@ -34389,11 +34435,24 @@ async function teeSweep(args, ctx) {
34389
34435
  const [address, extra] = parsed.positionals;
34390
34436
  if (!address || extra !== undefined)
34391
34437
  return usage2(ctx, "Usage: candle tee sweep <address> [--rpc-url <url>] [--emergency]");
34392
- const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
34438
+ const solana = demote ? demote.solana : await openSolanaClient(ctx, parsed.values["--rpc-url"]);
34393
34439
  if ("error" in solana)
34394
34440
  return usage2(ctx, solana.error);
34395
34441
  const emergency = parsed.booleans.has("--emergency");
34396
- 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
+ }
34397
34456
  if (!openedActive.ok)
34398
34457
  return openedActive.code;
34399
34458
  const { active } = openedActive;
@@ -35177,7 +35236,8 @@ Stop the agent from your Candle session if you have not, and re-run candle tee d
35177
35236
  `);
35178
35237
  return 3;
35179
35238
  } finally {
35180
- releaseActiveTee(active);
35239
+ if (!demote)
35240
+ releaseActiveTee(active);
35181
35241
  }
35182
35242
  }
35183
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;
@@ -35192,6 +35252,7 @@ var init_tee = __esm(() => {
35192
35252
  init_solana_endpoint();
35193
35253
  init_solana_lite();
35194
35254
  init_token_2022();
35255
+ init_errors();
35195
35256
  init_store();
35196
35257
  init_tee_resolve();
35197
35258
  init_wallet_import_flow();
@@ -52823,7 +52884,7 @@ init_render();
52823
52884
  init_secret_store();
52824
52885
 
52825
52886
  // src/version.ts
52826
- var CLI_VERSION = "0.11.17";
52887
+ var CLI_VERSION = "0.11.18";
52827
52888
 
52828
52889
  // src/commands/auth.ts
52829
52890
  init_keys_embedded_wallet();
@@ -52847,7 +52908,7 @@ async function authLogin(args, ctx) {
52847
52908
  }
52848
52909
  const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
52849
52910
  if (scopes?.includes("transfer:bound")) {
52850
- 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);
52851
52912
  return 2;
52852
52913
  }
52853
52914
  const label = parsed.values["--label"]?.trim();
@@ -53460,7 +53521,7 @@ var HELP = {
53460
53521
  transfer: {
53461
53522
  group: "Trade",
53462
53523
  summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
53463
- 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.",
53464
53525
  usage: [
53465
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]"
53466
53527
  ],
@@ -53578,14 +53639,14 @@ var HELP = {
53578
53639
  rows: [
53579
53640
  {
53580
53641
  invocation: "list [--scopes]",
53581
- 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"
53582
53643
  },
53583
53644
  {
53584
- 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]",
53585
- 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"
53586
53647
  },
53587
53648
  {
53588
- 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)",
53589
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"
53590
53651
  },
53591
53652
  {
@@ -53620,9 +53681,9 @@ var HELP = {
53620
53681
  ],
53621
53682
  examples: [
53622
53683
  "candle keys list",
53623
- "candle keys create --access read-write-transfer --label rebalancer",
53684
+ "candle keys create --access withdraw --label rebalancer",
53624
53685
  "candle keys create --scopes trade:write --label agent-one",
53625
- "candle keys access cndl --access read-write-transfer",
53686
+ "candle keys access cndl --access withdraw",
53626
53687
  "candle keys access self --access read --yes",
53627
53688
  "candle keys update agent-one --embedded-wallet deny --yes",
53628
53689
  "candle keys self embedded-wallet deny",
@@ -59415,10 +59476,14 @@ init_promote_support();
59415
59476
  init_keys();
59416
59477
  init_tee_rebind();
59417
59478
  var KEYS_PATH4 = "/api/v1/agent/keys";
59418
- 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]
59419
59480
  ` + " candle keys access <prefix|label> --history [--json]";
59420
59481
  var SELF = "self";
59421
- 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
+ };
59422
59487
  var NO_API_KEY2 = {
59423
59488
  code: "API_KEY_REQUIRED",
59424
59489
  message: "candle keys access self changes the profile's own API key, and this profile holds none.",
@@ -59563,7 +59628,7 @@ async function keysAccess(args, ctx) {
59563
59628
  }
59564
59629
  const preset = ACCESS_LEVELS[accessFlag];
59565
59630
  if (preset === undefined) {
59566
- 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);
59567
59632
  return 2;
59568
59633
  }
59569
59634
  await printIdentity(ctx);
@@ -62384,7 +62449,7 @@ async function pnl(args, ctx) {
62384
62449
  writeLocalFailure(deps, {
62385
62450
  code: "SCOPE_MISSING",
62386
62451
  message: "The account's P&L needs a key with the Read scope (account:read); this profile's key has none.",
62387
- 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>"
62388
62453
  }, json);
62389
62454
  return 1;
62390
62455
  }
@@ -64381,7 +64446,7 @@ async function transfer(args, ctx) {
64381
64446
  if (payer.kind === "embedded")
64382
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.");
64383
64448
  if (!payer.scopes.includes("transfer:bound"))
64384
- 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`);
64385
64450
  const wallet = payer.wallet;
64386
64451
  const destination = await resolveDestination(ctx, key, wallet, to);
64387
64452
  const label = asset ?? flags["--mint"];
@@ -64453,7 +64518,7 @@ async function hoodTransfer(ctx, args) {
64453
64518
  if (payer.kind === "embedded")
64454
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.");
64455
64520
  if (!payer.scopes.includes("transfer:bound"))
64456
- 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`);
64457
64522
  const wallet = payer.wallet;
64458
64523
  const destination = await resolveDestination(ctx, key, wallet, to, "hood");
64459
64524
  const decimals = asset ? HOOD_BASES[asset]?.decimals ?? 18 : await hoodDecimals({ chain: "hood", asset: tokenAddress }, lazyEvmRpc(ctx, flags["--rpc-url"]));
@@ -65817,10 +65882,10 @@ function recordCopyLine(outcome) {
65817
65882
  // src/commands/vault-demote.ts
65818
65883
  init_args();
65819
65884
  init_render();
65885
+ init_solana_endpoint();
65820
65886
  init_errors();
65821
65887
  init_promote_support();
65822
65888
  init_reconcile_grant();
65823
- init_store();
65824
65889
  init_tee_resolve();
65825
65890
  init_tee();
65826
65891
  init_tee_evm();
@@ -65839,7 +65904,6 @@ async function vaultDemote(args, ctx) {
65839
65904
  if (!address || extra !== undefined) {
65840
65905
  return usage(ctx, "Usage: candle vault demote <tee-address> [--rpc-url <url>] [--emergency] [--sweep-to <label>]");
65841
65906
  }
65842
- const rpcUrl = parsed.values["--rpc-url"];
65843
65907
  if (!refuseEnvPassphrase(ctx))
65844
65908
  return 1;
65845
65909
  if (!requireTty(ctx, "vault demote"))
@@ -65850,6 +65914,9 @@ async function vaultDemote(args, ctx) {
65850
65914
  const path = resolvedVault.path;
65851
65915
  const emergency = parsed.booleans.has("--emergency");
65852
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);
65853
65920
  const resolved = await resolveTeeAddress(ctx, parsed, address, async () => ({
65854
65921
  ok: false,
65855
65922
  code: 1
@@ -65872,49 +65939,33 @@ async function vaultDemote(args, ctx) {
65872
65939
  }, ctx.json);
65873
65940
  return 1;
65874
65941
  }
65875
- let released = false;
65942
+ const vaultResolved = resolved.resolved;
65876
65943
  try {
65877
- const vaultResolved = resolved.resolved;
65878
65944
  const reconciled = await maybeReconcileVaultTee(ctx, vaultResolved, "demote");
65879
65945
  if (reconciled.code !== null)
65880
65946
  return reconciled.code;
65881
65947
  const entry = reconciled.entry;
65948
+ const step = { resolved: vaultResolved, solana };
65882
65949
  if (entry.tee?.vaultDestination === undefined) {
65883
65950
  if (sweepTo === undefined) {
65884
65951
  throw new VaultError("GRANT_DESTINATION_UNRESOLVED", `${address} has no pinned vault destination.`, {
65885
65952
  suggestion: "Pass --sweep-to <vault-key-label> (subject to the recovery destination rule), or adopt the server's pin."
65886
65953
  });
65887
65954
  }
65888
- releaseResolvedTee(resolved.resolved);
65889
- released = true;
65890
- return await runVaultCommand(ctx, async ({ hold }) => {
65891
- const raw = await requireVaultRaw(ctx, resolvedVault);
65892
- const opened = await unlockInteractively(ctx, path, raw, {
65893
- acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
65894
- });
65895
- const vault = hold(opened.vault);
65896
- const destination = assertColdVaultDestination(vault.index, sweepTo, {});
65897
- await commitVault(vault, {
65898
- index: {
65899
- hd: vault.index.hd,
65900
- entries: vault.index.entries.map((e) => e.address === address ? {
65901
- ...e,
65902
- tee: {
65903
- ...e.tee ?? { network: "solana-mainnet", lifecycle: "local-candidate" },
65904
- vaultDestination: destination.address
65905
- }
65906
- } : e)
65907
- }
65908
- }, ctx.deps);
65909
- 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
+ };
65910
65962
  });
65963
+ applyVault(vaultResolved, next);
65964
+ return await demoteWithAdapter(ctx, step, address, parsed.values["--rpc-url"], true);
65911
65965
  }
65912
65966
  requireTeeDestination(entry);
65913
65967
  const needsEmergency = emergency || entry.linkedWalletId === undefined || entry.tee?.lifecycle === "stranded";
65914
- const snapshot = { linkedWalletId: entry.linkedWalletId };
65915
- releaseResolvedTee(resolved.resolved);
65916
- released = true;
65917
- return demoteWithAdapter(ctx, snapshot, address, rpcUrl, needsEmergency);
65968
+ return await demoteWithAdapter(ctx, step, address, parsed.values["--rpc-url"], needsEmergency);
65918
65969
  } catch (error) {
65919
65970
  if (error instanceof VaultError) {
65920
65971
  writeLocalFailure(ctx.deps, {
@@ -65926,23 +65977,39 @@ async function vaultDemote(args, ctx) {
65926
65977
  }
65927
65978
  throw error;
65928
65979
  } finally {
65929
- if (!released)
65930
- releaseResolvedTee(resolved.resolved);
65980
+ releaseResolvedTee(vaultResolved);
65931
65981
  }
65932
65982
  }
65933
- async function demoteWithAdapter(ctx, entry, address, rpcUrl, emergency) {
65934
- if (entry.linkedWalletId !== undefined) {
65935
- 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);
65936
65988
  if (disableCode !== 0 && disableCode !== 3 && !emergency)
65937
65989
  return disableCode;
65938
65990
  } else {
65939
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.
65940
65992
  `);
65941
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
+ `);
65942
66009
  const sweepArgs = rpcUrl === undefined ? [address] : [address, "--rpc-url", rpcUrl];
65943
- if (emergency || entry.linkedWalletId === undefined)
66010
+ if (emergency || linkedWalletId === undefined)
65944
66011
  sweepArgs.push("--emergency");
65945
- return teeSweep(sweepArgs, ctx);
66012
+ return teeSweep(sweepArgs, ctx, step);
65946
66013
  }
65947
66014
 
65948
66015
  // src/commands/vault-export-key.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.17",
3
+ "version": "0.11.18",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {