@candledottv/cli 0.11.15 → 0.11.16

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 +1 -1
  2. package/dist/index.js +2098 -1907
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -444,6 +444,9 @@ function writeUsageFailure(deps, message, json) {
444
444
  deps.stderr.write(`${message}
445
445
  `);
446
446
  }
447
+ function portalDevicesUrl(apiUrl, portalOrigin) {
448
+ return `${portalDeviceUrl(apiUrl, portalOrigin)}?tab=keys#connected-devices`;
449
+ }
447
450
  function portalDeviceUrl(apiUrl, portalOrigin) {
448
451
  if (portalOrigin) {
449
452
  try {
@@ -653,7 +656,14 @@ function profileTable(config, now) {
653
656
  }
654
657
  var PRE_PROFILE_FIELDS, API_KEY_PREFIXES, API_KEY_RANDOM_LENGTH = 43, INVALID_RPC_HOST = "invalid (fix with profile set)";
655
658
  var init_profiles = __esm(() => {
656
- PRE_PROFILE_FIELDS = ["apiUrl", "keyPrefix", "deviceTokenPrefix", "scopes", "label", "portalOrigin"];
659
+ PRE_PROFILE_FIELDS = [
660
+ "apiUrl",
661
+ "keyPrefix",
662
+ "deviceTokenPrefix",
663
+ "scopes",
664
+ "label",
665
+ "portalOrigin"
666
+ ];
657
667
  API_KEY_PREFIXES = ["cndl_live_", "cndl_test_"];
658
668
  });
659
669
 
@@ -821,7 +831,7 @@ class EncryptedFileSecretStore {
821
831
  }
822
832
  async function promptHiddenSecret(promptText) {
823
833
  if (!process.stdin.isTTY) {
824
- throw new Error("No TTY available for interactive input; pass --key-file instead");
834
+ throw new Error("No TTY available for hidden input; run this in a terminal.");
825
835
  }
826
836
  return readHiddenLine(promptText, realPromptStreams());
827
837
  }
@@ -26900,10 +26910,18 @@ async function assertNotOlderCopy(ctx, path, raw, accept) {
26900
26910
  }
26901
26911
  async function confirmLastSix(ctx, address, what) {
26902
26912
  const expected = address.slice(-6);
26903
- const typed = (await ctx.deps.promptLine(`Type the last six characters of ${what} (${address}) to confirm: `)).trim();
26904
- if (typed !== expected) {
26905
- throw new VaultError("DESTINATION_NOT_CONFIRMED", "That is not the last six characters of that address; nothing was done.", { suggestion: "Nothing was signed. Run it again and type the last six characters exactly as shown." });
26913
+ const normalize3 = (value) => address.startsWith("0x") ? value.toLowerCase() : value;
26914
+ for (let attempt = 0;attempt < 3; attempt++) {
26915
+ const typed = (await ctx.deps.promptLine(`Type the last six characters of ${what} (${address}) to confirm: `)).trim();
26916
+ if (normalize3(typed) === normalize3(expected))
26917
+ return;
26918
+ const difference = Array.from({ length: Math.max(expected.length, typed.length) }, (_, i) => i).find((i) => normalize3(typed)[i] !== normalize3(expected)[i]) ?? 0;
26919
+ ctx.deps.stderr.write(`Expected ${expected}
26920
+ You typed ${typed}
26921
+ ${" ".repeat(10 + difference)}^ character ${difference + 1} differs
26922
+ `);
26906
26923
  }
26924
+ throw new VaultError("DESTINATION_NOT_CONFIRMED", "That is not the last six characters of that address; nothing was done.", { suggestion: "Nothing was signed. Run it again and type the last six characters exactly as shown." });
26907
26925
  }
26908
26926
  function writeVaultFailure(ctx, error) {
26909
26927
  if (isUsageError(error))
@@ -30286,399 +30304,1233 @@ var init_wallet_keygen = __esm(() => {
30286
30304
  init_esm();
30287
30305
  });
30288
30306
 
30289
- // src/vault/evm-sweep.ts
30290
- function sameToken(a, b) {
30291
- return a.toLowerCase() === b.toLowerCase();
30307
+ // src/commands/swap.ts
30308
+ import { randomUUID } from "node:crypto";
30309
+ function tradingFailure(ctx, error, id) {
30310
+ if (error instanceof TradingUsage) {
30311
+ writeUsageFailure(ctx.deps, error.message, ctx.json);
30312
+ return 2;
30313
+ }
30314
+ writeLocalFailure(ctx.deps, {
30315
+ code: error instanceof TradingError ? error.code : "TRADING_FAILED",
30316
+ message: `${error instanceof Error ? describeRpcFailure(error) : "Trading failed."}${id ? ` Operation ${id}; use candle swap status ${id} before another attempt.` : ""}`,
30317
+ ...error instanceof TradingError && error.suggestion ? { suggestion: error.suggestion } : {},
30318
+ ...error instanceof TradingError && error.details ? { details: error.details } : {}
30319
+ }, ctx.json);
30320
+ return error instanceof TradingError ? error.exitCode : 1;
30292
30321
  }
30293
- function mergedTokens(sources) {
30294
- const out = [];
30295
- const seen = new Set;
30296
- for (const token of [
30297
- ...sources.always,
30298
- ...sources.flags,
30299
- ...sources.record.tokens,
30300
- ...sources.server.tokens,
30301
- ...sources.logs.tokens
30302
- ]) {
30303
- const key = token.toLowerCase();
30304
- if (seen.has(key))
30305
- continue;
30306
- seen.add(key);
30307
- out.push(toChecksumAddress(token));
30322
+ function lazySolanaClient(ctx, flag) {
30323
+ let pending;
30324
+ return () => {
30325
+ pending ??= tradingSolanaClient(ctx, flag);
30326
+ return pending;
30327
+ };
30328
+ }
30329
+ async function tradingRead(ctx, client, read) {
30330
+ try {
30331
+ return await read();
30332
+ } catch (error) {
30333
+ if (isRateLimited(error))
30334
+ throw tradingRateLimited(ctx, client.endpoint.host, error);
30335
+ throw error;
30308
30336
  }
30309
- return out;
30310
30337
  }
30311
- function alwaysTokens() {
30312
- return [toChecksumAddress(HOOD_USDG_ADDRESS), HOOD_WETH_ADDRESS];
30338
+ function printTradingResult(ctx, result) {
30339
+ ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
30340
+ ` : `${JSON.stringify(result, null, 2)}
30341
+ `);
30342
+ return 0;
30313
30343
  }
30314
- function recordSource(read, wallet) {
30315
- const mine = read.entries.filter((entry) => sameToken(entry.wallet, wallet));
30316
- return {
30317
- tokens: mine.flatMap((entry) => entry.kind === "token" ? [entry.token] : []),
30318
- scanStarts: mine.flatMap((entry) => entry.kind === "scanStart" ? [BigInt(entry.block)] : []),
30319
- absent: read.absent,
30320
- noKey: read.noKey,
30321
- unreadableLines: read.unreadableLines,
30322
- partialTail: read.partialTail,
30323
- path: read.path
30324
- };
30344
+ function reportSettlement(ctx, settlement, out, id) {
30345
+ const parsed = settlementSchema.safeParse(settlement);
30346
+ const reading = parsed.success ? parsed.data : undefined;
30347
+ if (reading?.state === "settled" && reading.settledOutRaw !== undefined)
30348
+ ctx.deps.stderr.write(`settled ${decimalAmount(reading.settledOutRaw, out.decimals)} ${out.asset}
30349
+ `);
30350
+ else {
30351
+ const state = !reading ? "not measured" : reading.state === "settled" ? "settled, amount not measured" : reading.state;
30352
+ ctx.deps.stderr.write(`Settlement: ${state}. Re-check with GET /api/v1/agent/swap/jobs/${id} (candle swap status ${id}).
30353
+ `);
30354
+ }
30355
+ return settlement !== null && typeof settlement === "object" ? { settlement } : {};
30325
30356
  }
30326
- function startBlock(scanStarts, fromBlockFlag) {
30327
- const recorded = scanStarts.length > 0 ? scanStarts.reduce((a, b) => a < b ? a : b) : undefined;
30328
- if (recorded === undefined && fromBlockFlag === undefined)
30329
- return;
30330
- if (recorded === undefined)
30331
- return { block: fromBlockFlag, source: "--from-block" };
30332
- if (fromBlockFlag === undefined)
30333
- return { block: recorded, source: "record" };
30334
- return { block: recorded < fromBlockFlag ? recorded : fromBlockFlag, source: "record and --from-block" };
30357
+ function validClientId(id) {
30358
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id);
30335
30359
  }
30336
- async function discoverTransferLogs(rpc, wallet, fromBlock, toBlock, opts = {}) {
30337
- const seen = new Map;
30338
- let page = opts.pageBlocks ?? LOG_PAGE_BLOCKS;
30339
- let pages = 0;
30340
- let from = fromBlock;
30341
- const topics = [ERC20_TRANSFER_TOPIC, null, addressTopic(wallet)];
30342
- while (from <= toBlock) {
30343
- const to = from + page - 1n > toBlock ? toBlock : from + page - 1n;
30360
+ async function lookupOperation(ctx, key, id, kind) {
30361
+ const local = await savedOperation(ctx, key, id);
30362
+ const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
30363
+ const found = [];
30364
+ for (const candidate of kinds) {
30344
30365
  try {
30345
- const logs = await rpc.getLogs({ fromBlock: from, toBlock: to, topics });
30346
- pages += 1;
30347
- for (const log of logs)
30348
- seen.set(log.address.toLowerCase(), log.address);
30349
- from = to + 1n;
30366
+ found.push({
30367
+ ...operationSchema.parse(await request(ctx, key, jobPath(candidate, id))),
30368
+ kind: candidate,
30369
+ clientTradeId: id
30370
+ });
30350
30371
  } catch (error) {
30351
- if (error instanceof EvmRpcError && error.kind === "rpc" && page > LOG_PAGE_FLOOR) {
30352
- page = page / 2n < LOG_PAGE_FLOOR ? LOG_PAGE_FLOOR : page / 2n;
30353
- continue;
30354
- }
30355
- const reason = error instanceof Error ? error.message : String(error);
30356
- return { failed: `eth_getLogs from block ${from} failed: ${reason}`, tokens: [...seen.values()], pages };
30372
+ if (!(error instanceof TradingError && error.code === "JOB_NOT_FOUND"))
30373
+ throw error;
30357
30374
  }
30358
30375
  }
30359
- return { tokens: [...seen.values()], pages };
30376
+ if (found.length > 1)
30377
+ throw new TradingError("AMBIGUOUS_OPERATION", "This id exists on multiple rails; specify --kind trade, swap or launch.");
30378
+ return found[0] ? { ...found[0], ...local?.signature ? { signature: local.signature } : {} } : null;
30360
30379
  }
30361
- function sourceLines(sources, host) {
30362
- const lines = ["Token sources:", " USDG and WETH: always read"];
30363
- const record = sources.record;
30364
- if (record.noKey && record.absent) {
30365
- lines.push(` sealed EVM record: none (this vault has no record key)`);
30366
- } else if (record.absent) {
30367
- lines.push(` sealed EVM record: absent at ${record.path} (not on this machine, or nothing recorded yet)`);
30368
- } else {
30369
- const damage = [
30370
- ...record.unreadableLines > 0 ? [`${record.unreadableLines} unreadable line(s) skipped`] : [],
30371
- ...record.partialTail ? ["a partial last line skipped"] : []
30372
- ];
30373
- lines.push(` sealed EVM record: ${record.tokens.length} token(s), ${record.scanStarts.length} scan start(s)${damage.length > 0 ? `; ${damage.join(", ")}` : ""}`);
30374
- }
30375
- const server = sources.server;
30376
- if (!server.asked)
30377
- lines.push(` server traded-token list: not asked (${server.reason ?? "--emergency calls no Candle API"})`);
30378
- else if (!server.answered)
30379
- lines.push(` server traded-token list: FAILED (${server.reason ?? "no answer"})`);
30380
- else
30381
- lines.push(` server traded-token list: ${server.tokens.length} token(s)${server.truncated ? " (truncated)" : ""}`);
30382
- lines.push(` --token: ${sources.flags.length}`);
30383
- const logs = sources.logs;
30384
- if (logs.ran && !logs.failed) {
30385
- lines.push(` Transfer logs over ${host}: blocks ${logs.fromBlock} to ${logs.toBlock} (start from ${logs.startSource}), ${logs.tokens.length} contract(s)`);
30386
- } else if (logs.failed) {
30387
- lines.push(` Transfer logs over ${host}: FAILED (${logs.reason})`);
30388
- } else {
30389
- lines.push(` Transfer logs: NOT scanned (${logs.reason})`);
30380
+ async function swapStatus(args, ctx) {
30381
+ const parsed = parseArgs(args, { valueFlags: ["--kind"], booleanFlags: ["--wait"] });
30382
+ if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "") || parsed.values["--kind"] !== undefined && !["trade", "swap", "launch"].includes(parsed.values["--kind"])) {
30383
+ writeUsageFailure(ctx.deps, "Usage: candle swap status <id> [--kind trade|swap|launch] [--wait]", ctx.json);
30384
+ return 2;
30390
30385
  }
30391
- if (logs.ran && logs.fromBlock !== undefined) {
30392
- lines.push(` A token received before block ${logs.fromBlock}, or from a contract that emits no Transfer log, still needs --token <0x...>.`);
30386
+ try {
30387
+ const key = await tradingKey(ctx);
30388
+ const id = parsed.positionals[0];
30389
+ const kind = parsed.values["--kind"];
30390
+ const facts = bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge);
30391
+ const found = await lookupOperation(ctx, key, id, kind);
30392
+ if (!found)
30393
+ throw new TradingError("JOB_NOT_FOUND", `No operation ${id} is visible to key ${apiKeyPrefix(key) ?? "this key"} on the ${kind ?? "trade, swap, launch"} rail(s). Nothing was resent.`, {
30394
+ suggestion: `Trade and launch jobs are readable by any key on this account; swap jobs only by the key that placed them. If another key or profile placed it, run: candle swap status ${id} --profile <that profile>`
30395
+ });
30396
+ const result = parsed.booleans.has("--wait") ? await waitForSettlement(ctx, key, id, found, facts) : { ...found, ...bridgeStatusField(ctx, found, facts) };
30397
+ return printTradingResult(ctx, result);
30398
+ } catch (error) {
30399
+ return tradingFailure(ctx, error);
30393
30400
  }
30394
- return lines;
30395
- }
30396
- var LOG_PAGE_BLOCKS = 10000n, LOG_PAGE_FLOOR = 250n;
30397
- var init_evm_sweep = __esm(() => {
30398
- init_evm_lite();
30399
- });
30400
-
30401
- // src/vault/evm-tee.ts
30402
- function resolveHoodClient(ctx, flag) {
30403
- const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
30404
- if ("error" in resolved)
30405
- return resolved;
30406
- return {
30407
- rpc: createEvmRpc(resolved.url, ctx.deps.fetch),
30408
- url: resolved.url,
30409
- host: rpcHostOf2(resolved.url),
30410
- builtIn: resolved.builtIn
30411
- };
30412
30401
  }
30413
- function hoodHostLine(client, what) {
30414
- return `Reading ${what} from ${client.host}${client.builtIn ? " (the built-in Hood RPC)" : ""}.`;
30402
+ function bridgeFactsOf(raw) {
30403
+ const parsed = bridgeFactsSchema.safeParse(raw);
30404
+ return parsed.success ? parsed.data : undefined;
30415
30405
  }
30416
- async function assertHoodChain(client) {
30417
- const chainId = await client.rpc.chainId();
30418
- if (chainId !== BigInt(HOOD_CHAIN_ID)) {
30419
- throw new VaultError("EVM_CHAIN_MISMATCH", `${client.host} answered chain id ${chainId}; Hood TEE wallets live on Hood, chain id ${HOOD_CHAIN_ID}, only.`, { suggestion: "Nothing was signed or written. Point --rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC." });
30420
- }
30406
+ function isBridgeJob(job, facts) {
30407
+ return facts !== undefined || job.bridge !== null && typeof job.bridge === "object";
30421
30408
  }
30422
- async function readEvmHoldings(rpc, address) {
30423
- const [eth, usdg, weth] = await Promise.all([
30424
- rpc.getBalance(address),
30425
- rpc.erc20BalanceOf(toChecksumAddress(HOOD_USDG_ADDRESS), address),
30426
- rpc.erc20BalanceOf(HOOD_WETH_ADDRESS, address)
30427
- ]);
30428
- return { eth, usdg, weth };
30409
+ function bridgeStatusField(ctx, found, facts) {
30410
+ const job = found.job ?? {};
30411
+ if (found.kind !== "swap" || !isBridgeJob(job, facts))
30412
+ return {};
30413
+ const status = describeBridgeJob(job, facts);
30414
+ for (const line of status.lines)
30415
+ ctx.deps.stderr.write(`${line}
30416
+ `);
30417
+ const { lines: _lines, ...rest } = status;
30418
+ return { bridgeStatus: rest };
30429
30419
  }
30430
- function holdingsLines(address, holdings, observedAt) {
30431
- return [
30432
- `Holdings at ${address} on Hood (observed ${observedAt}):`,
30433
- ` ETH ${formatUnits(holdings.eth, NATIVE_DECIMALS)} (${holdings.eth} wei)`,
30434
- ` USDG ${formatUnits(holdings.usdg, HOOD_USDG_DECIMALS)} (${holdings.usdg} raw)`,
30435
- ` WETH ${formatUnits(holdings.weth, NATIVE_DECIMALS)} (${holdings.weth} raw)`,
30436
- " Other tokens are not listed: an EVM RPC has no token-account listing."
30437
- ];
30420
+ function waitFinished(current, facts, sent) {
30421
+ const job = current.job ?? {};
30422
+ if (current.kind === "swap" && isBridgeJob(job, facts)) {
30423
+ const status = describeBridgeJob(job, facts);
30424
+ if (sent && status.phase === "not_broadcast")
30425
+ return false;
30426
+ return bridgeStatusFinal(status);
30427
+ }
30428
+ const settlement = settlementSchema.safeParse(job.settlement);
30429
+ return !settlement.success || settlement.data.state !== "pending";
30438
30430
  }
30439
- async function appendScanStart(ctx, vaultPath, wallet, block2) {
30440
- const outcome = await appendEvmRecordEntry({
30441
- vaultPath,
30442
- entry: { kind: "scanStart", wallet, block: Number(block2) },
30443
- clock: ctx.deps
30444
- });
30445
- const repaired = appendNotice(outcome, `the scan start for ${wallet}`);
30446
- if (outcome.written) {
30447
- if (repaired !== undefined)
30448
- ctx.deps.stderr.write(`${repaired}
30449
- `);
30450
- return { recorded: true, block: block2.toString() };
30431
+ async function waitForSettlement(ctx, key, id, first, facts, sent = false) {
30432
+ const deadline = ctx.deps.now() + BRIDGE_WAIT_MS;
30433
+ let current = first;
30434
+ while (!waitFinished(current, facts, sent) && ctx.deps.now() < deadline) {
30435
+ await ctx.deps.sleep(BRIDGE_WAIT_POLL_MS);
30436
+ current = { ...current, ...operationSchema.parse(await request(ctx, key, jobPath(current.kind, id))) };
30451
30437
  }
30452
- ctx.deps.stderr.write(`The promote height, Hood block ${block2}, was not written to the sealed EVM record at ${evmRecordPath(vaultPath)} (${outcome.detail}).
30453
- ` + `Keep it: a later sweep of ${wallet} finds traded tokens from it with: candle tee sweep ${wallet} --from-block ${block2}
30438
+ const field = bridgeStatusField(ctx, current, facts);
30439
+ const finished = waitFinished(current, facts, sent);
30440
+ if (!finished)
30441
+ ctx.deps.stderr.write(`Still not final after ten minutes. Re-check with candle swap status ${id} --wait.
30454
30442
  `);
30455
- return { recorded: false, block: block2.toString() };
30443
+ return { ...current, ...field, waited: { final: finished } };
30456
30444
  }
30457
- async function appendEvmRecordForTrade(ctx, entry) {
30458
- let vaultPath;
30459
- try {
30460
- vaultPath = defaultVaultPath(ctx.deps.env, ctx.deps.homedir());
30461
- } catch (error) {
30462
- return { appended: false, notice: error instanceof Error ? error.message : String(error) };
30463
- }
30445
+ async function decimalsFor(ctx, asset, client) {
30446
+ if (BASES[asset])
30447
+ return BASES[asset].decimals;
30448
+ const reader = await client();
30449
+ let result;
30464
30450
  try {
30465
- const outcome = await appendEvmRecordEntry({ vaultPath, entry, clock: ctx.deps });
30466
- if (!outcome.written)
30467
- return { appended: false, notice: outcome.detail };
30468
- const repaired = appendNotice(outcome, `token ${entry.token}`);
30469
- return { appended: true, ...repaired !== undefined ? { notice: repaired } : {} };
30451
+ result = await tradingRead(ctx, reader, () => reader.rpc.getTokenSupply(asset));
30470
30452
  } catch (error) {
30471
- return { appended: false, notice: error instanceof Error ? error.message : String(error) };
30453
+ if (error instanceof TradingError)
30454
+ throw error;
30455
+ throw new TradingError("RPC_FAILED", "Solana getTokenSupply failed; no automatic re-send was made.");
30472
30456
  }
30457
+ const decimals = result.decimals;
30458
+ if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 18)
30459
+ throw new TradingError("INVALID_RESPONSE", "RPC returned invalid mint decimals.");
30460
+ return decimals;
30473
30461
  }
30474
- async function readHoodTeeServer(ctx, entry) {
30475
- if (entry.linkedWalletId === undefined) {
30476
- return {
30477
- lock: {
30478
- state: "unknown",
30479
- reason: "this wallet has no linked wallet id recorded",
30480
- suggestion: `Nothing was signed. This wallet has no server row, so its lock cannot be read. candle tee sweep ${entry.address} --emergency moves the funds and exits 3; it does not record sweptAt or retire the entry.`
30481
- }
30482
- };
30483
- }
30484
- const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
30485
- if (apiKey === undefined)
30486
- return { lock: { state: "unknown", reason: "no API key for this profile" } };
30462
+ async function assertDeferredExecuteSupported(ctx, key, id) {
30487
30463
  try {
30488
- const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/hood-tee`, {
30489
- auth: "key",
30490
- credentials: { apiKey },
30491
- apiUrl: ctx.apiUrl,
30492
- fetch: ctx.deps.fetch,
30493
- env: ctx.deps.env
30494
- });
30495
- if (!result.ok)
30496
- return { lock: { state: "unknown", reason: result.message ?? `HTTP ${result.status}` } };
30497
- const body = result.body ?? {};
30498
- const reported = typeof body.address === "string" ? body.address : undefined;
30499
- if (reported === undefined || !sameAddress(reported, entry.address)) {
30500
- return {
30501
- lock: {
30502
- state: "unknown",
30503
- reason: reported === undefined ? "the response named no wallet address" : `the response is for ${reported}, not this wallet`
30504
- }
30505
- };
30506
- }
30507
- const tradedTokens = Array.isArray(body.tradedTokens) ? body.tradedTokens.filter((token) => typeof token === "string" && /^0x[0-9a-fA-F]{40}$/.test(token)) : undefined;
30508
- const bridges = walletBridgesOf(result.body);
30509
- const extras = {
30510
- ...tradedTokens !== undefined ? { tradedTokens } : {},
30511
- ...body.tradedTokensTruncated === true ? { tradedTokensTruncated: true } : {},
30512
- ...bridges !== undefined ? { bridges } : {}
30513
- };
30514
- if (body.activeOperation === null)
30515
- return { lock: { state: "free" }, ...extras };
30516
- const op = body.activeOperation;
30517
- if (op === undefined || typeof op.operationId !== "string") {
30518
- return { lock: { state: "unknown", reason: "the response carried no activeOperation field" }, ...extras };
30519
- }
30520
- return {
30521
- lock: {
30522
- state: "held",
30523
- operationId: op.operationId,
30524
- kind: typeof op.kind === "string" ? op.kind : "operation",
30525
- expiresAt: typeof op.expiresAt === "number" ? op.expiresAt : 0
30526
- },
30527
- ...extras
30528
- };
30464
+ await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30529
30465
  } catch (error) {
30530
- return { lock: { state: "unknown", reason: error instanceof Error ? error.message : String(error) } };
30466
+ if (error instanceof TradingError && error.code === "JOB_NOT_FOUND")
30467
+ return;
30468
+ throw new TradingError("EMBEDDED_PAYER_UNSUPPORTED", "This Candle deployment cannot hold an embedded-wallet trade back for confirmation, so the quote could not be shown before the money moved. Nothing was built. Name a TEE wallet with --wallet, or point at a deployment that has it.");
30531
30469
  }
30470
+ throw new TradingError("INVALID_RESPONSE", "The execute route answered for a trade that does not exist.");
30532
30471
  }
30533
- function busyStatusHint(kind, operationId) {
30534
- if (kind === "swap")
30535
- return `candle swap status ${operationId}`;
30536
- if (kind === "trade" || kind === "launch")
30537
- return `candle swap status ${operationId} --kind ${kind}`;
30538
- return `the ${kind} operation ${operationId}`;
30539
- }
30540
- function assertWalletLockFree(entry, lock, now) {
30541
- if (lock.state === "free")
30542
- return;
30543
- if (lock.state === "held") {
30544
- const left = Math.max(0, Math.ceil((lock.expiresAt - now) / 60000));
30545
- throw new VaultError("WALLET_BUSY", `${entry.label || entry.address} has a sequenced ${lock.kind} in flight (operation ${lock.operationId}); signing locally now could use the nonce one of its legs holds.`, {
30546
- suggestion: `Nothing was signed. Wait for operation ${lock.operationId} to finish (${busyStatusHint(lock.kind, lock.operationId)}); the lock expires within ${left} minute(s) at most. If funds must move now, candle tee sweep ${entry.address} --emergency compares the chain's nonces instead.`,
30547
- details: { operationId: lock.operationId }
30548
- });
30549
- }
30550
- throw new VaultError("WALLET_LOCK_UNKNOWN", `Could not read whether ${entry.label || entry.address} has a sequenced operation in flight (${lock.reason}), so nothing is signed at its nonce.`, {
30551
- suggestion: lock.suggestion ?? `Nothing was signed. Restore the API key or connectivity and run it again. If the server is unreachable and funds must move: candle tee sweep ${entry.address} --emergency`
30472
+ async function swap(args, ctx) {
30473
+ const parsed = parseArgs(args, {
30474
+ valueFlags: ["--amount", "--percent", "--wallet", "--to", "--client-trade-id", "--slippage-bps", "--rpc-url"],
30475
+ booleanFlags: ["--yes", "--wait"]
30552
30476
  });
30553
- }
30554
- var init_evm_tee = __esm(() => {
30555
- init_bridge();
30556
- init_deps();
30557
- init_evm_lite();
30558
- init_errors();
30559
- init_evm_record();
30560
- init_promote_support();
30561
- init_store();
30562
- });
30563
-
30564
- // src/vault/evm-transfer.ts
30565
- function refuse2(code, message, opts = {}) {
30566
- return new VaultError(code, message, { suggestion: opts.suggestion ?? "Nothing was signed.", ...opts });
30567
- }
30568
- function checkEvmDestination(to, from) {
30569
- if (!to.startsWith("0x")) {
30570
- 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." });
30571
- }
30572
- const checked = checkEvmAddress(to);
30573
- if (!checked.ok) {
30574
- throw refuse2("EVM_DESTINATION_INVALID", `${to} is not a valid EVM address: ${checked.reason}.`);
30575
- }
30576
- if (sameEvmAddress(checked.address, from.address)) {
30577
- throw refuse2("EVM_SELF_TRANSFER", `${to} is ${from.label || from.address}'s own address.`);
30477
+ if ("error" in parsed) {
30478
+ writeUsageFailure(ctx.deps, parsed.error, ctx.json);
30479
+ return 2;
30578
30480
  }
30579
- return checked.address;
30580
- }
30581
- function assertSolanaDestination(to, from) {
30582
- if (!looksLikeEvmAddress(to))
30583
- return;
30584
- 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." });
30585
- }
30586
- async function resolveEvmAsset(rpc, asset, chainId) {
30587
- const upper = asset.toUpperCase();
30588
- if (upper === "ETH")
30589
- return { kind: "native", symbol: "ETH", decimals: NATIVE_DECIMALS };
30590
- let token;
30591
- if (upper === "USDG") {
30592
- if (chainId !== BigInt(HOOD_CHAIN_ID)) {
30593
- return {
30594
- 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.`
30595
- };
30596
- }
30597
- token = toChecksumAddress(HOOD_USDG_ADDRESS);
30598
- } else {
30599
- const checked = checkEvmAddress(asset);
30600
- if (!checked.ok)
30601
- return {
30602
- usage: `--asset must be ETH, USDG (on Hood), or an ERC-20 contract address: ${asset} is ${checked.reason}.`
30603
- };
30604
- token = checked.address;
30481
+ const flags = parsed.values;
30482
+ const id = flags["--client-trade-id"] ?? `swap-${randomUUID()}`;
30483
+ const slippage = Number(flags["--slippage-bps"] ?? "50");
30484
+ if (parsed.positionals.length !== 2 || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
30485
+ writeUsageFailure(ctx.deps, "Usage: candle swap <from> <to> --amount <decimal> | --percent <n> [--wallet <tee-or-embedded>] [--to <tee>] [--wait] [--client-trade-id <id>] [--slippage-bps 50] [--rpc-url <url>] [--yes]. Solana: SOL, USDC, CNDL or a mint. Hood: ETH, USDG or a 0x token. SOL or USDC to ETH or USDG, or back, is a bridge between two TEE wallets.", ctx.json);
30486
+ return 2;
30605
30487
  }
30606
- let decimals;
30607
30488
  try {
30608
- decimals = await rpc.erc20Decimals(token);
30609
- } catch (error) {
30610
- 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." });
30611
- }
30612
- const symbol = upper === "USDG" ? "USDG" : await rpc.erc20Symbol(token) ?? `${token.slice(0, 10)}…`;
30613
- return { kind: "erc20", symbol, decimals, token };
30614
- }
30615
- function nativeName(hood, chainId) {
30616
- return hood ? "ETH" : `ETH on chain ${chainId}`;
30617
- }
30618
- function evmDisplayLines(plan) {
30619
- const { tx, asset, hood, chainId } = plan;
30620
- const native = nativeName(hood, chainId);
30621
- const lines = [
30622
- `chain ${chainId}${hood ? " (Hood)" : ""}`,
30623
- `from ${plan.from.label} ${plan.from.address}`
30624
- ];
30625
- if (asset.kind === "native") {
30626
- lines.push(`to ${tx.to}`);
30627
- lines.push(`amount ${plan.amount} ${native} = ${plan.amountRaw} wei`);
30628
- } else {
30629
- lines.push(`to ${tx.to} (the ${asset.symbol} contract, ${asset.decimals} dp)`);
30630
- lines.push(`recipient ${plan.recipient} (decoded from transfer(address,uint256))`);
30631
- lines.push(`amount ${plan.amount} ${asset.symbol} = ${plan.amountRaw} raw`);
30632
- }
30633
- lines.push(`gas limit ${tx.gas}`);
30634
- lines.push(`max fee ${tx.maxFeePerGas} wei/gas (priority ${tx.maxPriorityFeePerGas} wei/gas)`);
30635
- lines.push(`fee cap ${formatUnits(plan.feeCap, NATIVE_DECIMALS)} ${native} (gas × max fee)`);
30636
- lines.push(`nonce ${tx.nonce}`);
30637
- return lines;
30638
- }
30639
- async function planEvmTransfer(rpc, input) {
30640
- const to = checkEvmDestination(input.to, input.from);
30641
- const chainId = await rpc.chainId();
30642
- if (input.builtIn && chainId !== BigInt(HOOD_CHAIN_ID)) {
30643
- throw refuse2("EVM_CHAIN_MISMATCH", `The built-in Hood RPC answered chain id ${chainId}, not ${HOOD_CHAIN_ID}.`, {
30644
- suggestion: "Nothing was signed. Pass --rpc-url for another chain; the built-in endpoint is Hood's only."
30645
- });
30646
- }
30647
- const hood = chainId === BigInt(HOOD_CHAIN_ID);
30648
- const asset = await resolveEvmAsset(rpc, input.asset, chainId);
30649
- if ("usage" in asset)
30650
- return asset;
30651
- const max = input.amount.toLowerCase() === "max";
30652
- if (asset.kind === "erc20" && asset.token !== undefined && sameEvmAddress(to, asset.token)) {
30653
- throw refuse2("EVM_RECIPIENT_IS_TOKEN", `${to} is the ${asset.symbol} contract itself; sending it its own tokens is a loss.`);
30654
- }
30655
- let amountRaw = 0n;
30656
- if (!max) {
30657
- const parsed = parseUnits(input.amount, asset.decimals);
30658
- if (!parsed.ok && parsed.reason === "precision") {
30659
- throw refuse2("EVM_AMOUNT_PRECISION", `${input.amount} has more decimal places than ${asset.symbol}'s ${asset.decimals}.`);
30660
- }
30661
- if (!parsed.ok)
30662
- return { usage: `--amount must be a positive decimal or max; ${input.amount} is neither.` };
30663
- amountRaw = parsed.raw;
30664
- }
30665
- const nonce = await rpc.getTransactionCount(input.from.address, "pending");
30666
- const fees = await quoteFees(rpc);
30667
- const nativeBalance = await rpc.getBalance(input.from.address);
30668
- if (asset.kind === "erc20" && asset.token !== undefined) {
30669
- if (max) {
30670
- amountRaw = await rpc.erc20BalanceOf(asset.token, input.from.address);
30671
- if (amountRaw === 0n) {
30672
- throw new VaultError("VAULT_INDEX_INVALID", `${input.from.label} holds no ${asset.symbol}; there is nothing to send.`);
30673
- }
30674
- }
30675
- const draft2 = buildErc20Transfer({
30676
- chainId,
30677
- nonce,
30678
- maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
30679
- maxFeePerGas: fees.maxFeePerGas,
30680
- gas: 0n,
30681
- token: asset.token,
30489
+ const fromAsset = classifyAsset(parsed.positionals[0]);
30490
+ const toAsset = classifyAsset(parsed.positionals[1]);
30491
+ const bridge = bridgePair(fromAsset, toAsset);
30492
+ if (bridge)
30493
+ return await bridgeSwap(ctx, {
30494
+ flags,
30495
+ yes: parsed.booleans.has("--yes"),
30496
+ wait: parsed.booleans.has("--wait"),
30497
+ id,
30498
+ slippage,
30499
+ ...bridge
30500
+ });
30501
+ if (flags["--to"] !== undefined || parsed.booleans.has("--wait"))
30502
+ throw new TradingUsage("--to and --wait are for a bridge: SOL or USDC to ETH or USDG, or back.");
30503
+ const chain2 = pairChain(fromAsset, toAsset);
30504
+ const walletFlag = flags["--wallet"];
30505
+ const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
30506
+ if (named !== undefined && named !== chain2)
30507
+ throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, chain2);
30508
+ if (chain2 === "hood")
30509
+ return await hoodSwap(ctx, {
30510
+ flags,
30511
+ yes: parsed.booleans.has("--yes"),
30512
+ id,
30513
+ slippage,
30514
+ from: fromAsset,
30515
+ to: toAsset
30516
+ });
30517
+ const from = fromAsset.asset;
30518
+ const to = toAsset.asset;
30519
+ if (from === to)
30520
+ throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
30521
+ const fromBase = baseAsset(from);
30522
+ const toBase = baseAsset(to);
30523
+ if (!fromBase && !toBase)
30524
+ throw new TradingError("PAIR_UNSUPPORTED", "A token trade must have SOL, USDC or CNDL on one side; token-to-token routing is unavailable.");
30525
+ if (flags["--amount"])
30526
+ rawAmount(flags["--amount"], 18);
30527
+ const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
30528
+ if (percent !== undefined && percent > 100000000n)
30529
+ throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
30530
+ const kind = fromBase && toBase ? "swap" : "trade";
30531
+ const key = await tradingKey(ctx);
30532
+ const prior = await lookupOperation(ctx, key, id, kind);
30533
+ if (prior)
30534
+ return printTradingResult(ctx, prior);
30535
+ const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
30536
+ if (payerWallet.kind === "embedded" && kind === "swap")
30537
+ throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
30538
+ const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
30539
+ const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
30540
+ const decimals = await decimalsFor(ctx, from, solana);
30541
+ const outDecimals = await decimalsFor(ctx, to, solana);
30542
+ let amountRaw;
30543
+ if (percent !== undefined) {
30544
+ const reader = await solana();
30545
+ let balance;
30546
+ if (from === "SOL")
30547
+ balance = await tradingRead(ctx, reader, () => reader.rpc.getBalance(wallet.address));
30548
+ else {
30549
+ const mint = BASES[from]?.mint ?? from;
30550
+ const accounts = (await tradingRead(ctx, reader, () => Promise.all([
30551
+ reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_PROGRAM_ID),
30552
+ reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_2022_PROGRAM_ID)
30553
+ ]))).flat();
30554
+ balance = accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
30555
+ }
30556
+ amountRaw = (balance * percent / 100000000n).toString();
30557
+ if (amountRaw === "0")
30558
+ throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
30559
+ } else
30560
+ amountRaw = rawAmount(flags["--amount"], decimals);
30561
+ if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
30562
+ throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
30563
+ if (payerWallet.kind === "embedded")
30564
+ await assertDeferredExecuteSupported(ctx, key, id);
30565
+ if (!await claimOperation(ctx, key, id, kind))
30566
+ throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30567
+ ctx.deps.stderr.write(`Operation: ${id}
30568
+ `);
30569
+ const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
30570
+ const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
30571
+ clientTradeId: id,
30572
+ from,
30573
+ to,
30574
+ amountRaw,
30575
+ maxSlippageBps: slippage,
30576
+ payer
30577
+ }) : await request(ctx, key, "/api/v1/trade/agent/build", {
30578
+ clientTradeId: id,
30579
+ chain: "solana",
30580
+ mint: fromBase ? to : from,
30581
+ side: fromBase ? "buy" : "sell",
30582
+ quoteAsset: (fromBase ?? toBase)?.toLowerCase(),
30583
+ amountRaw,
30584
+ maxSlippageBps: slippage,
30585
+ payer,
30586
+ ...payerWallet.kind === "embedded" ? { deferExecution: true } : {}
30587
+ });
30588
+ if (built.job || built.status === "executed")
30589
+ return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
30590
+ const data = swapBuildSchema.parse(kind === "swap" ? built.payload : built);
30591
+ if (kind === "swap" && (data.venue !== "jupiter" || data.recipient !== wallet.address))
30592
+ throw new TradingError("INVALID_RESPONSE", "A TEE base swap must use Jupiter and return to its payer.");
30593
+ if (kind === "trade" && (built.chain !== "solana" || built.walletAddress !== wallet.address))
30594
+ throw new TradingError("INVALID_RESPONSE", "The token build does not name the requested Solana payer.");
30595
+ const artifacts = kind === "swap" ? { ...data, transactionBase64: undefined, quoteSource: undefined, quoteAsset: undefined } : data.artifacts;
30596
+ if (!artifacts)
30597
+ throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
30598
+ if (kind === "trade" && artifacts.quoteAsset !== (fromBase ?? toBase)?.toLowerCase())
30599
+ throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not the requested pair. Nothing was signed.`);
30600
+ if (data?.status !== "built" || typeof data.minOutRaw !== "string" || !/^\d+$/.test(data.minOutRaw) || !data.fee || !Number.isFinite(data.fee.bps))
30601
+ throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete quote; nothing was signed.");
30602
+ const minimumRaw = !fromBase && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
30603
+ const quote = {
30604
+ intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from} to ${to}`,
30605
+ wallet: wallet.address,
30606
+ venue: artifacts.quoteSource ?? artifacts.venue,
30607
+ priceImpactPct: artifacts.priceImpactPct ?? null,
30608
+ fee: data.fee,
30609
+ minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to}`,
30610
+ minOutRaw: data.minOutRaw,
30611
+ minimumReceivedRaw: minimumRaw,
30612
+ tokenRisks: artifacts.tokenRisks ?? []
30613
+ };
30614
+ if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
30615
+ return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind, quote });
30616
+ if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
30617
+ throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
30618
+ if (payerWallet.kind === "embedded") {
30619
+ const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30620
+ return printTradingResult(ctx, {
30621
+ ...executed,
30622
+ clientTradeId: id,
30623
+ kind,
30624
+ quote,
30625
+ wallet: safeText(payerWallet.address)
30626
+ });
30627
+ }
30628
+ const transaction = kind === "swap" ? data.transactionsBase64?.[0] : artifacts.transactionBase64;
30629
+ if (kind === "swap" && data.transactionsBase64?.length !== 1)
30630
+ throw new TradingError("INVALID_RESPONSE", "A TEE swap must contain exactly one same-chain transaction.");
30631
+ if (!transaction)
30632
+ throw new TradingError("INVALID_RESPONSE", "Missing transaction.");
30633
+ const signed = await relaySign(ctx, key, payerWallet.wallet, transaction);
30634
+ const result = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", {
30635
+ clientTradeId: id,
30636
+ swapId: data.swapId,
30637
+ signedTransactionsBase64: [signed]
30638
+ }) : await request(ctx, key, "/api/v1/trade/agent/submit", { clientTradeId: id, signedTransactions: [signed] });
30639
+ const settled = kind === "swap" ? reportSettlement(ctx, result.payload?.settlement, { asset: to, decimals: outDecimals }, id) : {};
30640
+ return printTradingResult(ctx, {
30641
+ ...result,
30642
+ clientTradeId: id,
30643
+ kind,
30644
+ quote,
30645
+ wallet: safeText(wallet.address),
30646
+ ...settled
30647
+ });
30648
+ } catch (error) {
30649
+ return tradingFailure(ctx, error, id);
30650
+ }
30651
+ }
30652
+ function lazyEvmRpc(ctx, flag) {
30653
+ let rpc;
30654
+ return () => {
30655
+ if (rpc)
30656
+ return rpc;
30657
+ const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
30658
+ if ("error" in resolved)
30659
+ throw new TradingUsage(resolved.error);
30660
+ ctx.deps.stderr.write(`Reading from ${rpcHostOf2(resolved.url)} (Hood RPC; reads only, nothing is sent there)
30661
+ `);
30662
+ rpc = createEvmRpc(resolved.url, ctx.deps.fetch);
30663
+ return rpc;
30664
+ };
30665
+ }
30666
+ async function evmRead(what, read) {
30667
+ try {
30668
+ return await read();
30669
+ } catch (error) {
30670
+ if (error instanceof TradingError || error instanceof TradingUsage)
30671
+ throw error;
30672
+ throw new TradingError("RPC_FAILED", `Reading ${what} over the Hood RPC failed; nothing was built or signed.`);
30673
+ }
30674
+ }
30675
+ async function hoodDecimals(asset, rpc) {
30676
+ const base = asset.base ? HOOD_BASES[asset.base] : undefined;
30677
+ if (base)
30678
+ return base.decimals;
30679
+ const decimals = await evmRead(`${asset.asset} decimals()`, () => rpc().erc20Decimals(asset.asset));
30680
+ if (decimals > 36)
30681
+ throw new TradingError("INVALID_RESPONSE", "The token's decimals() is out of range.");
30682
+ return decimals;
30683
+ }
30684
+ async function recordTradedToken(ctx, wallet, token) {
30685
+ const skipped = (reason) => `Notice: the sealed EVM record was not updated for ${token} (${reason}). The leg landed. A later sweep still finds this token with --token ${token}, or with --from-block.`;
30686
+ const append = ctx.deps.appendEvmRecord;
30687
+ let notice;
30688
+ if (!append)
30689
+ notice = skipped("this CLI build has no sealed EVM record writer");
30690
+ else {
30691
+ try {
30692
+ const outcome = await append(ctx, { kind: "token", wallet: toChecksumAddress(wallet), token });
30693
+ notice = outcome.appended ? outcome.notice : skipped(outcome.notice);
30694
+ } catch (error) {
30695
+ notice = skipped(error instanceof Error ? error.message : "the append failed");
30696
+ }
30697
+ }
30698
+ if (notice)
30699
+ ctx.deps.stderr.write(`${safeText(notice)}
30700
+ `);
30701
+ return notice;
30702
+ }
30703
+ function describeHoodLegs(first, hasFee, primary = "trade") {
30704
+ const kinds = plannedLegKinds(first.legKind, first.plannedLegCount, hasFee, primary);
30705
+ const names = {
30706
+ approval: "approve",
30707
+ permit2Approval: "Permit2 approve",
30708
+ trade: "trade",
30709
+ feeTransfer: "fee",
30710
+ bridgeDeposit: "bridge deposit"
30711
+ };
30712
+ return kinds ? kinds.map((kind) => names[kind] ?? kind) : [`${first.plannedLegCount} legs, starting with ${names[first.legKind] ?? first.legKind}`];
30713
+ }
30714
+ async function hoodSwap(ctx, args) {
30715
+ const { flags, id, from, to } = args;
30716
+ if (from.asset === to.asset)
30717
+ throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
30718
+ if (!from.base && !to.base)
30719
+ throw new TradingError("PAIR_UNSUPPORTED", "A Hood token trade must have ETH or USDG on one side; token-to-token routing is unavailable.");
30720
+ if (flags["--amount"])
30721
+ rawAmount(flags["--amount"], 18);
30722
+ const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
30723
+ if (percent !== undefined && percent > 100000000n)
30724
+ throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
30725
+ const kind = from.base && to.base ? "swap" : "trade";
30726
+ const key = await tradingKey(ctx);
30727
+ const prior = await lookupOperation(ctx, key, id, kind);
30728
+ if (prior)
30729
+ return printTradingResult(ctx, prior);
30730
+ const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
30731
+ if (payer.kind === "embedded" && kind === "swap")
30732
+ throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap ETH and USDG from this command: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a Hood TEE wallet.");
30733
+ const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
30734
+ const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
30735
+ const decimals = await hoodDecimals(from, rpc);
30736
+ const outDecimals = await hoodDecimals(to, rpc);
30737
+ let amountRaw;
30738
+ if (percent !== undefined) {
30739
+ const balance = from.asset === "ETH" ? await evmRead("the ETH balance", () => rpc().getBalance(payerAddress)) : await evmRead(`the ${from.asset} balance`, () => rpc().erc20BalanceOf(from.asset === "USDG" ? HOOD_USDG_ADDRESS : from.asset, payerAddress));
30740
+ amountRaw = (balance * percent / 100000000n).toString();
30741
+ if (amountRaw === "0")
30742
+ throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
30743
+ } else
30744
+ amountRaw = rawAmount(flags["--amount"], decimals);
30745
+ if (payer.kind === "embedded")
30746
+ await assertDeferredExecuteSupported(ctx, key, id);
30747
+ if (!await claimOperation(ctx, key, id, kind))
30748
+ throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30749
+ ctx.deps.stderr.write(`Operation: ${id}
30750
+ `);
30751
+ const payerBody = payer.kind === "tee" ? { type: "linked", linkedWalletId: payer.wallet.id } : { type: "main" };
30752
+ const base = from.base ?? to.base;
30753
+ const token = from.base ? to.asset : from.asset;
30754
+ const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
30755
+ clientTradeId: id,
30756
+ from: from.asset,
30757
+ to: to.asset,
30758
+ amountRaw,
30759
+ maxSlippageBps: args.slippage,
30760
+ payer: payerBody
30761
+ }) : await request(ctx, key, "/api/v1/trade/agent/build", {
30762
+ clientTradeId: id,
30763
+ chain: "hood",
30764
+ mint: token,
30765
+ side: from.base ? "buy" : "sell",
30766
+ quoteAsset: base.toLowerCase(),
30767
+ amountRaw,
30768
+ maxSlippageBps: args.slippage,
30769
+ payer: payerBody,
30770
+ ...payer.kind === "embedded" ? { deferExecution: true } : {}
30771
+ });
30772
+ if (built.job || built.status === "executed")
30773
+ return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
30774
+ const body = kind === "swap" ? built.payload : built;
30775
+ const data = swapBuildSchema.parse(body);
30776
+ const echoed = kind === "swap" ? body?.recipient : body?.walletAddress;
30777
+ if (body?.chain !== "hood" || typeof echoed !== "string" || echoed.toLowerCase() !== payerAddress.toLowerCase())
30778
+ throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
30779
+ const artifacts = kind === "swap" ? { ...data, quoteAsset: undefined, quoteSource: undefined } : data.artifacts;
30780
+ if (!artifacts)
30781
+ throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
30782
+ if (kind === "trade" && artifacts.quoteAsset !== base.toLowerCase())
30783
+ throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not ${base}. Nothing was signed.`);
30784
+ let sequenced;
30785
+ if (payer.kind === "tee") {
30786
+ const parsed = sequencedSchema.safeParse(body);
30787
+ if (!parsed.success)
30788
+ throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet trades one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
30789
+ sequenced = parsed.data;
30790
+ }
30791
+ const minimumRaw = !from.base && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
30792
+ const quote = {
30793
+ intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from.asset} to ${to.asset} on Hood`,
30794
+ wallet: payerAddress,
30795
+ venue: artifacts.quoteSource ?? artifacts.venue,
30796
+ priceImpactPct: artifacts.priceImpactPct ?? null,
30797
+ fee: data.fee,
30798
+ minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to.asset}`,
30799
+ minOutRaw: data.minOutRaw,
30800
+ minimumReceivedRaw: minimumRaw,
30801
+ tokenRisks: artifacts.tokenRisks ?? []
30802
+ };
30803
+ if (sequenced) {
30804
+ const leg = sequenced.nextLeg;
30805
+ const maxFee = BigInt(leg.maxFeePerGas);
30806
+ const reserve = sweepReserveFloor(maxFee, kind === "trade" ? [token] : []);
30807
+ quote.legs = describeHoodLegs(sequenced, BigInt(data.fee.feeRaw) > 0n);
30808
+ quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
30809
+ quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a trade never spends it`;
30810
+ quote.operationId = sequenced.operationId;
30811
+ }
30812
+ if (!await confirmQuote(ctx, quote, args.yes)) {
30813
+ if (sequenced)
30814
+ ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}; a new Hood trade from it is refused as WALLET_BUSY until then.
30815
+ `);
30816
+ return printTradingResult(ctx, {
30817
+ success: true,
30818
+ status: "cancelled",
30819
+ clientTradeId: id,
30820
+ kind,
30821
+ quote,
30822
+ ...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
30823
+ });
30824
+ }
30825
+ if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
30826
+ throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
30827
+ if (payer.kind === "embedded") {
30828
+ const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
30829
+ return printTradingResult(ctx, { ...executed, clientTradeId: id, kind, quote, wallet: safeText(payerAddress) });
30830
+ }
30831
+ const wallet = payer.wallet;
30832
+ if (wallet.chain !== "evm" || !sequenced)
30833
+ throw chainMismatch(`TEE wallet ${wallet.id}`, wallet.chain === "evm" ? "hood" : "solana", "hood");
30834
+ const recorded = kind === "trade" ? token : HOOD_USDG_ADDRESS;
30835
+ const notices = [];
30836
+ const run = await runSequencedLegs(ctx, key, {
30837
+ wallet,
30838
+ first: sequenced,
30839
+ submitPath: kind === "swap" ? "/api/v1/agent/swap/submit" : "/api/v1/trade/agent/submit",
30840
+ submitFields: kind === "swap" ? { clientTradeId: id, swapId: data.swapId } : { clientTradeId: id },
30841
+ unwrap: (answer) => kind === "swap" ? answer.payload ?? {} : answer,
30842
+ clientId: id,
30843
+ kind,
30844
+ allowedLegs: HOOD_TRADE_LEGS,
30845
+ onLanded: async (_leg) => {
30846
+ const notice = await recordTradedToken(ctx, wallet.address, toChecksumAddress(recorded));
30847
+ if (notice)
30848
+ notices.push(notice);
30849
+ }
30850
+ });
30851
+ const settled = kind === "swap" ? reportSettlement(ctx, run.final.settlement, { asset: to.asset, decimals: outDecimals }, id) : {};
30852
+ return printTradingResult(ctx, {
30853
+ ...run.final,
30854
+ ...settled,
30855
+ clientTradeId: id,
30856
+ kind,
30857
+ chain: "hood",
30858
+ quote,
30859
+ wallet: safeText(wallet.address),
30860
+ operationId: sequenced.operationId,
30861
+ landedLegs: run.landed,
30862
+ evmRecord: { token: toChecksumAddress(recorded), notices }
30863
+ });
30864
+ }
30865
+ async function bridgeOriginBalance(ctx, from, address, solana, evm) {
30866
+ if (from === "ETH")
30867
+ return evmRead("the ETH balance", () => evm().getBalance(address));
30868
+ if (from === "USDG")
30869
+ return evmRead("the USDG balance", () => evm().erc20BalanceOf(HOOD_USDG_ADDRESS, address));
30870
+ const reader = await solana();
30871
+ if (from === "SOL")
30872
+ return tradingRead(ctx, reader, () => reader.rpc.getBalance(address));
30873
+ const mint = BASES.USDC?.mint;
30874
+ const accounts = (await tradingRead(ctx, reader, () => Promise.all([
30875
+ reader.rpc.getTokenAccountsByOwner(address, TOKEN_PROGRAM_ID),
30876
+ reader.rpc.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID)
30877
+ ]))).flat();
30878
+ return accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
30879
+ }
30880
+ async function bridgeSwap(ctx, args) {
30881
+ const { flags, id, from, to } = args;
30882
+ const origin = BRIDGE_ASSETS[from].chain;
30883
+ const destination = BRIDGE_ASSETS[to].chain;
30884
+ const walletFlag = flags["--wallet"];
30885
+ const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
30886
+ if (named !== undefined && named !== origin)
30887
+ throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, origin);
30888
+ if (flags["--amount"])
30889
+ rawAmount(flags["--amount"], 18);
30890
+ const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
30891
+ if (percent !== undefined && percent > 100000000n)
30892
+ throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
30893
+ const key = await tradingKey(ctx);
30894
+ const prior = await lookupOperation(ctx, key, id, "swap");
30895
+ if (prior)
30896
+ return printTradingResult(ctx, {
30897
+ ...prior,
30898
+ ...bridgeStatusField(ctx, prior, bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge))
30899
+ });
30900
+ const payer = await tradingPayer(ctx, key, walletFlag, "swap:write", origin);
30901
+ if (payer.kind !== "tee")
30902
+ throw new TradingError("CHAIN_MISMATCH", `Only a TEE wallet bridges, and the embedded wallet ${safeText(payer.address)} is not one. Name a ${chainName(origin)} TEE wallet with --wallet. Nothing was built.`);
30903
+ const wallet = payer.wallet;
30904
+ const { rows } = await listTradingWallets(ctx, key, "swap:write");
30905
+ const onDestination = rows.filter((row) => rowChain(row) === destination);
30906
+ const toFlag = flags["--to"];
30907
+ let toWalletId;
30908
+ let candidates = onDestination.filter((row) => row.active);
30909
+ if (toFlag !== undefined) {
30910
+ const matches = rows.filter((row) => matchesName(row, toFlag));
30911
+ const match = matches[0];
30912
+ if (matches.length > 1)
30913
+ throw new TradingError("TEE_WALLET_REQUIRED", `"${safeText(toFlag)}" matches ${matches.length} TEE wallets on this key: ${matches.map(describeWallet).join("; ")}. Name one by id or address.`);
30914
+ if (!match || rowChain(match) !== destination)
30915
+ throw new TradingError("BRIDGE_DESTINATION_MISSING", `--to ${safeText(toFlag)} is not a ${chainName(destination)} TEE wallet on this key. A bridge lands only in this key's own TEE wallet on ${chainName(destination)}${onDestination.length > 0 ? `: ${onDestination.map(describeWallet).join("; ")}` : ""}. Nothing was built.`);
30916
+ if (!match.active)
30917
+ throw new TradingError("TEE_WALLET_INACTIVE", `The destination ${describeWallet(match)} is not a verified-active TEE wallet. Nothing was built.`);
30918
+ toWalletId = match.id;
30919
+ candidates = [match];
30920
+ } else if (candidates.length !== 1) {
30921
+ throw new TradingError("BRIDGE_DESTINATION_MISSING", candidates.length === 0 ? `This key has no active ${chainName(destination)} TEE wallet to bridge into. Promote one onto this key (candle vault promote), then bridge. Nothing was built.` : `This key has more than one ${chainName(destination)} TEE wallet; name the destination with --to: ${candidates.map(describeWallet).join("; ")}. Nothing was built.`);
30922
+ }
30923
+ const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
30924
+ const evm = lazyEvmRpc(ctx, flags["--rpc-url"]);
30925
+ const decimals = BRIDGE_ASSETS[from].decimals;
30926
+ let amountRaw;
30927
+ if (percent !== undefined) {
30928
+ const balance = await bridgeOriginBalance(ctx, from, wallet.address, solana, evm);
30929
+ amountRaw = (balance * percent / 100000000n).toString();
30930
+ if (amountRaw === "0")
30931
+ throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
30932
+ } else
30933
+ amountRaw = rawAmount(flags["--amount"], decimals);
30934
+ if (!await claimOperation(ctx, key, id, "swap"))
30935
+ throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
30936
+ ctx.deps.stderr.write(`Operation: ${id}
30937
+ `);
30938
+ const built = await request(ctx, key, "/api/v1/agent/swap/build", {
30939
+ clientTradeId: id,
30940
+ from,
30941
+ to,
30942
+ amountRaw,
30943
+ maxSlippageBps: args.slippage,
30944
+ payer: { type: "linked", linkedWalletId: wallet.id },
30945
+ ...toWalletId !== undefined ? { toWalletId } : {}
30946
+ });
30947
+ if (built.job || built.status === "executed")
30948
+ return printTradingResult(ctx, { ...built, clientTradeId: id, kind: "swap" });
30949
+ const body = built.payload ?? {};
30950
+ const parsedBuild = bridgeBuildSchema.safeParse(body);
30951
+ if (!parsedBuild.success)
30952
+ throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete bridge quote; nothing was signed.");
30953
+ const data = parsedBuild.data;
30954
+ if (data.venue !== "relay")
30955
+ throw new TradingError("INVALID_RESPONSE", `A bridge goes through Relay, not ${safeText(data.venue)}; nothing was signed.`);
30956
+ if (data.fee.bps !== 0 || data.fee.feeRaw !== "0")
30957
+ throw new TradingError("INVALID_RESPONSE", "A bridge carries no Candle fee, and this quote has one; nothing was signed.");
30958
+ const recipient = candidates.find((row) => destination === "hood" ? sameEvmAddress(row.address, data.recipient) : row.address === data.recipient);
30959
+ if (!recipient)
30960
+ throw new TradingError("INVALID_RESPONSE", `Candle named ${safeText(data.recipient)} as the destination, which is not ${toWalletId !== undefined ? "the TEE wallet --to named" : `this key's ${chainName(destination)} TEE wallet`}; nothing was signed.`);
30961
+ const facts = {
30962
+ from,
30963
+ to,
30964
+ recipient: recipient.address,
30965
+ ...data.statusChecks[0] !== undefined ? { statusCheck: data.statusChecks[0] } : {}
30966
+ };
30967
+ await saveOperationBridge(ctx, key, id, { ...facts });
30968
+ const refused = (problem) => new TradingError("RELAY_STEP_REFUSED", `Relay's deposit did not pass this machine's check (${problem}); nothing was signed.`);
30969
+ let sequenced;
30970
+ let transaction;
30971
+ const hoodOrigin = from === "ETH" || from === "USDG" ? from : undefined;
30972
+ if (hoodOrigin) {
30973
+ const parsedLeg = sequencedSchema.safeParse(body);
30974
+ if (!parsedLeg.success)
30975
+ throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet bridges one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
30976
+ sequenced = parsedLeg.data;
30977
+ if (typeof data.walletAddress !== "string" || !sameEvmAddress(data.walletAddress, wallet.address))
30978
+ throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
30979
+ const plan = plannedLegKinds(sequenced.legKind, sequenced.plannedLegCount, false, "bridgeDeposit");
30980
+ if (!bridgePlanAdmitted(hoodOrigin, plan))
30981
+ throw refused(`the plan is ${sequenced.plannedLegCount} leg(s) starting with ${sequenced.legKind}`);
30982
+ const problem = relayHoodLegProblem(sequenced.legKind, sequenced.nextLeg, {
30983
+ origin: hoodOrigin,
30984
+ payer: wallet.address,
30985
+ amountRaw
30986
+ });
30987
+ if (problem)
30988
+ throw refused(problem);
30989
+ } else {
30990
+ if (data.transactionsBase64?.length !== 1)
30991
+ throw refused(`Candle returned ${data.transactionsBase64?.length ?? 0} transactions, not one deposit`);
30992
+ transaction = data.transactionsBase64[0];
30993
+ const reader = await solana();
30994
+ const problem = await tradingRead(ctx, reader, () => relaySolanaDepositProblem(transaction, wallet.address, reader.rpc));
30995
+ if (problem)
30996
+ throw refused(problem);
30997
+ }
30998
+ const outDecimals = BRIDGE_ASSETS[to].decimals;
30999
+ const minimumRaw = (BigInt(data.expectedOutRaw) * BigInt(1e4 - args.slippage) / 10000n).toString();
31000
+ const quote = {
31001
+ intent: `Bridge ${decimalAmount(amountRaw, decimals)} ${from} on ${chainName(origin)} to ${to} on ${chainName(destination)}`,
31002
+ wallet: wallet.address,
31003
+ venue: "relay",
31004
+ priceImpactPct: null,
31005
+ minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to} (Relay's estimate ${decimalAmount(data.expectedOutRaw, outDecimals)} ${to}, less the ${args.slippage} bps slippage bound)`,
31006
+ minimumReceivedRaw: minimumRaw,
31007
+ expectedOutRaw: data.expectedOutRaw,
31008
+ destination: `${recipient.label ? `${recipient.label} ` : ""}${recipient.address} (${chainName(destination)} TEE wallet on this key)`,
31009
+ candleFee: "none",
31010
+ relayFees: data.venueCostUsd !== undefined ? `about $${data.venueCostUsd} as Relay reports it` : "not reported",
31011
+ estimatedTime: data.venueTimeEstimateSec !== undefined ? `about ${data.venueTimeEstimateSec}s as Relay reports it` : "not reported",
31012
+ tokenRisks: []
31013
+ };
31014
+ if (sequenced) {
31015
+ const leg = sequenced.nextLeg;
31016
+ const maxFee = BigInt(leg.maxFeePerGas);
31017
+ const reserve = sweepReserveFloor(maxFee, []);
31018
+ quote.legs = describeHoodLegs(sequenced, false, "bridgeDeposit");
31019
+ quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
31020
+ quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a bridge never spends it`;
31021
+ quote.operationId = sequenced.operationId;
31022
+ }
31023
+ if (!await confirmQuote(ctx, quote, args.yes)) {
31024
+ if (sequenced)
31025
+ ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}; a new Hood trade or sweep from it waits until then.
31026
+ `);
31027
+ return printTradingResult(ctx, {
31028
+ success: true,
31029
+ status: "cancelled",
31030
+ clientTradeId: id,
31031
+ kind: "swap",
31032
+ quote,
31033
+ ...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
31034
+ });
31035
+ }
31036
+ if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
31037
+ throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
31038
+ let result;
31039
+ let depositHash;
31040
+ if (sequenced && hoodOrigin) {
31041
+ const run = await runSequencedLegs(ctx, key, {
31042
+ wallet,
31043
+ first: sequenced,
31044
+ submitPath: "/api/v1/agent/swap/submit",
31045
+ submitFields: { clientTradeId: id, swapId: data.swapId },
31046
+ unwrap: (answer) => answer.payload ?? {},
31047
+ clientId: id,
31048
+ kind: "swap",
31049
+ primaryLeg: "bridgeDeposit",
31050
+ allowedLegs: bridgeLegKinds(hoodOrigin),
31051
+ checkLeg: (kind, leg) => relayHoodLegProblem(kind, leg, { origin: hoodOrigin, payer: wallet.address, amountRaw }) === null,
31052
+ onLanded: async () => {}
31053
+ });
31054
+ depositHash = run.landed.find((leg) => leg.kind === "bridgeDeposit")?.hash;
31055
+ result = {
31056
+ ...run.final,
31057
+ chain: "hood",
31058
+ operationId: sequenced.operationId,
31059
+ landedLegs: run.landed
31060
+ };
31061
+ } else {
31062
+ const signed = await relaySign(ctx, key, wallet, transaction);
31063
+ const submitted = await request(ctx, key, "/api/v1/agent/swap/submit", {
31064
+ clientTradeId: id,
31065
+ swapId: data.swapId,
31066
+ signedTransactionsBase64: [signed]
31067
+ });
31068
+ const payload = submitted.payload ?? {};
31069
+ const hashes = Array.isArray(payload.hashes) ? payload.hashes.filter((h) => typeof h === "string") : [];
31070
+ depositHash = hashes[0];
31071
+ result = submitted;
31072
+ }
31073
+ ctx.deps.stderr.write(`Deposit ${depositHash ? safeText(depositHash) : "sent"}: filling.${args.wait ? "" : ` Check it with candle swap status ${id} (add --wait to follow it).`}
31074
+ `);
31075
+ const receipt = {
31076
+ ...result,
31077
+ clientTradeId: id,
31078
+ kind: "swap",
31079
+ bridge: { from, to, destination: recipient.address, ...depositHash ? { depositHash } : {} },
31080
+ quote,
31081
+ wallet: safeText(wallet.address)
31082
+ };
31083
+ if (!args.wait)
31084
+ return printTradingResult(ctx, receipt);
31085
+ try {
31086
+ const found = await lookupOperation(ctx, key, id, "swap");
31087
+ if (!found)
31088
+ return printTradingResult(ctx, { ...receipt, waited: { final: false } });
31089
+ const waited = await waitForSettlement(ctx, key, id, found, facts, true);
31090
+ return printTradingResult(ctx, {
31091
+ ...receipt,
31092
+ job: waited.job,
31093
+ bridgeStatus: waited.bridgeStatus,
31094
+ waited: waited.waited
31095
+ });
31096
+ } catch (error) {
31097
+ const reason = safeText(error instanceof Error ? describeRpcFailure(error) : String(error));
31098
+ ctx.deps.stderr.write(`Could not follow the bridge (${reason}). The deposit was sent; do not send it again. Re-check with candle swap status ${id} --wait.
31099
+ `);
31100
+ return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
31101
+ }
31102
+ }
31103
+ var settlementSchema, bridgeFactsSchema, HOOD_TRADE_LEGS, bridgeBuildSchema;
31104
+ var init_swap = __esm(() => {
31105
+ init_zod();
31106
+ init_args();
31107
+ init_bridge();
31108
+ init_evm_lite();
31109
+ init_profiles();
31110
+ init_render();
31111
+ init_solana_endpoint();
31112
+ init_solana_lite();
31113
+ init_trading();
31114
+ settlementSchema = exports_external.object({
31115
+ state: exports_external.enum(["settled", "pending", "failed", "uncertain"]),
31116
+ settledOutRaw: exports_external.string().regex(/^\d+$/).optional()
31117
+ });
31118
+ bridgeFactsSchema = exports_external.object({
31119
+ from: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
31120
+ to: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
31121
+ recipient: exports_external.string(),
31122
+ statusCheck: exports_external.string().optional()
31123
+ });
31124
+ HOOD_TRADE_LEGS = ["approval", "permit2Approval", "trade", "feeTransfer"];
31125
+ bridgeBuildSchema = exports_external.object({
31126
+ status: exports_external.literal("built"),
31127
+ swapId: exports_external.string().min(1),
31128
+ venue: exports_external.string(),
31129
+ fee: exports_external.object({ bps: exports_external.number(), feeRaw: exports_external.string() }).passthrough(),
31130
+ expectedOutRaw: exports_external.string().regex(/^\d+$/),
31131
+ expiresAt: exports_external.number().finite(),
31132
+ recipient: exports_external.string().min(1),
31133
+ statusChecks: exports_external.array(exports_external.string()).default([]),
31134
+ transactionsBase64: exports_external.array(exports_external.string()).optional(),
31135
+ walletAddress: exports_external.string().optional(),
31136
+ venueCostUsd: exports_external.number().finite().optional(),
31137
+ venueTimeEstimateSec: exports_external.number().finite().optional()
31138
+ }).passthrough();
31139
+ });
31140
+
31141
+ // src/vault/evm-sweep.ts
31142
+ function sameToken(a, b) {
31143
+ return a.toLowerCase() === b.toLowerCase();
31144
+ }
31145
+ function mergedTokens(sources) {
31146
+ const out = [];
31147
+ const seen = new Set;
31148
+ for (const token of [
31149
+ ...sources.always,
31150
+ ...sources.flags,
31151
+ ...sources.record.tokens,
31152
+ ...sources.server.tokens,
31153
+ ...sources.logs.tokens
31154
+ ]) {
31155
+ const key = token.toLowerCase();
31156
+ if (seen.has(key))
31157
+ continue;
31158
+ seen.add(key);
31159
+ out.push(toChecksumAddress(token));
31160
+ }
31161
+ return out;
31162
+ }
31163
+ function alwaysTokens() {
31164
+ return [toChecksumAddress(HOOD_USDG_ADDRESS), HOOD_WETH_ADDRESS];
31165
+ }
31166
+ function recordSource(read, wallet) {
31167
+ const mine = read.entries.filter((entry) => sameToken(entry.wallet, wallet));
31168
+ return {
31169
+ tokens: mine.flatMap((entry) => entry.kind === "token" ? [entry.token] : []),
31170
+ scanStarts: mine.flatMap((entry) => entry.kind === "scanStart" ? [BigInt(entry.block)] : []),
31171
+ absent: read.absent,
31172
+ noKey: read.noKey,
31173
+ unreadableLines: read.unreadableLines,
31174
+ partialTail: read.partialTail,
31175
+ path: read.path
31176
+ };
31177
+ }
31178
+ function startBlock(scanStarts, fromBlockFlag) {
31179
+ const recorded = scanStarts.length > 0 ? scanStarts.reduce((a, b) => a < b ? a : b) : undefined;
31180
+ if (recorded === undefined && fromBlockFlag === undefined)
31181
+ return;
31182
+ if (recorded === undefined)
31183
+ return { block: fromBlockFlag, source: "--from-block" };
31184
+ if (fromBlockFlag === undefined)
31185
+ return { block: recorded, source: "record" };
31186
+ return { block: recorded < fromBlockFlag ? recorded : fromBlockFlag, source: "record and --from-block" };
31187
+ }
31188
+ async function discoverTransferLogs(rpc, wallet, fromBlock, toBlock, opts = {}) {
31189
+ const seen = new Map;
31190
+ let page = opts.pageBlocks ?? LOG_PAGE_BLOCKS;
31191
+ let pages = 0;
31192
+ let from = fromBlock;
31193
+ const topics = [ERC20_TRANSFER_TOPIC, null, addressTopic(wallet)];
31194
+ while (from <= toBlock) {
31195
+ const to = from + page - 1n > toBlock ? toBlock : from + page - 1n;
31196
+ try {
31197
+ const logs = await rpc.getLogs({ fromBlock: from, toBlock: to, topics });
31198
+ pages += 1;
31199
+ for (const log of logs)
31200
+ seen.set(log.address.toLowerCase(), log.address);
31201
+ from = to + 1n;
31202
+ } catch (error) {
31203
+ if (error instanceof EvmRpcError && error.kind === "rpc" && page > LOG_PAGE_FLOOR) {
31204
+ page = page / 2n < LOG_PAGE_FLOOR ? LOG_PAGE_FLOOR : page / 2n;
31205
+ continue;
31206
+ }
31207
+ const reason = error instanceof Error ? error.message : String(error);
31208
+ return { failed: `eth_getLogs from block ${from} failed: ${reason}`, tokens: [...seen.values()], pages };
31209
+ }
31210
+ }
31211
+ return { tokens: [...seen.values()], pages };
31212
+ }
31213
+ function sourceLines(sources, host) {
31214
+ const lines = ["Token sources:", " USDG and WETH: always read"];
31215
+ const record = sources.record;
31216
+ if (record.noKey && record.absent) {
31217
+ lines.push(` sealed EVM record: none (this vault has no record key)`);
31218
+ } else if (record.absent) {
31219
+ lines.push(` sealed EVM record: absent at ${record.path} (not on this machine, or nothing recorded yet)`);
31220
+ } else {
31221
+ const damage = [
31222
+ ...record.unreadableLines > 0 ? [`${record.unreadableLines} unreadable line(s) skipped`] : [],
31223
+ ...record.partialTail ? ["a partial last line skipped"] : []
31224
+ ];
31225
+ lines.push(` sealed EVM record: ${record.tokens.length} token(s), ${record.scanStarts.length} scan start(s)${damage.length > 0 ? `; ${damage.join(", ")}` : ""}`);
31226
+ }
31227
+ const server = sources.server;
31228
+ if (!server.asked)
31229
+ lines.push(` server traded-token list: not asked (${server.reason ?? "--emergency calls no Candle API"})`);
31230
+ else if (!server.answered)
31231
+ lines.push(` server traded-token list: FAILED (${server.reason ?? "no answer"})`);
31232
+ else
31233
+ lines.push(` server traded-token list: ${server.tokens.length} token(s)${server.truncated ? " (truncated)" : ""}`);
31234
+ lines.push(` --token: ${sources.flags.length}`);
31235
+ const logs = sources.logs;
31236
+ if (logs.ran && !logs.failed) {
31237
+ lines.push(` Transfer logs over ${host}: blocks ${logs.fromBlock} to ${logs.toBlock} (start from ${logs.startSource}), ${logs.tokens.length} contract(s)`);
31238
+ } else if (logs.failed) {
31239
+ lines.push(` Transfer logs over ${host}: FAILED (${logs.reason})`);
31240
+ } else {
31241
+ lines.push(` Transfer logs: NOT scanned (${logs.reason})`);
31242
+ }
31243
+ if (logs.ran && logs.fromBlock !== undefined) {
31244
+ lines.push(` A token received before block ${logs.fromBlock}, or from a contract that emits no Transfer log, still needs --token <0x...>.`);
31245
+ }
31246
+ return lines;
31247
+ }
31248
+ var LOG_PAGE_BLOCKS = 10000n, LOG_PAGE_FLOOR = 250n;
31249
+ var init_evm_sweep = __esm(() => {
31250
+ init_evm_lite();
31251
+ });
31252
+
31253
+ // src/vault/evm-tee.ts
31254
+ function resolveHoodClient(ctx, flag) {
31255
+ const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
31256
+ if ("error" in resolved)
31257
+ return resolved;
31258
+ return {
31259
+ rpc: createEvmRpc(resolved.url, ctx.deps.fetch),
31260
+ url: resolved.url,
31261
+ host: rpcHostOf2(resolved.url),
31262
+ builtIn: resolved.builtIn
31263
+ };
31264
+ }
31265
+ function hoodHostLine(client, what) {
31266
+ return `Reading ${what} from ${client.host}${client.builtIn ? " (the built-in Hood RPC)" : ""}.`;
31267
+ }
31268
+ async function assertHoodChain(client) {
31269
+ const chainId = await client.rpc.chainId();
31270
+ if (chainId !== BigInt(HOOD_CHAIN_ID)) {
31271
+ throw new VaultError("EVM_CHAIN_MISMATCH", `${client.host} answered chain id ${chainId}; Hood TEE wallets live on Hood, chain id ${HOOD_CHAIN_ID}, only.`, { suggestion: "Nothing was signed or written. Point --rpc-url (or CANDLE_EVM_RPC_URL) at a Hood RPC." });
31272
+ }
31273
+ }
31274
+ async function readEvmHoldings(rpc, address) {
31275
+ const [eth, usdg, weth] = await Promise.all([
31276
+ rpc.getBalance(address),
31277
+ rpc.erc20BalanceOf(toChecksumAddress(HOOD_USDG_ADDRESS), address),
31278
+ rpc.erc20BalanceOf(HOOD_WETH_ADDRESS, address)
31279
+ ]);
31280
+ return { eth, usdg, weth };
31281
+ }
31282
+ function holdingsLines(address, holdings, observedAt) {
31283
+ return [
31284
+ `Holdings at ${address} on Hood (observed ${observedAt}):`,
31285
+ ` ETH ${formatUnits(holdings.eth, NATIVE_DECIMALS)} (${holdings.eth} wei)`,
31286
+ ` USDG ${formatUnits(holdings.usdg, HOOD_USDG_DECIMALS)} (${holdings.usdg} raw)`,
31287
+ ` WETH ${formatUnits(holdings.weth, NATIVE_DECIMALS)} (${holdings.weth} raw)`,
31288
+ " Other tokens are not listed: an EVM RPC has no token-account listing."
31289
+ ];
31290
+ }
31291
+ async function appendScanStart(ctx, vaultPath, wallet, block2) {
31292
+ const outcome = await appendEvmRecordEntry({
31293
+ vaultPath,
31294
+ entry: { kind: "scanStart", wallet, block: Number(block2) },
31295
+ clock: ctx.deps
31296
+ });
31297
+ const repaired = appendNotice(outcome, `the scan start for ${wallet}`);
31298
+ if (outcome.written) {
31299
+ if (repaired !== undefined)
31300
+ ctx.deps.stderr.write(`${repaired}
31301
+ `);
31302
+ return { recorded: true, block: block2.toString() };
31303
+ }
31304
+ ctx.deps.stderr.write(`The promote height, Hood block ${block2}, was not written to the sealed EVM record at ${evmRecordPath(vaultPath)} (${outcome.detail}).
31305
+ ` + `Keep it: a later sweep of ${wallet} finds traded tokens from it with: candle tee sweep ${wallet} --from-block ${block2}
31306
+ `);
31307
+ return { recorded: false, block: block2.toString() };
31308
+ }
31309
+ async function appendEvmRecordForTrade(ctx, entry) {
31310
+ let vaultPath;
31311
+ try {
31312
+ vaultPath = defaultVaultPath(ctx.deps.env, ctx.deps.homedir());
31313
+ } catch (error) {
31314
+ return { appended: false, notice: error instanceof Error ? error.message : String(error) };
31315
+ }
31316
+ try {
31317
+ const outcome = await appendEvmRecordEntry({ vaultPath, entry, clock: ctx.deps });
31318
+ if (!outcome.written)
31319
+ return { appended: false, notice: outcome.detail };
31320
+ const repaired = appendNotice(outcome, `token ${entry.token}`);
31321
+ return { appended: true, ...repaired !== undefined ? { notice: repaired } : {} };
31322
+ } catch (error) {
31323
+ return { appended: false, notice: error instanceof Error ? error.message : String(error) };
31324
+ }
31325
+ }
31326
+ async function readHoodTeeServer(ctx, entry) {
31327
+ if (entry.linkedWalletId === undefined) {
31328
+ return {
31329
+ lock: {
31330
+ state: "unknown",
31331
+ reason: "this wallet has no linked wallet id recorded",
31332
+ suggestion: `Nothing was signed. This wallet has no server row, so its lock cannot be read. candle tee sweep ${entry.address} --emergency moves the funds and exits 3; it does not record sweptAt or retire the entry.`
31333
+ }
31334
+ };
31335
+ }
31336
+ const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
31337
+ if (apiKey === undefined)
31338
+ return { lock: { state: "unknown", reason: "no API key for this profile" } };
31339
+ try {
31340
+ const result = await apiRequest(`/api/v1/agent/wallets/${encodeURIComponent(entry.linkedWalletId)}/hood-tee`, {
31341
+ auth: "key",
31342
+ credentials: { apiKey },
31343
+ apiUrl: ctx.apiUrl,
31344
+ fetch: ctx.deps.fetch,
31345
+ env: ctx.deps.env
31346
+ });
31347
+ if (!result.ok)
31348
+ return { lock: { state: "unknown", reason: result.message ?? `HTTP ${result.status}` } };
31349
+ const body = result.body ?? {};
31350
+ const reported = typeof body.address === "string" ? body.address : undefined;
31351
+ if (reported === undefined || !sameAddress(reported, entry.address)) {
31352
+ return {
31353
+ lock: {
31354
+ state: "unknown",
31355
+ reason: reported === undefined ? "the response named no wallet address" : `the response is for ${reported}, not this wallet`
31356
+ }
31357
+ };
31358
+ }
31359
+ const tradedTokens = Array.isArray(body.tradedTokens) ? body.tradedTokens.filter((token) => typeof token === "string" && /^0x[0-9a-fA-F]{40}$/.test(token)) : undefined;
31360
+ const bridges = walletBridgesOf(result.body);
31361
+ const extras = {
31362
+ ...tradedTokens !== undefined ? { tradedTokens } : {},
31363
+ ...body.tradedTokensTruncated === true ? { tradedTokensTruncated: true } : {},
31364
+ ...bridges !== undefined ? { bridges } : {}
31365
+ };
31366
+ if (body.activeOperation === null)
31367
+ return { lock: { state: "free" }, ...extras };
31368
+ const op = body.activeOperation;
31369
+ if (op === undefined || typeof op.operationId !== "string") {
31370
+ return { lock: { state: "unknown", reason: "the response carried no activeOperation field" }, ...extras };
31371
+ }
31372
+ return {
31373
+ lock: {
31374
+ state: "held",
31375
+ operationId: op.operationId,
31376
+ kind: typeof op.kind === "string" ? op.kind : "operation",
31377
+ expiresAt: typeof op.expiresAt === "number" ? op.expiresAt : 0
31378
+ },
31379
+ ...extras
31380
+ };
31381
+ } catch (error) {
31382
+ return { lock: { state: "unknown", reason: error instanceof Error ? error.message : String(error) } };
31383
+ }
31384
+ }
31385
+ function busyStatusHint(kind, operationId) {
31386
+ if (kind === "swap")
31387
+ return `candle swap status ${operationId}`;
31388
+ if (kind === "trade" || kind === "launch")
31389
+ return `candle swap status ${operationId} --kind ${kind}`;
31390
+ return `the ${kind} operation ${operationId}`;
31391
+ }
31392
+ function assertWalletLockFree(entry, lock, now) {
31393
+ if (lock.state === "free")
31394
+ return;
31395
+ if (lock.state === "held") {
31396
+ const left = Math.max(0, Math.ceil((lock.expiresAt - now) / 60000));
31397
+ throw new VaultError("WALLET_BUSY", `${entry.label || entry.address} has a sequenced ${lock.kind} in flight (operation ${lock.operationId}); signing locally now could use the nonce one of its legs holds.`, {
31398
+ suggestion: `Nothing was signed. Wait for operation ${lock.operationId} to finish (${busyStatusHint(lock.kind, lock.operationId)}); the lock expires within ${left} minute(s) at most. If funds must move now, candle tee sweep ${entry.address} --emergency compares the chain's nonces instead.`,
31399
+ details: { operationId: lock.operationId }
31400
+ });
31401
+ }
31402
+ throw new VaultError("WALLET_LOCK_UNKNOWN", `Could not read whether ${entry.label || entry.address} has a sequenced operation in flight (${lock.reason}), so nothing is signed at its nonce.`, {
31403
+ suggestion: lock.suggestion ?? `Nothing was signed. Restore the API key or connectivity and run it again. If the server is unreachable and funds must move: candle tee sweep ${entry.address} --emergency`
31404
+ });
31405
+ }
31406
+ var init_evm_tee = __esm(() => {
31407
+ init_bridge();
31408
+ init_deps();
31409
+ init_evm_lite();
31410
+ init_errors();
31411
+ init_evm_record();
31412
+ init_promote_support();
31413
+ init_store();
31414
+ });
31415
+
31416
+ // src/vault/evm-transfer.ts
31417
+ function refuse2(code, message, opts = {}) {
31418
+ return new VaultError(code, message, { suggestion: opts.suggestion ?? "Nothing was signed.", ...opts });
31419
+ }
31420
+ function checkEvmDestination(to, from) {
31421
+ if (!to.startsWith("0x")) {
31422
+ 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." });
31423
+ }
31424
+ const checked = checkEvmAddress(to);
31425
+ if (!checked.ok) {
31426
+ throw refuse2("EVM_DESTINATION_INVALID", `${to} is not a valid EVM address: ${checked.reason}.`);
31427
+ }
31428
+ if (sameEvmAddress(checked.address, from.address)) {
31429
+ throw refuse2("EVM_SELF_TRANSFER", `${to} is ${from.label || from.address}'s own address.`);
31430
+ }
31431
+ return checked.address;
31432
+ }
31433
+ function assertSolanaDestination(to, from) {
31434
+ if (!looksLikeEvmAddress(to))
31435
+ return;
31436
+ 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." });
31437
+ }
31438
+ async function resolveEvmAsset(rpc, asset, chainId) {
31439
+ const upper = asset.toUpperCase();
31440
+ if (upper === "ETH")
31441
+ return { kind: "native", symbol: "ETH", decimals: NATIVE_DECIMALS };
31442
+ let token;
31443
+ if (upper === "USDG") {
31444
+ if (chainId !== BigInt(HOOD_CHAIN_ID)) {
31445
+ return {
31446
+ 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.`
31447
+ };
31448
+ }
31449
+ token = toChecksumAddress(HOOD_USDG_ADDRESS);
31450
+ } else {
31451
+ const checked = checkEvmAddress(asset);
31452
+ if (!checked.ok)
31453
+ return {
31454
+ usage: `--asset must be ETH, USDG (on Hood), or an ERC-20 contract address: ${asset} is ${checked.reason}.`
31455
+ };
31456
+ token = checked.address;
31457
+ }
31458
+ let decimals;
31459
+ try {
31460
+ decimals = await rpc.erc20Decimals(token);
31461
+ } catch (error) {
31462
+ 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." });
31463
+ }
31464
+ const symbol = upper === "USDG" ? "USDG" : await rpc.erc20Symbol(token) ?? `${token.slice(0, 10)}…`;
31465
+ return { kind: "erc20", symbol, decimals, token };
31466
+ }
31467
+ function nativeName(hood, chainId) {
31468
+ return hood ? "ETH" : `ETH on chain ${chainId}`;
31469
+ }
31470
+ function evmDisplayLines(plan) {
31471
+ const { tx, asset, hood, chainId } = plan;
31472
+ const native = nativeName(hood, chainId);
31473
+ const lines = [
31474
+ `chain ${chainId}${hood ? " (Hood)" : ""}`,
31475
+ `from ${plan.from.label} ${plan.from.address}`
31476
+ ];
31477
+ if (asset.kind === "native") {
31478
+ lines.push(`to ${tx.to}`);
31479
+ lines.push(`amount ${plan.amount} ${native} = ${plan.amountRaw} wei`);
31480
+ } else {
31481
+ lines.push(`to ${tx.to} (the ${asset.symbol} contract, ${asset.decimals} dp)`);
31482
+ lines.push(`recipient ${plan.recipient} (decoded from transfer(address,uint256))`);
31483
+ lines.push(`amount ${plan.amount} ${asset.symbol} = ${plan.amountRaw} raw`);
31484
+ }
31485
+ lines.push(`gas limit ${tx.gas}`);
31486
+ lines.push(`max fee ${tx.maxFeePerGas} wei/gas (priority ${tx.maxPriorityFeePerGas} wei/gas)`);
31487
+ lines.push(`fee cap ${formatUnits(plan.feeCap, NATIVE_DECIMALS)} ${native} (gas × max fee)`);
31488
+ lines.push(`nonce ${tx.nonce}`);
31489
+ return lines;
31490
+ }
31491
+ async function planEvmTransfer(rpc, input) {
31492
+ const to = checkEvmDestination(input.to, input.from);
31493
+ const chainId = await rpc.chainId();
31494
+ if (input.builtIn && chainId !== BigInt(HOOD_CHAIN_ID)) {
31495
+ throw refuse2("EVM_CHAIN_MISMATCH", `The built-in Hood RPC answered chain id ${chainId}, not ${HOOD_CHAIN_ID}.`, {
31496
+ suggestion: "Nothing was signed. Pass --rpc-url for another chain; the built-in endpoint is Hood's only."
31497
+ });
31498
+ }
31499
+ const hood = chainId === BigInt(HOOD_CHAIN_ID);
31500
+ const asset = await resolveEvmAsset(rpc, input.asset, chainId);
31501
+ if ("usage" in asset)
31502
+ return asset;
31503
+ const max = input.amount.toLowerCase() === "max";
31504
+ if (asset.kind === "erc20" && asset.token !== undefined && sameEvmAddress(to, asset.token)) {
31505
+ throw refuse2("EVM_RECIPIENT_IS_TOKEN", `${to} is the ${asset.symbol} contract itself; sending it its own tokens is a loss.`);
31506
+ }
31507
+ let amountRaw = 0n;
31508
+ if (!max) {
31509
+ const parsed = parseUnits(input.amount, asset.decimals);
31510
+ if (!parsed.ok && parsed.reason === "precision") {
31511
+ throw refuse2("EVM_AMOUNT_PRECISION", `${input.amount} has more decimal places than ${asset.symbol}'s ${asset.decimals}.`);
31512
+ }
31513
+ if (!parsed.ok)
31514
+ return { usage: `--amount must be a positive decimal or max; ${input.amount} is neither.` };
31515
+ amountRaw = parsed.raw;
31516
+ }
31517
+ const nonce = await rpc.getTransactionCount(input.from.address, "pending");
31518
+ const fees = await quoteFees(rpc);
31519
+ const nativeBalance = await rpc.getBalance(input.from.address);
31520
+ if (asset.kind === "erc20" && asset.token !== undefined) {
31521
+ if (max) {
31522
+ amountRaw = await rpc.erc20BalanceOf(asset.token, input.from.address);
31523
+ if (amountRaw === 0n) {
31524
+ throw new VaultError("VAULT_INDEX_INVALID", `${input.from.label} holds no ${asset.symbol}; there is nothing to send.`);
31525
+ }
31526
+ }
31527
+ const draft2 = buildErc20Transfer({
31528
+ chainId,
31529
+ nonce,
31530
+ maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
31531
+ maxFeePerGas: fees.maxFeePerGas,
31532
+ gas: 0n,
31533
+ token: asset.token,
30682
31534
  recipient: to,
30683
31535
  amount: amountRaw
30684
31536
  });
@@ -31128,6 +31980,8 @@ async function resolveKeyMaterial(keyFile, chain2, ctx) {
31128
31980
  return { ok: false, message: `Could not read --key-file: ${error instanceof Error ? error.message : error}` };
31129
31981
  }
31130
31982
  }
31983
+ if (!ctx.deps.isTTY.stdin)
31984
+ return { ok: false, message: "No TTY available for private key input; pass --key-file instead" };
31131
31985
  try {
31132
31986
  const promptText = chain2 === "solana" ? "Solana private key (base58 or id.json contents; input hidden): " : "EVM private key (hex; input hidden): ";
31133
31987
  const entered = (await ctx.deps.promptSecret(promptText)).trim();
@@ -32244,6 +33098,26 @@ var init_tee_evm = __esm(() => {
32244
33098
  init_wallets();
32245
33099
  });
32246
33100
 
33101
+ // src/commands/tee-status-wallet.ts
33102
+ async function serverTeeWallet(ctx, address, chain2) {
33103
+ const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
33104
+ const suggestion = "Pass --verify to read it from this machine's vault, or select the profile holding its key.";
33105
+ if (!apiKey)
33106
+ throw new TradingError("TEE_WALLET_NOT_ON_KEY", `${address}: no API key available.`, { suggestion });
33107
+ const { rows } = await listTradingWallets(ctx, apiKey);
33108
+ const row = rows.find((candidate) => candidate.chain === chain2 && (chain2 === "evm" ? sameEvmAddress(candidate.address, address) : candidate.address === address));
33109
+ if (!row)
33110
+ throw new TradingError("TEE_WALLET_NOT_ON_KEY", `${address} is not a TEE wallet bound to key ${apiKeyPrefix(apiKey) ?? "this key"}.`, { suggestion });
33111
+ return { row, apiKey };
33112
+ }
33113
+ var UNVERIFIED_KEY_LINE = "Key not verified on this machine. Pass --verify to open the vault and check it.";
33114
+ var init_tee_status_wallet = __esm(() => {
33115
+ init_deps();
33116
+ init_evm_lite();
33117
+ init_profiles();
33118
+ init_trading();
33119
+ });
33120
+
32247
33121
  // src/commands/tee-status-evm.ts
32248
33122
  async function readVaultEntry(ctx, address) {
32249
33123
  const resolved = vaultPathFor(ctx, { values: {}, booleans: new Set, positionals: [] });
@@ -32282,9 +33156,10 @@ async function teeStatusEvm(ctx, parsed, address) {
32282
33156
  writeUsageFailure(deps, rpcUrl.error, json);
32283
33157
  return 2;
32284
33158
  }
33159
+ const verify = parsed.booleans.has("--verify");
32285
33160
  let vault;
32286
33161
  try {
32287
- vault = await readVaultEntry(ctx, checked.address);
33162
+ vault = verify ? await readVaultEntry(ctx, checked.address) : null;
32288
33163
  } catch (error) {
32289
33164
  if (error instanceof TradingError && error.code === "USAGE") {
32290
33165
  writeUsageFailure(deps, error.message, json);
@@ -32296,11 +33171,21 @@ async function teeStatusEvm(ctx, parsed, address) {
32296
33171
  }
32297
33172
  throw error;
32298
33173
  }
33174
+ let serverWallet = null;
33175
+ if (!verify) {
33176
+ try {
33177
+ serverWallet = await serverTeeWallet(ctx, address, "evm");
33178
+ } catch (error) {
33179
+ return tradingFailure(ctx, error);
33180
+ }
33181
+ }
32299
33182
  const report = {
32300
33183
  address: checked.address,
32301
33184
  chain: "hood",
32302
33185
  label: vault?.label ?? null,
32303
33186
  source: vault ? "vault" : "server",
33187
+ verified: verify,
33188
+ ...!verify ? { localState: null, retainedSweepReceipts: null, pendingSweepTransactions: null, vaultLifecycle: null } : {},
32304
33189
  linkedWalletId: vault?.linkedWalletId ?? null,
32305
33190
  vaultDestination: vault?.vaultDestination ?? null,
32306
33191
  observedAt: new Date(deps.now()).toISOString()
@@ -32310,7 +33195,7 @@ async function teeStatusEvm(ctx, parsed, address) {
32310
33195
  report.server = { error: "no API key available; server state not read" };
32311
33196
  else {
32312
33197
  try {
32313
- const { rows } = await listTradingWallets(ctx, apiKey);
33198
+ const rows = serverWallet ? [serverWallet.row] : (await listTradingWallets(ctx, apiKey)).rows;
32314
33199
  const row = rows.find((candidate) => candidate.chain === "evm" && sameEvmAddress(candidate.address, address));
32315
33200
  if (row) {
32316
33201
  report.linkedWalletId ??= row.id;
@@ -32377,6 +33262,9 @@ async function teeStatusEvm(ctx, parsed, address) {
32377
33262
  deps.stdout.write(` chain Hood (4663)
32378
33263
  `);
32379
33264
  deps.stdout.write(` source ${report.source}
33265
+ `);
33266
+ if (!verify)
33267
+ deps.stdout.write(`${UNVERIFIED_KEY_LINE}
32380
33268
  `);
32381
33269
  if (vault?.vaultDestination)
32382
33270
  deps.stdout.write(` vault ${vault.vaultDestination}
@@ -32417,6 +33305,8 @@ var init_tee_status_evm = __esm(() => {
32417
33305
  init_trading();
32418
33306
  init_errors();
32419
33307
  init_store();
33308
+ init_swap();
33309
+ init_tee_status_wallet();
32420
33310
  init_vault_support();
32421
33311
  });
32422
33312
 
@@ -33082,43 +33972,59 @@ async function readLifecycle(ctx, apiKey, linkedWalletId) {
33082
33972
  }
33083
33973
  async function teeStatus(args, ctx) {
33084
33974
  const { deps, json } = ctx;
33085
- if (!refuseEnvPassphrase2(ctx))
33086
- return 1;
33087
- const parsed = parseArgs(args, { valueFlags: ["--rpc-url", "--keystore"], pathFlags: ["--keystore"] });
33975
+ const parsed = parseArgs(args, {
33976
+ valueFlags: ["--rpc-url", "--keystore"],
33977
+ pathFlags: ["--keystore"],
33978
+ booleanFlags: ["--verify"]
33979
+ });
33088
33980
  if ("error" in parsed)
33089
33981
  return usage2(ctx, parsed.error);
33982
+ const verify = parsed.booleans.has("--verify");
33983
+ if (verify && !refuseEnvPassphrase2(ctx))
33984
+ return 1;
33090
33985
  const [address, extra] = parsed.positionals;
33091
33986
  if (!address || extra !== undefined)
33092
- return usage2(ctx, "Usage: candle tee status <address> [--rpc-url <url>]");
33987
+ return usage2(ctx, "Usage: candle tee status <address> [--rpc-url <url>] [--verify]");
33093
33988
  if (/^0x/i.test(address))
33094
33989
  return await teeStatusEvm(ctx, parsed, address);
33095
33990
  const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
33096
33991
  if ("error" in solana)
33097
33992
  return usage2(ctx, solana.error);
33098
- const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
33099
- if (!resolved.ok)
33100
- return resolved.code;
33993
+ let local = null;
33994
+ let entry;
33995
+ if (verify) {
33996
+ const resolved = await resolveTeeAddress(ctx, parsed, address, () => openExistingTeeStore(ctx, parsed));
33997
+ if (!resolved.ok)
33998
+ return resolved.code;
33999
+ local = resolved.resolved;
34000
+ entry = local.source === "vault" ? local.legacyView : local.entry;
34001
+ } else {
34002
+ try {
34003
+ const { row } = await serverTeeWallet(ctx, address, "solana");
34004
+ entry = { address, label: row.label ?? "", linkedWalletId: row.id };
34005
+ } catch (error) {
34006
+ return tradingFailure(ctx, error);
34007
+ }
34008
+ }
33101
34009
  try {
33102
- if (resolved.resolved.source === "vault") {
33103
- const reconciled = await maybeReconcileVaultTee(ctx, resolved.resolved);
34010
+ if (local?.source === "vault") {
34011
+ const reconciled = await maybeReconcileVaultTee(ctx, local);
33104
34012
  if (reconciled.code !== null)
33105
34013
  return reconciled.code;
33106
34014
  }
33107
- const entry = resolved.resolved.source === "vault" ? resolved.resolved.legacyView : resolved.resolved.entry;
33108
34015
  const report = {
33109
34016
  address,
33110
34017
  label: entry.label,
33111
- source: resolved.resolved.source,
34018
+ source: local?.source ?? "server",
34019
+ verified: verify,
33112
34020
  linkedWalletId: entry.linkedWalletId ?? null,
33113
- vaultDestination: entry.tee?.vaultDestination ?? null,
33114
- localState: entry.tee?.sweptAt ? "swept" : entry.tee?.stopRequestedAt ? "stop-requested" : entry.linkedWalletId ? "enabled" : "local-only",
33115
- retainedSweepReceipts: entry.tee?.sweepReceipts?.length ?? 0,
33116
- pendingSweepTransactions: entry.tee?.sweepPending?.length ?? 0,
34021
+ vaultDestination: verify ? entry.tee?.vaultDestination ?? null : null,
34022
+ vaultLifecycle: local?.source === "vault" ? local.entry.tee?.lifecycle ?? null : null,
34023
+ localState: !verify ? null : entry.tee?.sweptAt ? "swept" : entry.tee?.stopRequestedAt ? "stop-requested" : entry.linkedWalletId ? "enabled" : "local-only",
34024
+ retainedSweepReceipts: verify ? entry.tee?.sweepReceipts?.length ?? 0 : null,
34025
+ pendingSweepTransactions: verify ? entry.tee?.sweepPending?.length ?? 0 : null,
33117
34026
  observedAt: new Date(deps.now()).toISOString()
33118
34027
  };
33119
- if (resolved.resolved.source === "vault") {
33120
- report.vaultLifecycle = resolved.resolved.entry.tee?.lifecycle ?? null;
33121
- }
33122
34028
  if (entry.linkedWalletId) {
33123
34029
  const apiKey = await resolveApiKey(deps, ctx.profile);
33124
34030
  if (apiKey) {
@@ -33164,13 +34070,17 @@ async function teeStatus(args, ctx) {
33164
34070
  }
33165
34071
  deps.stdout.write(`${address} ${entry.label}
33166
34072
  `);
33167
- if (resolved.resolved.source === "vault") {
34073
+ if (local?.source === "vault") {
33168
34074
  deps.stdout.write(` source vault (Phase 1 store is read-only for this address)
33169
34075
  `);
33170
- deps.stdout.write(` vault life ${resolved.resolved.entry.tee?.lifecycle ?? "?"}
34076
+ deps.stdout.write(` vault life ${local.entry.tee?.lifecycle ?? "?"}
33171
34077
  `);
33172
34078
  }
33173
- deps.stdout.write(` local state ${report.localState}
34079
+ if (!verify)
34080
+ deps.stdout.write(`${UNVERIFIED_KEY_LINE}
34081
+ `);
34082
+ else
34083
+ deps.stdout.write(` local state ${report.localState}
33174
34084
  `);
33175
34085
  if (entry.tee?.vaultDestination)
33176
34086
  deps.stdout.write(` vault ${entry.tee.vaultDestination}
@@ -33206,7 +34116,8 @@ async function teeStatus(args, ctx) {
33206
34116
  `);
33207
34117
  return 0;
33208
34118
  } finally {
33209
- releaseResolvedTee(resolved.resolved);
34119
+ if (local)
34120
+ releaseResolvedTee(local);
33210
34121
  }
33211
34122
  }
33212
34123
  async function teeDisable(args, ctx) {
@@ -34233,8 +35144,10 @@ var init_tee = __esm(() => {
34233
35144
  init_wallet_import_flow();
34234
35145
  init_wallet_keygen();
34235
35146
  init_wallet_keystore();
35147
+ init_swap();
34236
35148
  init_tee_evm();
34237
35149
  init_tee_status_evm();
35150
+ init_tee_status_wallet();
34238
35151
  init_wallets();
34239
35152
  StoreChangedError = class StoreChangedError extends Error {
34240
35153
  };
@@ -34400,7 +35313,7 @@ function missingSignerFailure(missing) {
34400
35313
  message: `The target key has a signer, so the owner of ${missing.length === 1 ? "this wallet" : "these wallets"} must change with the binding, and this machine does not hold the current owner. Nothing changed.
34401
35314
  ${missing.map((m) => ` ${m.walletId}: on ${m.where}`).join(`
34402
35315
  `)}`,
34403
- suggestion: "Run the same rebind on the machine named for each wallet, with the device token there."
35316
+ suggestion: "Run the same rebind on the machine named for each wallet. If that is a trading machine with no login, sign in there on a temporary profile for the move: https://docs.candle.tv/developers/cli-headless#moving-wallets-onto-a-key-that-has-a-signer"
34404
35317
  };
34405
35318
  }
34406
35319
  function rebindFailureDetails(result, context) {
@@ -34750,7 +35663,7 @@ function effectiveEmbeddedWallet(value) {
34750
35663
  return value === "denied" ? "denied" : "allowed";
34751
35664
  }
34752
35665
  function embeddedWalletLine(permission, keyPrefix) {
34753
- return permission === "allowed" ? "Embedded wallet: allowed. This key may trade, launch and transfer from the account's embedded wallet, with Candle signing." : `Embedded wallet: denied. This key cannot spend the account's embedded wallet; the owner can allow it with: candle keys update ${keyPrefix} --embedded-wallet allow`;
35666
+ return permission === "allowed" ? "Embedded wallet: allowed. This key may use the account's embedded wallet, with Candle signing, for what its scopes allow: trades with swap:write, launches with launch:write, transfers only with transfer:write." : `Embedded wallet: denied. This key cannot spend the account's embedded wallet; the owner can allow it with: candle keys update ${keyPrefix} --embedded-wallet allow`;
34754
35667
  }
34755
35668
  function keyName(keyPrefix, label) {
34756
35669
  const cleaned = labelCell(label ?? undefined);
@@ -34791,7 +35704,7 @@ async function confirmCreateAllow(ctx) {
34791
35704
  writeLocalFailure(deps, ALLOW_REQUIRES_TTY, json);
34792
35705
  return 1;
34793
35706
  }
34794
- deps.stderr.write(`This key will be allowed to trade, launch and transfer from the account's embedded wallet, with Candle signing.
35707
+ deps.stderr.write(`This key will be allowed to use the account's embedded wallet, with Candle signing, for what its scopes allow: trades with swap:write, launches with launch:write, transfers only with transfer:write.
34795
35708
  ` + `A key that only trades its own TEE wallets does not need it.
34796
35709
  `);
34797
35710
  const answer = await deps.promptLine("Create the key with the embedded wallet allowed? [y/N] ");
@@ -34890,7 +35803,7 @@ async function keysUpdate(args, ctx) {
34890
35803
  `API ${apiUrl} (${candleEnvironment(apiUrl) ?? "not a Candle host"})`,
34891
35804
  "",
34892
35805
  `Embedded wallet ${shown.from} -> ${shown.to} (${shown.direction})`,
34893
- widen ? "It can trade, launch and transfer from the account's embedded wallet as soon as you confirm." : "It stops spending the account's embedded wallet from its next request.",
35806
+ widen ? "It can use the account's embedded wallet for what its scopes allow: trades with swap:write, launches with launch:write, transfers only with transfer:write." : "It stops spending the account's embedded wallet from its next request.",
34894
35807
  "",
34895
35808
  ""
34896
35809
  ].join(`
@@ -39338,8 +40251,8 @@ var require_checkpoint = __commonJS((exports) => {
39338
40251
  const split2 = envelope.indexOf(CHECKPOINT_SEPARATOR);
39339
40252
  const header = envelope.slice(0, split2 + 1);
39340
40253
  const data = envelope.slice(split2 + CHECKPOINT_SEPARATOR.length);
39341
- const matches = data.matchAll(SIGNATURE_REGEX);
39342
- const signatures = Array.from(matches, (match) => {
40254
+ const matches2 = data.matchAll(SIGNATURE_REGEX);
40255
+ const signatures = Array.from(matches2, (match) => {
39343
40256
  const [, name2, signature] = match;
39344
40257
  const sigBytes = Buffer.from(signature, "base64");
39345
40258
  if (sigBytes.length < 5) {
@@ -43413,11 +44326,11 @@ var require_validate2 = __commonJS((exports) => {
43413
44326
  jsonPointer = $data;
43414
44327
  data = names_1.default.rootData;
43415
44328
  } else {
43416
- const matches2 = RELATIVE_JSON_POINTER.exec($data);
43417
- if (!matches2)
44329
+ const matches3 = RELATIVE_JSON_POINTER.exec($data);
44330
+ if (!matches3)
43418
44331
  throw new Error(`Invalid JSON-pointer: ${$data}`);
43419
- const up = +matches2[1];
43420
- jsonPointer = matches2[2];
44332
+ const up = +matches3[1];
44333
+ jsonPointer = matches3[2];
43421
44334
  if (jsonPointer === "#") {
43422
44335
  if (up >= dataLevel)
43423
44336
  throw new Error(errorMsg("property/index", up));
@@ -44094,11 +45007,11 @@ var require_schemes = __commonJS((exports, module) => {
44094
45007
  urnComponent.error = "URN can not be parsed";
44095
45008
  return urnComponent;
44096
45009
  }
44097
- const matches2 = urnComponent.path.match(URN_REG);
44098
- if (matches2) {
45010
+ const matches3 = urnComponent.path.match(URN_REG);
45011
+ if (matches3) {
44099
45012
  const scheme = options.scheme || urnComponent.scheme || "urn";
44100
- urnComponent.nid = matches2[1].toLowerCase();
44101
- urnComponent.nss = matches2[2];
45013
+ urnComponent.nid = matches3[1].toLowerCase();
45014
+ urnComponent.nss = matches3[2];
44102
45015
  const urnScheme = `${scheme}:${options.nid || urnComponent.nid}`;
44103
45016
  const schemeHandler = getSchemeHandler(urnScheme);
44104
45017
  urnComponent.path = undefined;
@@ -44344,8 +45257,8 @@ var require_fast_uri = __commonJS((exports, module) => {
44344
45257
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
44345
45258
  var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
44346
45259
  var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
44347
- function getParseError(parsed, matches2) {
44348
- if (matches2[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
45260
+ function getParseError(parsed, matches3) {
45261
+ if (matches3[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
44349
45262
  return 'URI path must start with "/" when authority is present.';
44350
45263
  }
44351
45264
  if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
@@ -44392,19 +45305,19 @@ var require_fast_uri = __commonJS((exports, module) => {
44392
45305
  }
44393
45306
  }
44394
45307
  }
44395
- const matches2 = uri.match(URI_PARSE);
44396
- if (matches2) {
44397
- parsed.scheme = matches2[1];
44398
- parsed.userinfo = matches2[3];
44399
- parsed.host = matches2[4];
44400
- parsed.port = parseInt(matches2[5], 10);
44401
- parsed.path = matches2[6] || "";
44402
- parsed.query = matches2[7];
44403
- parsed.fragment = matches2[8];
45308
+ const matches3 = uri.match(URI_PARSE);
45309
+ if (matches3) {
45310
+ parsed.scheme = matches3[1];
45311
+ parsed.userinfo = matches3[3];
45312
+ parsed.host = matches3[4];
45313
+ parsed.port = parseInt(matches3[5], 10);
45314
+ parsed.path = matches3[6] || "";
45315
+ parsed.query = matches3[7];
45316
+ parsed.fragment = matches3[8];
44404
45317
  if (isNaN(parsed.port)) {
44405
- parsed.port = matches2[5];
45318
+ parsed.port = matches3[5];
44406
45319
  }
44407
- const parseError = getParseError(parsed, matches2);
45320
+ const parseError = getParseError(parsed, matches3);
44408
45321
  if (parseError !== undefined) {
44409
45322
  parsed.error = parsed.error || parseError;
44410
45323
  malformedAuthorityOrPort = true;
@@ -47063,12 +47976,12 @@ var require_formats = __commonJS((exports) => {
47063
47976
  var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
47064
47977
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
47065
47978
  function date(str) {
47066
- const matches2 = DATE.exec(str);
47067
- if (!matches2)
47979
+ const matches3 = DATE.exec(str);
47980
+ if (!matches3)
47068
47981
  return false;
47069
- const year = +matches2[1];
47070
- const month = +matches2[2];
47071
- const day = +matches2[3];
47982
+ const year = +matches3[1];
47983
+ const month = +matches3[2];
47984
+ const day = +matches3[3];
47072
47985
  return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]);
47073
47986
  }
47074
47987
  function compareDate(d1, d2) {
@@ -47083,16 +47996,16 @@ var require_formats = __commonJS((exports) => {
47083
47996
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
47084
47997
  function getTime(strictTimeZone) {
47085
47998
  return function time(str) {
47086
- const matches2 = TIME.exec(str);
47087
- if (!matches2)
47999
+ const matches3 = TIME.exec(str);
48000
+ if (!matches3)
47088
48001
  return false;
47089
- const hr = +matches2[1];
47090
- const min = +matches2[2];
47091
- const sec = +matches2[3];
47092
- const tz = matches2[4];
47093
- const tzSign = matches2[5] === "-" ? -1 : 1;
47094
- const tzH = +(matches2[6] || 0);
47095
- const tzM = +(matches2[7] || 0);
48002
+ const hr = +matches3[1];
48003
+ const min = +matches3[2];
48004
+ const sec = +matches3[3];
48005
+ const tz = matches3[4];
48006
+ const tzSign = matches3[5] === "-" ? -1 : 1;
48007
+ const tzH = +(matches3[6] || 0);
48008
+ const tzM = +(matches3[7] || 0);
47096
48009
  if (tzH > 23 || tzM > 59 || strictTimeZone && !tz)
47097
48010
  return false;
47098
48011
  if (hr <= 23 && min <= 59 && sec < 60)
@@ -50732,10 +51645,10 @@ async function walletFor(cfg, fetch2, name2, scope) {
50732
51645
  throw new Refusal(fail3("SCOPE_MISSING", `The key needs ${scope}.`));
50733
51646
  const rows = Array.isArray(listed.page) ? listed.page : [];
50734
51647
  const evm = rows.filter((row) => row.chain === "evm");
50735
- const matches2 = name2 === undefined ? evm : evm.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
50736
- if (matches2.length !== 1)
51648
+ const matches3 = name2 === undefined ? evm : evm.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
51649
+ if (matches3.length !== 1)
50737
51650
  throw new Refusal(fail3("TEE_WALLET_REQUIRED", evm.length === 0 ? "This key has no EVM TEE wallet bound to it." : `Name exactly one EVM TEE wallet: ${evm.map((row) => row.label ?? row.id).join(", ")}`));
50738
- return { row: matches2[0], appId: typeof listed.privyAppId === "string" ? listed.privyAppId : "" };
51651
+ return { row: matches3[0], appId: typeof listed.privyAppId === "string" ? listed.privyAppId : "" };
50739
51652
  }
50740
51653
  async function builderPin2(cfg, fetch2, env) {
50741
51654
  const pinned = env.CANDLE_HYPERLIQUID_BUILDER?.trim() || CANDLE_HYPERLIQUID_BUILDER_ADDRESS2;
@@ -50869,10 +51782,10 @@ function rawUnits(amount, decimals) {
50869
51782
  }
50870
51783
  function pickWallet(rows, chain2, name2, what) {
50871
51784
  const onChain = rows.filter((row) => row.chain === chain2);
50872
- const matches2 = name2 === undefined ? onChain : onChain.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
50873
- if (matches2.length !== 1)
51785
+ const matches3 = name2 === undefined ? onChain : onChain.filter((row) => row.id === name2 || row.label === name2 || row.address.toLowerCase() === name2.toLowerCase());
51786
+ if (matches3.length !== 1)
50874
51787
  throw new Refusal(fail3("TEE_WALLET_REQUIRED", onChain.length === 0 ? `This key has no ${what} bound to it.` : `Name exactly one ${what}: ${onChain.map((row) => row.label ?? row.id).join(", ")}`));
50875
- return matches2[0];
51788
+ return matches3[0];
50876
51789
  }
50877
51790
  function depositProblem(build, expect) {
50878
51791
  const destination = build.destination ?? {};
@@ -51467,7 +52380,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
51467
52380
  }, async (args) => callAndRelay("candle_get_profile_trades", args, cfg));
51468
52381
  register("candle_get_portfolio", {
51469
52382
  title: "Read what the account holds, on Solana and Hood",
51470
- description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
52383
+ description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. `walletsComplete` is false only " + "when the wallet list itself was cut off, and is absent on a server that predates it: then " + "`complete: false` may be a cut-off list or a failed read. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
51471
52384
  inputSchema: {}
51472
52385
  }, async () => callAndRelay("candle_get_portfolio", {}, cfg));
51473
52386
  register("candle_resolve_token", {
@@ -51857,7 +52770,7 @@ init_render();
51857
52770
  init_secret_store();
51858
52771
 
51859
52772
  // src/version.ts
51860
- var CLI_VERSION = "0.11.15";
52773
+ var CLI_VERSION = "0.11.16";
51861
52774
 
51862
52775
  // src/commands/auth.ts
51863
52776
  init_keys_embedded_wallet();
@@ -52084,7 +52997,7 @@ async function authLogout(args, ctx) {
52084
52997
  const keepKey = parsed.booleans.has("--keep-key");
52085
52998
  await printIdentity(ctx);
52086
52999
  const config = await deps.readConfig();
52087
- const { keyPrefix, portalOrigin } = effectiveProfileFields(config, ctx.profile);
53000
+ const { keyPrefix, portalOrigin, deviceTokenPrefix, label } = effectiveProfileFields(config, ctx.profile);
52088
53001
  const deviceToken = await resolveDeviceToken(deps, ctx.profile);
52089
53002
  let revokedKey;
52090
53003
  if (!keepKey && deviceToken && keyPrefix) {
@@ -52120,12 +53033,18 @@ async function authLogout(args, ctx) {
52120
53033
  } else {
52121
53034
  await deps.store.delete(SECRET_REFS.deviceToken);
52122
53035
  await deps.store.delete(SECRET_REFS.apiKey);
52123
- await deps.clearConfig();
53036
+ const patch = Object.fromEntries(PRE_PROFILE_FIELDS.map((field) => [field, undefined]));
53037
+ const remaining = { ...config, ...patch };
53038
+ if (Object.values(remaining).some((value) => value !== undefined))
53039
+ await deps.writeConfig(patch);
53040
+ else
53041
+ await deps.clearConfig();
52124
53042
  }
52125
- const portalUrl = portalDeviceUrl(apiUrl, portalOrigin);
53043
+ const portalUrl = portalDevicesUrl(apiUrl, portalOrigin);
53044
+ const device = deviceTokenPrefix ? { prefix: deviceTokenPrefix, label: label ?? null } : null;
52126
53045
  const liveEnvOverrides = credentialEnvOverrides(deps.env);
52127
53046
  if (json) {
52128
- deps.stdout.write(`${JSON.stringify({ success: true, revokedKey: revokedKey ?? null, portalUrl, envOverrides: liveEnvOverrides })}
53047
+ deps.stdout.write(`${JSON.stringify({ success: true, revokedKey: revokedKey ?? null, portalUrl, device, envOverrides: liveEnvOverrides })}
52129
53048
  `);
52130
53049
  return 0;
52131
53050
  }
@@ -52137,7 +53056,11 @@ async function authLogout(args, ctx) {
52137
53056
  }
52138
53057
  deps.stdout.write(`The device token itself is session-only to revoke -- that is intentional (a stolen token cannot read device metadata or revoke a sibling device). Sign in to the portal to revoke it there.
52139
53058
  `);
52140
- deps.stdout.write(`Portal: ${portalUrl}
53059
+ if (device)
53060
+ deps.stdout.write(`Device ${device.prefix}${device.label ? ` (${device.label})` : ""} is still live on the server. Revoke it under Connected devices: ${portalUrl}
53061
+ `);
53062
+ else
53063
+ deps.stdout.write(`Connected devices: ${portalUrl}
52141
53064
  `);
52142
53065
  return 0;
52143
53066
  }
@@ -52543,9 +53466,14 @@ var HELP = {
52543
53466
  portfolio: {
52544
53467
  group: "Account",
52545
53468
  summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
52546
- description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade.",
52547
- usage: ["candle portfolio [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
52548
- rows: [],
53469
+ description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade. With a filter, only the wallets whose label or address contains it are read and shown, the subtotals and total are for those wallets, and the first line says how many matched. When Candle listed only part of the account's wallets, the result stays partial even if every wallet shown was read, and no match says so.",
53470
+ usage: ["candle portfolio [<filter>] [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--json]"],
53471
+ rows: [
53472
+ {
53473
+ invocation: "<filter>",
53474
+ description: "Keep the wallets whose label or address contains this text, in every group. Vault keys it leaves out are never sent to your RPC"
53475
+ }
53476
+ ],
52549
53477
  flags: [
52550
53478
  { invocation: "--chain solana|hood", description: "Show one chain, and read nothing on the other" },
52551
53479
  {
@@ -52561,6 +53489,7 @@ var HELP = {
52561
53489
  examples: [
52562
53490
  "candle portfolio",
52563
53491
  "candle portfolio --chain hood",
53492
+ "candle portfolio p-",
52564
53493
  "candle portfolio --rpc-url https://your-rpc.example --evm-rpc-url https://your-hood-rpc.example --json"
52565
53494
  ],
52566
53495
  env: [...ENV_API, ...ENV_LOCAL_SIGNING]
@@ -52859,8 +53788,8 @@ var HELP = {
52859
53788
  description: "Print the funding instruction for your vault to sign"
52860
53789
  },
52861
53790
  {
52862
- invocation: "status <address> [--rpc-url <url>]",
52863
- description: "Server lifecycle state and on-chain balances; on Hood, ETH, USDG, the gas reserve and gas: low"
53791
+ invocation: "status <address> [--rpc-url <url>] [--verify]",
53792
+ description: "Server lifecycle state and on-chain balances without unlocking; --verify opens the vault and checks the key. On Hood, ETH, USDG, the gas reserve and gas: low"
52864
53793
  },
52865
53794
  {
52866
53795
  invocation: "disable <address>",
@@ -52877,7 +53806,7 @@ var HELP = {
52877
53806
  { invocation: "rebinds [wallet]", description: "List TEE wallet rebinds for this account (owner only)" },
52878
53807
  {
52879
53808
  invocation: "signer new --key <prefix|label> [--out <pem>] [--force]",
52880
- description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an old one here still owns wallets"
53809
+ description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an older signer on THIS machine still owns wallets; it moves, revokes and deletes nothing"
52881
53810
  }
52882
53811
  ],
52883
53812
  flags: [KEYSTORE_FLAG],
@@ -53943,7 +54872,7 @@ init_tee_rebind();
53943
54872
  var USAGE_TEE_SIGNER = "Usage: candle tee signer new --key <prefix|label> [--out <pem>] [--force] [--json]";
53944
54873
  var USAGE_KEYS_SIGNER = "Usage: candle keys signer approve <code> --key <prefix|label> [--reject] | candle keys signer move <prefix|label> [--to-key <prefix|label>] [--json]";
53945
54874
  var SIGNER_POLL_MS = 5000;
53946
- var DEVICE_TOKEN_BESIDE_SIGNER_LINE = "Warning: this machine holds a device token as well as a key signer. A machine with both can approve its own signer request. Do not log the trading machine in: run candle auth logout here, and approve from the owner's machine.";
54875
+ var DEVICE_TOKEN_BESIDE_SIGNER_LINE = "Warning: this machine holds a device token as well as a key signer. A machine with both can approve its own signer request. Do not leave the trading machine logged in. If you signed in for a move, run candle auth logout --profile <that profile> here, and approve from the owner's machine.";
53947
54876
  var unsupported = {
53948
54877
  code: "KEY_SIGNER_UNSUPPORTED",
53949
54878
  message: "This Candle API does not serve key signers yet; nothing changed.",
@@ -54026,6 +54955,7 @@ async function teeSignerNew(args, ctx) {
54026
54955
  await writeOut(ctx, out, recorded);
54027
54956
  return reportApproved(ctx, view, recorded, { resumed: true, requested: false });
54028
54957
  }
54958
+ let replaces = null;
54029
54959
  let resumed = entry !== undefined;
54030
54960
  if (entry === undefined) {
54031
54961
  const all = allSignerWallets(view);
@@ -54040,6 +54970,11 @@ async function teeSignerNew(args, ctx) {
54040
54970
  }, json);
54041
54971
  return 1;
54042
54972
  }
54973
+ if (active !== null) {
54974
+ replaces = { fingerprint: active.fingerprint, wallets: view.wallets.onSigner.length };
54975
+ deps.stderr.write(`Key ${keyPrefix} already has active signer ${replaces.fingerprint}. Approving this request replaces it. ${replaces.wallets > 0 ? `Its ${plural(replaces.wallets, "wallet")} keep trading only from the machine that holds it until candle keys signer move ${keyPrefix} runs there.` : "It owns no wallets, so nothing has to move."}
54976
+ `);
54977
+ }
54043
54978
  const pair = generatePair();
54044
54979
  const spkiSha256 = spkiSha256Of(pair.publicKeyDer);
54045
54980
  const ref = keySignerRef(keyPrefix, spkiSha256);
@@ -54069,7 +55004,8 @@ async function teeSignerNew(args, ctx) {
54069
55004
  const reread = await readSigner(ctx, "self", { apiKey });
54070
55005
  return reportApproved(ctx, reread.ok ? reread.body : view, recorded, {
54071
55006
  resumed,
54072
- requested: true
55007
+ requested: true,
55008
+ replaces
54073
55009
  });
54074
55010
  }
54075
55011
  }
@@ -54080,6 +55016,9 @@ async function teeSignerNew(args, ctx) {
54080
55016
  return 1;
54081
55017
  }
54082
55018
  const body = requested.body;
55019
+ if (body.supersededPending)
55020
+ deps.stderr.write(`An earlier pending request for this key was cancelled.
55021
+ `);
54083
55022
  if (body.spkiSha256 !== entry.spkiSha256) {
54084
55023
  writeLocalFailure(deps, {
54085
55024
  code: "KEY_SIGNER_CHANGED",
@@ -54109,7 +55048,7 @@ async function teeSignerNew(args, ctx) {
54109
55048
  if (now !== null && now.spkiSha256 === entry.spkiSha256) {
54110
55049
  const recorded = { ...entry, signerQuorumId: now.signerQuorumId };
54111
55050
  await saveKeySignerEntry(deps, recorded);
54112
- return reportApproved(ctx, view, recorded, { resumed, requested: true });
55051
+ return reportApproved(ctx, view, recorded, { resumed, requested: true, replaces });
54113
55052
  }
54114
55053
  if (view.pending === null || view.pending.spkiSha256 !== entry.spkiSha256) {
54115
55054
  writeLocalFailure(deps, {
@@ -54153,6 +55092,7 @@ function reportApproved(ctx, view, entry, how) {
54153
55092
  signerQuorumId: entry.signerQuorumId,
54154
55093
  resumed: how.resumed,
54155
55094
  requested: how.requested,
55095
+ replaces: how.replaces ?? null,
54156
55096
  wallets: {
54157
55097
  onSigner: onSigner.map((w) => w.id),
54158
55098
  legacy: legacy.map((w) => w.id),
@@ -54261,6 +55201,7 @@ async function keysSignerApprove(args, ctx) {
54261
55201
  "",
54262
55202
  "Requested from a machine using this key. Compare the fingerprint with what that machine printed.",
54263
55203
  `Wallets on this key: ${all.length}.`,
55204
+ ...active !== null ? [`Approving replaces signer ${active.fingerprint} as this key's active signer.`] : [],
54264
55205
  ...active !== null && onActive > 0 ? [
54265
55206
  `${plural(onActive, "wallet")} ${onActive === 1 ? "is" : "are"} owned by signer ${active.fingerprint}. They keep trading from the machine that holds that signer until you move them.`
54266
55207
  ] : [],
@@ -58254,1491 +59195,662 @@ async function externalList(args, ctx) {
58254
59195
  return 0;
58255
59196
  }
58256
59197
  for (const row of rows) {
58257
- deps.stdout.write(`${row.address}
58258
- `);
58259
- deps.stdout.write(` label ${row.label || "(none)"}
58260
- `);
58261
- if (row.derivation)
58262
- deps.stdout.write(` derivation ${row.derivation}
58263
- `);
58264
- if (row.exposureUnknown)
58265
- deps.stdout.write(` history unknown (restored from the phrase)
58266
- `);
58267
- }
58268
- deps.stdout.write(`External wallets are never delegated to Privy and never registered with Candle. Balances are read over your own RPC; Candle is not in their transactions.
58269
- `);
58270
- return 0;
58271
- });
58272
- }
58273
- async function externalSweep(args, ctx) {
58274
- const parsed = parseArgs(args, {
58275
- valueFlags: ["--to", "--rpc-url", "--keystore"],
58276
- booleanFlags: ["--accept-older-copy"],
58277
- pathFlags: ["--keystore"]
58278
- });
58279
- if ("error" in parsed)
58280
- return usage(ctx, parsed.error);
58281
- const [source, extra] = parsed.positionals;
58282
- if (!source || extra !== undefined) {
58283
- return usage(ctx, "Usage: candle external sweep <external> --to <vault> [--rpc-url <url>]");
58284
- }
58285
- const to = parsed.values["--to"];
58286
- if (!to)
58287
- return usage(ctx, "--to <vault> is required: the vault receive key everything is sent to (never inferred).");
58288
- const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
58289
- if ("error" in solana)
58290
- return usage(ctx, solana.error);
58291
- if (!refuseEnvPassphrase(ctx))
58292
- return 1;
58293
- if (!requireTty(ctx, "external sweep"))
58294
- return 1;
58295
- const { deps } = ctx;
58296
- const resolvedVault = vaultPathFor(ctx, parsed);
58297
- if ("error" in resolvedVault)
58298
- return usage(ctx, resolvedVault.error);
58299
- const path = resolvedVault.path;
58300
- return runVaultCommand(ctx, async ({ hold }) => {
58301
- const raw = await requireVaultRaw(ctx, resolvedVault);
58302
- const opened = await unlockInteractively(ctx, path, raw, {
58303
- acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
58304
- });
58305
- const vault = hold(opened.vault);
58306
- assertNotEvmEntry(vault.index, source, "external sweep");
58307
- assertNotEvmEntry(vault.index, to, "external sweep");
58308
- const sourceEntry = requireExternalEntry(vault.index, source);
58309
- const destination = findVaultRoleEntry(vault.index, to);
58310
- if (destination === undefined || destination.role !== "vault") {
58311
- const other = vault.index.entries.find((candidate) => candidate.label === to || candidate.address === to);
58312
- throw new VaultError("VAULT_INDEX_INVALID", other === undefined ? `No vault key matches --to ${to}.` : `--to ${to} is ${describeRole(other)}; a sweep goes to a role:vault receive key only.`, { suggestion: "Name a vault key by label or address: candle vault status --unlock", exitCode: 2 });
58313
- }
58314
- if (destination.address === sourceEntry.address) {
58315
- return usage(ctx, "The source and the destination are the same address.");
58316
- }
58317
- if (!ctx.json) {
58318
- deps.stdout.write(`Sweep everything from external wallet:
58319
- `);
58320
- deps.stdout.write(` source ${sourceEntry.label} ${sourceEntry.address}
58321
- `);
58322
- deps.stdout.write(`to vault receive key:
58323
- `);
58324
- deps.stdout.write(` destination ${destination.label} ${destination.address}
58325
- `);
58326
- deps.stdout.write(`SOL, classic SPL and Token-2022 balances move, signed locally with the external key.
58327
- `);
58328
- }
58329
- await confirmLastSix(ctx, destination.address, "the vault destination");
58330
- await opened.confirm(`sweep ${sourceEntry.label} to ${destination.address}`);
58331
- const secret = await decryptKey(vault, sourceEntry.id);
58332
- try {
58333
- const outcome = await sweepEverythingTo({
58334
- rpc: solana.rpc,
58335
- ctx,
58336
- secret64: secret,
58337
- owner: sourceEntry.address,
58338
- destination: destination.address,
58339
- say: ctx.json ? () => {} : (line) => deps.stdout.write(`${line}
58340
- `)
58341
- });
58342
- const complete = outcome.leftovers.length === 0;
58343
- if (ctx.json) {
58344
- writeJson(deps, {
58345
- ok: complete,
58346
- source: sourceEntry.address,
58347
- sourceLabel: sourceEntry.label,
58348
- destination: destination.address,
58349
- receipts: outcome.receipts,
58350
- leftovers: outcome.leftovers
58351
- });
58352
- return complete ? 0 : 3;
58353
- }
58354
- if (outcome.receipts.length === 0 && outcome.leftovers.length === 0) {
58355
- deps.stdout.write(`Nothing to sweep: no balances found.
58356
- `);
58357
- return 0;
58358
- }
58359
- if (complete) {
58360
- deps.stdout.write(`Swept: ${outcome.receipts.length} transaction(s) finalized.
58361
- `);
58362
- return 0;
58363
- }
58364
- deps.stdout.write(`Sweep incomplete: ${outcome.leftovers.length} leftover(s).
58365
- `);
58366
- for (const l of outcome.leftovers) {
58367
- deps.stdout.write(` - ${l.kind}${l.mint ? ` ${l.mint}` : ""}${l.amountRaw ? ` (${l.amountRaw} raw)` : ""}: ${l.detail}
58368
- `);
58369
- }
58370
- deps.stdout.write(`Re-run this sweep once the leftovers are resolved.
58371
- `);
58372
- return 3;
58373
- } finally {
58374
- wipe(secret);
58375
- }
58376
- });
58377
- }
58378
-
58379
- // src/commands/help.ts
58380
- async function help(args, ctx) {
58381
- const word2 = args[0];
58382
- if (word2 === undefined) {
58383
- ctx.deps.stdout.write(renderTopLevel());
58384
- return 0;
58385
- }
58386
- const topic = renderTopic(word2);
58387
- if (topic === undefined) {
58388
- ctx.deps.stderr.write(`Unknown command: ${word2}
58389
- `);
58390
- ctx.deps.stderr.write(renderTopLevel());
58391
- return 1;
58392
- }
58393
- ctx.deps.stdout.write(topic);
58394
- return 0;
58395
- }
58396
-
58397
- // src/index.ts
58398
- init_keys();
58399
-
58400
- // src/commands/keys-access.ts
58401
- init_agent_key_access();
58402
- init_args();
58403
- init_deps();
58404
- init_profiles();
58405
- init_render();
58406
- init_promote_support();
58407
- init_keys();
58408
- init_tee_rebind();
58409
- var KEYS_PATH4 = "/api/v1/agent/keys";
58410
- var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|read-write|read-write-transfer> [--yes] [--json]
58411
- ` + " candle keys access <prefix|label> --history [--json]";
58412
- var SELF = "self";
58413
- var CLI_SPELLING = Object.fromEntries(Object.entries(ACCESS_LEVELS).map(([spelling, preset]) => [preset, spelling]));
58414
- var NO_API_KEY2 = {
58415
- code: "API_KEY_REQUIRED",
58416
- message: "candle keys access self changes the profile's own API key, and this profile holds none.",
58417
- suggestion: "Select the profile holding the key with --profile, or name the key by prefix to use the device token."
58418
- };
58419
- var REQUIRES_TTY = {
58420
- code: "KEY_ACCESS_REQUIRES_TTY",
58421
- message: "Widening a key needs a terminal: the key prefix is typed back, and nothing else supplies it.",
58422
- suggestion: "Run it in an interactive shell; there is no flag and no environment variable to widen a key."
58423
- };
58424
- function levelName(side) {
58425
- const preset = side.access ?? presetForScopes(side.scopes);
58426
- if (preset)
58427
- return AGENT_KEY_PRESET_LABELS[preset];
58428
- return `custom (${accessCell(side.scopes)})`;
58429
- }
58430
- function keyName2(keyPrefix, label) {
58431
- const cleaned = labelCell(label ?? undefined);
58432
- return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
58433
- }
58434
- function listWords(words) {
58435
- return words.length <= 1 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
58436
- }
58437
- function linkedTransferLine(ready) {
58438
- const assets = Object.entries(ready.assets);
58439
- const missing = assets.filter(([, ok]) => !ok).map(([asset]) => asset.toUpperCase());
58440
- const readyAssets = assets.filter(([, ok]) => ok).map(([asset]) => asset.toUpperCase());
58441
- const needs = [];
58442
- if (!ready.txLimit)
58443
- needs.push("a USD transaction limit");
58444
- if (missing.length > 0) {
58445
- needs.push(`${missing.map((asset) => `a ${asset}`).join(" and ")} spend cap`);
58446
- }
58447
- if (needs.length === 0)
58448
- return null;
58449
- const readyPart = readyAssets.length > 0 ? `; ${listWords(readyAssets)} ${readyAssets.length === 1 ? "is" : "are"} ready` : "";
58450
- return [
58451
- `Warning: to send to linked wallets it also needs ${needs.join(" and ")}${readyPart}.`,
58452
- " Set them in the web key manager: Agents, Keys, this key, Spend limits."
58453
- ].join(`
58454
- `);
58455
- }
58456
- var LOCALLY_RENDERED = new Set(["MOVES_FUNDS", "VAULT_TRANSFER_READY", "LINKED_TRANSFER_NEEDS_LIMITS"]);
58457
- function accessScreen(shown, identity) {
58458
- const environment = candleEnvironment(identity.apiUrl) ?? "not a Candle host";
58459
- const label = labelCell(shown.label ?? undefined);
58460
- const lines = [
58461
- `Key ${shown.keyPrefix}${label ? ` ${label}` : ""}`,
58462
- `Candle account ${identity.account}`,
58463
- `API ${identity.apiUrl} (${environment})`,
58464
- "",
58465
- `Access ${levelName(shown.from)} -> ${levelName(shown.to)} (${shown.direction})`,
58466
- `Adds ${shown.added.length > 0 ? formatScopesForSummary(shown.added) : "nothing"}`,
58467
- `Removes ${shown.removed.length > 0 ? shown.removed.join(", ") : "nothing"}`,
58468
- ""
58469
- ];
58470
- const bound = shown.effects.boundTeeWallets;
58471
- if (bound && bound.count > 0) {
58472
- const names = bound.sample.map((w) => labelCell(w.label ?? undefined) || shortAddress(w.address));
58473
- const more = bound.count > names.length ? `, and ${bound.count - names.length} more` : "";
58474
- lines.push(`TEE wallets on this key: ${bound.count} (${names.join(", ")}${more})`);
58475
- if (shown.direction === "widen" && shown.added.includes("transfer:bound")) {
58476
- lines.push("It can move these wallets' funds to their vaults as soon as you confirm.");
58477
- }
58478
- }
58479
- const ready = shown.effects.linkedTransferReady;
58480
- if (shown.direction === "widen" && ready) {
58481
- const line = linkedTransferLine(ready);
58482
- if (line)
58483
- lines.push(line);
58484
- }
58485
- for (const warning of shown.warnings) {
58486
- if (!LOCALLY_RENDERED.has(warning.code))
58487
- lines.push(`Warning: ${warning.message}`);
58488
- }
58489
- if (lines[lines.length - 1] !== "")
58490
- lines.push("");
58491
- return lines.join(`
58492
- `);
58493
- }
58494
- function accessFailureDetails(result, context) {
58495
- let code = result.code;
58496
- let message = result.message;
58497
- let suggestion;
58498
- if (result.status === 404 && result.code === undefined) {
58499
- code = "KEY_ACCESS_UNSUPPORTED";
58500
- message = context.history ? "This Candle API has no key access history yet." : "This Candle API cannot change a key's access yet; nothing changed.";
58501
- } else if (result.code === "LOOSEN_REQUIRES_SESSION" && context.self) {
58502
- const level = context.preset ? CLI_SPELLING[context.preset] : "<level>";
58503
- suggestion = `Widen it from a signed-in session or with the device token: candle keys access <prefix> --access ${level}`;
58504
- } else if (result.code === "KEY_ACCESS_STALE") {
58505
- suggestion = "Run the command again; the preview will show the key's current access.";
58506
- } else if (result.code === "KEY_ACCESS_PARTNER_KEY") {
58507
- suggestion = "Partner keys are managed by Candle; ask your Candle contact to change one.";
58508
- } else if (result.status === 404 && result.code === "VALIDATION_FAILED") {
58509
- suggestion = "Run: candle keys list, to see this account's keys and their prefixes.";
58510
- }
58511
- return { code: code ?? `HTTP ${result.status}`, message, ...suggestion !== undefined ? { suggestion } : {} };
58512
- }
58513
- function writeAccessFailure(ctx, result, context) {
58514
- const { deps, apiUrl, json } = ctx;
58515
- const { code, message, suggestion } = accessFailureDetails(result, context);
58516
- const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: context.self ? "key" : "device" });
58517
- if (json) {
58518
- deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
58519
- `);
58520
- } else {
58521
- deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
58522
- `);
58523
- }
58524
- return 1;
58525
- }
58526
- async function keysAccess(args, ctx) {
58527
- const { deps, apiUrl, json } = ctx;
58528
- const parsed = parseArgs(args, { valueFlags: ["--access"], booleanFlags: ["--yes", "--history"] });
58529
- if ("error" in parsed) {
58530
- writeUsageFailure(deps, parsed.error, json);
58531
- return 2;
58532
- }
58533
- const [target, extra] = parsed.positionals;
58534
- if (target === undefined || extra !== undefined) {
58535
- writeUsageFailure(deps, `${extra !== undefined ? `Unexpected argument: ${extra}. ` : "Name one key. "}${USAGE_ACCESS}`, json);
58536
- return 2;
58537
- }
58538
- const accessFlag = parsed.values["--access"];
58539
- const history = parsed.booleans.has("--history");
58540
- const yes = parsed.booleans.has("--yes");
58541
- if (accessFlag === undefined === !history) {
58542
- writeUsageFailure(deps, `Pass exactly one of --access and --history. ${USAGE_ACCESS}`, json);
58543
- return 2;
58544
- }
58545
- if (history) {
58546
- if (yes) {
58547
- writeUsageFailure(deps, "--yes only applies to a change, not to --history.", json);
58548
- return 2;
58549
- }
58550
- if (target === SELF) {
58551
- writeUsageFailure(deps, "--history needs the device token: name the key by prefix or label, not self.", json);
58552
- return 2;
58553
- }
58554
- return keysAccessHistory(target, ctx);
58555
- }
58556
- const preset = ACCESS_LEVELS[accessFlag];
58557
- if (preset === undefined) {
58558
- writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
58559
- return 2;
58560
- }
58561
- await printIdentity(ctx);
58562
- const self = target === SELF;
58563
- let path;
58564
- let auth;
58565
- let credentials;
58566
- if (self) {
58567
- const apiKey = await resolveApiKey(deps, ctx.profile);
58568
- if (!apiKey) {
58569
- writeLocalFailure(deps, NO_API_KEY2, json);
58570
- return 1;
58571
- }
58572
- path = `${KEYS_PATH4}/self/access`;
58573
- auth = "key";
58574
- credentials = { apiKey };
58575
- } else {
58576
- const deviceToken = await resolveDeviceToken(deps, ctx.profile);
58577
- if (!deviceToken) {
58578
- writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
58579
- return 1;
58580
- }
58581
- const resolved = await resolveTargetKey(ctx, deviceToken, target, {
58582
- codePrefix: "KEY_ACCESS",
58583
- writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false })
58584
- });
58585
- if (!resolved.ok)
58586
- return resolved.code;
58587
- path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
58588
- auth = "device";
58589
- credentials = { deviceToken };
58590
- }
58591
- const failureContext = { self, preset };
58592
- const call = (body) => apiRequest(path, { method: "PUT", body, auth, credentials, apiUrl, fetch: deps.fetch, env: deps.env });
58593
- const preview = await call({ access: preset, dryRun: true });
58594
- if (!preview.ok)
58595
- return writeAccessFailure(ctx, preview, failureContext);
58596
- const shown = preview.body;
58597
- const name2 = keyName2(shown.keyPrefix, shown.label);
58598
- if (shown.direction === "unchanged") {
58599
- if (json)
58600
- deps.stdout.write(`${JSON.stringify({ ...shown, command: "keys access" })}
58601
- `);
58602
- else
58603
- deps.stdout.write(`${name2} is already ${levelName(shown.to)}. Nothing changed.
58604
- `);
58605
- return 0;
58606
- }
58607
- const widen = shown.direction === "widen";
58608
- const interactive = deps.isTTY.stdin && deps.isTTY.stdout;
58609
- if (widen && yes) {
58610
- writeUsageFailure(deps, "--yes only skips the prompt when narrowing; nothing changed.", json);
58611
- return 2;
58612
- }
58613
- if (widen && !interactive) {
58614
- writeLocalFailure(deps, REQUIRES_TTY, json);
58615
- return 1;
58616
- }
58617
- if (!widen && !yes && !interactive) {
58618
- writeUsageFailure(deps, "Narrowing a key without a terminal needs --yes; nothing changed. Run it in an interactive shell, or pass --yes.", json);
58619
- return 2;
58620
- }
58621
- const config = await deps.readConfig();
58622
- const fields = effectiveProfileFields(config, ctx.profile);
58623
- const account = fields.account ? shortAddress(fields.account) : "unknown";
58624
- deps.stderr.write(`${accessScreen(shown, { account: fields.username ? `${fields.username} (${account})` : account, apiUrl })}
58625
- `);
58626
- if (widen) {
58627
- const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
58628
- if (typed.trim() !== shown.keyPrefix) {
58629
- writeLocalFailure(deps, {
58630
- code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
58631
- message: `The acknowledgement is the key prefix ${shown.keyPrefix}; nothing changed.`,
58632
- suggestion: `Run the command again and type ${shown.keyPrefix} at the prompt.`
58633
- }, json);
58634
- return 1;
58635
- }
58636
- } else if (!yes) {
58637
- const answer = await deps.promptLine(`Change ${name2} from ${levelName(shown.from)} to ${levelName(shown.to)}? [y/N] `);
58638
- if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
58639
- writeLocalFailure(deps, {
58640
- code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
58641
- message: "Not confirmed; nothing changed.",
58642
- suggestion: "Run the command again and answer y, or pass --yes."
58643
- }, json);
58644
- return 1;
58645
- }
58646
- }
58647
- const committed = await call({ access: preset, expectScopes: shown.from.scopes });
58648
- if (!committed.ok)
58649
- return writeAccessFailure(ctx, committed, failureContext);
58650
- const result = committed.body;
58651
- if (fields.keyPrefix !== undefined && fields.keyPrefix === result.keyPrefix && result.direction !== "unchanged") {
58652
- const scopes = scopesForPreset(result.to.access);
58653
- if (ctx.profile)
58654
- await deps.updateProfile(ctx.profile, { scopes });
58655
- else
58656
- await deps.writeConfig({ scopes });
58657
- }
58658
- if (json) {
58659
- deps.stdout.write(`${JSON.stringify({ ...result, command: "keys access" })}
58660
- `);
58661
- return 0;
58662
- }
58663
- if (result.direction === "unchanged") {
58664
- deps.stdout.write(`${keyName2(result.keyPrefix, result.label)} is already ${levelName(result.to)}. Nothing changed.
58665
- `);
58666
- return 0;
58667
- }
58668
- deps.stdout.write(`Changed ${keyName2(result.keyPrefix, result.label)} to ${levelName(result.to)}.` + `${result.changeId ? ` Change id ${result.changeId}` : ""}
58669
- `);
58670
- return 0;
58671
- }
58672
- async function keysAccessHistory(target, ctx) {
58673
- const { deps, apiUrl, json } = ctx;
58674
- await printIdentity(ctx);
58675
- const deviceToken = await resolveDeviceToken(deps, ctx.profile);
58676
- if (!deviceToken) {
58677
- writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
58678
- return 1;
58679
- }
58680
- const resolved = await resolveTargetKey(ctx, deviceToken, target, {
58681
- codePrefix: "KEY_ACCESS",
58682
- writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false, history: true })
58683
- });
58684
- if (!resolved.ok)
58685
- return resolved.code;
58686
- const result = await apiRequest(`${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access-changes`, {
58687
- auth: "device",
58688
- credentials: { deviceToken },
58689
- apiUrl,
58690
- fetch: deps.fetch,
58691
- env: deps.env
58692
- });
58693
- if (!result.ok)
58694
- return writeAccessFailure(ctx, result, { self: false, history: true });
58695
- if (json) {
58696
- deps.stdout.write(`${JSON.stringify({ ...result.body, command: "keys access" })}
59198
+ deps.stdout.write(`${row.address}
58697
59199
  `);
58698
- return 0;
58699
- }
58700
- const rows = result.body.changes ?? [];
58701
- if (rows.length === 0) {
58702
- deps.stdout.write(`No access changes on key ${resolved.keyPrefix}.
59200
+ deps.stdout.write(` label ${row.label || "(none)"}
58703
59201
  `);
58704
- return 0;
58705
- }
58706
- deps.stdout.write(`${renderTable(["Time", "From", "To", "Actor", "Device or key"], rows.map((row) => [
58707
- formatTimestamp(row.at),
58708
- levelName(row.from),
58709
- levelName(row.to),
58710
- row.actor,
58711
- row.actorDevicePrefix ?? row.actorKeyPrefix ?? ""
58712
- ]))}
59202
+ if (row.derivation)
59203
+ deps.stdout.write(` derivation ${row.derivation}
58713
59204
  `);
58714
- if (rows.some((row) => row.actor === "device"))
58715
- deps.stdout.write(`${PORTAL_REVOKE_LINE}
59205
+ if (row.exposureUnknown)
59206
+ deps.stdout.write(` history unknown (restored from the phrase)
58716
59207
  `);
58717
- return 0;
58718
- }
58719
-
58720
- // src/index.ts
58721
- init_keys_embedded_wallet();
58722
-
58723
- // src/commands/keys-wallets.ts
58724
- init_args();
58725
- init_deps();
58726
- init_profiles();
58727
- init_render();
58728
- var NO_API_KEY3 = {
58729
- code: "NO_API_KEY",
58730
- message: "No API key for this profile.",
58731
- suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
58732
- };
58733
- function widenRefusedHint(prefix, walletIds) {
58734
- const wallets2 = walletIds.length > 0 ? walletIds.join(" ") : "<wallet...>";
58735
- return `To move TEE wallets to this key, run: candle tee rebind ${wallets2} --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).";
58736
- }
58737
- function formatTimestamp2(ms) {
58738
- return ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) : "-";
59208
+ }
59209
+ deps.stdout.write(`External wallets are never delegated to Privy and never registered with Candle. Balances are read over your own RPC; Candle is not in their transactions.
59210
+ `);
59211
+ return 0;
59212
+ });
58739
59213
  }
58740
- async function keysWalletsList(args, ctx) {
58741
- const { deps, apiUrl, json } = ctx;
58742
- const parsed = parseArgs(args, {});
58743
- if ("error" in parsed) {
58744
- writeUsageFailure(deps, parsed.error, json);
58745
- return 2;
58746
- }
58747
- const prefix = parsed.positionals[0];
58748
- if (!prefix) {
58749
- writeUsageFailure(deps, "Usage: candle keys wallets <prefix>", json);
58750
- return 2;
59214
+ async function externalSweep(args, ctx) {
59215
+ const parsed = parseArgs(args, {
59216
+ valueFlags: ["--to", "--rpc-url", "--keystore"],
59217
+ booleanFlags: ["--accept-older-copy"],
59218
+ pathFlags: ["--keystore"]
59219
+ });
59220
+ if ("error" in parsed)
59221
+ return usage(ctx, parsed.error);
59222
+ const [source, extra] = parsed.positionals;
59223
+ if (!source || extra !== undefined) {
59224
+ return usage(ctx, "Usage: candle external sweep <external> --to <vault> [--rpc-url <url>]");
58751
59225
  }
58752
- await printIdentity(ctx);
58753
- const apiKey = await resolveApiKey(deps, ctx.profile);
58754
- if (!apiKey) {
58755
- writeLocalFailure(deps, NO_API_KEY3, json);
59226
+ const to = parsed.values["--to"];
59227
+ if (!to)
59228
+ return usage(ctx, "--to <vault> is required: the vault receive key everything is sent to (never inferred).");
59229
+ const solana = await openSolanaClient(ctx, parsed.values["--rpc-url"]);
59230
+ if ("error" in solana)
59231
+ return usage(ctx, solana.error);
59232
+ if (!refuseEnvPassphrase(ctx))
58756
59233
  return 1;
58757
- }
58758
- const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
58759
- auth: "key",
58760
- credentials: { apiKey },
58761
- apiUrl,
58762
- fetch: deps.fetch,
58763
- env: deps.env
58764
- });
58765
- if (!result.ok) {
58766
- writeFailure(deps, result, { apiUrl, authType: "key" }, json);
59234
+ if (!requireTty(ctx, "external sweep"))
58767
59235
  return 1;
58768
- }
58769
- if (json) {
58770
- deps.stdout.write(`${JSON.stringify(result.body)}
59236
+ const { deps } = ctx;
59237
+ const resolvedVault = vaultPathFor(ctx, parsed);
59238
+ if ("error" in resolvedVault)
59239
+ return usage(ctx, resolvedVault.error);
59240
+ const path = resolvedVault.path;
59241
+ return runVaultCommand(ctx, async ({ hold }) => {
59242
+ const raw = await requireVaultRaw(ctx, resolvedVault);
59243
+ const opened = await unlockInteractively(ctx, path, raw, {
59244
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
59245
+ });
59246
+ const vault = hold(opened.vault);
59247
+ assertNotEvmEntry(vault.index, source, "external sweep");
59248
+ assertNotEvmEntry(vault.index, to, "external sweep");
59249
+ const sourceEntry = requireExternalEntry(vault.index, source);
59250
+ const destination = findVaultRoleEntry(vault.index, to);
59251
+ if (destination === undefined || destination.role !== "vault") {
59252
+ const other = vault.index.entries.find((candidate) => candidate.label === to || candidate.address === to);
59253
+ throw new VaultError("VAULT_INDEX_INVALID", other === undefined ? `No vault key matches --to ${to}.` : `--to ${to} is ${describeRole(other)}; a sweep goes to a role:vault receive key only.`, { suggestion: "Name a vault key by label or address: candle vault status --unlock", exitCode: 2 });
59254
+ }
59255
+ if (destination.address === sourceEntry.address) {
59256
+ return usage(ctx, "The source and the destination are the same address.");
59257
+ }
59258
+ if (!ctx.json) {
59259
+ deps.stdout.write(`Sweep everything from external wallet:
58771
59260
  `);
58772
- return 0;
58773
- }
58774
- const body = result.body;
58775
- deps.stdout.write(body.walletScope === "selected" ? `Scope: selected — this profile can only spend from the wallets below.
58776
- ` : `Scope: all — this profile can spend from every wallet on the account, listed here or not.
59261
+ deps.stdout.write(` source ${sourceEntry.label} ${sourceEntry.address}
58777
59262
  `);
58778
- if (body.profileId)
58779
- deps.stdout.write(`Profile: ${body.profileId}
59263
+ deps.stdout.write(`to vault receive key:
58780
59264
  `);
58781
- if (body.wallets.length === 0) {
58782
- deps.stdout.write(`No wallets assigned.
59265
+ deps.stdout.write(` destination ${destination.label} ${destination.address}
58783
59266
  `);
58784
- return 0;
58785
- }
58786
- const rows = body.wallets.map((w) => [
58787
- w.linkedWalletId,
58788
- w.chain,
58789
- w.address,
58790
- w.label ?? "-",
58791
- w.spendCapable ? "yes" : "no",
58792
- formatTimestamp2(w.assignedAt)
58793
- ]);
58794
- deps.stdout.write(`${renderTable(["Id", "Chain", "Address", "Label", "Can sign", "Assigned"], rows)}
59267
+ deps.stdout.write(`SOL, classic SPL and Token-2022 balances move, signed locally with the external key.
58795
59268
  `);
58796
- return 0;
58797
- }
58798
- async function keysWalletsSet(args, ctx) {
58799
- const { deps, apiUrl, json } = ctx;
58800
- const parsed = parseArgs(args, { valueFlags: ["--wallets"] });
58801
- if ("error" in parsed) {
58802
- writeUsageFailure(deps, parsed.error, json);
58803
- return 2;
58804
- }
58805
- const prefix = parsed.positionals[0];
58806
- if (!prefix) {
58807
- writeUsageFailure(deps, "Usage: candle keys wallets set <prefix> --wallets <id,id,...>", json);
58808
- return 2;
58809
- }
58810
- const raw = parsed.values["--wallets"];
58811
- if (raw === undefined) {
58812
- writeUsageFailure(deps, 'Missing --wallets. Pass a comma-separated list, or "" to assign none.', json);
58813
- return 2;
58814
- }
58815
- const walletIds = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
58816
- await printIdentity(ctx);
58817
- const apiKey = await resolveApiKey(deps, ctx.profile);
58818
- if (!apiKey) {
58819
- writeLocalFailure(deps, NO_API_KEY3, json);
58820
- return 1;
58821
- }
58822
- const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
58823
- method: "PUT",
58824
- body: { walletIds },
58825
- auth: "key",
58826
- credentials: { apiKey },
58827
- apiUrl,
58828
- fetch: deps.fetch,
58829
- env: deps.env
58830
- });
58831
- if (!result.ok) {
58832
- if (result.code !== "LOOSEN_REQUIRES_SESSION") {
58833
- writeFailure(deps, result, { apiUrl, authType: "key" }, json);
58834
- return 1;
58835
59269
  }
58836
- const hint = widenRefusedHint(prefix, walletIds);
58837
- writeFailure(deps, { ...result, uiHint: hint }, { apiUrl, authType: "key" }, json);
58838
- if (!json)
58839
- deps.stderr.write(`${hint}
59270
+ await confirmLastSix(ctx, destination.address, "the vault destination");
59271
+ await opened.confirm(`sweep ${sourceEntry.label} to ${destination.address}`);
59272
+ const secret = await decryptKey(vault, sourceEntry.id);
59273
+ try {
59274
+ const outcome = await sweepEverythingTo({
59275
+ rpc: solana.rpc,
59276
+ ctx,
59277
+ secret64: secret,
59278
+ owner: sourceEntry.address,
59279
+ destination: destination.address,
59280
+ say: ctx.json ? () => {} : (line) => deps.stdout.write(`${line}
59281
+ `)
59282
+ });
59283
+ const complete = outcome.leftovers.length === 0;
59284
+ if (ctx.json) {
59285
+ writeJson(deps, {
59286
+ ok: complete,
59287
+ source: sourceEntry.address,
59288
+ sourceLabel: sourceEntry.label,
59289
+ destination: destination.address,
59290
+ receipts: outcome.receipts,
59291
+ leftovers: outcome.leftovers
59292
+ });
59293
+ return complete ? 0 : 3;
59294
+ }
59295
+ if (outcome.receipts.length === 0 && outcome.leftovers.length === 0) {
59296
+ deps.stdout.write(`Nothing to sweep: no balances found.
58840
59297
  `);
58841
- return 1;
58842
- }
58843
- if (json) {
58844
- deps.stdout.write(`${JSON.stringify(result.body)}
59298
+ return 0;
59299
+ }
59300
+ if (complete) {
59301
+ deps.stdout.write(`Swept: ${outcome.receipts.length} transaction(s) finalized.
58845
59302
  `);
58846
- return 0;
58847
- }
58848
- deps.stdout.write(walletIds.length === 0 ? `Cleared every wallet assignment on ${prefix}.
58849
- ` : `Assigned ${walletIds.length} wallet${walletIds.length === 1 ? "" : "s"} to ${prefix}.
59303
+ return 0;
59304
+ }
59305
+ deps.stdout.write(`Sweep incomplete: ${outcome.leftovers.length} leftover(s).
58850
59306
  `);
58851
- return 0;
58852
- }
58853
- async function keysWalletsScope(args, ctx) {
58854
- const { deps, apiUrl, json } = ctx;
58855
- const parsed = parseArgs(args, { valueFlags: ["--scope"] });
58856
- if ("error" in parsed) {
58857
- writeUsageFailure(deps, parsed.error, json);
58858
- return 2;
58859
- }
58860
- const prefix = parsed.positionals[0];
58861
- const scope = parsed.values["--scope"];
58862
- if (!prefix || scope !== "all" && scope !== "selected") {
58863
- writeUsageFailure(deps, "Usage: candle keys wallets scope <prefix> --scope <all|selected>", json);
58864
- return 2;
58865
- }
58866
- await printIdentity(ctx);
58867
- const apiKey = await resolveApiKey(deps, ctx.profile);
58868
- if (!apiKey) {
58869
- writeLocalFailure(deps, NO_API_KEY3, json);
58870
- return 1;
58871
- }
58872
- const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallet-scope`, {
58873
- method: "PUT",
58874
- body: { scope },
58875
- auth: "key",
58876
- credentials: { apiKey },
58877
- apiUrl,
58878
- fetch: deps.fetch,
58879
- env: deps.env
58880
- });
58881
- if (!result.ok) {
58882
- writeFailure(deps, result, { apiUrl, authType: "key" }, json);
58883
- return 1;
58884
- }
58885
- if (json) {
58886
- deps.stdout.write(`${JSON.stringify(result.body)}
59307
+ for (const l of outcome.leftovers) {
59308
+ deps.stdout.write(` - ${l.kind}${l.mint ? ` ${l.mint}` : ""}${l.amountRaw ? ` (${l.amountRaw} raw)` : ""}: ${l.detail}
59309
+ `);
59310
+ }
59311
+ deps.stdout.write(`Re-run this sweep once the leftovers are resolved.
58887
59312
  `);
59313
+ return 3;
59314
+ } finally {
59315
+ wipe(secret);
59316
+ }
59317
+ });
59318
+ }
59319
+
59320
+ // src/commands/help.ts
59321
+ async function help(args, ctx) {
59322
+ const word2 = args[0];
59323
+ if (word2 === undefined) {
59324
+ ctx.deps.stdout.write(renderTopLevel());
58888
59325
  return 0;
58889
59326
  }
58890
- deps.stdout.write(scope === "selected" ? `${prefix} is now limited to its assigned wallets.
58891
- ` : `${prefix} can now spend from every wallet on the account.
59327
+ const topic = renderTopic(word2);
59328
+ if (topic === undefined) {
59329
+ ctx.deps.stderr.write(`Unknown command: ${word2}
58892
59330
  `);
59331
+ ctx.deps.stderr.write(renderTopLevel());
59332
+ return 1;
59333
+ }
59334
+ ctx.deps.stdout.write(topic);
58893
59335
  return 0;
58894
59336
  }
58895
- async function keysWallets(args, ctx) {
58896
- const [verb, ...rest] = args;
58897
- if (verb === "set")
58898
- return keysWalletsSet(rest, ctx);
58899
- if (verb === "scope")
58900
- return keysWalletsScope(rest, ctx);
58901
- return keysWalletsList(args, ctx);
58902
- }
58903
59337
 
58904
- // src/commands/launch.ts
58905
- init_args();
58906
- init_evm_lite();
58907
- init_render();
58908
- init_solana_endpoint();
58909
- init_solana_lite();
58910
- init_trading();
58911
- import { randomUUID as randomUUID2 } from "node:crypto";
59338
+ // src/index.ts
59339
+ init_keys();
58912
59340
 
58913
- // src/commands/swap.ts
58914
- init_zod();
59341
+ // src/commands/keys-access.ts
59342
+ init_agent_key_access();
58915
59343
  init_args();
58916
- init_bridge();
58917
- init_evm_lite();
59344
+ init_deps();
59345
+ init_profiles();
58918
59346
  init_render();
58919
- init_solana_endpoint();
58920
- init_solana_lite();
58921
- init_trading();
58922
- import { randomUUID } from "node:crypto";
58923
- function tradingFailure(ctx, error, id) {
58924
- if (error instanceof TradingUsage) {
58925
- writeUsageFailure(ctx.deps, error.message, ctx.json);
58926
- return 2;
58927
- }
58928
- writeLocalFailure(ctx.deps, {
58929
- code: error instanceof TradingError ? error.code : "TRADING_FAILED",
58930
- message: `${error instanceof Error ? describeRpcFailure(error) : "Trading failed."}${id ? ` Operation ${id}; use candle swap status ${id} before another attempt.` : ""}`,
58931
- ...error instanceof TradingError && error.suggestion ? { suggestion: error.suggestion } : {},
58932
- ...error instanceof TradingError && error.details ? { details: error.details } : {}
58933
- }, ctx.json);
58934
- return error instanceof TradingError ? error.exitCode : 1;
58935
- }
58936
- function lazySolanaClient(ctx, flag) {
58937
- let pending;
58938
- return () => {
58939
- pending ??= tradingSolanaClient(ctx, flag);
58940
- return pending;
58941
- };
59347
+ init_promote_support();
59348
+ init_keys();
59349
+ init_tee_rebind();
59350
+ var KEYS_PATH4 = "/api/v1/agent/keys";
59351
+ var USAGE_ACCESS = `Usage: candle keys access <prefix|label|self> --access <read|read-write|read-write-transfer> [--yes] [--json]
59352
+ ` + " candle keys access <prefix|label> --history [--json]";
59353
+ var SELF = "self";
59354
+ var CLI_SPELLING = Object.fromEntries(Object.entries(ACCESS_LEVELS).map(([spelling, preset]) => [preset, spelling]));
59355
+ var NO_API_KEY2 = {
59356
+ code: "API_KEY_REQUIRED",
59357
+ message: "candle keys access self changes the profile's own API key, and this profile holds none.",
59358
+ suggestion: "Select the profile holding the key with --profile, or name the key by prefix to use the device token."
59359
+ };
59360
+ var REQUIRES_TTY = {
59361
+ code: "KEY_ACCESS_REQUIRES_TTY",
59362
+ message: "Widening a key needs a terminal: the key prefix is typed back, and nothing else supplies it.",
59363
+ suggestion: "Run it in an interactive shell; there is no flag and no environment variable to widen a key."
59364
+ };
59365
+ function levelName(side) {
59366
+ const preset = side.access ?? presetForScopes(side.scopes);
59367
+ if (preset)
59368
+ return AGENT_KEY_PRESET_LABELS[preset];
59369
+ return `custom (${accessCell(side.scopes)})`;
58942
59370
  }
58943
- async function tradingRead(ctx, client, read) {
58944
- try {
58945
- return await read();
58946
- } catch (error) {
58947
- if (isRateLimited(error))
58948
- throw tradingRateLimited(ctx, client.endpoint.host, error);
58949
- throw error;
58950
- }
59371
+ function keyName2(keyPrefix, label) {
59372
+ const cleaned = labelCell(label ?? undefined);
59373
+ return cleaned ? `${keyPrefix} (${cleaned})` : keyPrefix;
58951
59374
  }
58952
- function printTradingResult(ctx, result) {
58953
- ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(result)}
58954
- ` : `${JSON.stringify(result, null, 2)}
58955
- `);
58956
- return 0;
59375
+ function listWords(words) {
59376
+ return words.length <= 1 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
58957
59377
  }
58958
- var settlementSchema = exports_external.object({
58959
- state: exports_external.enum(["settled", "pending", "failed", "uncertain"]),
58960
- settledOutRaw: exports_external.string().regex(/^\d+$/).optional()
58961
- });
58962
- function reportSettlement(ctx, settlement, out, id) {
58963
- const parsed = settlementSchema.safeParse(settlement);
58964
- const reading = parsed.success ? parsed.data : undefined;
58965
- if (reading?.state === "settled" && reading.settledOutRaw !== undefined)
58966
- ctx.deps.stderr.write(`settled ${decimalAmount(reading.settledOutRaw, out.decimals)} ${out.asset}
58967
- `);
58968
- else {
58969
- const state = !reading ? "not measured" : reading.state === "settled" ? "settled, amount not measured" : reading.state;
58970
- ctx.deps.stderr.write(`Settlement: ${state}. Re-check with GET /api/v1/agent/swap/jobs/${id} (candle swap status ${id}).
58971
- `);
59378
+ function linkedTransferLine(ready) {
59379
+ const assets = Object.entries(ready.assets);
59380
+ const missing = assets.filter(([, ok]) => !ok).map(([asset]) => asset.toUpperCase());
59381
+ const readyAssets = assets.filter(([, ok]) => ok).map(([asset]) => asset.toUpperCase());
59382
+ const needs = [];
59383
+ if (!ready.txLimit)
59384
+ needs.push("a USD transaction limit");
59385
+ if (missing.length > 0) {
59386
+ needs.push(`${missing.map((asset) => `a ${asset}`).join(" and ")} spend cap`);
58972
59387
  }
58973
- return settlement !== null && typeof settlement === "object" ? { settlement } : {};
58974
- }
58975
- function validClientId(id) {
58976
- return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id);
59388
+ if (needs.length === 0)
59389
+ return null;
59390
+ const readyPart = readyAssets.length > 0 ? `; ${listWords(readyAssets)} ${readyAssets.length === 1 ? "is" : "are"} ready` : "";
59391
+ return [
59392
+ `Warning: to send to linked wallets it also needs ${needs.join(" and ")}${readyPart}.`,
59393
+ " Set them in the web key manager: Agents, Keys, this key, Spend limits."
59394
+ ].join(`
59395
+ `);
58977
59396
  }
58978
- async function lookupOperation(ctx, key, id, kind) {
58979
- const local = await savedOperation(ctx, key, id);
58980
- const kinds = kind ? [kind] : local && local.kind !== "lp" ? [local.kind] : ["trade", "swap", "launch"];
58981
- const found = [];
58982
- for (const candidate of kinds) {
58983
- try {
58984
- found.push({
58985
- ...operationSchema.parse(await request(ctx, key, jobPath(candidate, id))),
58986
- kind: candidate,
58987
- clientTradeId: id
58988
- });
58989
- } catch (error) {
58990
- if (!(error instanceof TradingError && error.code === "JOB_NOT_FOUND"))
58991
- throw error;
59397
+ var LOCALLY_RENDERED = new Set(["MOVES_FUNDS", "VAULT_TRANSFER_READY", "LINKED_TRANSFER_NEEDS_LIMITS"]);
59398
+ function accessScreen(shown, identity) {
59399
+ const environment = candleEnvironment(identity.apiUrl) ?? "not a Candle host";
59400
+ const label = labelCell(shown.label ?? undefined);
59401
+ const lines = [
59402
+ `Key ${shown.keyPrefix}${label ? ` ${label}` : ""}`,
59403
+ `Candle account ${identity.account}`,
59404
+ `API ${identity.apiUrl} (${environment})`,
59405
+ "",
59406
+ `Access ${levelName(shown.from)} -> ${levelName(shown.to)} (${shown.direction})`,
59407
+ `Adds ${shown.added.length > 0 ? formatScopesForSummary(shown.added) : "nothing"}`,
59408
+ `Removes ${shown.removed.length > 0 ? shown.removed.join(", ") : "nothing"}`,
59409
+ ""
59410
+ ];
59411
+ const bound = shown.effects.boundTeeWallets;
59412
+ if (bound && bound.count > 0) {
59413
+ const names = bound.sample.map((w) => labelCell(w.label ?? undefined) || shortAddress(w.address));
59414
+ const more = bound.count > names.length ? `, and ${bound.count - names.length} more` : "";
59415
+ lines.push(`TEE wallets on this key: ${bound.count} (${names.join(", ")}${more})`);
59416
+ if (shown.direction === "widen" && shown.added.includes("transfer:bound")) {
59417
+ lines.push("It can move these wallets' funds to their vaults as soon as you confirm.");
58992
59418
  }
58993
59419
  }
58994
- if (found.length > 1)
58995
- throw new TradingError("AMBIGUOUS_OPERATION", "This id exists on multiple rails; specify --kind trade, swap or launch.");
58996
- return found[0] ? { ...found[0], ...local?.signature ? { signature: local.signature } : {} } : null;
58997
- }
58998
- async function swapStatus(args, ctx) {
58999
- const parsed = parseArgs(args, { valueFlags: ["--kind"], booleanFlags: ["--wait"] });
59000
- if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "") || parsed.values["--kind"] !== undefined && !["trade", "swap", "launch"].includes(parsed.values["--kind"])) {
59001
- writeUsageFailure(ctx.deps, "Usage: candle swap status <id> [--kind trade|swap|launch] [--wait]", ctx.json);
59002
- return 2;
59003
- }
59004
- try {
59005
- const key = await tradingKey(ctx);
59006
- const id = parsed.positionals[0];
59007
- const kind = parsed.values["--kind"];
59008
- const facts = bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge);
59009
- const found = await lookupOperation(ctx, key, id, kind);
59010
- if (!found)
59011
- throw new TradingError("JOB_NOT_FOUND", "No operation found on the selected rail(s). This command does not resend a write.");
59012
- const result = parsed.booleans.has("--wait") ? await waitForSettlement(ctx, key, id, found, facts) : { ...found, ...bridgeStatusField(ctx, found, facts) };
59013
- return printTradingResult(ctx, result);
59014
- } catch (error) {
59015
- return tradingFailure(ctx, error);
59016
- }
59017
- }
59018
- var bridgeFactsSchema = exports_external.object({
59019
- from: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
59020
- to: exports_external.enum(["SOL", "USDC", "ETH", "USDG"]),
59021
- recipient: exports_external.string(),
59022
- statusCheck: exports_external.string().optional()
59023
- });
59024
- function bridgeFactsOf(raw) {
59025
- const parsed = bridgeFactsSchema.safeParse(raw);
59026
- return parsed.success ? parsed.data : undefined;
59027
- }
59028
- function isBridgeJob(job, facts) {
59029
- return facts !== undefined || job.bridge !== null && typeof job.bridge === "object";
59030
- }
59031
- function bridgeStatusField(ctx, found, facts) {
59032
- const job = found.job ?? {};
59033
- if (found.kind !== "swap" || !isBridgeJob(job, facts))
59034
- return {};
59035
- const status = describeBridgeJob(job, facts);
59036
- for (const line of status.lines)
59037
- ctx.deps.stderr.write(`${line}
59038
- `);
59039
- const { lines: _lines, ...rest } = status;
59040
- return { bridgeStatus: rest };
59041
- }
59042
- function waitFinished(current, facts, sent) {
59043
- const job = current.job ?? {};
59044
- if (current.kind === "swap" && isBridgeJob(job, facts)) {
59045
- const status = describeBridgeJob(job, facts);
59046
- if (sent && status.phase === "not_broadcast")
59047
- return false;
59048
- return bridgeStatusFinal(status);
59420
+ const ready = shown.effects.linkedTransferReady;
59421
+ if (shown.direction === "widen" && ready) {
59422
+ const line = linkedTransferLine(ready);
59423
+ if (line)
59424
+ lines.push(line);
59049
59425
  }
59050
- const settlement = settlementSchema.safeParse(job.settlement);
59051
- return !settlement.success || settlement.data.state !== "pending";
59052
- }
59053
- async function waitForSettlement(ctx, key, id, first, facts, sent = false) {
59054
- const deadline = ctx.deps.now() + BRIDGE_WAIT_MS;
59055
- let current = first;
59056
- while (!waitFinished(current, facts, sent) && ctx.deps.now() < deadline) {
59057
- await ctx.deps.sleep(BRIDGE_WAIT_POLL_MS);
59058
- current = { ...current, ...operationSchema.parse(await request(ctx, key, jobPath(current.kind, id))) };
59426
+ for (const warning of shown.warnings) {
59427
+ if (!LOCALLY_RENDERED.has(warning.code))
59428
+ lines.push(`Warning: ${warning.message}`);
59059
59429
  }
59060
- const field = bridgeStatusField(ctx, current, facts);
59061
- const finished = waitFinished(current, facts, sent);
59062
- if (!finished)
59063
- ctx.deps.stderr.write(`Still not final after ten minutes. Re-check with candle swap status ${id} --wait.
59430
+ if (lines[lines.length - 1] !== "")
59431
+ lines.push("");
59432
+ return lines.join(`
59064
59433
  `);
59065
- return { ...current, ...field, waited: { final: finished } };
59066
59434
  }
59067
- async function decimalsFor(ctx, asset, client) {
59068
- if (BASES[asset])
59069
- return BASES[asset].decimals;
59070
- const reader = await client();
59071
- let result;
59072
- try {
59073
- result = await tradingRead(ctx, reader, () => reader.rpc.getTokenSupply(asset));
59074
- } catch (error) {
59075
- if (error instanceof TradingError)
59076
- throw error;
59077
- throw new TradingError("RPC_FAILED", "Solana getTokenSupply failed; no automatic re-send was made.");
59435
+ function accessFailureDetails(result, context) {
59436
+ let code = result.code;
59437
+ let message = result.message;
59438
+ let suggestion;
59439
+ if (result.status === 404 && result.code === undefined) {
59440
+ code = "KEY_ACCESS_UNSUPPORTED";
59441
+ message = context.history ? "This Candle API has no key access history yet." : "This Candle API cannot change a key's access yet; nothing changed.";
59442
+ } else if (result.code === "LOOSEN_REQUIRES_SESSION" && context.self) {
59443
+ const level = context.preset ? CLI_SPELLING[context.preset] : "<level>";
59444
+ suggestion = `Widen it from a signed-in session or with the device token: candle keys access <prefix> --access ${level}`;
59445
+ } else if (result.code === "KEY_ACCESS_STALE") {
59446
+ suggestion = "Run the command again; the preview will show the key's current access.";
59447
+ } else if (result.code === "KEY_ACCESS_PARTNER_KEY") {
59448
+ suggestion = "Partner keys are managed by Candle; ask your Candle contact to change one.";
59449
+ } else if (result.status === 404 && result.code === "VALIDATION_FAILED") {
59450
+ suggestion = "Run: candle keys list, to see this account's keys and their prefixes.";
59078
59451
  }
59079
- const decimals = result.decimals;
59080
- if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 18)
59081
- throw new TradingError("INVALID_RESPONSE", "RPC returned invalid mint decimals.");
59082
- return decimals;
59452
+ return { code: code ?? `HTTP ${result.status}`, message, ...suggestion !== undefined ? { suggestion } : {} };
59083
59453
  }
59084
- async function assertDeferredExecuteSupported(ctx, key, id) {
59085
- try {
59086
- await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
59087
- } catch (error) {
59088
- if (error instanceof TradingError && error.code === "JOB_NOT_FOUND")
59089
- return;
59090
- throw new TradingError("EMBEDDED_PAYER_UNSUPPORTED", "This Candle deployment cannot hold an embedded-wallet trade back for confirmation, so the quote could not be shown before the money moved. Nothing was built. Name a TEE wallet with --wallet, or point at a deployment that has it.");
59454
+ function writeAccessFailure(ctx, result, context) {
59455
+ const { deps, apiUrl, json } = ctx;
59456
+ const { code, message, suggestion } = accessFailureDetails(result, context);
59457
+ const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: context.self ? "key" : "device" });
59458
+ if (json) {
59459
+ deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
59460
+ `);
59461
+ } else {
59462
+ deps.stderr.write(`${code}: ${message}${suggestion ? ` ${suggestion}` : ""}
59463
+ `);
59091
59464
  }
59092
- throw new TradingError("INVALID_RESPONSE", "The execute route answered for a trade that does not exist.");
59465
+ return 1;
59093
59466
  }
59094
- async function swap(args, ctx) {
59095
- const parsed = parseArgs(args, {
59096
- valueFlags: ["--amount", "--percent", "--wallet", "--to", "--client-trade-id", "--slippage-bps", "--rpc-url"],
59097
- booleanFlags: ["--yes", "--wait"]
59098
- });
59467
+ async function keysAccess(args, ctx) {
59468
+ const { deps, apiUrl, json } = ctx;
59469
+ const parsed = parseArgs(args, { valueFlags: ["--access"], booleanFlags: ["--yes", "--history"] });
59099
59470
  if ("error" in parsed) {
59100
- writeUsageFailure(ctx.deps, parsed.error, ctx.json);
59471
+ writeUsageFailure(deps, parsed.error, json);
59101
59472
  return 2;
59102
59473
  }
59103
- const flags = parsed.values;
59104
- const id = flags["--client-trade-id"] ?? `swap-${randomUUID()}`;
59105
- const slippage = Number(flags["--slippage-bps"] ?? "50");
59106
- if (parsed.positionals.length !== 2 || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
59107
- writeUsageFailure(ctx.deps, "Usage: candle swap <from> <to> --amount <decimal> | --percent <n> [--wallet <tee-or-embedded>] [--to <tee>] [--wait] [--client-trade-id <id>] [--slippage-bps 50] [--rpc-url <url>] [--yes]. Solana: SOL, USDC, CNDL or a mint. Hood: ETH, USDG or a 0x token. SOL or USDC to ETH or USDG, or back, is a bridge between two TEE wallets.", ctx.json);
59474
+ const [target, extra] = parsed.positionals;
59475
+ if (target === undefined || extra !== undefined) {
59476
+ writeUsageFailure(deps, `${extra !== undefined ? `Unexpected argument: ${extra}. ` : "Name one key. "}${USAGE_ACCESS}`, json);
59108
59477
  return 2;
59109
59478
  }
59110
- try {
59111
- const fromAsset = classifyAsset(parsed.positionals[0]);
59112
- const toAsset = classifyAsset(parsed.positionals[1]);
59113
- const bridge = bridgePair(fromAsset, toAsset);
59114
- if (bridge)
59115
- return await bridgeSwap(ctx, {
59116
- flags,
59117
- yes: parsed.booleans.has("--yes"),
59118
- wait: parsed.booleans.has("--wait"),
59119
- id,
59120
- slippage,
59121
- ...bridge
59122
- });
59123
- if (flags["--to"] !== undefined || parsed.booleans.has("--wait"))
59124
- throw new TradingUsage("--to and --wait are for a bridge: SOL or USDC to ETH or USDG, or back.");
59125
- const chain2 = pairChain(fromAsset, toAsset);
59126
- const walletFlag = flags["--wallet"];
59127
- const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
59128
- if (named !== undefined && named !== chain2)
59129
- throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, chain2);
59130
- if (chain2 === "hood")
59131
- return await hoodSwap(ctx, {
59132
- flags,
59133
- yes: parsed.booleans.has("--yes"),
59134
- id,
59135
- slippage,
59136
- from: fromAsset,
59137
- to: toAsset
59138
- });
59139
- const from = fromAsset.asset;
59140
- const to = toAsset.asset;
59141
- if (from === to)
59142
- throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
59143
- const fromBase = baseAsset(from);
59144
- const toBase = baseAsset(to);
59145
- if (!fromBase && !toBase)
59146
- throw new TradingError("PAIR_UNSUPPORTED", "A token trade must have SOL, USDC or CNDL on one side; token-to-token routing is unavailable.");
59147
- if (flags["--amount"])
59148
- rawAmount(flags["--amount"], 18);
59149
- const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
59150
- if (percent !== undefined && percent > 100000000n)
59151
- throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
59152
- const kind = fromBase && toBase ? "swap" : "trade";
59153
- const key = await tradingKey(ctx);
59154
- const prior = await lookupOperation(ctx, key, id, kind);
59155
- if (prior)
59156
- return printTradingResult(ctx, prior);
59157
- const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
59158
- if (payerWallet.kind === "embedded" && kind === "swap")
59159
- throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
59160
- const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
59161
- const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
59162
- const decimals = await decimalsFor(ctx, from, solana);
59163
- const outDecimals = await decimalsFor(ctx, to, solana);
59164
- let amountRaw;
59165
- if (percent !== undefined) {
59166
- const reader = await solana();
59167
- let balance;
59168
- if (from === "SOL")
59169
- balance = await tradingRead(ctx, reader, () => reader.rpc.getBalance(wallet.address));
59170
- else {
59171
- const mint = BASES[from]?.mint ?? from;
59172
- const accounts = (await tradingRead(ctx, reader, () => Promise.all([
59173
- reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_PROGRAM_ID),
59174
- reader.rpc.getTokenAccountsByOwner(wallet.address, TOKEN_2022_PROGRAM_ID)
59175
- ]))).flat();
59176
- balance = accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
59177
- }
59178
- amountRaw = (balance * percent / 100000000n).toString();
59179
- if (amountRaw === "0")
59180
- throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
59181
- } else
59182
- amountRaw = rawAmount(flags["--amount"], decimals);
59183
- if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
59184
- throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
59185
- if (payerWallet.kind === "embedded")
59186
- await assertDeferredExecuteSupported(ctx, key, id);
59187
- if (!await claimOperation(ctx, key, id, kind))
59188
- throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
59189
- ctx.deps.stderr.write(`Operation: ${id}
59190
- `);
59191
- const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
59192
- const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
59193
- clientTradeId: id,
59194
- from,
59195
- to,
59196
- amountRaw,
59197
- maxSlippageBps: slippage,
59198
- payer
59199
- }) : await request(ctx, key, "/api/v1/trade/agent/build", {
59200
- clientTradeId: id,
59201
- chain: "solana",
59202
- mint: fromBase ? to : from,
59203
- side: fromBase ? "buy" : "sell",
59204
- quoteAsset: (fromBase ?? toBase)?.toLowerCase(),
59205
- amountRaw,
59206
- maxSlippageBps: slippage,
59207
- payer,
59208
- ...payerWallet.kind === "embedded" ? { deferExecution: true } : {}
59209
- });
59210
- if (built.job || built.status === "executed")
59211
- return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
59212
- const data = swapBuildSchema.parse(kind === "swap" ? built.payload : built);
59213
- if (kind === "swap" && (data.venue !== "jupiter" || data.recipient !== wallet.address))
59214
- throw new TradingError("INVALID_RESPONSE", "A TEE base swap must use Jupiter and return to its payer.");
59215
- if (kind === "trade" && (built.chain !== "solana" || built.walletAddress !== wallet.address))
59216
- throw new TradingError("INVALID_RESPONSE", "The token build does not name the requested Solana payer.");
59217
- const artifacts = kind === "swap" ? { ...data, transactionBase64: undefined, quoteSource: undefined, quoteAsset: undefined } : data.artifacts;
59218
- if (!artifacts)
59219
- throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
59220
- if (kind === "trade" && artifacts.quoteAsset !== (fromBase ?? toBase)?.toLowerCase())
59221
- throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not the requested pair. Nothing was signed.`);
59222
- if (data?.status !== "built" || typeof data.minOutRaw !== "string" || !/^\d+$/.test(data.minOutRaw) || !data.fee || !Number.isFinite(data.fee.bps))
59223
- throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete quote; nothing was signed.");
59224
- const minimumRaw = !fromBase && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
59225
- const quote = {
59226
- intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from} to ${to}`,
59227
- wallet: wallet.address,
59228
- venue: artifacts.quoteSource ?? artifacts.venue,
59229
- priceImpactPct: artifacts.priceImpactPct ?? null,
59230
- fee: data.fee,
59231
- minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to}`,
59232
- minOutRaw: data.minOutRaw,
59233
- minimumReceivedRaw: minimumRaw,
59234
- tokenRisks: artifacts.tokenRisks ?? []
59235
- };
59236
- if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
59237
- return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind, quote });
59238
- if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
59239
- throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
59240
- if (payerWallet.kind === "embedded") {
59241
- const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
59242
- return printTradingResult(ctx, {
59243
- ...executed,
59244
- clientTradeId: id,
59245
- kind,
59246
- quote,
59247
- wallet: safeText(payerWallet.address)
59248
- });
59479
+ const accessFlag = parsed.values["--access"];
59480
+ const history = parsed.booleans.has("--history");
59481
+ const yes = parsed.booleans.has("--yes");
59482
+ if (accessFlag === undefined === !history) {
59483
+ writeUsageFailure(deps, `Pass exactly one of --access and --history. ${USAGE_ACCESS}`, json);
59484
+ return 2;
59485
+ }
59486
+ if (history) {
59487
+ if (yes) {
59488
+ writeUsageFailure(deps, "--yes only applies to a change, not to --history.", json);
59489
+ return 2;
59249
59490
  }
59250
- const transaction = kind === "swap" ? data.transactionsBase64?.[0] : artifacts.transactionBase64;
59251
- if (kind === "swap" && data.transactionsBase64?.length !== 1)
59252
- throw new TradingError("INVALID_RESPONSE", "A TEE swap must contain exactly one same-chain transaction.");
59253
- if (!transaction)
59254
- throw new TradingError("INVALID_RESPONSE", "Missing transaction.");
59255
- const signed = await relaySign(ctx, key, payerWallet.wallet, transaction);
59256
- const result = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", {
59257
- clientTradeId: id,
59258
- swapId: data.swapId,
59259
- signedTransactionsBase64: [signed]
59260
- }) : await request(ctx, key, "/api/v1/trade/agent/submit", { clientTradeId: id, signedTransactions: [signed] });
59261
- const settled = kind === "swap" ? reportSettlement(ctx, result.payload?.settlement, { asset: to, decimals: outDecimals }, id) : {};
59262
- return printTradingResult(ctx, {
59263
- ...result,
59264
- clientTradeId: id,
59265
- kind,
59266
- quote,
59267
- wallet: safeText(wallet.address),
59268
- ...settled
59269
- });
59270
- } catch (error) {
59271
- return tradingFailure(ctx, error, id);
59491
+ if (target === SELF) {
59492
+ writeUsageFailure(deps, "--history needs the device token: name the key by prefix or label, not self.", json);
59493
+ return 2;
59494
+ }
59495
+ return keysAccessHistory(target, ctx);
59272
59496
  }
59273
- }
59274
- function lazyEvmRpc(ctx, flag) {
59275
- let rpc;
59276
- return () => {
59277
- if (rpc)
59278
- return rpc;
59279
- const resolved = resolveEvmRpcUrl(flag, ctx.deps.env[EVM_RPC_URL_ENV], "--rpc-url");
59280
- if ("error" in resolved)
59281
- throw new TradingUsage(resolved.error);
59282
- ctx.deps.stderr.write(`Reading from ${rpcHostOf2(resolved.url)} (Hood RPC; reads only, nothing is sent there)
59283
- `);
59284
- rpc = createEvmRpc(resolved.url, ctx.deps.fetch);
59285
- return rpc;
59286
- };
59287
- }
59288
- async function evmRead(what, read) {
59289
- try {
59290
- return await read();
59291
- } catch (error) {
59292
- if (error instanceof TradingError || error instanceof TradingUsage)
59293
- throw error;
59294
- throw new TradingError("RPC_FAILED", `Reading ${what} over the Hood RPC failed; nothing was built or signed.`);
59497
+ const preset = ACCESS_LEVELS[accessFlag];
59498
+ if (preset === undefined) {
59499
+ writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
59500
+ return 2;
59295
59501
  }
59296
- }
59297
- async function hoodDecimals(asset, rpc) {
59298
- const base = asset.base ? HOOD_BASES[asset.base] : undefined;
59299
- if (base)
59300
- return base.decimals;
59301
- const decimals = await evmRead(`${asset.asset} decimals()`, () => rpc().erc20Decimals(asset.asset));
59302
- if (decimals > 36)
59303
- throw new TradingError("INVALID_RESPONSE", "The token's decimals() is out of range.");
59304
- return decimals;
59305
- }
59306
- async function recordTradedToken(ctx, wallet, token) {
59307
- const skipped = (reason) => `Notice: the sealed EVM record was not updated for ${token} (${reason}). The leg landed. A later sweep still finds this token with --token ${token}, or with --from-block.`;
59308
- const append = ctx.deps.appendEvmRecord;
59309
- let notice;
59310
- if (!append)
59311
- notice = skipped("this CLI build has no sealed EVM record writer");
59312
- else {
59313
- try {
59314
- const outcome = await append(ctx, { kind: "token", wallet: toChecksumAddress(wallet), token });
59315
- notice = outcome.appended ? outcome.notice : skipped(outcome.notice);
59316
- } catch (error) {
59317
- notice = skipped(error instanceof Error ? error.message : "the append failed");
59502
+ await printIdentity(ctx);
59503
+ const self = target === SELF;
59504
+ let path;
59505
+ let auth;
59506
+ let credentials;
59507
+ if (self) {
59508
+ const apiKey = await resolveApiKey(deps, ctx.profile);
59509
+ if (!apiKey) {
59510
+ writeLocalFailure(deps, NO_API_KEY2, json);
59511
+ return 1;
59512
+ }
59513
+ path = `${KEYS_PATH4}/self/access`;
59514
+ auth = "key";
59515
+ credentials = { apiKey };
59516
+ } else {
59517
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
59518
+ if (!deviceToken) {
59519
+ writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
59520
+ return 1;
59318
59521
  }
59522
+ const resolved = await resolveTargetKey(ctx, deviceToken, target, {
59523
+ codePrefix: "KEY_ACCESS",
59524
+ writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false })
59525
+ });
59526
+ if (!resolved.ok)
59527
+ return resolved.code;
59528
+ path = `${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access`;
59529
+ auth = "device";
59530
+ credentials = { deviceToken };
59319
59531
  }
59320
- if (notice)
59321
- ctx.deps.stderr.write(`${safeText(notice)}
59532
+ const failureContext = { self, preset };
59533
+ const call = (body) => apiRequest(path, { method: "PUT", body, auth, credentials, apiUrl, fetch: deps.fetch, env: deps.env });
59534
+ const preview = await call({ access: preset, dryRun: true });
59535
+ if (!preview.ok)
59536
+ return writeAccessFailure(ctx, preview, failureContext);
59537
+ const shown = preview.body;
59538
+ const name2 = keyName2(shown.keyPrefix, shown.label);
59539
+ if (shown.direction === "unchanged") {
59540
+ if (json)
59541
+ deps.stdout.write(`${JSON.stringify({ ...shown, command: "keys access" })}
59322
59542
  `);
59323
- return notice;
59324
- }
59325
- var HOOD_TRADE_LEGS = ["approval", "permit2Approval", "trade", "feeTransfer"];
59326
- function describeHoodLegs(first, hasFee, primary = "trade") {
59327
- const kinds = plannedLegKinds(first.legKind, first.plannedLegCount, hasFee, primary);
59328
- const names = {
59329
- approval: "approve",
59330
- permit2Approval: "Permit2 approve",
59331
- trade: "trade",
59332
- feeTransfer: "fee",
59333
- bridgeDeposit: "bridge deposit"
59334
- };
59335
- return kinds ? kinds.map((kind) => names[kind] ?? kind) : [`${first.plannedLegCount} legs, starting with ${names[first.legKind] ?? first.legKind}`];
59336
- }
59337
- async function hoodSwap(ctx, args) {
59338
- const { flags, id, from, to } = args;
59339
- if (from.asset === to.asset)
59340
- throw new TradingError("PAIR_UNSUPPORTED", "Choose two distinct assets.");
59341
- if (!from.base && !to.base)
59342
- throw new TradingError("PAIR_UNSUPPORTED", "A Hood token trade must have ETH or USDG on one side; token-to-token routing is unavailable.");
59343
- if (flags["--amount"])
59344
- rawAmount(flags["--amount"], 18);
59345
- const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
59346
- if (percent !== undefined && percent > 100000000n)
59347
- throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
59348
- const kind = from.base && to.base ? "swap" : "trade";
59349
- const key = await tradingKey(ctx);
59350
- const prior = await lookupOperation(ctx, key, id, kind);
59351
- if (prior)
59352
- return printTradingResult(ctx, prior);
59353
- const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
59354
- if (payer.kind === "embedded" && kind === "swap")
59355
- throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap ETH and USDG from this command: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a Hood TEE wallet.");
59356
- const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
59357
- const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
59358
- const decimals = await hoodDecimals(from, rpc);
59359
- const outDecimals = await hoodDecimals(to, rpc);
59360
- let amountRaw;
59361
- if (percent !== undefined) {
59362
- const balance = from.asset === "ETH" ? await evmRead("the ETH balance", () => rpc().getBalance(payerAddress)) : await evmRead(`the ${from.asset} balance`, () => rpc().erc20BalanceOf(from.asset === "USDG" ? HOOD_USDG_ADDRESS : from.asset, payerAddress));
59363
- amountRaw = (balance * percent / 100000000n).toString();
59364
- if (amountRaw === "0")
59365
- throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
59366
- } else
59367
- amountRaw = rawAmount(flags["--amount"], decimals);
59368
- if (payer.kind === "embedded")
59369
- await assertDeferredExecuteSupported(ctx, key, id);
59370
- if (!await claimOperation(ctx, key, id, kind))
59371
- throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
59372
- ctx.deps.stderr.write(`Operation: ${id}
59543
+ else
59544
+ deps.stdout.write(`${name2} is already ${levelName(shown.to)}. Nothing changed.
59373
59545
  `);
59374
- const payerBody = payer.kind === "tee" ? { type: "linked", linkedWalletId: payer.wallet.id } : { type: "main" };
59375
- const base = from.base ?? to.base;
59376
- const token = from.base ? to.asset : from.asset;
59377
- const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
59378
- clientTradeId: id,
59379
- from: from.asset,
59380
- to: to.asset,
59381
- amountRaw,
59382
- maxSlippageBps: args.slippage,
59383
- payer: payerBody
59384
- }) : await request(ctx, key, "/api/v1/trade/agent/build", {
59385
- clientTradeId: id,
59386
- chain: "hood",
59387
- mint: token,
59388
- side: from.base ? "buy" : "sell",
59389
- quoteAsset: base.toLowerCase(),
59390
- amountRaw,
59391
- maxSlippageBps: args.slippage,
59392
- payer: payerBody,
59393
- ...payer.kind === "embedded" ? { deferExecution: true } : {}
59394
- });
59395
- if (built.job || built.status === "executed")
59396
- return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
59397
- const body = kind === "swap" ? built.payload : built;
59398
- const data = swapBuildSchema.parse(body);
59399
- const echoed = kind === "swap" ? body?.recipient : body?.walletAddress;
59400
- if (body?.chain !== "hood" || typeof echoed !== "string" || echoed.toLowerCase() !== payerAddress.toLowerCase())
59401
- throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
59402
- const artifacts = kind === "swap" ? { ...data, quoteAsset: undefined, quoteSource: undefined } : data.artifacts;
59403
- if (!artifacts)
59404
- throw new TradingError("INVALID_RESPONSE", "Missing quote artifacts.");
59405
- if (kind === "trade" && artifacts.quoteAsset !== base.toLowerCase())
59406
- throw new TradingError("PAIR_UNSUPPORTED", `This token settles in ${artifacts.quoteAsset ?? "an unknown asset"}, not ${base}. Nothing was signed.`);
59407
- let sequenced;
59408
- if (payer.kind === "tee") {
59409
- const parsed = sequencedSchema.safeParse(body);
59410
- if (!parsed.success)
59411
- throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet trades one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
59412
- sequenced = parsed.data;
59546
+ return 0;
59413
59547
  }
59414
- const minimumRaw = !from.base && artifacts.venue === "curve" ? (BigInt(data.minOutRaw) > BigInt(data.fee.feeRaw) ? BigInt(data.minOutRaw) - BigInt(data.fee.feeRaw) : 0n).toString() : data.minOutRaw;
59415
- const quote = {
59416
- intent: `Swap ${decimalAmount(amountRaw, decimals)} ${from.asset} to ${to.asset} on Hood`,
59417
- wallet: payerAddress,
59418
- venue: artifacts.quoteSource ?? artifacts.venue,
59419
- priceImpactPct: artifacts.priceImpactPct ?? null,
59420
- fee: data.fee,
59421
- minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to.asset}`,
59422
- minOutRaw: data.minOutRaw,
59423
- minimumReceivedRaw: minimumRaw,
59424
- tokenRisks: artifacts.tokenRisks ?? []
59425
- };
59426
- if (sequenced) {
59427
- const leg = sequenced.nextLeg;
59428
- const maxFee = BigInt(leg.maxFeePerGas);
59429
- const reserve = sweepReserveFloor(maxFee, kind === "trade" ? [token] : []);
59430
- quote.legs = describeHoodLegs(sequenced, BigInt(data.fee.feeRaw) > 0n);
59431
- quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
59432
- quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a trade never spends it`;
59433
- quote.operationId = sequenced.operationId;
59548
+ const widen = shown.direction === "widen";
59549
+ const interactive = deps.isTTY.stdin && deps.isTTY.stdout;
59550
+ if (widen && yes) {
59551
+ writeUsageFailure(deps, "--yes only skips the prompt when narrowing; nothing changed.", json);
59552
+ return 2;
59434
59553
  }
59435
- if (!await confirmQuote(ctx, quote, args.yes)) {
59436
- if (sequenced)
59437
- ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}; a new Hood trade from it is refused as WALLET_BUSY until then.
59554
+ if (widen && !interactive) {
59555
+ writeLocalFailure(deps, REQUIRES_TTY, json);
59556
+ return 1;
59557
+ }
59558
+ if (!widen && !yes && !interactive) {
59559
+ writeUsageFailure(deps, "Narrowing a key without a terminal needs --yes; nothing changed. Run it in an interactive shell, or pass --yes.", json);
59560
+ return 2;
59561
+ }
59562
+ const config = await deps.readConfig();
59563
+ const fields = effectiveProfileFields(config, ctx.profile);
59564
+ const account = fields.account ? shortAddress(fields.account) : "unknown";
59565
+ deps.stderr.write(`${accessScreen(shown, { account: fields.username ? `${fields.username} (${account})` : account, apiUrl })}
59566
+ `);
59567
+ if (widen) {
59568
+ const typed = await deps.promptLine(`Type the key prefix ${shown.keyPrefix} to widen it: `);
59569
+ if (typed.trim() !== shown.keyPrefix) {
59570
+ writeLocalFailure(deps, {
59571
+ code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
59572
+ message: `The acknowledgement is the key prefix ${shown.keyPrefix}; nothing changed.`,
59573
+ suggestion: `Run the command again and type ${shown.keyPrefix} at the prompt.`
59574
+ }, json);
59575
+ return 1;
59576
+ }
59577
+ } else if (!yes) {
59578
+ const answer = await deps.promptLine(`Change ${name2} from ${levelName(shown.from)} to ${levelName(shown.to)}? [y/N] `);
59579
+ if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
59580
+ writeLocalFailure(deps, {
59581
+ code: "KEY_ACCESS_NOT_ACKNOWLEDGED",
59582
+ message: "Not confirmed; nothing changed.",
59583
+ suggestion: "Run the command again and answer y, or pass --yes."
59584
+ }, json);
59585
+ return 1;
59586
+ }
59587
+ }
59588
+ const committed = await call({ access: preset, expectScopes: shown.from.scopes });
59589
+ if (!committed.ok)
59590
+ return writeAccessFailure(ctx, committed, failureContext);
59591
+ const result = committed.body;
59592
+ if (fields.keyPrefix !== undefined && fields.keyPrefix === result.keyPrefix && result.direction !== "unchanged") {
59593
+ const scopes = scopesForPreset(result.to.access);
59594
+ if (ctx.profile)
59595
+ await deps.updateProfile(ctx.profile, { scopes });
59596
+ else
59597
+ await deps.writeConfig({ scopes });
59598
+ }
59599
+ if (json) {
59600
+ deps.stdout.write(`${JSON.stringify({ ...result, command: "keys access" })}
59601
+ `);
59602
+ return 0;
59603
+ }
59604
+ if (result.direction === "unchanged") {
59605
+ deps.stdout.write(`${keyName2(result.keyPrefix, result.label)} is already ${levelName(result.to)}. Nothing changed.
59438
59606
  `);
59439
- return printTradingResult(ctx, {
59440
- success: true,
59441
- status: "cancelled",
59442
- clientTradeId: id,
59443
- kind,
59444
- quote,
59445
- ...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
59446
- });
59607
+ return 0;
59447
59608
  }
59448
- if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
59449
- throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
59450
- if (payer.kind === "embedded") {
59451
- const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
59452
- return printTradingResult(ctx, { ...executed, clientTradeId: id, kind, quote, wallet: safeText(payerAddress) });
59609
+ deps.stdout.write(`Changed ${keyName2(result.keyPrefix, result.label)} to ${levelName(result.to)}.` + `${result.changeId ? ` Change id ${result.changeId}` : ""}
59610
+ `);
59611
+ return 0;
59612
+ }
59613
+ async function keysAccessHistory(target, ctx) {
59614
+ const { deps, apiUrl, json } = ctx;
59615
+ await printIdentity(ctx);
59616
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
59617
+ if (!deviceToken) {
59618
+ writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
59619
+ return 1;
59453
59620
  }
59454
- const wallet = payer.wallet;
59455
- if (wallet.chain !== "evm" || !sequenced)
59456
- throw chainMismatch(`TEE wallet ${wallet.id}`, wallet.chain === "evm" ? "hood" : "solana", "hood");
59457
- const recorded = kind === "trade" ? token : HOOD_USDG_ADDRESS;
59458
- const notices = [];
59459
- const run2 = await runSequencedLegs(ctx, key, {
59460
- wallet,
59461
- first: sequenced,
59462
- submitPath: kind === "swap" ? "/api/v1/agent/swap/submit" : "/api/v1/trade/agent/submit",
59463
- submitFields: kind === "swap" ? { clientTradeId: id, swapId: data.swapId } : { clientTradeId: id },
59464
- unwrap: (answer) => kind === "swap" ? answer.payload ?? {} : answer,
59465
- clientId: id,
59466
- kind,
59467
- allowedLegs: HOOD_TRADE_LEGS,
59468
- onLanded: async (_leg) => {
59469
- const notice = await recordTradedToken(ctx, wallet.address, toChecksumAddress(recorded));
59470
- if (notice)
59471
- notices.push(notice);
59472
- }
59621
+ const resolved = await resolveTargetKey(ctx, deviceToken, target, {
59622
+ codePrefix: "KEY_ACCESS",
59623
+ writeListFailure: (result2) => writeAccessFailure(ctx, result2, { self: false, history: true })
59473
59624
  });
59474
- const settled = kind === "swap" ? reportSettlement(ctx, run2.final.settlement, { asset: to.asset, decimals: outDecimals }, id) : {};
59475
- return printTradingResult(ctx, {
59476
- ...run2.final,
59477
- ...settled,
59478
- clientTradeId: id,
59479
- kind,
59480
- chain: "hood",
59481
- quote,
59482
- wallet: safeText(wallet.address),
59483
- operationId: sequenced.operationId,
59484
- landedLegs: run2.landed,
59485
- evmRecord: { token: toChecksumAddress(recorded), notices }
59625
+ if (!resolved.ok)
59626
+ return resolved.code;
59627
+ const result = await apiRequest(`${KEYS_PATH4}/${encodeURIComponent(resolved.keyPrefix)}/access-changes`, {
59628
+ auth: "device",
59629
+ credentials: { deviceToken },
59630
+ apiUrl,
59631
+ fetch: deps.fetch,
59632
+ env: deps.env
59486
59633
  });
59634
+ if (!result.ok)
59635
+ return writeAccessFailure(ctx, result, { self: false, history: true });
59636
+ if (json) {
59637
+ deps.stdout.write(`${JSON.stringify({ ...result.body, command: "keys access" })}
59638
+ `);
59639
+ return 0;
59640
+ }
59641
+ const rows = result.body.changes ?? [];
59642
+ if (rows.length === 0) {
59643
+ deps.stdout.write(`No access changes on key ${resolved.keyPrefix}.
59644
+ `);
59645
+ return 0;
59646
+ }
59647
+ deps.stdout.write(`${renderTable(["Time", "From", "To", "Actor", "Device or key"], rows.map((row) => [
59648
+ formatTimestamp(row.at),
59649
+ levelName(row.from),
59650
+ levelName(row.to),
59651
+ row.actor,
59652
+ row.actorDevicePrefix ?? row.actorKeyPrefix ?? ""
59653
+ ]))}
59654
+ `);
59655
+ if (rows.some((row) => row.actor === "device"))
59656
+ deps.stdout.write(`${PORTAL_REVOKE_LINE}
59657
+ `);
59658
+ return 0;
59487
59659
  }
59488
- var bridgeBuildSchema = exports_external.object({
59489
- status: exports_external.literal("built"),
59490
- swapId: exports_external.string().min(1),
59491
- venue: exports_external.string(),
59492
- fee: exports_external.object({ bps: exports_external.number(), feeRaw: exports_external.string() }).passthrough(),
59493
- expectedOutRaw: exports_external.string().regex(/^\d+$/),
59494
- expiresAt: exports_external.number().finite(),
59495
- recipient: exports_external.string().min(1),
59496
- statusChecks: exports_external.array(exports_external.string()).default([]),
59497
- transactionsBase64: exports_external.array(exports_external.string()).optional(),
59498
- walletAddress: exports_external.string().optional(),
59499
- venueCostUsd: exports_external.number().finite().optional(),
59500
- venueTimeEstimateSec: exports_external.number().finite().optional()
59501
- }).passthrough();
59502
- async function bridgeOriginBalance(ctx, from, address, solana, evm) {
59503
- if (from === "ETH")
59504
- return evmRead("the ETH balance", () => evm().getBalance(address));
59505
- if (from === "USDG")
59506
- return evmRead("the USDG balance", () => evm().erc20BalanceOf(HOOD_USDG_ADDRESS, address));
59507
- const reader = await solana();
59508
- if (from === "SOL")
59509
- return tradingRead(ctx, reader, () => reader.rpc.getBalance(address));
59510
- const mint = BASES.USDC?.mint;
59511
- const accounts = (await tradingRead(ctx, reader, () => Promise.all([
59512
- reader.rpc.getTokenAccountsByOwner(address, TOKEN_PROGRAM_ID),
59513
- reader.rpc.getTokenAccountsByOwner(address, TOKEN_2022_PROGRAM_ID)
59514
- ]))).flat();
59515
- return accounts.filter((account) => account.mint === mint).reduce((sum, account) => sum + BigInt(account.amountRaw), 0n);
59660
+
59661
+ // src/index.ts
59662
+ init_keys_embedded_wallet();
59663
+
59664
+ // src/commands/keys-wallets.ts
59665
+ init_args();
59666
+ init_deps();
59667
+ init_profiles();
59668
+ init_render();
59669
+ var NO_API_KEY3 = {
59670
+ code: "NO_API_KEY",
59671
+ message: "No API key for this profile.",
59672
+ suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
59673
+ };
59674
+ function widenRefusedHint(prefix, walletIds) {
59675
+ const wallets2 = walletIds.length > 0 ? walletIds.join(" ") : "<wallet...>";
59676
+ return `To move TEE wallets to this key, run: candle tee rebind ${wallets2} --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).";
59516
59677
  }
59517
- async function bridgeSwap(ctx, args) {
59518
- const { flags, id, from, to } = args;
59519
- const origin = BRIDGE_ASSETS[from].chain;
59520
- const destination = BRIDGE_ASSETS[to].chain;
59521
- const walletFlag = flags["--wallet"];
59522
- const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
59523
- if (named !== undefined && named !== origin)
59524
- throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, origin);
59525
- if (flags["--amount"])
59526
- rawAmount(flags["--amount"], 18);
59527
- const percent = flags["--percent"] ? BigInt(rawAmount(flags["--percent"], 6)) : undefined;
59528
- if (percent !== undefined && percent > 100000000n)
59529
- throw new TradingError("INVALID_AMOUNT", "Percent must be greater than 0 and at most 100 (up to six decimal places).");
59530
- const key = await tradingKey(ctx);
59531
- const prior = await lookupOperation(ctx, key, id, "swap");
59532
- if (prior)
59533
- return printTradingResult(ctx, {
59534
- ...prior,
59535
- ...bridgeStatusField(ctx, prior, bridgeFactsOf((await savedOperation(ctx, key, id))?.bridge))
59536
- });
59537
- const payer = await tradingPayer(ctx, key, walletFlag, "swap:write", origin);
59538
- if (payer.kind !== "tee")
59539
- throw new TradingError("CHAIN_MISMATCH", `Only a TEE wallet bridges, and the embedded wallet ${safeText(payer.address)} is not one. Name a ${chainName(origin)} TEE wallet with --wallet. Nothing was built.`);
59540
- const wallet = payer.wallet;
59541
- const { rows } = await listTradingWallets(ctx, key, "swap:write");
59542
- const onDestination = rows.filter((row) => rowChain(row) === destination);
59543
- const toFlag = flags["--to"];
59544
- let toWalletId;
59545
- let candidates = onDestination.filter((row) => row.active);
59546
- if (toFlag !== undefined) {
59547
- const matches = rows.filter((row) => matchesName(row, toFlag));
59548
- const match = matches[0];
59549
- if (matches.length > 1)
59550
- throw new TradingError("TEE_WALLET_REQUIRED", `"${safeText(toFlag)}" matches ${matches.length} TEE wallets on this key: ${matches.map(describeWallet).join("; ")}. Name one by id or address.`);
59551
- if (!match || rowChain(match) !== destination)
59552
- throw new TradingError("BRIDGE_DESTINATION_MISSING", `--to ${safeText(toFlag)} is not a ${chainName(destination)} TEE wallet on this key. A bridge lands only in this key's own TEE wallet on ${chainName(destination)}${onDestination.length > 0 ? `: ${onDestination.map(describeWallet).join("; ")}` : ""}. Nothing was built.`);
59553
- if (!match.active)
59554
- throw new TradingError("TEE_WALLET_INACTIVE", `The destination ${describeWallet(match)} is not a verified-active TEE wallet. Nothing was built.`);
59555
- toWalletId = match.id;
59556
- candidates = [match];
59557
- } else if (candidates.length !== 1) {
59558
- throw new TradingError("BRIDGE_DESTINATION_MISSING", candidates.length === 0 ? `This key has no active ${chainName(destination)} TEE wallet to bridge into. Promote one onto this key (candle vault promote), then bridge. Nothing was built.` : `This key has more than one ${chainName(destination)} TEE wallet; name the destination with --to: ${candidates.map(describeWallet).join("; ")}. Nothing was built.`);
59678
+ function formatTimestamp2(ms) {
59679
+ return ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) : "-";
59680
+ }
59681
+ async function keysWalletsList(args, ctx) {
59682
+ const { deps, apiUrl, json } = ctx;
59683
+ const parsed = parseArgs(args, {});
59684
+ if ("error" in parsed) {
59685
+ writeUsageFailure(deps, parsed.error, json);
59686
+ return 2;
59559
59687
  }
59560
- const solana = lazySolanaClient(ctx, flags["--rpc-url"]);
59561
- const evm = lazyEvmRpc(ctx, flags["--rpc-url"]);
59562
- const decimals = BRIDGE_ASSETS[from].decimals;
59563
- let amountRaw;
59564
- if (percent !== undefined) {
59565
- const balance = await bridgeOriginBalance(ctx, from, wallet.address, solana, evm);
59566
- amountRaw = (balance * percent / 100000000n).toString();
59567
- if (amountRaw === "0")
59568
- throw new TradingError("INVALID_AMOUNT", "The selected percentage rounds to zero raw units.");
59569
- } else
59570
- amountRaw = rawAmount(flags["--amount"], decimals);
59571
- if (!await claimOperation(ctx, key, id, "swap"))
59572
- throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
59573
- ctx.deps.stderr.write(`Operation: ${id}
59574
- `);
59575
- const built = await request(ctx, key, "/api/v1/agent/swap/build", {
59576
- clientTradeId: id,
59577
- from,
59578
- to,
59579
- amountRaw,
59580
- maxSlippageBps: args.slippage,
59581
- payer: { type: "linked", linkedWalletId: wallet.id },
59582
- ...toWalletId !== undefined ? { toWalletId } : {}
59688
+ const prefix = parsed.positionals[0];
59689
+ if (!prefix) {
59690
+ writeUsageFailure(deps, "Usage: candle keys wallets <prefix>", json);
59691
+ return 2;
59692
+ }
59693
+ await printIdentity(ctx);
59694
+ const apiKey = await resolveApiKey(deps, ctx.profile);
59695
+ if (!apiKey) {
59696
+ writeLocalFailure(deps, NO_API_KEY3, json);
59697
+ return 1;
59698
+ }
59699
+ const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
59700
+ auth: "key",
59701
+ credentials: { apiKey },
59702
+ apiUrl,
59703
+ fetch: deps.fetch,
59704
+ env: deps.env
59583
59705
  });
59584
- if (built.job || built.status === "executed")
59585
- return printTradingResult(ctx, { ...built, clientTradeId: id, kind: "swap" });
59586
- const body = built.payload ?? {};
59587
- const parsedBuild = bridgeBuildSchema.safeParse(body);
59588
- if (!parsedBuild.success)
59589
- throw new TradingError("INVALID_RESPONSE", "Candle did not return a complete bridge quote; nothing was signed.");
59590
- const data = parsedBuild.data;
59591
- if (data.venue !== "relay")
59592
- throw new TradingError("INVALID_RESPONSE", `A bridge goes through Relay, not ${safeText(data.venue)}; nothing was signed.`);
59593
- if (data.fee.bps !== 0 || data.fee.feeRaw !== "0")
59594
- throw new TradingError("INVALID_RESPONSE", "A bridge carries no Candle fee, and this quote has one; nothing was signed.");
59595
- const recipient = candidates.find((row) => destination === "hood" ? sameEvmAddress(row.address, data.recipient) : row.address === data.recipient);
59596
- if (!recipient)
59597
- throw new TradingError("INVALID_RESPONSE", `Candle named ${safeText(data.recipient)} as the destination, which is not ${toWalletId !== undefined ? "the TEE wallet --to named" : `this key's ${chainName(destination)} TEE wallet`}; nothing was signed.`);
59598
- const facts = {
59599
- from,
59600
- to,
59601
- recipient: recipient.address,
59602
- ...data.statusChecks[0] !== undefined ? { statusCheck: data.statusChecks[0] } : {}
59603
- };
59604
- await saveOperationBridge(ctx, key, id, { ...facts });
59605
- const refused = (problem) => new TradingError("RELAY_STEP_REFUSED", `Relay's deposit did not pass this machine's check (${problem}); nothing was signed.`);
59606
- let sequenced;
59607
- let transaction;
59608
- const hoodOrigin = from === "ETH" || from === "USDG" ? from : undefined;
59609
- if (hoodOrigin) {
59610
- const parsedLeg = sequencedSchema.safeParse(body);
59611
- if (!parsedLeg.success)
59612
- throw new TradingError("SEQUENCED_RAIL_REQUIRED", "A Hood TEE wallet bridges one leg at a time, and this Candle deployment did not answer with a sequenced leg. Nothing was signed.");
59613
- sequenced = parsedLeg.data;
59614
- if (typeof data.walletAddress !== "string" || !sameEvmAddress(data.walletAddress, wallet.address))
59615
- throw new TradingError("INVALID_RESPONSE", "The Hood build does not name the requested payer; nothing was signed.");
59616
- const plan = plannedLegKinds(sequenced.legKind, sequenced.plannedLegCount, false, "bridgeDeposit");
59617
- if (!bridgePlanAdmitted(hoodOrigin, plan))
59618
- throw refused(`the plan is ${sequenced.plannedLegCount} leg(s) starting with ${sequenced.legKind}`);
59619
- const problem = relayHoodLegProblem(sequenced.legKind, sequenced.nextLeg, {
59620
- origin: hoodOrigin,
59621
- payer: wallet.address,
59622
- amountRaw
59623
- });
59624
- if (problem)
59625
- throw refused(problem);
59626
- } else {
59627
- if (data.transactionsBase64?.length !== 1)
59628
- throw refused(`Candle returned ${data.transactionsBase64?.length ?? 0} transactions, not one deposit`);
59629
- transaction = data.transactionsBase64[0];
59630
- const reader = await solana();
59631
- const problem = await tradingRead(ctx, reader, () => relaySolanaDepositProblem(transaction, wallet.address, reader.rpc));
59632
- if (problem)
59633
- throw refused(problem);
59706
+ if (!result.ok) {
59707
+ writeFailure(deps, result, { apiUrl, authType: "key" }, json);
59708
+ return 1;
59634
59709
  }
59635
- const outDecimals = BRIDGE_ASSETS[to].decimals;
59636
- const minimumRaw = (BigInt(data.expectedOutRaw) * BigInt(1e4 - args.slippage) / 10000n).toString();
59637
- const quote = {
59638
- intent: `Bridge ${decimalAmount(amountRaw, decimals)} ${from} on ${chainName(origin)} to ${to} on ${chainName(destination)}`,
59639
- wallet: wallet.address,
59640
- venue: "relay",
59641
- priceImpactPct: null,
59642
- minimumReceived: `${decimalAmount(minimumRaw, outDecimals)} ${to} (Relay's estimate ${decimalAmount(data.expectedOutRaw, outDecimals)} ${to}, less the ${args.slippage} bps slippage bound)`,
59643
- minimumReceivedRaw: minimumRaw,
59644
- expectedOutRaw: data.expectedOutRaw,
59645
- destination: `${recipient.label ? `${recipient.label} ` : ""}${recipient.address} (${chainName(destination)} TEE wallet on this key)`,
59646
- candleFee: "none",
59647
- relayFees: data.venueCostUsd !== undefined ? `about $${data.venueCostUsd} as Relay reports it` : "not reported",
59648
- estimatedTime: data.venueTimeEstimateSec !== undefined ? `about ${data.venueTimeEstimateSec}s as Relay reports it` : "not reported",
59649
- tokenRisks: []
59650
- };
59651
- if (sequenced) {
59652
- const leg = sequenced.nextLeg;
59653
- const maxFee = BigInt(leg.maxFeePerGas);
59654
- const reserve = sweepReserveFloor(maxFee, []);
59655
- quote.legs = describeHoodLegs(sequenced, false, "bridgeDeposit");
59656
- quote.gas = `the ${sequenced.legKind} leg up to ${formatUnits(BigInt(leg.gas) * maxFee, 18)} ETH (gas ${leg.gas} at ${formatUnits(maxFee, 9)} gwei); each later leg is priced by Candle when it becomes next`;
59657
- quote.reserve = `at least ${formatUnits(reserve.wei, 18)} ETH stays in the wallet for a sweep home (${reserve.erc20Transfers} ERC-20 transfers and the final ETH transfer at twice the fee); a bridge never spends it`;
59658
- quote.operationId = sequenced.operationId;
59710
+ if (json) {
59711
+ deps.stdout.write(`${JSON.stringify(result.body)}
59712
+ `);
59713
+ return 0;
59659
59714
  }
59660
- if (!await confirmQuote(ctx, quote, args.yes)) {
59661
- if (sequenced)
59662
- ctx.deps.stderr.write(`Nothing was signed. Operation ${sequenced.operationId} holds this wallet until ${new Date(sequenced.expiresAt).toISOString()}; a new Hood trade or sweep from it waits until then.
59715
+ const body = result.body;
59716
+ deps.stdout.write(body.walletScope === "selected" ? `Scope: selected — this profile can only spend from the wallets below.
59717
+ ` : `Scope: all — this profile can spend from every wallet on the account, listed here or not.
59663
59718
  `);
59664
- return printTradingResult(ctx, {
59665
- success: true,
59666
- status: "cancelled",
59667
- clientTradeId: id,
59668
- kind: "swap",
59669
- quote,
59670
- ...sequenced ? { operationId: sequenced.operationId, walletHeldUntil: sequenced.expiresAt } : {}
59671
- });
59719
+ if (body.profileId)
59720
+ deps.stdout.write(`Profile: ${body.profileId}
59721
+ `);
59722
+ if (body.wallets.length === 0) {
59723
+ deps.stdout.write(`No wallets assigned.
59724
+ `);
59725
+ return 0;
59672
59726
  }
59673
- if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
59674
- throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
59675
- let result;
59676
- let depositHash;
59677
- if (sequenced && hoodOrigin) {
59678
- const run2 = await runSequencedLegs(ctx, key, {
59679
- wallet,
59680
- first: sequenced,
59681
- submitPath: "/api/v1/agent/swap/submit",
59682
- submitFields: { clientTradeId: id, swapId: data.swapId },
59683
- unwrap: (answer) => answer.payload ?? {},
59684
- clientId: id,
59685
- kind: "swap",
59686
- primaryLeg: "bridgeDeposit",
59687
- allowedLegs: bridgeLegKinds(hoodOrigin),
59688
- checkLeg: (kind, leg) => relayHoodLegProblem(kind, leg, { origin: hoodOrigin, payer: wallet.address, amountRaw }) === null,
59689
- onLanded: async () => {}
59690
- });
59691
- depositHash = run2.landed.find((leg) => leg.kind === "bridgeDeposit")?.hash;
59692
- result = {
59693
- ...run2.final,
59694
- chain: "hood",
59695
- operationId: sequenced.operationId,
59696
- landedLegs: run2.landed
59697
- };
59698
- } else {
59699
- const signed = await relaySign(ctx, key, wallet, transaction);
59700
- const submitted = await request(ctx, key, "/api/v1/agent/swap/submit", {
59701
- clientTradeId: id,
59702
- swapId: data.swapId,
59703
- signedTransactionsBase64: [signed]
59704
- });
59705
- const payload = submitted.payload ?? {};
59706
- const hashes = Array.isArray(payload.hashes) ? payload.hashes.filter((h) => typeof h === "string") : [];
59707
- depositHash = hashes[0];
59708
- result = submitted;
59727
+ const rows = body.wallets.map((w) => [
59728
+ w.linkedWalletId,
59729
+ w.chain,
59730
+ w.address,
59731
+ w.label ?? "-",
59732
+ w.spendCapable ? "yes" : "no",
59733
+ formatTimestamp2(w.assignedAt)
59734
+ ]);
59735
+ deps.stdout.write(`${renderTable(["Id", "Chain", "Address", "Label", "Can sign", "Assigned"], rows)}
59736
+ `);
59737
+ return 0;
59738
+ }
59739
+ async function keysWalletsSet(args, ctx) {
59740
+ const { deps, apiUrl, json } = ctx;
59741
+ const parsed = parseArgs(args, { valueFlags: ["--wallets"] });
59742
+ if ("error" in parsed) {
59743
+ writeUsageFailure(deps, parsed.error, json);
59744
+ return 2;
59709
59745
  }
59710
- ctx.deps.stderr.write(`Deposit ${depositHash ? safeText(depositHash) : "sent"}: filling.${args.wait ? "" : ` Check it with candle swap status ${id} (add --wait to follow it).`}
59746
+ const prefix = parsed.positionals[0];
59747
+ if (!prefix) {
59748
+ writeUsageFailure(deps, "Usage: candle keys wallets set <prefix> --wallets <id,id,...>", json);
59749
+ return 2;
59750
+ }
59751
+ const raw = parsed.values["--wallets"];
59752
+ if (raw === undefined) {
59753
+ writeUsageFailure(deps, 'Missing --wallets. Pass a comma-separated list, or "" to assign none.', json);
59754
+ return 2;
59755
+ }
59756
+ const walletIds = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
59757
+ await printIdentity(ctx);
59758
+ const apiKey = await resolveApiKey(deps, ctx.profile);
59759
+ if (!apiKey) {
59760
+ writeLocalFailure(deps, NO_API_KEY3, json);
59761
+ return 1;
59762
+ }
59763
+ const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
59764
+ method: "PUT",
59765
+ body: { walletIds },
59766
+ auth: "key",
59767
+ credentials: { apiKey },
59768
+ apiUrl,
59769
+ fetch: deps.fetch,
59770
+ env: deps.env
59771
+ });
59772
+ if (!result.ok) {
59773
+ if (result.code !== "LOOSEN_REQUIRES_SESSION") {
59774
+ writeFailure(deps, result, { apiUrl, authType: "key" }, json);
59775
+ return 1;
59776
+ }
59777
+ const hint = widenRefusedHint(prefix, walletIds);
59778
+ writeFailure(deps, { ...result, uiHint: hint }, { apiUrl, authType: "key" }, json);
59779
+ if (!json)
59780
+ deps.stderr.write(`${hint}
59711
59781
  `);
59712
- const receipt = {
59713
- ...result,
59714
- clientTradeId: id,
59715
- kind: "swap",
59716
- bridge: { from, to, destination: recipient.address, ...depositHash ? { depositHash } : {} },
59717
- quote,
59718
- wallet: safeText(wallet.address)
59719
- };
59720
- if (!args.wait)
59721
- return printTradingResult(ctx, receipt);
59722
- try {
59723
- const found = await lookupOperation(ctx, key, id, "swap");
59724
- if (!found)
59725
- return printTradingResult(ctx, { ...receipt, waited: { final: false } });
59726
- const waited = await waitForSettlement(ctx, key, id, found, facts, true);
59727
- return printTradingResult(ctx, {
59728
- ...receipt,
59729
- job: waited.job,
59730
- bridgeStatus: waited.bridgeStatus,
59731
- waited: waited.waited
59732
- });
59733
- } catch (error) {
59734
- const reason = safeText(error instanceof Error ? describeRpcFailure(error) : String(error));
59735
- ctx.deps.stderr.write(`Could not follow the bridge (${reason}). The deposit was sent; do not send it again. Re-check with candle swap status ${id} --wait.
59782
+ return 1;
59783
+ }
59784
+ if (json) {
59785
+ deps.stdout.write(`${JSON.stringify(result.body)}
59736
59786
  `);
59737
- return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
59787
+ return 0;
59788
+ }
59789
+ deps.stdout.write(walletIds.length === 0 ? `Cleared every wallet assignment on ${prefix}.
59790
+ ` : `Assigned ${walletIds.length} wallet${walletIds.length === 1 ? "" : "s"} to ${prefix}.
59791
+ `);
59792
+ return 0;
59793
+ }
59794
+ async function keysWalletsScope(args, ctx) {
59795
+ const { deps, apiUrl, json } = ctx;
59796
+ const parsed = parseArgs(args, { valueFlags: ["--scope"] });
59797
+ if ("error" in parsed) {
59798
+ writeUsageFailure(deps, parsed.error, json);
59799
+ return 2;
59800
+ }
59801
+ const prefix = parsed.positionals[0];
59802
+ const scope = parsed.values["--scope"];
59803
+ if (!prefix || scope !== "all" && scope !== "selected") {
59804
+ writeUsageFailure(deps, "Usage: candle keys wallets scope <prefix> --scope <all|selected>", json);
59805
+ return 2;
59806
+ }
59807
+ await printIdentity(ctx);
59808
+ const apiKey = await resolveApiKey(deps, ctx.profile);
59809
+ if (!apiKey) {
59810
+ writeLocalFailure(deps, NO_API_KEY3, json);
59811
+ return 1;
59812
+ }
59813
+ const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallet-scope`, {
59814
+ method: "PUT",
59815
+ body: { scope },
59816
+ auth: "key",
59817
+ credentials: { apiKey },
59818
+ apiUrl,
59819
+ fetch: deps.fetch,
59820
+ env: deps.env
59821
+ });
59822
+ if (!result.ok) {
59823
+ writeFailure(deps, result, { apiUrl, authType: "key" }, json);
59824
+ return 1;
59825
+ }
59826
+ if (json) {
59827
+ deps.stdout.write(`${JSON.stringify(result.body)}
59828
+ `);
59829
+ return 0;
59738
59830
  }
59831
+ deps.stdout.write(scope === "selected" ? `${prefix} is now limited to its assigned wallets.
59832
+ ` : `${prefix} can now spend from every wallet on the account.
59833
+ `);
59834
+ return 0;
59835
+ }
59836
+ async function keysWallets(args, ctx) {
59837
+ const [verb, ...rest] = args;
59838
+ if (verb === "set")
59839
+ return keysWalletsSet(rest, ctx);
59840
+ if (verb === "scope")
59841
+ return keysWalletsScope(rest, ctx);
59842
+ return keysWalletsList(args, ctx);
59739
59843
  }
59740
59844
 
59741
59845
  // src/commands/launch.ts
59846
+ init_args();
59847
+ init_evm_lite();
59848
+ init_render();
59849
+ init_solana_endpoint();
59850
+ init_solana_lite();
59851
+ init_trading();
59852
+ init_swap();
59853
+ import { randomUUID as randomUUID2 } from "node:crypto";
59742
59854
  var USAGE2 = "Usage: candle launch --name <name> --symbol <symbol> --image-url <https-url> --wallet <tee> [--client-trade-id <id>] [--quote-asset sol|usdc|cndl|eth|usdg] [--dex-version v3|v4] [--mode <mode>] [--rpc-url <url>] [--yes]. A Hood TEE wallet launches on Hood and needs --dex-version.";
59743
59855
  var QUOTE_CHAIN = {
59744
59856
  sol: "solana",
@@ -59985,6 +60097,7 @@ init_render();
59985
60097
  init_solana_endpoint();
59986
60098
  init_solana_lite();
59987
60099
  init_trading();
60100
+ init_swap();
59988
60101
  import { randomUUID as randomUUID3 } from "node:crypto";
59989
60102
  var LP_SCOPE = "lp:write";
59990
60103
  var CONFIRM_POLL_MS3 = 2000;
@@ -60916,6 +61029,7 @@ function hyperliquidRelayBody(typedData) {
60916
61029
  // src/commands/perps.ts
60917
61030
  init_render();
60918
61031
  init_trading();
61032
+ init_swap();
60919
61033
  var PERPS_SCOPE = "perps:write";
60920
61034
  var DEC_RE = /^(0|[1-9][0-9]*)(\.[0-9]+)?$/;
60921
61035
  var CLOID_RE = /^0x[0-9a-fA-F]{32}$/;
@@ -62508,8 +62622,12 @@ async function portfolio(args, ctx) {
62508
62622
  });
62509
62623
  if ("error" in parsed)
62510
62624
  return usage(ctx, parsed.error);
62511
- if (parsed.positionals.length > 0)
62512
- return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
62625
+ if (parsed.positionals.length > 1)
62626
+ return usage(ctx, `Unexpected argument: ${parsed.positionals[1]}`);
62627
+ const filter = parsed.positionals[0];
62628
+ if (filter !== undefined && filter.trim() === "")
62629
+ return usage(ctx, "The filter is empty.");
62630
+ const keep = (wallet) => filter === undefined || matches(wallet, filter);
62513
62631
  const { deps } = ctx;
62514
62632
  const chainFlag = parsed.values["--chain"];
62515
62633
  if (chainFlag !== undefined && !CHAINS2.includes(chainFlag)) {
@@ -62550,6 +62668,7 @@ async function portfolio(args, ctx) {
62550
62668
  let vaultEntries;
62551
62669
  let evmEntries;
62552
62670
  let vaultReason;
62671
+ let known = 0;
62553
62672
  const raw = await readVaultRaw(resolvedVault.path);
62554
62673
  if (raw === null) {
62555
62674
  vaultReason = `no vault at ${resolvedVault.path}`;
@@ -62561,6 +62680,9 @@ async function portfolio(args, ctx) {
62561
62680
  const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
62562
62681
  vaultEntries = showSolana ? vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role })) : [];
62563
62682
  evmEntries = showHood ? vault.index.entries.filter((entry) => entry.chain === "evm" && entry.role === "vault").map((entry) => ({ address: entry.address, label: entry.label })) : [];
62683
+ known += vaultEntries.length + evmEntries.length;
62684
+ vaultEntries = vaultEntries.filter(keep);
62685
+ evmEntries = evmEntries.filter(keep);
62564
62686
  }
62565
62687
  await printIdentity(ctx);
62566
62688
  const rpcHost = solana?.endpoint.host;
@@ -62603,7 +62725,9 @@ async function portfolio(args, ctx) {
62603
62725
  writeFailure(deps, candle, { apiUrl: ctx.apiUrl, authType: "key" }, ctx.json);
62604
62726
  return 1;
62605
62727
  }
62606
- const fromCandle = candle.body;
62728
+ const answered = candle.body;
62729
+ known += (showSolana ? (answered.tee ?? []).length + (answered.embedded ?? []).length : 0) + (showHood ? (answered.hood?.tee ?? []).length + (answered.hood?.embedded ?? []).length : 0);
62730
+ const fromCandle = filter === undefined ? answered : filterCandle(answered, keep);
62607
62731
  const prices = { ...fromCandle.prices ?? {} };
62608
62732
  const candleHood = showHood ? fromCandle.hood : undefined;
62609
62733
  let priceFailure;
@@ -62750,9 +62874,10 @@ async function portfolio(args, ctx) {
62750
62874
  };
62751
62875
  const unavailable = chains.flatMap((chain2) => unavailableByChain[chain2]);
62752
62876
  const lpUnread = (lpSection?.unreadable ?? []).length;
62753
- const candleExplained = (fromCandle.unavailable ?? []).length > 0 || (fromCandle.hood?.unavailable ?? []).length > 0 || (fromCandle.lp?.unreadable ?? []).length > 0;
62754
- const listCutOff = fromCandle.complete === false && !candleExplained;
62755
- const complete = !listCutOff && unavailable.length === 0 && lpUnread === 0;
62877
+ const listStatus = walletListStatus(answered);
62878
+ const shownUnread = unavailable.length > 0 || lpUnread > 0;
62879
+ const listNote = listStatus === "cut-off" ? "cut-off" : listStatus === "unknown" && !shownUnread ? "unknown" : null;
62880
+ const complete = listNote === null && !shownUnread;
62756
62881
  const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
62757
62882
  const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
62758
62883
  const lp = lpSection ? {
@@ -62772,26 +62897,31 @@ async function portfolio(args, ctx) {
62772
62897
  }
62773
62898
  const hoodTotals = byChain.hood;
62774
62899
  if (hoodTotals) {
62775
- const reasons = { ...candleHood?.unpricedByReason ?? {} };
62776
- let vaultUnpriced = 0;
62777
- for (const w of groups[0]?.wallets ?? []) {
62900
+ const counted = filter === undefined ? groups[0]?.wallets ?? [] : all;
62901
+ const reasons = filter === undefined ? { ...candleHood?.unpricedByReason ?? {} } : {};
62902
+ let localUnpriced = 0;
62903
+ for (const w of counted) {
62778
62904
  if (w.chain !== "hood")
62779
62905
  continue;
62780
62906
  for (const h of w.holdings ?? []) {
62781
62907
  if (h.priceUsd !== null)
62782
62908
  continue;
62783
- vaultUnpriced += 1;
62909
+ localUnpriced += 1;
62784
62910
  const reason = h.unpricedReason ?? "source-unavailable";
62785
62911
  reasons[reason] = (reasons[reason] ?? 0) + 1;
62786
62912
  }
62787
62913
  }
62788
- if (candleHood)
62789
- hoodTotals.unpriced = (candleHood.unpriced ?? 0) + vaultUnpriced;
62914
+ if (filter !== undefined)
62915
+ hoodTotals.unpriced = localUnpriced;
62916
+ else if (candleHood)
62917
+ hoodTotals.unpriced = (candleHood.unpriced ?? 0) + localUnpriced;
62790
62918
  hoodTotals.unpricedByReason = reasons;
62791
62919
  }
62920
+ const matched = filter === undefined ? undefined : { filter, matched: all.length, wallets: known };
62792
62921
  if (ctx.json) {
62793
62922
  writeJson(deps, {
62794
62923
  ok: true,
62924
+ ...matched ?? {},
62795
62925
  chains,
62796
62926
  totalUsd,
62797
62927
  unpriced,
@@ -62806,18 +62936,79 @@ async function portfolio(args, ctx) {
62806
62936
  });
62807
62937
  return complete ? 0 : 3;
62808
62938
  }
62939
+ if (matched) {
62940
+ deps.stdout.write(matched.matched === 0 ? `No wallet matches "${terminalText(matched.filter)}". ${matched.wallets} ${matched.wallets === 1 ? "wallet" : "wallets"} on this account and vault; candle portfolio with no filter shows them.
62941
+ ` : `${matched.matched} of ${matched.wallets} wallets match "${terminalText(matched.filter)}". Every figure below is for those wallets only.
62942
+ `);
62943
+ if (matched.matched === 0) {
62944
+ writeListNote(deps.stdout, listNote);
62945
+ return complete ? 0 : 3;
62946
+ }
62947
+ }
62809
62948
  writeTable(ctx, groups, {
62810
62949
  totalUsd,
62811
62950
  unpriced,
62812
62951
  chains,
62813
62952
  byChain,
62814
62953
  unavailableByChain,
62815
- listCutOff,
62954
+ listNote,
62816
62955
  lp
62817
62956
  });
62818
62957
  return complete ? 0 : 3;
62819
62958
  });
62820
62959
  }
62960
+ function matches(wallet, filter) {
62961
+ const needle = filter.toLowerCase();
62962
+ return (wallet.label ?? "").toLowerCase().includes(needle) || wallet.address.toLowerCase().includes(needle);
62963
+ }
62964
+ function filterCandle(body, keep) {
62965
+ const tee = (body.tee ?? []).filter(keep);
62966
+ const embedded = (body.embedded ?? []).filter(keep);
62967
+ const kept = new Set([...tee, ...embedded].map((row) => row.address));
62968
+ const hoodTee = (body.hood?.tee ?? []).filter(keep);
62969
+ const hoodEmbedded = (body.hood?.embedded ?? []).filter(keep);
62970
+ const hoodKept = new Set([...hoodTee, ...hoodEmbedded].map((row) => row.address));
62971
+ return {
62972
+ ...body,
62973
+ tee,
62974
+ embedded,
62975
+ unavailable: (body.unavailable ?? []).filter((address) => kept.has(address)),
62976
+ ...body.lp ? {
62977
+ lp: {
62978
+ ...body.lp,
62979
+ positions: body.lp.positions.filter((position) => kept.has(position.wallet)),
62980
+ unreadable: body.lp.unreadable.filter((position) => kept.has(position.wallet))
62981
+ }
62982
+ } : {},
62983
+ ...body.hood ? {
62984
+ hood: {
62985
+ tee: hoodTee,
62986
+ embedded: hoodEmbedded,
62987
+ unavailable: (body.hood.unavailable ?? []).filter((address) => hoodKept.has(address)),
62988
+ unpriced: 0,
62989
+ unpricedByReason: {}
62990
+ }
62991
+ } : {}
62992
+ };
62993
+ }
62994
+ function walletListStatus(answered) {
62995
+ if (typeof answered.walletsComplete === "boolean")
62996
+ return answered.walletsComplete ? "complete" : "cut-off";
62997
+ if (answered.complete !== false)
62998
+ return "complete";
62999
+ const readFailed = (answered.unavailable ?? []).length > 0 || (answered.hood?.unavailable ?? []).length > 0 || (answered.lp?.unreadable ?? []).length > 0;
63000
+ return readFailed ? "unknown" : "cut-off";
63001
+ }
63002
+ var LIST_CUT_OFF_LINE = `Candle listed only part of this account's wallets. The total is partial.
63003
+ `;
63004
+ var LIST_UNKNOWN_LINE = `Candle did not say whether it listed every wallet. One it left out would be missing here, so this is not a complete result.
63005
+ `;
63006
+ function writeListNote(stdout, note) {
63007
+ if (note === "cut-off")
63008
+ stdout.write(LIST_CUT_OFF_LINE);
63009
+ else if (note === "unknown")
63010
+ stdout.write(LIST_UNKNOWN_LINE);
63011
+ }
62821
63012
  async function startEvmRead(addresses, rpcUrl, fetchFn) {
62822
63013
  const rpc = createEvmRpc(rpcUrl, fetchFn);
62823
63014
  const host = rpcHostOf2(rpcUrl);
@@ -63128,10 +63319,7 @@ ${renderTable(["GROUP", "WALLET", "POSITION", "POOL", "HOLDINGS", "UNCLAIMED FEE
63128
63319
  deps.stdout.write(`${unavailable} ${unavailable === 1 ? "wallet" : "wallets"} could not be read in full (${unreadByChain.map(([chain2, n]) => `${n} on ${CHAIN_NAMES[chain2]}`).join(", ")}). What was not read is marked "not read" and is not in the total.
63129
63320
  `);
63130
63321
  }
63131
- if (totals.listCutOff) {
63132
- deps.stdout.write(`Candle listed only part of this account's wallets. The total is partial.
63133
- `);
63134
- }
63322
+ writeListNote(deps.stdout, totals.listNote);
63135
63323
  const truncated = groups.flatMap((g) => g.wallets).filter((w) => w.truncated).length;
63136
63324
  if (truncated > 0) {
63137
63325
  deps.stdout.write(`${truncated} Hood ${truncated === 1 ? "wallet holds" : "wallets hold"} more tokens than Candle lists. The rest are not shown and not in the total.
@@ -63953,6 +64141,7 @@ async function signMessage2(args, ctx) {
63953
64141
  }
63954
64142
 
63955
64143
  // src/index.ts
64144
+ init_swap();
63956
64145
  init_tee();
63957
64146
  init_tee_rebind();
63958
64147
 
@@ -63962,6 +64151,7 @@ init_evm_lite();
63962
64151
  init_profiles();
63963
64152
  init_render();
63964
64153
  init_trading();
64154
+ init_swap();
63965
64155
  var USAGE4 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
63966
64156
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
63967
64157
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
@@ -63999,10 +64189,10 @@ async function resolveDestination(ctx, key, source, to, chain2 = "solana") {
63999
64189
  }
64000
64190
  const rowChain2 = hood ? "evm" : "solana";
64001
64191
  const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === rowChain2 && row.revokedAt === undefined && !same(row.address, source.address));
64002
- const matches = rows.filter((row) => row.label === to || row._id === to || same(row.address, to));
64003
- if (matches.length > 1)
64004
- 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.`);
64005
- const match = matches[0];
64192
+ const matches2 = rows.filter((row) => row.label === to || row._id === to || same(row.address, to));
64193
+ if (matches2.length > 1)
64194
+ throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${matches2.length} linked wallets: ${matches2.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
64195
+ const match = matches2[0];
64006
64196
  if (match)
64007
64197
  return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
64008
64198
  if (same(to, source.address))
@@ -68149,7 +68339,7 @@ async function vaultList(args, ctx) {
68149
68339
  throw missingVault(ctx, resolvedVault);
68150
68340
  const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
68151
68341
  const all = vault.index.entries;
68152
- const matched = filter === undefined ? all : all.filter((entry) => matches(entry, filter));
68342
+ const matched = filter === undefined ? all : all.filter((entry) => matches2(entry, filter));
68153
68343
  const solana = balances ? matched.filter((entry) => entry.chain === "solana") : [];
68154
68344
  const requests = Math.ceil(solana.length / CHUNK2);
68155
68345
  const rpcHost = solanaClient?.endpoint.host;
@@ -68284,7 +68474,7 @@ total ${formatSol3(totalLamports)} SOL across ${solana.length - unavailable.len
68284
68474
  return complete ? 0 : 3;
68285
68475
  });
68286
68476
  }
68287
- function matches(entry, filter) {
68477
+ function matches2(entry, filter) {
68288
68478
  const needle = filter.toLowerCase();
68289
68479
  return entry.label.toLowerCase().includes(needle) || entry.address.toLowerCase().includes(needle);
68290
68480
  }
@@ -70346,8 +70536,8 @@ async function runRebindPhase(ctx, phase, results) {
70346
70536
  async function verifySubjectSecret(vault, subject) {
70347
70537
  const secret = await decryptKey(vault, subject.id);
70348
70538
  try {
70349
- const matches2 = subject.chain === "evm" ? sameEvmAddress(evmAddressFromSecret(secret), subject.address) : addressFromSecret64(secret) === subject.address;
70350
- if (!matches2) {
70539
+ const matches3 = subject.chain === "evm" ? sameEvmAddress(evmAddressFromSecret(secret), subject.address) : addressFromSecret64(secret) === subject.address;
70540
+ if (!matches3) {
70351
70541
  throw new VaultError("VAULT_VERIFY_FAILED", "Stored secret does not match the subject address.");
70352
70542
  }
70353
70543
  } catch (error) {
@@ -70837,7 +71027,7 @@ New vault at ${path}, verified in full (all eight steps).
70837
71027
  const solana = rpcUrlFlag === undefined ? undefined : solanaClientFor(ctx, flagEndpoint(rpcUrlFlag));
70838
71028
  const derived = await deriveWithinBounds(ctx, vault, counts, solana);
70839
71029
  const apiKey = await resolveApiKey(deps, ctx.profile);
70840
- let matches2 = {
71030
+ let matches3 = {
70841
71031
  matched: [],
70842
71032
  unmatched: [],
70843
71033
  account: undefined,
@@ -70845,16 +71035,16 @@ New vault at ${path}, verified in full (all eight steps).
70845
71035
  reason: "no API key is stored, so no exposure could be read"
70846
71036
  };
70847
71037
  if (apiKey !== undefined) {
70848
- matches2 = await matchAgainstAccount(ctx, apiKey, derived);
71038
+ matches3 = await matchAgainstAccount(ctx, apiKey, derived);
70849
71039
  } else {
70850
71040
  deps.stderr.write(`No API key is stored for this profile, so this restore read no exposure at all.
70851
71041
  `);
70852
71042
  }
70853
- const outcome = await writeRestoredIndex(ctx, vault, derived, matches2, counts);
71043
+ const outcome = await writeRestoredIndex(ctx, vault, derived, matches3, counts);
70854
71044
  committed = true;
70855
71045
  const config = await deps.readConfig();
70856
71046
  const ambient = Boolean(deps.env[RPC_URL_ENV]?.trim()) || Boolean(ctx.profile !== undefined && config.profiles?.[ctx.profile]?.rpcUrl?.trim());
70857
- const result = reportRestore(ctx, vault, derived, matches2, outcome, counts, ambient);
71047
+ const result = reportRestore(ctx, vault, derived, matches3, outcome, counts, ambient);
70858
71048
  const footer = nonDefaultVaultFooter(resolvedVault);
70859
71049
  if (footer !== undefined)
70860
71050
  deps.stdout.write(footer);
@@ -71071,9 +71261,9 @@ This profile acts as account ${read.account}.
71071
71261
  }
71072
71262
  return { matched, unmatched, externalListed, account: read.account, complete: true };
71073
71263
  }
71074
- async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
71264
+ async function writeRestoredIndex(ctx, vault, derived, matches3, counts) {
71075
71265
  const now = new Date(ctx.deps.now()).toISOString();
71076
- const matchByAddress = new Map(matches2.matched.map((match) => [match.entry.address, match.row]));
71266
+ const matchByAddress = new Map(matches3.matched.map((match) => [match.entry.address, match.row]));
71077
71267
  const entries = derived.entries.map((entry) => {
71078
71268
  const row = matchByAddress.get(entry.address);
71079
71269
  if (entry.branch === "evmTee") {
@@ -71143,7 +71333,7 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
71143
71333
  exposed[located.branch].push(located.index);
71144
71334
  }
71145
71335
  const highestMatched = { solanaVault: -1, solanaTee: -1, solanaExternal: -1 };
71146
- for (const match of matches2.matched) {
71336
+ for (const match of matches3.matched) {
71147
71337
  if (match.entry.branch === "solanaVault")
71148
71338
  highestMatched.solanaVault = Math.max(highestMatched.solanaVault, match.entry.index);
71149
71339
  if (match.entry.branch === "solanaTee")
@@ -71168,12 +71358,12 @@ async function writeRestoredIndex(ctx, vault, derived, matches2, counts) {
71168
71358
  },
71169
71359
  discovery: {
71170
71360
  restoredAt: now,
71171
- account: matches2.account ?? "",
71361
+ account: matches3.account ?? "",
71172
71362
  requestedCounts: counts.requested,
71173
71363
  highestMatched,
71174
71364
  complete: false
71175
71365
  },
71176
- ...matches2.complete ? { exposureReconciledAt: now } : {}
71366
+ ...matches3.complete ? { exposureReconciledAt: now } : {}
71177
71367
  };
71178
71368
  await commitVault(vault, { index: { hd, entries }, addKeys: derived.entries.map((entry) => entry.blob) }, ctx.deps);
71179
71369
  return { entries, hd };
@@ -71201,7 +71391,7 @@ function teeFieldsFor(row, ctx, network2 = "solana-mainnet") {
71201
71391
  }
71202
71392
  return { role: "tee-wallet", tee: { ...common, lifecycle: "stranded", grantIdentity } };
71203
71393
  }
71204
- function reportRestore(ctx, vault, derived, matches2, outcome, counts, ambientSolanaEndpoint) {
71394
+ function reportRestore(ctx, vault, derived, matches3, outcome, counts, ambientSolanaEndpoint) {
71205
71395
  const { deps } = ctx;
71206
71396
  deps.stdout.write(`
71207
71397
  Recovered ${outcome.entries.length} address(es) from the phrase.
@@ -71244,29 +71434,29 @@ Every one of them is recorded with an unknown history and stays that way: this p
71244
71434
  deps.stdout.write(` evm tee: indices 0 to ${counts.evmTee - 1}, on m/44'/60'/n'/1'/0'. The sealed EVM record is not restored by a phrase: a sweep of these wallets needs --from-block <n> (a Hood block at or before the first transfer in) or --token <0x...> to find tokens beyond USDG and WETH.
71245
71435
  `);
71246
71436
  }
71247
- if ((matches2.externalListed?.length ?? 0) > 0) {
71437
+ if ((matches3.externalListed?.length ?? 0) > 0) {
71248
71438
  deps.stdout.write(`
71249
- ${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.
71439
+ ${matches3.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.
71250
71440
  `);
71251
71441
  }
71252
- if (!matches2.complete) {
71253
- if (matches2.refused !== undefined)
71442
+ if (!matches3.complete) {
71443
+ if (matches3.refused !== undefined)
71254
71444
  deps.stdout.write(`
71255
- ${matches2.refused}: the account was not confirmed.
71445
+ ${matches3.refused}: the account was not confirmed.
71256
71446
  `);
71257
71447
  deps.stdout.write(`
71258
- No exposure was recorded: ${matches2.reason ?? "the linked-wallet read did not complete"}.
71448
+ No exposure was recorded: ${matches3.reason ?? "the linked-wallet read did not complete"}.
71259
71449
  `);
71260
- deps.stdout.write(matches2.refused !== undefined ? `The vault was created, verified and written in full; nothing in it is flagged. Run \`candle vault reconcile-exposure\` once you are on the right profile.
71450
+ deps.stdout.write(matches3.refused !== undefined ? `The vault was created, verified and written in full; nothing in it is flagged. Run \`candle vault reconcile-exposure\` once you are on the right profile.
71261
71451
  ` : `A partial list that happened to be empty would read as good news, so nothing was flagged at all. Run \`candle vault reconcile-exposure\` once the read succeeds.
71262
71452
  `);
71263
71453
  return 3;
71264
71454
  }
71265
- if (matches2.unmatched.length > 0) {
71455
+ if (matches3.unmatched.length > 0) {
71266
71456
  deps.stdout.write(`
71267
- ${matches2.unmatched.length} address(es) this account imported were NOT derived by this restore:
71457
+ ${matches3.unmatched.length} address(es) this account imported were NOT derived by this restore:
71268
71458
  `);
71269
- for (const address of matches2.unmatched)
71459
+ for (const address of matches3.unmatched)
71270
71460
  deps.stdout.write(` ${address}
71271
71461
  `);
71272
71462
  deps.stdout.write(`
@@ -71278,7 +71468,7 @@ Either this root derives them at an index beyond the bounds used here, in which
71278
71468
  }
71279
71469
  closeVault(vault);
71280
71470
  deps.stdout.write(`
71281
- ${matches2.matched.length} of them are addresses this account imported, and each is flagged as remotely exposed.
71471
+ ${matches3.matched.length} of them are addresses this account imported, and each is flagged as remotely exposed.
71282
71472
  `);
71283
71473
  return 0;
71284
71474
  }
@@ -72141,6 +72331,7 @@ init_wallets();
72141
72331
  init_args();
72142
72332
  init_render();
72143
72333
  init_trading();
72334
+ init_swap();
72144
72335
  import { randomUUID as randomUUID5 } from "node:crypto";
72145
72336
  var USAGE6 = "Usage: candle wallet close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
72146
72337
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;