@candledottv/cli 0.11.31 → 0.11.32

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 (2) hide show
  1. package/dist/index.js +566 -76
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10972,6 +10972,7 @@ var init_errors2 = __esm(() => {
10972
10972
  "EVM_RECIPIENT_IS_TOKEN",
10973
10973
  "EVM_TRANSFER_REVERTED",
10974
10974
  "TRANSFER_CHAIN_MISMATCH",
10975
+ "TRANSFER_INSUFFICIENT_FUNDS",
10975
10976
  "SOLANA_COMMAND_EVM_KEY",
10976
10977
  "RPC_RATE_LIMITED",
10977
10978
  "EVM_RECORD_ENTRY_TOO_LONG",
@@ -17577,6 +17578,7 @@ function parseMintAccount(mint, owner, data) {
17577
17578
  tokenProgram: owner,
17578
17579
  token2022: owner === TOKEN_2022_PROGRAM_ID,
17579
17580
  decimals,
17581
+ associatedTokenAccountSize: owner === TOKEN_2022_PROGRAM_ID ? TLV_START + 4 : ACCOUNT_TYPE_OFFSET2,
17580
17582
  nonTransferable: false,
17581
17583
  defaultFrozen: false,
17582
17584
  paused: false,
@@ -17601,6 +17603,7 @@ function parseMintAccount(mint, owner, data) {
17601
17603
  throw new MintReadError(`${mint} has an invalid mint extension length or a duplicate extension`);
17602
17604
  }
17603
17605
  seen.add(type);
17606
+ profile.associatedTokenAccountSize += REQUIRED_ACCOUNT_TLV_SIZE.get(type) ?? 0;
17604
17607
  const requireLength = (expected) => {
17605
17608
  if (length !== expected)
17606
17609
  throw new MintReadError(`${mint} has an invalid extension ${type} length`);
@@ -17890,11 +17893,17 @@ function classifyTokenSendFailure(input) {
17890
17893
  return "TOKEN_2022_HOOK_REFUSED";
17891
17894
  return;
17892
17895
  }
17893
- var MintReadError, MINT_DECIMALS_OFFSET = 44, MINT_IS_INITIALIZED_OFFSET = 45, MINT_BASE_SIZE2 = 82, ACCOUNT_TYPE_OFFSET2 = 165, TLV_START = 166, MULTISIG_SIZE2 = 355, EXT_TRANSFER_FEE_CONFIG = 1, EXT_DEFAULT_ACCOUNT_STATE = 6, EXT_NON_TRANSFERABLE = 9, EXT_PERMANENT_DELEGATE = 12, EXT_TRANSFER_HOOK = 14, EXT_PAUSABLE = 26, EXECUTE_DISCRIMINATOR, EXTRA_ACCOUNT_METAS_SEED, EXTRA_ACCOUNT_META_SIZE = 35, HookResolutionError;
17896
+ var MintReadError, MINT_DECIMALS_OFFSET = 44, MINT_IS_INITIALIZED_OFFSET = 45, MINT_BASE_SIZE2 = 82, ACCOUNT_TYPE_OFFSET2 = 165, TLV_START = 166, MULTISIG_SIZE2 = 355, EXT_TRANSFER_FEE_CONFIG = 1, EXT_DEFAULT_ACCOUNT_STATE = 6, EXT_NON_TRANSFERABLE = 9, EXT_PERMANENT_DELEGATE = 12, EXT_TRANSFER_HOOK = 14, EXT_PAUSABLE = 26, REQUIRED_ACCOUNT_TLV_SIZE, EXECUTE_DISCRIMINATOR, EXTRA_ACCOUNT_METAS_SEED, EXTRA_ACCOUNT_META_SIZE = 35, HookResolutionError;
17894
17897
  var init_token_2022 = __esm(() => {
17895
17898
  init_solana_lite();
17896
17899
  MintReadError = class MintReadError extends Error {
17897
17900
  };
17901
+ REQUIRED_ACCOUNT_TLV_SIZE = new Map([
17902
+ [EXT_TRANSFER_FEE_CONFIG, 4 + 8],
17903
+ [EXT_NON_TRANSFERABLE, 4],
17904
+ [EXT_TRANSFER_HOOK, 4 + 1],
17905
+ [EXT_PAUSABLE, 4]
17906
+ ]);
17898
17907
  EXECUTE_DISCRIMINATOR = new Uint8Array([105, 37, 101, 197, 75, 251, 102, 26]);
17899
17908
  EXTRA_ACCOUNT_METAS_SEED = new TextEncoder().encode("extra-account-metas");
17900
17909
  HookResolutionError = class HookResolutionError extends Error {
@@ -19194,6 +19203,12 @@ function describeBiometry(report) {
19194
19203
  suggestion: "Unlock the Mac with its password to reset Touch ID, then retry. Nothing was written and no other factor is substituted."
19195
19204
  };
19196
19205
  case "not-interactive":
19206
+ if (report.laError?.name === "systemCancel") {
19207
+ return {
19208
+ message: `Touch ID did not complete${la}: ${SYSTEM_CANCEL_WORDS}${reason}. This Mac ${report.biometryType && report.biometryType !== "none" ? `has ${biometryTypeWord(report.biometryType)}` : "may have Touch ID"}.`,
19209
+ suggestion: SYSTEM_CANCEL_SUGGESTION
19210
+ };
19211
+ }
19197
19212
  return {
19198
19213
  message: `Touch ID is not available from this session${la}: this Mac ${report.biometryType && report.biometryType !== "none" ? `has ${biometryTypeWord(report.biometryType)}, but` : "may have Touch ID, but"} no prompt can be shown to a process outside the interactive login session (SSH, a background agent, the lid closed with no display)${reason}.`,
19199
19214
  suggestion: NOT_INTERACTIVE_SUGGESTION
@@ -19256,6 +19271,11 @@ function translateEnclaveFailure(code, message) {
19256
19271
  DOMAIN_NOT_ASSOCIATED: `macOS did not associate the helper with cli.candle.tv: ${message}. ${AASA_REQUIREMENT}`,
19257
19272
  NO_CREDENTIAL: `No synced passkey with this envelope's credential id is available to this Mac or this Apple account: ${message}.`
19258
19273
  };
19274
+ if (code === "NOT_INTERACTIVE" && /\bsystemCancel\b/.test(message)) {
19275
+ return new VaultError("VAULT_FACTOR_UNAVAILABLE", `Touch ID did not complete: ${SYSTEM_CANCEL_WORDS}: ${message}.`, {
19276
+ suggestion: SYSTEM_CANCEL_SUGGESTION
19277
+ });
19278
+ }
19259
19279
  const suggestion = {
19260
19280
  BIOMETRY_UNAVAILABLE: "Open the lid, or use a keyboard with Touch ID, or unlock the Mac with its password first. Nothing was derived and no other factor was tried; the passphrase still opens the vault.",
19261
19281
  NOT_INTERACTIVE: NOT_INTERACTIVE_SUGGESTION,
@@ -19470,7 +19490,7 @@ async function deleteEnclaveKey(deps, session, opts) {
19470
19490
  return false;
19471
19491
  }
19472
19492
  }
19473
- var ENCLAVE_HELPER_ENV = "CANDLE_ENCLAVE_HELPER", CODESIGN_PATH = "/usr/bin/codesign", ENCLAVE_HELPER_TIMEOUT_MS = 120000, CODESIGN_TIMEOUT_MS = 30000, AASA_URL = "https://cli.candle.tv/.well-known/apple-app-site-association", AASA_REQUIREMENT, ENCLAVE_INSTALL_SUGGESTION = "Install a release build of the CLI that ships the signed helper (the darwin tarball and Homebrew place candle-enclave.app beside candle), or set CANDLE_ENCLAVE_HELPER to the path of a signed candle-enclave.app. No other factor is substituted.", NOT_INTERACTIVE_SUGGESTION = "Run the command from a Terminal window inside the logged-in session on that Mac (not over SSH, not from a background agent, not with the lid closed and no display), then retry. Nothing was derived and no other factor is substituted; the passphrase still opens the vault.";
19493
+ var ENCLAVE_HELPER_ENV = "CANDLE_ENCLAVE_HELPER", CODESIGN_PATH = "/usr/bin/codesign", ENCLAVE_HELPER_TIMEOUT_MS = 120000, CODESIGN_TIMEOUT_MS = 30000, AASA_URL = "https://cli.candle.tv/.well-known/apple-app-site-association", AASA_REQUIREMENT, ENCLAVE_INSTALL_SUGGESTION = "Install a release build of the CLI that ships the signed helper (the darwin tarball and Homebrew place candle-enclave.app beside candle), or set CANDLE_ENCLAVE_HELPER to the path of a signed candle-enclave.app. No other factor is substituted.", NOT_INTERACTIVE_SUGGESTION = "Run the command from a Terminal window inside the logged-in session on that Mac (not over SSH, not from a background agent, not with the lid closed and no display), then retry. Nothing was derived and no other factor is substituted; the passphrase still opens the vault.", SYSTEM_CANCEL_SUGGESTION = "Retry with the Terminal window in front, and leave it in front until the prompt is answered. If it is cancelled again, run the command from a Terminal window inside the logged-in session on that Mac (not over SSH, not from a background agent, not with the lid closed and no display). Nothing was derived and no other factor is substituted; the passphrase still opens the vault.", SYSTEM_CANCEL_WORDS = "the system cancelled the prompt (for example another window took focus, or this is not the logged-in session: SSH, a background agent, the lid closed with no display)";
19474
19494
  var init_enclave = __esm(() => {
19475
19495
  init_sha256();
19476
19496
  init_esm();
@@ -28207,6 +28227,7 @@ __export(exports_vault_support, {
28207
28227
  requireVaultRaw: () => requireVaultRaw,
28208
28228
  requireTty: () => requireTty,
28209
28229
  requirePromptStreams: () => requirePromptStreams,
28230
+ refusePromptsWithoutTerminal: () => refusePromptsWithoutTerminal,
28210
28231
  refuseEnvPassphrase: () => refuseEnvPassphrase,
28211
28232
  openWithTypedPassphrase: () => openWithTypedPassphrase,
28212
28233
  nonDefaultVaultFooter: () => nonDefaultVaultFooter,
@@ -28261,6 +28282,16 @@ function requirePromptStreams(ctx, what) {
28261
28282
  writeVaultFailure(ctx, new VaultError("VAULT_UNLOCK_FAILED", `${what} needs a terminal for the passphrase prompt: standard input and standard error must both be a terminal. Standard output may be redirected; that is where the listing goes.`));
28262
28283
  return false;
28263
28284
  }
28285
+ function refusePromptsWithoutTerminal(ctx, what) {
28286
+ if (ctx.deps.isTTY.stdin && ctx.deps.isTTY.stderr)
28287
+ return ctx;
28288
+ const refuse2 = async () => {
28289
+ throw new VaultError("VAULT_UNLOCK_FAILED", `${what} needs a terminal here: this unlock reads a vault passphrase, a security key PIN or a choice from a prompt, and standard input and standard error must both be a terminal for that. Standard output may be redirected.`, {
28290
+ suggestion: "Run it from a terminal, or pass --factor touch-id or --factor passkey to unlock with a factor that reads no prompt. No environment variable or flag supplies a passphrase or PIN."
28291
+ });
28292
+ };
28293
+ return { ...ctx, deps: { ...ctx.deps, promptSecret: refuse2, promptLine: refuse2 } };
28294
+ }
28264
28295
  function vaultPathFor(ctx, parsed) {
28265
28296
  const flag = parsed.values["--keystore"];
28266
28297
  if (flag !== undefined)
@@ -45684,7 +45715,7 @@ __export(exports_util, {
45684
45715
  joinValues: () => joinValues,
45685
45716
  issue: () => issue,
45686
45717
  isPlainObject: () => isPlainObject,
45687
- isObject: () => isObject,
45718
+ isObject: () => isObject2,
45688
45719
  hexToUint8Array: () => hexToUint8Array,
45689
45720
  getSizableOrigin: () => getSizableOrigin,
45690
45721
  getParsedType: () => getParsedType3,
@@ -45851,11 +45882,11 @@ function esc(str) {
45851
45882
  function slugify(input) {
45852
45883
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
45853
45884
  }
45854
- function isObject(data) {
45885
+ function isObject2(data) {
45855
45886
  return typeof data === "object" && data !== null && !Array.isArray(data);
45856
45887
  }
45857
45888
  function isPlainObject(o) {
45858
- if (isObject(o) === false)
45889
+ if (isObject2(o) === false)
45859
45890
  return false;
45860
45891
  const ctor = o.constructor;
45861
45892
  if (ctor === undefined)
@@ -45863,7 +45894,7 @@ function isPlainObject(o) {
45863
45894
  if (typeof ctor !== "function")
45864
45895
  return true;
45865
45896
  const prot = ctor.prototype;
45866
- if (isObject(prot) === false)
45897
+ if (isObject2(prot) === false)
45867
45898
  return false;
45868
45899
  if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
45869
45900
  return false;
@@ -47744,13 +47775,13 @@ var init_schemas = __esm(() => {
47744
47775
  }
47745
47776
  return propValues;
47746
47777
  });
47747
- const isObject2 = isObject;
47778
+ const isObject3 = isObject2;
47748
47779
  const catchall = def.catchall;
47749
47780
  let value;
47750
47781
  inst._zod.parse = (payload, ctx) => {
47751
47782
  value ?? (value = _normalized.value);
47752
47783
  const input = payload.value;
47753
- if (!isObject2(input)) {
47784
+ if (!isObject3(input)) {
47754
47785
  payload.issues.push({
47755
47786
  expected: "object",
47756
47787
  code: "invalid_type",
@@ -47848,7 +47879,7 @@ var init_schemas = __esm(() => {
47848
47879
  return (payload, ctx) => fn(shape, payload, ctx);
47849
47880
  };
47850
47881
  let fastpass;
47851
- const isObject2 = isObject;
47882
+ const isObject3 = isObject2;
47852
47883
  const jit = !globalConfig.jitless;
47853
47884
  const allowsEval2 = allowsEval;
47854
47885
  const fastEnabled = jit && allowsEval2.value;
@@ -47857,7 +47888,7 @@ var init_schemas = __esm(() => {
47857
47888
  inst._zod.parse = (payload, ctx) => {
47858
47889
  value ?? (value = _normalized.value);
47859
47890
  const input = payload.value;
47860
- if (!isObject2(input)) {
47891
+ if (!isObject3(input)) {
47861
47892
  payload.issues.push({
47862
47893
  expected: "object",
47863
47894
  code: "invalid_type",
@@ -47961,7 +47992,7 @@ var init_schemas = __esm(() => {
47961
47992
  });
47962
47993
  inst._zod.parse = (payload, ctx) => {
47963
47994
  const input = payload.value;
47964
- if (!isObject(input)) {
47995
+ if (!isObject2(input)) {
47965
47996
  payload.issues.push({
47966
47997
  code: "invalid_type",
47967
47998
  expected: "object",
@@ -62370,7 +62401,7 @@ var init_image = __esm(() => {
62370
62401
  });
62371
62402
 
62372
62403
  // ../mcp/src/orchestrate.ts
62373
- import { randomUUID as randomUUID7 } from "node:crypto";
62404
+ import { randomUUID as randomUUID8 } from "node:crypto";
62374
62405
  function requireApiKey(cfg) {
62375
62406
  if (!cfg.apiKey) {
62376
62407
  throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
@@ -62483,7 +62514,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
62483
62514
  }
62484
62515
  async function executeTrade(args, cfg, doFetch) {
62485
62516
  const apiKey = requireApiKey(cfg);
62486
- const clientTradeId = args.clientTradeId ?? randomUUID7();
62517
+ const clientTradeId = args.clientTradeId ?? randomUUID8();
62487
62518
  if (args.amount !== undefined && args.percent !== undefined) {
62488
62519
  return errText("pass exactly one of amount or percent, not both", { clientTradeId });
62489
62520
  }
@@ -62621,7 +62652,7 @@ async function executeTrade(args, cfg, doFetch) {
62621
62652
  }
62622
62653
  async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
62623
62654
  const apiKey = requireApiKey(cfg);
62624
- const clientLaunchId = args.clientLaunchId ?? randomUUID7();
62655
+ const clientLaunchId = args.clientLaunchId ?? randomUUID8();
62625
62656
  const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...imageFields } = args;
62626
62657
  let buyAmount;
62627
62658
  if (devBuy !== undefined) {
@@ -63169,7 +63200,7 @@ function checkOrderMarket2(action, intent, market) {
63169
63200
  function verifyPerpsBuild2(build, opts) {
63170
63201
  try {
63171
63202
  const network2 = opts.network ?? "mainnet";
63172
- if (!EVM_ADDRESS_RE2.test(opts.builder))
63203
+ if (!EVM_ADDRESS_RE3.test(opts.builder))
63173
63204
  return { ok: false, reason: "no valid Candle builder address to check against" };
63174
63205
  if (build.network !== undefined && build.network !== network2) {
63175
63206
  return { ok: false, reason: `the build is for ${build.network}, this client trades ${network2}` };
@@ -63257,7 +63288,7 @@ function hyperliquidExchangeBody2(action, nonce, signature) {
63257
63288
  function hyperliquidRelayBody2(typedData) {
63258
63289
  return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
63259
63290
  }
63260
- var HYPERLIQUID_ALLOWED_ACTION_TYPES2, HYPERLIQUID_EXCHANGE_URLS2, HYPERLIQUID_INFO_URLS2, CANDLE_HYPERLIQUID_BUILDER_ADDRESS2 = null, HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS2 = 100, HYPERLIQUID_MAX_BUILDER_FEE_RATE2 = "0.1%", HYPERLIQUID_SIGNATURE_CHAIN_ID2 = "0xa4b1", ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000", UINT64_MAX2, INT64_MIN2, EVM_ADDRESS_RE2, HYPERLIQUID_DEFAULT_SLIPPAGE_BPS2 = 100, HYPERLIQUID_MID_DRIFT_BPS2 = 100, HYPERLIQUID_TPSL_SLIPPAGE_BPS2 = 500;
63291
+ var HYPERLIQUID_ALLOWED_ACTION_TYPES2, HYPERLIQUID_EXCHANGE_URLS2, HYPERLIQUID_INFO_URLS2, CANDLE_HYPERLIQUID_BUILDER_ADDRESS2 = null, HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS2 = 100, HYPERLIQUID_MAX_BUILDER_FEE_RATE2 = "0.1%", HYPERLIQUID_SIGNATURE_CHAIN_ID2 = "0xa4b1", ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000", UINT64_MAX2, INT64_MIN2, EVM_ADDRESS_RE3, HYPERLIQUID_DEFAULT_SLIPPAGE_BPS2 = 100, HYPERLIQUID_MID_DRIFT_BPS2 = 100, HYPERLIQUID_TPSL_SLIPPAGE_BPS2 = 500;
63261
63292
  var init_hyperliquid = __esm(() => {
63262
63293
  init_sha3();
63263
63294
  HYPERLIQUID_ALLOWED_ACTION_TYPES2 = [
@@ -63277,7 +63308,7 @@ var init_hyperliquid = __esm(() => {
63277
63308
  };
63278
63309
  UINT64_MAX2 = (1n << 64n) - 1n;
63279
63310
  INT64_MIN2 = -(1n << 63n);
63280
- EVM_ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
63311
+ EVM_ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
63281
63312
  });
63282
63313
 
63283
63314
  // ../mcp/src/perps.ts
@@ -63764,7 +63795,7 @@ var init_plans = __esm(() => {
63764
63795
  });
63765
63796
 
63766
63797
  // ../mcp/src/tools.ts
63767
- import { randomUUID as randomUUID8 } from "node:crypto";
63798
+ import { randomUUID as randomUUID9 } from "node:crypto";
63768
63799
  function resolveToolAllowlist(env) {
63769
63800
  const raw = env.CANDLE_MCP_TOOLS?.trim();
63770
63801
  if (!raw)
@@ -64096,7 +64127,7 @@ async function getOperation(args, cfg, doFetch) {
64096
64127
  };
64097
64128
  }
64098
64129
  async function swap2(args, cfg, doFetch) {
64099
- const clientSwapId = typeof args.clientSwapId === "string" && args.clientSwapId.length > 0 ? args.clientSwapId : randomUUID8();
64130
+ const clientSwapId = typeof args.clientSwapId === "string" && args.clientSwapId.length > 0 ? args.clientSwapId : randomUUID9();
64100
64131
  const { url, init } = buildRequest("candle_swap", { ...args, clientSwapId }, cfg);
64101
64132
  let ok;
64102
64133
  let body;
@@ -65007,7 +65038,7 @@ init_render();
65007
65038
  init_secret_store();
65008
65039
 
65009
65040
  // src/version.ts
65010
- var CLI_VERSION = "0.11.31";
65041
+ var CLI_VERSION = "0.11.32";
65011
65042
 
65012
65043
  // src/commands/auth.ts
65013
65044
  init_keys_embedded_wallet();
@@ -65438,6 +65469,10 @@ var ENVIRONMENT = [
65438
65469
  description: "candle perps: Candle's Hyperliquid builder address, pinned for the check every build passes before it is signed; without it the release's, else the one the server reports"
65439
65470
  },
65440
65471
  { name: "CANDLE_HYPERLIQUID_NETWORK", description: "candle perps: set to testnet to trade Hyperliquid testnet" },
65472
+ {
65473
+ name: "CANDLE_PERPS_WITHDRAW_PINS",
65474
+ description: "candle perps withdraw: JSON {token, destinationDex, depositories, amountForm} pinning what every withdrawal is checked against before it is signed; this release ships none, so without it withdrawals are refused"
65475
+ },
65441
65476
  { name: "CANDLE_NO_UPDATE_NOTIFIER", description: "Set to 1 to silence the update notice" },
65442
65477
  {
65443
65478
  name: "CANDLE_KEYSTORE_PASSPHRASE",
@@ -65626,8 +65661,8 @@ var HELP = {
65626
65661
  },
65627
65662
  perps: {
65628
65663
  group: "Trade",
65629
- summary: "Hyperliquid perpetuals from your EVM TEE wallet: setup, deposit, open, close, cancel, orders, positions, leverage",
65630
- description: "Trade perpetuals on Hyperliquid's main perp exchange from the EVM TEE wallet bound to this key (scope perps:write, opt-in). Candle builds each action within the key's limits; this machine recomputes its hash and checks the action type and Candle's builder before anything is signed, then Candle's relay signs and this machine submits to Hyperliquid. Every plan is eligible for perps, once the deployment has perps enabled. Every plan except Max pays Candle a builder fee on each order (0.1% today); Max pays none. candle plans shows the rate in force. Deposits move SOL, USDC, ETH or USDG onto the Hyperliquid account through Relay, and the account's P&L (candle pnl) carries a Hyperliquid section.",
65664
+ summary: "Hyperliquid perpetuals from your EVM TEE wallet: setup, deposit, withdraw, open, close, cancel, orders, positions, leverage",
65665
+ description: "Trade perpetuals on Hyperliquid's main perp exchange from the EVM TEE wallet bound to this key (scope perps:write, opt-in). Candle builds each action within the key's limits; this machine recomputes its hash and checks the action type and Candle's builder before anything is signed, then Candle's relay signs and this machine submits to Hyperliquid. Every plan is eligible for perps, once the deployment has perps enabled. Every plan except Max pays Candle a builder fee on each order (0.1% today); Max pays none. candle plans shows the rate in force. Deposits move SOL, USDC, ETH or USDG onto the Hyperliquid account through Relay; withdrawals move USDC back through Relay to the key's own Solana or Hood TEE wallet (scope perps:withdraw, opt-in, after one vault unlock and a typed confirm). The account's P&L (candle pnl) carries a Hyperliquid section.",
65631
65666
  usage: ["candle perps <subcommand> [flags]"],
65632
65667
  rows: [
65633
65668
  {
@@ -65648,7 +65683,14 @@ var HELP = {
65648
65683
  invocation: "orders [--wallet <tee>]",
65649
65684
  description: "Open orders on Hyperliquid, and every action Candle built"
65650
65685
  },
65651
- { invocation: "positions [--wallet <tee>]", description: "Positions and account value, read live" },
65686
+ {
65687
+ invocation: "withdraw <amount> --to solana|hood [--wallet <evm tee>] | withdraw status <id>",
65688
+ description: "Move USDC from the Hyperliquid account to this key's own Solana or Hood TEE wallet through Relay (perps:withdraw); one vault unlock and a typed confirm, no --yes"
65689
+ },
65690
+ {
65691
+ invocation: "positions [--wallet <tee>]",
65692
+ description: "Positions, account value and withdrawable USDC, read live, and any withdrawal in flight"
65693
+ },
65652
65694
  { invocation: "leverage <coin> <x> [--isolated]", description: "Set a market's leverage and margin mode" }
65653
65695
  ],
65654
65696
  flags: [
@@ -65666,7 +65708,8 @@ var HELP = {
65666
65708
  invocation: "--to <evm tee>",
65667
65709
  description: "deposit from Solana: the EVM TEE wallet whose account receives it"
65668
65710
  },
65669
- { invocation: "--id <clientDepositId>", description: "deposit: the idempotency id (default: a new one)" },
65711
+ { invocation: "--id <id>", description: "deposit and withdraw: the idempotency id (default: a new one)" },
65712
+ { invocation: "--keystore <path>", description: "withdraw: the vault whose unlock confirms the withdrawal" },
65670
65713
  { invocation: "--rpc-url <url>", description: "deposit from Solana: the RPC the deposit check reads" },
65671
65714
  { invocation: "--no-submit", description: "Build and check only; sign and submit nothing" },
65672
65715
  { invocation: "--sign-only", description: "Build, check and sign through the relay; submit nothing" },
@@ -65678,9 +65721,10 @@ var HELP = {
65678
65721
  "candle perps open BTC long 0.01",
65679
65722
  "candle perps open ETH short 0.5 --price 3200 --tp 2900 --sl 3400",
65680
65723
  "candle perps close BTC",
65681
- "candle perps leverage BTC 5 --isolated"
65724
+ "candle perps leverage BTC 5 --isolated",
65725
+ "candle perps withdraw 25 --to solana"
65682
65726
  ],
65683
- env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK"]
65727
+ env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK", "CANDLE_PERPS_WITHDRAW_PINS"]
65684
65728
  },
65685
65729
  transfer: {
65686
65730
  group: "Trade",
@@ -74258,6 +74302,13 @@ async function perpsLeverage(args, ctx) {
74258
74302
  return tradingFailure(ctx, error);
74259
74303
  }
74260
74304
  }
74305
+ function formatUsdcMicros(value) {
74306
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
74307
+ return "?";
74308
+ const micros = BigInt(value);
74309
+ const frac = (micros % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
74310
+ return frac ? `${micros / 1000000n}.${frac}` : `${micros / 1000000n}`;
74311
+ }
74261
74312
  async function perpsPositions(args, ctx) {
74262
74313
  const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
74263
74314
  if ("error" in parsed)
@@ -74281,6 +74332,13 @@ async function perpsPositions(args, ctx) {
74281
74332
  for (const p of positions) {
74282
74333
  const leverage = p.leverage ?? {};
74283
74334
  out.write(` ${safeText(p.coin)} ${safeText(p.szi)} at ${safeText(p.entryPx)}, value ${safeText(p.positionValue)}, uPnL ${safeText(p.unrealizedPnl)}, ${safeText(leverage.value)}x ${safeText(leverage.type)}, liq ${safeText(p.liquidationPx ?? "none")}
74335
+ `);
74336
+ }
74337
+ const open5 = Array.isArray(result.openWithdrawals) ? result.openWithdrawals : null;
74338
+ out.write(`Withdrawable now: ${safeText(result.withdrawable)} USDC${open5 === null ? "" : open5.length === 0 ? "; no withdrawal in flight" : ""}.
74339
+ `);
74340
+ for (const w of open5 ?? []) {
74341
+ out.write(` Withdrawal ${safeText(w.id)}: ${safeText(w.status)}, ${formatUsdcMicros(w.amountUsdcMicros)} USDC to ${safeText(w.to)} (candle perps withdraw status ${safeText(w.id)})
74284
74342
  `);
74285
74343
  }
74286
74344
  return 0;
@@ -74466,7 +74524,7 @@ async function perpsDeposit(args, ctx) {
74466
74524
  `);
74467
74525
  output.write(`Relay delivers at least ${minimum} USDC (estimate ${estimate2})${build.firstDeposit ? "; Hyperliquid keeps 1 USDC to activate this new account" : ""}. Candle fee: none.
74468
74526
  `);
74469
- output.write("There is no withdrawal from Hyperliquid in this release, and `candle tee disable` or `tee sweep` do not reach it.\n");
74527
+ output.write("To bring it back: candle perps withdraw <amount> --to solana|hood (perps:withdraw). `candle tee disable` and `tee sweep` do not reach it, so withdraw before disabling.\n");
74470
74528
  if (sequenced)
74471
74529
  output.write(`Legs, signed one at a time: ${sequenced.plannedLegCount === 2 ? "approval, bridgeDeposit" : "bridgeDeposit"}
74472
74530
  `);
@@ -74520,6 +74578,382 @@ async function perpsDeposit(args, ctx) {
74520
74578
  }
74521
74579
  }
74522
74580
 
74581
+ // src/commands/perps-withdraw.ts
74582
+ init_args();
74583
+ import { randomUUID as randomUUID5 } from "node:crypto";
74584
+
74585
+ // src/hyperliquid-withdraw.ts
74586
+ var CANDLE_PERPS_WITHDRAW_PINS = null;
74587
+ var RELAY_API_URL = "https://api.relay.link";
74588
+ var ZERO = "0x0000000000000000000000000000000000000000";
74589
+ var EVM_ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
74590
+ var BYTES32_RE = /^0x[0-9a-fA-F]{64}$/;
74591
+ var RELAY_NONCE_MAPPING_TYPES = {
74592
+ NonceMapping: [
74593
+ { name: "chainId", type: "string" },
74594
+ { name: "wallet", type: "address" },
74595
+ { name: "depositor", type: "address" },
74596
+ { name: "id", type: "bytes32" },
74597
+ { name: "nonce", type: "uint256" }
74598
+ ]
74599
+ };
74600
+ var HYPERLIQUID_SEND_ASSET_TYPES = {
74601
+ "HyperliquidTransaction:SendAsset": [
74602
+ { name: "hyperliquidChain", type: "string" },
74603
+ { name: "destination", type: "string" },
74604
+ { name: "sourceDex", type: "string" },
74605
+ { name: "destinationDex", type: "string" },
74606
+ { name: "token", type: "string" },
74607
+ { name: "amount", type: "string" },
74608
+ { name: "fromSubAccount", type: "string" },
74609
+ { name: "nonce", type: "uint64" }
74610
+ ]
74611
+ };
74612
+ var SEND_ASSET_KEYS = HYPERLIQUID_SEND_ASSET_TYPES["HyperliquidTransaction:SendAsset"].map((field) => field.name);
74613
+ var REQUEST_AMOUNT_RE = /^(0|[1-9][0-9]{0,11})(\.[0-9]{1,6})?$/;
74614
+ var CANONICAL_RE = /^(0|[1-9][0-9]*)(\.[0-9]{0,5}[1-9])?$/;
74615
+ var FIXED6_RE = /^(0|[1-9][0-9]*)\.[0-9]{6}$/;
74616
+ function parseWithdrawAmount(value) {
74617
+ if (!REQUEST_AMOUNT_RE.test(value))
74618
+ return null;
74619
+ const [whole = "0", frac = ""] = value.split(".");
74620
+ const micros = BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
74621
+ return micros > 0n ? micros : null;
74622
+ }
74623
+ function formatWithdrawAmount(micros) {
74624
+ const frac = (micros % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
74625
+ return frac ? `${micros / 1000000n}.${frac}` : `${micros / 1000000n}`;
74626
+ }
74627
+ function parseSendAssetAmount(value, form) {
74628
+ if (typeof value !== "string" || !(form === "canonical" ? CANONICAL_RE : FIXED6_RE).test(value))
74629
+ return null;
74630
+ const [whole = "0", frac = ""] = value.split(".");
74631
+ if (frac.length > 6)
74632
+ return null;
74633
+ return BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
74634
+ }
74635
+ function isObject(value) {
74636
+ return typeof value === "object" && value !== null && !Array.isArray(value);
74637
+ }
74638
+ function exactKeys(value, keys) {
74639
+ const own = Object.keys(value);
74640
+ return own.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
74641
+ }
74642
+ function sameEvm(a, b) {
74643
+ return typeof a === "string" && EVM_ADDRESS_RE2.test(a) && a.toLowerCase() === b.toLowerCase();
74644
+ }
74645
+ function typesMatch(types2, expected) {
74646
+ if (!isObject(types2) || !exactKeys(types2, Object.keys(expected)))
74647
+ return false;
74648
+ return Object.entries(expected).every(([key, want]) => {
74649
+ const fields = types2[key];
74650
+ return Array.isArray(fields) && fields.length === want.length && fields.every((field, i) => isObject(field) && exactKeys(field, ["name", "type"]) && field.name === want[i]?.name && field.type === want[i]?.type);
74651
+ });
74652
+ }
74653
+ function domainMatches(domain, expected) {
74654
+ return isObject(domain) && exactKeys(domain, ["name", "version", "chainId", "verifyingContract"]) && domain.name === expected.name && domain.version === expected.version && domain.chainId === expected.chainId && typeof domain.verifyingContract === "string" && domain.verifyingContract.toLowerCase() === ZERO;
74655
+ }
74656
+ function verifyWithdrawMapping(build, expect) {
74657
+ const refuse3 = (reason) => ({ ok: false, reason });
74658
+ if (expect.pins === null)
74659
+ return refuse3("the withdrawal pins are pending verification in this release");
74660
+ if (build.id !== expect.id)
74661
+ return refuse3("the build names another withdrawal id");
74662
+ if (!sameEvm(build.from?.address, expect.account))
74663
+ return refuse3("the build withdraws from another account");
74664
+ if (build.to?.chain !== expect.to)
74665
+ return refuse3(`the build lands on ${String(build.to?.chain)}, not ${expect.to}`);
74666
+ const destination = build.to.address;
74667
+ const own = expect.ownDestinations.some((address) => address.startsWith("0x") ? sameEvm(destination, address) : destination === address);
74668
+ if (!own)
74669
+ return refuse3("the build lands on a wallet that is not this key's own");
74670
+ if (parseWithdrawAmount(build.amountUsdc) !== expect.amountMicros)
74671
+ return refuse3("the build is for another amount");
74672
+ const typed = build.typedData;
74673
+ if (!isObject(typed) || typed.primary_type !== "NonceMapping")
74674
+ return refuse3("the typed data is not a NonceMapping");
74675
+ if (!domainMatches(typed.domain, { name: "RelayNonceMapping", version: "2", chainId: 1 }))
74676
+ return refuse3("the NonceMapping domain is not Relay's");
74677
+ if (!typesMatch(typed.types, RELAY_NONCE_MAPPING_TYPES))
74678
+ return refuse3("the NonceMapping type list differs");
74679
+ const message = typed.message;
74680
+ if (!isObject(message) || !exactKeys(message, ["chainId", "wallet", "depositor", "id", "nonce"]))
74681
+ return refuse3("the NonceMapping message has other fields");
74682
+ if (message.chainId !== "hyperliquid")
74683
+ return refuse3("the NonceMapping names another chain");
74684
+ if (!sameEvm(message.wallet, expect.account))
74685
+ return refuse3("the NonceMapping wallet is not this key's account");
74686
+ if (!sameEvm(message.depositor, expect.account))
74687
+ return refuse3("the NonceMapping depositor is not this key's account");
74688
+ if (typeof message.id !== "string" || !BYTES32_RE.test(message.id) || message.id !== build.relayRequestId)
74689
+ return refuse3("the NonceMapping names another Relay request");
74690
+ if (message.nonce !== build.nonce || !Number.isSafeInteger(build.nonce) || build.nonce <= 0)
74691
+ return refuse3("the NonceMapping nonce is not the build's");
74692
+ return { ok: true };
74693
+ }
74694
+ function verifyWithdrawSendAsset(authorization, expect) {
74695
+ const refuse3 = (reason) => ({ ok: false, reason });
74696
+ const pins = expect.pins;
74697
+ if (pins === null)
74698
+ return refuse3("the withdrawal pins are pending verification in this release");
74699
+ const typed = authorization.typedData;
74700
+ if (!isObject(typed) || typed.primary_type !== "HyperliquidTransaction:SendAsset")
74701
+ return refuse3("the typed data is not a SendAsset");
74702
+ if (!domainMatches(typed.domain, { name: "HyperliquidSignTransaction", version: "1", chainId: 42161 }))
74703
+ return refuse3("the SendAsset domain is not Hyperliquid's user-signed domain");
74704
+ if (!typesMatch(typed.types, HYPERLIQUID_SEND_ASSET_TYPES))
74705
+ return refuse3("the SendAsset type list differs");
74706
+ const m = typed.message;
74707
+ if (!isObject(m) || !exactKeys(m, SEND_ASSET_KEYS))
74708
+ return refuse3("the SendAsset message has other fields");
74709
+ if (m.hyperliquidChain !== "Mainnet")
74710
+ return refuse3("the SendAsset is not on Mainnet");
74711
+ if (m.nonce !== expect.mappingNonce || authorization.nonce !== expect.mappingNonce)
74712
+ return refuse3("the SendAsset nonce is not the NonceMapping's");
74713
+ if (parseSendAssetAmount(m.amount, pins.amountForm) !== expect.amountMicros)
74714
+ return refuse3("the SendAsset amount is not the amount asked for");
74715
+ if (m.fromSubAccount !== "")
74716
+ return refuse3("the SendAsset moves from a sub-account");
74717
+ if (m.sourceDex !== "")
74718
+ return refuse3("the SendAsset moves from another dex");
74719
+ if (m.token !== pins.token)
74720
+ return refuse3("the SendAsset moves another token");
74721
+ if (m.destinationDex !== pins.destinationDex)
74722
+ return refuse3("the SendAsset names another destination dex");
74723
+ if (typeof m.destination !== "string" || !pins.depositories.includes(m.destination.toLowerCase()))
74724
+ return refuse3("the SendAsset destination is not a Relay depository on the allow-list");
74725
+ const action = authorization.action;
74726
+ if (!isObject(action) || !exactKeys(action, ["type", "signatureChainId", ...SEND_ASSET_KEYS]))
74727
+ return refuse3("the action has other fields");
74728
+ if (action.type !== "sendAsset" || action.signatureChainId !== "0xa4b1")
74729
+ return refuse3("the action is not a sendAsset");
74730
+ for (const key of SEND_ASSET_KEYS) {
74731
+ if (action[key] !== m[key])
74732
+ return refuse3(`the action's ${key} differs from what is signed`);
74733
+ }
74734
+ return { ok: true };
74735
+ }
74736
+ async function relayRecipientProblem(fetchImpl, requestId, recipient, relayUrl = RELAY_API_URL) {
74737
+ let body;
74738
+ try {
74739
+ const res = await fetchImpl(`${relayUrl}/requests/v3?id=${encodeURIComponent(requestId)}`, { method: "GET" });
74740
+ if (!res.ok)
74741
+ return `Relay's record of the request could not be read (HTTP ${res.status})`;
74742
+ body = await res.json();
74743
+ } catch (error) {
74744
+ return `Relay's record of the request could not be read (${error instanceof Error ? error.message : "unknown"})`;
74745
+ }
74746
+ const requests = isObject(body) && Array.isArray(body.requests) ? body.requests : [];
74747
+ const match = requests.find((r) => isObject(r) && r.id === requestId);
74748
+ if (!isObject(match))
74749
+ return "Relay does not list this request";
74750
+ const named = match.recipient;
74751
+ const same = recipient.startsWith("0x") ? sameEvm(named, recipient) : named === recipient;
74752
+ return same ? null : `Relay's request pays ${String(named)}, not this key's wallet ${recipient}`;
74753
+ }
74754
+
74755
+ // src/commands/perps-withdraw.ts
74756
+ init_render();
74757
+ init_trading();
74758
+ init_errors2();
74759
+ init_store();
74760
+ init_swap();
74761
+ init_vault_support();
74762
+ var WITHDRAW_SCOPE = "perps:withdraw";
74763
+ var ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
74764
+ var USAGE5 = "Usage: candle perps withdraw <amount> --to solana|hood [--wallet <evm tee>] [--id <id>] [--keystore <path>] | candle perps withdraw status <id>";
74765
+ var WITHDRAW_POLL_MS = 10 * 60 * 1000;
74766
+ var WITHDRAW_POLL_INTERVAL_MS = 5000;
74767
+ var TERMINAL = new Set(["settled", "refunded", "failed"]);
74768
+ function usage5(ctx, line) {
74769
+ writeUsageFailure(ctx.deps, line, ctx.json);
74770
+ return 2;
74771
+ }
74772
+ function withdrawPins(env) {
74773
+ const raw = env.CANDLE_PERPS_WITHDRAW_PINS;
74774
+ if (raw === undefined || raw.trim() === "")
74775
+ return CANDLE_PERPS_WITHDRAW_PINS;
74776
+ let parsed;
74777
+ try {
74778
+ parsed = JSON.parse(raw);
74779
+ } catch {
74780
+ throw new TradingError("INVALID_PINS", "CANDLE_PERPS_WITHDRAW_PINS is not JSON.");
74781
+ }
74782
+ const p = parsed;
74783
+ if (typeof p !== "object" || p === null || typeof p.token !== "string" || typeof p.destinationDex !== "string" || !Array.isArray(p.depositories) || p.depositories.length === 0 || !p.depositories.every((d) => typeof d === "string" && /^0x[0-9a-fA-F]{40}$/.test(d)) || p.amountForm !== "canonical" && p.amountForm !== "fixed6") {
74784
+ throw new TradingError("INVALID_PINS", "CANDLE_PERPS_WITHDRAW_PINS must be {token, destinationDex, depositories: [0x...], amountForm: canonical|fixed6}.");
74785
+ }
74786
+ return {
74787
+ token: p.token,
74788
+ destinationDex: p.destinationDex,
74789
+ depositories: p.depositories.map((d) => d.toLowerCase()),
74790
+ amountForm: p.amountForm
74791
+ };
74792
+ }
74793
+ async function withdrawStatus(ctx, id) {
74794
+ const key = await tradingKey(ctx);
74795
+ const result = await perpsRequest(ctx, key, `/api/v1/agent/perps/withdraw/${encodeURIComponent(id)}`);
74796
+ if (ctx.json)
74797
+ return printTradingResult(ctx, result);
74798
+ const to = result.to ?? {};
74799
+ const relay = result.relay;
74800
+ ctx.deps.stdout.write(`Withdrawal ${safeText(id)}: ${safeText(result.status)}, ${safeText(result.amountUsdc)} USDC to ${safeText(to.address)} (${safeText(to.chain)}), fees ${safeText(result.feesUsdc)} USDC${result.failureReason ? `, ${safeText(result.failureReason)}` : ""}. Relay: ${relay ? safeText(relay.status) : "not read"}.
74801
+ `);
74802
+ return 0;
74803
+ }
74804
+ async function perpsWithdraw(args, ctx) {
74805
+ if (args[0] === "status") {
74806
+ if (args.length !== 2 || !ID_RE.test(args[1]))
74807
+ return usage5(ctx, USAGE5);
74808
+ try {
74809
+ return await withdrawStatus(ctx, args[1]);
74810
+ } catch (error) {
74811
+ return tradingFailure(ctx, error);
74812
+ }
74813
+ }
74814
+ if (args.includes("--yes") || args.includes("-y")) {
74815
+ return usage5(ctx, "candle perps withdraw has no --yes: every withdrawal is confirmed at the terminal.");
74816
+ }
74817
+ const parsed = parseArgs(args, {
74818
+ valueFlags: ["--to", "--wallet", "--id", "--keystore"],
74819
+ pathFlags: ["--keystore"]
74820
+ });
74821
+ if ("error" in parsed)
74822
+ return usage5(ctx, parsed.error);
74823
+ const [amount] = parsed.positionals;
74824
+ const to = parsed.values["--to"];
74825
+ if (parsed.positionals.length !== 1 || !amount || to !== "solana" && to !== "hood")
74826
+ return usage5(ctx, USAGE5);
74827
+ const amountMicros = parseWithdrawAmount(amount);
74828
+ if (amountMicros === null)
74829
+ return usage5(ctx, "The amount must be a positive USDC decimal with at most 6 decimals.");
74830
+ const id = parsed.values["--id"] ?? `withdraw-${randomUUID5()}`;
74831
+ if (!ID_RE.test(id))
74832
+ return usage5(ctx, "--id must be 1 to 128 letters, digits, '.', '_', ':' or '-'.");
74833
+ if (!refuseEnvPassphrase(ctx))
74834
+ return 1;
74835
+ if (!requireTty(ctx, "perps withdraw"))
74836
+ return 1;
74837
+ const resolvedVault = vaultPathFor(ctx, parsed);
74838
+ if ("error" in resolvedVault)
74839
+ return usage5(ctx, resolvedVault.error);
74840
+ try {
74841
+ const pins = withdrawPins(ctx.deps.env);
74842
+ if (pins === null) {
74843
+ throw new TradingError("PERPS_WITHDRAW_UNVERIFIED", "Hyperliquid withdrawals wait on a recorded verification of Relay's quote shape in this release. Nothing was built.");
74844
+ }
74845
+ const key = await tradingKey(ctx);
74846
+ const { wallet } = await perpsWallet(ctx, key, parsed.values["--wallet"], WITHDRAW_SCOPE);
74847
+ if (!wallet)
74848
+ throw new TradingError("TEE_WALLET_REQUIRED", "No EVM TEE wallet resolved.");
74849
+ const { rows } = await listTradingWallets(ctx, key);
74850
+ const ownDestinations = to === "hood" ? [wallet.address] : rows.filter((row) => row.chain === "solana").map((row) => row.address);
74851
+ if (ownDestinations.length === 0)
74852
+ throw new TradingError("TEE_WALLET_REQUIRED", "This key has no Solana TEE wallet, so --to solana has nowhere to land. Withdraw --to hood instead.");
74853
+ ctx.deps.stderr.write(`Withdrawal: ${id}
74854
+ `);
74855
+ const build = await perpsRequest(ctx, key, "/api/v1/agent/perps/withdraw", {
74856
+ clientWithdrawalId: id,
74857
+ amountUsdc: amount,
74858
+ to,
74859
+ wallet: wallet.id
74860
+ });
74861
+ if (build.status !== "quoted")
74862
+ return printTradingResult(ctx, build);
74863
+ const refused = (problem) => new TradingError("PERPS_BUILD_REFUSED", `Refused to sign this withdrawal: ${problem}. Nothing was signed.`);
74864
+ const mapping = verifyWithdrawMapping(build, {
74865
+ id,
74866
+ account: wallet.address,
74867
+ to,
74868
+ amountMicros,
74869
+ ownDestinations,
74870
+ pins
74871
+ });
74872
+ if (!mapping.ok)
74873
+ throw refused(mapping.reason);
74874
+ const recipient = await relayRecipientProblem(ctx.deps.fetch, build.relayRequestId, build.to.address);
74875
+ if (recipient)
74876
+ throw refused(recipient);
74877
+ const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
74878
+ output.write(`Withdraw ${formatWithdrawAmount(amountMicros)} USDC from Hyperliquid account ${safeText(wallet.address)} through Relay.
74879
+ `);
74880
+ output.write(`Relay fees ${safeText(build.feesUsdc)} USDC; you receive about ${safeText(build.netUsdc)} USDC, at least ${safeText(build.minimumOutUsdc)}.
74881
+ `);
74882
+ output.write(`To this key's ${to === "hood" ? "Hood" : "Solana"} TEE wallet:
74883
+ `);
74884
+ output.write(`to ${groupAddress(build.to.address)}
74885
+ `);
74886
+ const positions = await perpsRequest(ctx, key, `/api/v1/agent/perps/positions?walletId=${encodeURIComponent(wallet.id)}`).catch(() => null);
74887
+ if (Array.isArray(positions?.positions) && positions.positions.length > 0)
74888
+ output.write(`This account has open positions: withdrawing margin raises their liquidation risk.
74889
+ `);
74890
+ const raw = await requireVaultRaw(ctx, resolvedVault);
74891
+ const opened = await unlockInteractively(ctx, resolvedVault.path, raw, {
74892
+ promptText: "Vault passphrase (input hidden): ",
74893
+ reason: `withdraw ${formatWithdrawAmount(amountMicros)} USDC to ${build.to.address}`
74894
+ });
74895
+ closeVault(opened.vault);
74896
+ const typed = await ctx.deps.promptLine(`Type confirm to withdraw ${formatWithdrawAmount(amountMicros)} USDC to ${build.to.address}: `);
74897
+ if (typed.trim().toLowerCase() !== "confirm")
74898
+ throw new TradingError("CANCELLED", "The withdrawal was not confirmed; nothing was signed.");
74899
+ if (typeof build.expiresAt !== "number" || build.expiresAt <= ctx.deps.now())
74900
+ throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start again with a new --id.");
74901
+ const mappingSignature = await relaySignature(ctx, key, wallet, build.typedData);
74902
+ const authorization = await perpsRequest(ctx, key, "/api/v1/agent/perps/withdraw/authorize", {
74903
+ clientWithdrawalId: id,
74904
+ signature: mappingSignature
74905
+ });
74906
+ const send = verifyWithdrawSendAsset(authorization, { mappingNonce: build.nonce, amountMicros, pins });
74907
+ if (!send.ok)
74908
+ throw new TradingError("PERPS_BUILD_REFUSED", `Refused to sign the transfer: ${send.reason}. The nonce mapping was posted; nothing can move without this signature.`);
74909
+ const signature = await relaySignature(ctx, key, wallet, authorization.typedData);
74910
+ const exchangeBody = hyperliquidExchangeBody(authorization.action, authorization.nonce, signature);
74911
+ let exchange;
74912
+ try {
74913
+ const res = await ctx.deps.fetch(HYPERLIQUID_EXCHANGE_URLS.mainnet, {
74914
+ method: "POST",
74915
+ headers: { "content-type": "application/json" },
74916
+ body: JSON.stringify(exchangeBody)
74917
+ });
74918
+ const text = await res.text();
74919
+ if (!res.ok)
74920
+ throw new Error(`HTTP ${res.status}`);
74921
+ exchange = JSON.parse(text);
74922
+ } catch (error) {
74923
+ throw new TradingError("PERPS_SUBMIT_FAILED", `Signed, but the submit to Hyperliquid failed (${error instanceof Error ? error.message : "unknown"}). The signed transfer is in details.exchangeBody and stays valid for two days; check candle perps withdraw status ${id} before resubmitting.`, { exitCode: 3, details: { clientWithdrawalId: id, exchangeBody } });
74924
+ }
74925
+ await perpsRequest(ctx, key, "/api/v1/agent/perps/withdraw/submitted", {
74926
+ clientWithdrawalId: id,
74927
+ exchangeResponse: exchange
74928
+ }).catch(() => null);
74929
+ const refusal = venueRefusal(exchange);
74930
+ if (refusal)
74931
+ throw new TradingError("PERPS_VENUE_REFUSED", refusal, { details: { clientWithdrawalId: id, exchange } });
74932
+ ctx.deps.stderr.write(`Submitted to Hyperliquid; following Relay's fill.
74933
+ `);
74934
+ const deadline = ctx.deps.now() + WITHDRAW_POLL_MS;
74935
+ let status = await perpsRequest(ctx, key, `/api/v1/agent/perps/withdraw/${encodeURIComponent(id)}`);
74936
+ while (!TERMINAL.has(String(status.status)) && ctx.deps.now() < deadline) {
74937
+ await ctx.deps.sleep(WITHDRAW_POLL_INTERVAL_MS);
74938
+ status = await perpsRequest(ctx, key, `/api/v1/agent/perps/withdraw/${encodeURIComponent(id)}`);
74939
+ }
74940
+ if (!TERMINAL.has(String(status.status)))
74941
+ ctx.deps.stderr.write(`Still ${safeText(status.status)} after 10 minutes. Check it with candle perps withdraw status ${id}.
74942
+ `);
74943
+ return printTradingResult(ctx, { ...status, signature, exchange });
74944
+ } catch (error) {
74945
+ if (isVaultError(error))
74946
+ return writeVaultFailure(ctx, error);
74947
+ return tradingFailure(ctx, error);
74948
+ }
74949
+ }
74950
+ async function relaySignature(ctx, key, wallet, typedData) {
74951
+ const relay = await perpsRequest(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(wallet.id)}/sign`, typedDataAuthorization(wallet, typedData));
74952
+ if (typeof relay.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(relay.signature))
74953
+ throw new TradingError("INVALID_RESPONSE", "The relay did not return a signature.");
74954
+ return relay.signature;
74955
+ }
74956
+
74523
74957
  // src/commands/plans.ts
74524
74958
  init_args();
74525
74959
  init_client();
@@ -74594,12 +75028,12 @@ function planPromotionLine(table) {
74594
75028
 
74595
75029
  // src/commands/plans.ts
74596
75030
  init_render();
74597
- var USAGE5 = "Usage: candle plans";
75031
+ var USAGE6 = "Usage: candle plans";
74598
75032
  async function plans(args, ctx) {
74599
75033
  const parsed = parseArgs(args, {});
74600
75034
  if ("error" in parsed || parsed.positionals.length > 0) {
74601
75035
  writeUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
74602
- ${USAGE5}` : USAGE5, ctx.json);
75036
+ ${USAGE6}` : USAGE6, ctx.json);
74603
75037
  return 2;
74604
75038
  }
74605
75039
  const result = await apiRequest("/api/v1/agent/plans", {
@@ -74838,20 +75272,20 @@ async function writeSecretNames(deps, profile, names) {
74838
75272
  else
74839
75273
  await deps.updateProfile(profile, { secretNames: sorted });
74840
75274
  }
74841
- function usage5(ctx, line) {
75275
+ function usage6(ctx, line) {
74842
75276
  writeUsageFailure(ctx.deps, line, ctx.json);
74843
75277
  return 2;
74844
75278
  }
74845
75279
  async function secretsSet(args, ctx) {
74846
75280
  const parsed = parseArgs(args, {});
74847
75281
  if ("error" in parsed)
74848
- return usage5(ctx, parsed.error);
75282
+ return usage6(ctx, parsed.error);
74849
75283
  const [raw, extra] = parsed.positionals;
74850
75284
  if (!raw || extra !== undefined)
74851
- return usage5(ctx, "Usage: candle secrets set <name>");
75285
+ return usage6(ctx, "Usage: candle secrets set <name>");
74852
75286
  const name2 = canonicalSecretName(raw);
74853
75287
  if (name2 === undefined) {
74854
- return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
75288
+ return usage6(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
74855
75289
  }
74856
75290
  if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
74857
75291
  writeLocalFailure(ctx.deps, {
@@ -74863,7 +75297,7 @@ async function secretsSet(args, ctx) {
74863
75297
  }
74864
75298
  const value = await ctx.deps.promptSecret(`Value for ${name2} (input hidden): `);
74865
75299
  if (value.length === 0)
74866
- return usage5(ctx, "An empty value was typed; nothing was stored.");
75300
+ return usage6(ctx, "An empty value was typed; nothing was stored.");
74867
75301
  try {
74868
75302
  await ctx.deps.secretsStore.set(secretRef(ctx.profile, name2), value);
74869
75303
  } catch (error) {
@@ -74883,9 +75317,9 @@ async function secretsSet(args, ctx) {
74883
75317
  async function secretsList(args, ctx) {
74884
75318
  const parsed = parseArgs(args, {});
74885
75319
  if ("error" in parsed)
74886
- return usage5(ctx, parsed.error);
75320
+ return usage6(ctx, parsed.error);
74887
75321
  if (parsed.positionals.length > 0)
74888
- return usage5(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
75322
+ return usage6(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
74889
75323
  const names = await storedSecretNames(ctx.deps, ctx.profile);
74890
75324
  if (ctx.json) {
74891
75325
  ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
@@ -74904,13 +75338,13 @@ async function secretsList(args, ctx) {
74904
75338
  async function secretsRemove(args, ctx) {
74905
75339
  const parsed = parseArgs(args, {});
74906
75340
  if ("error" in parsed)
74907
- return usage5(ctx, parsed.error);
75341
+ return usage6(ctx, parsed.error);
74908
75342
  const [raw, extra] = parsed.positionals;
74909
75343
  if (!raw || extra !== undefined)
74910
- return usage5(ctx, "Usage: candle secrets remove <name>");
75344
+ return usage6(ctx, "Usage: candle secrets remove <name>");
74911
75345
  const name2 = canonicalSecretName(raw);
74912
75346
  if (name2 === undefined) {
74913
- return usage5(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
75347
+ return usage6(ctx, `A secret name is letters, digits and underscores, starting with a letter: ${raw}`);
74914
75348
  }
74915
75349
  await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name2));
74916
75350
  const names = await storedSecretNames(ctx.deps, ctx.profile);
@@ -77064,7 +77498,7 @@ init_args();
77064
77498
  init_render();
77065
77499
  init_trading();
77066
77500
  init_swap();
77067
- import { randomUUID as randomUUID5 } from "node:crypto";
77501
+ import { randomUUID as randomUUID6 } from "node:crypto";
77068
77502
  var withdrawal = exports_external.object({ withdrawal: exports_external.string(), amountRaw: exports_external.string(), endTime: exports_external.string(), claimable: exports_external.boolean() }).passthrough();
77069
77503
  var positionsSchema = exports_external.object({
77070
77504
  success: exports_external.literal(true),
@@ -77113,16 +77547,16 @@ async function runStake(action, args, ctx) {
77113
77547
  valueFlags: ["--wallet", "--client-stake-id"],
77114
77548
  booleanFlags: ["--yes", ...action === "claim" ? ["--all"] : []]
77115
77549
  });
77116
- const usage6 = "Usage: candle stake [add <amount> | remove <amount> | finish [<withdrawal>] | claim <rewardId> | claim --all] [--wallet <main|id|address|label>] [--client-stake-id <id>] [--yes] [--json]";
77550
+ const usage7 = "Usage: candle stake [add <amount> | remove <amount> | finish [<withdrawal>] | claim <rewardId> | claim --all] [--wallet <main|id|address|label>] [--client-stake-id <id>] [--yes] [--json]";
77117
77551
  if ("error" in parsed) {
77118
77552
  writeUsageFailure(ctx.deps, parsed.error, ctx.json);
77119
77553
  return 2;
77120
77554
  }
77121
77555
  const { values, positionals, booleans } = parsed;
77122
- const id = values["--client-stake-id"] ?? `stake-${randomUUID5()}`;
77556
+ const id = values["--client-stake-id"] ?? `stake-${randomUUID6()}`;
77123
77557
  const all = booleans.has("--all");
77124
77558
  if (!validClientId(id) || id.length > 128 || (action === "positions" ? positionals.length !== 0 : action === "finish" ? positionals.length > 1 : action === "claim" ? all ? positionals.length !== 0 : positionals.length !== 1 : positionals.length !== 1)) {
77125
- writeUsageFailure(ctx.deps, usage6, ctx.json);
77559
+ writeUsageFailure(ctx.deps, usage7, ctx.json);
77126
77560
  return 2;
77127
77561
  }
77128
77562
  try {
@@ -77904,7 +78338,7 @@ init_render();
77904
78338
  init_solana_verify();
77905
78339
  init_trading();
77906
78340
  init_swap();
77907
- var USAGE6 = "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]";
78341
+ var USAGE7 = "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]";
77908
78342
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
77909
78343
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
77910
78344
  async function readLinkedWallets(ctx, key) {
@@ -77990,7 +78424,7 @@ async function transfer(args, ctx) {
77990
78424
  const flags = parsed.values;
77991
78425
  const asset = flags["--asset"]?.toUpperCase();
77992
78426
  if (parsed.positionals.length !== 0 || !flags["--to"] || !flags["--amount"] || [flags["--asset"], flags["--mint"], flags["--token"]].filter(Boolean).length !== 1 || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags["--mint"] !== undefined && !BASE58_ADDRESS.test(flags["--mint"]) || flags["--token"] !== undefined && !checkEvmAddress(flags["--token"]).ok) {
77993
- writeUsageFailure(ctx.deps, USAGE6, ctx.json);
78427
+ writeUsageFailure(ctx.deps, USAGE7, ctx.json);
77994
78428
  return 2;
77995
78429
  }
77996
78430
  const to = flags["--to"];
@@ -81225,9 +81659,11 @@ async function planTransfer(input) {
81225
81659
  const destination = ataFor(profile, toKey);
81226
81660
  const instructions = [];
81227
81661
  const accountCreationLines = [];
81662
+ let accountRent;
81228
81663
  const destinationState = await tokenAccountFrozen(rpc, encodePubkey(destination));
81229
81664
  if (!destinationState.exists) {
81230
- const rent = await rpc.getMinimumBalanceForRentExemption(165);
81665
+ const rent = await rpc.getMinimumBalanceForRentExemption(profile.associatedTokenAccountSize);
81666
+ accountRent = rent;
81231
81667
  instructions.push(createAssociatedTokenAccountIdempotent({ payer: fromKey, owner: toKey, mint, tokenProgram }));
81232
81668
  accountCreationLines.push(`create associated token account ${encodePubkey(destination)} (idempotent)`, `account owner ${input.to}`, `account rent ${rent} lamports, paid by ${input.from} if created`);
81233
81669
  }
@@ -81263,6 +81699,8 @@ async function planTransfer(input) {
81263
81699
  from: input.from,
81264
81700
  to: input.to,
81265
81701
  instructions,
81702
+ sourceTokenAccount: encodePubkey(source),
81703
+ ...accountRent !== undefined ? { accountRent } : {},
81266
81704
  token: {
81267
81705
  profile,
81268
81706
  extraAccountsMissing: !hook.ok,
@@ -81293,6 +81731,45 @@ async function quoteTransferFee(rpc, from, instructions) {
81293
81731
  });
81294
81732
  return rpc.getFeeForMessage(toBase642(message));
81295
81733
  }
81734
+ var FALLBACK_FEE_LAMPORTS = 5000n;
81735
+ var TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
81736
+ function tokenAccountAmount(data) {
81737
+ if (data.length < TOKEN_ACCOUNT_AMOUNT_OFFSET + 8)
81738
+ return 0n;
81739
+ return new DataView(data.buffer, data.byteOffset + TOKEN_ACCOUNT_AMOUNT_OFFSET, 8).getBigUint64(0, true);
81740
+ }
81741
+ async function assertTransferAffordable(solana, plan, feeQuote) {
81742
+ const fee = feeQuote ?? FALLBACK_FEE_LAMPORTS;
81743
+ const rent = plan.accountRent ?? 0n;
81744
+ const lamportsNeeded = (plan.asset === "SOL" ? plan.amountRaw : 0n) + fee + rent;
81745
+ const lamports = await solana.read(() => solana.rpc.getBalance(plan.from));
81746
+ if (plan.sourceTokenAccount !== undefined) {
81747
+ const account = await solana.read(() => solana.rpc.getAccountInfo(plan.sourceTokenAccount));
81748
+ const held = account === null ? 0n : tokenAccountAmount(account.data);
81749
+ if (held < plan.amountRaw) {
81750
+ throw new VaultError("TRANSFER_INSUFFICIENT_FUNDS", `${plan.from} holds ${held} raw of ${plan.asset} and this transfer moves ${plan.amountRaw} raw (${plan.amount}). Nothing was signed or sent.`, {
81751
+ suggestion: "Fund the key with this token first, or send a smaller --amount.",
81752
+ details: {
81753
+ from: plan.from,
81754
+ asset: plan.asset,
81755
+ heldRaw: held.toString(),
81756
+ neededRaw: plan.amountRaw.toString()
81757
+ }
81758
+ });
81759
+ }
81760
+ }
81761
+ if (lamports < lamportsNeeded) {
81762
+ const parts = [
81763
+ ...plan.asset === "SOL" ? [`${plan.amountRaw} to send`] : [],
81764
+ `${fee} fee${feeQuote === null ? " (estimated)" : ""}`,
81765
+ ...rent > 0n ? [`${rent} token account rent`] : []
81766
+ ];
81767
+ throw new VaultError("TRANSFER_INSUFFICIENT_FUNDS", `${plan.from} holds ${lamports} lamports and this transfer needs ${lamportsNeeded} (${parts.join(" + ")}). Nothing was signed or sent.`, {
81768
+ suggestion: "Fund the key with SOL first, or send a smaller --amount. candle vault list --balances shows each key's SOL.",
81769
+ details: { from: plan.from, heldLamports: lamports.toString(), neededLamports: lamportsNeeded.toString() }
81770
+ });
81771
+ }
81772
+ }
81296
81773
  function displayTransferPlan(ctx, plan, feeQuote) {
81297
81774
  ctx.deps.stdout.write(`Decoded transfer (local signing only):
81298
81775
  `);
@@ -81327,8 +81804,9 @@ async function signAndBroadcastTransfer(input) {
81327
81804
  } catch (error) {
81328
81805
  if (isRateLimited(error))
81329
81806
  notePostSignatureRateLimit(input.ctx, sigB58);
81330
- return { signature: sigB58, finalized: false };
81807
+ return { signature: sigB58, finalized: false, seen: true };
81331
81808
  }
81809
+ let seen = false;
81332
81810
  for (let i = 0;i < 30; i++) {
81333
81811
  await input.ctx.deps.sleep(500);
81334
81812
  let status;
@@ -81337,17 +81815,24 @@ async function signAndBroadcastTransfer(input) {
81337
81815
  } catch (error) {
81338
81816
  if (isRateLimited(error))
81339
81817
  notePostSignatureRateLimit(input.ctx, sigB58);
81340
- return { signature: sigB58, finalized: false };
81818
+ return { signature: sigB58, finalized: false, seen: true };
81341
81819
  }
81820
+ if (status)
81821
+ seen = true;
81342
81822
  if (status?.confirmationStatus === "finalized") {
81343
81823
  if (status.err) {
81344
81824
  const named = namedSendFailure(input.plan);
81345
81825
  throw new VaultError("VAULT_WRITE_FAILED", `Transfer failed on chain${named ? ` (${named})` : ""}. Any pending funding receipt will be reconciled on a rerun.`);
81346
81826
  }
81347
- return { signature: sigB58, finalized: true };
81827
+ return { signature: sigB58, finalized: true, seen: true };
81348
81828
  }
81349
81829
  }
81350
- return { signature: sigB58, finalized: false };
81830
+ return { signature: sigB58, finalized: false, seen };
81831
+ }
81832
+ function unfinalizedSendLine(result, suffix = "") {
81833
+ return result.seen ? `Submitted ${result.signature}; finality is not yet confirmed.${suffix}
81834
+ ` : `Sent ${result.signature}, but no status read found it on the network: it was most likely dropped and nothing moved. Check the signature in an explorer before sending again.${suffix}
81835
+ `;
81351
81836
  }
81352
81837
 
81353
81838
  // src/commands/vault-fund.ts
@@ -81386,6 +81871,7 @@ async function fundExternal(ctx, vault, external2, input) {
81386
81871
  }));
81387
81872
  const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan.from, plan.instructions));
81388
81873
  displayTransferPlan(ctx, plan, feeQuote);
81874
+ await assertTransferAffordable(solana, plan, feeQuote);
81389
81875
  await confirmSend(ctx, { amount: plan.amount, asset: plan.asset, address: external2.address });
81390
81876
  const secret = await decryptKey(vault, fromEntry.id);
81391
81877
  try {
@@ -81399,12 +81885,12 @@ async function fundExternal(ctx, vault, external2, input) {
81399
81885
  from: plan.from,
81400
81886
  amount: plan.amount,
81401
81887
  asset: plan.asset,
81402
- finalized: result.finalized
81888
+ finalized: result.finalized,
81889
+ ...result.finalized ? {} : { seen: result.seen }
81403
81890
  });
81404
81891
  } else {
81405
81892
  ctx.deps.stdout.write(result.finalized ? `Funded ${external2.label} (${external2.address}) with ${plan.amount} ${plan.asset}: ${result.signature}
81406
- ` : `Submitted ${result.signature}; finality is not yet confirmed.
81407
- `);
81893
+ ` : unfinalizedSendLine(result));
81408
81894
  }
81409
81895
  return result.finalized ? 0 : 3;
81410
81896
  } finally {
@@ -81501,6 +81987,7 @@ async function vaultFund(args, ctx) {
81501
81987
  }));
81502
81988
  const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan.from, plan.instructions));
81503
81989
  displayTransferPlan(ctx, plan, feeQuote);
81990
+ await assertTransferAffordable(solana, plan, feeQuote);
81504
81991
  await confirmSend(ctx, { amount: plan.amount, asset: plan.asset, address: teeAddress });
81505
81992
  const secret = await decryptKey(vault, fromEntry.id);
81506
81993
  try {
@@ -81535,12 +82022,12 @@ async function vaultFund(args, ctx) {
81535
82022
  from: plan.from,
81536
82023
  amount: plan.amount,
81537
82024
  asset: plan.asset,
81538
- finalized: result.finalized
82025
+ finalized: result.finalized,
82026
+ ...result.finalized ? {} : { seen: result.seen }
81539
82027
  });
81540
82028
  } else {
81541
82029
  ctx.deps.stdout.write(result.finalized ? `Funded ${teeAddress} with ${plan.amount} ${plan.asset}: ${result.signature}
81542
- ` : `Submitted ${result.signature}; finality is not yet confirmed. Receipt recorded.
81543
- `);
82030
+ ` : unfinalizedSendLine(result, " Receipt recorded."));
81544
82031
  }
81545
82032
  return result.finalized ? 0 : 3;
81546
82033
  } finally {
@@ -85350,8 +85837,7 @@ async function vaultStatus(args, ctx) {
85350
85837
  return usage(ctx, resolvedVault.error);
85351
85838
  const path = resolvedVault.path;
85352
85839
  const unlock = parsed.booleans.has("--unlock");
85353
- if (unlock && !requireTty(ctx, "vault status --unlock"))
85354
- return 1;
85840
+ const unlockCtx = refusePromptsWithoutTerminal(ctx, "vault status --unlock");
85355
85841
  return runVaultCommand(ctx, async ({ hold }) => {
85356
85842
  const raw = await readVaultRaw(path);
85357
85843
  if (raw === null)
@@ -85368,7 +85854,9 @@ async function vaultStatus(args, ctx) {
85368
85854
  const backup = sidecar ? describeLastBackup(sidecar, file.envelopes) : undefined;
85369
85855
  let unlocked;
85370
85856
  if (unlock) {
85371
- const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
85857
+ const vault = hold((await unlockInteractively(unlockCtx, path, raw, {
85858
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
85859
+ })).vault);
85372
85860
  unlocked = {
85373
85861
  entries: vault.index.entries.map(describeEntry),
85374
85862
  duplicateLabels: duplicateLabels(vault.index).map((duplicate) => ({
@@ -85794,6 +86282,7 @@ async function vaultTransfer(args, ctx) {
85794
86282
  }));
85795
86283
  const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan.from, plan.instructions));
85796
86284
  displayTransferPlan(ctx, plan, feeQuote);
86285
+ await assertTransferAffordable(solana, plan, feeQuote);
85797
86286
  if (promoted) {
85798
86287
  const notice = serverStateNotice(await readTeeServerState(ctx, fromEntry, apiKey));
85799
86288
  if (notice !== undefined)
@@ -85822,12 +86311,12 @@ async function vaultTransfer(args, ctx) {
85822
86311
  asset: plan.asset,
85823
86312
  amountRaw: plan.amountRaw.toString(),
85824
86313
  finalized: result.finalized,
86314
+ ...result.finalized ? {} : { seen: result.seen },
85825
86315
  ...activityReport !== undefined ? { activityReport } : {}
85826
86316
  });
85827
86317
  } else {
85828
86318
  ctx.deps.stdout.write(result.finalized ? `Transferred ${plan.amount} ${plan.asset} to ${to}: ${result.signature}
85829
- ` : `Submitted ${result.signature}; finality is not yet confirmed.
85830
- `);
86319
+ ` : unfinalizedSendLine(result));
85831
86320
  }
85832
86321
  return result.finalized ? 0 : 3;
85833
86322
  } finally {
@@ -85841,7 +86330,7 @@ init_args();
85841
86330
  init_release();
85842
86331
  import { dirname as dirname10, join as join11 } from "node:path";
85843
86332
  init_render();
85844
- var USAGE7 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
86333
+ var USAGE8 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
85845
86334
  async function resolveIdentity(deps, bundlePath, flag) {
85846
86335
  if (flag)
85847
86336
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -85873,19 +86362,19 @@ async function verify(args, ctx) {
85873
86362
  });
85874
86363
  if ("error" in parsed) {
85875
86364
  writeUsageFailure(deps, `${parsed.error}
85876
- ${USAGE7}`, json);
86365
+ ${USAGE8}`, json);
85877
86366
  return 2;
85878
86367
  }
85879
86368
  const file = parsed.positionals[0];
85880
86369
  if (parsed.positionals.length !== 1 || file === undefined) {
85881
86370
  writeUsageFailure(deps, `verify takes exactly one file.
85882
- ${USAGE7}`, json);
86371
+ ${USAGE8}`, json);
85883
86372
  return 2;
85884
86373
  }
85885
86374
  const bundlePath = parsed.values["--bundle"];
85886
86375
  if (!bundlePath) {
85887
86376
  writeUsageFailure(deps, `--bundle is required.
85888
- ${USAGE7}`, json);
86377
+ ${USAGE8}`, json);
85889
86378
  return 2;
85890
86379
  }
85891
86380
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -85899,7 +86388,7 @@ ${USAGE7}`, json);
85899
86388
  }
85900
86389
  if (resolved.kind === "absent") {
85901
86390
  writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
85902
- ${USAGE7}`, json);
86391
+ ${USAGE8}`, json);
85903
86392
  return 2;
85904
86393
  }
85905
86394
  const identity = resolved.uri;
@@ -86008,7 +86497,7 @@ function writeFailure2(ctx, result) {
86008
86497
  }
86009
86498
  async function setAllowLaunch(args, ctx, enabled) {
86010
86499
  const { deps, apiUrl, json } = ctx;
86011
- const usage6 = enabled ? USAGE_ALLOW : USAGE_DISALLOW;
86500
+ const usage7 = enabled ? USAGE_ALLOW : USAGE_DISALLOW;
86012
86501
  const command = enabled ? "wallets allow-launch" : "wallets disallow-launch";
86013
86502
  const parsed = parseArgs(args, enabled ? {} : { booleanFlags: ["--yes"] });
86014
86503
  if ("error" in parsed) {
@@ -86016,7 +86505,7 @@ async function setAllowLaunch(args, ctx, enabled) {
86016
86505
  return 2;
86017
86506
  }
86018
86507
  if (parsed.positionals.length === 0) {
86019
- writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
86508
+ writeUsageFailure(deps, `Name at least one wallet. ${usage7}`, json);
86020
86509
  return 2;
86021
86510
  }
86022
86511
  const skipConfirm = !enabled && parsed.booleans.has("--yes");
@@ -86119,8 +86608,8 @@ init_args();
86119
86608
  init_render();
86120
86609
  init_trading();
86121
86610
  init_swap();
86122
- import { randomUUID as randomUUID6 } from "node:crypto";
86123
- var USAGE8 = "Usage: candle wallet close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
86611
+ import { randomUUID as randomUUID7 } from "node:crypto";
86612
+ var USAGE9 = "Usage: candle wallet close-empty [--wallet embedded] [--keep <mint>]... [--client-trade-id <id>] [--yes] [--json]";
86124
86613
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
86125
86614
  var LISTED_ROWS = 50;
86126
86615
  function splitKeep(args) {
@@ -86195,7 +86684,7 @@ async function embeddedSolana(ctx, key, wallet) {
86195
86684
  async function walletsCloseEmpty(args, ctx) {
86196
86685
  const lifted = splitKeep(args);
86197
86686
  if ("error" in lifted) {
86198
- writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE8}`, ctx.json);
86687
+ writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE9}`, ctx.json);
86199
86688
  return 2;
86200
86689
  }
86201
86690
  const parsed = parseArgs(lifted.rest, { valueFlags: ["--wallet", "--client-trade-id"], booleanFlags: ["--yes"] });
@@ -86204,9 +86693,9 @@ async function walletsCloseEmpty(args, ctx) {
86204
86693
  return 2;
86205
86694
  }
86206
86695
  const named = parsed.values["--client-trade-id"];
86207
- const id = named ?? `close-${randomUUID6()}`;
86696
+ const id = named ?? `close-${randomUUID7()}`;
86208
86697
  if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
86209
- writeUsageFailure(ctx.deps, USAGE8, ctx.json);
86698
+ writeUsageFailure(ctx.deps, USAGE9, ctx.json);
86210
86699
  return 2;
86211
86700
  }
86212
86701
  const keep = lifted.keep;
@@ -86335,7 +86824,7 @@ function trustTable(rows) {
86335
86824
  }
86336
86825
  async function setTrust(args, ctx, trusted) {
86337
86826
  const { deps, apiUrl, json } = ctx;
86338
- const usage6 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
86827
+ const usage7 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
86339
86828
  const command = trusted ? "wallets trust" : "wallets untrust";
86340
86829
  const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
86341
86830
  if ("error" in parsed) {
@@ -86343,7 +86832,7 @@ async function setTrust(args, ctx, trusted) {
86343
86832
  return 2;
86344
86833
  }
86345
86834
  if (parsed.positionals.length === 0) {
86346
- writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
86835
+ writeUsageFailure(deps, `Name at least one wallet. ${usage7}`, json);
86347
86836
  return 2;
86348
86837
  }
86349
86838
  const skipConfirm = !trusted && parsed.booleans.has("--yes");
@@ -86630,7 +87119,8 @@ var COMMANDS = {
86630
87119
  cancel: perpsCancel,
86631
87120
  orders: perpsOrders,
86632
87121
  positions: perpsPositions,
86633
- leverage: perpsLeverage
87122
+ leverage: perpsLeverage,
87123
+ withdraw: perpsWithdraw
86634
87124
  }
86635
87125
  },
86636
87126
  auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.31",
3
+ "version": "0.11.32",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {