@candledottv/cli 0.11.19 → 0.11.21
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 +26 -1
- package/dist/index.js +691 -175
- 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.21): 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.
|
|
@@ -8,6 +8,30 @@ one self-contained `dist/index.js` that runs under plain Node.
|
|
|
8
8
|
The full custody guide is [CLI custody](https://docs.candle.tv/developers/cli-custody), and every
|
|
9
9
|
command and flag is in the [Candle CLI reference](https://docs.candle.tv/developers/cli).
|
|
10
10
|
|
|
11
|
+
`candle vault rename --pairs-from <file> [--dry-run]` renames 1–256 vault or external keys under
|
|
12
|
+
one unlock and one commit. Use `<label|address|id> <new-label>` lines or CSV columns `from,to`
|
|
13
|
+
(picked by name; quote names with spaces or commas). Blank lines and `#` comments are skipped.
|
|
14
|
+
Every finding refuses the whole file before any write; swaps within the batch work. `--dry-run`
|
|
15
|
+
unlocks and prints the plan without writing; `--json` returns
|
|
16
|
+
`{ ok, renamed: [{ id, address, role, from, to }] }` (plus `dryRun: true` for a dry run).
|
|
17
|
+
Single and batch rename require new labels of at most 64 characters, without edge whitespace,
|
|
18
|
+
control characters or a leading `-`; names are never trimmed. Existing longer labels still
|
|
19
|
+
resolve as targets. A terminal is required. TEE wallets are refused by `vault rename`; use
|
|
20
|
+
`candle tee rename`.
|
|
21
|
+
|
|
22
|
+
`candle tee rename <label|address|id> <new-label>` renames a TEE wallet on both sides: the label
|
|
23
|
+
Candle holds and, when the wallet's key is in this vault, the vault entry.
|
|
24
|
+
`candle tee rename --pairs-from <file> [--dry-run]` does 1–256 of them from the same file shape
|
|
25
|
+
(lines, or CSV columns `from,to`) under the same label rule (at most 64 characters, never
|
|
26
|
+
trimmed). It is owner only (the device token; an API key cannot rename a wallet). One server
|
|
27
|
+
call renames every row, all or nothing, then one vault commit; the vault is unlocked once, and
|
|
28
|
+
only when a named TEE wallet may be in it. A new name a vault key outside the rename already has
|
|
29
|
+
refuses before the server is called. `--json` returns
|
|
30
|
+
`{ ok, renamed: [{ id, chain, address, from, to, server, vault }] }`. If the vault write fails
|
|
31
|
+
after the server accepted, the command prints address-keyed rows (`<address> <new-label>`, `rerun`
|
|
32
|
+
under `--json`) to run again: the old label no longer resolves on the server, and a re-run leaves
|
|
33
|
+
the server label alone and finishes the vault.
|
|
34
|
+
|
|
11
35
|
## Quick start
|
|
12
36
|
|
|
13
37
|
```
|
|
@@ -93,6 +117,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
|
|
|
93
117
|
| `candle tee status\|disable\|sweep\|fund` | Operate a TEE wallet (Tier 2): `status` shows its server state and on-chain balances, `disable` stops the agent (exit 0 only on a verified stop, 3 while pending), `sweep --rpc-url <url>` moves everything to the pinned vault, signed locally (closing DAMM v2 positions after verifying each server-built close; `--emergency` sweeps when the API cannot be read and moves the position NFT instead), and `fund` prints what your vault signs. On a Hood wallet (0x), `status` shows ETH, USDG and the gas reserve, and `sweep` moves USDG, WETH, every `--token` and then ETH last. See [CLI custody](https://docs.candle.tv/developers/cli-custody). |
|
|
94
118
|
| `candle tee signer new --key <prefix\|label> [--out <pem>] [--force]` | On the trading machine, with that key's API key: generates the key's signer here and waits for the owner to approve it with `keys signer approve`. The key's wallets then trade from this machine. `--out` also writes a plaintext PEM for an SDK process. |
|
|
95
119
|
| `candle tee rebind <wallet...> --to-key <prefix\|label>`, `candle tee rebinds [wallet]` | Move TEE wallets to another key on the same account (owner only, preview then `confirm`; funds and the relay signer do not move), and list the rebind history. |
|
|
120
|
+
| `candle tee rename <label\|address\|id> <new-label>`, `candle tee rename --pairs-from <file> [--dry-run]` | Rename one TEE wallet, or 1–256 from a pairs file: the label Candle holds and the vault entry when there is one (owner only; one server call, all or nothing, then one vault commit). |
|
|
96
121
|
| `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
122
|
| `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
123
|
| `candle swap status <id> [--kind trade\|swap\|launch] [--wait]` | Read an operation without resending it; `--wait` follows a bridge until it ends. |
|
package/dist/index.js
CHANGED
|
@@ -1319,6 +1319,7 @@ var init_errors = __esm(() => {
|
|
|
1319
1319
|
"VAULT_LABEL_TAKEN",
|
|
1320
1320
|
"VAULT_LABEL_UNCHANGED",
|
|
1321
1321
|
"VAULT_RENAME_ROLE_REFUSED",
|
|
1322
|
+
"VAULT_RENAME_BATCH_REFUSED",
|
|
1322
1323
|
"PROMOTE_BATCH_REFUSED",
|
|
1323
1324
|
"WALLET_LIMIT_REACHED",
|
|
1324
1325
|
"TIER_REQUIRED",
|
|
@@ -61914,9 +61915,9 @@ function verifyPerpsBuild2(build, opts) {
|
|
|
61914
61915
|
return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
|
|
61915
61916
|
}
|
|
61916
61917
|
if (type === "order") {
|
|
61917
|
-
const
|
|
61918
|
-
if (
|
|
61919
|
-
return { ok: false, reason:
|
|
61918
|
+
const refusal2 = checkBuilderField2(action, opts.builder);
|
|
61919
|
+
if (refusal2)
|
|
61920
|
+
return { ok: false, reason: refusal2 };
|
|
61920
61921
|
} else if ("builder" in action) {
|
|
61921
61922
|
return { ok: false, reason: `a ${type} action carries no builder` };
|
|
61922
61923
|
}
|
|
@@ -63166,7 +63167,7 @@ init_render();
|
|
|
63166
63167
|
init_secret_store();
|
|
63167
63168
|
|
|
63168
63169
|
// src/version.ts
|
|
63169
|
-
var CLI_VERSION = "0.11.
|
|
63170
|
+
var CLI_VERSION = "0.11.21";
|
|
63170
63171
|
|
|
63171
63172
|
// src/commands/auth.ts
|
|
63172
63173
|
init_keys_embedded_wallet();
|
|
@@ -64067,8 +64068,12 @@ var HELP = {
|
|
|
64067
64068
|
description: "Derive the next Solana key (m/44'/501'/n'/0') or EVM key (m/44'/60'/n'/0/0), or n of them under one unlock; the name must be free"
|
|
64068
64069
|
},
|
|
64069
64070
|
{
|
|
64070
|
-
invocation: "rename <label|address> <new-label> [--id <entry-id>]",
|
|
64071
|
-
description: "Rename one key. The address, the derivation and the key blob do not change"
|
|
64071
|
+
invocation: "rename <label|address|id> <new-label> [--id <entry-id>]",
|
|
64072
|
+
description: "Rename one vault or external key. New names: at most 64 characters, no edge whitespace. The address, the derivation and the key blob do not change"
|
|
64073
|
+
},
|
|
64074
|
+
{
|
|
64075
|
+
invocation: " rename --pairs-from <file> [--dry-run]",
|
|
64076
|
+
description: "Rename 1–256 vault or external keys under one unlock and one commit. All findings refuse the whole file; swaps are allowed. --dry-run prints the plan and writes nothing; --json returns the renamed rows"
|
|
64072
64077
|
},
|
|
64073
64078
|
{ invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
|
|
64074
64079
|
{
|
|
@@ -64149,7 +64154,11 @@ var HELP = {
|
|
|
64149
64154
|
},
|
|
64150
64155
|
{
|
|
64151
64156
|
invocation: "--pairs-from <file>",
|
|
64152
|
-
description: "promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
|
|
64157
|
+
description: "rename: one '<label|address|id> <new-label>' per line, or CSV columns from,to (1–256 rows; blank lines and # comments skipped). No trimming of names; use CSV for names with spaces. promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
|
|
64158
|
+
},
|
|
64159
|
+
{
|
|
64160
|
+
invocation: "--dry-run",
|
|
64161
|
+
description: "rename --pairs-from: unlock, check every row, print the plan, and write nothing"
|
|
64153
64162
|
},
|
|
64154
64163
|
{
|
|
64155
64164
|
invocation: "--to-key <prefix|label>",
|
|
@@ -64163,6 +64172,7 @@ var HELP = {
|
|
|
64163
64172
|
"candle vault transfer 0x000000000000000000000000000000000000dEaD --amount 0.5 --asset USDG --from hood-cold",
|
|
64164
64173
|
"candle vault list cn-s",
|
|
64165
64174
|
"candle vault rename key-7 treasury-cold",
|
|
64175
|
+
"candle vault rename --pairs-from ./rename-plan.csv --dry-run",
|
|
64166
64176
|
"candle vault new-key --chain solana --labels-from ./replacement-names.txt",
|
|
64167
64177
|
"candle vault promote-batch --pairs-from ./promote-plan.csv",
|
|
64168
64178
|
"candle vault promote-batch --pairs-from ./promote-plan.csv --to-key tr-01",
|
|
@@ -64214,12 +64224,30 @@ var HELP = {
|
|
|
64214
64224
|
description: "Move TEE wallets to another key on this account, which is how a key gets TEE wallets. Name them, or --label-prefix for every wallet whose label starts with it (owner only; funds do not move)"
|
|
64215
64225
|
},
|
|
64216
64226
|
{ invocation: "rebinds [wallet]", description: "List TEE wallet rebinds for this account (owner only)" },
|
|
64227
|
+
{
|
|
64228
|
+
invocation: "rename <label|address|id> <new-label>",
|
|
64229
|
+
description: "Rename one TEE wallet: the label Candle holds and, when its key is in this vault, the vault entry (owner only). New names: at most 64 characters, no edge whitespace. Funds, the address and the key do not change"
|
|
64230
|
+
},
|
|
64231
|
+
{
|
|
64232
|
+
invocation: " rename --pairs-from <file> [--dry-run]",
|
|
64233
|
+
description: "Rename 1–256 TEE wallets with one server call, all or nothing, then one vault commit. Every finding refuses the whole file. The vault is unlocked once, only when a named TEE wallet may be in it. --dry-run prints the plan and changes nothing; if the vault write fails after the server accepted, it prints address-keyed rows to re-run"
|
|
64234
|
+
},
|
|
64217
64235
|
{
|
|
64218
64236
|
invocation: "signer new --key <prefix|label> [--out <pem>] [--force]",
|
|
64219
64237
|
description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an older signer on THIS machine still owns wallets; it moves, revokes and deletes nothing"
|
|
64220
64238
|
}
|
|
64221
64239
|
],
|
|
64222
|
-
flags: [
|
|
64240
|
+
flags: [
|
|
64241
|
+
KEYSTORE_FLAG,
|
|
64242
|
+
{
|
|
64243
|
+
invocation: "--pairs-from <file>",
|
|
64244
|
+
description: "rename: one '<label|address|id> <new-label>' per line, or CSV columns from,to (1–256 rows; blank lines and # comments skipped). No trimming of names; use CSV for names with spaces."
|
|
64245
|
+
},
|
|
64246
|
+
{
|
|
64247
|
+
invocation: "--dry-run",
|
|
64248
|
+
description: "rename --pairs-from: resolve and check every row, print the plan, and change nothing"
|
|
64249
|
+
}
|
|
64250
|
+
],
|
|
64223
64251
|
examples: [
|
|
64224
64252
|
"candle tee new --label AgentOne",
|
|
64225
64253
|
"candle tee status AgentOneAddress",
|
|
@@ -64227,6 +64255,8 @@ var HELP = {
|
|
|
64227
64255
|
"candle tee sweep 0x000000000000000000000000000000000000dEaD --emergency --from-block 1200000",
|
|
64228
64256
|
"candle tee rebind tr-01 tr-02 --to-key Ab3dEf9h",
|
|
64229
64257
|
"candle tee rebind --label-prefix dest- --to-key Ab3dEf9h",
|
|
64258
|
+
"candle tee rename tr-01 desk-01",
|
|
64259
|
+
"candle tee rename --pairs-from ./tee-names.csv --dry-run",
|
|
64230
64260
|
"candle tee signer new --key tr-2"
|
|
64231
64261
|
],
|
|
64232
64262
|
env: ENV_LOCAL_SIGNING
|
|
@@ -74618,6 +74648,644 @@ init_swap();
|
|
|
74618
74648
|
init_tee();
|
|
74619
74649
|
init_tee_rebind();
|
|
74620
74650
|
|
|
74651
|
+
// src/commands/tee-rename.ts
|
|
74652
|
+
init_args();
|
|
74653
|
+
init_deps();
|
|
74654
|
+
init_profiles();
|
|
74655
|
+
init_render();
|
|
74656
|
+
init_errors();
|
|
74657
|
+
|
|
74658
|
+
// src/vault/labels.ts
|
|
74659
|
+
function entriesWithLabel(index, label) {
|
|
74660
|
+
return index.entries.filter((entry) => entry.label === label);
|
|
74661
|
+
}
|
|
74662
|
+
function duplicateLabels(index) {
|
|
74663
|
+
const byLabel = new Map;
|
|
74664
|
+
for (const entry of index.entries) {
|
|
74665
|
+
const holders = byLabel.get(entry.label);
|
|
74666
|
+
if (holders === undefined)
|
|
74667
|
+
byLabel.set(entry.label, [entry]);
|
|
74668
|
+
else
|
|
74669
|
+
holders.push(entry);
|
|
74670
|
+
}
|
|
74671
|
+
const out = [];
|
|
74672
|
+
for (const [label, entries] of byLabel) {
|
|
74673
|
+
if (entries.length > 1)
|
|
74674
|
+
out.push({ label, entries });
|
|
74675
|
+
}
|
|
74676
|
+
return out;
|
|
74677
|
+
}
|
|
74678
|
+
function hasControlCharacter(label) {
|
|
74679
|
+
for (const char of label) {
|
|
74680
|
+
const code = char.codePointAt(0) ?? 0;
|
|
74681
|
+
if (code < 32 || code === 127)
|
|
74682
|
+
return true;
|
|
74683
|
+
}
|
|
74684
|
+
return false;
|
|
74685
|
+
}
|
|
74686
|
+
function validateLabel(label) {
|
|
74687
|
+
if (label.trim().length === 0)
|
|
74688
|
+
return "A key's label cannot be empty.";
|
|
74689
|
+
if (hasControlCharacter(label)) {
|
|
74690
|
+
return "A key's label cannot contain a newline, a tab or a control character.";
|
|
74691
|
+
}
|
|
74692
|
+
if (label.startsWith("-")) {
|
|
74693
|
+
return `A key's label cannot begin with "-": it would be read as a flag everywhere a label is typed.`;
|
|
74694
|
+
}
|
|
74695
|
+
if (label !== label.trim())
|
|
74696
|
+
return "A key's label cannot begin or end with whitespace.";
|
|
74697
|
+
if (label.length > 64)
|
|
74698
|
+
return "A key's label cannot exceed 64 characters.";
|
|
74699
|
+
return;
|
|
74700
|
+
}
|
|
74701
|
+
function resolveRenameTarget(index, old, id) {
|
|
74702
|
+
if (id !== undefined) {
|
|
74703
|
+
const entry = index.entries.find((candidate) => candidate.id === id);
|
|
74704
|
+
return entry === undefined ? { kind: "none" } : { kind: "found", entry, by: "id" };
|
|
74705
|
+
}
|
|
74706
|
+
const byLabel = entriesWithLabel(index, old);
|
|
74707
|
+
if (byLabel.length === 1)
|
|
74708
|
+
return { kind: "found", entry: byLabel[0], by: "label" };
|
|
74709
|
+
if (byLabel.length > 1)
|
|
74710
|
+
return { kind: "ambiguous", by: "label", candidates: byLabel };
|
|
74711
|
+
const byAddress = index.entries.filter((entry) => entry.address === old);
|
|
74712
|
+
if (byAddress.length === 1)
|
|
74713
|
+
return { kind: "found", entry: byAddress[0], by: "address" };
|
|
74714
|
+
if (byAddress.length > 1)
|
|
74715
|
+
return { kind: "ambiguous", by: "address", candidates: byAddress };
|
|
74716
|
+
const byId = index.entries.find((entry) => entry.id === old);
|
|
74717
|
+
if (byId !== undefined)
|
|
74718
|
+
return { kind: "found", entry: byId, by: "id" };
|
|
74719
|
+
return { kind: "none" };
|
|
74720
|
+
}
|
|
74721
|
+
|
|
74722
|
+
// src/vault/rename-batch.ts
|
|
74723
|
+
var MAX_RENAME_BATCH = 256;
|
|
74724
|
+
function csvCells(text) {
|
|
74725
|
+
const cells = [];
|
|
74726
|
+
let cell = "";
|
|
74727
|
+
let quoted = false;
|
|
74728
|
+
let closed = false;
|
|
74729
|
+
for (let i = 0;i < text.length; i++) {
|
|
74730
|
+
const char = text[i];
|
|
74731
|
+
if (quoted) {
|
|
74732
|
+
if (char === '"') {
|
|
74733
|
+
if (text[i + 1] === '"') {
|
|
74734
|
+
cell += '"';
|
|
74735
|
+
i++;
|
|
74736
|
+
} else {
|
|
74737
|
+
quoted = false;
|
|
74738
|
+
closed = true;
|
|
74739
|
+
}
|
|
74740
|
+
} else
|
|
74741
|
+
cell += char;
|
|
74742
|
+
} else if (char === ",") {
|
|
74743
|
+
cells.push(cell);
|
|
74744
|
+
cell = "";
|
|
74745
|
+
closed = false;
|
|
74746
|
+
} else if (char === '"' && cell === "" && !closed)
|
|
74747
|
+
quoted = true;
|
|
74748
|
+
else if (char === '"' || closed)
|
|
74749
|
+
return;
|
|
74750
|
+
else
|
|
74751
|
+
cell += char;
|
|
74752
|
+
}
|
|
74753
|
+
if (quoted)
|
|
74754
|
+
return;
|
|
74755
|
+
cells.push(cell);
|
|
74756
|
+
return cells;
|
|
74757
|
+
}
|
|
74758
|
+
function parseRenamePairs(contents) {
|
|
74759
|
+
const lines = contents.split(/\r?\n/).map((text, at) => ({ text, line: at + 1 })).filter(({ text }) => text.trim() !== "" && !/^\s*#/.test(text));
|
|
74760
|
+
const rows = [];
|
|
74761
|
+
const findings = [];
|
|
74762
|
+
const header = csvCells(lines[0]?.text ?? "")?.map((cell) => cell.trim()) ?? [];
|
|
74763
|
+
const csv = header.length > 1 && (header.includes("from") || header.includes("to"));
|
|
74764
|
+
if (csv && (header.filter((name2) => name2 === "from").length !== 1 || header.filter((name2) => name2 === "to").length !== 1)) {
|
|
74765
|
+
return {
|
|
74766
|
+
rows,
|
|
74767
|
+
findings: [{ line: lines[0]?.line ?? 0, problem: "A CSV needs exactly one from and one to column." }]
|
|
74768
|
+
};
|
|
74769
|
+
}
|
|
74770
|
+
const data = csv ? lines.slice(1) : lines;
|
|
74771
|
+
if (data.length < 1 || data.length > MAX_RENAME_BATCH) {
|
|
74772
|
+
findings.push({ line: 0, problem: `A rename file must hold 1 to ${MAX_RENAME_BATCH} rows; found ${data.length}.` });
|
|
74773
|
+
}
|
|
74774
|
+
for (const { text, line } of data) {
|
|
74775
|
+
let from;
|
|
74776
|
+
let to;
|
|
74777
|
+
if (csv) {
|
|
74778
|
+
const cells = csvCells(text);
|
|
74779
|
+
if (cells === undefined || cells.length !== header.length) {
|
|
74780
|
+
findings.push({ line, problem: "Malformed CSV row or field count does not match the header." });
|
|
74781
|
+
continue;
|
|
74782
|
+
}
|
|
74783
|
+
from = cells[header.indexOf("from")];
|
|
74784
|
+
to = cells[header.indexOf("to")];
|
|
74785
|
+
} else {
|
|
74786
|
+
const match = /^(\S+)[ \t]+(\S+)([ \t]*)$/.exec(text);
|
|
74787
|
+
if (match !== null)
|
|
74788
|
+
[from, to] = [match[1], `${match[2]}${match[3]}`];
|
|
74789
|
+
}
|
|
74790
|
+
if (from === undefined || from === "" || to === undefined) {
|
|
74791
|
+
findings.push({ line, problem: 'A row needs "<label|address|id> <new-label>" (CSV columns: from,to).' });
|
|
74792
|
+
continue;
|
|
74793
|
+
}
|
|
74794
|
+
rows.push({ line, from, to });
|
|
74795
|
+
const invalid = validateLabel(to);
|
|
74796
|
+
if (invalid !== undefined)
|
|
74797
|
+
findings.push({ line, problem: invalid });
|
|
74798
|
+
}
|
|
74799
|
+
for (const field of ["from", "to"]) {
|
|
74800
|
+
const seen = new Map;
|
|
74801
|
+
for (const row of rows) {
|
|
74802
|
+
const earlier = seen.get(row[field]);
|
|
74803
|
+
if (earlier !== undefined)
|
|
74804
|
+
findings.push({ line: row.line, problem: `Duplicate ${field} ${row[field]} (line ${earlier}).` });
|
|
74805
|
+
else
|
|
74806
|
+
seen.set(row[field], row.line);
|
|
74807
|
+
}
|
|
74808
|
+
}
|
|
74809
|
+
return { rows, findings };
|
|
74810
|
+
}
|
|
74811
|
+
function planBatchRename(index, rows) {
|
|
74812
|
+
const plan = [];
|
|
74813
|
+
const findings = [];
|
|
74814
|
+
const seen = new Map;
|
|
74815
|
+
for (const row of rows) {
|
|
74816
|
+
const match = resolveRenameTarget(index, row.from);
|
|
74817
|
+
if (match.kind !== "found") {
|
|
74818
|
+
findings.push({
|
|
74819
|
+
line: row.line,
|
|
74820
|
+
problem: match.kind === "none" ? `No key matches ${row.from}.` : `${row.from} is ambiguous; use an id. Candidates: ${match.candidates.map((entry2) => `${entry2.address} (${entry2.id})`).join(", ")}.`
|
|
74821
|
+
});
|
|
74822
|
+
continue;
|
|
74823
|
+
}
|
|
74824
|
+
const entry = match.entry;
|
|
74825
|
+
const earlier = seen.get(entry.id);
|
|
74826
|
+
if (earlier !== undefined)
|
|
74827
|
+
findings.push({ line: row.line, problem: `Same key as line ${earlier}, named by another handle.` });
|
|
74828
|
+
else
|
|
74829
|
+
seen.set(entry.id, row.line);
|
|
74830
|
+
if (entry.role !== "vault" && entry.role !== "external") {
|
|
74831
|
+
findings.push({
|
|
74832
|
+
line: row.line,
|
|
74833
|
+
problem: `${entry.label} is a TEE wallet; vault rename only changes vault or external keys. Use candle tee rename.`
|
|
74834
|
+
});
|
|
74835
|
+
}
|
|
74836
|
+
if (entry.label === row.to)
|
|
74837
|
+
findings.push({ line: row.line, problem: `${row.to} is already this key's label.` });
|
|
74838
|
+
plan.push({ ...row, entry });
|
|
74839
|
+
}
|
|
74840
|
+
const targets = new Set(plan.map(({ entry }) => entry.id));
|
|
74841
|
+
for (const row of plan) {
|
|
74842
|
+
if (index.entries.some((entry) => entry.label === row.to && !targets.has(entry.id))) {
|
|
74843
|
+
findings.push({ line: row.line, problem: `New name ${row.to} is taken by a key outside the batch.` });
|
|
74844
|
+
}
|
|
74845
|
+
}
|
|
74846
|
+
return { plan, findings };
|
|
74847
|
+
}
|
|
74848
|
+
|
|
74849
|
+
// src/commands/tee-rename.ts
|
|
74850
|
+
init_store();
|
|
74851
|
+
init_tee();
|
|
74852
|
+
|
|
74853
|
+
// src/commands/vault-rename.ts
|
|
74854
|
+
init_args();
|
|
74855
|
+
init_errors();
|
|
74856
|
+
init_store();
|
|
74857
|
+
init_vault_support();
|
|
74858
|
+
var RENAME_USAGE = "Usage: candle vault rename <label|address|id> <new-label> [--id <entry-id>] | --pairs-from <file> [--dry-run]";
|
|
74859
|
+
var SAME_STRING_LINE = "The two arguments are the same string; nothing to do.";
|
|
74860
|
+
async function vaultRename(args, ctx) {
|
|
74861
|
+
const parsed = parseArgs(args, {
|
|
74862
|
+
valueFlags: ["--keystore", "--id", "--pairs-from"],
|
|
74863
|
+
booleanFlags: ["--accept-older-copy", "--dry-run"],
|
|
74864
|
+
pathFlags: ["--keystore", "--pairs-from"]
|
|
74865
|
+
});
|
|
74866
|
+
if ("error" in parsed)
|
|
74867
|
+
return usage(ctx, parsed.error);
|
|
74868
|
+
const [old, next, extra] = parsed.positionals;
|
|
74869
|
+
const pairsFile = parsed.values["--pairs-from"];
|
|
74870
|
+
const batch = pairsFile !== undefined;
|
|
74871
|
+
if (batch ? parsed.positionals.length > 0 || parsed.values["--id"] !== undefined : old === undefined || next === undefined || extra !== undefined || parsed.booleans.has("--dry-run")) {
|
|
74872
|
+
return usage(ctx, RENAME_USAGE);
|
|
74873
|
+
}
|
|
74874
|
+
if (!batch && next !== undefined) {
|
|
74875
|
+
const invalid = validateLabel(next);
|
|
74876
|
+
if (invalid !== undefined)
|
|
74877
|
+
return usage(ctx, invalid);
|
|
74878
|
+
if (old === next)
|
|
74879
|
+
return usage(ctx, SAME_STRING_LINE);
|
|
74880
|
+
}
|
|
74881
|
+
const id = parsed.values["--id"];
|
|
74882
|
+
if (!refuseEnvPassphrase(ctx))
|
|
74883
|
+
return 1;
|
|
74884
|
+
if (!requireTty(ctx, "vault rename"))
|
|
74885
|
+
return 1;
|
|
74886
|
+
const { deps } = ctx;
|
|
74887
|
+
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
74888
|
+
if ("error" in resolvedVault)
|
|
74889
|
+
return usage(ctx, resolvedVault.error);
|
|
74890
|
+
const path = resolvedVault.path;
|
|
74891
|
+
let rows = [];
|
|
74892
|
+
if (pairsFile !== undefined) {
|
|
74893
|
+
let contents;
|
|
74894
|
+
try {
|
|
74895
|
+
contents = await deps.readFile(pairsFile);
|
|
74896
|
+
} catch (error) {
|
|
74897
|
+
return usage(ctx, `Could not read --pairs-from: ${error instanceof Error ? error.message : error}`);
|
|
74898
|
+
}
|
|
74899
|
+
const parsedFile = parseRenamePairs(contents);
|
|
74900
|
+
if (parsedFile.findings.length > 0)
|
|
74901
|
+
return batchRefusal(ctx, parsedFile.findings, 2);
|
|
74902
|
+
rows = parsedFile.rows;
|
|
74903
|
+
}
|
|
74904
|
+
return runVaultCommand(ctx, async ({ hold }) => {
|
|
74905
|
+
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
74906
|
+
const opened = await unlockInteractively(ctx, path, raw, {
|
|
74907
|
+
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
74908
|
+
});
|
|
74909
|
+
const vault = hold(opened.vault);
|
|
74910
|
+
if (batch) {
|
|
74911
|
+
const { plan, findings } = planBatchRename(vault.index, rows);
|
|
74912
|
+
if (findings.length > 0)
|
|
74913
|
+
return batchRefusal(ctx, findings, 1);
|
|
74914
|
+
const renamed = plan.map(({ entry: entry2, to }) => ({
|
|
74915
|
+
id: entry2.id,
|
|
74916
|
+
address: entry2.address,
|
|
74917
|
+
role: entry2.role,
|
|
74918
|
+
from: entry2.label,
|
|
74919
|
+
to
|
|
74920
|
+
}));
|
|
74921
|
+
const dryRun = parsed.booleans.has("--dry-run");
|
|
74922
|
+
if (!dryRun) {
|
|
74923
|
+
const names = new Map(renamed.map(({ id: id2, to }) => [id2, to]));
|
|
74924
|
+
await commitVault(vault, {
|
|
74925
|
+
index: {
|
|
74926
|
+
hd: vault.index.hd,
|
|
74927
|
+
entries: vault.index.entries.map((entry2) => names.has(entry2.id) ? { ...entry2, label: names.get(entry2.id) } : entry2)
|
|
74928
|
+
}
|
|
74929
|
+
}, deps);
|
|
74930
|
+
}
|
|
74931
|
+
if (ctx.json)
|
|
74932
|
+
writeJson(deps, { ok: true, renamed, ...dryRun ? { dryRun: true } : {} });
|
|
74933
|
+
else {
|
|
74934
|
+
deps.stdout.write(dryRun ? `Dry run: nothing was written.
|
|
74935
|
+
` : `Renamed ${renamed.length} keys in one commit.
|
|
74936
|
+
`);
|
|
74937
|
+
for (const row of renamed)
|
|
74938
|
+
deps.stdout.write(` ${row.from} -> ${row.to} ${row.address} (${row.id}, ${row.role})
|
|
74939
|
+
`);
|
|
74940
|
+
}
|
|
74941
|
+
return 0;
|
|
74942
|
+
}
|
|
74943
|
+
if (old === undefined || next === undefined)
|
|
74944
|
+
return usage(ctx, RENAME_USAGE);
|
|
74945
|
+
const entry = resolveTarget(vault.index, old, id);
|
|
74946
|
+
if (entry.role === "tee-wallet") {
|
|
74947
|
+
throw new VaultError("VAULT_RENAME_ROLE_REFUSED", `${entry.label} is a TEE wallet. Its label was sent to Candle when it was enabled and \`candle wallets\` lists that copy, so renaming it here would give one wallet two names.`, {
|
|
74948
|
+
suggestion: `A vault key or an external wallet renames here. A TEE wallet renames on both sides with: candle tee rename ${entry.address} <new-label>`
|
|
74949
|
+
});
|
|
74950
|
+
}
|
|
74951
|
+
if (entry.label === next) {
|
|
74952
|
+
throw new VaultError("VAULT_LABEL_UNCHANGED", `${next} is already this key's label. Nothing was written.`, {
|
|
74953
|
+
suggestion: "Nothing to rename. `candle vault status --unlock` lists every label."
|
|
74954
|
+
});
|
|
74955
|
+
}
|
|
74956
|
+
if (entriesWithLabel(vault.index, next).length > 0) {
|
|
74957
|
+
throw new VaultError("VAULT_LABEL_TAKEN", `A key labelled ${next} already exists in this vault. Nothing was written.`, {
|
|
74958
|
+
suggestion: "Choose a name no key has, or rename that key first: `candle vault status --unlock` lists them."
|
|
74959
|
+
});
|
|
74960
|
+
}
|
|
74961
|
+
const from = entry.label;
|
|
74962
|
+
await commitVault(vault, {
|
|
74963
|
+
index: {
|
|
74964
|
+
hd: vault.index.hd,
|
|
74965
|
+
entries: vault.index.entries.map((candidate) => candidate.id === entry.id ? { ...candidate, label: next } : candidate)
|
|
74966
|
+
}
|
|
74967
|
+
}, deps);
|
|
74968
|
+
if (ctx.json) {
|
|
74969
|
+
writeJson(deps, { ok: true, id: entry.id, address: entry.address, role: entry.role, from, to: next });
|
|
74970
|
+
return 0;
|
|
74971
|
+
}
|
|
74972
|
+
deps.stdout.write(`Renamed ${from} to ${next}.
|
|
74973
|
+
`);
|
|
74974
|
+
deps.stdout.write(` address ${entry.address}
|
|
74975
|
+
`);
|
|
74976
|
+
deps.stdout.write(` id ${entry.id}
|
|
74977
|
+
`);
|
|
74978
|
+
deps.stdout.write(` role ${entry.role}
|
|
74979
|
+
`);
|
|
74980
|
+
deps.stdout.write(` unchanged address, derivation path, key blob, every envelope
|
|
74981
|
+
`);
|
|
74982
|
+
return 0;
|
|
74983
|
+
});
|
|
74984
|
+
}
|
|
74985
|
+
function batchRefusal(ctx, findings, exit) {
|
|
74986
|
+
const message = "Batch rename refused. Nothing was written.";
|
|
74987
|
+
if (ctx.json)
|
|
74988
|
+
writeJson(ctx.deps, { ok: false, code: "VAULT_RENAME_BATCH_REFUSED", message, findings });
|
|
74989
|
+
else {
|
|
74990
|
+
ctx.deps.stderr.write(`${message}
|
|
74991
|
+
`);
|
|
74992
|
+
for (const finding of findings)
|
|
74993
|
+
ctx.deps.stderr.write(` ${finding.line === 0 ? "file" : `line ${finding.line}`}: ${finding.problem}
|
|
74994
|
+
`);
|
|
74995
|
+
}
|
|
74996
|
+
return exit;
|
|
74997
|
+
}
|
|
74998
|
+
function resolveTarget(index, old, id) {
|
|
74999
|
+
const match = resolveRenameTarget(index, old, id);
|
|
75000
|
+
if (match.kind === "found")
|
|
75001
|
+
return match.entry;
|
|
75002
|
+
if (match.kind === "none") {
|
|
75003
|
+
throw new VaultError("VAULT_LABEL_NOT_FOUND", id === undefined ? `No key in this vault is called ${old}, and no key has that address or id.` : `No key in this vault has the id ${id}.`, { suggestion: "List them with their labels: `candle vault status --unlock`" });
|
|
75004
|
+
}
|
|
75005
|
+
const candidates = match.candidates.map((entry) => `${entry.address} (${entry.id})`).join(", ");
|
|
75006
|
+
const count2 = match.candidates.length;
|
|
75007
|
+
const first = match.candidates[0];
|
|
75008
|
+
throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${count2} keys in this vault are called ${old}, so this rename would not say which one it meant. Nothing was written.` : `${count2} keys in this vault have the address ${old}, so this rename would not say which one it meant. Nothing was written.`, {
|
|
75009
|
+
suggestion: match.by === "label" ? `Name one by address or id. The candidates are ${candidates}.` : `Name one by id. The candidates are ${candidates}. Re-run: \`candle vault rename ${old} <new-label> --id ${first.id}\`.`
|
|
75010
|
+
});
|
|
75011
|
+
}
|
|
75012
|
+
|
|
75013
|
+
// src/commands/tee-rename.ts
|
|
75014
|
+
init_vault_support();
|
|
75015
|
+
var RENAME_PATH = "/api/v1/agent/linked-wallets/rename";
|
|
75016
|
+
var TEE_RENAME_USAGE = "Usage: candle tee rename <label|address|id> <new-label> | --pairs-from <file> [--dry-run]";
|
|
75017
|
+
var DEVICE_TOKEN_REQUIRED2 = {
|
|
75018
|
+
code: "DEVICE_TOKEN_REQUIRED",
|
|
75019
|
+
message: "Renaming a TEE wallet needs the device token, the owner's credential; an API key cannot do it.",
|
|
75020
|
+
suggestion: "Run: candle auth login"
|
|
75021
|
+
};
|
|
75022
|
+
function rerunFile(rows) {
|
|
75023
|
+
if (rows.every((row) => !/[\s,"]/.test(row.to)))
|
|
75024
|
+
return rows.map((row) => `${row.from} ${row.to}`);
|
|
75025
|
+
return ["from,to", ...rows.map((row) => `${row.from},"${row.to.replaceAll('"', '""')}"`)];
|
|
75026
|
+
}
|
|
75027
|
+
function refusal(ctx, findings, exit) {
|
|
75028
|
+
const message = "TEE rename refused. Nothing was changed.";
|
|
75029
|
+
if (ctx.json)
|
|
75030
|
+
writeJson(ctx.deps, { ok: false, code: "TEE_RENAME_BATCH_REFUSED", message, findings });
|
|
75031
|
+
else {
|
|
75032
|
+
ctx.deps.stderr.write(`${message}
|
|
75033
|
+
`);
|
|
75034
|
+
for (const finding of findings)
|
|
75035
|
+
ctx.deps.stderr.write(` ${finding.line === 0 ? "file" : `line ${finding.line}`}: ${finding.problem}
|
|
75036
|
+
`);
|
|
75037
|
+
}
|
|
75038
|
+
return exit;
|
|
75039
|
+
}
|
|
75040
|
+
function serverFailure(ctx, result, pairs, batch) {
|
|
75041
|
+
const { deps, apiUrl, json } = ctx;
|
|
75042
|
+
if (result.status === 404) {
|
|
75043
|
+
writeLocalFailure(deps, {
|
|
75044
|
+
code: "TEE_RENAME_UNSUPPORTED",
|
|
75045
|
+
message: "This Candle API does not support renaming TEE wallets yet; nothing changed."
|
|
75046
|
+
}, json);
|
|
75047
|
+
return 1;
|
|
75048
|
+
}
|
|
75049
|
+
const raw = result.raw?.error?.findings;
|
|
75050
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
75051
|
+
const findings = raw.map((finding) => {
|
|
75052
|
+
const ids = Array.isArray(finding.matches) ? ` Candidates: ${finding.matches.map((m) => String(m.id)).join(", ")}.` : "";
|
|
75053
|
+
return {
|
|
75054
|
+
line: batch ? pairs[Number(finding.row)]?.line ?? 0 : 0,
|
|
75055
|
+
problem: `${String(finding.problem)}${ids}`
|
|
75056
|
+
};
|
|
75057
|
+
});
|
|
75058
|
+
if (batch)
|
|
75059
|
+
return refusal(ctx, findings, 1);
|
|
75060
|
+
writeLocalFailure(deps, {
|
|
75061
|
+
code: "TEE_RENAME_REFUSED",
|
|
75062
|
+
message: `${findings.map((finding) => finding.problem).join(" ")} Nothing was changed.`,
|
|
75063
|
+
suggestion: "Run: candle wallets, to see this account's linked wallets and their labels."
|
|
75064
|
+
}, json);
|
|
75065
|
+
return 1;
|
|
75066
|
+
}
|
|
75067
|
+
writeFailure(deps, result, { apiUrl, authType: "device" }, json);
|
|
75068
|
+
return 1;
|
|
75069
|
+
}
|
|
75070
|
+
function vaultEntriesFor(entries, row) {
|
|
75071
|
+
const lower = row.address.toLowerCase();
|
|
75072
|
+
return entries.filter((entry) => entry.role === "tee-wallet" && (entry.linkedWalletId === row.id || entry.address === row.address || entry.chain === "evm" && row.chain === "evm" && entry.address.toLowerCase() === lower));
|
|
75073
|
+
}
|
|
75074
|
+
function unknownCommit(ctx, rows, onServer, inVault) {
|
|
75075
|
+
const { deps, json } = ctx;
|
|
75076
|
+
const changing = new Set([...onServer.map((row) => row.id), ...inVault.map(({ row }) => row.id)]);
|
|
75077
|
+
const rerun = rows.filter((row) => changing.has(row.id)).map((row) => ({ from: row.address, to: row.label }));
|
|
75078
|
+
const message = `The rename request got no answer, so whether the server applied it is unknown. Nothing was written to the vault. ${rerun.length} wallet${rerun.length === 1 ? "" : "s"} may or may not carry the new name on the server.`;
|
|
75079
|
+
if (json)
|
|
75080
|
+
writeJson(deps, { ok: false, code: "TEE_RENAME_SERVER_UNKNOWN", message, rerun });
|
|
75081
|
+
else {
|
|
75082
|
+
deps.stderr.write(`${message}
|
|
75083
|
+
`);
|
|
75084
|
+
deps.stderr.write(`Re-run it with these rows, saved to a file (the old labels may no longer resolve):
|
|
75085
|
+
`);
|
|
75086
|
+
for (const line of rerunFile(rerun))
|
|
75087
|
+
deps.stderr.write(` ${line}
|
|
75088
|
+
`);
|
|
75089
|
+
deps.stderr.write(`Then run: candle tee rename --pairs-from <file>
|
|
75090
|
+
`);
|
|
75091
|
+
}
|
|
75092
|
+
return 1;
|
|
75093
|
+
}
|
|
75094
|
+
async function teeRename(args, ctx) {
|
|
75095
|
+
const { deps, apiUrl, json } = ctx;
|
|
75096
|
+
if (!refuseEnvPassphrase2(ctx))
|
|
75097
|
+
return 1;
|
|
75098
|
+
const parsed = parseArgs(args, {
|
|
75099
|
+
valueFlags: ["--pairs-from"],
|
|
75100
|
+
booleanFlags: ["--dry-run", "--accept-older-copy"],
|
|
75101
|
+
pathFlags: ["--pairs-from"]
|
|
75102
|
+
});
|
|
75103
|
+
if ("error" in parsed)
|
|
75104
|
+
return usage(ctx, parsed.error);
|
|
75105
|
+
const [old, next, extra] = parsed.positionals;
|
|
75106
|
+
const pairsFile = parsed.values["--pairs-from"];
|
|
75107
|
+
const batch = pairsFile !== undefined;
|
|
75108
|
+
const dryRun = parsed.booleans.has("--dry-run");
|
|
75109
|
+
if (batch ? parsed.positionals.length > 0 : old === undefined || next === undefined || extra !== undefined || dryRun) {
|
|
75110
|
+
return usage(ctx, TEE_RENAME_USAGE);
|
|
75111
|
+
}
|
|
75112
|
+
let pairs;
|
|
75113
|
+
if (pairsFile !== undefined) {
|
|
75114
|
+
let contents;
|
|
75115
|
+
try {
|
|
75116
|
+
contents = await deps.readFile(pairsFile);
|
|
75117
|
+
} catch (error) {
|
|
75118
|
+
return usage(ctx, `Could not read --pairs-from: ${error instanceof Error ? error.message : error}`);
|
|
75119
|
+
}
|
|
75120
|
+
const parsedFile = parseRenamePairs(contents);
|
|
75121
|
+
if (parsedFile.findings.length > 0)
|
|
75122
|
+
return refusal(ctx, parsedFile.findings, 2);
|
|
75123
|
+
pairs = parsedFile.rows;
|
|
75124
|
+
} else {
|
|
75125
|
+
const invalid = validateLabel(next);
|
|
75126
|
+
if (invalid !== undefined)
|
|
75127
|
+
return usage(ctx, invalid);
|
|
75128
|
+
if (old === next)
|
|
75129
|
+
return usage(ctx, SAME_STRING_LINE);
|
|
75130
|
+
pairs = [{ line: 0, from: old, to: next }];
|
|
75131
|
+
}
|
|
75132
|
+
await printIdentity(ctx);
|
|
75133
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
75134
|
+
if (!deviceToken) {
|
|
75135
|
+
writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
|
|
75136
|
+
return 1;
|
|
75137
|
+
}
|
|
75138
|
+
const call = (body) => apiRequest(RENAME_PATH, {
|
|
75139
|
+
method: "POST",
|
|
75140
|
+
body,
|
|
75141
|
+
auth: "device",
|
|
75142
|
+
credentials: { deviceToken },
|
|
75143
|
+
apiUrl,
|
|
75144
|
+
fetch: deps.fetch,
|
|
75145
|
+
env: deps.env
|
|
75146
|
+
});
|
|
75147
|
+
const preview = await call({
|
|
75148
|
+
dryRun: true,
|
|
75149
|
+
renames: pairs.map((pair) => ({ wallet: pair.from, label: pair.to }))
|
|
75150
|
+
});
|
|
75151
|
+
if (!preview.ok)
|
|
75152
|
+
return serverFailure(ctx, preview, pairs, batch);
|
|
75153
|
+
const shown = preview.body;
|
|
75154
|
+
const rows = [...shown.renamed ?? [], ...shown.unchanged ?? []].sort((a, b) => a.row - b.row);
|
|
75155
|
+
if (rows.length !== pairs.length || rows.some((row, at) => row.row !== at || row.label !== pairs[at]?.to)) {
|
|
75156
|
+
writeLocalFailure(deps, {
|
|
75157
|
+
code: "TEE_RENAME_PREVIEW_INVALID",
|
|
75158
|
+
message: "The API's preview did not answer every row; nothing was changed."
|
|
75159
|
+
}, json);
|
|
75160
|
+
return 1;
|
|
75161
|
+
}
|
|
75162
|
+
const vaultPath = vaultPathFor(ctx, { values: {}, booleans: new Set, positionals: [] });
|
|
75163
|
+
if ("error" in vaultPath)
|
|
75164
|
+
return usage(ctx, vaultPath.error);
|
|
75165
|
+
const raw = rows.some((row) => row.tee) ? await readVaultRaw(vaultPath.path) : null;
|
|
75166
|
+
if (raw !== null && !requireTty(ctx, "tee rename"))
|
|
75167
|
+
return 1;
|
|
75168
|
+
return runVaultCommand(ctx, async ({ hold }) => {
|
|
75169
|
+
const vault = raw === null ? null : hold((await unlockInteractively(ctx, vaultPath.path, raw, {
|
|
75170
|
+
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
75171
|
+
})).vault);
|
|
75172
|
+
const local = [];
|
|
75173
|
+
const findings = [];
|
|
75174
|
+
let twice = false;
|
|
75175
|
+
if (vault !== null) {
|
|
75176
|
+
for (const row of rows) {
|
|
75177
|
+
const line = pairs[row.row]?.line ?? 0;
|
|
75178
|
+
const entries = vaultEntriesFor(vault.index.entries, row);
|
|
75179
|
+
if (entries.length > 1) {
|
|
75180
|
+
twice = true;
|
|
75181
|
+
findings.push({
|
|
75182
|
+
line,
|
|
75183
|
+
problem: `The vault holds ${entries.length} TEE entries for ${row.address}; a rename would give them one name.`
|
|
75184
|
+
});
|
|
75185
|
+
} else if (entries[0] !== undefined)
|
|
75186
|
+
local.push({ row, entry: entries[0] });
|
|
75187
|
+
}
|
|
75188
|
+
const targets = new Set(local.map(({ entry }) => entry.id));
|
|
75189
|
+
for (const { row, entry } of local) {
|
|
75190
|
+
if (entry.label === row.label)
|
|
75191
|
+
continue;
|
|
75192
|
+
if (vault.index.entries.some((other) => other.label === row.label && !targets.has(other.id))) {
|
|
75193
|
+
findings.push({
|
|
75194
|
+
line: pairs[row.row]?.line ?? 0,
|
|
75195
|
+
problem: `New name ${row.label} is taken by a vault key outside this rename.`
|
|
75196
|
+
});
|
|
75197
|
+
}
|
|
75198
|
+
}
|
|
75199
|
+
}
|
|
75200
|
+
if (findings.length > 0) {
|
|
75201
|
+
if (batch)
|
|
75202
|
+
return refusal(ctx, findings, 1);
|
|
75203
|
+
writeLocalFailure(deps, {
|
|
75204
|
+
code: twice ? "VAULT_LABEL_AMBIGUOUS" : "VAULT_LABEL_TAKEN",
|
|
75205
|
+
message: `${findings.map((finding) => finding.problem).join(" ")} Nothing was changed.`,
|
|
75206
|
+
suggestion: twice ? "`candle vault list` shows both entries." : "Choose a name no vault key has: `candle vault list` shows them."
|
|
75207
|
+
}, json);
|
|
75208
|
+
return 1;
|
|
75209
|
+
}
|
|
75210
|
+
const localById = new Map(local.map(({ row, entry }) => [row.id, entry]));
|
|
75211
|
+
const receipt = rows.map((row) => {
|
|
75212
|
+
const entry = localById.get(row.id);
|
|
75213
|
+
return {
|
|
75214
|
+
id: row.id,
|
|
75215
|
+
chain: row.chain,
|
|
75216
|
+
address: row.address,
|
|
75217
|
+
from: row.from,
|
|
75218
|
+
to: row.label,
|
|
75219
|
+
server: row.from === row.label ? "unchanged" : "renamed",
|
|
75220
|
+
vault: entry === undefined ? "none" : entry.label === row.label ? "unchanged" : "renamed"
|
|
75221
|
+
};
|
|
75222
|
+
});
|
|
75223
|
+
const onServer = rows.filter((row) => row.from !== row.label);
|
|
75224
|
+
const inVault = local.filter(({ row, entry }) => entry.label !== row.label);
|
|
75225
|
+
const print = (headline) => {
|
|
75226
|
+
if (json)
|
|
75227
|
+
return writeJson(deps, { ok: true, renamed: receipt, ...dryRun ? { dryRun: true } : {} });
|
|
75228
|
+
deps.stdout.write(`${headline}
|
|
75229
|
+
`);
|
|
75230
|
+
for (const row of receipt) {
|
|
75231
|
+
const sides = row.server === "renamed" && row.vault === "renamed" ? "server and vault" : row.server === "renamed" ? "server" : row.vault === "renamed" ? "vault; the server already had this name" : "already named";
|
|
75232
|
+
const was = row.server === "renamed" ? row.from : localById.get(row.id)?.label ?? row.from;
|
|
75233
|
+
deps.stdout.write(` ${was ?? "(none)"} -> ${row.to} ${row.address} (${sides})
|
|
75234
|
+
`);
|
|
75235
|
+
}
|
|
75236
|
+
};
|
|
75237
|
+
if (dryRun) {
|
|
75238
|
+
print("Dry run: nothing was changed.");
|
|
75239
|
+
return 0;
|
|
75240
|
+
}
|
|
75241
|
+
if (onServer.length === 0 && inVault.length === 0) {
|
|
75242
|
+
print("Nothing to change: every wallet already has its new name.");
|
|
75243
|
+
return 0;
|
|
75244
|
+
}
|
|
75245
|
+
if (onServer.length > 0) {
|
|
75246
|
+
const committed = await call({ renames: onServer.map((row) => ({ id: row.id, label: row.label })) });
|
|
75247
|
+
if (!committed.ok) {
|
|
75248
|
+
if (committed.status === 0)
|
|
75249
|
+
return unknownCommit(ctx, rows, onServer, inVault);
|
|
75250
|
+
return serverFailure(ctx, committed, onServer.map((row) => pairs[row.row]), batch);
|
|
75251
|
+
}
|
|
75252
|
+
}
|
|
75253
|
+
if (vault !== null && inVault.length > 0) {
|
|
75254
|
+
const names = new Map(inVault.map(({ row, entry }) => [entry.id, row.label]));
|
|
75255
|
+
try {
|
|
75256
|
+
await commitVault(vault, {
|
|
75257
|
+
index: {
|
|
75258
|
+
hd: vault.index.hd,
|
|
75259
|
+
entries: vault.index.entries.map((entry) => names.has(entry.id) ? { ...entry, label: names.get(entry.id) } : entry)
|
|
75260
|
+
}
|
|
75261
|
+
}, deps);
|
|
75262
|
+
} catch (error) {
|
|
75263
|
+
const rerun = inVault.map(({ row }) => ({ from: row.address, to: row.label }));
|
|
75264
|
+
const cause = isVaultError(error) ? error.code : "VAULT_UNREADABLE";
|
|
75265
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
75266
|
+
const message = `The server renamed ${onServer.length} wallet${onServer.length === 1 ? "" : "s"}, but the vault write failed (${cause}: ${why}). ${rerun.length} vault ${rerun.length === 1 ? "entry still has its" : "entries still have their"} old name.`;
|
|
75267
|
+
if (json) {
|
|
75268
|
+
writeJson(deps, { ok: false, code: "TEE_RENAME_VAULT_INCOMPLETE", message, cause, rerun, renamed: receipt });
|
|
75269
|
+
} else {
|
|
75270
|
+
deps.stderr.write(`${message}
|
|
75271
|
+
`);
|
|
75272
|
+
deps.stderr.write(`Finish it with these rows, saved to a file (the old labels no longer resolve):
|
|
75273
|
+
`);
|
|
75274
|
+
for (const line of rerunFile(rerun))
|
|
75275
|
+
deps.stderr.write(` ${line}
|
|
75276
|
+
`);
|
|
75277
|
+
deps.stderr.write(`Then run: candle tee rename --pairs-from <file>
|
|
75278
|
+
`);
|
|
75279
|
+
}
|
|
75280
|
+
return 1;
|
|
75281
|
+
}
|
|
75282
|
+
}
|
|
75283
|
+
const changed = receipt.filter((row) => row.server === "renamed" || row.vault === "renamed").length;
|
|
75284
|
+
print(`Renamed ${changed} wallet${changed === 1 ? "" : "s"}.`);
|
|
75285
|
+
return 0;
|
|
75286
|
+
});
|
|
75287
|
+
}
|
|
75288
|
+
|
|
74621
75289
|
// src/commands/transfer.ts
|
|
74622
75290
|
init_args();
|
|
74623
75291
|
init_evm_lite();
|
|
@@ -79621,9 +80289,9 @@ async function refuseWithoutRoom(ctx) {
|
|
|
79621
80289
|
ctx.deps.stderr.write(unreadableRoomLine(read.reason));
|
|
79622
80290
|
return;
|
|
79623
80291
|
}
|
|
79624
|
-
const
|
|
79625
|
-
if (
|
|
79626
|
-
throw
|
|
80292
|
+
const refusal2 = singleRoomRefusal(read.room);
|
|
80293
|
+
if (refusal2 !== null)
|
|
80294
|
+
throw refusal2;
|
|
79627
80295
|
}
|
|
79628
80296
|
async function reopenFromDisk(path, reopen, previous) {
|
|
79629
80297
|
closeVault(previous);
|
|
@@ -80396,15 +81064,15 @@ function annotate(error, row, classified, pinnedBy, promotedBy) {
|
|
|
80396
81064
|
var CONTINUATION_SENTENCE = (total) => `Each row was checked against the vault as it will be when the rows above it have run, so this is
|
|
80397
81065
|
` + `every row that fails on that basis. Fix these rows and run again; the next run re-checks all ${total}
|
|
80398
81066
|
` + `from scratch.`;
|
|
80399
|
-
function writeBatchRefusal(deps, json,
|
|
80400
|
-
const n =
|
|
81067
|
+
function writeBatchRefusal(deps, json, refusal2) {
|
|
81068
|
+
const n = refusal2.rows.length;
|
|
80401
81069
|
if (json) {
|
|
80402
81070
|
deps.stdout.write(`${JSON.stringify({
|
|
80403
81071
|
ok: false,
|
|
80404
81072
|
code: "PROMOTE_BATCH_REFUSED",
|
|
80405
|
-
message: `${n} of ${
|
|
81073
|
+
message: `${n} of ${refusal2.total} rows cannot run; nothing was written.`,
|
|
80406
81074
|
suggestion: "Fix the rows below and run again; the next run re-checks every row from scratch.",
|
|
80407
|
-
rows:
|
|
81075
|
+
rows: refusal2.rows.map((row) => ({
|
|
80408
81076
|
line: row.line,
|
|
80409
81077
|
label: row.label,
|
|
80410
81078
|
destination: row.destination,
|
|
@@ -80416,18 +81084,18 @@ function writeBatchRefusal(deps, json, refusal) {
|
|
|
80416
81084
|
`);
|
|
80417
81085
|
return;
|
|
80418
81086
|
}
|
|
80419
|
-
const table = renderTable(["line", "label", "destination", "code", "why"],
|
|
81087
|
+
const table = renderTable(["line", "label", "destination", "code", "why"], refusal2.rows.map((row) => [
|
|
80420
81088
|
String(row.line),
|
|
80421
81089
|
row.label,
|
|
80422
81090
|
row.destination,
|
|
80423
81091
|
row.code,
|
|
80424
81092
|
row.why !== undefined ? `${row.message} ${row.why}` : row.message
|
|
80425
81093
|
]));
|
|
80426
|
-
deps.stderr.write(`This batch was refused and NOTHING was written. ${n} of ${
|
|
81094
|
+
deps.stderr.write(`This batch was refused and NOTHING was written. ${n} of ${refusal2.total} rows cannot run:
|
|
80427
81095
|
|
|
80428
81096
|
${table}
|
|
80429
81097
|
|
|
80430
|
-
${CONTINUATION_SENTENCE(
|
|
81098
|
+
${CONTINUATION_SENTENCE(refusal2.total)}
|
|
80431
81099
|
`);
|
|
80432
81100
|
}
|
|
80433
81101
|
function parseValueUsdCell(raw) {
|
|
@@ -81248,159 +81916,6 @@ function finish(ctx, opts) {
|
|
|
81248
81916
|
return exit;
|
|
81249
81917
|
}
|
|
81250
81918
|
|
|
81251
|
-
// src/commands/vault-rename.ts
|
|
81252
|
-
init_args();
|
|
81253
|
-
init_errors();
|
|
81254
|
-
|
|
81255
|
-
// src/vault/labels.ts
|
|
81256
|
-
function entriesWithLabel(index, label) {
|
|
81257
|
-
return index.entries.filter((entry) => entry.label === label);
|
|
81258
|
-
}
|
|
81259
|
-
function duplicateLabels(index) {
|
|
81260
|
-
const byLabel = new Map;
|
|
81261
|
-
for (const entry of index.entries) {
|
|
81262
|
-
const holders = byLabel.get(entry.label);
|
|
81263
|
-
if (holders === undefined)
|
|
81264
|
-
byLabel.set(entry.label, [entry]);
|
|
81265
|
-
else
|
|
81266
|
-
holders.push(entry);
|
|
81267
|
-
}
|
|
81268
|
-
const out = [];
|
|
81269
|
-
for (const [label, entries] of byLabel) {
|
|
81270
|
-
if (entries.length > 1)
|
|
81271
|
-
out.push({ label, entries });
|
|
81272
|
-
}
|
|
81273
|
-
return out;
|
|
81274
|
-
}
|
|
81275
|
-
function hasControlCharacter(label) {
|
|
81276
|
-
for (const char of label) {
|
|
81277
|
-
const code = char.codePointAt(0) ?? 0;
|
|
81278
|
-
if (code < 32 || code === 127)
|
|
81279
|
-
return true;
|
|
81280
|
-
}
|
|
81281
|
-
return false;
|
|
81282
|
-
}
|
|
81283
|
-
function validateLabel(label) {
|
|
81284
|
-
if (label.trim().length === 0)
|
|
81285
|
-
return "A key's label cannot be empty.";
|
|
81286
|
-
if (hasControlCharacter(label)) {
|
|
81287
|
-
return "A key's label cannot contain a newline, a tab or a control character.";
|
|
81288
|
-
}
|
|
81289
|
-
if (label.startsWith("-")) {
|
|
81290
|
-
return `A key's label cannot begin with "-": it would be read as a flag everywhere a label is typed.`;
|
|
81291
|
-
}
|
|
81292
|
-
return;
|
|
81293
|
-
}
|
|
81294
|
-
function resolveRenameTarget(index, old, id) {
|
|
81295
|
-
if (id !== undefined) {
|
|
81296
|
-
const entry = index.entries.find((candidate) => candidate.id === id);
|
|
81297
|
-
return entry === undefined ? { kind: "none" } : { kind: "found", entry, by: "id" };
|
|
81298
|
-
}
|
|
81299
|
-
const byLabel = entriesWithLabel(index, old);
|
|
81300
|
-
if (byLabel.length === 1)
|
|
81301
|
-
return { kind: "found", entry: byLabel[0], by: "label" };
|
|
81302
|
-
if (byLabel.length > 1)
|
|
81303
|
-
return { kind: "ambiguous", by: "label", candidates: byLabel };
|
|
81304
|
-
const byAddress = index.entries.filter((entry) => entry.address === old);
|
|
81305
|
-
if (byAddress.length === 1)
|
|
81306
|
-
return { kind: "found", entry: byAddress[0], by: "address" };
|
|
81307
|
-
if (byAddress.length > 1)
|
|
81308
|
-
return { kind: "ambiguous", by: "address", candidates: byAddress };
|
|
81309
|
-
return { kind: "none" };
|
|
81310
|
-
}
|
|
81311
|
-
|
|
81312
|
-
// src/commands/vault-rename.ts
|
|
81313
|
-
init_store();
|
|
81314
|
-
init_vault_support();
|
|
81315
|
-
var RENAME_USAGE = "Usage: candle vault rename <label|address> <new-label> [--id <entry-id>]";
|
|
81316
|
-
var SAME_STRING_LINE = "The two arguments are the same string; nothing to do.";
|
|
81317
|
-
async function vaultRename(args, ctx) {
|
|
81318
|
-
const parsed = parseArgs(args, {
|
|
81319
|
-
valueFlags: ["--keystore", "--id"],
|
|
81320
|
-
booleanFlags: ["--accept-older-copy"],
|
|
81321
|
-
pathFlags: ["--keystore"]
|
|
81322
|
-
});
|
|
81323
|
-
if ("error" in parsed)
|
|
81324
|
-
return usage(ctx, parsed.error);
|
|
81325
|
-
const [old, next, extra] = parsed.positionals;
|
|
81326
|
-
if (old === undefined || next === undefined || extra !== undefined)
|
|
81327
|
-
return usage(ctx, RENAME_USAGE);
|
|
81328
|
-
const invalid = validateLabel(next);
|
|
81329
|
-
if (invalid !== undefined)
|
|
81330
|
-
return usage(ctx, invalid);
|
|
81331
|
-
if (old === next)
|
|
81332
|
-
return usage(ctx, SAME_STRING_LINE);
|
|
81333
|
-
const id = parsed.values["--id"];
|
|
81334
|
-
if (!refuseEnvPassphrase(ctx))
|
|
81335
|
-
return 1;
|
|
81336
|
-
if (!requireTty(ctx, "vault rename"))
|
|
81337
|
-
return 1;
|
|
81338
|
-
const { deps } = ctx;
|
|
81339
|
-
const resolvedVault = vaultPathFor(ctx, parsed);
|
|
81340
|
-
if ("error" in resolvedVault)
|
|
81341
|
-
return usage(ctx, resolvedVault.error);
|
|
81342
|
-
const path = resolvedVault.path;
|
|
81343
|
-
return runVaultCommand(ctx, async ({ hold }) => {
|
|
81344
|
-
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
81345
|
-
const opened = await unlockInteractively(ctx, path, raw, {
|
|
81346
|
-
acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
|
|
81347
|
-
});
|
|
81348
|
-
const vault = hold(opened.vault);
|
|
81349
|
-
const entry = resolveTarget(vault.index, old, id);
|
|
81350
|
-
if (entry.role === "tee-wallet") {
|
|
81351
|
-
throw new VaultError("VAULT_RENAME_ROLE_REFUSED", `${entry.label} is a TEE wallet. Its label was sent to Candle when it was enabled and \`candle wallets\` lists that copy, so renaming it here would give one wallet two names and nothing reconciles them.`, {
|
|
81352
|
-
suggestion: "A vault key or an external wallet renames here. For a TEE wallet, nothing in this release changes the name on either side."
|
|
81353
|
-
});
|
|
81354
|
-
}
|
|
81355
|
-
if (entry.label === next) {
|
|
81356
|
-
throw new VaultError("VAULT_LABEL_UNCHANGED", `${next} is already this key's label. Nothing was written.`, {
|
|
81357
|
-
suggestion: "Nothing to rename. `candle vault status --unlock` lists every label."
|
|
81358
|
-
});
|
|
81359
|
-
}
|
|
81360
|
-
if (entriesWithLabel(vault.index, next).length > 0) {
|
|
81361
|
-
throw new VaultError("VAULT_LABEL_TAKEN", `A key labelled ${next} already exists in this vault. Nothing was written.`, {
|
|
81362
|
-
suggestion: "Choose a name no key has, or rename that key first: `candle vault status --unlock` lists them."
|
|
81363
|
-
});
|
|
81364
|
-
}
|
|
81365
|
-
const from = entry.label;
|
|
81366
|
-
await commitVault(vault, {
|
|
81367
|
-
index: {
|
|
81368
|
-
hd: vault.index.hd,
|
|
81369
|
-
entries: vault.index.entries.map((candidate) => candidate.id === entry.id ? { ...candidate, label: next } : candidate)
|
|
81370
|
-
}
|
|
81371
|
-
}, deps);
|
|
81372
|
-
if (ctx.json) {
|
|
81373
|
-
writeJson(deps, { ok: true, id: entry.id, address: entry.address, role: entry.role, from, to: next });
|
|
81374
|
-
return 0;
|
|
81375
|
-
}
|
|
81376
|
-
deps.stdout.write(`Renamed ${from} to ${next}.
|
|
81377
|
-
`);
|
|
81378
|
-
deps.stdout.write(` address ${entry.address}
|
|
81379
|
-
`);
|
|
81380
|
-
deps.stdout.write(` id ${entry.id}
|
|
81381
|
-
`);
|
|
81382
|
-
deps.stdout.write(` role ${entry.role}
|
|
81383
|
-
`);
|
|
81384
|
-
deps.stdout.write(` unchanged address, derivation path, key blob, every envelope
|
|
81385
|
-
`);
|
|
81386
|
-
return 0;
|
|
81387
|
-
});
|
|
81388
|
-
}
|
|
81389
|
-
function resolveTarget(index, old, id) {
|
|
81390
|
-
const match = resolveRenameTarget(index, old, id);
|
|
81391
|
-
if (match.kind === "found")
|
|
81392
|
-
return match.entry;
|
|
81393
|
-
if (match.kind === "none") {
|
|
81394
|
-
throw new VaultError("VAULT_LABEL_NOT_FOUND", id === undefined ? `No key in this vault is called ${old}, and no key has that address or id.` : `No key in this vault has the id ${id}.`, { suggestion: "List them with their labels: `candle vault status --unlock`" });
|
|
81395
|
-
}
|
|
81396
|
-
const candidates = match.candidates.map((entry) => `${entry.address} (${entry.id})`).join(", ");
|
|
81397
|
-
const count2 = match.candidates.length;
|
|
81398
|
-
const first = match.candidates[0];
|
|
81399
|
-
throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${count2} keys in this vault are called ${old}, so this rename would not say which one it meant. Nothing was written.` : `${count2} keys in this vault have the address ${old}, so this rename would not say which one it meant. Nothing was written.`, {
|
|
81400
|
-
suggestion: match.by === "label" ? `Name one by address or id. The candidates are ${candidates}.` : `Name one by id. The candidates are ${candidates}. Re-run: \`candle vault rename ${old} <new-label> --id ${first.id}\`.`
|
|
81401
|
-
});
|
|
81402
|
-
}
|
|
81403
|
-
|
|
81404
81919
|
// src/commands/vault-restore.ts
|
|
81405
81920
|
init_args();
|
|
81406
81921
|
init_deps();
|
|
@@ -82810,7 +83325,7 @@ var PREVIEW_PATH = "/api/v1/agent/tee-wallets/allow-launch/preview";
|
|
|
82810
83325
|
var capabilityPath = (id) => `/api/v1/agent/wallets/${encodeURIComponent(id)}/capabilities`;
|
|
82811
83326
|
var USAGE_ALLOW = "Usage: candle wallets allow-launch <label|address|id|prefix*>... [--json]";
|
|
82812
83327
|
var USAGE_DISALLOW = "Usage: candle wallets disallow-launch <label|address|id|prefix*>... [--yes] [--json]";
|
|
82813
|
-
var
|
|
83328
|
+
var DEVICE_TOKEN_REQUIRED3 = {
|
|
82814
83329
|
code: "DEVICE_TOKEN_REQUIRED",
|
|
82815
83330
|
message: "Changing whether a TEE wallet may launch needs the device token, the owner's credential; an API key cannot do it.",
|
|
82816
83331
|
suggestion: "Run: candle auth login"
|
|
@@ -82879,7 +83394,7 @@ async function setAllowLaunch(args, ctx, enabled) {
|
|
|
82879
83394
|
await printIdentity(ctx);
|
|
82880
83395
|
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
82881
83396
|
if (!deviceToken) {
|
|
82882
|
-
writeLocalFailure(deps,
|
|
83397
|
+
writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED3, json);
|
|
82883
83398
|
return 1;
|
|
82884
83399
|
}
|
|
82885
83400
|
if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
|
|
@@ -83127,7 +83642,7 @@ init_promote_support();
|
|
|
83127
83642
|
var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
|
|
83128
83643
|
var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
|
|
83129
83644
|
var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
|
|
83130
|
-
var
|
|
83645
|
+
var DEVICE_TOKEN_REQUIRED4 = {
|
|
83131
83646
|
code: "DEVICE_TOKEN_REQUIRED",
|
|
83132
83647
|
message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
|
|
83133
83648
|
suggestion: "Run: candle auth login"
|
|
@@ -83205,7 +83720,7 @@ async function setTrust(args, ctx, trusted) {
|
|
|
83205
83720
|
await printIdentity(ctx);
|
|
83206
83721
|
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
83207
83722
|
if (!deviceToken) {
|
|
83208
|
-
writeLocalFailure(deps,
|
|
83723
|
+
writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED4, json);
|
|
83209
83724
|
return 1;
|
|
83210
83725
|
}
|
|
83211
83726
|
if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
|
|
@@ -83542,6 +84057,7 @@ var COMMANDS = {
|
|
|
83542
84057
|
sweep: teeSweep,
|
|
83543
84058
|
rebind: teeRebind,
|
|
83544
84059
|
rebinds: teeRebinds,
|
|
84060
|
+
rename: teeRename,
|
|
83545
84061
|
signer: teeSigner
|
|
83546
84062
|
}
|
|
83547
84063
|
},
|
package/package.json
CHANGED