@candledottv/cli 0.11.30 → 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 +570 -101
  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",
@@ -12707,24 +12708,6 @@ function decodeArgs(data, types2, base = 4) {
12707
12708
  return;
12708
12709
  }
12709
12710
  }
12710
- function v2CallOk(data, value, terms) {
12711
- const selector = selectorOf(data);
12712
- if (selector === V2_SWAP_ETH_FOR_TOKENS_SELECTOR) {
12713
- const args = decodeArgs(data, ["uint", ADDRESSES, "address", "uint"]);
12714
- if (!args || terms.inputToken !== undefined)
12715
- return false;
12716
- const path = asList(args[1]);
12717
- return path.length >= 2 && sameEvmAddress(asAddress(path[0]), HOOD_WETH_ADDRESS) && terms.outputToken !== undefined && sameEvmAddress(asAddress(path[path.length - 1]), terms.outputToken) && asNumber(args[0]) >= terms.minOutRaw && sameEvmAddress(asAddress(args[2]), terms.wallet);
12718
- }
12719
- if (selector === V2_SWAP_TOKENS_FOR_ETH_SELECTOR) {
12720
- const args = decodeArgs(data, ["uint", "uint", ADDRESSES, "address", "uint"]);
12721
- if (!args || terms.inputToken === undefined || value !== 0n)
12722
- return false;
12723
- const path = asList(args[2]);
12724
- return asNumber(args[0]) <= terms.amountRaw && asNumber(args[1]) >= terms.minOutRaw && terms.outputToken === undefined && path.length >= 2 && sameEvmAddress(asAddress(path[0]), terms.inputToken) && sameEvmAddress(asAddress(path[path.length - 1]), HOOD_WETH_ADDRESS) && sameEvmAddress(asAddress(args[3]), terms.wallet);
12725
- }
12726
- return false;
12727
- }
12728
12711
  function v3SwapOf(call) {
12729
12712
  const selector = selectorOf(call);
12730
12713
  if (selector === V3_EXACT_INPUT_SINGLE_SELECTOR) {
@@ -12905,9 +12888,7 @@ function kyberCallOk(data, terms) {
12905
12888
  const amount = asNumber(desc[7]);
12906
12889
  return sameEvmAddress(asAddress(desc[0]), terms.inputToken ?? KYBER_NATIVE) && sameEvmAddress(asAddress(desc[1]), terms.outputToken ?? KYBER_NATIVE) && asNumber(desc[8]) >= terms.minOutRaw && sameEvmAddress(asAddress(desc[6]), terms.wallet) && amount === terms.amountRaw && asList(desc[3]).reduce((sum, part) => sum + asNumber(part), 0n) <= amount && asList(desc[4]).length === 0 && asList(desc[5]).every((part) => asNumber(part) === 0n) && asBytes(desc[10]).length === 0;
12907
12890
  }
12908
- function dexCallOk(to, data, value, terms) {
12909
- if (sameEvmAddress(to, HOOD_UNIV2_ROUTER))
12910
- return v2CallOk(data, value, terms);
12891
+ function dexCallOk(to, data, terms) {
12911
12892
  if (sameEvmAddress(to, HOOD_UNIV3_ROUTER))
12912
12893
  return v3CallOk(data, terms);
12913
12894
  if (sameEvmAddress(to, HOOD_V4_UNIVERSAL_ROUTER))
@@ -13052,7 +13033,7 @@ function hoodTradeLegCheck(terms) {
13052
13033
  if (terms.inputToken !== undefined ? value !== 0n : value > terms.amountRaw)
13053
13034
  return false;
13054
13035
  if (terms.venue === "dex")
13055
- return dexCallOk(leg.to, data, value, {
13036
+ return dexCallOk(leg.to, data, {
13056
13037
  wallet: terms.wallet,
13057
13038
  ...terms.inputToken !== undefined ? { inputToken: terms.inputToken } : {},
13058
13039
  amountRaw: terms.amountRaw,
@@ -13088,7 +13069,7 @@ async function registeredCurveFor(rpc, token) {
13088
13069
  }
13089
13070
  return;
13090
13071
  }
13091
- var HOOD_CURVE_FACTORY = "0xD08f8489F026957EdeD3Bb8Ef8A9d151A7B1DF22", HOOD_CURVE_REGISTRIES, HOOD_UNIV2_ROUTER = "0x89e5db8b5aa49aa85ac63f691524311aeb649eba", HOOD_UNIV3_ROUTER = "0xCaf681a66D020601342297493863E78C959E5cb2", HOOD_V4_UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904", HOOD_KYBER_ROUTER = "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5", HOOD_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3", HOOD_SPLITTER_FACTORY = "0x9318bD328Ac32DBAe08bbD85B634E6B6B06683BF", KYBER_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", ADDRESS_THIS = "0x0000000000000000000000000000000000000002", DEX_SPENDERS, CREATE_CURVE_SELECTOR = "0xe5ab9610", ERC20_APPROVE_SELECTOR = "0x095ea7b3", PERMIT2_APPROVE_SELECTOR = "0x87517c45", CURVE_BUY_SELECTOR = "0xd96a094a", CURVE_BUY_WITH_QUOTE_SELECTOR = "0x6818735c", CURVE_SELL_SELECTOR = "0xd79875eb", V2_SWAP_ETH_FOR_TOKENS_SELECTOR = "0xb6f9de95", V2_SWAP_TOKENS_FOR_ETH_SELECTOR = "0x791ac947", V3_MULTICALL_SELECTOR = "0x5ae401dc", V3_EXACT_INPUT_SINGLE_SELECTOR = "0x04e45aaf", V3_EXACT_INPUT_SELECTOR = "0xb858183f", V3_UNWRAP_WETH9_SELECTOR = "0x49404b7c", V3_REFUND_ETH_SELECTOR = "0x12210e8a", UNIVERSAL_EXECUTE_SELECTOR = "0x3593564c", KYBER_SWAP_SELECTOR = "0xe21fd0e9", KYBER_SWAP_GENERIC_SELECTOR = "0x59e50fed", KYBER_SWAP_SIMPLE_SELECTOR = "0x8af033fb", SPLITTER_DEV_SELECTOR = "0x91cca3db", SPLITTER_IS_SPLITTER_SELECTOR = "0xa4c597d1", CURVE_FOR_BASE_TOKEN_SELECTOR = "0x446b9253", SPLITTER_PLATFORM_VAULT_SELECTOR = "0x20f094f8", CURVE_CREATED_TOPIC = "0xed3a740542131b3621499825e40efbcaf096e3b1608de865c08a2109378bb36b", LAUNCH_FEE_MAX_BPS = 1000n, MAX_ABI_ARRAY = 64, ADDRESSES, V3_SINGLE, V3_PATH, KYBER_DESC, KYBER_EXECUTION, V4_POOL_KEY, V4_SWAP_PARAMS, asBytes = (value) => value, asAddress = (value) => value, asNumber = (value) => value, asList = (value) => value, routedInput = (terms) => terms.inputToken ?? HOOD_WETH_ADDRESS, routedOutput = (terms) => terms.outputToken ?? HOOD_WETH_ADDRESS, UR_V3_SWAP_EXACT_IN = 0, UR_WRAP_ETH = 11, UR_UNWRAP_WETH = 12, UR_V4_SWAP = 16, V4_SWAP_EXACT_IN_SINGLE = 6, V4_SETTLE = 11, V4_SETTLE_ALL = 12, V4_TAKE = 14, V4_TAKE_ALL = 15, NATIVE_CURRENCY = "0x0000000000000000000000000000000000000000";
13072
+ var HOOD_CURVE_FACTORY = "0xD08f8489F026957EdeD3Bb8Ef8A9d151A7B1DF22", HOOD_CURVE_REGISTRIES, HOOD_UNIV3_ROUTER = "0xCaf681a66D020601342297493863E78C959E5cb2", HOOD_V4_UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904", HOOD_KYBER_ROUTER = "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5", HOOD_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3", HOOD_SPLITTER_FACTORY = "0x9318bD328Ac32DBAe08bbD85B634E6B6B06683BF", KYBER_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", ADDRESS_THIS = "0x0000000000000000000000000000000000000002", DEX_SPENDERS, CREATE_CURVE_SELECTOR = "0xe5ab9610", ERC20_APPROVE_SELECTOR = "0x095ea7b3", PERMIT2_APPROVE_SELECTOR = "0x87517c45", CURVE_BUY_SELECTOR = "0xd96a094a", CURVE_BUY_WITH_QUOTE_SELECTOR = "0x6818735c", CURVE_SELL_SELECTOR = "0xd79875eb", V3_MULTICALL_SELECTOR = "0x5ae401dc", V3_EXACT_INPUT_SINGLE_SELECTOR = "0x04e45aaf", V3_EXACT_INPUT_SELECTOR = "0xb858183f", V3_UNWRAP_WETH9_SELECTOR = "0x49404b7c", V3_REFUND_ETH_SELECTOR = "0x12210e8a", UNIVERSAL_EXECUTE_SELECTOR = "0x3593564c", KYBER_SWAP_SELECTOR = "0xe21fd0e9", KYBER_SWAP_GENERIC_SELECTOR = "0x59e50fed", KYBER_SWAP_SIMPLE_SELECTOR = "0x8af033fb", SPLITTER_DEV_SELECTOR = "0x91cca3db", SPLITTER_IS_SPLITTER_SELECTOR = "0xa4c597d1", CURVE_FOR_BASE_TOKEN_SELECTOR = "0x446b9253", SPLITTER_PLATFORM_VAULT_SELECTOR = "0x20f094f8", CURVE_CREATED_TOPIC = "0xed3a740542131b3621499825e40efbcaf096e3b1608de865c08a2109378bb36b", LAUNCH_FEE_MAX_BPS = 1000n, MAX_ABI_ARRAY = 64, V3_SINGLE, V3_PATH, KYBER_DESC, KYBER_EXECUTION, V4_POOL_KEY, V4_SWAP_PARAMS, asBytes = (value) => value, asAddress = (value) => value, asNumber = (value) => value, asList = (value) => value, routedInput = (terms) => terms.inputToken ?? HOOD_WETH_ADDRESS, routedOutput = (terms) => terms.outputToken ?? HOOD_WETH_ADDRESS, UR_V3_SWAP_EXACT_IN = 0, UR_WRAP_ETH = 11, UR_UNWRAP_WETH = 12, UR_V4_SWAP = 16, V4_SWAP_EXACT_IN_SINGLE = 6, V4_SETTLE = 11, V4_SETTLE_ALL = 12, V4_TAKE = 14, V4_TAKE_ALL = 15, NATIVE_CURRENCY = "0x0000000000000000000000000000000000000000";
13092
13073
  var init_hood_legs = __esm(() => {
13093
13074
  init_evm_lite();
13094
13075
  HOOD_CURVE_REGISTRIES = [
@@ -13096,8 +13077,7 @@ var init_hood_legs = __esm(() => {
13096
13077
  "0x75934c16162075583435358f9F042D8D6E42Bd5C",
13097
13078
  "0xFD54D094ad93349CA316bA309A3C8573E3D6C0cd"
13098
13079
  ];
13099
- DEX_SPENDERS = [HOOD_UNIV2_ROUTER, HOOD_UNIV3_ROUTER, HOOD_KYBER_ROUTER, HOOD_PERMIT2];
13100
- ADDRESSES = { array: "address" };
13080
+ DEX_SPENDERS = [HOOD_UNIV3_ROUTER, HOOD_KYBER_ROUTER, HOOD_PERMIT2];
13101
13081
  V3_SINGLE = { tuple: ["address", "address", "uint", "address", "uint", "uint", "uint"] };
13102
13082
  V3_PATH = { tuple: ["bytes", "address", "uint", "uint"] };
13103
13083
  KYBER_DESC = {
@@ -17598,6 +17578,7 @@ function parseMintAccount(mint, owner, data) {
17598
17578
  tokenProgram: owner,
17599
17579
  token2022: owner === TOKEN_2022_PROGRAM_ID,
17600
17580
  decimals,
17581
+ associatedTokenAccountSize: owner === TOKEN_2022_PROGRAM_ID ? TLV_START + 4 : ACCOUNT_TYPE_OFFSET2,
17601
17582
  nonTransferable: false,
17602
17583
  defaultFrozen: false,
17603
17584
  paused: false,
@@ -17622,6 +17603,7 @@ function parseMintAccount(mint, owner, data) {
17622
17603
  throw new MintReadError(`${mint} has an invalid mint extension length or a duplicate extension`);
17623
17604
  }
17624
17605
  seen.add(type);
17606
+ profile.associatedTokenAccountSize += REQUIRED_ACCOUNT_TLV_SIZE.get(type) ?? 0;
17625
17607
  const requireLength = (expected) => {
17626
17608
  if (length !== expected)
17627
17609
  throw new MintReadError(`${mint} has an invalid extension ${type} length`);
@@ -17911,11 +17893,17 @@ function classifyTokenSendFailure(input) {
17911
17893
  return "TOKEN_2022_HOOK_REFUSED";
17912
17894
  return;
17913
17895
  }
17914
- 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;
17915
17897
  var init_token_2022 = __esm(() => {
17916
17898
  init_solana_lite();
17917
17899
  MintReadError = class MintReadError extends Error {
17918
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
+ ]);
17919
17907
  EXECUTE_DISCRIMINATOR = new Uint8Array([105, 37, 101, 197, 75, 251, 102, 26]);
17920
17908
  EXTRA_ACCOUNT_METAS_SEED = new TextEncoder().encode("extra-account-metas");
17921
17909
  HookResolutionError = class HookResolutionError extends Error {
@@ -19215,6 +19203,12 @@ function describeBiometry(report) {
19215
19203
  suggestion: "Unlock the Mac with its password to reset Touch ID, then retry. Nothing was written and no other factor is substituted."
19216
19204
  };
19217
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
+ }
19218
19212
  return {
19219
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}.`,
19220
19214
  suggestion: NOT_INTERACTIVE_SUGGESTION
@@ -19277,6 +19271,11 @@ function translateEnclaveFailure(code, message) {
19277
19271
  DOMAIN_NOT_ASSOCIATED: `macOS did not associate the helper with cli.candle.tv: ${message}. ${AASA_REQUIREMENT}`,
19278
19272
  NO_CREDENTIAL: `No synced passkey with this envelope's credential id is available to this Mac or this Apple account: ${message}.`
19279
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
+ }
19280
19279
  const suggestion = {
19281
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.",
19282
19281
  NOT_INTERACTIVE: NOT_INTERACTIVE_SUGGESTION,
@@ -19491,7 +19490,7 @@ async function deleteEnclaveKey(deps, session, opts) {
19491
19490
  return false;
19492
19491
  }
19493
19492
  }
19494
- 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)";
19495
19494
  var init_enclave = __esm(() => {
19496
19495
  init_sha256();
19497
19496
  init_esm();
@@ -28228,6 +28227,7 @@ __export(exports_vault_support, {
28228
28227
  requireVaultRaw: () => requireVaultRaw,
28229
28228
  requireTty: () => requireTty,
28230
28229
  requirePromptStreams: () => requirePromptStreams,
28230
+ refusePromptsWithoutTerminal: () => refusePromptsWithoutTerminal,
28231
28231
  refuseEnvPassphrase: () => refuseEnvPassphrase,
28232
28232
  openWithTypedPassphrase: () => openWithTypedPassphrase,
28233
28233
  nonDefaultVaultFooter: () => nonDefaultVaultFooter,
@@ -28282,6 +28282,16 @@ function requirePromptStreams(ctx, what) {
28282
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.`));
28283
28283
  return false;
28284
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
+ }
28285
28295
  function vaultPathFor(ctx, parsed) {
28286
28296
  const flag = parsed.values["--keystore"];
28287
28297
  if (flag !== undefined)
@@ -45705,7 +45715,7 @@ __export(exports_util, {
45705
45715
  joinValues: () => joinValues,
45706
45716
  issue: () => issue,
45707
45717
  isPlainObject: () => isPlainObject,
45708
- isObject: () => isObject,
45718
+ isObject: () => isObject2,
45709
45719
  hexToUint8Array: () => hexToUint8Array,
45710
45720
  getSizableOrigin: () => getSizableOrigin,
45711
45721
  getParsedType: () => getParsedType3,
@@ -45872,11 +45882,11 @@ function esc(str) {
45872
45882
  function slugify(input) {
45873
45883
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
45874
45884
  }
45875
- function isObject(data) {
45885
+ function isObject2(data) {
45876
45886
  return typeof data === "object" && data !== null && !Array.isArray(data);
45877
45887
  }
45878
45888
  function isPlainObject(o) {
45879
- if (isObject(o) === false)
45889
+ if (isObject2(o) === false)
45880
45890
  return false;
45881
45891
  const ctor = o.constructor;
45882
45892
  if (ctor === undefined)
@@ -45884,7 +45894,7 @@ function isPlainObject(o) {
45884
45894
  if (typeof ctor !== "function")
45885
45895
  return true;
45886
45896
  const prot = ctor.prototype;
45887
- if (isObject(prot) === false)
45897
+ if (isObject2(prot) === false)
45888
45898
  return false;
45889
45899
  if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
45890
45900
  return false;
@@ -47765,13 +47775,13 @@ var init_schemas = __esm(() => {
47765
47775
  }
47766
47776
  return propValues;
47767
47777
  });
47768
- const isObject2 = isObject;
47778
+ const isObject3 = isObject2;
47769
47779
  const catchall = def.catchall;
47770
47780
  let value;
47771
47781
  inst._zod.parse = (payload, ctx) => {
47772
47782
  value ?? (value = _normalized.value);
47773
47783
  const input = payload.value;
47774
- if (!isObject2(input)) {
47784
+ if (!isObject3(input)) {
47775
47785
  payload.issues.push({
47776
47786
  expected: "object",
47777
47787
  code: "invalid_type",
@@ -47869,7 +47879,7 @@ var init_schemas = __esm(() => {
47869
47879
  return (payload, ctx) => fn(shape, payload, ctx);
47870
47880
  };
47871
47881
  let fastpass;
47872
- const isObject2 = isObject;
47882
+ const isObject3 = isObject2;
47873
47883
  const jit = !globalConfig.jitless;
47874
47884
  const allowsEval2 = allowsEval;
47875
47885
  const fastEnabled = jit && allowsEval2.value;
@@ -47878,7 +47888,7 @@ var init_schemas = __esm(() => {
47878
47888
  inst._zod.parse = (payload, ctx) => {
47879
47889
  value ?? (value = _normalized.value);
47880
47890
  const input = payload.value;
47881
- if (!isObject2(input)) {
47891
+ if (!isObject3(input)) {
47882
47892
  payload.issues.push({
47883
47893
  expected: "object",
47884
47894
  code: "invalid_type",
@@ -47982,7 +47992,7 @@ var init_schemas = __esm(() => {
47982
47992
  });
47983
47993
  inst._zod.parse = (payload, ctx) => {
47984
47994
  const input = payload.value;
47985
- if (!isObject(input)) {
47995
+ if (!isObject2(input)) {
47986
47996
  payload.issues.push({
47987
47997
  code: "invalid_type",
47988
47998
  expected: "object",
@@ -62391,7 +62401,7 @@ var init_image = __esm(() => {
62391
62401
  });
62392
62402
 
62393
62403
  // ../mcp/src/orchestrate.ts
62394
- import { randomUUID as randomUUID7 } from "node:crypto";
62404
+ import { randomUUID as randomUUID8 } from "node:crypto";
62395
62405
  function requireApiKey(cfg) {
62396
62406
  if (!cfg.apiKey) {
62397
62407
  throw new Error("CANDLE_AGENT_API_KEY is required for this tool. Set it in the environment or MCP client config.");
@@ -62504,7 +62514,7 @@ async function readPaperInventory(cfg, doFetch, extra) {
62504
62514
  }
62505
62515
  async function executeTrade(args, cfg, doFetch) {
62506
62516
  const apiKey = requireApiKey(cfg);
62507
- const clientTradeId = args.clientTradeId ?? randomUUID7();
62517
+ const clientTradeId = args.clientTradeId ?? randomUUID8();
62508
62518
  if (args.amount !== undefined && args.percent !== undefined) {
62509
62519
  return errText("pass exactly one of amount or percent, not both", { clientTradeId });
62510
62520
  }
@@ -62642,7 +62652,7 @@ async function executeTrade(args, cfg, doFetch) {
62642
62652
  }
62643
62653
  async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
62644
62654
  const apiKey = requireApiKey(cfg);
62645
- const clientLaunchId = args.clientLaunchId ?? randomUUID7();
62655
+ const clientLaunchId = args.clientLaunchId ?? randomUUID8();
62646
62656
  const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...imageFields } = args;
62647
62657
  let buyAmount;
62648
62658
  if (devBuy !== undefined) {
@@ -63190,7 +63200,7 @@ function checkOrderMarket2(action, intent, market) {
63190
63200
  function verifyPerpsBuild2(build, opts) {
63191
63201
  try {
63192
63202
  const network2 = opts.network ?? "mainnet";
63193
- if (!EVM_ADDRESS_RE2.test(opts.builder))
63203
+ if (!EVM_ADDRESS_RE3.test(opts.builder))
63194
63204
  return { ok: false, reason: "no valid Candle builder address to check against" };
63195
63205
  if (build.network !== undefined && build.network !== network2) {
63196
63206
  return { ok: false, reason: `the build is for ${build.network}, this client trades ${network2}` };
@@ -63278,7 +63288,7 @@ function hyperliquidExchangeBody2(action, nonce, signature) {
63278
63288
  function hyperliquidRelayBody2(typedData) {
63279
63289
  return { method: "eth_signTypedData_v4", params: { typed_data: typedData } };
63280
63290
  }
63281
- 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;
63282
63292
  var init_hyperliquid = __esm(() => {
63283
63293
  init_sha3();
63284
63294
  HYPERLIQUID_ALLOWED_ACTION_TYPES2 = [
@@ -63298,7 +63308,7 @@ var init_hyperliquid = __esm(() => {
63298
63308
  };
63299
63309
  UINT64_MAX2 = (1n << 64n) - 1n;
63300
63310
  INT64_MIN2 = -(1n << 63n);
63301
- EVM_ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
63311
+ EVM_ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
63302
63312
  });
63303
63313
 
63304
63314
  // ../mcp/src/perps.ts
@@ -63785,7 +63795,7 @@ var init_plans = __esm(() => {
63785
63795
  });
63786
63796
 
63787
63797
  // ../mcp/src/tools.ts
63788
- import { randomUUID as randomUUID8 } from "node:crypto";
63798
+ import { randomUUID as randomUUID9 } from "node:crypto";
63789
63799
  function resolveToolAllowlist(env) {
63790
63800
  const raw = env.CANDLE_MCP_TOOLS?.trim();
63791
63801
  if (!raw)
@@ -64117,7 +64127,7 @@ async function getOperation(args, cfg, doFetch) {
64117
64127
  };
64118
64128
  }
64119
64129
  async function swap2(args, cfg, doFetch) {
64120
- 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();
64121
64131
  const { url, init } = buildRequest("candle_swap", { ...args, clientSwapId }, cfg);
64122
64132
  let ok;
64123
64133
  let body;
@@ -65028,7 +65038,7 @@ init_render();
65028
65038
  init_secret_store();
65029
65039
 
65030
65040
  // src/version.ts
65031
- var CLI_VERSION = "0.11.30";
65041
+ var CLI_VERSION = "0.11.32";
65032
65042
 
65033
65043
  // src/commands/auth.ts
65034
65044
  init_keys_embedded_wallet();
@@ -65459,6 +65469,10 @@ var ENVIRONMENT = [
65459
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"
65460
65470
  },
65461
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
+ },
65462
65476
  { name: "CANDLE_NO_UPDATE_NOTIFIER", description: "Set to 1 to silence the update notice" },
65463
65477
  {
65464
65478
  name: "CANDLE_KEYSTORE_PASSPHRASE",
@@ -65647,8 +65661,8 @@ var HELP = {
65647
65661
  },
65648
65662
  perps: {
65649
65663
  group: "Trade",
65650
- summary: "Hyperliquid perpetuals from your EVM TEE wallet: setup, deposit, open, close, cancel, orders, positions, leverage",
65651
- 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.",
65652
65666
  usage: ["candle perps <subcommand> [flags]"],
65653
65667
  rows: [
65654
65668
  {
@@ -65669,7 +65683,14 @@ var HELP = {
65669
65683
  invocation: "orders [--wallet <tee>]",
65670
65684
  description: "Open orders on Hyperliquid, and every action Candle built"
65671
65685
  },
65672
- { 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
+ },
65673
65694
  { invocation: "leverage <coin> <x> [--isolated]", description: "Set a market's leverage and margin mode" }
65674
65695
  ],
65675
65696
  flags: [
@@ -65687,7 +65708,8 @@ var HELP = {
65687
65708
  invocation: "--to <evm tee>",
65688
65709
  description: "deposit from Solana: the EVM TEE wallet whose account receives it"
65689
65710
  },
65690
- { 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" },
65691
65713
  { invocation: "--rpc-url <url>", description: "deposit from Solana: the RPC the deposit check reads" },
65692
65714
  { invocation: "--no-submit", description: "Build and check only; sign and submit nothing" },
65693
65715
  { invocation: "--sign-only", description: "Build, check and sign through the relay; submit nothing" },
@@ -65699,9 +65721,10 @@ var HELP = {
65699
65721
  "candle perps open BTC long 0.01",
65700
65722
  "candle perps open ETH short 0.5 --price 3200 --tp 2900 --sl 3400",
65701
65723
  "candle perps close BTC",
65702
- "candle perps leverage BTC 5 --isolated"
65724
+ "candle perps leverage BTC 5 --isolated",
65725
+ "candle perps withdraw 25 --to solana"
65703
65726
  ],
65704
- env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK"]
65727
+ env: [...ENV_API, "CANDLE_HYPERLIQUID_BUILDER", "CANDLE_HYPERLIQUID_NETWORK", "CANDLE_PERPS_WITHDRAW_PINS"]
65705
65728
  },
65706
65729
  transfer: {
65707
65730
  group: "Trade",
@@ -74279,6 +74302,13 @@ async function perpsLeverage(args, ctx) {
74279
74302
  return tradingFailure(ctx, error);
74280
74303
  }
74281
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
+ }
74282
74312
  async function perpsPositions(args, ctx) {
74283
74313
  const parsed = parseArgs(args, { valueFlags: ["--wallet"] });
74284
74314
  if ("error" in parsed)
@@ -74302,6 +74332,13 @@ async function perpsPositions(args, ctx) {
74302
74332
  for (const p of positions) {
74303
74333
  const leverage = p.leverage ?? {};
74304
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)})
74305
74342
  `);
74306
74343
  }
74307
74344
  return 0;
@@ -74487,7 +74524,7 @@ async function perpsDeposit(args, ctx) {
74487
74524
  `);
74488
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.
74489
74526
  `);
74490
- 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");
74491
74528
  if (sequenced)
74492
74529
  output.write(`Legs, signed one at a time: ${sequenced.plannedLegCount === 2 ? "approval, bridgeDeposit" : "bridgeDeposit"}
74493
74530
  `);
@@ -74541,6 +74578,382 @@ async function perpsDeposit(args, ctx) {
74541
74578
  }
74542
74579
  }
74543
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
+
74544
74957
  // src/commands/plans.ts
74545
74958
  init_args();
74546
74959
  init_client();
@@ -74615,12 +75028,12 @@ function planPromotionLine(table) {
74615
75028
 
74616
75029
  // src/commands/plans.ts
74617
75030
  init_render();
74618
- var USAGE5 = "Usage: candle plans";
75031
+ var USAGE6 = "Usage: candle plans";
74619
75032
  async function plans(args, ctx) {
74620
75033
  const parsed = parseArgs(args, {});
74621
75034
  if ("error" in parsed || parsed.positionals.length > 0) {
74622
75035
  writeUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
74623
- ${USAGE5}` : USAGE5, ctx.json);
75036
+ ${USAGE6}` : USAGE6, ctx.json);
74624
75037
  return 2;
74625
75038
  }
74626
75039
  const result = await apiRequest("/api/v1/agent/plans", {
@@ -74859,20 +75272,20 @@ async function writeSecretNames(deps, profile, names) {
74859
75272
  else
74860
75273
  await deps.updateProfile(profile, { secretNames: sorted });
74861
75274
  }
74862
- function usage5(ctx, line) {
75275
+ function usage6(ctx, line) {
74863
75276
  writeUsageFailure(ctx.deps, line, ctx.json);
74864
75277
  return 2;
74865
75278
  }
74866
75279
  async function secretsSet(args, ctx) {
74867
75280
  const parsed = parseArgs(args, {});
74868
75281
  if ("error" in parsed)
74869
- return usage5(ctx, parsed.error);
75282
+ return usage6(ctx, parsed.error);
74870
75283
  const [raw, extra] = parsed.positionals;
74871
75284
  if (!raw || extra !== undefined)
74872
- return usage5(ctx, "Usage: candle secrets set <name>");
75285
+ return usage6(ctx, "Usage: candle secrets set <name>");
74873
75286
  const name2 = canonicalSecretName(raw);
74874
75287
  if (name2 === undefined) {
74875
- 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}`);
74876
75289
  }
74877
75290
  if (!ctx.deps.isTTY.stdin || !ctx.deps.isTTY.stdout) {
74878
75291
  writeLocalFailure(ctx.deps, {
@@ -74884,7 +75297,7 @@ async function secretsSet(args, ctx) {
74884
75297
  }
74885
75298
  const value = await ctx.deps.promptSecret(`Value for ${name2} (input hidden): `);
74886
75299
  if (value.length === 0)
74887
- return usage5(ctx, "An empty value was typed; nothing was stored.");
75300
+ return usage6(ctx, "An empty value was typed; nothing was stored.");
74888
75301
  try {
74889
75302
  await ctx.deps.secretsStore.set(secretRef(ctx.profile, name2), value);
74890
75303
  } catch (error) {
@@ -74904,9 +75317,9 @@ async function secretsSet(args, ctx) {
74904
75317
  async function secretsList(args, ctx) {
74905
75318
  const parsed = parseArgs(args, {});
74906
75319
  if ("error" in parsed)
74907
- return usage5(ctx, parsed.error);
75320
+ return usage6(ctx, parsed.error);
74908
75321
  if (parsed.positionals.length > 0)
74909
- return usage5(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
75322
+ return usage6(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
74910
75323
  const names = await storedSecretNames(ctx.deps, ctx.profile);
74911
75324
  if (ctx.json) {
74912
75325
  ctx.deps.stdout.write(`${JSON.stringify({ ok: true, names, backend: ctx.deps.backend })}
@@ -74925,13 +75338,13 @@ async function secretsList(args, ctx) {
74925
75338
  async function secretsRemove(args, ctx) {
74926
75339
  const parsed = parseArgs(args, {});
74927
75340
  if ("error" in parsed)
74928
- return usage5(ctx, parsed.error);
75341
+ return usage6(ctx, parsed.error);
74929
75342
  const [raw, extra] = parsed.positionals;
74930
75343
  if (!raw || extra !== undefined)
74931
- return usage5(ctx, "Usage: candle secrets remove <name>");
75344
+ return usage6(ctx, "Usage: candle secrets remove <name>");
74932
75345
  const name2 = canonicalSecretName(raw);
74933
75346
  if (name2 === undefined) {
74934
- 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}`);
74935
75348
  }
74936
75349
  await ctx.deps.secretsStore.delete(secretRef(ctx.profile, name2));
74937
75350
  const names = await storedSecretNames(ctx.deps, ctx.profile);
@@ -77085,7 +77498,7 @@ init_args();
77085
77498
  init_render();
77086
77499
  init_trading();
77087
77500
  init_swap();
77088
- import { randomUUID as randomUUID5 } from "node:crypto";
77501
+ import { randomUUID as randomUUID6 } from "node:crypto";
77089
77502
  var withdrawal = exports_external.object({ withdrawal: exports_external.string(), amountRaw: exports_external.string(), endTime: exports_external.string(), claimable: exports_external.boolean() }).passthrough();
77090
77503
  var positionsSchema = exports_external.object({
77091
77504
  success: exports_external.literal(true),
@@ -77134,16 +77547,16 @@ async function runStake(action, args, ctx) {
77134
77547
  valueFlags: ["--wallet", "--client-stake-id"],
77135
77548
  booleanFlags: ["--yes", ...action === "claim" ? ["--all"] : []]
77136
77549
  });
77137
- 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]";
77138
77551
  if ("error" in parsed) {
77139
77552
  writeUsageFailure(ctx.deps, parsed.error, ctx.json);
77140
77553
  return 2;
77141
77554
  }
77142
77555
  const { values, positionals, booleans } = parsed;
77143
- const id = values["--client-stake-id"] ?? `stake-${randomUUID5()}`;
77556
+ const id = values["--client-stake-id"] ?? `stake-${randomUUID6()}`;
77144
77557
  const all = booleans.has("--all");
77145
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)) {
77146
- writeUsageFailure(ctx.deps, usage6, ctx.json);
77559
+ writeUsageFailure(ctx.deps, usage7, ctx.json);
77147
77560
  return 2;
77148
77561
  }
77149
77562
  try {
@@ -77925,7 +78338,7 @@ init_render();
77925
78338
  init_solana_verify();
77926
78339
  init_trading();
77927
78340
  init_swap();
77928
- 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]";
77929
78342
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
77930
78343
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
77931
78344
  async function readLinkedWallets(ctx, key) {
@@ -78011,7 +78424,7 @@ async function transfer(args, ctx) {
78011
78424
  const flags = parsed.values;
78012
78425
  const asset = flags["--asset"]?.toUpperCase();
78013
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) {
78014
- writeUsageFailure(ctx.deps, USAGE6, ctx.json);
78427
+ writeUsageFailure(ctx.deps, USAGE7, ctx.json);
78015
78428
  return 2;
78016
78429
  }
78017
78430
  const to = flags["--to"];
@@ -81246,9 +81659,11 @@ async function planTransfer(input) {
81246
81659
  const destination = ataFor(profile, toKey);
81247
81660
  const instructions = [];
81248
81661
  const accountCreationLines = [];
81662
+ let accountRent;
81249
81663
  const destinationState = await tokenAccountFrozen(rpc, encodePubkey(destination));
81250
81664
  if (!destinationState.exists) {
81251
- const rent = await rpc.getMinimumBalanceForRentExemption(165);
81665
+ const rent = await rpc.getMinimumBalanceForRentExemption(profile.associatedTokenAccountSize);
81666
+ accountRent = rent;
81252
81667
  instructions.push(createAssociatedTokenAccountIdempotent({ payer: fromKey, owner: toKey, mint, tokenProgram }));
81253
81668
  accountCreationLines.push(`create associated token account ${encodePubkey(destination)} (idempotent)`, `account owner ${input.to}`, `account rent ${rent} lamports, paid by ${input.from} if created`);
81254
81669
  }
@@ -81284,6 +81699,8 @@ async function planTransfer(input) {
81284
81699
  from: input.from,
81285
81700
  to: input.to,
81286
81701
  instructions,
81702
+ sourceTokenAccount: encodePubkey(source),
81703
+ ...accountRent !== undefined ? { accountRent } : {},
81287
81704
  token: {
81288
81705
  profile,
81289
81706
  extraAccountsMissing: !hook.ok,
@@ -81314,6 +81731,45 @@ async function quoteTransferFee(rpc, from, instructions) {
81314
81731
  });
81315
81732
  return rpc.getFeeForMessage(toBase642(message));
81316
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
+ }
81317
81773
  function displayTransferPlan(ctx, plan, feeQuote) {
81318
81774
  ctx.deps.stdout.write(`Decoded transfer (local signing only):
81319
81775
  `);
@@ -81348,8 +81804,9 @@ async function signAndBroadcastTransfer(input) {
81348
81804
  } catch (error) {
81349
81805
  if (isRateLimited(error))
81350
81806
  notePostSignatureRateLimit(input.ctx, sigB58);
81351
- return { signature: sigB58, finalized: false };
81807
+ return { signature: sigB58, finalized: false, seen: true };
81352
81808
  }
81809
+ let seen = false;
81353
81810
  for (let i = 0;i < 30; i++) {
81354
81811
  await input.ctx.deps.sleep(500);
81355
81812
  let status;
@@ -81358,17 +81815,24 @@ async function signAndBroadcastTransfer(input) {
81358
81815
  } catch (error) {
81359
81816
  if (isRateLimited(error))
81360
81817
  notePostSignatureRateLimit(input.ctx, sigB58);
81361
- return { signature: sigB58, finalized: false };
81818
+ return { signature: sigB58, finalized: false, seen: true };
81362
81819
  }
81820
+ if (status)
81821
+ seen = true;
81363
81822
  if (status?.confirmationStatus === "finalized") {
81364
81823
  if (status.err) {
81365
81824
  const named = namedSendFailure(input.plan);
81366
81825
  throw new VaultError("VAULT_WRITE_FAILED", `Transfer failed on chain${named ? ` (${named})` : ""}. Any pending funding receipt will be reconciled on a rerun.`);
81367
81826
  }
81368
- return { signature: sigB58, finalized: true };
81827
+ return { signature: sigB58, finalized: true, seen: true };
81369
81828
  }
81370
81829
  }
81371
- 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
+ `;
81372
81836
  }
81373
81837
 
81374
81838
  // src/commands/vault-fund.ts
@@ -81407,6 +81871,7 @@ async function fundExternal(ctx, vault, external2, input) {
81407
81871
  }));
81408
81872
  const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan.from, plan.instructions));
81409
81873
  displayTransferPlan(ctx, plan, feeQuote);
81874
+ await assertTransferAffordable(solana, plan, feeQuote);
81410
81875
  await confirmSend(ctx, { amount: plan.amount, asset: plan.asset, address: external2.address });
81411
81876
  const secret = await decryptKey(vault, fromEntry.id);
81412
81877
  try {
@@ -81420,12 +81885,12 @@ async function fundExternal(ctx, vault, external2, input) {
81420
81885
  from: plan.from,
81421
81886
  amount: plan.amount,
81422
81887
  asset: plan.asset,
81423
- finalized: result.finalized
81888
+ finalized: result.finalized,
81889
+ ...result.finalized ? {} : { seen: result.seen }
81424
81890
  });
81425
81891
  } else {
81426
81892
  ctx.deps.stdout.write(result.finalized ? `Funded ${external2.label} (${external2.address}) with ${plan.amount} ${plan.asset}: ${result.signature}
81427
- ` : `Submitted ${result.signature}; finality is not yet confirmed.
81428
- `);
81893
+ ` : unfinalizedSendLine(result));
81429
81894
  }
81430
81895
  return result.finalized ? 0 : 3;
81431
81896
  } finally {
@@ -81522,6 +81987,7 @@ async function vaultFund(args, ctx) {
81522
81987
  }));
81523
81988
  const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan.from, plan.instructions));
81524
81989
  displayTransferPlan(ctx, plan, feeQuote);
81990
+ await assertTransferAffordable(solana, plan, feeQuote);
81525
81991
  await confirmSend(ctx, { amount: plan.amount, asset: plan.asset, address: teeAddress });
81526
81992
  const secret = await decryptKey(vault, fromEntry.id);
81527
81993
  try {
@@ -81556,12 +82022,12 @@ async function vaultFund(args, ctx) {
81556
82022
  from: plan.from,
81557
82023
  amount: plan.amount,
81558
82024
  asset: plan.asset,
81559
- finalized: result.finalized
82025
+ finalized: result.finalized,
82026
+ ...result.finalized ? {} : { seen: result.seen }
81560
82027
  });
81561
82028
  } else {
81562
82029
  ctx.deps.stdout.write(result.finalized ? `Funded ${teeAddress} with ${plan.amount} ${plan.asset}: ${result.signature}
81563
- ` : `Submitted ${result.signature}; finality is not yet confirmed. Receipt recorded.
81564
- `);
82030
+ ` : unfinalizedSendLine(result, " Receipt recorded."));
81565
82031
  }
81566
82032
  return result.finalized ? 0 : 3;
81567
82033
  } finally {
@@ -85371,8 +85837,7 @@ async function vaultStatus(args, ctx) {
85371
85837
  return usage(ctx, resolvedVault.error);
85372
85838
  const path = resolvedVault.path;
85373
85839
  const unlock = parsed.booleans.has("--unlock");
85374
- if (unlock && !requireTty(ctx, "vault status --unlock"))
85375
- return 1;
85840
+ const unlockCtx = refusePromptsWithoutTerminal(ctx, "vault status --unlock");
85376
85841
  return runVaultCommand(ctx, async ({ hold }) => {
85377
85842
  const raw = await readVaultRaw(path);
85378
85843
  if (raw === null)
@@ -85389,7 +85854,9 @@ async function vaultStatus(args, ctx) {
85389
85854
  const backup = sidecar ? describeLastBackup(sidecar, file.envelopes) : undefined;
85390
85855
  let unlocked;
85391
85856
  if (unlock) {
85392
- 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);
85393
85860
  unlocked = {
85394
85861
  entries: vault.index.entries.map(describeEntry),
85395
85862
  duplicateLabels: duplicateLabels(vault.index).map((duplicate) => ({
@@ -85815,6 +86282,7 @@ async function vaultTransfer(args, ctx) {
85815
86282
  }));
85816
86283
  const feeQuote = await solana.read(() => quoteTransferFee(solana.rpc, plan.from, plan.instructions));
85817
86284
  displayTransferPlan(ctx, plan, feeQuote);
86285
+ await assertTransferAffordable(solana, plan, feeQuote);
85818
86286
  if (promoted) {
85819
86287
  const notice = serverStateNotice(await readTeeServerState(ctx, fromEntry, apiKey));
85820
86288
  if (notice !== undefined)
@@ -85843,12 +86311,12 @@ async function vaultTransfer(args, ctx) {
85843
86311
  asset: plan.asset,
85844
86312
  amountRaw: plan.amountRaw.toString(),
85845
86313
  finalized: result.finalized,
86314
+ ...result.finalized ? {} : { seen: result.seen },
85846
86315
  ...activityReport !== undefined ? { activityReport } : {}
85847
86316
  });
85848
86317
  } else {
85849
86318
  ctx.deps.stdout.write(result.finalized ? `Transferred ${plan.amount} ${plan.asset} to ${to}: ${result.signature}
85850
- ` : `Submitted ${result.signature}; finality is not yet confirmed.
85851
- `);
86319
+ ` : unfinalizedSendLine(result));
85852
86320
  }
85853
86321
  return result.finalized ? 0 : 3;
85854
86322
  } finally {
@@ -85862,7 +86330,7 @@ init_args();
85862
86330
  init_release();
85863
86331
  import { dirname as dirname10, join as join11 } from "node:path";
85864
86332
  init_render();
85865
- 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>]";
85866
86334
  async function resolveIdentity(deps, bundlePath, flag) {
85867
86335
  if (flag)
85868
86336
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -85894,19 +86362,19 @@ async function verify(args, ctx) {
85894
86362
  });
85895
86363
  if ("error" in parsed) {
85896
86364
  writeUsageFailure(deps, `${parsed.error}
85897
- ${USAGE7}`, json);
86365
+ ${USAGE8}`, json);
85898
86366
  return 2;
85899
86367
  }
85900
86368
  const file = parsed.positionals[0];
85901
86369
  if (parsed.positionals.length !== 1 || file === undefined) {
85902
86370
  writeUsageFailure(deps, `verify takes exactly one file.
85903
- ${USAGE7}`, json);
86371
+ ${USAGE8}`, json);
85904
86372
  return 2;
85905
86373
  }
85906
86374
  const bundlePath = parsed.values["--bundle"];
85907
86375
  if (!bundlePath) {
85908
86376
  writeUsageFailure(deps, `--bundle is required.
85909
- ${USAGE7}`, json);
86377
+ ${USAGE8}`, json);
85910
86378
  return 2;
85911
86379
  }
85912
86380
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -85920,7 +86388,7 @@ ${USAGE7}`, json);
85920
86388
  }
85921
86389
  if (resolved.kind === "absent") {
85922
86390
  writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
85923
- ${USAGE7}`, json);
86391
+ ${USAGE8}`, json);
85924
86392
  return 2;
85925
86393
  }
85926
86394
  const identity = resolved.uri;
@@ -86029,7 +86497,7 @@ function writeFailure2(ctx, result) {
86029
86497
  }
86030
86498
  async function setAllowLaunch(args, ctx, enabled) {
86031
86499
  const { deps, apiUrl, json } = ctx;
86032
- const usage6 = enabled ? USAGE_ALLOW : USAGE_DISALLOW;
86500
+ const usage7 = enabled ? USAGE_ALLOW : USAGE_DISALLOW;
86033
86501
  const command = enabled ? "wallets allow-launch" : "wallets disallow-launch";
86034
86502
  const parsed = parseArgs(args, enabled ? {} : { booleanFlags: ["--yes"] });
86035
86503
  if ("error" in parsed) {
@@ -86037,7 +86505,7 @@ async function setAllowLaunch(args, ctx, enabled) {
86037
86505
  return 2;
86038
86506
  }
86039
86507
  if (parsed.positionals.length === 0) {
86040
- writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
86508
+ writeUsageFailure(deps, `Name at least one wallet. ${usage7}`, json);
86041
86509
  return 2;
86042
86510
  }
86043
86511
  const skipConfirm = !enabled && parsed.booleans.has("--yes");
@@ -86140,8 +86608,8 @@ init_args();
86140
86608
  init_render();
86141
86609
  init_trading();
86142
86610
  init_swap();
86143
- import { randomUUID as randomUUID6 } from "node:crypto";
86144
- 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]";
86145
86613
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
86146
86614
  var LISTED_ROWS = 50;
86147
86615
  function splitKeep(args) {
@@ -86216,7 +86684,7 @@ async function embeddedSolana(ctx, key, wallet) {
86216
86684
  async function walletsCloseEmpty(args, ctx) {
86217
86685
  const lifted = splitKeep(args);
86218
86686
  if ("error" in lifted) {
86219
- writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE8}`, ctx.json);
86687
+ writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE9}`, ctx.json);
86220
86688
  return 2;
86221
86689
  }
86222
86690
  const parsed = parseArgs(lifted.rest, { valueFlags: ["--wallet", "--client-trade-id"], booleanFlags: ["--yes"] });
@@ -86225,9 +86693,9 @@ async function walletsCloseEmpty(args, ctx) {
86225
86693
  return 2;
86226
86694
  }
86227
86695
  const named = parsed.values["--client-trade-id"];
86228
- const id = named ?? `close-${randomUUID6()}`;
86696
+ const id = named ?? `close-${randomUUID7()}`;
86229
86697
  if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
86230
- writeUsageFailure(ctx.deps, USAGE8, ctx.json);
86698
+ writeUsageFailure(ctx.deps, USAGE9, ctx.json);
86231
86699
  return 2;
86232
86700
  }
86233
86701
  const keep = lifted.keep;
@@ -86356,7 +86824,7 @@ function trustTable(rows) {
86356
86824
  }
86357
86825
  async function setTrust(args, ctx, trusted) {
86358
86826
  const { deps, apiUrl, json } = ctx;
86359
- const usage6 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
86827
+ const usage7 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
86360
86828
  const command = trusted ? "wallets trust" : "wallets untrust";
86361
86829
  const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
86362
86830
  if ("error" in parsed) {
@@ -86364,7 +86832,7 @@ async function setTrust(args, ctx, trusted) {
86364
86832
  return 2;
86365
86833
  }
86366
86834
  if (parsed.positionals.length === 0) {
86367
- writeUsageFailure(deps, `Name at least one wallet. ${usage6}`, json);
86835
+ writeUsageFailure(deps, `Name at least one wallet. ${usage7}`, json);
86368
86836
  return 2;
86369
86837
  }
86370
86838
  const skipConfirm = !trusted && parsed.booleans.has("--yes");
@@ -86651,7 +87119,8 @@ var COMMANDS = {
86651
87119
  cancel: perpsCancel,
86652
87120
  orders: perpsOrders,
86653
87121
  positions: perpsPositions,
86654
- leverage: perpsLeverage
87122
+ leverage: perpsLeverage,
87123
+ withdraw: perpsWithdraw
86655
87124
  }
86656
87125
  },
86657
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.30",
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": {