@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.
- package/README.md +8 -6
- package/dist/index.js +245 -178
- 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.
|
|
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,
|
|
74
|
-
| `candle keys create [--access read\|
|
|
75
|
-
| `candle keys access <prefix\|label\|self> (--access read\|
|
|
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
|
|
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
|
|
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: "
|
|
347
|
-
readwritetransfer: "
|
|
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
|
-
["
|
|
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,
|
|
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
|
|
32376
|
-
|
|
32377
|
-
|
|
32378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32997
|
-
|
|
32998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33022
|
-
|
|
33023
|
-
|
|
33024
|
-
|
|
33025
|
-
|
|
33026
|
-
|
|
33027
|
-
|
|
33028
|
-
|
|
33029
|
-
|
|
33030
|
-
|
|
33031
|
-
|
|
33032
|
-
|
|
33033
|
-
|
|
33034
|
-
|
|
33035
|
-
|
|
33036
|
-
|
|
33037
|
-
|
|
33038
|
-
|
|
33039
|
-
|
|
33040
|
-
|
|
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
|
-
|
|
33043
|
-
|
|
33044
|
-
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
|
|
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
|
-
|
|
33052
|
-
|
|
33053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33088
|
-
|
|
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
|
-
|
|
33109
|
-
|
|
33110
|
-
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
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
|
-
|
|
33122
|
-
|
|
33123
|
-
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
33128
|
-
|
|
33129
|
-
|
|
33130
|
-
|
|
33131
|
-
|
|
33132
|
-
|
|
33133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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|
|
|
53585
|
-
description: "Create an API key; --access mints one of the three levels (
|
|
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|
|
|
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
|
|
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
|
|
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|
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
65889
|
-
|
|
65890
|
-
|
|
65891
|
-
|
|
65892
|
-
|
|
65893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65930
|
-
releaseResolvedTee(resolved.resolved);
|
|
65980
|
+
releaseResolvedTee(vaultResolved);
|
|
65931
65981
|
}
|
|
65932
65982
|
}
|
|
65933
|
-
async function demoteWithAdapter(ctx,
|
|
65934
|
-
|
|
65935
|
-
|
|
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 ||
|
|
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