@candledottv/cli 0.11.7 → 0.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +134 -12
  2. package/dist/index.js +2560 -433
  3. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -278,6 +278,16 @@ var init_args = __esm(() => {
278
278
  });
279
279
 
280
280
  // src/agent-key-access.ts
281
+ function scopesForPreset(preset) {
282
+ switch (preset) {
283
+ case "read":
284
+ return [...READ_SCOPES];
285
+ case "readwrite":
286
+ return [...READWRITE_SCOPES];
287
+ case "readwritetransfer":
288
+ return [...READWRITETRANSFER_SCOPES];
289
+ }
290
+ }
281
291
  function sameSet(scopes, preset) {
282
292
  const held = new Set(scopes);
283
293
  return held.size === preset.length && preset.every((scope) => held.has(scope));
@@ -287,6 +297,8 @@ function presetForScopes(scopes) {
287
297
  return "read";
288
298
  if (sameSet(scopes, READWRITE_SCOPES))
289
299
  return "readwrite";
300
+ if (sameSet(scopes, READWRITETRANSFER_SCOPES))
301
+ return "readwritetransfer";
290
302
  return null;
291
303
  }
292
304
  function agentKeyCapabilities(scopes) {
@@ -295,6 +307,7 @@ function agentKeyCapabilities(scopes) {
295
307
  launch: scopes.includes("launch:write"),
296
308
  trade: scopes.includes("swap:write"),
297
309
  transfer: scopes.includes("transfer:write"),
310
+ linkedTransfer: scopes.includes("transfer:bound"),
298
311
  report: scopes.includes("activity:write")
299
312
  };
300
313
  }
@@ -308,7 +321,7 @@ function agentKeyAccess(scopes) {
308
321
  function sortAgentKeyScopes(scopes) {
309
322
  return [...scopes].sort();
310
323
  }
311
- var READ_SCOPES, READWRITE_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
324
+ var READ_SCOPES, READWRITE_SCOPES, READWRITETRANSFER_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
312
325
  var init_agent_key_access = __esm(() => {
313
326
  READ_SCOPES = ["account:read"];
314
327
  READWRITE_SCOPES = [
@@ -319,18 +332,32 @@ var init_agent_key_access = __esm(() => {
319
332
  "transfer:write",
320
333
  "account:read"
321
334
  ];
322
- AGENT_KEY_PRESET_LABELS = { read: "Read", readwrite: "Read:Write" };
335
+ READWRITETRANSFER_SCOPES = [
336
+ "launch:write",
337
+ "launch:read",
338
+ "activity:write",
339
+ "swap:write",
340
+ "transfer:write",
341
+ "account:read",
342
+ "transfer:bound"
343
+ ];
344
+ AGENT_KEY_PRESET_LABELS = {
345
+ read: "Read",
346
+ readwrite: "Read:Write",
347
+ readwritetransfer: "Read:Write:Transfer"
348
+ };
323
349
  AGENT_KEY_CAPABILITY_CHIPS = [
324
350
  ["Launch", "launch"],
325
351
  ["Trade", "trade"],
326
352
  ["Transfer", "transfer"],
353
+ ["Linked transfer", "linkedTransfer"],
327
354
  ["Report", "report"]
328
355
  ];
329
356
  });
330
357
 
331
358
  // src/render.ts
332
359
  function formatScopesForSummary(scopes) {
333
- return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope).join(", ");
360
+ return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope === "transfer:bound" ? `${scope} (${TRANSFER_BOUND_NOTE})` : scope).join(", ");
334
361
  }
335
362
  function terminalText(value) {
336
363
  return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
@@ -436,7 +463,7 @@ function portalDeviceUrl(apiUrl, portalOrigin) {
436
463
  return `${apiUrl}/agents`;
437
464
  }
438
465
  }
439
- var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets";
466
+ var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets", TRANSFER_BOUND_NOTE = "moves funds -- this key can move funds out of the wallet it runs, to your linked wallets and its vault";
440
467
  var init_render = __esm(() => {
441
468
  init_agent_key_access();
442
469
  ALL_AGENT_SCOPES = [
@@ -445,9 +472,10 @@ var init_render = __esm(() => {
445
472
  "account:read",
446
473
  "activity:write",
447
474
  "swap:write",
448
- "transfer:write"
475
+ "transfer:write",
476
+ "transfer:bound"
449
477
  ];
450
- DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write");
478
+ DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write" && scope !== "transfer:bound");
451
479
  });
452
480
 
453
481
  // src/profiles.ts
@@ -1021,6 +1049,9 @@ function createView(arr) {
1021
1049
  function rotr(word, shift) {
1022
1050
  return word << 32 - shift | word >>> shift;
1023
1051
  }
1052
+ function rotl(word, shift) {
1053
+ return word << shift | word >>> 32 - shift >>> 0;
1054
+ }
1024
1055
  function byteSwap(word) {
1025
1056
  return word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255;
1026
1057
  }
@@ -2086,7 +2117,7 @@ var gcd = (a, b) => b === 0 ? a : gcd(b, a % b), radix2carry = (from, to) => fro
2086
2117
  if (s.length > 0 && !re.test(s))
2087
2118
  throw new Error("invalid base64");
2088
2119
  return Uint8Array.fromBase64(s, { alphabet: alphabet2, lastChunkHandling: "strict" });
2089
- }, base64, base64nopad, base64url, base64urlnopad, genBase58 = (abc) => chain(radix(58), alphabet(abc), join3("")), base58, base58flickr, base58xrp, BECH_ALPHABET, POLYMOD_GENERATORS, bech32, bech32m, hasHexBuiltin2, hexBuiltin, hex;
2120
+ }, base64, base64nopad, base64url, base64urlnopad, genBase58 = (abc) => chain(radix(58), alphabet(abc), join3("")), base58, base58flickr, base58xrp, createBase58check = (sha2563) => chain(checksum(4, (data) => sha2563(sha2563(data))), base58), BECH_ALPHABET, POLYMOD_GENERATORS, bech32, bech32m, hasHexBuiltin2, hexBuiltin, hex;
2090
2121
  var init_esm = __esm(() => {
2091
2122
  /*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */
2092
2123
  powers = /* @__PURE__ */ (() => {
@@ -2164,6 +2195,9 @@ function libraryInstallInstruction(platform) {
2164
2195
  }
2165
2196
 
2166
2197
  // src/fido2-helper/protocol.ts
2198
+ function excludeCapacity(device) {
2199
+ return device.maxCredentialCountInList ?? 1;
2200
+ }
2167
2201
  var HELPER_PROTOCOL = 1, RP_ID = "cli.candle.tv", PROBE_MAX_CREDENTIALS = 16, AUTHDATA_FLAG_UP = 1, AUTHDATA_FLAG_UV = 4, AUTHDATA_MIN_LENGTH = 37;
2168
2202
  var init_protocol = () => {};
2169
2203
 
@@ -3144,7 +3178,19 @@ var init_errors = __esm(() => {
3144
3178
  "TIER_REQUIRED",
3145
3179
  "LINKED_WALLET_ROOM_UNREADABLE",
3146
3180
  "PROMOTE_ACCOUNT_UNRESOLVED",
3147
- "TRANSFER_SWEEP_PENDING"
3181
+ "TRANSFER_SWEEP_PENDING",
3182
+ "VAULT_KEY_ALREADY_ENROLLED",
3183
+ "EVM_CHAIN_MISMATCH",
3184
+ "EVM_NONCE_STALE",
3185
+ "EVM_TOKEN_UNREADABLE",
3186
+ "EVM_AMOUNT_PRECISION",
3187
+ "EVM_INSUFFICIENT_FOR_FEES",
3188
+ "EVM_DESTINATION_INVALID",
3189
+ "EVM_SELF_TRANSFER",
3190
+ "EVM_RECIPIENT_IS_TOKEN",
3191
+ "EVM_TRANSFER_REVERTED",
3192
+ "TRANSFER_CHAIN_MISMATCH",
3193
+ "SOLANA_COMMAND_EVM_KEY"
3148
3194
  ];
3149
3195
  VaultError = class VaultError extends Error {
3150
3196
  code;
@@ -5957,6 +6003,9 @@ function translateHelperFailure(code, message, platform) {
5957
6003
  LIBRARY_MISSING: "VAULT_HELPER_MISSING"
5958
6004
  };
5959
6005
  const suggestion = "Nothing was derived and no other factor was tried.";
6006
+ if (code === "CREDENTIAL_EXCLUDED") {
6007
+ return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `This security key already holds one of this vault's credentials, so enrolling it again would not add a second key (${message}). Nothing was written.`, { suggestion: ALREADY_ENROLLED_SUGGESTION });
6008
+ }
5960
6009
  if (code === "DEVICE_NOT_READABLE") {
5961
6010
  return new VaultError("VAULT_AUTHENTICATOR_NOT_READABLE", platform === "linux" ? HIDRAW_MESSAGE : `A security key is attached but this user cannot open it: ${message}.`, { suggestion: "No other factor is substituted." });
5962
6011
  }
@@ -6017,6 +6066,51 @@ async function callHelper(deps, helperPath, request) {
6017
6066
  throw translateHelperFailure(String(response.code), String(response.message), deps.platform);
6018
6067
  return response;
6019
6068
  }
6069
+ function alreadyEnrolled(device, envelopeId) {
6070
+ const product = device.product || "This security key";
6071
+ return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `${product} (--device ${device.deviceId}) already holds this vault's security key credential${envelopeId !== undefined ? ` for envelope ${envelopeId}` : ""}, so enrolling it again would not add a second key. Nothing was written.`, {
6072
+ suggestion: ALREADY_ENROLLED_SUGGESTION,
6073
+ ...envelopeId !== undefined ? { details: { envelopeId } } : {}
6074
+ });
6075
+ }
6076
+ function helperPredatesExcludeList(helperPath, leftover) {
6077
+ return new VaultError("VAULT_HELPER_MISSING", `The security key helper at ${helperPath} predates the exclude list this command needs; reinstall the CLI so candle and candle-fido2 come from the same release.`, {
6078
+ suggestion: leftover ? "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again." : "Nothing was sent to any key and nothing was written. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again."
6079
+ });
6080
+ }
6081
+ function assertHelperExcludeList(info, helperPath, excludeCount) {
6082
+ if (excludeCount === 0)
6083
+ return;
6084
+ const features = Array.isArray(info.features) ? info.features : [];
6085
+ if (!features.includes(EXCLUDE_LIST_FEATURE))
6086
+ throw helperPredatesExcludeList(helperPath, false);
6087
+ }
6088
+ function assertExcludeListFits(device, excludeCount) {
6089
+ const capacity = excludeCapacity(device);
6090
+ if (excludeCount <= capacity)
6091
+ return;
6092
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${device.product || "This security key"} (--device ${device.deviceId}) takes at most ${capacity} credential${capacity === 1 ? "" : "s"} in one request${device.maxCredentialCountInList === null ? " (it reports no maxCredentialCountInList)" : ""}, and this vault has ${excludeCount} security key credentials to exclude, so it cannot enroll on this vault. Nothing was sent to it.`, { suggestion: "Nothing was written. Use a security key that reports a larger maxCredentialCountInList." });
6093
+ }
6094
+ function assertDeviceServesFactor(device) {
6095
+ if (!device.extensions.includes("hmac-secret")) {
6096
+ throw new VaultError("VAULT_PRF_UNSUPPORTED", `${device.product || "This security key"} does not support the hmac-secret extension, which this factor needs.`, { suggestion: "Use a key that supports hmac-secret (FIDO2 with PRF). No other derivation is substituted." });
6097
+ }
6098
+ if (device.options.clientPin !== true && device.options.uv !== true) {
6099
+ throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
6100
+ }
6101
+ }
6102
+ async function listSecurityKeys(deps, opts) {
6103
+ const location = await locateFido2Helper(deps);
6104
+ if (location.state === "absent")
6105
+ throw helperMissing(location);
6106
+ const info = await callHelper(deps, location.path, {
6107
+ op: "info",
6108
+ vaultId: opts.vaultId,
6109
+ envelopeId: opts.envelopeId,
6110
+ digest: base64.encode(operationDigest({ vaultId: opts.vaultId, envelopeId: opts.envelopeId, op: "info", nonce: b64u(randomBytes2(16)) }))
6111
+ });
6112
+ return { helperPath: location.path, info };
6113
+ }
6020
6114
  function describeDeviceForList(device) {
6021
6115
  const product = device.product || "security key";
6022
6116
  return `--device ${device.deviceId} ${product}${device.manufacturer ? ` (${device.manufacturer})` : ""}${device.readable ? "" : " [not readable by this user]"}`;
@@ -6099,17 +6193,12 @@ async function openSecurityKeySession(deps, opts) {
6099
6193
  deps.stderr.write(`Using the attached security key: ${device.product || "security key"} (--device ${device.deviceId})
6100
6194
  `);
6101
6195
  }
6102
- if (opts.requireFeatures) {
6103
- if (!device.extensions.includes("hmac-secret")) {
6104
- throw new VaultError("VAULT_PRF_UNSUPPORTED", `${device.product || "This security key"} does not support the hmac-secret extension, which this factor needs.`, { suggestion: "Use a key that supports hmac-secret (FIDO2 with PRF). No other derivation is substituted." });
6105
- }
6106
- if (device.options.clientPin !== true && device.options.uv !== true) {
6107
- throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
6108
- }
6109
- }
6196
+ if (opts.requireFeatures)
6197
+ assertDeviceServesFactor(device);
6198
+ opts.beforePin?.(device, info);
6110
6199
  const session = { helperPath: location.path, device, snapshotId: info.snapshotId };
6111
6200
  if (device.options.clientPin === true) {
6112
- const typed = await deps.promptSecret(`PIN for ${device.product || "the security key"} (input hidden): `);
6201
+ const typed = await deps.promptSecret(opts.pinPrompt?.(device) ?? `PIN for ${device.product || "the security key"} (input hidden): `);
6113
6202
  if (typed === "") {
6114
6203
  throw new VaultError("VAULT_PIN_REQUIRED", "This security key needs its PIN and none was typed; nothing was sent to it.");
6115
6204
  }
@@ -6163,7 +6252,7 @@ async function probeAttachedKeys(deps, opts) {
6163
6252
  if (opts.deviceFlag === undefined && allReadable && !uncertain)
6164
6253
  state.set(envelopeId, "not attached");
6165
6254
  }
6166
- return { state, holder };
6255
+ return { state, holder, devices, credentials: credentialOf };
6167
6256
  } catch {
6168
6257
  return;
6169
6258
  }
@@ -6171,20 +6260,37 @@ async function probeAttachedKeys(deps, opts) {
6171
6260
  async function registerCredential(deps, session, opts) {
6172
6261
  deps.stderr.write(`Touch ${session.device.product || "the security key"} to register the vault's credential on it.
6173
6262
  `);
6174
- const digest = operationDigest({ ...opts, op: "register", nonce: b64u(randomBytes2(16)) });
6175
- const response = await callHelper(deps, session.helperPath, {
6176
- op: "register",
6263
+ const digest = operationDigest({
6177
6264
  vaultId: opts.vaultId,
6178
6265
  envelopeId: opts.envelopeId,
6179
- digest: base64.encode(digest),
6180
- deviceId: session.device.deviceId,
6181
- expectSnapshot: session.snapshotId,
6182
- rpId: RP_ID,
6183
- userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
6184
- userName: userNameFor(opts.vaultId, opts.envelopeId),
6185
- clientDataHash: base64.encode(digest),
6186
- ...session.pin !== undefined ? { pin: session.pin } : {}
6266
+ op: "register",
6267
+ nonce: b64u(randomBytes2(16))
6187
6268
  });
6269
+ const exclude = opts.excludeCredentialIds ?? [];
6270
+ let response;
6271
+ try {
6272
+ response = await callHelper(deps, session.helperPath, {
6273
+ op: "register",
6274
+ vaultId: opts.vaultId,
6275
+ envelopeId: opts.envelopeId,
6276
+ digest: base64.encode(digest),
6277
+ deviceId: session.device.deviceId,
6278
+ expectSnapshot: session.snapshotId,
6279
+ rpId: RP_ID,
6280
+ userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
6281
+ userName: userNameFor(opts.vaultId, opts.envelopeId),
6282
+ clientDataHash: base64.encode(digest),
6283
+ ...session.pin !== undefined ? { pin: session.pin } : {},
6284
+ ...exclude.length > 0 ? { excludeCredentialIds: exclude } : {}
6285
+ });
6286
+ } catch (error) {
6287
+ if (error instanceof VaultError && error.code === "VAULT_KEY_ALREADY_ENROLLED")
6288
+ throw alreadyEnrolled(session.device);
6289
+ throw error;
6290
+ }
6291
+ if (exclude.length > 0 && response.excluded !== exclude.length) {
6292
+ throw helperPredatesExcludeList(session.helperPath, true);
6293
+ }
6188
6294
  const authData = base64.decode(response.authData);
6189
6295
  assertAuthenticatorData(authData, RP_ID, "registration");
6190
6296
  return {
@@ -6224,7 +6330,7 @@ async function assertPrf(deps, session, envelope, vaultId, purpose) {
6224
6330
  }
6225
6331
  return prfOutput;
6226
6332
  }
6227
- var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.";
6333
+ var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.", ALREADY_ENROLLED_SUGGESTION = "Insert a different security key, or name one with --device.", EXCLUDE_LIST_FEATURE = "exclude-list";
6228
6334
  var init_fido2 = __esm(() => {
6229
6335
  init_sha256();
6230
6336
  init_esm();
@@ -8669,7 +8775,7 @@ async function keysList(args, ctx) {
8669
8775
  async function keysCreate(args, ctx) {
8670
8776
  const { deps, apiUrl, json } = ctx;
8671
8777
  const parsed = parseArgs(args, {
8672
- valueFlags: ["--scopes", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
8778
+ valueFlags: ["--scopes", "--access", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
8673
8779
  });
8674
8780
  if ("error" in parsed) {
8675
8781
  writeUsageFailure(deps, parsed.error, json);
@@ -8679,7 +8785,20 @@ async function keysCreate(args, ctx) {
8679
8785
  writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}`, json);
8680
8786
  return 2;
8681
8787
  }
8682
- const requestedScopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
8788
+ if (parsed.values["--access"] !== undefined && parsed.values["--scopes"] !== undefined) {
8789
+ writeUsageFailure(deps, "--access and --scopes are mutually exclusive; pass one of them.", json);
8790
+ return 2;
8791
+ }
8792
+ let accessScopes;
8793
+ if (parsed.values["--access"] !== undefined) {
8794
+ const preset = ACCESS_LEVELS[parsed.values["--access"]];
8795
+ if (preset === undefined) {
8796
+ writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
8797
+ return 2;
8798
+ }
8799
+ accessScopes = scopesForPreset(preset);
8800
+ }
8801
+ const requestedScopes = accessScopes ?? (parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined);
8683
8802
  const environment = parsed.values["--environment"];
8684
8803
  const label = parsed.values["--label"]?.trim();
8685
8804
  if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > 64)) {
@@ -8775,7 +8894,7 @@ WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError
8775
8894
  `);
8776
8895
  }
8777
8896
  if (!requestedScopes) {
8778
- deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
8897
+ deps.stdout.write(`No --access or --scopes given: the server granted the default scopes (swap:write excluded).
8779
8898
  `);
8780
8899
  }
8781
8900
  if (storeError === undefined) {
@@ -8842,7 +8961,7 @@ async function keysRevoke(args, ctx) {
8842
8961
  }
8843
8962
  return 0;
8844
8963
  }
8845
- var KEYS_PATH = "/api/v1/agent/keys", NO_DEVICE_TOKEN;
8964
+ var KEYS_PATH = "/api/v1/agent/keys", ACCESS_LEVELS, NO_DEVICE_TOKEN;
8846
8965
  var init_keys = __esm(() => {
8847
8966
  init_agent_key_access();
8848
8967
  init_args();
@@ -8850,6 +8969,11 @@ var init_keys = __esm(() => {
8850
8969
  init_profiles();
8851
8970
  init_render();
8852
8971
  init_secret_store();
8972
+ ACCESS_LEVELS = {
8973
+ read: "read",
8974
+ "read-write": "readwrite",
8975
+ "read-write-transfer": "readwritetransfer"
8976
+ };
8853
8977
  NO_DEVICE_TOKEN = {
8854
8978
  code: "NO_DEVICE_TOKEN",
8855
8979
  message: "No device token available.",
@@ -8862,7 +8986,7 @@ function tokenMintFilters(key) {
8862
8986
  return [{ dataSize: 82 }, memcmp(4, key)];
8863
8987
  }
8864
8988
  function tokenFreezeFilters(key) {
8865
- return [{ dataSize: 82 }, memcmp(46, concat2(COPTION_SOME_U32, key))];
8989
+ return [{ dataSize: 82 }, memcmp(46, concat3(COPTION_SOME_U32, key))];
8866
8990
  }
8867
8991
  function token2022MintFilters(key) {
8868
8992
  const authority = memcmp(4, key);
@@ -8872,7 +8996,7 @@ function token2022MintFilters(key) {
8872
8996
  ];
8873
8997
  }
8874
8998
  function token2022FreezeFilters(key) {
8875
- const authority = memcmp(46, concat2(COPTION_SOME_U32, key));
8999
+ const authority = memcmp(46, concat3(COPTION_SOME_U32, key));
8876
9000
  return [
8877
9001
  [{ dataSize: 82 }, authority],
8878
9002
  [memcmp(165, TOKEN_2022_MINT_ACCOUNT_TYPE), authority]
@@ -8882,7 +9006,7 @@ function keepSetAuthority(hits) {
8882
9006
  return hits.filter((hit) => hit.data.length === COPTION_SOME_U32.length && hit.data.every((byte, i) => byte === COPTION_SOME_U32[i])).map((hit) => hit.pubkey);
8883
9007
  }
8884
9008
  function programUpgradeFilters(key) {
8885
- return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat2(COPTION_SOME_U8, key))];
9009
+ return [memcmp(0, LOADER_PROGRAM_DATA_TAG), memcmp(12, concat3(COPTION_SOME_U8, key))];
8886
9010
  }
8887
9011
  function programIdFilters(programData) {
8888
9012
  return [memcmp(0, LOADER_PROGRAM_TAG), memcmp(4, programData)];
@@ -9232,7 +9356,7 @@ function authoritiesJson(result) {
9232
9356
  found: result.found.map(({ address, role, target, program }) => ({ address, role, target, program }))
9233
9357
  };
9234
9358
  }
9235
- var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111", ROLE_GROUP_IDS, PAGINATED_TOKEN_GROUPS, RPC_PAGINATION_REQUIRED = -32600, MAX_V2_PAGES = 10, RPC_METHOD_NOT_FOUND = -32601, REQUESTS_PER_KEY = 9, b642 = (bytes) => base64.encode(bytes), concat2 = (...parts) => {
9359
+ var BPF_UPGRADEABLE_LOADER_ID = "BPFLoaderUpgradeab1e11111111111111111111111", STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111", ROLE_GROUP_IDS, PAGINATED_TOKEN_GROUPS, RPC_PAGINATION_REQUIRED = -32600, MAX_V2_PAGES = 10, RPC_METHOD_NOT_FOUND = -32601, REQUESTS_PER_KEY = 9, b642 = (bytes) => base64.encode(bytes), concat3 = (...parts) => {
9236
9360
  const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
9237
9361
  let at = 0;
9238
9362
  for (const part of parts) {
@@ -9354,6 +9478,7 @@ __export(exports_promote_support, {
9354
9478
  confirmPrompt: () => confirmPrompt,
9355
9479
  confirmPromotion: () => confirmPromotion,
9356
9480
  assertNotPinnedDestination: () => assertNotPinnedDestination,
9481
+ assertNotEvmEntry: () => assertNotEvmEntry,
9357
9482
  assertInPlacePreconditions: () => assertInPlacePreconditions,
9358
9483
  assertColdVaultDestination: () => assertColdVaultDestination,
9359
9484
  applyPromotion: () => applyPromotion,
@@ -9361,6 +9486,7 @@ __export(exports_promote_support, {
9361
9486
  CONFIRM_WORD: () => CONFIRM_WORD
9362
9487
  });
9363
9488
  function assertColdVaultDestination(index, destinationLabelOrAddress, opts = {}) {
9489
+ assertNotEvmEntry(index, destinationLabelOrAddress, "this destination");
9364
9490
  const destination = findVaultRoleEntry(index, destinationLabelOrAddress);
9365
9491
  if (destination === undefined) {
9366
9492
  throw new VaultError("PROMOTE_DESTINATION_NOT_COLD", `No vault key matches ${destinationLabelOrAddress}.`, {
@@ -9388,11 +9514,20 @@ function assertColdVaultDestination(index, destinationLabelOrAddress, opts = {})
9388
9514
  }
9389
9515
  return destination;
9390
9516
  }
9517
+ function assertNotEvmEntry(index, labelOrAddress, command) {
9518
+ const named = index.entries.find((entry) => entry.chain === "evm" && (entry.label === labelOrAddress || entry.address.toLowerCase() === labelOrAddress.toLowerCase()));
9519
+ if (named === undefined)
9520
+ return;
9521
+ throw new VaultError("SOLANA_COMMAND_EVM_KEY", `${named.label || named.address} is an EVM key (${named.address}); ${command} works on Solana keys only.`, {
9522
+ suggestion: `Nothing was signed or written. An EVM vault key moves funds with: candle vault transfer <0x address> --from ${named.label || named.address}`
9523
+ });
9524
+ }
9391
9525
  function findVaultRoleEntry(index, labelOrAddress) {
9392
- const byLabel = index.entries.find((entry) => entry.role === "vault" && entry.label !== undefined && entry.label === labelOrAddress);
9526
+ const solana = index.entries.filter((entry) => entry.chain === "solana");
9527
+ const byLabel = solana.find((entry) => entry.role === "vault" && entry.label !== undefined && entry.label === labelOrAddress);
9393
9528
  if (byLabel !== undefined)
9394
9529
  return byLabel;
9395
- return index.entries.find((entry) => entry.address === labelOrAddress);
9530
+ return solana.find((entry) => entry.address === labelOrAddress);
9396
9531
  }
9397
9532
  function findTransferSource(index, labelOrAddress) {
9398
9533
  const byLabel = (role) => index.entries.find((entry) => entry.role === role && entry.label !== undefined && entry.label === labelOrAddress);
@@ -9413,6 +9548,7 @@ function assertNotPinnedDestination(index, subjectAddress) {
9413
9548
  });
9414
9549
  }
9415
9550
  function assertInPlacePreconditions(index, subjectLabel, sweepToLabel, opts) {
9551
+ assertNotEvmEntry(index, subjectLabel, "vault promote");
9416
9552
  const subject = findEntryByLabelOrAddress(index, subjectLabel);
9417
9553
  if (subject === undefined) {
9418
9554
  throw new VaultError("PROMOTE_NOT_VAULT_KEY", `No entry matches ${subjectLabel}.`);
@@ -13707,11 +13843,13 @@ async function listTradingWallets(ctx, key, scope) {
13707
13843
  const rows = [];
13708
13844
  const cursors = new Set;
13709
13845
  let appId = "";
13846
+ let scopes = [];
13710
13847
  for (;; ) {
13711
13848
  const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
13712
13849
  if (scope !== undefined && !response.scopes?.includes(scope))
13713
13850
  throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
13714
13851
  appId = response.privyAppId ?? "";
13852
+ scopes = response.scopes;
13715
13853
  if (!Array.isArray(response.page))
13716
13854
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
13717
13855
  rows.push(...response.page);
@@ -13722,7 +13860,7 @@ async function listTradingWallets(ctx, key, scope) {
13722
13860
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
13723
13861
  cursors.add(cursor);
13724
13862
  }
13725
- return { rows, appId };
13863
+ return { rows, appId, scopes };
13726
13864
  }
13727
13865
  function matchesName(row, name) {
13728
13866
  return row.id === name || row.address === name || row.label === name;
@@ -13759,25 +13897,24 @@ async function tradingWallet(ctx, key, name, scope) {
13759
13897
  }
13760
13898
  return await completeTradingWallet(ctx, matches[0], appId, scope);
13761
13899
  }
13762
- async function tradingPayer(ctx, key, name) {
13763
- const scope = "swap:write";
13764
- const { rows, appId } = await listTradingWallets(ctx, key, scope);
13900
+ async function tradingPayer(ctx, key, name, scope = "swap:write") {
13901
+ const { rows, appId, scopes } = await listTradingWallets(ctx, key, scope);
13765
13902
  const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
13766
- const asEmbedded = () => ({ kind: "embedded", address: embedded });
13903
+ const asEmbedded = () => ({ kind: "embedded", address: embedded, scopes });
13767
13904
  const options = [
13768
13905
  ...rows.map((row) => `TEE ${describeWallet(row)}`),
13769
13906
  ...embedded ? [`embedded (${embedded})`] : []
13770
13907
  ].join("; ");
13771
13908
  if (name === undefined) {
13772
13909
  if (rows.length === 1 && !embedded)
13773
- return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope) };
13910
+ return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope), scopes };
13774
13911
  if (rows.length === 0 && embedded)
13775
13912
  return asEmbedded();
13776
13913
  throw new TradingError("PAYER_REQUIRED", options.length === 0 ? "This account has no wallet that can pay for a swap. Enrol a TEE wallet, or create an embedded wallet in the app." : `Name the payer with --wallet. This account can pay from: ${options}.`);
13777
13914
  }
13778
13915
  const matches = rows.filter((row) => matchesName(row, name));
13779
13916
  if (matches.length === 1)
13780
- return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope) };
13917
+ return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope), scopes };
13781
13918
  if (matches.length === 0 && embedded === name)
13782
13919
  return asEmbedded();
13783
13920
  throw new TradingError("TEE_WALLET_REQUIRED", matches.length > 1 ? `"${name}" matches ${matches.length} TEE wallets on this key: ${teeWalletList(matches)}. Name one by id or address.` : options.length === 0 ? `"${name}" is not a wallet this account can pay from, and it has none: enrol a TEE wallet, or create an embedded wallet in the app.` : `"${name}" is not a wallet this account can pay from. It can pay from: ${options}.`);
@@ -22179,6 +22316,8 @@ __export(exports_vault_support, {
22179
22316
  menuSafeText: () => menuSafeText,
22180
22317
  findExternalEntry: () => findExternalEntry,
22181
22318
  factorMenu: () => factorMenu,
22319
+ factorAddPassphraseOnlyReason: () => factorAddPassphraseOnlyReason,
22320
+ factorAddAmong: () => factorAddAmong,
22182
22321
  describeRole: () => describeRole,
22183
22322
  describeEntry: () => describeEntry,
22184
22323
  derivationNotice: () => derivationNotice,
@@ -22277,6 +22416,20 @@ async function requireVaultRaw(ctx, resolved) {
22277
22416
  throw missingVault(ctx, resolved);
22278
22417
  return raw;
22279
22418
  }
22419
+ function factorAddAmong(envelopes) {
22420
+ return {
22421
+ envelopeIds: envelopes.filter((envelope) => isPassphraseEnvelope(envelope) || isCtap2Envelope(envelope)).map((envelope) => envelope.id),
22422
+ because: factorAddPassphraseOnlyReason,
22423
+ excludedBecause: "adding a factor opens the vault with the passphrase or a security key."
22424
+ };
22425
+ }
22426
+ function factorAddPassphraseOnlyReason(unusable) {
22427
+ if (unusable.length === 0) {
22428
+ return "adding a factor opens the vault with the passphrase or a security key, and this vault has no security key.";
22429
+ }
22430
+ const keys = unusable.map((entry) => `${entry.word} ${entry.id} cannot be used on this machine: ${entry.availability}`);
22431
+ return `adding a factor opens the vault with the passphrase or a security key, and ${keys.join("; ")}.`;
22432
+ }
22280
22433
  function derivationNotice(line, purpose) {
22281
22434
  if (purpose === undefined)
22282
22435
  return line;
@@ -22303,6 +22456,9 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22303
22456
  const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
22304
22457
  const notice = noticeFor(opts.purpose);
22305
22458
  if (choice.kind === "passphrase") {
22459
+ if (opts.onFactorChosen) {
22460
+ await opts.onFactorChosen(choice.envelopeId !== undefined ? { kind: "passphrase", envelopeId: choice.envelopeId } : { kind: "passphrase" });
22461
+ }
22306
22462
  if (choice.onlyBecause !== undefined)
22307
22463
  deps.stderr.write(`${PASSPHRASE_ONLY_PREFIX}${choice.onlyBecause}
22308
22464
  `);
@@ -22324,6 +22480,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22324
22480
  }
22325
22481
  if (choice.kind === "touch-id") {
22326
22482
  const envelope2 = choice.envelope;
22483
+ if (opts.onFactorChosen)
22484
+ await opts.onFactorChosen({ kind: "touch-id", envelope: envelope2 });
22327
22485
  const session2 = await openEnclaveSession(deps, envelope2.helper);
22328
22486
  const open5 = async (p, r, reason2) => {
22329
22487
  const current = parseVaultFile(r);
@@ -22352,6 +22510,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22352
22510
  }
22353
22511
  if (choice.kind === "passkey") {
22354
22512
  const envelope2 = choice.envelope;
22513
+ if (opts.onFactorChosen)
22514
+ await opts.onFactorChosen({ kind: "passkey", envelope: envelope2 });
22355
22515
  const session2 = await openPasskeySession(deps, envelope2.helper);
22356
22516
  const open5 = async (p, r, purpose) => {
22357
22517
  const current = parseVaultFile(r);
@@ -22379,11 +22539,14 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
22379
22539
  };
22380
22540
  }
22381
22541
  const envelope = choice.envelope;
22542
+ const keyDevice = opts.onFactorChosen ? await opts.onFactorChosen({ kind: "security-key", envelope }) : undefined;
22382
22543
  const session = await openSecurityKeySession(deps, {
22383
22544
  vaultId: file.vaultId,
22384
22545
  envelopeId: envelope.id,
22385
- deviceFlag: ctx.vaultDevice,
22386
- ...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {},
22546
+ ...keyDevice !== undefined ? { preferDevice: keyDevice.preferDevice } : {
22547
+ deviceFlag: ctx.vaultDevice,
22548
+ ...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {}
22549
+ },
22387
22550
  requireFeatures: false
22388
22551
  });
22389
22552
  const open4 = async (p, r) => {
@@ -22477,7 +22640,7 @@ async function chooseFactor(ctx, vaultId, allEnvelopes, facts, requested, opts =
22477
22640
  }
22478
22641
  if (passphrases.length === 0 && drivable.length === 1)
22479
22642
  return choiceFor(drivable[0]);
22480
- const attached = ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
22643
+ const attached = "attached" in opts ? opts.attached : ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
22481
22644
  const notUsableHere = envelopes.filter((envelope) => !isPassphraseEnvelope(envelope) && !drivable.includes(envelope));
22482
22645
  const menu = factorMenu({
22483
22646
  keys: drivableKeys,
@@ -22836,6 +22999,12 @@ async function findTeeInVault(ctx, address, opts = {}) {
22836
22999
  return { hit: false };
22837
23000
  const opened = await unlockInteractively(ctx, path, raw, { acceptOlderCopy: true });
22838
23001
  const vault = opened.vault;
23002
+ try {
23003
+ assertNotEvmEntry(vault.index, address, "this command");
23004
+ } catch (error) {
23005
+ closeVault(vault);
23006
+ throw error;
23007
+ }
22839
23008
  const entry = vault.index.entries.find((candidate) => candidate.address === address && candidate.role === "tee-wallet");
22840
23009
  if (entry === undefined) {
22841
23010
  closeVault(vault);
@@ -22849,6 +23018,7 @@ function vaultOwnsTeeAddress(vault, address) {
22849
23018
  var init_tee_lookup = __esm(() => {
22850
23019
  init_args();
22851
23020
  init_vault_support();
23021
+ init_promote_support();
22852
23022
  init_store();
22853
23023
  });
22854
23024
 
@@ -38715,7 +38885,7 @@ init_render();
38715
38885
  init_secret_store();
38716
38886
 
38717
38887
  // src/version.ts
38718
- var CLI_VERSION = "0.11.7";
38888
+ var CLI_VERSION = "0.11.9";
38719
38889
 
38720
38890
  // src/commands/auth.ts
38721
38891
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -38737,6 +38907,10 @@ async function authLogin(args, ctx) {
38737
38907
  return 2;
38738
38908
  }
38739
38909
  const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
38910
+ if (scopes?.includes("transfer:bound")) {
38911
+ writeUsageFailure(deps, "transfer:bound is not available on a device login. Mint a Read:Write:Transfer key with: candle keys create --access read-write-transfer", json);
38912
+ return 2;
38913
+ }
38740
38914
  const label = parsed.values["--label"]?.trim();
38741
38915
  const noBrowser = parsed.booleans.has("--no-browser");
38742
38916
  if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > MAX_CLIENT_NAME_LENGTH)) {
@@ -39106,6 +39280,10 @@ var ENVIRONMENT = [
39106
39280
  description: "Unlocks the encrypted-file backend where no OS keychain exists"
39107
39281
  },
39108
39282
  { name: "CANDLE_SOLANA_RPC_URL", description: "Solana RPC endpoint, when --rpc-url is not given" },
39283
+ {
39284
+ name: "CANDLE_EVM_RPC_URL",
39285
+ description: "EVM RPC endpoint for an EVM vault key, when --rpc-url (transfer) or --evm-rpc-url (list) is not given; without it the built-in Hood RPC is used"
39286
+ },
39109
39287
  {
39110
39288
  name: "CANDLE_FIDO2_HELPER",
39111
39289
  description: "Path to the candle-fido2 helper, when it is not beside the binary"
@@ -39253,6 +39431,32 @@ var HELP = {
39253
39431
  ],
39254
39432
  env: ENV_API
39255
39433
  },
39434
+ transfer: {
39435
+ group: "Trade",
39436
+ summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
39437
+ description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Read:Write:Transfer key (transfer:bound); from that wallet, Candle allows only its own pinned vault or another of the account's wallets you linked while signed in or marked trusted. To a linked wallet the amount counts against the key's spend caps and must be a base asset; to the vault any token and max are allowed. Candle builds the transaction, this machine approves the relay, Privy signs, Candle broadcasts. The destination and its kind are shown before you confirm.",
39438
+ usage: [
39439
+ "candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
39440
+ ],
39441
+ rows: [],
39442
+ flags: [
39443
+ {
39444
+ invocation: "--to <address|wallet name|vault>",
39445
+ description: "Where the funds go: vault is the wallet's own pin"
39446
+ },
39447
+ { invocation: "--asset <SOL|USDC|CNDL>", description: "A base asset (required for a linked-wallet destination)" },
39448
+ { invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
39449
+ { invocation: "--amount <decimal|max>", description: "How much, or max for the whole spendable balance" },
39450
+ { invocation: "--wallet <tee>", description: "The TEE wallet the funds leave; optional with one payer" },
39451
+ { invocation: "--rpc-url <url>", description: "Your own Solana RPC, to read a --mint's decimals" },
39452
+ { invocation: "--yes", description: "Skip the confirmation prompt (the destination is still printed)" }
39453
+ ],
39454
+ examples: [
39455
+ "candle transfer --to vault --asset SOL --amount max --wallet AgentOne",
39456
+ "candle transfer --to treasury --asset USDC --amount 250 --wallet AgentOne --yes --json"
39457
+ ],
39458
+ env: ENV_API
39459
+ },
39256
39460
  launch: {
39257
39461
  group: "Trade",
39258
39462
  summary: "Create a Solana token (the first buy is a separate swap)",
@@ -39296,20 +39500,26 @@ var HELP = {
39296
39500
  keys: {
39297
39501
  group: "Account",
39298
39502
  summary: "API keys, and the wallets each key may use",
39299
- description: "API keys are minted over the device token and shown exactly once. A key's wallet set and scope decide which wallets an agent holding it may act on.",
39503
+ description: "API keys are minted over the device token and shown exactly once. A key's wallet set and scope decide which wallets an agent holding it may act on. To give a key TEE wallets, move them to it with candle tee rebind; keys wallets set cannot widen a key's set from the CLI.",
39300
39504
  usage: ["candle keys <subcommand> [flags]"],
39301
39505
  rows: [
39302
39506
  {
39303
39507
  invocation: "list [--scopes]",
39304
- description: "List API keys: name and Read or Read:Write access; --scopes adds the raw scopes"
39508
+ description: "List API keys: name and Read, Read:Write or Read:Write:Transfer access; --scopes adds the raw scopes"
39305
39509
  },
39306
39510
  {
39307
- invocation: "create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
39308
- description: "Create an API key"
39511
+ invocation: "create [--access read|read-write|read-write-transfer | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
39512
+ description: "Create an API key; --access mints one of the three levels (read-write-transfer can move funds out of the wallet it runs)"
39309
39513
  },
39310
39514
  { invocation: "revoke <prefix>", description: "Revoke an API key" },
39311
- { invocation: "wallets <prefix>", description: "Wallets an agent profile can use" },
39312
- { invocation: " set <prefix> --wallets <id,id>", description: "Replace the profile's wallet set" },
39515
+ {
39516
+ invocation: "wallets <prefix>",
39517
+ description: "Wallets an agent profile can use. TEE wallets move between keys with candle tee rebind"
39518
+ },
39519
+ {
39520
+ invocation: " set <prefix> --wallets <id,id>",
39521
+ description: "Replace the profile's wallet set; from a key it can only narrow. To move TEE wallets to a key: candle tee rebind"
39522
+ },
39313
39523
  {
39314
39524
  invocation: " scope <prefix> --scope <all|selected>",
39315
39525
  description: "Limit a profile to assigned wallets"
@@ -39317,8 +39527,10 @@ var HELP = {
39317
39527
  ],
39318
39528
  examples: [
39319
39529
  "candle keys list",
39530
+ "candle keys create --access read-write-transfer --label rebalancer",
39320
39531
  "candle keys create --scopes trade:write --label agent-one",
39321
39532
  "candle keys wallets ck_live_ab12",
39533
+ "candle tee rebind --label-prefix dest- --to-key ck_live_ab12",
39322
39534
  "candle keys revoke ck_live_ab12"
39323
39535
  ],
39324
39536
  env: ENV_API
@@ -39327,19 +39539,28 @@ var HELP = {
39327
39539
  group: "Account",
39328
39540
  display: "wallet",
39329
39541
  summary: "Launch and linked wallets; import or revoke one (wallets is an alias)",
39330
- description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
39542
+ description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key and a Trusted column saying whether it is yours: linked while signed in, or marked with wallet trust. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
39331
39543
  usage: ["candle wallet [flags]", "candle wallet <subcommand> [flags]"],
39332
39544
  rows: [
39333
39545
  {
39334
39546
  invocation: "import --chain <solana|evm> [options]",
39335
39547
  description: "Import a wallet you own (key via --key-file or hidden prompt)"
39336
39548
  },
39337
- { invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" }
39549
+ { invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" },
39550
+ {
39551
+ invocation: "trust <label|address|id|prefix*>...",
39552
+ description: "Mark linked wallets yours, so agents can move funds into them (owner only; typed confirm)"
39553
+ },
39554
+ {
39555
+ invocation: "untrust <label|address|id|prefix*>... [--yes]",
39556
+ description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
39557
+ }
39338
39558
  ],
39339
39559
  examples: [
39340
39560
  "candle wallet",
39341
39561
  "candle wallet import --chain solana --key-file ./signer.json",
39342
- "candle wallet revoke wal_123"
39562
+ "candle wallet revoke wal_123",
39563
+ "candle wallet trust 'tr-*' 'dest-*'"
39343
39564
  ],
39344
39565
  env: ENV_API
39345
39566
  },
@@ -39374,12 +39595,12 @@ var HELP = {
39374
39595
  },
39375
39596
  { invocation: "status [--unlock]", description: "What the vault holds, and what opens it" },
39376
39597
  {
39377
- invocation: "list [<filter>] [--balances] [--rpc-url <url>]",
39378
- description: "One line per key: address, label, role, derivation. Prompts for the passphrase"
39598
+ invocation: "list [<filter>] [--balances] [--rpc-url <url>] [--evm-rpc-url <url>]",
39599
+ description: "One line per key: address, label, role, derivation. Prompts for the passphrase. --balances reads SOL over your Solana RPC and ETH (plus USDG on Hood) for EVM keys over --evm-rpc-url, CANDLE_EVM_RPC_URL, or the built-in Hood RPC"
39379
39600
  },
39380
39601
  {
39381
- invocation: "new-key --chain solana [--label <name>] [--count <n>] [--labels-from <file>]",
39382
- description: "Derive the next Solana key, or n of them under one unlock; the name must be free"
39602
+ invocation: "new-key --chain solana|evm [--label <name>] [--count <n>] [--labels-from <file>]",
39603
+ 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"
39383
39604
  },
39384
39605
  {
39385
39606
  invocation: "rename <label|address> <new-label> [--id <entry-id>]",
@@ -39387,8 +39608,8 @@ var HELP = {
39387
39608
  },
39388
39609
  { invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
39389
39610
  {
39390
- invocation: "restore --phrase [--own-passphrase] [--count <n>] [--tee-count <k>] [--external-count <e>] [--rpc-url <url>]",
39391
- description: "Rebuild a vault from the recovery phrase; it gets a new passphrase"
39611
+ invocation: "restore --phrase [--own-passphrase] [--count <n>] [--tee-count <k>] [--external-count <e>] [--evm-count <m>] [--rpc-url <url>]",
39612
+ description: "Rebuild a vault from the recovery phrase; it gets a new passphrase. --evm-count derives EVM indices 0..m-1 (default 0, never gap-scanned)"
39392
39613
  },
39393
39614
  {
39394
39615
  invocation: "reconcile-exposure",
@@ -39413,8 +39634,8 @@ var HELP = {
39413
39634
  description: "Rename the Phase 1 store after a verified backup"
39414
39635
  },
39415
39636
  {
39416
- invocation: "transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>",
39417
- description: "Sign a transfer locally from a vault key or a promoted TEE wallet"
39637
+ invocation: "transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]",
39638
+ description: "Sign a transfer locally from a vault key or a promoted TEE wallet. From an EVM key: ETH or an ERC-20, on Hood by default (--rpc-url for any EVM chain; the chain id is read from the RPC), exit 0 means depth-confirmed (1 block on Hood, 2 elsewhere), not finalized. A Solana key still needs --rpc-url"
39418
39639
  },
39419
39640
  {
39420
39641
  invocation: "promote --from|--in-place <label> [--sweep-to <label>] [--rpc-url <url>] [--to-key <prefix|label>]",
@@ -39443,10 +39664,17 @@ var HELP = {
39443
39664
  invocation: "--factor <id|kind>",
39444
39665
  description: "Unlock with this envelope: an id from factor list, or passphrase, security-key, touch-id, passkey; without it, a terminal lists the factors to pick by number"
39445
39666
  },
39446
- { invocation: "--device <id>", description: "The security key to use when more than one is attached" },
39667
+ {
39668
+ invocation: "--device <id>",
39669
+ description: "The security key to use when more than one is attached; on factor add security-key, the key to add (the vault opens with the passphrase or another key)"
39670
+ },
39447
39671
  {
39448
39672
  invocation: "--balances",
39449
- description: "list: SOL per matched key, read from your RPC (one request per 100 matched keys). Every matched address goes to that one endpoint together, which links them; tokens are never read"
39673
+ description: "list: SOL per matched key, read from your RPC (one request per 100 matched keys). Every matched address goes to that one endpoint together, which links them; SPL tokens are never read. EVM keys are read over the EVM endpoint: ETH, and USDG on Hood"
39674
+ },
39675
+ {
39676
+ invocation: "--evm-rpc-url <url>",
39677
+ description: "list --balances: the EVM endpoint for EVM keys. Else CANDLE_EVM_RPC_URL, else the built-in Hood RPC; the Solana --rpc-url is never sent an EVM address. The host is printed on stderr before the read"
39450
39678
  },
39451
39679
  {
39452
39680
  invocation: "--labels-from <file>",
@@ -39464,6 +39692,8 @@ var HELP = {
39464
39692
  examples: [
39465
39693
  "candle vault init",
39466
39694
  "candle vault new-key --chain solana --label treasury",
39695
+ "candle vault new-key --chain evm --label hood-cold",
39696
+ "candle vault transfer 0x000000000000000000000000000000000000dEaD --amount 0.5 --asset USDG --from hood-cold",
39467
39697
  "candle vault list cn-s",
39468
39698
  "candle vault rename key-7 treasury-cold",
39469
39699
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
@@ -39474,7 +39704,14 @@ var HELP = {
39474
39704
  "candle vault backup --to icloud",
39475
39705
  "CANDLE_CONFIG_DIR=$HOME/t47 candle vault status"
39476
39706
  ],
39477
- env: ["CANDLE_CONFIG_DIR", "CANDLE_FIDO2_HELPER", "CANDLE_ENCLAVE_HELPER", "CANDLE_KEYSTORE_PASSPHRASE"]
39707
+ env: [
39708
+ "CANDLE_CONFIG_DIR",
39709
+ "CANDLE_SOLANA_RPC_URL",
39710
+ "CANDLE_EVM_RPC_URL",
39711
+ "CANDLE_FIDO2_HELPER",
39712
+ "CANDLE_ENCLAVE_HELPER",
39713
+ "CANDLE_KEYSTORE_PASSPHRASE"
39714
+ ]
39478
39715
  },
39479
39716
  tee: {
39480
39717
  group: "Custody, on this machine",
@@ -39504,8 +39741,8 @@ var HELP = {
39504
39741
  description: "Sign locally and move everything to the pinned vault; closes DAMM v2 LP positions after verifying each server-built close (--emergency moves the position NFT instead, with no API)"
39505
39742
  },
39506
39743
  {
39507
- invocation: "rebind <wallet...> --to-key <prefix|label>",
39508
- description: "Move TEE wallets to another key on this account (owner only; funds do not move)"
39744
+ invocation: "rebind <wallet...> --to-key <prefix|label> [--label-prefix <p>]",
39745
+ 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)"
39509
39746
  },
39510
39747
  { invocation: "rebinds [wallet]", description: "List TEE wallet rebinds for this account (owner only)" }
39511
39748
  ],
@@ -39514,7 +39751,8 @@ var HELP = {
39514
39751
  "candle tee new --label AgentOne",
39515
39752
  "candle tee status AgentOneAddress",
39516
39753
  "candle tee sweep AgentOneAddress --rpc-url https://api.mainnet-beta.solana.com",
39517
- "candle tee rebind tr-01 tr-02 --to-key Ab3dEf9h"
39754
+ "candle tee rebind tr-01 tr-02 --to-key Ab3dEf9h",
39755
+ "candle tee rebind --label-prefix dest- --to-key Ab3dEf9h"
39518
39756
  ],
39519
39757
  env: ENV_LOCAL_SIGNING
39520
39758
  },
@@ -42460,6 +42698,1022 @@ zone
42460
42698
  zoo`.split(`
42461
42699
  `);
42462
42700
 
42701
+ // ../../node_modules/@noble/curves/esm/secp256k1.js
42702
+ init_sha2();
42703
+ init__shortw_utils();
42704
+ init_modular();
42705
+ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
42706
+ var secp256k1_CURVE = {
42707
+ p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),
42708
+ n: BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),
42709
+ h: BigInt(1),
42710
+ a: BigInt(0),
42711
+ b: BigInt(7),
42712
+ Gx: BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),
42713
+ Gy: BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")
42714
+ };
42715
+ var secp256k1_ENDO = {
42716
+ beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),
42717
+ basises: [
42718
+ [BigInt("0x3086d221a7d46bcde86c90e49284eb15"), -BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],
42719
+ [BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"), BigInt("0x3086d221a7d46bcde86c90e49284eb15")]
42720
+ ]
42721
+ };
42722
+ var _2n5 = /* @__PURE__ */ BigInt(2);
42723
+ function sqrtMod(y) {
42724
+ const P2 = secp256k1_CURVE.p;
42725
+ const _3n4 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22);
42726
+ const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
42727
+ const b2 = y * y * y % P2;
42728
+ const b3 = b2 * b2 * y % P2;
42729
+ const b6 = pow2(b3, _3n4, P2) * b3 % P2;
42730
+ const b9 = pow2(b6, _3n4, P2) * b3 % P2;
42731
+ const b11 = pow2(b9, _2n5, P2) * b2 % P2;
42732
+ const b22 = pow2(b11, _11n, P2) * b11 % P2;
42733
+ const b44 = pow2(b22, _22n, P2) * b22 % P2;
42734
+ const b88 = pow2(b44, _44n, P2) * b44 % P2;
42735
+ const b176 = pow2(b88, _88n, P2) * b88 % P2;
42736
+ const b220 = pow2(b176, _44n, P2) * b44 % P2;
42737
+ const b223 = pow2(b220, _3n4, P2) * b3 % P2;
42738
+ const t1 = pow2(b223, _23n, P2) * b22 % P2;
42739
+ const t2 = pow2(t1, _6n, P2) * b2 % P2;
42740
+ const root = pow2(t2, _2n5, P2);
42741
+ if (!Fpk1.eql(Fpk1.sqr(root), y))
42742
+ throw new Error("Cannot find square root");
42743
+ return root;
42744
+ }
42745
+ var Fpk1 = Field(secp256k1_CURVE.p, { sqrt: sqrtMod });
42746
+ var secp256k1 = createCurve({ ...secp256k1_CURVE, Fp: Fpk1, lowS: true, endo: secp256k1_ENDO }, sha256);
42747
+
42748
+ // ../../node_modules/@noble/hashes/esm/sha3.js
42749
+ init__u64();
42750
+ init_utils();
42751
+ var _0n7 = BigInt(0);
42752
+ var _1n7 = BigInt(1);
42753
+ var _2n6 = BigInt(2);
42754
+ var _7n2 = BigInt(7);
42755
+ var _256n = BigInt(256);
42756
+ var _0x71n = BigInt(113);
42757
+ var SHA3_PI = [];
42758
+ var SHA3_ROTL = [];
42759
+ var _SHA3_IOTA = [];
42760
+ for (let round = 0, R = _1n7, x = 1, y = 0;round < 24; round++) {
42761
+ [x, y] = [y, (2 * x + 3 * y) % 5];
42762
+ SHA3_PI.push(2 * (5 * y + x));
42763
+ SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64);
42764
+ let t = _0n7;
42765
+ for (let j = 0;j < 7; j++) {
42766
+ R = (R << _1n7 ^ (R >> _7n2) * _0x71n) % _256n;
42767
+ if (R & _2n6)
42768
+ t ^= _1n7 << (_1n7 << /* @__PURE__ */ BigInt(j)) - _1n7;
42769
+ }
42770
+ _SHA3_IOTA.push(t);
42771
+ }
42772
+ var IOTAS = split(_SHA3_IOTA, true);
42773
+ var SHA3_IOTA_H = IOTAS[0];
42774
+ var SHA3_IOTA_L = IOTAS[1];
42775
+ var rotlH = (h, l, s) => s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s);
42776
+ var rotlL = (h, l, s) => s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s);
42777
+ function keccakP(s, rounds = 24) {
42778
+ const B = new Uint32Array(5 * 2);
42779
+ for (let round = 24 - rounds;round < 24; round++) {
42780
+ for (let x = 0;x < 10; x++)
42781
+ B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
42782
+ for (let x = 0;x < 10; x += 2) {
42783
+ const idx1 = (x + 8) % 10;
42784
+ const idx0 = (x + 2) % 10;
42785
+ const B0 = B[idx0];
42786
+ const B1 = B[idx0 + 1];
42787
+ const Th = rotlH(B0, B1, 1) ^ B[idx1];
42788
+ const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
42789
+ for (let y = 0;y < 50; y += 10) {
42790
+ s[x + y] ^= Th;
42791
+ s[x + y + 1] ^= Tl;
42792
+ }
42793
+ }
42794
+ let curH = s[2];
42795
+ let curL = s[3];
42796
+ for (let t = 0;t < 24; t++) {
42797
+ const shift = SHA3_ROTL[t];
42798
+ const Th = rotlH(curH, curL, shift);
42799
+ const Tl = rotlL(curH, curL, shift);
42800
+ const PI = SHA3_PI[t];
42801
+ curH = s[PI];
42802
+ curL = s[PI + 1];
42803
+ s[PI] = Th;
42804
+ s[PI + 1] = Tl;
42805
+ }
42806
+ for (let y = 0;y < 50; y += 10) {
42807
+ for (let x = 0;x < 10; x++)
42808
+ B[x] = s[y + x];
42809
+ for (let x = 0;x < 10; x++)
42810
+ s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
42811
+ }
42812
+ s[0] ^= SHA3_IOTA_H[round];
42813
+ s[1] ^= SHA3_IOTA_L[round];
42814
+ }
42815
+ clean(B);
42816
+ }
42817
+
42818
+ class Keccak extends Hash {
42819
+ constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) {
42820
+ super();
42821
+ this.pos = 0;
42822
+ this.posOut = 0;
42823
+ this.finished = false;
42824
+ this.destroyed = false;
42825
+ this.enableXOF = false;
42826
+ this.blockLen = blockLen;
42827
+ this.suffix = suffix;
42828
+ this.outputLen = outputLen;
42829
+ this.enableXOF = enableXOF;
42830
+ this.rounds = rounds;
42831
+ anumber(outputLen);
42832
+ if (!(0 < blockLen && blockLen < 200))
42833
+ throw new Error("only keccak-f1600 function is supported");
42834
+ this.state = new Uint8Array(200);
42835
+ this.state32 = u32(this.state);
42836
+ }
42837
+ clone() {
42838
+ return this._cloneInto();
42839
+ }
42840
+ keccak() {
42841
+ swap32IfBE(this.state32);
42842
+ keccakP(this.state32, this.rounds);
42843
+ swap32IfBE(this.state32);
42844
+ this.posOut = 0;
42845
+ this.pos = 0;
42846
+ }
42847
+ update(data) {
42848
+ aexists(this);
42849
+ data = toBytes(data);
42850
+ abytes(data);
42851
+ const { blockLen, state } = this;
42852
+ const len = data.length;
42853
+ for (let pos = 0;pos < len; ) {
42854
+ const take = Math.min(blockLen - this.pos, len - pos);
42855
+ for (let i = 0;i < take; i++)
42856
+ state[this.pos++] ^= data[pos++];
42857
+ if (this.pos === blockLen)
42858
+ this.keccak();
42859
+ }
42860
+ return this;
42861
+ }
42862
+ finish() {
42863
+ if (this.finished)
42864
+ return;
42865
+ this.finished = true;
42866
+ const { state, suffix, pos, blockLen } = this;
42867
+ state[pos] ^= suffix;
42868
+ if ((suffix & 128) !== 0 && pos === blockLen - 1)
42869
+ this.keccak();
42870
+ state[blockLen - 1] ^= 128;
42871
+ this.keccak();
42872
+ }
42873
+ writeInto(out) {
42874
+ aexists(this, false);
42875
+ abytes(out);
42876
+ this.finish();
42877
+ const bufferOut = this.state;
42878
+ const { blockLen } = this;
42879
+ for (let pos = 0, len = out.length;pos < len; ) {
42880
+ if (this.posOut >= blockLen)
42881
+ this.keccak();
42882
+ const take = Math.min(blockLen - this.posOut, len - pos);
42883
+ out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos);
42884
+ this.posOut += take;
42885
+ pos += take;
42886
+ }
42887
+ return out;
42888
+ }
42889
+ xofInto(out) {
42890
+ if (!this.enableXOF)
42891
+ throw new Error("XOF is not possible for this instance");
42892
+ return this.writeInto(out);
42893
+ }
42894
+ xof(bytes) {
42895
+ anumber(bytes);
42896
+ return this.xofInto(new Uint8Array(bytes));
42897
+ }
42898
+ digestInto(out) {
42899
+ aoutput(out, this);
42900
+ if (this.finished)
42901
+ throw new Error("digest() was already called");
42902
+ this.writeInto(out);
42903
+ this.destroy();
42904
+ return out;
42905
+ }
42906
+ digest() {
42907
+ return this.digestInto(new Uint8Array(this.outputLen));
42908
+ }
42909
+ destroy() {
42910
+ this.destroyed = true;
42911
+ clean(this.state);
42912
+ }
42913
+ _cloneInto(to) {
42914
+ const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
42915
+ to || (to = new Keccak(blockLen, suffix, outputLen, enableXOF, rounds));
42916
+ to.state32.set(this.state32);
42917
+ to.pos = this.pos;
42918
+ to.posOut = this.posOut;
42919
+ to.finished = this.finished;
42920
+ to.rounds = rounds;
42921
+ to.suffix = suffix;
42922
+ to.outputLen = outputLen;
42923
+ to.enableXOF = enableXOF;
42924
+ to.destroyed = this.destroyed;
42925
+ return to;
42926
+ }
42927
+ }
42928
+ var gen = (suffix, blockLen, outputLen) => createHasher(() => new Keccak(blockLen, suffix, outputLen));
42929
+ var keccak_256 = /* @__PURE__ */ (() => gen(1, 136, 256 / 8))();
42930
+
42931
+ // ../../node_modules/@scure/bip32/lib/esm/index.js
42932
+ init_modular();
42933
+ init_hmac();
42934
+
42935
+ // ../../node_modules/@noble/hashes/esm/legacy.js
42936
+ init__md();
42937
+ init_utils();
42938
+ var Rho160 = /* @__PURE__ */ Uint8Array.from([
42939
+ 7,
42940
+ 4,
42941
+ 13,
42942
+ 1,
42943
+ 10,
42944
+ 6,
42945
+ 15,
42946
+ 3,
42947
+ 12,
42948
+ 0,
42949
+ 9,
42950
+ 5,
42951
+ 2,
42952
+ 14,
42953
+ 11,
42954
+ 8
42955
+ ]);
42956
+ var Id160 = /* @__PURE__ */ (() => Uint8Array.from(new Array(16).fill(0).map((_, i) => i)))();
42957
+ var Pi160 = /* @__PURE__ */ (() => Id160.map((i) => (9 * i + 5) % 16))();
42958
+ var idxLR = /* @__PURE__ */ (() => {
42959
+ const L = [Id160];
42960
+ const R = [Pi160];
42961
+ const res = [L, R];
42962
+ for (let i = 0;i < 4; i++)
42963
+ for (let j of res)
42964
+ j.push(j[i].map((k) => Rho160[k]));
42965
+ return res;
42966
+ })();
42967
+ var idxL = /* @__PURE__ */ (() => idxLR[0])();
42968
+ var idxR = /* @__PURE__ */ (() => idxLR[1])();
42969
+ var shifts160 = /* @__PURE__ */ [
42970
+ [11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8],
42971
+ [12, 13, 11, 15, 6, 9, 9, 7, 12, 15, 11, 13, 7, 8, 7, 7],
42972
+ [13, 15, 14, 11, 7, 7, 6, 8, 13, 14, 13, 12, 5, 5, 6, 9],
42973
+ [14, 11, 12, 14, 8, 6, 5, 5, 15, 12, 15, 14, 9, 9, 8, 6],
42974
+ [15, 12, 13, 13, 9, 5, 8, 6, 14, 11, 12, 11, 8, 6, 5, 5]
42975
+ ].map((i) => Uint8Array.from(i));
42976
+ var shiftsL160 = /* @__PURE__ */ idxL.map((idx, i) => idx.map((j) => shifts160[i][j]));
42977
+ var shiftsR160 = /* @__PURE__ */ idxR.map((idx, i) => idx.map((j) => shifts160[i][j]));
42978
+ var Kl160 = /* @__PURE__ */ Uint32Array.from([
42979
+ 0,
42980
+ 1518500249,
42981
+ 1859775393,
42982
+ 2400959708,
42983
+ 2840853838
42984
+ ]);
42985
+ var Kr160 = /* @__PURE__ */ Uint32Array.from([
42986
+ 1352829926,
42987
+ 1548603684,
42988
+ 1836072691,
42989
+ 2053994217,
42990
+ 0
42991
+ ]);
42992
+ function ripemd_f(group, x, y, z) {
42993
+ if (group === 0)
42994
+ return x ^ y ^ z;
42995
+ if (group === 1)
42996
+ return x & y | ~x & z;
42997
+ if (group === 2)
42998
+ return (x | ~y) ^ z;
42999
+ if (group === 3)
43000
+ return x & z | y & ~z;
43001
+ return x ^ (y | ~z);
43002
+ }
43003
+ var BUF_160 = /* @__PURE__ */ new Uint32Array(16);
43004
+
43005
+ class RIPEMD160 extends HashMD {
43006
+ constructor() {
43007
+ super(64, 20, 8, true);
43008
+ this.h0 = 1732584193 | 0;
43009
+ this.h1 = 4023233417 | 0;
43010
+ this.h2 = 2562383102 | 0;
43011
+ this.h3 = 271733878 | 0;
43012
+ this.h4 = 3285377520 | 0;
43013
+ }
43014
+ get() {
43015
+ const { h0, h1, h2, h3, h4 } = this;
43016
+ return [h0, h1, h2, h3, h4];
43017
+ }
43018
+ set(h0, h1, h2, h3, h4) {
43019
+ this.h0 = h0 | 0;
43020
+ this.h1 = h1 | 0;
43021
+ this.h2 = h2 | 0;
43022
+ this.h3 = h3 | 0;
43023
+ this.h4 = h4 | 0;
43024
+ }
43025
+ process(view, offset) {
43026
+ for (let i = 0;i < 16; i++, offset += 4)
43027
+ BUF_160[i] = view.getUint32(offset, true);
43028
+ let al = this.h0 | 0, ar = al, bl = this.h1 | 0, br = bl, cl = this.h2 | 0, cr = cl, dl = this.h3 | 0, dr = dl, el = this.h4 | 0, er = el;
43029
+ for (let group = 0;group < 5; group++) {
43030
+ const rGroup = 4 - group;
43031
+ const hbl = Kl160[group], hbr = Kr160[group];
43032
+ const rl = idxL[group], rr = idxR[group];
43033
+ const sl = shiftsL160[group], sr = shiftsR160[group];
43034
+ for (let i = 0;i < 16; i++) {
43035
+ const tl = rotl(al + ripemd_f(group, bl, cl, dl) + BUF_160[rl[i]] + hbl, sl[i]) + el | 0;
43036
+ al = el, el = dl, dl = rotl(cl, 10) | 0, cl = bl, bl = tl;
43037
+ }
43038
+ for (let i = 0;i < 16; i++) {
43039
+ const tr = rotl(ar + ripemd_f(rGroup, br, cr, dr) + BUF_160[rr[i]] + hbr, sr[i]) + er | 0;
43040
+ ar = er, er = dr, dr = rotl(cr, 10) | 0, cr = br, br = tr;
43041
+ }
43042
+ }
43043
+ this.set(this.h1 + cl + dr | 0, this.h2 + dl + er | 0, this.h3 + el + ar | 0, this.h4 + al + br | 0, this.h0 + bl + cr | 0);
43044
+ }
43045
+ roundClean() {
43046
+ clean(BUF_160);
43047
+ }
43048
+ destroy() {
43049
+ this.destroyed = true;
43050
+ clean(this.buffer);
43051
+ this.set(0, 0, 0, 0, 0);
43052
+ }
43053
+ }
43054
+ var ripemd160 = /* @__PURE__ */ createHasher(() => new RIPEMD160);
43055
+
43056
+ // ../../node_modules/@scure/bip32/lib/esm/index.js
43057
+ init_sha2();
43058
+ init_utils();
43059
+ init_esm();
43060
+ /*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */
43061
+ var Point = secp256k1.ProjectivePoint;
43062
+ var base58check = createBase58check(sha256);
43063
+ function bytesToNumber(bytes) {
43064
+ abytes(bytes);
43065
+ const h = bytes.length === 0 ? "0" : bytesToHex(bytes);
43066
+ return BigInt("0x" + h);
43067
+ }
43068
+ function numberToBytes(num) {
43069
+ if (typeof num !== "bigint")
43070
+ throw new Error("bigint expected");
43071
+ return hexToBytes(num.toString(16).padStart(64, "0"));
43072
+ }
43073
+ var MASTER_SECRET = utf8ToBytes("Bitcoin seed");
43074
+ var BITCOIN_VERSIONS = { private: 76066276, public: 76067358 };
43075
+ var HARDENED_OFFSET = 2147483648;
43076
+ var hash160 = (data) => ripemd160(sha256(data));
43077
+ var fromU32 = (data) => createView(data).getUint32(0, false);
43078
+ var toU32 = (n) => {
43079
+ if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) {
43080
+ throw new Error("invalid number, should be from 0 to 2**32-1, got " + n);
43081
+ }
43082
+ const buf = new Uint8Array(4);
43083
+ createView(buf).setUint32(0, n, false);
43084
+ return buf;
43085
+ };
43086
+
43087
+ class HDKey {
43088
+ get fingerprint() {
43089
+ if (!this.pubHash) {
43090
+ throw new Error("No publicKey set!");
43091
+ }
43092
+ return fromU32(this.pubHash);
43093
+ }
43094
+ get identifier() {
43095
+ return this.pubHash;
43096
+ }
43097
+ get pubKeyHash() {
43098
+ return this.pubHash;
43099
+ }
43100
+ get privateKey() {
43101
+ return this.privKeyBytes || null;
43102
+ }
43103
+ get publicKey() {
43104
+ return this.pubKey || null;
43105
+ }
43106
+ get privateExtendedKey() {
43107
+ const priv = this.privateKey;
43108
+ if (!priv) {
43109
+ throw new Error("No private key");
43110
+ }
43111
+ return base58check.encode(this.serialize(this.versions.private, concatBytes(new Uint8Array([0]), priv)));
43112
+ }
43113
+ get publicExtendedKey() {
43114
+ if (!this.pubKey) {
43115
+ throw new Error("No public key");
43116
+ }
43117
+ return base58check.encode(this.serialize(this.versions.public, this.pubKey));
43118
+ }
43119
+ static fromMasterSeed(seed, versions = BITCOIN_VERSIONS) {
43120
+ abytes(seed);
43121
+ if (8 * seed.length < 128 || 8 * seed.length > 512) {
43122
+ throw new Error("HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got " + seed.length);
43123
+ }
43124
+ const I = hmac(sha512, MASTER_SECRET, seed);
43125
+ return new HDKey({
43126
+ versions,
43127
+ chainCode: I.slice(32),
43128
+ privateKey: I.slice(0, 32)
43129
+ });
43130
+ }
43131
+ static fromExtendedKey(base58key, versions = BITCOIN_VERSIONS) {
43132
+ const keyBuffer = base58check.decode(base58key);
43133
+ const keyView = createView(keyBuffer);
43134
+ const version = keyView.getUint32(0, false);
43135
+ const opt = {
43136
+ versions,
43137
+ depth: keyBuffer[4],
43138
+ parentFingerprint: keyView.getUint32(5, false),
43139
+ index: keyView.getUint32(9, false),
43140
+ chainCode: keyBuffer.slice(13, 45)
43141
+ };
43142
+ const key = keyBuffer.slice(45);
43143
+ const isPriv = key[0] === 0;
43144
+ if (version !== versions[isPriv ? "private" : "public"]) {
43145
+ throw new Error("Version mismatch");
43146
+ }
43147
+ if (isPriv) {
43148
+ return new HDKey({ ...opt, privateKey: key.slice(1) });
43149
+ } else {
43150
+ return new HDKey({ ...opt, publicKey: key });
43151
+ }
43152
+ }
43153
+ static fromJSON(json) {
43154
+ return HDKey.fromExtendedKey(json.xpriv);
43155
+ }
43156
+ constructor(opt) {
43157
+ this.depth = 0;
43158
+ this.index = 0;
43159
+ this.chainCode = null;
43160
+ this.parentFingerprint = 0;
43161
+ if (!opt || typeof opt !== "object") {
43162
+ throw new Error("HDKey.constructor must not be called directly");
43163
+ }
43164
+ this.versions = opt.versions || BITCOIN_VERSIONS;
43165
+ this.depth = opt.depth || 0;
43166
+ this.chainCode = opt.chainCode || null;
43167
+ this.index = opt.index || 0;
43168
+ this.parentFingerprint = opt.parentFingerprint || 0;
43169
+ if (!this.depth) {
43170
+ if (this.parentFingerprint || this.index) {
43171
+ throw new Error("HDKey: zero depth with non-zero index/parent fingerprint");
43172
+ }
43173
+ }
43174
+ if (opt.publicKey && opt.privateKey) {
43175
+ throw new Error("HDKey: publicKey and privateKey at same time.");
43176
+ }
43177
+ if (opt.privateKey) {
43178
+ if (!secp256k1.utils.isValidPrivateKey(opt.privateKey)) {
43179
+ throw new Error("Invalid private key");
43180
+ }
43181
+ this.privKey = typeof opt.privateKey === "bigint" ? opt.privateKey : bytesToNumber(opt.privateKey);
43182
+ this.privKeyBytes = numberToBytes(this.privKey);
43183
+ this.pubKey = secp256k1.getPublicKey(opt.privateKey, true);
43184
+ } else if (opt.publicKey) {
43185
+ this.pubKey = Point.fromHex(opt.publicKey).toRawBytes(true);
43186
+ } else {
43187
+ throw new Error("HDKey: no public or private key provided");
43188
+ }
43189
+ this.pubHash = hash160(this.pubKey);
43190
+ }
43191
+ derive(path) {
43192
+ if (!/^[mM]'?/.test(path)) {
43193
+ throw new Error('Path must start with "m" or "M"');
43194
+ }
43195
+ if (/^[mM]'?$/.test(path)) {
43196
+ return this;
43197
+ }
43198
+ const parts = path.replace(/^[mM]'?\//, "").split("/");
43199
+ let child = this;
43200
+ for (const c of parts) {
43201
+ const m = /^(\d+)('?)$/.exec(c);
43202
+ const m1 = m && m[1];
43203
+ if (!m || m.length !== 3 || typeof m1 !== "string")
43204
+ throw new Error("invalid child index: " + c);
43205
+ let idx = +m1;
43206
+ if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) {
43207
+ throw new Error("Invalid index");
43208
+ }
43209
+ if (m[2] === "'") {
43210
+ idx += HARDENED_OFFSET;
43211
+ }
43212
+ child = child.deriveChild(idx);
43213
+ }
43214
+ return child;
43215
+ }
43216
+ deriveChild(index) {
43217
+ if (!this.pubKey || !this.chainCode) {
43218
+ throw new Error("No publicKey or chainCode set");
43219
+ }
43220
+ let data = toU32(index);
43221
+ if (index >= HARDENED_OFFSET) {
43222
+ const priv = this.privateKey;
43223
+ if (!priv) {
43224
+ throw new Error("Could not derive hardened child key");
43225
+ }
43226
+ data = concatBytes(new Uint8Array([0]), priv, data);
43227
+ } else {
43228
+ data = concatBytes(this.pubKey, data);
43229
+ }
43230
+ const I = hmac(sha512, this.chainCode, data);
43231
+ const childTweak = bytesToNumber(I.slice(0, 32));
43232
+ const chainCode = I.slice(32);
43233
+ if (!secp256k1.utils.isValidPrivateKey(childTweak)) {
43234
+ throw new Error("Tweak bigger than curve order");
43235
+ }
43236
+ const opt = {
43237
+ versions: this.versions,
43238
+ chainCode,
43239
+ depth: this.depth + 1,
43240
+ parentFingerprint: this.fingerprint,
43241
+ index
43242
+ };
43243
+ try {
43244
+ if (this.privateKey) {
43245
+ const added = mod(this.privKey + childTweak, secp256k1.CURVE.n);
43246
+ if (!secp256k1.utils.isValidPrivateKey(added)) {
43247
+ throw new Error("The tweak was out of range or the resulted private key is invalid");
43248
+ }
43249
+ opt.privateKey = added;
43250
+ } else {
43251
+ const added = Point.fromHex(this.pubKey).add(Point.fromPrivateKey(childTweak));
43252
+ if (added.equals(Point.ZERO)) {
43253
+ throw new Error("The tweak was equal to negative P, which made the result key invalid");
43254
+ }
43255
+ opt.publicKey = added.toRawBytes(true);
43256
+ }
43257
+ return new HDKey(opt);
43258
+ } catch (err) {
43259
+ return this.deriveChild(index + 1);
43260
+ }
43261
+ }
43262
+ sign(hash) {
43263
+ if (!this.privateKey) {
43264
+ throw new Error("No privateKey set!");
43265
+ }
43266
+ abytes(hash, 32);
43267
+ return secp256k1.sign(hash, this.privKey).toCompactRawBytes();
43268
+ }
43269
+ verify(hash, signature) {
43270
+ abytes(hash, 32);
43271
+ abytes(signature, 64);
43272
+ if (!this.publicKey) {
43273
+ throw new Error("No publicKey set!");
43274
+ }
43275
+ let sig;
43276
+ try {
43277
+ sig = secp256k1.Signature.fromCompact(signature);
43278
+ } catch (error) {
43279
+ return false;
43280
+ }
43281
+ return secp256k1.verify(sig, hash, this.publicKey);
43282
+ }
43283
+ wipePrivateData() {
43284
+ this.privKey = undefined;
43285
+ if (this.privKeyBytes) {
43286
+ this.privKeyBytes.fill(0);
43287
+ this.privKeyBytes = undefined;
43288
+ }
43289
+ return this;
43290
+ }
43291
+ toJSON() {
43292
+ return {
43293
+ xpriv: this.privateExtendedKey,
43294
+ xpub: this.publicExtendedKey
43295
+ };
43296
+ }
43297
+ serialize(version, key) {
43298
+ if (!this.chainCode) {
43299
+ throw new Error("No chainCode set");
43300
+ }
43301
+ abytes(key, 33);
43302
+ return concatBytes(toU32(version), new Uint8Array([this.depth]), toU32(this.parentFingerprint), toU32(this.index), this.chainCode, key);
43303
+ }
43304
+ }
43305
+
43306
+ // src/evm-lite.ts
43307
+ var HOOD_CHAIN_ID = 4663;
43308
+ var DEFAULT_HOOD_RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
43309
+ var EVM_RPC_URL_ENV = "CANDLE_EVM_RPC_URL";
43310
+ var HOOD_USDG_ADDRESS = "0x5fc5360d0400a0fd4f2af552add042d716f1d168";
43311
+ var HOOD_USDG_DECIMALS = 6;
43312
+ var NATIVE_DECIMALS = 18;
43313
+ var EVM_DERIVATION_SCHEME = "bip32-secp256k1";
43314
+ var EVM_SECRET_BYTES = 32;
43315
+ function bytesToHex2(bytes) {
43316
+ let out = "";
43317
+ for (const b of bytes)
43318
+ out += b.toString(16).padStart(2, "0");
43319
+ return `0x${out}`;
43320
+ }
43321
+ function hexToBytes2(hex2) {
43322
+ const body = hex2.startsWith("0x") ? hex2.slice(2) : hex2;
43323
+ if (body.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(body))
43324
+ throw new Error(`not a hex string: ${hex2}`);
43325
+ const out = new Uint8Array(body.length / 2);
43326
+ for (let i = 0;i < out.length; i++)
43327
+ out[i] = Number.parseInt(body.slice(i * 2, i * 2 + 2), 16);
43328
+ return out;
43329
+ }
43330
+ function hexToBigInt(hex2) {
43331
+ const body = hex2.startsWith("0x") ? hex2.slice(2) : hex2;
43332
+ if (body === "")
43333
+ return 0n;
43334
+ if (!/^[0-9a-fA-F]+$/.test(body))
43335
+ throw new Error(`not a hex quantity: ${hex2}`);
43336
+ return BigInt(`0x${body}`);
43337
+ }
43338
+ function quantity(value) {
43339
+ return `0x${BigInt(value).toString(16)}`;
43340
+ }
43341
+ function uintToMinimalBytes(value) {
43342
+ if (value < 0n)
43343
+ throw new Error("RLP integers are non-negative");
43344
+ if (value === 0n)
43345
+ return new Uint8Array(0);
43346
+ let hex2 = value.toString(16);
43347
+ if (hex2.length % 2 !== 0)
43348
+ hex2 = `0${hex2}`;
43349
+ return hexToBytes2(hex2);
43350
+ }
43351
+ function rlpLength(length, offset) {
43352
+ if (length < 56)
43353
+ return Uint8Array.of(offset + length);
43354
+ const lengthBytes = uintToMinimalBytes(BigInt(length));
43355
+ return concat2(Uint8Array.of(offset + 55 + lengthBytes.length), lengthBytes);
43356
+ }
43357
+ function concat2(...parts) {
43358
+ const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));
43359
+ let at = 0;
43360
+ for (const part of parts) {
43361
+ out.set(part, at);
43362
+ at += part.length;
43363
+ }
43364
+ return out;
43365
+ }
43366
+ function rlpEncode(item) {
43367
+ if (item instanceof Uint8Array) {
43368
+ if (item.length === 1 && item[0] < 128)
43369
+ return item;
43370
+ return concat2(rlpLength(item.length, 128), item);
43371
+ }
43372
+ const body = concat2(...item.map(rlpEncode));
43373
+ return concat2(rlpLength(body.length, 192), body);
43374
+ }
43375
+ function toChecksumAddress(address) {
43376
+ const lower = (address.startsWith("0x") ? address.slice(2) : address).toLowerCase();
43377
+ if (!/^[0-9a-f]{40}$/.test(lower))
43378
+ throw new Error(`not an EVM address: ${address}`);
43379
+ const digest = bytesToHex2(keccak_256(new TextEncoder().encode(lower))).slice(2);
43380
+ let out = "0x";
43381
+ for (let i = 0;i < lower.length; i++) {
43382
+ const c = lower[i];
43383
+ out += Number.parseInt(digest[i], 16) >= 8 ? c.toUpperCase() : c;
43384
+ }
43385
+ return out;
43386
+ }
43387
+ function looksLikeEvmAddress(value) {
43388
+ return /^0x[0-9a-fA-F]{40}$/.test(value);
43389
+ }
43390
+ function checkEvmAddress(value) {
43391
+ if (!looksLikeEvmAddress(value))
43392
+ return { ok: false, reason: "not 0x followed by 40 hex characters" };
43393
+ const body = value.slice(2);
43394
+ const checksummed = toChecksumAddress(value);
43395
+ const hasLower = /[a-f]/.test(body);
43396
+ const hasUpper = /[A-F]/.test(body);
43397
+ if (hasLower && hasUpper && checksummed !== value) {
43398
+ return { ok: false, reason: "its mixed-case EIP-55 checksum does not match" };
43399
+ }
43400
+ return { ok: true, address: checksummed };
43401
+ }
43402
+ function sameEvmAddress(a, b) {
43403
+ return a.toLowerCase() === b.toLowerCase();
43404
+ }
43405
+ function evmAddressFromSecret(secret) {
43406
+ if (secret.length !== EVM_SECRET_BYTES) {
43407
+ throw new Error(`expected a ${EVM_SECRET_BYTES}-byte secp256k1 scalar, got ${secret.length}`);
43408
+ }
43409
+ const pub = secp256k1.getPublicKey(secret, false).slice(1);
43410
+ return toChecksumAddress(bytesToHex2(keccak_256(pub)).slice(-40));
43411
+ }
43412
+ function evmDerivationPath(index) {
43413
+ if (!Number.isInteger(index) || index < 0 || index >= 2147483648)
43414
+ throw new Error(`index out of range: ${index}`);
43415
+ return `m/44'/60'/${index}'/0/0`;
43416
+ }
43417
+ function deriveEvmKey(seed, index) {
43418
+ const path = evmDerivationPath(index);
43419
+ const root = HDKey.fromMasterSeed(seed);
43420
+ try {
43421
+ const leaf = root.derive(path);
43422
+ try {
43423
+ if (leaf.privateKey === null)
43424
+ throw new Error("BIP-32 derivation produced no private key");
43425
+ const secret = Uint8Array.from(leaf.privateKey);
43426
+ return { secret, address: evmAddressFromSecret(secret), path };
43427
+ } finally {
43428
+ leaf.wipePrivateData();
43429
+ }
43430
+ } finally {
43431
+ root.wipePrivateData();
43432
+ }
43433
+ }
43434
+ function buildNativeTransfer(input) {
43435
+ return { ...input, to: toChecksumAddress(input.to), data: new Uint8Array(0) };
43436
+ }
43437
+ function buildErc20Transfer(input) {
43438
+ const { token, recipient, amount, ...fees } = input;
43439
+ return { ...fees, to: toChecksumAddress(token), value: 0n, data: encodeErc20Transfer(recipient, amount) };
43440
+ }
43441
+ var ERC20_TRANSFER_SELECTOR = "0xa9059cbb";
43442
+ var ERC20_DECIMALS_SELECTOR = "0x313ce567";
43443
+ var ERC20_SYMBOL_SELECTOR = "0x95d89b41";
43444
+ var ERC20_BALANCE_OF_SELECTOR = "0x70a08231";
43445
+ function abiWord(value) {
43446
+ if (value < 0n || value >= 1n << 256n)
43447
+ throw new Error("uint256 out of range");
43448
+ const out = new Uint8Array(32);
43449
+ out.set(uintToMinimalBytes(value), 32 - uintToMinimalBytes(value).length);
43450
+ return out;
43451
+ }
43452
+ function abiAddress(address) {
43453
+ const out = new Uint8Array(32);
43454
+ out.set(hexToBytes2(address), 12);
43455
+ return out;
43456
+ }
43457
+ function encodeErc20Transfer(recipient, amount) {
43458
+ return concat2(hexToBytes2(ERC20_TRANSFER_SELECTOR), abiAddress(recipient), abiWord(amount));
43459
+ }
43460
+ var TYPE_2 = Uint8Array.of(2);
43461
+ function unsignedFields(tx) {
43462
+ return [
43463
+ uintToMinimalBytes(tx.chainId),
43464
+ uintToMinimalBytes(tx.nonce),
43465
+ uintToMinimalBytes(tx.maxPriorityFeePerGas),
43466
+ uintToMinimalBytes(tx.maxFeePerGas),
43467
+ uintToMinimalBytes(tx.gas),
43468
+ hexToBytes2(tx.to),
43469
+ uintToMinimalBytes(tx.value),
43470
+ tx.data,
43471
+ []
43472
+ ];
43473
+ }
43474
+ function signingPayload(tx) {
43475
+ return keccak_256(concat2(TYPE_2, rlpEncode(unsignedFields(tx))));
43476
+ }
43477
+ function signTransaction(tx, secret) {
43478
+ if (secret.length !== EVM_SECRET_BYTES) {
43479
+ throw new Error(`expected a ${EVM_SECRET_BYTES}-byte secp256k1 scalar, got ${secret.length}`);
43480
+ }
43481
+ const signature = secp256k1.sign(signingPayload(tx), secret, { lowS: true, prehash: false });
43482
+ const yParity = signature.recovery === 1 ? 1 : 0;
43483
+ const raw = concat2(TYPE_2, rlpEncode([
43484
+ ...unsignedFields(tx),
43485
+ uintToMinimalBytes(BigInt(yParity)),
43486
+ uintToMinimalBytes(signature.r),
43487
+ uintToMinimalBytes(signature.s)
43488
+ ]));
43489
+ return { raw, hash: bytesToHex2(keccak_256(raw)), yParity, r: signature.r, s: signature.s };
43490
+ }
43491
+ function formatUnits(raw, decimals) {
43492
+ const negative = raw < 0n;
43493
+ const magnitude = negative ? -raw : raw;
43494
+ const base = 10n ** BigInt(decimals);
43495
+ const whole = magnitude / base;
43496
+ const fraction = decimals === 0 ? "" : (magnitude % base).toString().padStart(decimals, "0").replace(/0+$/, "");
43497
+ const text = fraction === "" ? whole.toString() : `${whole}.${fraction}`;
43498
+ return negative ? `-${text}` : text;
43499
+ }
43500
+ function parseUnits(decimal, decimals) {
43501
+ if (!/^\d+(\.\d+)?$/.test(decimal))
43502
+ return { ok: false, reason: "not-a-number" };
43503
+ const [whole, fraction = ""] = decimal.split(".");
43504
+ if (fraction.length > decimals)
43505
+ return { ok: false, reason: "precision" };
43506
+ const raw = BigInt((whole ?? "0") + fraction.padEnd(decimals, "0"));
43507
+ if (raw === 0n)
43508
+ return { ok: false, reason: "zero" };
43509
+ return { ok: true, raw };
43510
+ }
43511
+
43512
+ class EvmRpcError extends Error {
43513
+ kind;
43514
+ method;
43515
+ rpcCode;
43516
+ constructor(kind, method, message, rpcCode) {
43517
+ super(message);
43518
+ this.name = "EvmRpcError";
43519
+ this.kind = kind;
43520
+ this.method = method;
43521
+ this.rpcCode = rpcCode;
43522
+ }
43523
+ }
43524
+ function decodeAbiString(bytes) {
43525
+ if (bytes.length === 0)
43526
+ return;
43527
+ if (bytes.length >= 64) {
43528
+ const offset = Number(hexToBigInt(bytesToHex2(bytes.subarray(0, 32))));
43529
+ if (offset + 32 <= bytes.length) {
43530
+ const length = Number(hexToBigInt(bytesToHex2(bytes.subarray(offset, offset + 32))));
43531
+ if (offset + 32 + length <= bytes.length) {
43532
+ return new TextDecoder().decode(bytes.subarray(offset + 32, offset + 32 + length));
43533
+ }
43534
+ }
43535
+ }
43536
+ if (bytes.length === 32) {
43537
+ let end = 32;
43538
+ while (end > 0 && bytes[end - 1] === 0)
43539
+ end--;
43540
+ const text = new TextDecoder().decode(bytes.subarray(0, end));
43541
+ return /^[\x20-\x7e]+$/.test(text) ? text : undefined;
43542
+ }
43543
+ return;
43544
+ }
43545
+ function createEvmRpc(url, fetchFn) {
43546
+ let id = 0;
43547
+ async function call(method, params) {
43548
+ id += 1;
43549
+ let res;
43550
+ try {
43551
+ res = await fetchFn(url, {
43552
+ method: "POST",
43553
+ headers: { "content-type": "application/json" },
43554
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
43555
+ });
43556
+ } catch (error) {
43557
+ throw new EvmRpcError("transport", method, `RPC ${method} failed: ${error instanceof Error ? error.message : error}`);
43558
+ }
43559
+ if (!res.ok)
43560
+ throw new EvmRpcError("transport", method, `RPC ${method} failed: HTTP ${res.status}`);
43561
+ let json;
43562
+ try {
43563
+ json = await res.json();
43564
+ } catch {
43565
+ throw new EvmRpcError("transport", method, `RPC ${method} failed: the answer was not JSON`);
43566
+ }
43567
+ if (json.error) {
43568
+ throw new EvmRpcError("rpc", method, `${json.error.message ?? "RPC error"}`, typeof json.error.code === "number" ? json.error.code : undefined);
43569
+ }
43570
+ return json.result;
43571
+ }
43572
+ const asQuantity = (value, method) => {
43573
+ if (typeof value !== "string")
43574
+ throw new EvmRpcError("rpc", method, `RPC ${method} answered without a quantity`);
43575
+ return hexToBigInt(value);
43576
+ };
43577
+ return {
43578
+ async chainId() {
43579
+ return asQuantity(await call("eth_chainId", []), "eth_chainId");
43580
+ },
43581
+ async getTransactionCount(address, tag) {
43582
+ return asQuantity(await call("eth_getTransactionCount", [address, tag]), "eth_getTransactionCount");
43583
+ },
43584
+ async estimateGas(input) {
43585
+ return asQuantity(await call("eth_estimateGas", [
43586
+ {
43587
+ from: input.from,
43588
+ to: input.to,
43589
+ value: quantity(input.value),
43590
+ ...input.data.length > 0 ? { data: bytesToHex2(input.data) } : {}
43591
+ }
43592
+ ]), "eth_estimateGas");
43593
+ },
43594
+ async feeHistory(blockCount, newestBlock, rewardPercentiles) {
43595
+ const r = await call("eth_feeHistory", [
43596
+ quantity(blockCount),
43597
+ newestBlock,
43598
+ rewardPercentiles
43599
+ ]);
43600
+ const base = Array.isArray(r?.baseFeePerGas) ? r.baseFeePerGas : [];
43601
+ if (base.length === 0)
43602
+ throw new EvmRpcError("rpc", "eth_feeHistory", "RPC eth_feeHistory answered no baseFeePerGas");
43603
+ return {
43604
+ baseFeePerGas: base.map((value) => asQuantity(value, "eth_feeHistory")),
43605
+ reward: (Array.isArray(r.reward) ? r.reward : []).map((row) => (Array.isArray(row) ? row : []).map((value) => asQuantity(value, "eth_feeHistory")))
43606
+ };
43607
+ },
43608
+ async maxPriorityFeePerGas() {
43609
+ return asQuantity(await call("eth_maxPriorityFeePerGas", []), "eth_maxPriorityFeePerGas");
43610
+ },
43611
+ async getBalance(address) {
43612
+ return asQuantity(await call("eth_getBalance", [address, "latest"]), "eth_getBalance");
43613
+ },
43614
+ async call(input) {
43615
+ const r = await call("eth_call", [{ to: input.to, data: bytesToHex2(input.data) }, "latest"]);
43616
+ if (typeof r !== "string")
43617
+ throw new EvmRpcError("rpc", "eth_call", "RPC eth_call answered without data");
43618
+ return hexToBytes2(r);
43619
+ },
43620
+ async sendRawTransaction(raw) {
43621
+ const r = await call("eth_sendRawTransaction", [bytesToHex2(raw)]);
43622
+ if (typeof r !== "string") {
43623
+ throw new EvmRpcError("rpc", "eth_sendRawTransaction", "RPC eth_sendRawTransaction answered without a hash");
43624
+ }
43625
+ return r;
43626
+ },
43627
+ async getTransactionReceipt(hash) {
43628
+ const r = await call("eth_getTransactionReceipt", [hash]);
43629
+ if (r === null || r === undefined)
43630
+ return null;
43631
+ const status = asQuantity(r.status, "eth_getTransactionReceipt");
43632
+ return {
43633
+ status: status === 1n ? 1 : 0,
43634
+ blockNumber: asQuantity(r.blockNumber, "eth_getTransactionReceipt"),
43635
+ transactionHash: typeof r.transactionHash === "string" ? r.transactionHash : hash
43636
+ };
43637
+ },
43638
+ async blockNumber() {
43639
+ return asQuantity(await call("eth_blockNumber", []), "eth_blockNumber");
43640
+ },
43641
+ async erc20Decimals(token) {
43642
+ const answer = await this.call({ to: token, data: hexToBytes2(ERC20_DECIMALS_SELECTOR) });
43643
+ if (answer.length !== 32)
43644
+ throw new EvmRpcError("rpc", "eth_call", "the contract did not answer decimals()");
43645
+ const value = hexToBigInt(bytesToHex2(answer));
43646
+ if (value > 255n)
43647
+ throw new EvmRpcError("rpc", "eth_call", "the contract's decimals() is not a uint8");
43648
+ return Number(value);
43649
+ },
43650
+ async erc20Symbol(token) {
43651
+ try {
43652
+ return decodeAbiString(await this.call({ to: token, data: hexToBytes2(ERC20_SYMBOL_SELECTOR) }));
43653
+ } catch (error) {
43654
+ if (error instanceof EvmRpcError && error.kind === "rpc")
43655
+ return;
43656
+ throw error;
43657
+ }
43658
+ },
43659
+ async erc20BalanceOf(token, owner) {
43660
+ const answer = await this.call({
43661
+ to: token,
43662
+ data: concat2(hexToBytes2(ERC20_BALANCE_OF_SELECTOR), abiAddress(owner))
43663
+ });
43664
+ if (answer.length !== 32)
43665
+ throw new EvmRpcError("rpc", "eth_call", "the contract did not answer balanceOf()");
43666
+ return hexToBigInt(bytesToHex2(answer));
43667
+ }
43668
+ };
43669
+ }
43670
+ var FEE_HISTORY_BLOCKS = 10;
43671
+ async function quoteFees(rpc) {
43672
+ const history = await rpc.feeHistory(FEE_HISTORY_BLOCKS, "latest", [50]);
43673
+ const baseFee = history.baseFeePerGas[history.baseFeePerGas.length - 1];
43674
+ let tip;
43675
+ let tipSource;
43676
+ try {
43677
+ tip = await rpc.maxPriorityFeePerGas();
43678
+ tipSource = "eth_maxPriorityFeePerGas";
43679
+ } catch (error) {
43680
+ if (!(error instanceof EvmRpcError) || error.kind !== "rpc")
43681
+ throw error;
43682
+ const rewards = history.reward.map((row) => row[0] ?? 0n).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
43683
+ tip = rewards.length === 0 ? 0n : rewards[Math.floor(rewards.length / 2)];
43684
+ tipSource = "eth_feeHistory";
43685
+ }
43686
+ return { baseFee, maxPriorityFeePerGas: tip, maxFeePerGas: 2n * baseFee + tip, tipSource };
43687
+ }
43688
+ function gasWithHeadroom(estimate) {
43689
+ return (estimate * 12n + 9n) / 10n;
43690
+ }
43691
+ function requiredDepth(chainId) {
43692
+ return chainId === BigInt(HOOD_CHAIN_ID) ? 1 : 2;
43693
+ }
43694
+ function rpcHostOf(url) {
43695
+ return new URL(url).host;
43696
+ }
43697
+ function resolveEvmRpcUrl(flag, envValue, flagName) {
43698
+ const fromFlag = flag?.trim() || undefined;
43699
+ const fromEnv = envValue?.trim() || undefined;
43700
+ const url = fromFlag ?? fromEnv ?? DEFAULT_HOOD_RPC_URL;
43701
+ const source = fromFlag !== undefined ? flagName : fromEnv !== undefined ? EVM_RPC_URL_ENV : undefined;
43702
+ let parsed;
43703
+ try {
43704
+ parsed = new URL(url);
43705
+ } catch {
43706
+ return { error: `${source ?? flagName} is not a valid URL: ${url}` };
43707
+ }
43708
+ const local = parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost";
43709
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
43710
+ return {
43711
+ error: `${source ?? flagName} must be https:// (plain http is allowed only for 127.0.0.1 / localhost).`
43712
+ };
43713
+ }
43714
+ return { url, builtIn: url === DEFAULT_HOOD_RPC_URL };
43715
+ }
43716
+
42463
43717
  // src/vault/ed25519.ts
42464
43718
  init_ed25519();
42465
43719
  init_esm();
@@ -42495,7 +43749,7 @@ var sha5122 = sha512;
42495
43749
 
42496
43750
  // src/vault/slip10.ts
42497
43751
  init_errors();
42498
- var HARDENED_OFFSET = 2147483648;
43752
+ var HARDENED_OFFSET2 = 2147483648;
42499
43753
  var ED25519_SEED_KEY = new TextEncoder().encode("ed25519 seed");
42500
43754
  function masterFromSeed(seed) {
42501
43755
  const i = hmac(sha5122, ED25519_SEED_KEY, seed);
@@ -42506,7 +43760,7 @@ function masterFromSeed(seed) {
42506
43760
  }
42507
43761
  }
42508
43762
  function deriveChild(node, index) {
42509
- if (!Number.isInteger(index) || index < HARDENED_OFFSET || index > 4294967295) {
43763
+ if (!Number.isInteger(index) || index < HARDENED_OFFSET2 || index > 4294967295) {
42510
43764
  throw new VaultError("VAULT_INDEX_INVALID", `SLIP-0010 Ed25519 has no non-hardened children; refusing index ${index}.`);
42511
43765
  }
42512
43766
  const data = new Uint8Array(1 + 32 + 4);
@@ -42533,9 +43787,9 @@ function parsePath(path) {
42533
43787
  if (!hardened) {
42534
43788
  throw new VaultError("VAULT_INDEX_INVALID", `SLIP-0010 Ed25519 has no non-hardened children; ${path} asks for one.`);
42535
43789
  }
42536
- if (index >= HARDENED_OFFSET)
43790
+ if (index >= HARDENED_OFFSET2)
42537
43791
  throw new VaultError("VAULT_INDEX_INVALID", `Derivation index out of range: ${part}`);
42538
- return index + HARDENED_OFFSET;
43792
+ return index + HARDENED_OFFSET2;
42539
43793
  });
42540
43794
  }
42541
43795
  function derivePath(seed, path) {
@@ -42570,6 +43824,10 @@ function solanaExternalPath(index) {
42570
43824
  function evmPath(index) {
42571
43825
  return `m/44'/60'/${assertIndex(index)}'/0/0`;
42572
43826
  }
43827
+ function evmIndexOfPath(path) {
43828
+ const match = /^m\/44'\/60'\/(\d+)'\/0\/0$/.exec(path);
43829
+ return match?.[1] === undefined ? undefined : Number(match[1]);
43830
+ }
42573
43831
  function pathForBranch(branch, index) {
42574
43832
  if (branch === "solanaVault")
42575
43833
  return solanaVaultPath(index);
@@ -42627,6 +43885,15 @@ async function deriveSolanaKey(entropy, path) {
42627
43885
  wipe(seed);
42628
43886
  }
42629
43887
  }
43888
+ async function deriveEvmKeyFromRoot(entropy, index) {
43889
+ const seed = await seedFromEntropy(entropy);
43890
+ try {
43891
+ const derived = deriveEvmKey(seed, index);
43892
+ return { ...derived, secret: ownSecret(derived.secret) };
43893
+ } finally {
43894
+ wipe(seed);
43895
+ }
43896
+ }
42630
43897
  // src/vault/local-sweep.ts
42631
43898
  init_esm();
42632
43899
  init_solana_lite();
@@ -43316,12 +44583,11 @@ async function vaultNewKey(args, ctx) {
43316
44583
  return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
43317
44584
  const chain2 = parsed.values["--chain"];
43318
44585
  if (chain2 === undefined)
43319
- return usage(ctx, "--chain solana is required.");
43320
- if (chain2 === "evm") {
43321
- return usage(ctx, "CHAIN_NOT_OFFERED: EVM keys arrive in Phase 4. This release derives Solana keys only.");
44586
+ return usage(ctx, "--chain solana|evm is required.");
44587
+ if (chain2 !== "solana" && chain2 !== "evm") {
44588
+ return usage(ctx, `Unknown chain: ${chain2}. This release derives Solana and EVM keys (--chain solana|evm).`);
43322
44589
  }
43323
- if (chain2 !== "solana")
43324
- return usage(ctx, `Unknown chain: ${chain2}. This release derives Solana keys only.`);
44590
+ const branch = chain2 === "evm" ? "evm" : "solanaVault";
43325
44591
  if (!refuseEnvPassphrase(ctx))
43326
44592
  return 1;
43327
44593
  if (!requireTty(ctx, "vault new-key"))
@@ -43356,7 +44622,7 @@ async function vaultNewKey(args, ctx) {
43356
44622
  suggestion: "Create a second vault with a fresh root (`candle vault init`) and move the funds across with `candle vault transfer`. There is no flag for this: no fact you could assert would make the old boundary known."
43357
44623
  });
43358
44624
  }
43359
- const clash = labelClash(vault.index, plannedLabels(vault.index.hd, batch, parsed.values["--label"]));
44625
+ const clash = labelClash(vault.index, plannedLabels(vault.index.hd, batch, parsed.values["--label"], branch));
43360
44626
  if (clash !== undefined) {
43361
44627
  return usage(ctx, batch.labels !== undefined ? `A key labelled ${clash} already exists in this vault; every --labels-from name must be new.` : parsed.values["--label"] !== undefined ? `A key labelled ${clash} already exists in this vault; choose another --label.` : `A key labelled ${clash} already exists in this vault; pass --label <name> to choose a different name for this key.`);
43362
44628
  }
@@ -43366,41 +44632,52 @@ async function vaultNewKey(args, ctx) {
43366
44632
  const root = await decryptRoot(vault);
43367
44633
  try {
43368
44634
  for (let made = 0;made < batch.count; made++) {
43369
- const index = nextAllocatableIndex(current.index.hd.nextIndex.solanaVault, current.index.hd.exposedIndexes.solanaVault);
43370
- const derivationPath = solanaVaultPath(index);
43371
- const label = batch.labels?.[made] ?? parsed.values["--label"] ?? `key-${index}`;
44635
+ const index = nextAllocatableIndex(current.index.hd.nextIndex[branch], current.index.hd.exposedIndexes[branch]);
44636
+ const derivationPath = branch === "evm" ? evmPath(index) : solanaVaultPath(index);
44637
+ const label = batch.labels?.[made] ?? parsed.values["--label"] ?? defaultLabel(branch, index);
43372
44638
  let address;
43373
44639
  let keyId;
43374
44640
  let blob;
43375
- const key = await deriveSolanaKey(root, derivationPath);
43376
- try {
43377
- address = key.address;
43378
- keyId = freshKeyId();
43379
- blob = await sealKeyBlob(current, keyId, key.secret64);
43380
- } finally {
43381
- wipe(key.secret64);
44641
+ if (branch === "evm") {
44642
+ const key = await deriveEvmKeyFromRoot(root, index);
44643
+ try {
44644
+ address = key.address;
44645
+ keyId = freshKeyId();
44646
+ blob = await sealKeyBlob(current, keyId, key.secret);
44647
+ } finally {
44648
+ wipe(key.secret);
44649
+ }
44650
+ } else {
44651
+ const key = await deriveSolanaKey(root, derivationPath);
44652
+ try {
44653
+ address = key.address;
44654
+ keyId = freshKeyId();
44655
+ blob = await sealKeyBlob(current, keyId, key.secret64);
44656
+ } finally {
44657
+ wipe(key.secret64);
44658
+ }
43382
44659
  }
43383
44660
  const entry = {
43384
44661
  id: keyId,
43385
- chain: "solana",
43386
- curve: "ed25519",
44662
+ chain: chain2,
44663
+ curve: chain2 === "evm" ? "secp256k1" : "ed25519",
43387
44664
  address,
43388
44665
  label,
43389
44666
  createdAt: new Date(deps.now()).toISOString(),
43390
44667
  role: "vault",
43391
44668
  origin: "derived",
43392
- derivation: { scheme: DERIVATION_SCHEME, path: derivationPath },
44669
+ derivation: { scheme: chain2 === "evm" ? EVM_DERIVATION_SCHEME : DERIVATION_SCHEME, path: derivationPath },
43393
44670
  exposure: { everRemoteExposed: false, everExported: false }
43394
44671
  };
43395
44672
  current = await commitVault(current, {
43396
44673
  index: {
43397
- hd: { ...current.index.hd, nextIndex: { ...current.index.hd.nextIndex, solanaVault: index + 1 } },
44674
+ hd: { ...current.index.hd, nextIndex: { ...current.index.hd.nextIndex, [branch]: index + 1 } },
43398
44675
  entries: [...current.index.entries, entry]
43399
44676
  },
43400
44677
  addKeys: [blob]
43401
44678
  }, deps);
43402
44679
  derived.push({ address, label, path: derivationPath, index, keyId });
43403
- await verifyWrittenFromDisk(current, address, keyId);
44680
+ await verifyWrittenFromDisk(current, address, keyId, chain2);
43404
44681
  last = { address, keyId };
43405
44682
  if (!ctx.json && batchRequested)
43406
44683
  deps.stdout.write(`${address} ${label} ${derivationPath}
@@ -43415,7 +44692,7 @@ async function vaultNewKey(args, ctx) {
43415
44692
  const only = derived[0];
43416
44693
  if (only === undefined || last === undefined)
43417
44694
  throw new VaultError("VAULT_WRITE_FAILED", "No key was derived.");
43418
- await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx);
44695
+ await verifyWritten(path, last.address, last.keyId, opened.reopen, ctx, chain2);
43419
44696
  if (ctx.json) {
43420
44697
  if (batchRequested)
43421
44698
  writeJson(deps, { ok: true, count: derived.length, keys: derived });
@@ -43454,16 +44731,25 @@ ${derived.length} of ${requested} keys were created and ARE in the vault; the va
43454
44731
  ` + `Re-run for the remaining ${requested - derived.length}${derived.length > 0 ? " (with a --labels-from file holding the names that did not land)" : ""}.
43455
44732
  `);
43456
44733
  }
43457
- function plannedLabels(hd, batch, labelFlag) {
44734
+ function plannedLabels(hd, batch, labelFlag, branch = "solanaVault") {
43458
44735
  const labels = [];
43459
- let counter = hd.nextIndex.solanaVault;
44736
+ let counter = hd.nextIndex[branch];
43460
44737
  for (let made = 0;made < batch.count; made++) {
43461
- const index = nextAllocatableIndex(counter, hd.exposedIndexes.solanaVault);
43462
- labels.push(batch.labels?.[made] ?? labelFlag ?? `key-${index}`);
44738
+ const index = nextAllocatableIndex(counter, hd.exposedIndexes[branch]);
44739
+ labels.push(batch.labels?.[made] ?? labelFlag ?? defaultLabel(branch, index));
43463
44740
  counter = index + 1;
43464
44741
  }
43465
44742
  return labels;
43466
44743
  }
44744
+ function defaultLabel(branch, index) {
44745
+ return branch === "evm" ? `evm-${index}` : `key-${index}`;
44746
+ }
44747
+ function addressOfSecret(chain2, secret) {
44748
+ return chain2 === "evm" ? evmAddressFromSecret(secret) : addressFromSecret64(secret);
44749
+ }
44750
+ function sameAddress(chain2, a, b) {
44751
+ return chain2 === "evm" ? sameEvmAddress(a, b) : a === b;
44752
+ }
43467
44753
  function labelClash(index, planned) {
43468
44754
  const taken = new Set(index.entries.map((entry) => entry.label));
43469
44755
  const seen = new Set;
@@ -43480,7 +44766,7 @@ function nextAllocatableIndex(counter, exposed) {
43480
44766
  index++;
43481
44767
  return index;
43482
44768
  }
43483
- async function verifyWrittenFromDisk(vault, address, keyId) {
44769
+ async function verifyWrittenFromDisk(vault, address, keyId, chain2 = "solana") {
43484
44770
  const raw = await readVaultRaw(vault.path);
43485
44771
  if (raw === null) {
43486
44772
  throw new VaultError("VAULT_WRITE_FAILED", `The vault at ${vault.path} could not be read back after the write.`);
@@ -43488,14 +44774,14 @@ async function verifyWrittenFromDisk(vault, address, keyId) {
43488
44774
  const onDisk = { ...vault, raw, file: parseVaultFile(raw) };
43489
44775
  const secret = await decryptKey(onDisk, keyId);
43490
44776
  try {
43491
- if (addressFromSecret64(secret) !== address) {
44777
+ if (!sameAddress(chain2, addressOfSecret(chain2, secret), address)) {
43492
44778
  throw new VaultError("VAULT_VERIFY_FAILED", "The key written to the vault does not produce the address just derived.");
43493
44779
  }
43494
44780
  } finally {
43495
44781
  wipe(secret);
43496
44782
  }
43497
44783
  }
43498
- async function verifyWritten(path, address, keyId, reopen, _ctx) {
44784
+ async function verifyWritten(path, address, keyId, reopen, _ctx, chain2 = "solana") {
43499
44785
  const raw = await readVaultRaw(path);
43500
44786
  if (raw === null)
43501
44787
  throw new VaultError("VAULT_WRITE_FAILED", `The vault at ${path} could not be read back after the write.`);
@@ -43503,7 +44789,7 @@ async function verifyWritten(path, address, keyId, reopen, _ctx) {
43503
44789
  try {
43504
44790
  const secret = await decryptKey(reopened, keyId);
43505
44791
  try {
43506
- if (addressFromSecret64(secret) !== address) {
44792
+ if (!sameAddress(chain2, addressOfSecret(chain2, secret), address)) {
43507
44793
  throw new VaultError("VAULT_VERIFY_FAILED", "The key written to the vault does not produce the address just derived.");
43508
44794
  }
43509
44795
  } finally {
@@ -43718,6 +45004,8 @@ async function externalSweep(args, ctx) {
43718
45004
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
43719
45005
  });
43720
45006
  const vault = hold(opened.vault);
45007
+ assertNotEvmEntry(vault.index, source, "external sweep");
45008
+ assertNotEvmEntry(vault.index, to, "external sweep");
43721
45009
  const sourceEntry = requireExternalEntry(vault.index, source);
43722
45010
  const destination = findVaultRoleEntry(vault.index, to);
43723
45011
  if (destination === undefined || destination.role !== "vault") {
@@ -43821,6 +45109,10 @@ var NO_API_KEY = {
43821
45109
  message: "No API key for this profile.",
43822
45110
  suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
43823
45111
  };
45112
+ function widenRefusedHint(prefix, walletIds) {
45113
+ const wallets = walletIds.length > 0 ? walletIds.join(" ") : "<wallet...>";
45114
+ return `To move TEE wallets to this key, run: candle tee rebind ${wallets} --to-key ${prefix} ` + "(owner, device token; --label-prefix <p> names many at once). To grant a linked wallet to the key " + "instead, use the agent console's Agents tab in a signed-in session. If `tee rebind` says the wallets " + "are already bound, grant them to the key in the agent console's Agents tab (signed in).";
45115
+ }
43824
45116
  function formatTimestamp2(ms) {
43825
45117
  return ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) : "-";
43826
45118
  }
@@ -43916,7 +45208,15 @@ async function keysWalletsSet(args, ctx) {
43916
45208
  env: deps.env
43917
45209
  });
43918
45210
  if (!result.ok) {
43919
- writeFailure(deps, result, { apiUrl, authType: "key" }, json);
45211
+ if (result.code !== "LOOSEN_REQUIRES_SESSION") {
45212
+ writeFailure(deps, result, { apiUrl, authType: "key" }, json);
45213
+ return 1;
45214
+ }
45215
+ const hint = widenRefusedHint(prefix, walletIds);
45216
+ writeFailure(deps, { ...result, uiHint: hint }, { apiUrl, authType: "key" }, json);
45217
+ if (!json)
45218
+ deps.stderr.write(`${hint}
45219
+ `);
43920
45220
  return 1;
43921
45221
  }
43922
45222
  if (json) {
@@ -46487,6 +47787,7 @@ function programNameOf(programId) {
46487
47787
  // src/commands/sign.ts
46488
47788
  init_solana_lite();
46489
47789
  init_errors();
47790
+ init_promote_support();
46490
47791
  init_store();
46491
47792
  init_vault_support();
46492
47793
  async function readInput(ctx, file) {
@@ -46665,6 +47966,7 @@ async function sign2(args, ctx) {
46665
47966
  const vault = hold(opened.vault);
46666
47967
  const named = [];
46667
47968
  for (const requested of lifted.values) {
47969
+ assertNotEvmEntry(vault.index, requested, "candle sign");
46668
47970
  const entry = findExternalEntry(vault.index, requested);
46669
47971
  if (entry === undefined) {
46670
47972
  const other = vault.index.entries.find((candidate) => candidate.label === requested || candidate.address === requested);
@@ -46820,6 +48122,7 @@ async function signMessage2(args, ctx) {
46820
48122
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
46821
48123
  });
46822
48124
  const vault = hold(opened.vault);
48125
+ assertNotEvmEntry(vault.index, requested, "candle sign message");
46823
48126
  const entry = findExternalEntry(vault.index, requested);
46824
48127
  if (entry === undefined) {
46825
48128
  const other = vault.index.entries.find((candidate) => candidate.label === requested || candidate.address === requested);
@@ -47469,11 +48772,11 @@ async function resolveTeeAddress(ctx, _parsed, address, openLegacy, access3 = "r
47469
48772
  } catch (error) {
47470
48773
  if (isUsageError(error)) {
47471
48774
  writeUsageFailure(ctx.deps, error.message, ctx.json);
47472
- return { ok: false, code: 2 };
48775
+ return { ok: false, code: 2, reported: true };
47473
48776
  }
47474
48777
  if (isVaultError(error)) {
47475
48778
  writeLocalFailure(ctx.deps, { code: error.code, message: error.message, ...error.suggestion ? { suggestion: error.suggestion } : {} }, ctx.json);
47476
- return { ok: false, code: error.exitCode };
48779
+ return { ok: false, code: error.exitCode, reported: true };
47477
48780
  }
47478
48781
  throw error;
47479
48782
  }
@@ -47823,7 +49126,7 @@ var sigma16 = _utf8ToBytes("expand 16-byte k");
47823
49126
  var sigma32 = _utf8ToBytes("expand 32-byte k");
47824
49127
  var sigma16_32 = u322(sigma16);
47825
49128
  var sigma32_32 = u322(sigma32);
47826
- function rotl(a, b) {
49129
+ function rotl2(a, b) {
47827
49130
  return a << b | a >>> 32 - b;
47828
49131
  }
47829
49132
  function isAligned322(b) {
@@ -48238,69 +49541,69 @@ function chachaCore(s, k, n, out, cnt, rounds = 20) {
48238
49541
  let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15;
48239
49542
  for (let r = 0;r < rounds; r += 2) {
48240
49543
  x00 = x00 + x04 | 0;
48241
- x12 = rotl(x12 ^ x00, 16);
49544
+ x12 = rotl2(x12 ^ x00, 16);
48242
49545
  x08 = x08 + x12 | 0;
48243
- x04 = rotl(x04 ^ x08, 12);
49546
+ x04 = rotl2(x04 ^ x08, 12);
48244
49547
  x00 = x00 + x04 | 0;
48245
- x12 = rotl(x12 ^ x00, 8);
49548
+ x12 = rotl2(x12 ^ x00, 8);
48246
49549
  x08 = x08 + x12 | 0;
48247
- x04 = rotl(x04 ^ x08, 7);
49550
+ x04 = rotl2(x04 ^ x08, 7);
48248
49551
  x01 = x01 + x05 | 0;
48249
- x13 = rotl(x13 ^ x01, 16);
49552
+ x13 = rotl2(x13 ^ x01, 16);
48250
49553
  x09 = x09 + x13 | 0;
48251
- x05 = rotl(x05 ^ x09, 12);
49554
+ x05 = rotl2(x05 ^ x09, 12);
48252
49555
  x01 = x01 + x05 | 0;
48253
- x13 = rotl(x13 ^ x01, 8);
49556
+ x13 = rotl2(x13 ^ x01, 8);
48254
49557
  x09 = x09 + x13 | 0;
48255
- x05 = rotl(x05 ^ x09, 7);
49558
+ x05 = rotl2(x05 ^ x09, 7);
48256
49559
  x02 = x02 + x06 | 0;
48257
- x14 = rotl(x14 ^ x02, 16);
49560
+ x14 = rotl2(x14 ^ x02, 16);
48258
49561
  x10 = x10 + x14 | 0;
48259
- x06 = rotl(x06 ^ x10, 12);
49562
+ x06 = rotl2(x06 ^ x10, 12);
48260
49563
  x02 = x02 + x06 | 0;
48261
- x14 = rotl(x14 ^ x02, 8);
49564
+ x14 = rotl2(x14 ^ x02, 8);
48262
49565
  x10 = x10 + x14 | 0;
48263
- x06 = rotl(x06 ^ x10, 7);
49566
+ x06 = rotl2(x06 ^ x10, 7);
48264
49567
  x03 = x03 + x07 | 0;
48265
- x15 = rotl(x15 ^ x03, 16);
49568
+ x15 = rotl2(x15 ^ x03, 16);
48266
49569
  x11 = x11 + x15 | 0;
48267
- x07 = rotl(x07 ^ x11, 12);
49570
+ x07 = rotl2(x07 ^ x11, 12);
48268
49571
  x03 = x03 + x07 | 0;
48269
- x15 = rotl(x15 ^ x03, 8);
49572
+ x15 = rotl2(x15 ^ x03, 8);
48270
49573
  x11 = x11 + x15 | 0;
48271
- x07 = rotl(x07 ^ x11, 7);
49574
+ x07 = rotl2(x07 ^ x11, 7);
48272
49575
  x00 = x00 + x05 | 0;
48273
- x15 = rotl(x15 ^ x00, 16);
49576
+ x15 = rotl2(x15 ^ x00, 16);
48274
49577
  x10 = x10 + x15 | 0;
48275
- x05 = rotl(x05 ^ x10, 12);
49578
+ x05 = rotl2(x05 ^ x10, 12);
48276
49579
  x00 = x00 + x05 | 0;
48277
- x15 = rotl(x15 ^ x00, 8);
49580
+ x15 = rotl2(x15 ^ x00, 8);
48278
49581
  x10 = x10 + x15 | 0;
48279
- x05 = rotl(x05 ^ x10, 7);
49582
+ x05 = rotl2(x05 ^ x10, 7);
48280
49583
  x01 = x01 + x06 | 0;
48281
- x12 = rotl(x12 ^ x01, 16);
49584
+ x12 = rotl2(x12 ^ x01, 16);
48282
49585
  x11 = x11 + x12 | 0;
48283
- x06 = rotl(x06 ^ x11, 12);
49586
+ x06 = rotl2(x06 ^ x11, 12);
48284
49587
  x01 = x01 + x06 | 0;
48285
- x12 = rotl(x12 ^ x01, 8);
49588
+ x12 = rotl2(x12 ^ x01, 8);
48286
49589
  x11 = x11 + x12 | 0;
48287
- x06 = rotl(x06 ^ x11, 7);
49590
+ x06 = rotl2(x06 ^ x11, 7);
48288
49591
  x02 = x02 + x07 | 0;
48289
- x13 = rotl(x13 ^ x02, 16);
49592
+ x13 = rotl2(x13 ^ x02, 16);
48290
49593
  x08 = x08 + x13 | 0;
48291
- x07 = rotl(x07 ^ x08, 12);
49594
+ x07 = rotl2(x07 ^ x08, 12);
48292
49595
  x02 = x02 + x07 | 0;
48293
- x13 = rotl(x13 ^ x02, 8);
49596
+ x13 = rotl2(x13 ^ x02, 8);
48294
49597
  x08 = x08 + x13 | 0;
48295
- x07 = rotl(x07 ^ x08, 7);
49598
+ x07 = rotl2(x07 ^ x08, 7);
48296
49599
  x03 = x03 + x04 | 0;
48297
- x14 = rotl(x14 ^ x03, 16);
49600
+ x14 = rotl2(x14 ^ x03, 16);
48298
49601
  x09 = x09 + x14 | 0;
48299
- x04 = rotl(x04 ^ x09, 12);
49602
+ x04 = rotl2(x04 ^ x09, 12);
48300
49603
  x03 = x03 + x04 | 0;
48301
- x14 = rotl(x14 ^ x03, 8);
49604
+ x14 = rotl2(x14 ^ x03, 8);
48302
49605
  x09 = x09 + x14 | 0;
48303
- x04 = rotl(x04 ^ x09, 7);
49606
+ x04 = rotl2(x04 ^ x09, 7);
48304
49607
  }
48305
49608
  let oi = 0;
48306
49609
  out[oi++] = y00 + x00 | 0;
@@ -48573,7 +49876,7 @@ function i2Osp(n, w) {
48573
49876
  }
48574
49877
  return ret;
48575
49878
  }
48576
- function concat3(a, b) {
49879
+ function concat4(a, b) {
48577
49880
  const ret = new Uint8Array(a.length + b.length);
48578
49881
  ret.set(a, 0);
48579
49882
  ret.set(b, a.length);
@@ -48718,11 +50021,11 @@ class Dhkem {
48718
50021
  const sks = isCryptoKeyPair(params.senderKey) ? params.senderKey.privateKey : params.senderKey;
48719
50022
  const dh1 = new Uint8Array(await this._prim.dh(ke.privateKey, params.recipientPublicKey));
48720
50023
  const dh2 = new Uint8Array(await this._prim.dh(sks, params.recipientPublicKey));
48721
- dh = concat3(dh1, dh2);
50024
+ dh = concat4(dh1, dh2);
48722
50025
  }
48723
50026
  let kemContext;
48724
50027
  if (params.senderKey === undefined) {
48725
- kemContext = concat3(new Uint8Array(enc), new Uint8Array(pkrm));
50028
+ kemContext = concat4(new Uint8Array(enc), new Uint8Array(pkrm));
48726
50029
  } else {
48727
50030
  const pks = isCryptoKeyPair(params.senderKey) ? params.senderKey.publicKey : await this._prim.derivePublicKey(params.senderKey);
48728
50031
  const pksm = await this._prim.serializePublicKey(pks);
@@ -48749,11 +50052,11 @@ class Dhkem {
48749
50052
  } else {
48750
50053
  const dh1 = new Uint8Array(await this._prim.dh(skr, pke));
48751
50054
  const dh2 = new Uint8Array(await this._prim.dh(skr, params.senderPublicKey));
48752
- dh = concat3(dh1, dh2);
50055
+ dh = concat4(dh1, dh2);
48753
50056
  }
48754
50057
  let kemContext;
48755
50058
  if (params.senderPublicKey === undefined) {
48756
- kemContext = concat3(new Uint8Array(params.enc), new Uint8Array(pkrm));
50059
+ kemContext = concat4(new Uint8Array(params.enc), new Uint8Array(pkrm));
48757
50060
  } else {
48758
50061
  const pksm = await this._prim.serializePublicKey(params.senderPublicKey);
48759
50062
  kemContext = new Uint8Array(params.enc.byteLength + pkrm.byteLength + pksm.byteLength);
@@ -50194,241 +51497,9 @@ async function runImportFlow(params) {
50194
51497
 
50195
51498
  // src/wallet-keygen.ts
50196
51499
  import { generateKeyPairSync } from "node:crypto";
50197
-
50198
- // ../../node_modules/@noble/curves/esm/secp256k1.js
50199
- init_sha2();
50200
- init__shortw_utils();
50201
- init_modular();
50202
- /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
50203
- var secp256k1_CURVE = {
50204
- p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),
50205
- n: BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),
50206
- h: BigInt(1),
50207
- a: BigInt(0),
50208
- b: BigInt(7),
50209
- Gx: BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),
50210
- Gy: BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")
50211
- };
50212
- var secp256k1_ENDO = {
50213
- beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),
50214
- basises: [
50215
- [BigInt("0x3086d221a7d46bcde86c90e49284eb15"), -BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],
50216
- [BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"), BigInt("0x3086d221a7d46bcde86c90e49284eb15")]
50217
- ]
50218
- };
50219
- var _2n5 = /* @__PURE__ */ BigInt(2);
50220
- function sqrtMod(y) {
50221
- const P2 = secp256k1_CURVE.p;
50222
- const _3n4 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22);
50223
- const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
50224
- const b2 = y * y * y % P2;
50225
- const b3 = b2 * b2 * y % P2;
50226
- const b6 = pow2(b3, _3n4, P2) * b3 % P2;
50227
- const b9 = pow2(b6, _3n4, P2) * b3 % P2;
50228
- const b11 = pow2(b9, _2n5, P2) * b2 % P2;
50229
- const b22 = pow2(b11, _11n, P2) * b11 % P2;
50230
- const b44 = pow2(b22, _22n, P2) * b22 % P2;
50231
- const b88 = pow2(b44, _44n, P2) * b44 % P2;
50232
- const b176 = pow2(b88, _88n, P2) * b88 % P2;
50233
- const b220 = pow2(b176, _44n, P2) * b44 % P2;
50234
- const b223 = pow2(b220, _3n4, P2) * b3 % P2;
50235
- const t1 = pow2(b223, _23n, P2) * b22 % P2;
50236
- const t2 = pow2(t1, _6n, P2) * b2 % P2;
50237
- const root = pow2(t2, _2n5, P2);
50238
- if (!Fpk1.eql(Fpk1.sqr(root), y))
50239
- throw new Error("Cannot find square root");
50240
- return root;
50241
- }
50242
- var Fpk1 = Field(secp256k1_CURVE.p, { sqrt: sqrtMod });
50243
- var secp256k1 = createCurve({ ...secp256k1_CURVE, Fp: Fpk1, lowS: true, endo: secp256k1_ENDO }, sha256);
50244
-
50245
- // ../../node_modules/@noble/hashes/esm/sha3.js
50246
- init__u64();
50247
- init_utils();
50248
- var _0n7 = BigInt(0);
50249
- var _1n7 = BigInt(1);
50250
- var _2n6 = BigInt(2);
50251
- var _7n2 = BigInt(7);
50252
- var _256n = BigInt(256);
50253
- var _0x71n = BigInt(113);
50254
- var SHA3_PI = [];
50255
- var SHA3_ROTL = [];
50256
- var _SHA3_IOTA = [];
50257
- for (let round = 0, R = _1n7, x = 1, y = 0;round < 24; round++) {
50258
- [x, y] = [y, (2 * x + 3 * y) % 5];
50259
- SHA3_PI.push(2 * (5 * y + x));
50260
- SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64);
50261
- let t = _0n7;
50262
- for (let j = 0;j < 7; j++) {
50263
- R = (R << _1n7 ^ (R >> _7n2) * _0x71n) % _256n;
50264
- if (R & _2n6)
50265
- t ^= _1n7 << (_1n7 << /* @__PURE__ */ BigInt(j)) - _1n7;
50266
- }
50267
- _SHA3_IOTA.push(t);
50268
- }
50269
- var IOTAS = split(_SHA3_IOTA, true);
50270
- var SHA3_IOTA_H = IOTAS[0];
50271
- var SHA3_IOTA_L = IOTAS[1];
50272
- var rotlH = (h, l, s) => s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s);
50273
- var rotlL = (h, l, s) => s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s);
50274
- function keccakP(s, rounds = 24) {
50275
- const B = new Uint32Array(5 * 2);
50276
- for (let round = 24 - rounds;round < 24; round++) {
50277
- for (let x = 0;x < 10; x++)
50278
- B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
50279
- for (let x = 0;x < 10; x += 2) {
50280
- const idx1 = (x + 8) % 10;
50281
- const idx0 = (x + 2) % 10;
50282
- const B0 = B[idx0];
50283
- const B1 = B[idx0 + 1];
50284
- const Th = rotlH(B0, B1, 1) ^ B[idx1];
50285
- const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
50286
- for (let y = 0;y < 50; y += 10) {
50287
- s[x + y] ^= Th;
50288
- s[x + y + 1] ^= Tl;
50289
- }
50290
- }
50291
- let curH = s[2];
50292
- let curL = s[3];
50293
- for (let t = 0;t < 24; t++) {
50294
- const shift = SHA3_ROTL[t];
50295
- const Th = rotlH(curH, curL, shift);
50296
- const Tl = rotlL(curH, curL, shift);
50297
- const PI = SHA3_PI[t];
50298
- curH = s[PI];
50299
- curL = s[PI + 1];
50300
- s[PI] = Th;
50301
- s[PI + 1] = Tl;
50302
- }
50303
- for (let y = 0;y < 50; y += 10) {
50304
- for (let x = 0;x < 10; x++)
50305
- B[x] = s[y + x];
50306
- for (let x = 0;x < 10; x++)
50307
- s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
50308
- }
50309
- s[0] ^= SHA3_IOTA_H[round];
50310
- s[1] ^= SHA3_IOTA_L[round];
50311
- }
50312
- clean(B);
50313
- }
50314
-
50315
- class Keccak extends Hash {
50316
- constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) {
50317
- super();
50318
- this.pos = 0;
50319
- this.posOut = 0;
50320
- this.finished = false;
50321
- this.destroyed = false;
50322
- this.enableXOF = false;
50323
- this.blockLen = blockLen;
50324
- this.suffix = suffix;
50325
- this.outputLen = outputLen;
50326
- this.enableXOF = enableXOF;
50327
- this.rounds = rounds;
50328
- anumber(outputLen);
50329
- if (!(0 < blockLen && blockLen < 200))
50330
- throw new Error("only keccak-f1600 function is supported");
50331
- this.state = new Uint8Array(200);
50332
- this.state32 = u32(this.state);
50333
- }
50334
- clone() {
50335
- return this._cloneInto();
50336
- }
50337
- keccak() {
50338
- swap32IfBE(this.state32);
50339
- keccakP(this.state32, this.rounds);
50340
- swap32IfBE(this.state32);
50341
- this.posOut = 0;
50342
- this.pos = 0;
50343
- }
50344
- update(data) {
50345
- aexists(this);
50346
- data = toBytes(data);
50347
- abytes(data);
50348
- const { blockLen, state } = this;
50349
- const len = data.length;
50350
- for (let pos = 0;pos < len; ) {
50351
- const take2 = Math.min(blockLen - this.pos, len - pos);
50352
- for (let i = 0;i < take2; i++)
50353
- state[this.pos++] ^= data[pos++];
50354
- if (this.pos === blockLen)
50355
- this.keccak();
50356
- }
50357
- return this;
50358
- }
50359
- finish() {
50360
- if (this.finished)
50361
- return;
50362
- this.finished = true;
50363
- const { state, suffix, pos, blockLen } = this;
50364
- state[pos] ^= suffix;
50365
- if ((suffix & 128) !== 0 && pos === blockLen - 1)
50366
- this.keccak();
50367
- state[blockLen - 1] ^= 128;
50368
- this.keccak();
50369
- }
50370
- writeInto(out) {
50371
- aexists(this, false);
50372
- abytes(out);
50373
- this.finish();
50374
- const bufferOut = this.state;
50375
- const { blockLen } = this;
50376
- for (let pos = 0, len = out.length;pos < len; ) {
50377
- if (this.posOut >= blockLen)
50378
- this.keccak();
50379
- const take2 = Math.min(blockLen - this.posOut, len - pos);
50380
- out.set(bufferOut.subarray(this.posOut, this.posOut + take2), pos);
50381
- this.posOut += take2;
50382
- pos += take2;
50383
- }
50384
- return out;
50385
- }
50386
- xofInto(out) {
50387
- if (!this.enableXOF)
50388
- throw new Error("XOF is not possible for this instance");
50389
- return this.writeInto(out);
50390
- }
50391
- xof(bytes) {
50392
- anumber(bytes);
50393
- return this.xofInto(new Uint8Array(bytes));
50394
- }
50395
- digestInto(out) {
50396
- aoutput(out, this);
50397
- if (this.finished)
50398
- throw new Error("digest() was already called");
50399
- this.writeInto(out);
50400
- this.destroy();
50401
- return out;
50402
- }
50403
- digest() {
50404
- return this.digestInto(new Uint8Array(this.outputLen));
50405
- }
50406
- destroy() {
50407
- this.destroyed = true;
50408
- clean(this.state);
50409
- }
50410
- _cloneInto(to) {
50411
- const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
50412
- to || (to = new Keccak(blockLen, suffix, outputLen, enableXOF, rounds));
50413
- to.state32.set(this.state32);
50414
- to.pos = this.pos;
50415
- to.posOut = this.posOut;
50416
- to.finished = this.finished;
50417
- to.rounds = rounds;
50418
- to.suffix = suffix;
50419
- to.outputLen = outputLen;
50420
- to.enableXOF = enableXOF;
50421
- to.destroyed = this.destroyed;
50422
- return to;
50423
- }
50424
- }
50425
- var gen = (suffix, blockLen, outputLen) => createHasher(() => new Keccak(blockLen, suffix, outputLen));
50426
- var keccak_256 = /* @__PURE__ */ (() => gen(1, 136, 256 / 8))();
50427
-
50428
- // src/wallet-keygen.ts
50429
51500
  init_esm();
50430
51501
  var hex3 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
50431
- function toChecksumAddress(lowercaseBody) {
51502
+ function toChecksumAddress2(lowercaseBody) {
50432
51503
  const digest = hex3(keccak_256(new TextEncoder().encode(lowercaseBody)));
50433
51504
  let out = "0x";
50434
51505
  for (let i = 0;i < lowercaseBody.length; i++) {
@@ -50449,7 +51520,7 @@ function generateSolana() {
50449
51520
  function generateEvm() {
50450
51521
  const priv = secp256k1.utils.randomPrivateKey();
50451
51522
  const pub = secp256k1.getPublicKey(priv, false).slice(1);
50452
- return { address: toChecksumAddress(hex3(keccak_256(pub)).slice(-40)), privateKey: `0x${hex3(priv)}` };
51523
+ return { address: toChecksumAddress2(hex3(keccak_256(pub)).slice(-40)), privateKey: `0x${hex3(priv)}` };
50453
51524
  }
50454
51525
  function generateWallet(chain2) {
50455
51526
  return chain2 === "solana" ? generateSolana() : generateEvm();
@@ -50465,6 +51536,11 @@ init_deps();
50465
51536
  init_profiles();
50466
51537
  init_render();
50467
51538
  init_secret_store();
51539
+ function trustedCell(row) {
51540
+ if (row.revokedAt)
51541
+ return "-";
51542
+ return row.addedVia === "session" || typeof row.trustedAt === "number" ? "yes" : "no";
51543
+ }
50468
51544
  var TEE_PROFILES = new Set(["ember-tee", "ember-hot"]);
50469
51545
  function isTeeRow(row) {
50470
51546
  return row.profile !== undefined && TEE_PROFILES.has(row.profile);
@@ -50515,6 +51591,9 @@ var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade
50515
51591
  var TEE_HINT = `A wallet marked tee is a TEE trading wallet: pass its id, address or label to candle swap --wallet.
50516
51592
  ` + `This account's embedded wallet, shown above, can pay for a token trade too.
50517
51593
  `;
51594
+ var UNTRUSTED_HINT = `A wallet marked Trusted no was linked by an API key: agents can move funds into it only once you mark it.
51595
+ ` + `Run: candle wallets trust <label|address|prefix*> (the device token, never an API key)
51596
+ `;
50518
51597
  var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
50519
51598
  `;
50520
51599
  async function probeSignerStates(rows, store) {
@@ -50621,24 +51700,28 @@ Linked wallets (${linkedRows.length}):
50621
51700
  `);
50622
51701
  } else {
50623
51702
  const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
50624
- deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
51703
+ deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Trusted", "Signer"], linkedRows.map((wallet, index) => [
50625
51704
  wallet._id,
50626
51705
  wallet.chain,
50627
51706
  wallet.address,
50628
51707
  wallet.label ?? "-",
50629
51708
  isTeeRow(wallet) ? "tee" : "linked",
50630
51709
  wallet.revokedAt ? "yes" : "no",
51710
+ trustedCell(wallet),
50631
51711
  cells[index] ?? "-"
50632
51712
  ]))}
50633
51713
  `);
50634
51714
  const anyNone = cells.includes("none");
50635
51715
  const anyStale = cells.includes("stale");
50636
51716
  const anyTee = linkedRows.some(isTeeRow);
50637
- if (anyNone || anyStale || anyTee)
51717
+ const anyUntrusted = linkedRows.some((row) => trustedCell(row) === "no");
51718
+ if (anyNone || anyStale || anyTee || anyUntrusted)
50638
51719
  deps.stdout.write(`
50639
51720
  `);
50640
51721
  if (anyTee)
50641
51722
  deps.stdout.write(TEE_HINT);
51723
+ if (anyUntrusted)
51724
+ deps.stdout.write(UNTRUSTED_HINT);
50642
51725
  if (anyNone)
50643
51726
  deps.stdout.write(NONE_HINT);
50644
51727
  if (anyStale)
@@ -51180,10 +52263,8 @@ async function addressOwnedByVault(ctx, address) {
51180
52263
  writeUsageFailure(ctx.deps, error.message, ctx.json);
51181
52264
  return "usage";
51182
52265
  }
51183
- writeLocalFailure(ctx.deps, {
51184
- code: "VAULT_UNLOCK_FAILED",
51185
- message: error instanceof Error ? error.message : String(error)
51186
- }, ctx.json);
52266
+ const { isVaultError: isVaultError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
52267
+ writeLocalFailure(ctx.deps, isVaultError2(error) ? { code: error.code, message: error.message, ...error.suggestion ? { suggestion: error.suggestion } : {} } : { code: "VAULT_UNLOCK_FAILED", message: error instanceof Error ? error.message : String(error) }, ctx.json);
51187
52268
  return "error";
51188
52269
  }
51189
52270
  }
@@ -52834,6 +53915,150 @@ async function teeRebinds(args, ctx) {
52834
53915
  return 0;
52835
53916
  }
52836
53917
 
53918
+ // src/commands/transfer.ts
53919
+ init_args();
53920
+ init_render();
53921
+ init_trading();
53922
+ var USAGE2 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
53923
+ var TRANSFER_ASSETS = Object.keys(BASES);
53924
+ var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
53925
+ async function readLinkedWallets(ctx, key) {
53926
+ const rows = [];
53927
+ let cursor = null;
53928
+ const seen = new Set;
53929
+ for (let page = 0;page < 25; page++) {
53930
+ const query = cursor === null ? "?limit=100" : `?limit=100&cursor=${encodeURIComponent(cursor)}`;
53931
+ const body = await request(ctx, key, `/api/v1/agent/wallets${query}`);
53932
+ if (!Array.isArray(body.page))
53933
+ throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
53934
+ rows.push(...body.page);
53935
+ if (body.isDone !== false || typeof body.continueCursor !== "string" || seen.has(body.continueCursor))
53936
+ return rows;
53937
+ seen.add(body.continueCursor);
53938
+ cursor = body.continueCursor;
53939
+ }
53940
+ return rows;
53941
+ }
53942
+ async function resolveDestination(ctx, key, source, to) {
53943
+ if (to === "vault") {
53944
+ const lifecycle = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/lifecycle`);
53945
+ const vault = lifecycle.vaultDestination;
53946
+ if (typeof vault !== "string" || vault.length === 0)
53947
+ throw new TradingError("VAULT_NOT_PINNED", "This wallet has no pinned vault recorded on Candle, so --to vault names nothing. Pass the address instead.");
53948
+ return { kind: "vault", address: vault };
53949
+ }
53950
+ const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === "solana" && row.revokedAt === undefined && row.address !== source.address);
53951
+ const matches = rows.filter((row) => row.label === to || row._id === to || row.address === to);
53952
+ if (matches.length > 1)
53953
+ throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${matches.length} linked wallets: ${matches.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
53954
+ const match = matches[0];
53955
+ if (match)
53956
+ return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
53957
+ if (to === source.address)
53958
+ throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
53959
+ if (BASE58_ADDRESS.test(to))
53960
+ return { kind: "address", address: to };
53961
+ throw new TradingError("DESTINATION_UNKNOWN", rows.length === 0 ? `"${to}" is not vault, a Solana address, or a linked wallet on this account (it has no other active Solana linked wallets).` : `"${to}" is not vault, a Solana address, or a linked wallet on this account. Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`);
53962
+ }
53963
+ function describeDestination(destination) {
53964
+ switch (destination.kind) {
53965
+ case "vault":
53966
+ return `this wallet's pinned vault ${destination.address}`;
53967
+ case "linked":
53968
+ return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
53969
+ case "address":
53970
+ return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
53971
+ }
53972
+ }
53973
+ async function confirmTransfer(ctx, lines, yes) {
53974
+ const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
53975
+ for (const line of lines)
53976
+ output.write(`${safeText(line)}
53977
+ `);
53978
+ if (yes)
53979
+ return true;
53980
+ if (!ctx.deps.isTTY.stdin)
53981
+ throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary transfer prompt.");
53982
+ return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
53983
+ }
53984
+ async function transfer(args, ctx) {
53985
+ const parsed = parseArgs(args, {
53986
+ valueFlags: ["--wallet", "--to", "--asset", "--mint", "--amount", "--rpc-url"],
53987
+ booleanFlags: ["--yes"]
53988
+ });
53989
+ if ("error" in parsed) {
53990
+ writeUsageFailure(ctx.deps, parsed.error, ctx.json);
53991
+ return 2;
53992
+ }
53993
+ const flags = parsed.values;
53994
+ const asset = flags["--asset"]?.toUpperCase();
53995
+ if (parsed.positionals.length !== 0 || !flags["--to"] || !flags["--amount"] || Boolean(flags["--asset"]) === Boolean(flags["--mint"]) || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags["--mint"] !== undefined && !BASE58_ADDRESS.test(flags["--mint"])) {
53996
+ writeUsageFailure(ctx.deps, USAGE2, ctx.json);
53997
+ return 2;
53998
+ }
53999
+ const to = flags["--to"];
54000
+ const isMax = flags["--amount"] === "max";
54001
+ try {
54002
+ if (!isMax)
54003
+ rawAmount(flags["--amount"], 18);
54004
+ const key = await tradingKey(ctx);
54005
+ const payer = await tradingPayer(ctx, key, flags["--wallet"], "transfer:write");
54006
+ if (payer.kind === "embedded")
54007
+ 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.");
54008
+ if (!payer.scopes.includes("transfer:bound"))
54009
+ throw new TradingError("SCOPE_MISSING", "Moving funds out of a TEE wallet needs a Read:Write:Transfer key. Mint one with: candle keys create --access read-write-transfer, then bind the wallet to it with: candle tee rebind");
54010
+ const wallet = payer.wallet;
54011
+ const destination = await resolveDestination(ctx, key, wallet, to);
54012
+ const label = asset ?? flags["--mint"];
54013
+ let amountRaw;
54014
+ if (isMax)
54015
+ amountRaw = "max";
54016
+ else {
54017
+ const decimals = asset ? BASES[asset]?.decimals ?? 9 : await decimalsFor(ctx, flags["--mint"], flags["--rpc-url"]);
54018
+ amountRaw = rawAmount(flags["--amount"], decimals);
54019
+ }
54020
+ const amountText = isMax ? `the full spendable balance of ${label}` : `${flags["--amount"]} ${label}`;
54021
+ const confirmed = await confirmTransfer(ctx, [
54022
+ `Transfer ${amountText} from ${wallet.address} (${wallet.id}) to ${describeDestination(destination)}`,
54023
+ destination.kind === "vault" ? "Destination: this wallet's own vault, pinned when it was imported." : destination.kind === "linked" ? "Destination: a linked wallet on this account. Candle allows it only if you linked it while signed in or marked it trusted, and the amount counts against this key's spend caps." : "Destination: an address Candle will classify at build; from a TEE wallet only its vault or a trusted linked wallet is allowed."
54024
+ ], parsed.booleans.has("--yes"));
54025
+ if (!confirmed)
54026
+ return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet.id, destination });
54027
+ const built = await request(ctx, key, "/api/v1/agent/transfer/build", {
54028
+ walletId: wallet.id,
54029
+ chain: "solana",
54030
+ ...asset ? { asset } : { mint: flags["--mint"] },
54031
+ amountRaw,
54032
+ to: destination.address
54033
+ });
54034
+ const transferId = built.transferId;
54035
+ const unsigned = built.unsignedTransactionsBase64;
54036
+ if (typeof transferId !== "string" || !Array.isArray(unsigned) || unsigned.length !== 1 || typeof unsigned[0] !== "string")
54037
+ throw new TradingError("INVALID_RESPONSE", "Candle did not return one unsigned transfer transaction; nothing was signed.");
54038
+ if (typeof built.payerAddress === "string" && built.payerAddress !== wallet.address)
54039
+ throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the requested payer; nothing was signed.");
54040
+ const builtAmount = typeof built.amountRaw === "string" ? built.amountRaw : amountRaw;
54041
+ ctx.deps.stderr.write(`Built transfer ${transferId}: ${asset ? `${decimalAmount(builtAmount, BASES[asset]?.decimals ?? 0)} ${asset}` : `${builtAmount} raw units of ${label}`}${typeof built.destinationKind === "string" ? ` to ${built.destinationKind === "vault" ? "the vault" : "a linked wallet"}` : ""}.
54042
+ `);
54043
+ const signed = await relaySign(ctx, key, wallet, unsigned[0]);
54044
+ const result = await request(ctx, key, "/api/v1/agent/transfer/submit", {
54045
+ transferId,
54046
+ signedTransactionsBase64: [signed]
54047
+ });
54048
+ const receipt = {
54049
+ ...result,
54050
+ transferId,
54051
+ walletId: wallet.id,
54052
+ wallet: safeText(wallet.address),
54053
+ destination,
54054
+ ...typeof built.destinationKind === "string" ? { destinationKind: built.destinationKind } : {}
54055
+ };
54056
+ return printTradingResult(ctx, receipt);
54057
+ } catch (error) {
54058
+ return tradingFailure(ctx, error);
54059
+ }
54060
+ }
54061
+
52837
54062
  // src/commands/update.ts
52838
54063
  init_args();
52839
54064
  import { randomBytes as randomBytes3 } from "node:crypto";
@@ -53563,12 +54788,32 @@ async function verifyEntry(copy, entry, root, report, observer) {
53563
54788
  if (addressFromSecret64(secret) !== entry.address) {
53564
54789
  fail2(5, "its stored secret does not produce the address the index records", entry.id);
53565
54790
  }
54791
+ } else if (!sameEvmAddress(evmAddressFromSecret(secret), entry.address)) {
54792
+ fail2(5, "its stored secret does not produce the address the index records", entry.id);
53566
54793
  }
53567
54794
  report.addressChecked.push(entry.id);
53568
54795
  if (entry.origin !== "derived" || entry.derivation === undefined) {
53569
54796
  report.notRederived.push(entry.id);
53570
54797
  return;
53571
54798
  }
54799
+ if (entry.derivation.scheme === "bip32-secp256k1") {
54800
+ const index = evmIndexOfPath(entry.derivation.path);
54801
+ if (index === undefined) {
54802
+ fail2(5, `its recorded path ${entry.derivation.path} is not on the EVM branch`, entry.id);
54803
+ }
54804
+ const derivedEvm = await deriveEvmKeyFromRoot(root, index);
54805
+ observer?.onLeafLive?.(2);
54806
+ try {
54807
+ if (!bytesEqual(derivedEvm.secret, secret)) {
54808
+ fail2(5, `it does not re-derive from the root along its recorded path ${entry.derivation.path}`, entry.id);
54809
+ }
54810
+ report.rederived.push(entry.id);
54811
+ } finally {
54812
+ wipe(derivedEvm.secret);
54813
+ observer?.onLeafLive?.(1);
54814
+ }
54815
+ return;
54816
+ }
53572
54817
  if (entry.derivation.scheme !== "slip10-ed25519") {
53573
54818
  report.notRederived.push(entry.id);
53574
54819
  return;
@@ -54104,6 +55349,8 @@ async function vaultDemote(args, ctx) {
54104
55349
  code: 1
54105
55350
  }));
54106
55351
  if (!resolved.ok) {
55352
+ if (resolved.reported)
55353
+ return resolved.code;
54107
55354
  writeLocalFailure(ctx.deps, {
54108
55355
  code: "TEE_WALLET_UNKNOWN",
54109
55356
  message: `${address} is not a TEE wallet in the vault.`,
@@ -54406,10 +55653,15 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
54406
55653
  const opened = await unlockInteractively(ctx, path, raw, {
54407
55654
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
54408
55655
  promptText: "Current vault passphrase, to unlock (input hidden): ",
54409
- factor: "passphrase",
54410
- passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
55656
+ among: factorAddAmong(parseVaultFile(raw).envelopes)
54411
55657
  });
54412
55658
  const vault = hold(opened.vault);
55659
+ const openedWith = {
55660
+ factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
55661
+ envelopeId: opened.factor.envelopeId
55662
+ };
55663
+ if (opened.factor.kind === "security-key")
55664
+ opened.factor.session.pin = undefined;
54413
55665
  const vaultId = vault.file.vaultId;
54414
55666
  let registered;
54415
55667
  try {
@@ -54507,7 +55759,8 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
54507
55759
  saltDerivation: "platform",
54508
55760
  helper: envelope.helper,
54509
55761
  recoverableFactors: recoverable,
54510
- verified: true
55762
+ verified: true,
55763
+ openedWith
54511
55764
  });
54512
55765
  return 0;
54513
55766
  }
@@ -54530,6 +55783,7 @@ ${PASSKEY_NOTE}
54530
55783
  }
54531
55784
 
54532
55785
  // src/commands/vault-factor-security-key.ts
55786
+ init_esm();
54533
55787
  init_crypto();
54534
55788
  init_errors();
54535
55789
  init_fido2();
@@ -54653,20 +55907,84 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
54653
55907
  const raw = await requireVaultRaw(ctx, resolvedVault);
54654
55908
  const file = parseVaultFile(raw);
54655
55909
  const envelopeId = freshEnvelopeId();
54656
- const session = await openSecurityKeySession(deps, {
54657
- vaultId: file.vaultId,
54658
- envelopeId,
54659
- deviceFlag: ctx.vaultDevice,
54660
- requireFeatures: true
54661
- });
55910
+ const vaultId = file.vaultId;
55911
+ const keysInHeader = file.envelopes.filter(isCtap2Envelope);
55912
+ const { helperPath, info } = await listSecurityKeys(deps, { vaultId, envelopeId });
55913
+ assertHelperExcludeList(info, helperPath, keysInHeader.length);
55914
+ const attached = keysInHeader.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: keysInHeader }) : undefined;
55915
+ const excludeCountNow = keysInHeader.length;
55916
+ const addingLine = (device) => deps.stderr.write(`Adding: ${device.product || "security key"} (--device ${device.deviceId}), a different key from the one that opened the vault.
55917
+ `);
55918
+ let session;
55919
+ let enrolling;
55920
+ let deferredAfter;
54662
55921
  const opened = await unlockInteractively(ctx, path, raw, {
54663
55922
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
54664
55923
  promptText: "Current vault passphrase, to unlock (input hidden): ",
54665
- factor: "passphrase",
54666
- passphraseOnlyBecause: "this command's security key session is for the key being added, so the vault opens with the passphrase here."
55924
+ among: factorAddAmong(file.envelopes),
55925
+ attached,
55926
+ onFactorChosen: async (chosen) => {
55927
+ if (chosen.kind === "passphrase") {
55928
+ session = await openSecurityKeySession(deps, {
55929
+ vaultId,
55930
+ envelopeId,
55931
+ deviceFlag: ctx.vaultDevice,
55932
+ requireFeatures: true,
55933
+ beforePin: (device) => {
55934
+ refuseIfEnrolled(device, attached);
55935
+ assertExcludeListFits(device, excludeCountNow);
55936
+ }
55937
+ });
55938
+ return;
55939
+ }
55940
+ if (chosen.kind !== "security-key")
55941
+ return;
55942
+ const plan = planDevices(ctx, chosen.envelope, info.devices, attached);
55943
+ if (plan.enrolling !== undefined) {
55944
+ refuseIfEnrolled(plan.enrolling, attached);
55945
+ assertDeviceServesFactor(plan.enrolling);
55946
+ assertExcludeListFits(plan.enrolling, excludeCountNow);
55947
+ enrolling = plan.enrolling;
55948
+ addingLine(plan.enrolling);
55949
+ } else {
55950
+ deferredAfter = plan.opening;
55951
+ }
55952
+ return { preferDevice: plan.opening.deviceId };
55953
+ }
54667
55954
  });
54668
55955
  const vault = hold(opened.vault);
54669
- const registered = await registerCredential(deps, session, { vaultId: vault.file.vaultId, envelopeId });
55956
+ const openedWith = {
55957
+ factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
55958
+ envelopeId: opened.factor.envelopeId
55959
+ };
55960
+ if (opened.factor.kind === "security-key")
55961
+ opened.factor.session.pin = undefined;
55962
+ const enrolledKeys = vault.file.envelopes.filter(isCtap2Envelope);
55963
+ const excludeCredentialIds = enrolledKeys.map((envelope2) => base64.encode(unb64u(envelope2.credentialId, "credentialId")));
55964
+ if (session === undefined) {
55965
+ if (deferredAfter !== undefined) {
55966
+ await deps.promptLine(`The vault is open. Unplug ${deferredAfter.product || "the security key that opened it"}, insert the security key to add, then press Enter.`);
55967
+ const again = await listSecurityKeys(deps, { vaultId, envelopeId });
55968
+ const probeAgain = enrolledKeys.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: enrolledKeys }) : undefined;
55969
+ const inserted = chooseAfterInsert(ctx, again.info.devices, probeAgain);
55970
+ refuseIfEnrolled(inserted, probeAgain);
55971
+ assertDeviceServesFactor(inserted);
55972
+ assertExcludeListFits(inserted, excludeCredentialIds.length);
55973
+ addingLine(inserted);
55974
+ enrolling = inserted;
55975
+ }
55976
+ if (enrolling === undefined)
55977
+ throw new Error("factor add security-key: no enrolling device was decided");
55978
+ const target = enrolling;
55979
+ session = await openSecurityKeySession(deps, {
55980
+ vaultId,
55981
+ envelopeId,
55982
+ deviceFlag: target.deviceId,
55983
+ requireFeatures: true,
55984
+ pinPrompt: (device) => `PIN for the key being added, ${device.product || "the security key"} (input hidden): `
55985
+ });
55986
+ }
55987
+ const registered = await registerCredential(deps, session, { vaultId, envelopeId, excludeCredentialIds });
54670
55988
  if (registered.backupEligible) {
54671
55989
  throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${session.device.product || "This authenticator"} reports the credential it created as backup-eligible (synced), so it is not a hardware-bound security key credential and cannot be recorded as a hardware-token factor.`, {
54672
55990
  suggestion: "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Use a hardware security key for this factor."
@@ -54723,7 +56041,8 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
54723
56041
  backupState: envelope.backupState,
54724
56042
  userVerification: "required",
54725
56043
  recoverableFactors: recoverable,
54726
- verified: true
56044
+ verified: true,
56045
+ openedWith
54727
56046
  });
54728
56047
  return 0;
54729
56048
  }
@@ -54742,6 +56061,76 @@ ${SECURITY_KEY_PAIR_NOTE}
54742
56061
  `);
54743
56062
  return 0;
54744
56063
  }
56064
+ function refuseIfEnrolled(device, attached) {
56065
+ const entry = attached?.devices?.find((candidate) => candidate.deviceId === device.deviceId);
56066
+ if (entry === undefined || attached?.credentials === undefined)
56067
+ return;
56068
+ for (const [envelopeId, credentialId] of attached.credentials) {
56069
+ if (entry.present.includes(credentialId))
56070
+ throw alreadyEnrolled(device, envelopeId);
56071
+ }
56072
+ }
56073
+ function planDevices(ctx, opener, devices, attached) {
56074
+ const platform = ctx.deps.platform;
56075
+ if (devices.length === 0) {
56076
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key is attached.", {
56077
+ suggestion: "Plug the key in and run the command again. No other factor is substituted."
56078
+ });
56079
+ }
56080
+ const named = ctx.vaultDevice;
56081
+ const openerCredential = attached?.credentials?.get(opener.id);
56082
+ const holders = openerCredential === undefined ? [] : (attached?.devices ?? []).filter((device) => device.present.includes(openerCredential));
56083
+ let opening;
56084
+ if (holders.length === 1) {
56085
+ opening = devices.find((device) => device.deviceId === holders[0].deviceId);
56086
+ }
56087
+ if (opening === undefined) {
56088
+ const candidates = devices.filter((device) => device.deviceId !== named);
56089
+ if (candidates.length === 1)
56090
+ opening = candidates[0];
56091
+ else if (devices.length === 1)
56092
+ opening = devices[0];
56093
+ }
56094
+ if (opening === undefined) {
56095
+ throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${devices.length} security keys are attached and the one that opens the vault could not be told apart. Leave only the key that opens the vault attached; this command asks for the key to add after the vault is open.`, { suggestion: "Nothing was sent to any key. Run again with only the key that opens the vault attached." });
56096
+ }
56097
+ if (!opening.readable)
56098
+ selectDevice([opening], undefined, platform);
56099
+ const others = devices.filter((device) => device.deviceId !== opening.deviceId);
56100
+ if (named !== undefined) {
56101
+ if (named === opening.deviceId)
56102
+ throw alreadyEnrolled(opening, opener.id);
56103
+ return { opening, enrolling: selectDevice(others, named, platform) };
56104
+ }
56105
+ if (others.length === 1)
56106
+ return { opening, enrolling: selectDevice(others, undefined, platform) };
56107
+ if (others.length > 1) {
56108
+ const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
56109
+ `);
56110
+ throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opens the vault, and none was named, so nothing was sent to any of them:
56111
+ ${listing}`, { suggestion: "Run again with --device <id> naming the key to add." });
56112
+ }
56113
+ return { opening, enrolling: undefined };
56114
+ }
56115
+ function chooseAfterInsert(ctx, devices, probe) {
56116
+ const platform = ctx.deps.platform;
56117
+ if (devices.length === 0) {
56118
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key other than the one that opened the vault is attached. Nothing was written.", { suggestion: "Run the command again with the key to add inserted when asked." });
56119
+ }
56120
+ if (devices.length === 1)
56121
+ return selectDevice(devices, undefined, platform);
56122
+ const holdsVault = (device) => (probe?.devices?.find((entry) => entry.deviceId === device.deviceId)?.present.length ?? 0) > 0;
56123
+ const others = devices.filter((device) => !holdsVault(device));
56124
+ if (others.length === 1)
56125
+ return selectDevice(others, undefined, platform);
56126
+ if (others.length === 0) {
56127
+ throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "Every attached security key already holds one of this vault's credentials, so there is no key to add. Nothing was written.", { suggestion: "Run the command again with a security key this vault does not have inserted when asked." });
56128
+ }
56129
+ const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
56130
+ `);
56131
+ throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opened the vault, so nothing was sent to any of them:
56132
+ ${listing}`, { suggestion: "Run the command again with only the key to add inserted when asked. Nothing was written." });
56133
+ }
54745
56134
 
54746
56135
  // src/commands/vault-factor-touch-id.ts
54747
56136
  init_enclave();
@@ -54780,10 +56169,15 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
54780
56169
  const opened = await unlockInteractively(ctx, path, raw, {
54781
56170
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
54782
56171
  promptText: "Current vault passphrase, to unlock (input hidden): ",
54783
- factor: "passphrase",
54784
- passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
56172
+ among: factorAddAmong(parseVaultFile(raw).envelopes)
54785
56173
  });
54786
56174
  const vault = hold(opened.vault);
56175
+ const openedWith = {
56176
+ factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
56177
+ envelopeId: opened.factor.envelopeId
56178
+ };
56179
+ if (opened.factor.kind === "security-key")
56180
+ opened.factor.session.pin = undefined;
54787
56181
  const vaultId = vault.file.vaultId;
54788
56182
  const keyTag = keyTagFor(vaultId, envelopeId);
54789
56183
  let created;
@@ -54867,7 +56261,8 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
54867
56261
  helper: { teamId: session.identity.teamId, bundleId: session.identity.bundleId, minVersion: session.version },
54868
56262
  accessControl: "biometryCurrentSet",
54869
56263
  recoverableFactors: recoverable,
54870
- verified: true
56264
+ verified: true,
56265
+ openedWith
54871
56266
  });
54872
56267
  return 0;
54873
56268
  }
@@ -55556,6 +56951,7 @@ async function reconcileFundingReceipts(vault, addresses, rpcUrl2, ctx) {
55556
56951
  }
55557
56952
 
55558
56953
  // src/commands/vault-fund.ts
56954
+ init_promote_support();
55559
56955
  init_store();
55560
56956
 
55561
56957
  // src/vault/vault-transfer-sign.ts
@@ -55773,10 +57169,11 @@ async function signAndBroadcastTransfer(input) {
55773
57169
  // src/commands/vault-fund.ts
55774
57170
  init_vault_support();
55775
57171
  async function fundExternal(ctx, opened, vault, external2, input) {
55776
- const vaultKeys = vault.index.entries.filter((entry) => entry.role === "vault");
57172
+ const vaultKeys = vault.index.entries.filter((entry) => entry.role === "vault" && entry.chain === "solana");
55777
57173
  const fromFlag = input.parsed.values["--from"];
55778
57174
  let fromEntry;
55779
57175
  if (fromFlag !== undefined) {
57176
+ assertNotEvmEntry(vault.index, fromFlag, "vault fund");
55780
57177
  fromEntry = vaultKeys.find((entry) => entry.label === fromFlag) ?? vaultKeys.find((entry) => entry.address === fromFlag);
55781
57178
  if (fromEntry === undefined)
55782
57179
  return usage(ctx, `No vault key matches --from ${fromFlag}.`);
@@ -55866,6 +57263,9 @@ async function vaultFund(args, ctx) {
55866
57263
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
55867
57264
  });
55868
57265
  const vault = hold(opened.vault);
57266
+ assertNotEvmEntry(vault.index, teeAddress, "vault fund");
57267
+ if (parsed.values["--from"] !== undefined)
57268
+ assertNotEvmEntry(vault.index, parsed.values["--from"], "vault fund");
55869
57269
  const teeEntry = vault.index.entries.find((entry) => entry.address === teeAddress && entry.role === "tee-wallet");
55870
57270
  if (teeEntry === undefined) {
55871
57271
  const external2 = findExternalEntry(vault.index, teeAddress);
@@ -55897,7 +57297,7 @@ async function vaultFund(args, ctx) {
55897
57297
  suggestion: "Nothing was signed. Name the source key with --from <label>, or pin one: candle tee enable <address> --vault <address>"
55898
57298
  });
55899
57299
  }
55900
- const fromEntry = vault.index.entries.find((entry) => entry.address === destination && entry.role === "vault");
57300
+ const fromEntry = vault.index.entries.find((entry) => entry.address === destination && entry.role === "vault" && entry.chain === "solana");
55901
57301
  if (fromEntry === undefined) {
55902
57302
  throw new VaultError("GRANT_DESTINATION_UNRESOLVED", `Pinned destination ${destination} is not a vault key in this vault.`, {
55903
57303
  suggestion: "Nothing was signed. Name the source key with --from <label>; candle vault status lists this vault's keys."
@@ -56354,9 +57754,48 @@ async function readLamports(addresses, rpcUrl2, fetchFn) {
56354
57754
  }
56355
57755
  return { lamports, unavailable, ...failure === undefined ? {} : { failure } };
56356
57756
  }
57757
+ async function readEvmBalances(addresses, rpcUrl2, fetchFn) {
57758
+ const rpc2 = createEvmRpc(rpcUrl2, fetchFn);
57759
+ const read = {
57760
+ host: rpcHostOf(rpcUrl2),
57761
+ chainId: undefined,
57762
+ requests: 0,
57763
+ wei: new Map,
57764
+ usdg: new Map,
57765
+ unavailable: []
57766
+ };
57767
+ try {
57768
+ read.requests += 1;
57769
+ read.chainId = await rpc2.chainId();
57770
+ } catch (error) {
57771
+ read.unavailable.push(...addresses);
57772
+ read.failure = error instanceof Error ? error.message : String(error);
57773
+ return read;
57774
+ }
57775
+ const hood = read.chainId === BigInt(HOOD_CHAIN_ID);
57776
+ for (const address of addresses) {
57777
+ try {
57778
+ read.requests += 1;
57779
+ read.wei.set(address, await rpc2.getBalance(address));
57780
+ if (hood) {
57781
+ read.requests += 1;
57782
+ read.usdg.set(address, await rpc2.erc20BalanceOf(HOOD_USDG_ADDRESS, address));
57783
+ }
57784
+ } catch (error) {
57785
+ read.wei.delete(address);
57786
+ read.usdg.delete(address);
57787
+ read.unavailable.push(address);
57788
+ read.failure ??= error instanceof Error ? error.message : String(error);
57789
+ }
57790
+ }
57791
+ return read;
57792
+ }
57793
+ function evmRequestsPlanned(count) {
57794
+ return `${1 + count} to ${1 + 2 * count}`;
57795
+ }
56357
57796
  async function vaultList(args, ctx) {
56358
57797
  const parsed = parseArgs(args, {
56359
- valueFlags: ["--keystore", "--rpc-url"],
57798
+ valueFlags: ["--keystore", "--rpc-url", "--evm-rpc-url"],
56360
57799
  booleanFlags: ["--balances", "--accept-older-copy"],
56361
57800
  pathFlags: ["--keystore"]
56362
57801
  });
@@ -56372,6 +57811,9 @@ async function vaultList(args, ctx) {
56372
57811
  if (!balances && parsed.values["--rpc-url"] !== undefined) {
56373
57812
  return usage(ctx, "--rpc-url has no effect without --balances; vault list is offline by default.");
56374
57813
  }
57814
+ if (!balances && parsed.values["--evm-rpc-url"] !== undefined) {
57815
+ return usage(ctx, "--evm-rpc-url has no effect without --balances; vault list is offline by default.");
57816
+ }
56375
57817
  const resolvedVault = vaultPathFor(ctx, parsed);
56376
57818
  if ("error" in resolvedVault)
56377
57819
  return usage(ctx, resolvedVault.error);
@@ -56383,6 +57825,10 @@ async function vaultList(args, ctx) {
56383
57825
  return usage(ctx, resolved.error);
56384
57826
  rpcUrl2 = resolved;
56385
57827
  }
57828
+ const evmRpc = resolveEvmRpcUrl(parsed.values["--evm-rpc-url"], deps.env[EVM_RPC_URL_ENV], "--evm-rpc-url");
57829
+ if (balances && "error" in evmRpc)
57830
+ return usage(ctx, evmRpc.error);
57831
+ const evmRpcUrl = "url" in evmRpc ? evmRpc.url : DEFAULT_HOOD_RPC_URL;
56386
57832
  if (!requirePromptStreams(ctx, "vault list"))
56387
57833
  return 1;
56388
57834
  return runVaultCommand(ctx, async ({ hold }) => {
@@ -56408,12 +57854,26 @@ async function vaultList(args, ctx) {
56408
57854
  unavailable = outcome.unavailable;
56409
57855
  failure = outcome.failure;
56410
57856
  }
56411
- const complete = unavailable.length === 0;
57857
+ const solanaComplete = unavailable.length === 0;
56412
57858
  const totalLamports = [...lamports.values()].reduce((sum, value) => sum + value, 0n);
56413
- if (!complete) {
57859
+ if (!solanaComplete) {
56414
57860
  deps.stderr.write(`${unavailable.length} ${unavailable.length === 1 ? "address" : "addresses"} could not be read: ${failure ?? "the RPC did not answer"}. Narrow with a filter, or use your own endpoint with --rpc-url.
56415
57861
  `);
56416
57862
  }
57863
+ const evm = balances ? matched.filter((entry) => entry.chain === "evm") : [];
57864
+ let evmRead;
57865
+ if (evm.length > 0) {
57866
+ const host = rpcHostOf(evmRpcUrl);
57867
+ deps.stderr.write(`Reading ETH (and USDG when the chain is Hood) for ${evm.length} EVM ${evm.length === 1 ? "address" : "addresses"} from ${host}, in ${evmRequestsPlanned(evm.length)} requests. That endpoint sees all ${evm.length} together.
57868
+ `);
57869
+ evmRead = await readEvmBalances(evm.map((entry) => entry.address), evmRpcUrl, deps.fetch);
57870
+ if (evmRead.unavailable.length > 0) {
57871
+ deps.stderr.write(`${evmRead.unavailable.length} EVM ${evmRead.unavailable.length === 1 ? "address" : "addresses"} could not be read: ${evmRead.failure ?? "the RPC did not answer"}. Narrow with a filter, or use your own endpoint with --evm-rpc-url.
57872
+ `);
57873
+ }
57874
+ }
57875
+ const hood = evmRead?.chainId === BigInt(HOOD_CHAIN_ID);
57876
+ const complete = solanaComplete && (evmRead === undefined || evmRead.unavailable.length === 0);
56417
57877
  if (ctx.json) {
56418
57878
  writeJson(deps, {
56419
57879
  ok: true,
@@ -56425,17 +57885,30 @@ async function vaultList(args, ctx) {
56425
57885
  const held = lamports.get(entry.address);
56426
57886
  return {
56427
57887
  ...describeEntry(entry),
56428
- ...balances && entry.chain === "solana" ? { lamports: held === undefined ? null : held.toString() } : {}
57888
+ ...balances && entry.chain === "solana" ? { lamports: held === undefined ? null : held.toString() } : {},
57889
+ ...balances && entry.chain === "evm" ? {
57890
+ wei: evmRead?.wei.get(entry.address)?.toString() ?? null,
57891
+ ...hood ? { usdgRaw: evmRead?.usdg.get(entry.address)?.toString() ?? null } : {}
57892
+ } : {}
56429
57893
  };
56430
57894
  }),
56431
57895
  ...balances && rpcHost !== undefined ? {
56432
57896
  balances: {
56433
57897
  rpcHost,
56434
57898
  requests,
56435
- complete,
57899
+ complete: solanaComplete,
56436
57900
  totalLamports: totalLamports.toString(),
56437
57901
  unavailable
56438
57902
  }
57903
+ } : {},
57904
+ ...evmRead !== undefined ? {
57905
+ evmBalances: {
57906
+ rpcHost: evmRead.host,
57907
+ chainId: evmRead.chainId === undefined ? null : Number(evmRead.chainId),
57908
+ requests: evmRead.requests,
57909
+ complete: evmRead.unavailable.length === 0,
57910
+ unavailable: evmRead.unavailable
57911
+ }
56439
57912
  } : {}
56440
57913
  });
56441
57914
  return complete ? 0 : 3;
@@ -56451,24 +57924,50 @@ async function vaultList(args, ctx) {
56451
57924
  const headers = ["ADDRESS", "LABEL", "ROLE", "DERIVATION"];
56452
57925
  if (balances)
56453
57926
  headers.push("SOL");
57927
+ const evmColumns = evmRead !== undefined;
57928
+ const nativeHeader = hood || evmRead?.chainId === undefined ? "ETH" : `ETH@${evmRead.chainId}`;
57929
+ if (evmColumns)
57930
+ headers.push(nativeHeader);
57931
+ if (evmColumns && hood)
57932
+ headers.push("USDG");
56454
57933
  const rows = matched.map((entry) => {
56455
57934
  const row = [entry.address, entry.label || "(none)", entry.role, entry.derivation?.path ?? "-"];
56456
57935
  if (balances) {
56457
57936
  const held = lamports.get(entry.address);
56458
57937
  row.push(entry.chain !== "solana" ? "-" : held === undefined ? "?" : formatSol3(held));
56459
57938
  }
57939
+ if (evmColumns) {
57940
+ const wei = evmRead?.wei.get(entry.address);
57941
+ row.push(entry.chain !== "evm" ? "-" : wei === undefined ? "?" : formatUnits(wei, NATIVE_DECIMALS));
57942
+ if (hood) {
57943
+ const usdg = evmRead?.usdg.get(entry.address);
57944
+ row.push(entry.chain !== "evm" ? "-" : usdg === undefined ? "?" : formatUnits(usdg, HOOD_USDG_DECIMALS));
57945
+ }
57946
+ }
56460
57947
  return row;
56461
57948
  });
56462
57949
  deps.stdout.write(`
56463
57950
  ${renderTable(headers, rows)}
56464
57951
  `);
56465
57952
  if (balances) {
56466
- deps.stdout.write(complete ? `
57953
+ deps.stdout.write(solanaComplete ? `
56467
57954
  total ${formatSol3(totalLamports)} SOL across ${solana.length} keys
56468
57955
  ` : `
56469
57956
  total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.length} of ${solana.length} keys read
56470
57957
  `);
56471
57958
  }
57959
+ if (evmRead !== undefined) {
57960
+ const totalWei = [...evmRead.wei.values()].reduce((sum, value) => sum + value, 0n);
57961
+ const readCount = evm.length - evmRead.unavailable.length;
57962
+ const chain2 = evmRead.chainId === undefined ? "an unread chain" : hood ? "Hood" : `chain id ${evmRead.chainId}`;
57963
+ deps.stdout.write(`total ${formatUnits(totalWei, NATIVE_DECIMALS)} ${nativeHeader} across ${readCount}${readCount === evm.length ? "" : ` of ${evm.length}`} EVM keys${readCount === evm.length ? "" : " read"} on ${chain2}
57964
+ `);
57965
+ if (hood) {
57966
+ const totalUsdg = [...evmRead.usdg.values()].reduce((sum, value) => sum + value, 0n);
57967
+ deps.stdout.write(`total ${formatUnits(totalUsdg, HOOD_USDG_DECIMALS)} USDG across those keys
57968
+ `);
57969
+ }
57970
+ }
56472
57971
  return complete ? 0 : 3;
56473
57972
  });
56474
57973
  }
@@ -57148,6 +58647,7 @@ async function promoteInPlace(ctx, parsed, subjectLabel, toKey) {
57148
58647
  });
57149
58648
  let vault = hold(opened.vault);
57150
58649
  assertRecoverableFactorExists(vault.file.envelopes);
58650
+ assertNotEvmEntry(vault.index, subjectLabel, "vault promote");
57151
58651
  const existing = findEntryByLabelOrAddress(vault.index, subjectLabel);
57152
58652
  if (existing === undefined) {
57153
58653
  throw new VaultError("PROMOTE_NOT_VAULT_KEY", `No entry matches ${subjectLabel}.`, {
@@ -58778,8 +60278,8 @@ function resolveTarget(index, old, id) {
58778
60278
  // src/commands/vault-restore.ts
58779
60279
  init_args();
58780
60280
  init_deps();
58781
- init_solana_lite();
58782
60281
  import { rm as rm3 } from "node:fs/promises";
60282
+ init_solana_lite();
58783
60283
  init_crypto();
58784
60284
  init_errors();
58785
60285
  init_format();
@@ -58791,7 +60291,7 @@ var GAP_LIMIT = 20;
58791
60291
  var SCAN_CEILING = 500;
58792
60292
  async function vaultRestore(args, ctx) {
58793
60293
  const parsed = parseArgs(args, {
58794
- valueFlags: ["--keystore", "--count", "--tee-count", "--external-count", "--rpc-url"],
60294
+ valueFlags: ["--keystore", "--count", "--tee-count", "--external-count", "--evm-count", "--rpc-url"],
58795
60295
  booleanFlags: ["--phrase", "--own-passphrase"],
58796
60296
  pathFlags: ["--keystore"]
58797
60297
  });
@@ -58808,7 +60308,7 @@ async function vaultRestore(args, ctx) {
58808
60308
  }
58809
60309
  if (!requireTty(ctx, "vault restore"))
58810
60310
  return 1;
58811
- const counts = parseCounts(parsed.values["--count"], parsed.values["--tee-count"], parsed.values["--external-count"], parsed.values["--rpc-url"]);
60311
+ const counts = parseCounts(parsed.values["--count"], parsed.values["--tee-count"], parsed.values["--external-count"], parsed.values["--rpc-url"], parsed.values["--evm-count"]);
58812
60312
  if ("error" in counts)
58813
60313
  return usage(ctx, counts.error);
58814
60314
  const { deps } = ctx;
@@ -58908,7 +60408,7 @@ async function discardIncompleteRestore(ctx, path, sidecarExisted) {
58908
60408
  }
58909
60409
  }
58910
60410
  var RESTORED_BRANCHES = ["solanaVault", "solanaTee", "solanaExternal"];
58911
- function parseCounts(count, teeCount, externalCount, rpcUrl2) {
60411
+ function parseCounts(count, teeCount, externalCount, rpcUrl2, evmCount) {
58912
60412
  const parse = (raw, flag) => {
58913
60413
  if (raw === undefined)
58914
60414
  return;
@@ -58926,6 +60426,9 @@ function parseCounts(count, teeCount, externalCount, rpcUrl2) {
58926
60426
  const externalParsed = parse(externalCount, "--external-count");
58927
60427
  if (typeof externalParsed === "object" && externalParsed !== null)
58928
60428
  return externalParsed;
60429
+ const evmParsed = parse(evmCount, "--evm-count");
60430
+ if (typeof evmParsed === "object" && evmParsed !== null)
60431
+ return evmParsed;
58929
60432
  const allOmitted = vaultCount === undefined && teeParsed === undefined && externalParsed === undefined;
58930
60433
  const scan = rpcUrl2 !== undefined;
58931
60434
  const resolve4 = (value) => {
@@ -58940,6 +60443,7 @@ function parseCounts(count, teeCount, externalCount, rpcUrl2) {
58940
60443
  solanaVault,
58941
60444
  solanaTee,
58942
60445
  solanaExternal,
60446
+ evm: evmParsed ?? 0,
58943
60447
  requested: { solanaVault: solanaVault ?? -1, solanaTee: solanaTee ?? -1, solanaExternal: solanaExternal ?? -1 }
58944
60448
  };
58945
60449
  }
@@ -58969,17 +60473,41 @@ async function deriveWithinBounds(ctx, vault, counts, rpc2) {
58969
60473
  ctx.deps.stdout.write(`Gap scan on ${branch} stopped at index ${index - 1} after ${GAP_LIMIT} consecutive indices with no balance, no token account and no signature history.
58970
60474
  `);
58971
60475
  }
60476
+ for (let index = 0;index < counts.evm; index++) {
60477
+ set.entries.push(await deriveOneEvm(vault, root, index));
60478
+ }
58972
60479
  } finally {
58973
60480
  wipe(root);
58974
60481
  }
58975
60482
  for (const entry of set.entries) {
58976
60483
  if (entry.branch === "solanaExternal")
58977
60484
  set.externalByAddress.set(entry.address, entry);
60485
+ else if (entry.branch === "evm")
60486
+ set.byAddress.set(entry.address.toLowerCase(), entry);
58978
60487
  else
58979
60488
  set.byAddress.set(entry.address, entry);
58980
60489
  }
58981
60490
  return set;
58982
60491
  }
60492
+ async function deriveOneEvm(vault, root, index) {
60493
+ const derived = await deriveEvmKeyFromRoot(root, index);
60494
+ try {
60495
+ const keyId = freshKeyId();
60496
+ return {
60497
+ branch: "evm",
60498
+ index,
60499
+ address: derived.address,
60500
+ path: derived.path,
60501
+ keyId,
60502
+ blob: await sealKeyBlob(vault, keyId, derived.secret)
60503
+ };
60504
+ } finally {
60505
+ wipe(derived.secret);
60506
+ }
60507
+ }
60508
+ function lookupKey(address) {
60509
+ return looksLikeEvmAddress(address) ? address.toLowerCase() : address;
60510
+ }
58983
60511
  async function deriveOne(vault, root, branch, index) {
58984
60512
  const path = pathForBranch(branch, index);
58985
60513
  const derived = await deriveSolanaKey(root, path);
@@ -59037,7 +60565,7 @@ This profile acts as account ${read.account}.
59037
60565
  for (const row of read.rows) {
59038
60566
  if (typeof row.address !== "string")
59039
60567
  continue;
59040
- const entry = derived.byAddress.get(row.address);
60568
+ const entry = derived.byAddress.get(lookupKey(row.address));
59041
60569
  if (entry)
59042
60570
  matched.push({ entry, row });
59043
60571
  else if (derived.externalByAddress.has(row.address))
@@ -59052,6 +60580,20 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
59052
60580
  const matchByAddress = new Map(matches2.matched.map((match) => [match.entry.address, match.row]));
59053
60581
  const entries = derived.entries.map((entry) => {
59054
60582
  const row = matchByAddress.get(entry.address);
60583
+ if (entry.branch === "evm") {
60584
+ return {
60585
+ id: entry.keyId,
60586
+ chain: "evm",
60587
+ curve: "secp256k1",
60588
+ address: entry.address,
60589
+ label: row?.label ?? `evm-${entry.index}`,
60590
+ createdAt: now,
60591
+ role: "vault",
60592
+ origin: "derived",
60593
+ derivation: { scheme: EVM_DERIVATION_SCHEME, path: entry.path },
60594
+ exposure: { everRemoteExposed: row !== undefined, everExported: false, exposureUnknown: true }
60595
+ };
60596
+ }
59055
60597
  const external2 = entry.branch === "solanaExternal";
59056
60598
  const base = {
59057
60599
  id: entry.keyId,
@@ -59092,14 +60634,14 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
59092
60634
  solanaVault: highest.solanaVault + 1,
59093
60635
  solanaTee: highest.solanaTee + 1,
59094
60636
  solanaExternal: highest.solanaExternal + 1,
59095
- evm: 0
60637
+ evm: highest.evm + 1
59096
60638
  },
59097
60639
  rootExported: false,
59098
60640
  exposedIndexes: {
59099
60641
  solanaVault: exposed.solanaVault.sort((a, b) => a - b),
59100
60642
  solanaTee: exposed.solanaTee.sort((a, b) => a - b),
59101
60643
  solanaExternal: exposed.solanaExternal.sort((a, b) => a - b),
59102
- evm: []
60644
+ evm: exposed.evm.sort((a, b) => a - b)
59103
60645
  },
59104
60646
  discovery: {
59105
60647
  restoredAt: now,
@@ -59162,6 +60704,13 @@ Every one of them is recorded with an unknown history and stays that way: this p
59162
60704
  `);
59163
60705
  }
59164
60706
  }
60707
+ if (counts.evm === 0) {
60708
+ deps.stdout.write(` evm: none. If this root has EVM keys, re-run with --evm-count <n>; the EVM branch is never gap-scanned.
60709
+ `);
60710
+ } else {
60711
+ deps.stdout.write(` evm: indices 0 to ${counts.evm - 1}, on m/44'/60'/n'/0/0.
60712
+ `);
60713
+ }
59165
60714
  if ((matches2.externalListed?.length ?? 0) > 0) {
59166
60715
  deps.stdout.write(`
59167
60716
  ${matches2.externalListed?.length} address(es) this account imported are external-branch keys of this root. An external key is never registered with Candle, so this read does not flag them; their history is unknown like every other recovered key's.
@@ -59238,8 +60787,8 @@ async function vaultReconcileExposure(args, ctx) {
59238
60787
  if (recorded !== undefined && recorded !== "" && recorded !== read.account) {
59239
60788
  throw new VaultError("EXPOSURE_ACCOUNT_MISMATCH", `This vault recorded its discovery against account ${recorded}, and this profile acts as ${read.account}.`, { suggestion: "Switch profile and run it again. Nothing was flagged." });
59240
60789
  }
59241
- const listed = new Set(read.rows.map((row) => row.address));
59242
- const entries = vault.index.entries.map((entry) => entry.role !== "external" && listed.has(entry.address) && !entry.exposure.everRemoteExposed ? { ...entry, exposure: { ...entry.exposure, everRemoteExposed: true } } : entry);
60790
+ const listed = new Set(read.rows.map((row) => lookupKey(row.address)));
60791
+ const entries = vault.index.entries.map((entry) => entry.role !== "external" && listed.has(lookupKey(entry.address)) && !entry.exposure.everRemoteExposed ? { ...entry, exposure: { ...entry.exposure, everRemoteExposed: true } } : entry);
59243
60792
  const added = entries.filter((entry, i) => entry !== vault.index.entries[i]);
59244
60793
  const exposedIndexes = { ...vault.index.hd.exposedIndexes };
59245
60794
  for (const entry of entries) {
@@ -59706,6 +61255,375 @@ function describeEnvelope(envelope, facts) {
59706
61255
  // src/commands/vault-transfer.ts
59707
61256
  init_args();
59708
61257
  init_deps();
61258
+ init_errors();
61259
+
61260
+ // src/vault/evm-transfer.ts
61261
+ init_errors();
61262
+ var EVM_RECEIPT_WAIT_MS = 120000;
61263
+ var EVM_RECEIPT_POLL_MS = 2000;
61264
+ function refuse2(code, message, opts = {}) {
61265
+ return new VaultError(code, message, { suggestion: opts.suggestion ?? "Nothing was signed.", ...opts });
61266
+ }
61267
+ function checkEvmDestination(to, from) {
61268
+ if (!to.startsWith("0x")) {
61269
+ throw refuse2("TRANSFER_CHAIN_MISMATCH", `${to} is not an EVM address, and ${from.label || from.address} is an EVM key.`, { suggestion: "Nothing was signed. An EVM key sends to a 0x address; a Solana key sends to a Solana address." });
61270
+ }
61271
+ const checked = checkEvmAddress(to);
61272
+ if (!checked.ok) {
61273
+ throw refuse2("EVM_DESTINATION_INVALID", `${to} is not a valid EVM address: ${checked.reason}.`);
61274
+ }
61275
+ if (sameEvmAddress(checked.address, from.address)) {
61276
+ throw refuse2("EVM_SELF_TRANSFER", `${to} is ${from.label || from.address}'s own address.`);
61277
+ }
61278
+ return checked.address;
61279
+ }
61280
+ function assertSolanaDestination(to, from) {
61281
+ if (!looksLikeEvmAddress(to))
61282
+ return;
61283
+ throw refuse2("TRANSFER_CHAIN_MISMATCH", `${to} is an EVM address, and ${from.label || from.address} is a Solana key.`, { suggestion: "Nothing was signed. A Solana key sends to a Solana address; an EVM key sends to a 0x address." });
61284
+ }
61285
+ async function resolveEvmAsset(rpc2, asset, chainId) {
61286
+ const upper = asset.toUpperCase();
61287
+ if (upper === "ETH")
61288
+ return { kind: "native", symbol: "ETH", decimals: NATIVE_DECIMALS };
61289
+ let token;
61290
+ if (upper === "USDG") {
61291
+ if (chainId !== BigInt(HOOD_CHAIN_ID)) {
61292
+ return {
61293
+ usage: `USDG is named on Hood (chain id ${HOOD_CHAIN_ID}) only; this RPC answered chain id ${chainId}. Name the token by its contract address.`
61294
+ };
61295
+ }
61296
+ token = toChecksumAddress(HOOD_USDG_ADDRESS);
61297
+ } else {
61298
+ const checked = checkEvmAddress(asset);
61299
+ if (!checked.ok)
61300
+ return {
61301
+ usage: `--asset must be ETH, USDG (on Hood), or an ERC-20 contract address: ${asset} is ${checked.reason}.`
61302
+ };
61303
+ token = checked.address;
61304
+ }
61305
+ let decimals;
61306
+ try {
61307
+ decimals = await rpc2.erc20Decimals(token);
61308
+ } catch (error) {
61309
+ throw refuse2("EVM_TOKEN_UNREADABLE", `${token} did not answer decimals(): ${error instanceof Error ? error.message : String(error)}`, { suggestion: "Nothing was signed. Check the contract address, and that this RPC serves the chain it lives on." });
61310
+ }
61311
+ const symbol = upper === "USDG" ? "USDG" : await rpc2.erc20Symbol(token) ?? `${token.slice(0, 10)}…`;
61312
+ return { kind: "erc20", symbol, decimals, token };
61313
+ }
61314
+ function nativeName(hood, chainId) {
61315
+ return hood ? "ETH" : `ETH on chain ${chainId}`;
61316
+ }
61317
+ function evmDisplayLines(plan) {
61318
+ const { tx, asset, hood, chainId } = plan;
61319
+ const native = nativeName(hood, chainId);
61320
+ const lines = [
61321
+ `chain ${chainId}${hood ? " (Hood)" : ""}`,
61322
+ `from ${plan.from.label} ${plan.from.address}`
61323
+ ];
61324
+ if (asset.kind === "native") {
61325
+ lines.push(`to ${tx.to}`);
61326
+ lines.push(`amount ${plan.amount} ${native} = ${plan.amountRaw} wei`);
61327
+ } else {
61328
+ lines.push(`to ${tx.to} (the ${asset.symbol} contract, ${asset.decimals} dp)`);
61329
+ lines.push(`recipient ${plan.recipient} (decoded from transfer(address,uint256))`);
61330
+ lines.push(`amount ${plan.amount} ${asset.symbol} = ${plan.amountRaw} raw`);
61331
+ }
61332
+ lines.push(`gas limit ${tx.gas}`);
61333
+ lines.push(`max fee ${tx.maxFeePerGas} wei/gas (priority ${tx.maxPriorityFeePerGas} wei/gas)`);
61334
+ lines.push(`fee cap ${formatUnits(plan.feeCap, NATIVE_DECIMALS)} ${native} (gas × max fee)`);
61335
+ lines.push(`nonce ${tx.nonce}`);
61336
+ return lines;
61337
+ }
61338
+ async function planEvmTransfer(rpc2, input) {
61339
+ const to = checkEvmDestination(input.to, input.from);
61340
+ const chainId = await rpc2.chainId();
61341
+ if (input.builtIn && chainId !== BigInt(HOOD_CHAIN_ID)) {
61342
+ throw refuse2("EVM_CHAIN_MISMATCH", `The built-in Hood RPC answered chain id ${chainId}, not ${HOOD_CHAIN_ID}.`, {
61343
+ suggestion: "Nothing was signed. Pass --rpc-url for another chain; the built-in endpoint is Hood's only."
61344
+ });
61345
+ }
61346
+ const hood = chainId === BigInt(HOOD_CHAIN_ID);
61347
+ const asset = await resolveEvmAsset(rpc2, input.asset, chainId);
61348
+ if ("usage" in asset)
61349
+ return asset;
61350
+ const max = input.amount.toLowerCase() === "max";
61351
+ if (asset.kind === "erc20" && asset.token !== undefined && sameEvmAddress(to, asset.token)) {
61352
+ throw refuse2("EVM_RECIPIENT_IS_TOKEN", `${to} is the ${asset.symbol} contract itself; sending it its own tokens is a loss.`);
61353
+ }
61354
+ let amountRaw = 0n;
61355
+ if (!max) {
61356
+ const parsed = parseUnits(input.amount, asset.decimals);
61357
+ if (!parsed.ok && parsed.reason === "precision") {
61358
+ throw refuse2("EVM_AMOUNT_PRECISION", `${input.amount} has more decimal places than ${asset.symbol}'s ${asset.decimals}.`);
61359
+ }
61360
+ if (!parsed.ok)
61361
+ return { usage: `--amount must be a positive decimal or max; ${input.amount} is neither.` };
61362
+ amountRaw = parsed.raw;
61363
+ }
61364
+ const nonce = await rpc2.getTransactionCount(input.from.address, "pending");
61365
+ const fees = await quoteFees(rpc2);
61366
+ const nativeBalance = await rpc2.getBalance(input.from.address);
61367
+ if (asset.kind === "erc20" && asset.token !== undefined) {
61368
+ if (max) {
61369
+ amountRaw = await rpc2.erc20BalanceOf(asset.token, input.from.address);
61370
+ if (amountRaw === 0n) {
61371
+ throw new VaultError("VAULT_INDEX_INVALID", `${input.from.label} holds no ${asset.symbol}; there is nothing to send.`);
61372
+ }
61373
+ }
61374
+ const draft2 = buildErc20Transfer({
61375
+ chainId,
61376
+ nonce,
61377
+ maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
61378
+ maxFeePerGas: fees.maxFeePerGas,
61379
+ gas: 0n,
61380
+ token: asset.token,
61381
+ recipient: to,
61382
+ amount: amountRaw
61383
+ });
61384
+ const gas2 = gasWithHeadroom(await estimate(rpc2, input.from.address, draft2));
61385
+ const tx2 = { ...draft2, gas: gas2 };
61386
+ const feeCap2 = gas2 * tx2.maxFeePerGas;
61387
+ if (nativeBalance < feeCap2) {
61388
+ throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, below the fee cap of ${formatUnits(feeCap2, NATIVE_DECIMALS)}.`);
61389
+ }
61390
+ const plan2 = {
61391
+ chainId,
61392
+ hood,
61393
+ from: input.from,
61394
+ asset,
61395
+ recipient: to,
61396
+ amountRaw,
61397
+ amount: formatUnits(amountRaw, asset.decimals),
61398
+ tx: tx2,
61399
+ feeCap: feeCap2,
61400
+ displayLines: []
61401
+ };
61402
+ plan2.displayLines = evmDisplayLines(plan2);
61403
+ return plan2;
61404
+ }
61405
+ const draft = buildNativeTransfer({
61406
+ chainId,
61407
+ nonce,
61408
+ maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
61409
+ maxFeePerGas: fees.maxFeePerGas,
61410
+ gas: 0n,
61411
+ to,
61412
+ value: max ? 0n : amountRaw
61413
+ });
61414
+ if (!max && nativeBalance < amountRaw) {
61415
+ throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, below the amount of ${formatUnits(amountRaw, NATIVE_DECIMALS)}.`);
61416
+ }
61417
+ const gas = gasWithHeadroom(await estimate(rpc2, input.from.address, draft));
61418
+ const feeCap = gas * draft.maxFeePerGas;
61419
+ let value = amountRaw;
61420
+ if (max) {
61421
+ value = nativeBalance - feeCap;
61422
+ if (value <= 0n) {
61423
+ throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, not above the fee cap of ${formatUnits(feeCap, NATIVE_DECIMALS)}; max leaves nothing to send.`);
61424
+ }
61425
+ } else if (nativeBalance < value + feeCap) {
61426
+ throw refuse2("EVM_INSUFFICIENT_FOR_FEES", `${input.from.label} holds ${formatUnits(nativeBalance, NATIVE_DECIMALS)} ${nativeName(hood, chainId)}, below the amount plus the fee cap of ${formatUnits(value + feeCap, NATIVE_DECIMALS)}.`);
61427
+ }
61428
+ const tx = { ...draft, gas, value };
61429
+ const plan = {
61430
+ chainId,
61431
+ hood,
61432
+ from: input.from,
61433
+ asset,
61434
+ recipient: to,
61435
+ amountRaw: value,
61436
+ amount: formatUnits(value, NATIVE_DECIMALS),
61437
+ tx,
61438
+ feeCap,
61439
+ displayLines: []
61440
+ };
61441
+ plan.displayLines = evmDisplayLines(plan);
61442
+ return plan;
61443
+ }
61444
+ async function estimate(rpc2, from, tx) {
61445
+ try {
61446
+ return await rpc2.estimateGas({ from, to: tx.to, value: tx.value, data: tx.data });
61447
+ } catch (error) {
61448
+ throw new VaultError("VAULT_UNREADABLE", `Could not estimate gas: ${error instanceof Error ? error.message : String(error)}`, { suggestion: "Nothing was signed. A revert here usually means the balance does not cover the amount." });
61449
+ }
61450
+ }
61451
+ function displayEvmTransferPlan(ctx, plan) {
61452
+ ctx.deps.stdout.write(`Decoded EVM transfer (local signing only):
61453
+ `);
61454
+ for (const line of plan.displayLines)
61455
+ ctx.deps.stdout.write(` ${line}
61456
+ `);
61457
+ }
61458
+ function evmFactorPrompt(plan) {
61459
+ return `sign transfer of ${plan.amount} ${plan.asset.symbol} to ${plan.recipient}${plan.hood ? " on Hood" : ` on chain ${plan.chainId}`}`;
61460
+ }
61461
+ function judgeReceipt(receipt, head, depth, hash, chainId) {
61462
+ if (receipt.status === 0) {
61463
+ throw new VaultError("EVM_TRANSFER_REVERTED", `Transaction ${hash} reverted in block ${receipt.blockNumber}; the fee was spent.`, {
61464
+ suggestion: "Nothing else was signed. Read the transaction on an explorer before sending again.",
61465
+ details: { hash, chainId: chainId.toString(), blockNumber: receipt.blockNumber.toString(), status: "reverted" }
61466
+ });
61467
+ }
61468
+ const reached = head - receipt.blockNumber + 1n;
61469
+ if (reached < BigInt(depth))
61470
+ return;
61471
+ return {
61472
+ status: "confirmed",
61473
+ exit: 0,
61474
+ blockNumber: receipt.blockNumber,
61475
+ line: `Confirmed ${hash} in block ${receipt.blockNumber}, ${reached} block${reached === 1n ? "" : "s"} deep (depth ${depth}, not finality).`
61476
+ };
61477
+ }
61478
+ async function awaitReceipt(rpc2, ctx, hash, chainId) {
61479
+ const depth = requiredDepth(chainId);
61480
+ const deadline = ctx.deps.now() + EVM_RECEIPT_WAIT_MS;
61481
+ let seen;
61482
+ for (;; ) {
61483
+ try {
61484
+ const receipt = await rpc2.getTransactionReceipt(hash);
61485
+ if (receipt !== null) {
61486
+ seen = receipt;
61487
+ const outcome = judgeReceipt(receipt, await rpc2.blockNumber(), depth, hash, chainId);
61488
+ if (outcome !== undefined)
61489
+ return outcome;
61490
+ }
61491
+ } catch (error) {
61492
+ if (error instanceof VaultError)
61493
+ throw error;
61494
+ }
61495
+ if (ctx.deps.now() >= deadline)
61496
+ break;
61497
+ await ctx.deps.sleep(EVM_RECEIPT_POLL_MS);
61498
+ }
61499
+ if (seen !== undefined) {
61500
+ return {
61501
+ status: "uncertain",
61502
+ exit: 3,
61503
+ blockNumber: seen.blockNumber,
61504
+ line: `Submitted ${hash}: it is in block ${seen.blockNumber} but not yet ${depth} block${depth === 1 ? "" : "s"} deep after ${EVM_RECEIPT_WAIT_MS / 1000} s. Do not resend; check the hash on an explorer.`
61505
+ };
61506
+ }
61507
+ return {
61508
+ status: "uncertain",
61509
+ exit: 3,
61510
+ line: `Submitted ${hash}; no receipt after ${EVM_RECEIPT_WAIT_MS / 1000} s. It may still land: do not resend blindly; check the hash on an explorer first.`
61511
+ };
61512
+ }
61513
+ async function broadcast(rpc2, ctx, raw, hash, chainId) {
61514
+ try {
61515
+ await rpc2.sendRawTransaction(raw);
61516
+ } catch (error) {
61517
+ if (!(error instanceof EvmRpcError))
61518
+ throw error;
61519
+ const message = error.message.toLowerCase();
61520
+ if (error.kind === "transport") {
61521
+ return {
61522
+ status: "uncertain",
61523
+ exit: 3,
61524
+ line: `Submitted ${hash}, but the RPC did not answer eth_sendRawTransaction (${error.message}). It may still land: do not resend blindly; check the hash on an explorer first.`
61525
+ };
61526
+ }
61527
+ if (message.includes("already known")) {
61528
+ return {
61529
+ status: "uncertain",
61530
+ exit: 3,
61531
+ line: `Submitted ${hash}: the RPC already knows it, so it is in flight. Do not resend; check the hash on an explorer.`
61532
+ };
61533
+ }
61534
+ if (message.includes("nonce too low")) {
61535
+ let receipt = null;
61536
+ try {
61537
+ receipt = await rpc2.getTransactionReceipt(hash);
61538
+ } catch {
61539
+ receipt = null;
61540
+ }
61541
+ if (receipt !== null) {
61542
+ let head;
61543
+ try {
61544
+ head = await rpc2.blockNumber();
61545
+ } catch {
61546
+ return awaitReceipt(rpc2, ctx, hash, chainId);
61547
+ }
61548
+ const outcome = judgeReceipt(receipt, head, requiredDepth(chainId), hash, chainId);
61549
+ if (outcome !== undefined)
61550
+ return outcome;
61551
+ return awaitReceipt(rpc2, ctx, hash, chainId);
61552
+ }
61553
+ throw new VaultError("EVM_NONCE_STALE", `The RPC refused ${hash}: nonce too low, and it has no receipt for that hash. Another transaction used this nonce.`, {
61554
+ suggestion: "Nothing was resent. Run the transfer again; it reads the pending nonce afresh. The CLI never re-signs on its own.",
61555
+ details: { hash, chainId: chainId.toString() }
61556
+ });
61557
+ }
61558
+ return {
61559
+ status: "uncertain",
61560
+ exit: 3,
61561
+ line: `Submitted ${hash}, and the RPC answered: ${error.message}. The CLI cannot tell from that whether it is in flight: do not resend blindly; check the hash on an explorer first.`
61562
+ };
61563
+ }
61564
+ return awaitReceipt(rpc2, ctx, hash, chainId);
61565
+ }
61566
+ async function runEvmTransfer(input, rpc2) {
61567
+ const { ctx } = input;
61568
+ const { deps } = ctx;
61569
+ checkEvmDestination(input.to, input.from);
61570
+ deps.stderr.write(`Reading chain id, nonce, fees and balances for ${input.from.label} from ${rpcHostOf(input.rpcUrl)}${input.rpcUrl === DEFAULT_HOOD_RPC_URL ? " (the built-in Hood RPC)" : ""}.
61571
+ `);
61572
+ const planned = await planEvmTransfer(rpc2, input);
61573
+ if ("usage" in planned)
61574
+ return input.usage(planned.usage);
61575
+ const plan = planned;
61576
+ displayEvmTransferPlan(ctx, plan);
61577
+ await input.confirmLastSix(plan.recipient, plan.asset.kind === "erc20" ? "the token recipient" : "the destination");
61578
+ await input.confirmFactor(evmFactorPrompt(plan));
61579
+ const chainIdAgain = await rpc2.chainId();
61580
+ if (chainIdAgain !== plan.chainId) {
61581
+ throw refuse2("EVM_CHAIN_MISMATCH", `The RPC answered chain id ${chainIdAgain} after the factor, but ${plan.chainId} was displayed.`);
61582
+ }
61583
+ const nonceAgain = await rpc2.getTransactionCount(plan.from.address, "pending");
61584
+ if (nonceAgain !== plan.tx.nonce) {
61585
+ throw refuse2("EVM_NONCE_STALE", `The pending nonce is ${nonceAgain} after the factor, but ${plan.tx.nonce} was displayed; another transaction moved it.`, { suggestion: "Nothing was signed. Run the transfer again; it reads the pending nonce afresh." });
61586
+ }
61587
+ const secret = await input.decryptSecret();
61588
+ let signed;
61589
+ try {
61590
+ if (!sameEvmAddress(evmAddressFromSecret(secret), plan.from.address)) {
61591
+ throw new VaultError("VAULT_VERIFY_FAILED", "The decrypted key does not match the planned sender.");
61592
+ }
61593
+ signed = signTransaction(plan.tx, secret);
61594
+ } finally {
61595
+ wipe(secret);
61596
+ }
61597
+ const outcome = await broadcast(rpc2, ctx, signed.raw, signed.hash, plan.chainId);
61598
+ if (ctx.json) {
61599
+ input.writeJson({
61600
+ ok: outcome.status === "confirmed",
61601
+ chainId: Number(plan.chainId),
61602
+ hash: signed.hash,
61603
+ status: outcome.status,
61604
+ ...outcome.blockNumber !== undefined ? { blockNumber: outcome.blockNumber.toString() } : {},
61605
+ depth: requiredDepth(plan.chainId),
61606
+ finalized: false,
61607
+ from: plan.from.address,
61608
+ to: plan.tx.to,
61609
+ recipient: plan.recipient,
61610
+ amount: plan.amount,
61611
+ asset: plan.asset.symbol,
61612
+ amountRaw: plan.amountRaw.toString(),
61613
+ ...plan.asset.token !== undefined ? { token: plan.asset.token } : {},
61614
+ nonce: plan.tx.nonce.toString(),
61615
+ gas: plan.tx.gas.toString(),
61616
+ maxFeePerGas: plan.tx.maxFeePerGas.toString(),
61617
+ maxPriorityFeePerGas: plan.tx.maxPriorityFeePerGas.toString()
61618
+ });
61619
+ } else {
61620
+ deps.stdout.write(`${outcome.line}
61621
+ `);
61622
+ }
61623
+ return outcome.exit;
61624
+ }
61625
+
61626
+ // src/commands/vault-transfer.ts
59709
61627
  init_promote_support();
59710
61628
  init_store();
59711
61629
 
@@ -59788,20 +61706,29 @@ async function vaultTransfer(args, ctx) {
59788
61706
  return usage(ctx, parsed.error);
59789
61707
  const [to, extra] = parsed.positionals;
59790
61708
  if (!to || extra !== undefined) {
59791
- return usage(ctx, "Usage: candle vault transfer <to> --amount <n> --asset SOL|<mint> --from <label> --rpc-url <url>");
61709
+ return usage(ctx, "Usage: candle vault transfer <to> --amount <n|max> --asset SOL|<mint>|ETH|USDG|<0x token> --from <label> [--rpc-url <url>]");
59792
61710
  }
59793
61711
  const amount = parsed.values["--amount"];
59794
61712
  const asset = parsed.values["--asset"];
59795
61713
  const fromLabel = parsed.values["--from"];
59796
- const rpcUrl2 = parsed.values["--rpc-url"];
61714
+ const rpcUrlFlag = parsed.values["--rpc-url"];
59797
61715
  if (!amount)
59798
61716
  return usage(ctx, "--amount <n> is required.");
59799
61717
  if (!asset)
59800
- return usage(ctx, "--asset SOL|<mint> is required.");
61718
+ return usage(ctx, "--asset SOL|<mint>|ETH|USDG|<0x token> is required.");
59801
61719
  if (!fromLabel)
59802
61720
  return usage(ctx, "--from <label> is required.");
59803
- if (!rpcUrl2)
59804
- return usage(ctx, "--rpc-url <url> is required.");
61721
+ if (rpcUrlFlag !== undefined) {
61722
+ try {
61723
+ new URL(rpcUrlFlag);
61724
+ } catch {
61725
+ return usage(ctx, `--rpc-url is not a valid URL: ${rpcUrlFlag}`);
61726
+ }
61727
+ } else {
61728
+ const evmFromEnv = resolveEvmRpcUrl(undefined, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
61729
+ if ("error" in evmFromEnv)
61730
+ return usage(ctx, evmFromEnv.error);
61731
+ }
59805
61732
  if (!refuseEnvPassphrase(ctx))
59806
61733
  return 1;
59807
61734
  if (!requireTty(ctx, "vault transfer"))
@@ -59822,6 +61749,34 @@ async function vaultTransfer(args, ctx) {
59822
61749
  return usage(ctx, `No vault key or promoted wallet matches --from ${fromLabel}.`);
59823
61750
  }
59824
61751
  assertTransferSigner(fromEntry);
61752
+ if (fromEntry.chain === "evm") {
61753
+ if (fromEntry.role !== "vault") {
61754
+ throw new VaultError("PROMOTE_NOT_VAULT_KEY", `${fromEntry.label || fromEntry.address} is an EVM ${fromEntry.role} entry; this release signs EVM transfers from vault keys only.`, { suggestion: "Nothing was signed." });
61755
+ }
61756
+ const evmRpc = resolveEvmRpcUrl(rpcUrlFlag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
61757
+ if ("error" in evmRpc)
61758
+ return usage(ctx, evmRpc.error);
61759
+ const evmRpcUrl = evmRpc.url;
61760
+ const secretRef2 = fromEntry;
61761
+ return runEvmTransfer({
61762
+ ctx,
61763
+ rpcUrl: evmRpcUrl,
61764
+ builtIn: evmRpc.builtIn,
61765
+ from: fromEntry,
61766
+ to,
61767
+ amount,
61768
+ asset,
61769
+ confirmLastSix: (address, what) => confirmLastSix(ctx, address, what),
61770
+ confirmFactor: (what) => opened.confirm(what),
61771
+ decryptSecret: () => decryptKey(vault, secretRef2.id),
61772
+ usage: (line) => usage(ctx, line),
61773
+ writeJson: (value) => writeJson(ctx.deps, value)
61774
+ }, createEvmRpc(evmRpcUrl, ctx.deps.fetch));
61775
+ }
61776
+ assertSolanaDestination(to, fromEntry);
61777
+ const rpcUrl2 = rpcUrlFlag;
61778
+ if (!rpcUrl2)
61779
+ return usage(ctx, "--rpc-url <url> is required for a transfer from a Solana key.");
59825
61780
  const promoted = fromEntry.role === "tee-wallet";
59826
61781
  if (promoted)
59827
61782
  assertNoPendingSweep(fromEntry);
@@ -59889,7 +61844,7 @@ init_args();
59889
61844
  init_release();
59890
61845
  import { dirname as dirname10, join as join11 } from "node:path";
59891
61846
  init_render();
59892
- var USAGE2 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
61847
+ var USAGE3 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
59893
61848
  async function resolveIdentity(deps, bundlePath, flag) {
59894
61849
  if (flag)
59895
61850
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -59921,19 +61876,19 @@ async function verify(args, ctx) {
59921
61876
  });
59922
61877
  if ("error" in parsed) {
59923
61878
  writeUsageFailure(deps, `${parsed.error}
59924
- ${USAGE2}`, json);
61879
+ ${USAGE3}`, json);
59925
61880
  return 2;
59926
61881
  }
59927
61882
  const file = parsed.positionals[0];
59928
61883
  if (parsed.positionals.length !== 1 || file === undefined) {
59929
61884
  writeUsageFailure(deps, `verify takes exactly one file.
59930
- ${USAGE2}`, json);
61885
+ ${USAGE3}`, json);
59931
61886
  return 2;
59932
61887
  }
59933
61888
  const bundlePath = parsed.values["--bundle"];
59934
61889
  if (!bundlePath) {
59935
61890
  writeUsageFailure(deps, `--bundle is required.
59936
- ${USAGE2}`, json);
61891
+ ${USAGE3}`, json);
59937
61892
  return 2;
59938
61893
  }
59939
61894
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -59947,7 +61902,7 @@ ${USAGE2}`, json);
59947
61902
  }
59948
61903
  if (resolved.kind === "absent") {
59949
61904
  writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
59950
- ${USAGE2}`, json);
61905
+ ${USAGE3}`, json);
59951
61906
  return 2;
59952
61907
  }
59953
61908
  const identity = resolved.uri;
@@ -59988,6 +61943,177 @@ function messageOf2(error) {
59988
61943
  return error instanceof Error ? error.message : String(error);
59989
61944
  }
59990
61945
 
61946
+ // src/commands/wallets-trust.ts
61947
+ init_args();
61948
+ init_deps();
61949
+ init_profiles();
61950
+ init_render();
61951
+ init_promote_support();
61952
+ var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
61953
+ var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
61954
+ var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
61955
+ var DEVICE_TOKEN_REQUIRED2 = {
61956
+ code: "DEVICE_TOKEN_REQUIRED",
61957
+ message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
61958
+ suggestion: "Run: candle auth login"
61959
+ };
61960
+ function sessionLinkedLine(rows) {
61961
+ const n = rows.length;
61962
+ const it = n === 1 ? "it" : "them";
61963
+ const names = rows.map((row) => row.label ?? row.address).join(", ");
61964
+ return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
61965
+ }
61966
+ function nothingToChangeLine(shown, trusted) {
61967
+ const session = shown.sessionLinked ?? [];
61968
+ const unchanged = shown.unchanged;
61969
+ if (session.length > 0 && unchanged.length === 0) {
61970
+ const one = session.length === 1;
61971
+ return `Nothing to change: ${one ? "that wallet is" : `those ${session.length} wallets are`} always yours while linked. Revoke ${one ? "it" : "them"} to remove ${one ? "it" : "them"}.
61972
+ `;
61973
+ }
61974
+ const bits = [];
61975
+ if (session.length > 0)
61976
+ bits.push(sessionLinkedLine(session).replace(/\.$/, ""));
61977
+ if (unchanged.length > 0) {
61978
+ bits.push(unchanged.length === 1 ? `that wallet is already ${trusted ? "trusted" : "untrusted"}` : `all ${unchanged.length} wallets are already ${trusted ? "trusted" : "untrusted"}`);
61979
+ }
61980
+ if (bits.length === 0)
61981
+ return `Nothing to change.
61982
+ `;
61983
+ return `Nothing to change: ${bits.join("; ")}.
61984
+ `;
61985
+ }
61986
+ function writeTrustFailure(ctx, result) {
61987
+ const { deps, apiUrl, json } = ctx;
61988
+ let code = result.code;
61989
+ let message = result.message;
61990
+ let suggestion;
61991
+ if (result.status === 404) {
61992
+ code = "TRUST_UNSUPPORTED";
61993
+ message = "This Candle API does not support trusting wallets yet; nothing changed.";
61994
+ } else {
61995
+ const error = result.raw?.error;
61996
+ if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
61997
+ const ids = error.matches.map((m) => String(m.id)).join(", ");
61998
+ suggestion = `Name one of them by id or address: ${ids}`;
61999
+ } else if (error?.reason === "not_found") {
62000
+ suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
62001
+ }
62002
+ }
62003
+ const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
62004
+ if (json) {
62005
+ deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
62006
+ `);
62007
+ } else {
62008
+ deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
62009
+ `);
62010
+ }
62011
+ return 1;
62012
+ }
62013
+ function trustTable(rows) {
62014
+ return renderTable(["line", "label", "wallet", "address"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address]));
62015
+ }
62016
+ async function setTrust(args, ctx, trusted) {
62017
+ const { deps, apiUrl, json } = ctx;
62018
+ const usage5 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
62019
+ const command = trusted ? "wallets trust" : "wallets untrust";
62020
+ const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
62021
+ if ("error" in parsed) {
62022
+ writeUsageFailure(deps, parsed.error, json);
62023
+ return 2;
62024
+ }
62025
+ if (parsed.positionals.length === 0) {
62026
+ writeUsageFailure(deps, `Name at least one wallet. ${usage5}`, json);
62027
+ return 2;
62028
+ }
62029
+ const skipConfirm = !trusted && parsed.booleans.has("--yes");
62030
+ await printIdentity(ctx);
62031
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
62032
+ if (!deviceToken) {
62033
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
62034
+ return 1;
62035
+ }
62036
+ if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
62037
+ writeLocalFailure(deps, {
62038
+ code: "TRUST_REQUIRES_TTY",
62039
+ message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
62040
+ suggestion: trusted ? "Run it in an interactive shell; there is no flag and no environment variable for confirm." : "Run it in an interactive shell, or pass --yes to untrust without the prompt."
62041
+ }, json);
62042
+ return 1;
62043
+ }
62044
+ const call = (body) => apiRequest(TRUST_PATH, {
62045
+ method: "POST",
62046
+ body,
62047
+ auth: "device",
62048
+ credentials: { deviceToken },
62049
+ apiUrl,
62050
+ fetch: deps.fetch,
62051
+ env: deps.env
62052
+ });
62053
+ const preview = await call({ dryRun: true, wallets: parsed.positionals, trusted });
62054
+ if (!preview.ok)
62055
+ return writeTrustFailure(ctx, preview);
62056
+ const shown = preview.body;
62057
+ const changing = shown.changed;
62058
+ if (changing.length === 0) {
62059
+ if (json) {
62060
+ deps.stdout.write(`${JSON.stringify({ ...shown, command })}
62061
+ `);
62062
+ } else {
62063
+ deps.stdout.write(nothingToChangeLine(shown, trusted));
62064
+ }
62065
+ return 0;
62066
+ }
62067
+ const n = changing.length;
62068
+ const screen = [
62069
+ trustTable(changing),
62070
+ ...shown.unchanged.length > 0 ? [
62071
+ `${shown.unchanged.length} already ${trusted ? "trusted" : "untrusted"}: ${shown.unchanged.map((row) => row.label ?? row.address).join(", ")}`
62072
+ ] : [],
62073
+ ...(shown.sessionLinked?.length ?? 0) > 0 ? [sessionLinkedLine(shown.sessionLinked ?? [])] : [],
62074
+ "",
62075
+ trusted ? `${n === 1 ? "This wallet" : `These ${n} wallets`} will be trusted: your agents can move funds into ${n === 1 ? "it" : "them"} with no cap, like a wallet you linked while signed in.` : `${n === 1 ? "This wallet" : `These ${n} wallets`} will no longer be trusted: moving funds into ${n === 1 ? "it" : "them"} will need the withdrawal allowlist again.`,
62076
+ ""
62077
+ ];
62078
+ deps.stderr.write(`${screen.join(`
62079
+ `)}
62080
+ `);
62081
+ if (!skipConfirm) {
62082
+ const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${trusted ? "trust" : "untrust"} ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
62083
+ if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
62084
+ writeLocalFailure(deps, {
62085
+ code: "TRUST_NOT_ACKNOWLEDGED",
62086
+ message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
62087
+ suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
62088
+ }, json);
62089
+ return 1;
62090
+ }
62091
+ }
62092
+ const committed = await call({ walletIds: changing.map((row) => row.id), trusted });
62093
+ if (!committed.ok)
62094
+ return writeTrustFailure(ctx, committed);
62095
+ const result = committed.body;
62096
+ if (json) {
62097
+ deps.stdout.write(`${JSON.stringify({ ...result, command })}
62098
+ `);
62099
+ return result.skipped.length > 0 ? 1 : 0;
62100
+ }
62101
+ const done = result.changed.length;
62102
+ deps.stdout.write(`${trusted ? "Trusted" : "Untrusted"} ${done} wallet${done === 1 ? "" : "s"}.
62103
+ `);
62104
+ for (const skip of result.skipped) {
62105
+ deps.stderr.write(`Skipped ${skip.walletId}: ${skip.reason === "revoked" ? "revoked" : "not on this account"}.
62106
+ `);
62107
+ }
62108
+ return result.skipped.length > 0 ? 1 : 0;
62109
+ }
62110
+ function walletsTrust(args, ctx) {
62111
+ return setTrust(args, ctx, true);
62112
+ }
62113
+ function walletsUntrust(args, ctx) {
62114
+ return setTrust(args, ctx, false);
62115
+ }
62116
+
59991
62117
  // src/config.ts
59992
62118
  import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
59993
62119
  import { homedir as homedir5 } from "node:os";
@@ -60303,6 +62429,7 @@ function extractGlobalFlags(argv) {
60303
62429
  }
60304
62430
  var COMMANDS = {
60305
62431
  swap: { bare: swap, subcommands: { status: swapStatus } },
62432
+ transfer: { bare: transfer },
60306
62433
  launch: { bare: launch },
60307
62434
  pnl: { bare: pnl },
60308
62435
  portfolio: { bare: portfolio },
@@ -60310,7 +62437,7 @@ var COMMANDS = {
60310
62437
  auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
60311
62438
  keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
60312
62439
  wallets: {
60313
- subcommands: { import: walletsImport, revoke: walletsRevoke },
62440
+ subcommands: { import: walletsImport, revoke: walletsRevoke, trust: walletsTrust, untrust: walletsUntrust },
60314
62441
  bare: wallets
60315
62442
  },
60316
62443
  vault: {