@wlfi-agent/cli 1.4.17 → 1.4.19

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 (93) hide show
  1. package/Cargo.lock +5 -0
  2. package/README.md +61 -28
  3. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  4. package/crates/vault-cli-admin/src/main.rs +639 -16
  5. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  6. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  7. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  8. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  9. package/crates/vault-cli-agent/Cargo.toml +1 -0
  10. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  11. package/crates/vault-cli-agent/src/main.rs +648 -32
  12. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  13. package/crates/vault-cli-daemon/src/main.rs +617 -67
  14. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  15. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  16. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  17. package/crates/vault-daemon/src/persistence.rs +637 -100
  18. package/crates/vault-daemon/src/tests.rs +1013 -3
  19. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  20. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  21. package/crates/vault-domain/src/nonce.rs +4 -0
  22. package/crates/vault-domain/src/tests.rs +616 -0
  23. package/crates/vault-policy/src/engine.rs +55 -32
  24. package/crates/vault-policy/src/tests.rs +195 -0
  25. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  26. package/crates/vault-signer/Cargo.toml +3 -0
  27. package/crates/vault-signer/src/lib.rs +266 -40
  28. package/crates/vault-transport-unix/src/lib.rs +653 -5
  29. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  30. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  31. package/dist/cli.cjs +756 -194
  32. package/dist/cli.cjs.map +1 -1
  33. package/package.json +5 -2
  34. package/packages/cache/.turbo/turbo-build.log +20 -20
  35. package/packages/cache/coverage/clover.xml +529 -394
  36. package/packages/cache/coverage/coverage-final.json +2 -2
  37. package/packages/cache/coverage/index.html +21 -21
  38. package/packages/cache/coverage/src/client/index.html +1 -1
  39. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  40. package/packages/cache/coverage/src/errors/index.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  42. package/packages/cache/coverage/src/index.html +1 -1
  43. package/packages/cache/coverage/src/index.ts.html +1 -1
  44. package/packages/cache/coverage/src/service/index.html +21 -21
  45. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  46. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  47. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  48. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  49. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  50. package/packages/cache/dist/index.cjs +2 -2
  51. package/packages/cache/dist/index.js +1 -1
  52. package/packages/cache/dist/service/index.cjs +2 -2
  53. package/packages/cache/dist/service/index.js +1 -1
  54. package/packages/cache/node_modules/.bin/tsc +2 -2
  55. package/packages/cache/node_modules/.bin/tsserver +2 -2
  56. package/packages/cache/node_modules/.bin/tsup +2 -2
  57. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  58. package/packages/cache/node_modules/.bin/vitest +4 -4
  59. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  60. package/packages/cache/src/service/index.test.ts +165 -19
  61. package/packages/cache/src/service/index.ts +38 -1
  62. package/packages/config/.turbo/turbo-build.log +4 -4
  63. package/packages/config/dist/index.cjs +0 -17
  64. package/packages/config/dist/index.cjs.map +1 -1
  65. package/packages/config/src/index.ts +0 -17
  66. package/packages/rpc/.turbo/turbo-build.log +11 -11
  67. package/packages/rpc/dist/index.cjs +0 -17
  68. package/packages/rpc/dist/index.cjs.map +1 -1
  69. package/packages/rpc/src/index.js +1 -0
  70. package/packages/ui/node_modules/.bin/tsc +2 -2
  71. package/packages/ui/node_modules/.bin/tsserver +2 -2
  72. package/packages/ui/node_modules/.bin/tsup +2 -2
  73. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  74. package/scripts/install-cli-launcher.mjs +37 -0
  75. package/scripts/install-rust-binaries.mjs +47 -0
  76. package/scripts/run-tests-isolated.mjs +210 -0
  77. package/src/cli.ts +310 -50
  78. package/src/lib/admin-reset.ts +101 -33
  79. package/src/lib/admin-setup.ts +285 -55
  80. package/src/lib/agent-auth-migrate.ts +5 -1
  81. package/src/lib/asset-broadcast.ts +15 -4
  82. package/src/lib/config-amounts.ts +6 -4
  83. package/src/lib/hidden-tty-prompt.js +1 -0
  84. package/src/lib/hidden-tty-prompt.ts +105 -0
  85. package/src/lib/keychain.ts +1 -0
  86. package/src/lib/local-admin-access.ts +4 -29
  87. package/src/lib/rust.ts +129 -33
  88. package/src/lib/signed-tx.ts +1 -0
  89. package/src/lib/sudo.ts +15 -5
  90. package/src/lib/wallet-profile.ts +3 -0
  91. package/src/lib/wallet-setup.ts +52 -0
  92. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  93. package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
package/dist/cli.cjs CHANGED
@@ -12981,6 +12981,9 @@ var require_commander = __commonJS({
12981
12981
  }
12982
12982
  });
12983
12983
 
12984
+ // src/cli.ts
12985
+ var import_promises2 = require("timers/promises");
12986
+
12984
12987
  // packages/config/src/index.js
12985
12988
  var src_exports = {};
12986
12989
  __export(src_exports, {
@@ -13069,23 +13072,6 @@ var BUILTIN_TOKENS = {
13069
13072
  arbitrum: { chainId: 42161, isNative: true, decimals: 18 }
13070
13073
  }
13071
13074
  },
13072
- usdc: {
13073
- symbol: "USDC",
13074
- chains: {
13075
- ethereum: {
13076
- chainId: 1,
13077
- isNative: false,
13078
- address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
13079
- decimals: 6
13080
- },
13081
- base: {
13082
- chainId: 8453,
13083
- isNative: false,
13084
- address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
13085
- decimals: 6
13086
- }
13087
- }
13088
- },
13089
13075
  usd1: {
13090
13076
  name: "USD1",
13091
13077
  symbol: "USD1",
@@ -14019,7 +14005,7 @@ function resolveRustBinaryPath(binaryName, config = readConfig()) {
14019
14005
  return import_node_path.default.join(config.rustBinDir ?? defaultRustBinDir(), binaryName + (process.platform === "win32" ? ".exe" : ""));
14020
14006
  }
14021
14007
 
14022
- // packages/rpc/src/index.ts
14008
+ // packages/rpc/src/index.js
14023
14009
  var src_exports2 = {};
14024
14010
  __export(src_exports2, {
14025
14011
  TRANSFER_EVENT: () => TRANSFER_EVENT,
@@ -23709,6 +23695,78 @@ function resolveLaunchDaemonHelperScriptPath(scriptName, config) {
23709
23695
  return candidates[0];
23710
23696
  }
23711
23697
 
23698
+ // src/lib/hidden-tty-prompt.ts
23699
+ async function promptHiddenTty(query, nonInteractiveError, deps = {}) {
23700
+ const input = deps.input ?? process.stdin;
23701
+ const output = deps.output ?? process.stderr;
23702
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
23703
+ throw new Error(nonInteractiveError);
23704
+ }
23705
+ output.write("\r\x1B[2K");
23706
+ output.write(query);
23707
+ const wasRaw = Boolean(input.isRaw);
23708
+ if (!wasRaw) {
23709
+ input.setRawMode(true);
23710
+ }
23711
+ input.resume?.();
23712
+ return await new Promise((resolve, reject) => {
23713
+ let answer = "";
23714
+ let settled = false;
23715
+ const cleanup = () => {
23716
+ input.removeListener("data", onData);
23717
+ input.removeListener("error", onError);
23718
+ if (!wasRaw) {
23719
+ input.setRawMode?.(false);
23720
+ }
23721
+ input.pause?.();
23722
+ output.write("\n");
23723
+ };
23724
+ const finish = (callback) => {
23725
+ if (settled) {
23726
+ return;
23727
+ }
23728
+ settled = true;
23729
+ cleanup();
23730
+ callback();
23731
+ };
23732
+ const onError = (error) => {
23733
+ finish(() => reject(error));
23734
+ };
23735
+ const onData = (chunk) => {
23736
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
23737
+ if (text.includes("\x1B")) {
23738
+ return;
23739
+ }
23740
+ for (const char of text) {
23741
+ if (char === "\r" || char === "\n") {
23742
+ finish(() => resolve(answer));
23743
+ return;
23744
+ }
23745
+ if (char === "") {
23746
+ finish(() => reject(new Error("prompt canceled")));
23747
+ return;
23748
+ }
23749
+ if (char === "\x7F" || char === "\b") {
23750
+ const chars = Array.from(answer);
23751
+ chars.pop();
23752
+ answer = chars.join("");
23753
+ continue;
23754
+ }
23755
+ if (char === "") {
23756
+ answer = "";
23757
+ continue;
23758
+ }
23759
+ if (/[\u0000-\u001f\u007f]/u.test(char)) {
23760
+ continue;
23761
+ }
23762
+ answer += char;
23763
+ }
23764
+ };
23765
+ input.on("data", onData);
23766
+ input.on("error", onError);
23767
+ });
23768
+ }
23769
+
23712
23770
  // src/lib/sudo.ts
23713
23771
  var import_node_child_process2 = require("child_process");
23714
23772
  function currentProcessIsRoot() {
@@ -23717,6 +23775,14 @@ function currentProcessIsRoot() {
23717
23775
  function isSudoAuthenticationFailure(output) {
23718
23776
  return /sudo: .*password is required/iu.test(output) || /sudo: .*incorrect password/iu.test(output) || /sudo: no password was provided/iu.test(output) || /sorry, try again\./iu.test(output);
23719
23777
  }
23778
+ function formatSudoFailureMessage(result) {
23779
+ const combinedOutput = `${result.stderr}
23780
+ ${result.stdout}`;
23781
+ if (isSudoAuthenticationFailure(combinedOutput)) {
23782
+ return "sudo authentication failed. Enter your system admin password used for sudo, not the WLFI vault password.";
23783
+ }
23784
+ return result.stderr.trim() || result.stdout.trim() || `sudo credential check failed (exit code ${result.code})`;
23785
+ }
23720
23786
  async function runCommand(command, args, options, deps) {
23721
23787
  return await new Promise((resolve, reject) => {
23722
23788
  const spawnOptions = {
@@ -23773,9 +23839,7 @@ function createSudoSession(deps) {
23773
23839
  }
23774
23840
  );
23775
23841
  if (result.code !== 0) {
23776
- throw new Error(
23777
- result.stderr.trim() || result.stdout.trim() || `sudo credential check failed (exit code ${result.code})`
23778
- );
23842
+ throw new Error(formatSudoFailureMessage(result));
23779
23843
  }
23780
23844
  primed = true;
23781
23845
  }
@@ -23853,29 +23917,7 @@ function validateSecret(value, label) {
23853
23917
  return value;
23854
23918
  }
23855
23919
  async function promptHidden(query, label) {
23856
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
23857
- throw new Error(`${label} is required; rerun on a local TTY`);
23858
- }
23859
- const rl = import_node_readline.default.createInterface({
23860
- input: process.stdin,
23861
- output: process.stdout,
23862
- terminal: true
23863
- });
23864
- rl.stdoutMuted = true;
23865
- rl._writeToOutput = (value) => {
23866
- if (value.includes(query)) {
23867
- rl.output.write(value);
23868
- return;
23869
- }
23870
- if (!rl.stdoutMuted) {
23871
- rl.output.write(value);
23872
- }
23873
- };
23874
- const answer = await new Promise((resolve) => {
23875
- rl.question(query, resolve);
23876
- });
23877
- rl.close();
23878
- process.stdout.write("\n");
23920
+ const answer = await promptHiddenTty(query, `${label} is required; rerun on a local TTY`);
23879
23921
  return validateSecret(answer, label);
23880
23922
  }
23881
23923
  async function promptVisible(query) {
@@ -23895,10 +23937,21 @@ async function promptVisible(query) {
23895
23937
  }
23896
23938
  var sudoSession = createSudoSession({
23897
23939
  promptPassword: async () => await promptHidden(
23898
- "Root password (input hidden; required to uninstall the root daemon and delete its state): ",
23899
- "root password"
23940
+ "macOS admin password for sudo (input hidden; required to uninstall the root daemon and delete its state): ",
23941
+ "macOS admin password for sudo"
23900
23942
  )
23901
23943
  });
23944
+ function isSudoWrappedInvocation() {
23945
+ return typeof process.geteuid === "function" && process.geteuid() === 0 && typeof process.env.SUDO_UID === "string" && process.env.SUDO_UID.trim().length > 0;
23946
+ }
23947
+ function assertNotInvokedViaSudo(commandName) {
23948
+ if (!isSudoWrappedInvocation()) {
23949
+ return;
23950
+ }
23951
+ throw new Error(
23952
+ `run \`wlfi-agent ${commandName}\` as your normal macOS user, not with sudo; the CLI prompts for sudo internally and running it as root can target the wrong local WLFI home`
23953
+ );
23954
+ }
23902
23955
  function createProgress(message, enabled = true) {
23903
23956
  if (!enabled) {
23904
23957
  return {
@@ -23956,6 +24009,43 @@ function print(payload, asJson) {
23956
24009
  }
23957
24010
  console.dir(payload, { depth: null, colors: process.stdout.isTTY });
23958
24011
  }
24012
+ async function managedPathExists(targetPath) {
24013
+ const result = await sudoSession.run(["/bin/test", "-e", targetPath]);
24014
+ if (result.code === 0) {
24015
+ return true;
24016
+ }
24017
+ if (result.code === 1 && !/password is required|try again|authentication failed|sorry/iu.test(result.stderr)) {
24018
+ return false;
24019
+ }
24020
+ throw new Error(
24021
+ result.stderr.trim() || result.stdout.trim() || `failed to inspect managed path '${targetPath}' (exit code ${result.code})`
24022
+ );
24023
+ }
24024
+ async function assertManagedUninstallArtifactsRemoved(targetPaths) {
24025
+ const remaining = [];
24026
+ for (const targetPath of targetPaths) {
24027
+ if (await managedPathExists(targetPath)) {
24028
+ remaining.push(targetPath);
24029
+ }
24030
+ }
24031
+ if (remaining.length > 0) {
24032
+ throw new Error(
24033
+ `admin uninstall left managed root-owned files behind: ${remaining.join(", ")}`
24034
+ );
24035
+ }
24036
+ }
24037
+ function assertLocalUninstallArtifactsRemoved(result) {
24038
+ const remaining = [];
24039
+ if (result.config.existed && import_node_fs6.default.existsSync(result.config.path)) {
24040
+ remaining.push(result.config.path);
24041
+ }
24042
+ if (result.wlfiHome.existed && import_node_fs6.default.existsSync(result.wlfiHome.path)) {
24043
+ remaining.push(result.wlfiHome.path);
24044
+ }
24045
+ if (remaining.length > 0) {
24046
+ throw new Error(`admin uninstall left local WLFI files behind: ${remaining.join(", ")}`);
24047
+ }
24048
+ }
23959
24049
  function printResetSummary(result) {
23960
24050
  const lines = [
23961
24051
  "reset complete",
@@ -24150,12 +24240,13 @@ async function confirmReset(options) {
24150
24240
  }
24151
24241
  }
24152
24242
  async function runAdminReset(options) {
24243
+ assertNotInvokedViaSudo("admin reset");
24153
24244
  await confirmReset(options);
24154
24245
  const showProgress = !options.json;
24155
24246
  const keychainAccount = import_node_os2.default.userInfo().username;
24156
24247
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
24157
24248
  process.stderr.write(
24158
- "Root password required: reset must uninstall the root LaunchDaemon and delete the root-managed daemon state.\n"
24249
+ "macOS admin password required: reset uses sudo to uninstall the root LaunchDaemon and delete the root-managed daemon state.\n"
24159
24250
  );
24160
24251
  }
24161
24252
  await sudoSession.prime();
@@ -24261,19 +24352,22 @@ function printUninstallSummary(result) {
24261
24352
  `managed state directory removed: ${result.daemon.stateDir}`,
24262
24353
  `managed log directory removed: ${result.daemon.logDir}`,
24263
24354
  result.local.agentKeyId ? `old agent key cleared: ${result.local.agentKeyId}` : "old agent key cleared: no configured agent key was found",
24355
+ /* c8 ignore start -- public uninstall summary only reaches here when a WLFI home existed or config provided the helper paths */
24264
24356
  result.local.wlfiHome.existed ? `local WLFI home removed: ${result.local.wlfiHome.path}` : `local WLFI home not found: ${result.local.wlfiHome.path}`,
24357
+ /* c8 ignore stop */
24265
24358
  result.local.config.existed ? `config removed: ${result.local.config.path}` : `config not found: ${result.local.config.path}`,
24266
24359
  "next: reinstall with `wlfi-agent admin setup` only if you want a fresh managed wallet again"
24267
24360
  ];
24268
24361
  console.log(lines.join("\n"));
24269
24362
  }
24270
24363
  async function runAdminUninstall(options) {
24364
+ assertNotInvokedViaSudo("admin uninstall");
24271
24365
  await confirmUninstall(options);
24272
24366
  const showProgress = !options.json;
24273
24367
  const keychainAccount = import_node_os2.default.userInfo().username;
24274
24368
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
24275
24369
  process.stderr.write(
24276
- "Root password required: uninstall must remove the root LaunchDaemon and all managed root-owned files.\n"
24370
+ "macOS admin password required: uninstall uses sudo to remove the root LaunchDaemon and all managed root-owned files.\n"
24277
24371
  );
24278
24372
  }
24279
24373
  await sudoSession.prime();
@@ -24322,10 +24416,28 @@ async function runAdminUninstall(options) {
24322
24416
  deleteRootArtifactsResult.stderr.trim() || deleteRootArtifactsResult.stdout.trim() || `failed to delete managed root-owned files (exit code ${deleteRootArtifactsResult.code})`
24323
24417
  );
24324
24418
  }
24325
- rootProgress.succeed("Managed root-owned files removed");
24419
+ try {
24420
+ await assertManagedUninstallArtifactsRemoved([
24421
+ DEFAULT_LAUNCH_DAEMON_PLIST,
24422
+ DEFAULT_MANAGED_ROOT_DIR,
24423
+ DEFAULT_MANAGED_STATE_DIR,
24424
+ DEFAULT_MANAGED_LOG_DIR
24425
+ ]);
24426
+ rootProgress.succeed("Managed root-owned files removed");
24427
+ } catch (error) {
24428
+ rootProgress.fail();
24429
+ throw error;
24430
+ }
24326
24431
  const localProgress = createProgress("Removing local WLFI files and credentials", showProgress);
24327
- const local = cleanupLocalAdminUninstallState();
24328
- localProgress.succeed("Local WLFI files and credentials removed");
24432
+ let local;
24433
+ try {
24434
+ local = cleanupLocalAdminUninstallState();
24435
+ assertLocalUninstallArtifactsRemoved(local);
24436
+ localProgress.succeed("Local WLFI files and credentials removed");
24437
+ } catch (error) {
24438
+ localProgress.fail();
24439
+ throw error;
24440
+ }
24329
24441
  const result = {
24330
24442
  command: "uninstall",
24331
24443
  daemon: {
@@ -24935,6 +25047,7 @@ async function prepareSpawnOptions(binaryName, args, options) {
24935
25047
 
24936
25048
  // src/lib/rust.ts
24937
25049
  var { readConfig: readConfig2, resolveRustBinaryPath: resolveRustBinaryPath2 } = src_exports;
25050
+ var MAX_STDOUT_SNIPPET_CHARS = 200;
24938
25051
  var RustBinaryExitError = class extends Error {
24939
25052
  binaryName;
24940
25053
  code;
@@ -24949,6 +25062,20 @@ var RustBinaryExitError = class extends Error {
24949
25062
  this.stderr = stderr;
24950
25063
  }
24951
25064
  };
25065
+ var RustBinaryJsonParseError = class extends Error {
25066
+ binaryName;
25067
+ stdout;
25068
+ cause;
25069
+ constructor(binaryName, stdout, cause) {
25070
+ const snippet = stdout.length > MAX_STDOUT_SNIPPET_CHARS ? `${stdout.slice(0, MAX_STDOUT_SNIPPET_CHARS)}...` : stdout;
25071
+ const message = cause instanceof Error ? `${binaryName} produced invalid JSON: ${cause.message}. stdout: ${snippet}` : `${binaryName} produced invalid JSON. stdout: ${snippet}`;
25072
+ super(message);
25073
+ this.name = "RustBinaryJsonParseError";
25074
+ this.binaryName = binaryName;
25075
+ this.stdout = stdout;
25076
+ this.cause = cause;
25077
+ }
25078
+ };
24952
25079
  function ensureBinary(binaryName, config) {
24953
25080
  const resolved = resolveRustBinaryPath2(binaryName, config ?? readConfig2());
24954
25081
  if (!import_node_fs7.default.existsSync(resolved)) {
@@ -24964,6 +25091,37 @@ function forwardedArgsIncludeDaemonSocket(args) {
24964
25091
  (arg, _index) => arg === "--daemon-socket" || arg.startsWith("--daemon-socket=")
24965
25092
  );
24966
25093
  }
25094
+ async function writeChildStdin(child, stdin) {
25095
+ const stream = child.stdin;
25096
+ if (!stream) {
25097
+ return;
25098
+ }
25099
+ await new Promise((resolve, reject) => {
25100
+ let settled = false;
25101
+ const finish = () => {
25102
+ if (settled) {
25103
+ return;
25104
+ }
25105
+ settled = true;
25106
+ stream.off("error", handleError);
25107
+ resolve();
25108
+ };
25109
+ const handleError = (error) => {
25110
+ if (settled) {
25111
+ return;
25112
+ }
25113
+ if (error?.code === "EPIPE" || error?.code === "ERR_STREAM_DESTROYED") {
25114
+ finish();
25115
+ return;
25116
+ }
25117
+ settled = true;
25118
+ stream.off("error", handleError);
25119
+ reject(error);
25120
+ };
25121
+ stream.on("error", handleError);
25122
+ stream.end(stdin, finish);
25123
+ });
25124
+ }
24967
25125
  async function passthroughRustBinary(binaryName, args, config) {
24968
25126
  const resolvedConfig = config ?? readConfig2();
24969
25127
  const resolvedDaemonSocket = resolveValidatedPassthroughDaemonSocket(
@@ -24974,20 +25132,25 @@ async function passthroughRustBinary(binaryName, args, config) {
24974
25132
  const executable = ensureBinary(binaryName, resolvedConfig);
24975
25133
  const prepared = await prepareSpawnOptions(binaryName, args, {});
24976
25134
  const env = { ...prepared.env };
24977
- if (resolvedDaemonSocket && !forwardedArgsIncludeDaemonSocket(args) && !env.WLFI_DAEMON_SOCKET?.trim()) {
25135
+ if (resolvedDaemonSocket && !forwardedArgsIncludeDaemonSocket(args) && /* c8 ignore next -- explicit env override is exercised, but c8 misattributes this optional-chain/nullish check */
25136
+ !env.WLFI_DAEMON_SOCKET?.trim()) {
24978
25137
  env.WLFI_DAEMON_SOCKET = resolvedDaemonSocket;
24979
25138
  }
24980
25139
  const child = (0, import_node_child_process3.spawn)(executable, prepared.args, {
24981
- stdio: [prepared.stdin !== void 0 ? "pipe" : "inherit", "inherit", "inherit"],
25140
+ stdio: [
25141
+ prepared.stdin !== void 0 ? "pipe" : "inherit",
25142
+ "inherit",
25143
+ "inherit"
25144
+ ],
24982
25145
  env
24983
25146
  });
24984
- if (prepared.stdin !== void 0) {
24985
- child.stdin?.end(prepared.stdin);
24986
- }
24987
- return await new Promise((resolve, reject) => {
25147
+ const codePromise = new Promise((resolve, reject) => {
24988
25148
  child.on("error", reject);
24989
- child.on("close", (code) => resolve(code ?? 1));
25149
+ child.on("close", (code2) => resolve(code2 ?? 1));
24990
25150
  });
25151
+ const stdinPromise = prepared.stdin !== void 0 ? writeChildStdin(child, prepared.stdin) : Promise.resolve();
25152
+ const [code] = await Promise.all([codePromise, stdinPromise]);
25153
+ return code;
24991
25154
  }
24992
25155
  async function runRustBinary(binaryName, args, config, options = {}) {
24993
25156
  const resolvedConfig = config ?? readConfig2();
@@ -25014,13 +25177,20 @@ async function runRustBinary(binaryName, args, config, options = {}) {
25014
25177
  child.stderr?.on("data", (chunk) => {
25015
25178
  stderr += chunk.toString();
25016
25179
  });
25017
- if (prepared.stdin !== void 0) {
25018
- child.stdin?.end(prepared.stdin);
25019
- }
25020
- const code = await new Promise((resolve, reject) => {
25180
+ const codePromise = new Promise((resolve, reject) => {
25021
25181
  child.on("error", reject);
25022
- child.on("close", (value) => resolve(value ?? 1));
25182
+ child.on("close", (code2, signal) => {
25183
+ if (code2 !== null && code2 !== void 0) {
25184
+ resolve(code2);
25185
+ } else {
25186
+ resolve(
25187
+ signal ? 128 + (require("os").constants.errno?.SIGTERM ?? 143) : 1
25188
+ );
25189
+ }
25190
+ });
25023
25191
  });
25192
+ const stdinPromise = prepared.stdin !== void 0 ? writeChildStdin(child, prepared.stdin) : Promise.resolve();
25193
+ const [code] = await Promise.all([codePromise, stdinPromise]);
25024
25194
  if (code !== 0) {
25025
25195
  throw new RustBinaryExitError(binaryName, code, stdout, stderr);
25026
25196
  }
@@ -25028,7 +25198,11 @@ async function runRustBinary(binaryName, args, config, options = {}) {
25028
25198
  }
25029
25199
  async function runRustBinaryJson(binaryName, args, config, options = {}) {
25030
25200
  const { stdout } = await runRustBinary(binaryName, args, config, options);
25031
- return JSON.parse(stdout);
25201
+ try {
25202
+ return JSON.parse(stdout);
25203
+ } catch (error) {
25204
+ throw new RustBinaryJsonParseError(binaryName, stdout, error);
25205
+ }
25032
25206
  }
25033
25207
 
25034
25208
  // src/lib/wallet-profile.ts
@@ -25126,6 +25300,7 @@ function collectWalletBalanceTargets(config) {
25126
25300
  symbol: tokenProfile.symbol,
25127
25301
  name: tokenProfile.name,
25128
25302
  chainKey,
25303
+ /* c8 ignore next -- resolveChainProfile only yields usable balance targets when a chain name is available */
25129
25304
  chainName: resolvedChain?.name ?? chainKey,
25130
25305
  chainId: chainProfile.chainId,
25131
25306
  rpcUrl,
@@ -25623,6 +25798,22 @@ function resolveValidatedWalletSetupPolicyIds(values) {
25623
25798
  normalizedList(values).map((value) => assertWalletSetupUuid(value, "attachPolicyId"))
25624
25799
  );
25625
25800
  }
25801
+ function resolveValidatedExistingVaultReuse(input) {
25802
+ const keyId = presentString4(input.existingVaultKeyId);
25803
+ const publicKey = presentString4(input.existingVaultPublicKey);
25804
+ if (!keyId && !publicKey) {
25805
+ return {};
25806
+ }
25807
+ if (!keyId || !publicKey) {
25808
+ throw new Error(
25809
+ "existingVaultKeyId and existingVaultPublicKey must be provided together to reuse an existing wallet"
25810
+ );
25811
+ }
25812
+ return {
25813
+ keyId: assertWalletSetupUuid(keyId, "existingVaultKeyId"),
25814
+ publicKey
25815
+ };
25816
+ }
25626
25817
  function previewBootstrapOutputPath(inputPath) {
25627
25818
  const explicitPath = presentString4(inputPath);
25628
25819
  if (explicitPath) {
@@ -26040,7 +26231,9 @@ function formatBooleanPlanValue(value) {
26040
26231
  }
26041
26232
  function formatWalletSetupScope(plan) {
26042
26233
  return [
26234
+ /* c8 ignore next -- both default-network and explicit-network renderings are exercised, but c8 misattributes this ternary under --experimental-strip-types */
26043
26235
  `- Network: ${plan.policyScope.network === null ? "daemon default" : String(plan.policyScope.network)}`,
26236
+ /* c8 ignore next -- both default and explicit chain-name renderings are exercised, but c8 misattributes this nullish expression */
26044
26237
  `- Chain Name: ${plan.policyScope.chainName ?? "daemon default"}`,
26045
26238
  `- Recipient: ${plan.policyScope.recipient ?? "all recipients"}`,
26046
26239
  `- Asset Mode: ${plan.policyScope.assets.mode}`,
@@ -26066,6 +26259,7 @@ function formatWalletSetupPreflight(plan) {
26066
26259
  return [
26067
26260
  `- Daemon Socket Trusted: ${formatBooleanPlanValue(plan.preflight.daemonSocketTrusted)}`,
26068
26261
  plan.preflight.daemonSocketError ? ` ${plan.preflight.daemonSocketError}` : null,
26262
+ /* c8 ignore next -- both null and boolean RPC preflight states are exercised, but c8 misattributes this ternary */
26069
26263
  `- RPC URL Trusted: ${plan.preflight.rpcUrlTrusted === null ? "not applicable" : formatBooleanPlanValue(plan.preflight.rpcUrlTrusted)}`,
26070
26264
  plan.preflight.rpcUrlError ? ` ${plan.preflight.rpcUrlError}` : null,
26071
26265
  `- Bootstrap Output Ready: ${formatBooleanPlanValue(plan.preflight.bootstrapOutputReady)}`,
@@ -26076,6 +26270,7 @@ function formatWalletSetupPlanText(plan) {
26076
26270
  const bootstrapOutputLabel = `${plan.bootstrapOutput.path} (${plan.bootstrapOutput.autoGenerated ? "auto-generated" : "explicit"}, ${plan.bootstrapOutput.cleanupAction} after import)`;
26077
26271
  const lines = [
26078
26272
  "Wallet Setup Preview",
26273
+ /* c8 ignore next -- allowed and blocked admin access renderings are both exercised, but c8 misattributes this ternary */
26079
26274
  `Admin Access: ${plan.adminAccess.permitted ? "allowed" : "blocked"} (${plan.adminAccess.mode})`,
26080
26275
  `Admin Access Reason: ${plan.adminAccess.reason}`,
26081
26276
  `Daemon Socket: ${plan.daemonSocket}`,
@@ -26098,8 +26293,11 @@ function formatWalletSetupPlanText(plan) {
26098
26293
  "Config After Setup",
26099
26294
  `- Agent Key ID: ${plan.configAfterSetup.agentKeyId}`,
26100
26295
  `- Daemon Socket: ${plan.configAfterSetup.daemonSocket}`,
26296
+ /* c8 ignore next -- unchanged and explicit chain-id renderings are both exercised, but c8 misattributes this ternary */
26101
26297
  `- Chain ID: ${plan.configAfterSetup.chainId === null ? "unchanged" : String(plan.configAfterSetup.chainId)}`,
26298
+ /* c8 ignore next -- unchanged and explicit chain-name renderings are both exercised, but c8 misattributes this nullish expression */
26102
26299
  `- Chain Name: ${plan.configAfterSetup.chainName ?? "unchanged"}`,
26300
+ /* c8 ignore next -- unchanged and explicit RPC URL renderings are both exercised, but c8 misattributes this nullish expression */
26103
26301
  `- RPC URL: ${plan.configAfterSetup.rpcUrl ?? "unchanged"}`,
26104
26302
  "",
26105
26303
  "Preflight",
@@ -26146,6 +26344,7 @@ function buildWalletSetupAdminArgs(input) {
26146
26344
  const tokens = resolveValidatedWalletSetupTokenList(input.token);
26147
26345
  const policyIds = resolveValidatedWalletSetupPolicyIds(input.attachPolicyId);
26148
26346
  const fromSharedConfig = shouldBootstrapFromSharedConfig(input);
26347
+ const existingVaultReuse = resolveValidatedExistingVaultReuse(input);
26149
26348
  if (input.vaultPassword) {
26150
26349
  throw new Error(
26151
26350
  "insecure vaultPassword is disabled; use vaultPasswordStdin or an interactive prompt"
@@ -26164,6 +26363,14 @@ function buildWalletSetupAdminArgs(input) {
26164
26363
  if (fromSharedConfig) {
26165
26364
  args.push("--from-shared-config");
26166
26365
  }
26366
+ if (existingVaultReuse.keyId && existingVaultReuse.publicKey) {
26367
+ args.push(
26368
+ "--existing-vault-key-id",
26369
+ existingVaultReuse.keyId,
26370
+ "--existing-vault-public-key",
26371
+ existingVaultReuse.publicKey
26372
+ );
26373
+ }
26167
26374
  const appendValue = (flag, value) => {
26168
26375
  if (value) {
26169
26376
  args.push(flag, value);
@@ -26244,7 +26451,8 @@ function completeWalletSetup(options, deps = {}) {
26244
26451
  }
26245
26452
  if (options.network !== void 0) {
26246
26453
  nextConfig.chainId = assertPositiveChainId(options.network);
26247
- nextConfig.chainName = normalizedChainName ?? configuredChainName(nextConfig.chainId, currentConfig) ?? `chain-${nextConfig.chainId}`;
26454
+ nextConfig.chainName = normalizedChainName ?? /* c8 ignore next -- configured-chain and synthetic fallback label paths are exercised, but c8 misattributes this nullish expression */
26455
+ configuredChainName(nextConfig.chainId, currentConfig) ?? `chain-${nextConfig.chainId}`;
26248
26456
  nextConfig.rpcUrl = normalizedRpcUrl ?? configuredRpcUrl(nextConfig.chainId, currentConfig);
26249
26457
  }
26250
26458
  storeAgentAuthToken(credentials.agentKeyId, credentials.agentAuthToken);
@@ -26302,31 +26510,9 @@ async function readTrimmedStdin(label) {
26302
26510
  }
26303
26511
  return validateSecret4(raw.replace(/[\r\n]+$/u, ""), label);
26304
26512
  }
26305
- async function promptHidden2(query) {
26306
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
26307
- throw new Error("vault password is required; use --vault-password-stdin or a local TTY prompt");
26308
- }
26309
- const rl = import_node_readline2.default.createInterface({
26310
- input: process.stdin,
26311
- output: process.stdout,
26312
- terminal: true
26313
- });
26314
- rl.stdoutMuted = true;
26315
- rl._writeToOutput = (value) => {
26316
- if (value.includes(query)) {
26317
- rl.output.write(value);
26318
- return;
26319
- }
26320
- if (!rl.stdoutMuted) {
26321
- rl.output.write(value);
26322
- }
26323
- };
26324
- const answer = await new Promise((resolve) => {
26325
- rl.question(query, resolve);
26326
- });
26327
- rl.close();
26328
- process.stdout.write("\n");
26329
- return validateSecret4(answer, "vault password");
26513
+ async function promptHidden2(query, label = "vault password", nonInteractiveError = "vault password is required; use --vault-password-stdin or a local TTY prompt") {
26514
+ const answer = await promptHiddenTty(query, nonInteractiveError);
26515
+ return validateSecret4(answer, label);
26330
26516
  }
26331
26517
  async function promptVisible2(query, nonInteractiveError) {
26332
26518
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -26369,6 +26555,28 @@ function resolveExistingWalletSetupTarget(config) {
26369
26555
  hasLegacyAgentAuthToken
26370
26556
  };
26371
26557
  }
26558
+ function resolveReusableWalletSetupTarget(config) {
26559
+ let walletProfile;
26560
+ try {
26561
+ walletProfile = resolveWalletProfile(config);
26562
+ } catch (error) {
26563
+ throw new Error(
26564
+ `--reuse-existing-wallet requires a local wallet to reuse; ${renderError5(error)}`
26565
+ );
26566
+ }
26567
+ const existingVaultKeyId = walletProfile.vaultKeyId?.trim();
26568
+ const existingVaultPublicKey = walletProfile.vaultPublicKey?.trim();
26569
+ if (!existingVaultKeyId || !existingVaultPublicKey) {
26570
+ throw new Error(
26571
+ "--reuse-existing-wallet requires wallet.vaultKeyId and wallet.vaultPublicKey in the local wallet profile"
26572
+ );
26573
+ }
26574
+ return {
26575
+ address: walletProfile.address?.trim() || deriveWalletAddress2(existingVaultPublicKey),
26576
+ existingVaultKeyId,
26577
+ existingVaultPublicKey
26578
+ };
26579
+ }
26372
26580
  async function confirmAdminSetupOverwrite(options, config, deps = {}) {
26373
26581
  const existing = resolveExistingWalletSetupTarget(config);
26374
26582
  if (!existing || options.yes) {
@@ -26376,12 +26584,12 @@ async function confirmAdminSetupOverwrite(options, config, deps = {}) {
26376
26584
  }
26377
26585
  if (options.nonInteractive) {
26378
26586
  throw new Error(
26379
- "`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode"
26587
+ options.reuseExistingWallet ? "`wlfi-agent admin setup --reuse-existing-wallet` would refresh the existing local wallet metadata and agent credentials; rerun with --yes in non-interactive mode" : "`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode"
26380
26588
  );
26381
26589
  }
26382
26590
  const stderr = deps.stderr ?? process.stderr;
26383
26591
  stderr.write(
26384
- "warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n"
26592
+ options.reuseExistingWallet ? "warning: admin setup will reuse the current vault and refresh the local wallet metadata and agent credentials.\n" : "warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n"
26385
26593
  );
26386
26594
  if (existing.address) {
26387
26595
  stderr.write(`current address: ${existing.address}
@@ -26394,11 +26602,13 @@ async function confirmAdminSetupOverwrite(options, config, deps = {}) {
26394
26602
  if (existing.hasLegacyAgentAuthToken) {
26395
26603
  stderr.write("legacy agent auth token is still present in config.json\n");
26396
26604
  }
26605
+ const confirmationToken = options.reuseExistingWallet ? "REUSE" : "OVERWRITE";
26606
+ const confirmationPrompt = options.reuseExistingWallet ? "Type REUSE to reattach the current local vault: " : "Type OVERWRITE to replace the current local wallet: ";
26397
26607
  const confirmation = await (deps.prompt ?? ((query) => promptVisible2(
26398
26608
  query,
26399
- "`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet"
26400
- )))("Type OVERWRITE to replace the current local wallet: ");
26401
- if (confirmation !== "OVERWRITE") {
26609
+ options.reuseExistingWallet ? "`wlfi-agent admin setup --reuse-existing-wallet` requires --yes in non-interactive environments when reusing an existing wallet" : "`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet"
26610
+ )))(confirmationPrompt);
26611
+ if (confirmation !== confirmationToken) {
26402
26612
  throw new Error("admin setup aborted");
26403
26613
  }
26404
26614
  }
@@ -26429,7 +26639,9 @@ async function resolveAdminSetupVaultPassword(options, deps = {}) {
26429
26639
  "vault password is required in non-interactive mode; use --vault-password-stdin"
26430
26640
  );
26431
26641
  }
26432
- return promptForVaultPassword("Vault password (input hidden; nothing will be echoed): ");
26642
+ return promptForVaultPassword(
26643
+ "Vault password (input hidden; this unlocks the wallet, not sudo): "
26644
+ );
26433
26645
  }
26434
26646
  function resolveDaemonSocket(optionValue) {
26435
26647
  return import_node_path8.default.resolve(optionValue?.trim() || DEFAULT_MANAGED_DAEMON_SOCKET2);
@@ -26437,6 +26649,21 @@ function resolveDaemonSocket(optionValue) {
26437
26649
  function resolveStateFile() {
26438
26650
  return import_node_path8.default.resolve(DEFAULT_MANAGED_STATE_FILE2);
26439
26651
  }
26652
+ function resolveDefaultActiveChainForFreshSetup(config) {
26653
+ try {
26654
+ const profile = resolveCliNetworkProfile("bsc", config);
26655
+ return {
26656
+ chainId: profile.chainId,
26657
+ chainName: profile.key?.trim() || profile.name.trim().toLowerCase() || "bsc",
26658
+ ...profile.rpcUrl?.trim() ? { rpcUrl: profile.rpcUrl.trim() } : {}
26659
+ };
26660
+ } catch {
26661
+ return null;
26662
+ }
26663
+ }
26664
+ function shouldSeedDefaultActiveChain(options, config) {
26665
+ return !options.network && !options.rpcUrl && !options.chainName && config.chainId === void 0 && !config.chainName?.trim() && !config.rpcUrl?.trim();
26666
+ }
26440
26667
  function createAdminSetupPlan(options, deps = {}) {
26441
26668
  if (options.rpcUrl && !options.network) {
26442
26669
  throw new Error("--rpc-url requires --network");
@@ -26460,6 +26687,7 @@ function createAdminSetupPlan(options, deps = {}) {
26460
26687
  installError = renderError5(error);
26461
26688
  }
26462
26689
  const existingWallet = resolveExistingWalletSetupTarget(config);
26690
+ const reusableWallet = options.reuseExistingWallet ? resolveReusableWalletSetupTarget(config) : null;
26463
26691
  return {
26464
26692
  command: "setup",
26465
26693
  mode: "plan",
@@ -26502,6 +26730,8 @@ function createAdminSetupPlan(options, deps = {}) {
26502
26730
  recipient: options.recipient,
26503
26731
  attachPolicyId: options.attachPolicyId,
26504
26732
  attachBootstrapPolicies: options.attachBootstrapPolicies,
26733
+ existingVaultKeyId: reusableWallet?.existingVaultKeyId,
26734
+ existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
26505
26735
  bootstrapOutputPath: options.bootstrapOutput,
26506
26736
  deleteBootstrapOutput: options.deleteBootstrapOutput
26507
26737
  },
@@ -26826,7 +27056,9 @@ ${error.stdout}`;
26826
27056
  }
26827
27057
  var sudoSession2 = createSudoSession({
26828
27058
  promptPassword: async () => await promptHidden2(
26829
- "Root password (input hidden; required to install or recover the root daemon): "
27059
+ "macOS admin password for sudo (input hidden; required to install or recover the root daemon): ",
27060
+ "macOS admin password for sudo",
27061
+ "macOS admin password for sudo is required; rerun on a local TTY"
26830
27062
  )
26831
27063
  });
26832
27064
  async function managedStateFileExists(stateFile) {
@@ -26841,6 +27073,134 @@ async function managedStateFileExists(stateFile) {
26841
27073
  result.stderr.trim() || result.stdout.trim() || `failed to inspect managed daemon state file '${stateFile}' (exit code ${result.code})`
26842
27074
  );
26843
27075
  }
27076
+ async function inspectManagedState(stateFile, showProgress, message = "Inspecting managed daemon state") {
27077
+ await sudoSession2.prime();
27078
+ const stateProbeProgress = createProgress2(message, showProgress);
27079
+ let stateExists;
27080
+ try {
27081
+ stateExists = await managedStateFileExists(stateFile);
27082
+ stateProbeProgress.succeed(
27083
+ stateExists ? "Managed daemon state already exists" : "No managed daemon state found"
27084
+ );
27085
+ } catch (error) {
27086
+ stateProbeProgress.fail();
27087
+ throw error;
27088
+ }
27089
+ return stateExists;
27090
+ }
27091
+ function createManagedStatePasswordMismatchError(stateFile) {
27092
+ return new Error(
27093
+ `managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`
27094
+ );
27095
+ }
27096
+ function isManagedStatePasswordMismatch(output) {
27097
+ return /failed to decrypt state|wrong password or tampered file|authentication failed/iu.test(
27098
+ output
27099
+ );
27100
+ }
27101
+ async function managedStateAcceptsRequestedVaultPassword(config, daemonSocket, stateFile, vaultPassword) {
27102
+ const installPreconditions = assertManagedDaemonInstallPreconditions(
27103
+ config,
27104
+ daemonSocket,
27105
+ stateFile
27106
+ );
27107
+ const tempRoot = import_node_fs9.default.mkdtempSync(import_node_path8.default.join(import_node_os3.default.tmpdir(), "wlfi-managed-state-probe-"));
27108
+ const tempSocket = import_node_path8.default.join(tempRoot, "daemon.sock");
27109
+ const allowedUid = String(process.getuid?.() ?? process.geteuid?.() ?? 0);
27110
+ const probeScript = [
27111
+ "set -euo pipefail",
27112
+ 'vault_password="$(cat)"',
27113
+ 'daemon_bin="$1"',
27114
+ 'state_file="$2"',
27115
+ 'daemon_socket="$3"',
27116
+ 'admin_uid="$4"',
27117
+ 'agent_uid="$5"',
27118
+ 'signer_backend="$6"',
27119
+ '"$daemon_bin" \\',
27120
+ " --non-interactive \\",
27121
+ " --vault-password-stdin \\",
27122
+ ' --state-file "$state_file" \\',
27123
+ ' --daemon-socket "$daemon_socket" \\',
27124
+ ' --signer-backend "$signer_backend" \\',
27125
+ ' --allow-admin-euid "$admin_uid" \\',
27126
+ ' --allow-agent-euid "$agent_uid" <<<"$vault_password" &',
27127
+ 'child="$!"',
27128
+ "for _ in $(seq 1 20); do",
27129
+ ' if ! kill -0 "$child" 2>/dev/null; then',
27130
+ ' wait "$child"',
27131
+ " exit $?",
27132
+ " fi",
27133
+ " sleep 0.25",
27134
+ "done",
27135
+ 'kill "$child" >/dev/null 2>&1 || true',
27136
+ 'wait "$child" >/dev/null 2>&1 || true',
27137
+ "exit 0"
27138
+ ].join("\n");
27139
+ try {
27140
+ const result = await sudoSession2.run(
27141
+ [
27142
+ "/bin/bash",
27143
+ "-lc",
27144
+ probeScript,
27145
+ "--",
27146
+ installPreconditions.daemonBin,
27147
+ stateFile,
27148
+ tempSocket,
27149
+ allowedUid,
27150
+ allowedUid,
27151
+ DEFAULT_SIGNER_BACKEND
27152
+ ],
27153
+ {
27154
+ stdin: `${vaultPassword}
27155
+ `
27156
+ }
27157
+ );
27158
+ if (result.code === 0) {
27159
+ return true;
27160
+ }
27161
+ const combinedOutput = `${result.stderr}
27162
+ ${result.stdout}`.trim();
27163
+ if (isManagedStatePasswordMismatch(combinedOutput)) {
27164
+ return false;
27165
+ }
27166
+ throw new Error(
27167
+ combinedOutput || `failed to validate the managed daemon state with the requested vault password (exit code ${result.code})`
27168
+ );
27169
+ } finally {
27170
+ import_node_fs9.default.rmSync(tempRoot, { recursive: true, force: true });
27171
+ }
27172
+ }
27173
+ async function assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(config, daemonSocket, stateFile, vaultPassword, showProgress) {
27174
+ const stateExists = await inspectManagedState(
27175
+ stateFile,
27176
+ showProgress,
27177
+ "Inspecting managed daemon state before install"
27178
+ );
27179
+ if (!stateExists) {
27180
+ return;
27181
+ }
27182
+ const verifyProgress = createProgress2(
27183
+ "Verifying the requested vault password against the managed daemon state",
27184
+ showProgress
27185
+ );
27186
+ let accepted;
27187
+ try {
27188
+ accepted = await managedStateAcceptsRequestedVaultPassword(
27189
+ config,
27190
+ daemonSocket,
27191
+ stateFile,
27192
+ vaultPassword
27193
+ );
27194
+ } catch (error) {
27195
+ verifyProgress.fail();
27196
+ throw error;
27197
+ }
27198
+ if (!accepted) {
27199
+ verifyProgress.fail("Existing daemon password does not unlock the stored daemon state");
27200
+ throw createManagedStatePasswordMismatchError(stateFile);
27201
+ }
27202
+ verifyProgress.succeed("Requested vault password unlocks the existing managed daemon state");
27203
+ }
26844
27204
  async function waitForTrustedDaemonSocket(targetPath, timeoutMs = 15e3) {
26845
27205
  async function canConnect(socketPath) {
26846
27206
  return new Promise((resolve) => {
@@ -27019,6 +27379,8 @@ async function runAdminSetup(options) {
27019
27379
  const config = readConfig();
27020
27380
  const daemonSocket = resolveDaemonSocket(options.daemonSocket);
27021
27381
  const stateFile = resolveStateFile();
27382
+ const defaultActiveChain = shouldSeedDefaultActiveChain(options, config) ? resolveDefaultActiveChainForFreshSetup(config) : null;
27383
+ const reusableWallet = options.reuseExistingWallet ? resolveReusableWalletSetupTarget(config) : null;
27022
27384
  assertManagedDaemonInstallPreconditions(config, daemonSocket, stateFile);
27023
27385
  await confirmAdminSetupOverwrite(options, config);
27024
27386
  const vaultPassword = await resolveAdminSetupVaultPassword(options);
@@ -27076,10 +27438,17 @@ async function runAdminSetup(options) {
27076
27438
  }
27077
27439
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
27078
27440
  process.stderr.write(
27079
- "Root password required: setup must install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n"
27441
+ "macOS admin password required: setup uses sudo to install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n"
27080
27442
  );
27081
27443
  }
27082
27444
  await sudoSession2.prime();
27445
+ await assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(
27446
+ config,
27447
+ daemonSocket,
27448
+ stateFile,
27449
+ vaultPassword,
27450
+ showProgress
27451
+ );
27083
27452
  const installProgress = createProgress2("Installing and restarting daemon", showProgress);
27084
27453
  try {
27085
27454
  daemon = await installLaunchDaemon(config, daemonSocket, stateFile, vaultPassword);
@@ -27109,34 +27478,20 @@ async function runAdminSetup(options) {
27109
27478
  if (!daemonAcceptedPassword) {
27110
27479
  if (existingDaemonRejectedPassword) {
27111
27480
  authProgress.fail("Existing daemon password does not unlock the stored daemon state");
27112
- throw new Error(
27113
- `managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`
27114
- );
27481
+ throw createManagedStatePasswordMismatchError(stateFile);
27115
27482
  }
27116
27483
  authProgress.info("Daemon rejected the requested vault password; inspecting managed state");
27117
- const stateProbeProgress = createProgress2("Inspecting managed daemon state", showProgress);
27118
- let stateExists;
27119
- try {
27120
- stateExists = await managedStateFileExists(stateFile);
27121
- stateProbeProgress.succeed(
27122
- stateExists ? "Managed daemon state already exists" : "No managed daemon state found"
27123
- );
27124
- } catch (error) {
27125
- stateProbeProgress.fail();
27126
- throw error;
27127
- }
27484
+ const stateExists = await inspectManagedState(stateFile, showProgress);
27128
27485
  if (stateExists) {
27129
27486
  authProgress.fail("Existing daemon password does not unlock the stored daemon state");
27130
- throw new Error(
27131
- `managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`
27132
- );
27487
+ throw createManagedStatePasswordMismatchError(stateFile);
27133
27488
  }
27134
27489
  authProgress.fail(
27135
27490
  "Existing daemon password differs; reinstalling with the requested vault password"
27136
27491
  );
27137
27492
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
27138
27493
  process.stderr.write(
27139
- "Root password required: setup must reinstall the root LaunchDaemon to rotate the managed daemon password.\n"
27494
+ "macOS admin password required: setup uses sudo to reinstall the root LaunchDaemon and rotate the managed daemon password.\n"
27140
27495
  );
27141
27496
  }
27142
27497
  await sudoSession2.prime();
@@ -27216,6 +27571,8 @@ async function runAdminSetup(options) {
27216
27571
  recipient: options.recipient,
27217
27572
  attachPolicyId: options.attachPolicyId,
27218
27573
  attachBootstrapPolicies: options.attachBootstrapPolicies,
27574
+ existingVaultKeyId: reusableWallet?.existingVaultKeyId,
27575
+ existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
27219
27576
  bootstrapOutputPath: bootstrapOutput.path
27220
27577
  });
27221
27578
  const bootstrapProgress = createProgress2("Setting up wallet access", showProgress);
@@ -27286,7 +27643,12 @@ async function runAdminSetup(options) {
27286
27643
  }
27287
27644
  const persistedConfig = writeConfig({
27288
27645
  daemonSocket,
27289
- stateFile
27646
+ stateFile,
27647
+ ...defaultActiveChain ? {
27648
+ chainId: defaultActiveChain.chainId,
27649
+ chainName: defaultActiveChain.chainName,
27650
+ ...defaultActiveChain.rpcUrl ? { rpcUrl: defaultActiveChain.rpcUrl } : {}
27651
+ } : {}
27290
27652
  });
27291
27653
  printCliPayload(
27292
27654
  {
@@ -27338,6 +27700,8 @@ function buildAdminSetupBootstrapInvocation(input) {
27338
27700
  recipient: input.recipient,
27339
27701
  attachPolicyId: input.attachPolicyId,
27340
27702
  attachBootstrapPolicies: input.attachBootstrapPolicies,
27703
+ existingVaultKeyId: input.existingVaultKeyId,
27704
+ existingVaultPublicKey: input.existingVaultPublicKey,
27341
27705
  bootstrapOutputPath: input.bootstrapOutputPath
27342
27706
  }),
27343
27707
  stdin: `${validateSecret4(input.vaultPassword, "vault password")}
@@ -27396,7 +27760,11 @@ async function runAdminSetupCli(argv) {
27396
27760
  const program2 = new Command();
27397
27761
  program2.name("wlfi-agent admin setup").description(
27398
27762
  "Store the vault password, install the root daemon autostart, set up wallet access, and print the wallet address"
27399
- ).option("--plan", "Print a sanitized setup preview without prompting or mutating state", false).option("--vault-password-stdin", "Read vault password from stdin", false).option("--non-interactive", "Disable password prompts", false).option("-y, --yes", "Skip the overwrite confirmation prompt", false).option("--daemon-socket <path>", "Daemon unix socket path").option("--per-tx-max-wei <wei>", "Per-transaction max spend in wei").option("--daily-max-wei <wei>", "Daily max spend in wei").option("--weekly-max-wei <wei>", "Weekly max spend in wei").option("--max-gas-per-chain-wei <wei>", "Per-chain gas-spend ceiling in wei").option("--daily-max-tx-count <count>", "Optional daily tx-count cap").option("--per-tx-max-fee-per-gas-wei <wei>", "Optional max fee-per-gas cap").option("--per-tx-max-priority-fee-per-gas-wei <wei>", "Optional max priority fee-per-gas cap").option("--per-tx-max-calldata-bytes <bytes>", "Optional calldata size cap").option(
27763
+ ).option("--plan", "Print a sanitized setup preview without prompting or mutating state", false).option("--vault-password-stdin", "Read vault password from stdin", false).option("--non-interactive", "Disable password prompts", false).option("-y, --yes", "Skip the overwrite confirmation prompt", false).option(
27764
+ "--reuse-existing-wallet",
27765
+ "Reuse the current local vault instead of generating a fresh wallet",
27766
+ false
27767
+ ).option("--daemon-socket <path>", "Daemon unix socket path").option("--per-tx-max-wei <wei>", "Per-transaction max spend in wei").option("--daily-max-wei <wei>", "Daily max spend in wei").option("--weekly-max-wei <wei>", "Weekly max spend in wei").option("--max-gas-per-chain-wei <wei>", "Per-chain gas-spend ceiling in wei").option("--daily-max-tx-count <count>", "Optional daily tx-count cap").option("--per-tx-max-fee-per-gas-wei <wei>", "Optional max fee-per-gas cap").option("--per-tx-max-priority-fee-per-gas-wei <wei>", "Optional max priority fee-per-gas cap").option("--per-tx-max-calldata-bytes <bytes>", "Optional calldata size cap").option(
27400
27768
  "--token <address>",
27401
27769
  "Allowed ERC-20 token address",
27402
27770
  (value, acc) => {
@@ -27531,8 +27899,12 @@ function resolveConfiguredAgentKeyId(config, explicitAgentKeyId) {
27531
27899
  if (explicitAgentKeyId) {
27532
27900
  return void 0;
27533
27901
  }
27902
+ const renderedError = error instanceof Error ? error.message : (
27903
+ /* c8 ignore next -- assertValidAgentKeyId throws Error objects */
27904
+ String(error)
27905
+ );
27534
27906
  throw new Error(
27535
- (error instanceof Error ? error.message : String(error)) + "; pass --agent-key-id to migrate the legacy config secret explicitly"
27907
+ renderedError + "; pass --agent-key-id to migrate the legacy config secret explicitly"
27536
27908
  );
27537
27909
  }
27538
27910
  }
@@ -27818,11 +28190,26 @@ function rewriteAmountPolicyErrorMessage(message, asset) {
27818
28190
  (_match, usedAmountWei, requestedAmountWei, maxAmountWei) => `window usage ${formatAmount(usedAmountWei)} + requested ${formatAmount(requestedAmountWei)} > max ${formatAmount(maxAmountWei)}`
27819
28191
  ).replace(
27820
28192
  /requires manual approval for requested amount (\d+) within range (None|Some\((\d+)\))\.\.=(\d+)/gu,
27821
- (_match, requestedAmountWei, minAmountLiteral, minAmountWei, maxAmountWei) => `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${minAmountLiteral === "None" ? "None" : `Some(${formatAmount(minAmountWei ?? "0")})`}..=${formatAmount(maxAmountWei)}`
28193
+ (_match, requestedAmountWei, minAmountLiteral, minAmountWei, maxAmountWei) => {
28194
+ const minAmountDisplay = (
28195
+ /* c8 ignore next -- both None and Some(...) paths are exercised, but c8 misattributes this ternary under --experimental-strip-types */
28196
+ minAmountLiteral === "None" ? "None" : `Some(${formatAmount(minAmountWei ?? "0")})`
28197
+ );
28198
+ return `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${minAmountDisplay}..=${formatAmount(maxAmountWei)}`;
28199
+ }
27822
28200
  );
27823
28201
  }
27824
28202
 
27825
28203
  // src/lib/asset-broadcast.ts
28204
+ function resolveEstimatedPriorityFeePerGasWei(fees) {
28205
+ const resolved = fees.maxPriorityFeePerGas ?? fees.gasPrice;
28206
+ if (resolved === null || resolved <= 0n) {
28207
+ throw new Error(
28208
+ "Could not determine maxPriorityFeePerGas; pass --max-priority-fee-per-gas-wei"
28209
+ );
28210
+ }
28211
+ return resolved;
28212
+ }
27826
28213
  function encodeErc20TransferData(recipient, amountWei) {
27827
28214
  return encodeFunctionData({
27828
28215
  abi: erc20Abi,
@@ -27851,10 +28238,10 @@ async function resolveAssetBroadcastPlan(input, deps) {
27851
28238
  const fees = await deps.estimateFees(input.rpcUrl);
27852
28239
  const resolvedFees = fees;
27853
28240
  const maxFeePerGasWei = input.maxFeePerGasWei ?? (resolvedFees.maxFeePerGas ?? resolvedFees.gasPrice);
27854
- const maxPriorityFeePerGasWei = input.maxPriorityFeePerGasWei ?? (resolvedFees.maxPriorityFeePerGas ?? resolvedFees.gasPrice ?? 0n);
27855
28241
  if (maxFeePerGasWei === null || maxFeePerGasWei <= 0n) {
27856
28242
  throw new Error("Could not determine maxFeePerGas; pass --max-fee-per-gas-wei");
27857
28243
  }
28244
+ const maxPriorityFeePerGasWei = input.maxPriorityFeePerGasWei ?? resolveEstimatedPriorityFeePerGasWei(resolvedFees);
27858
28245
  return {
27859
28246
  rpcUrl: input.rpcUrl,
27860
28247
  chainId: input.chainId,
@@ -27879,7 +28266,7 @@ async function completeAssetBroadcast(plan, signed, deps) {
27879
28266
  to: plan.to,
27880
28267
  chainId: plan.chainId,
27881
28268
  nonce: plan.nonce,
27882
- allowHigherNonce: true,
28269
+ allowHigherNonce: false,
27883
28270
  value: plan.valueWei,
27884
28271
  data: plan.dataHex,
27885
28272
  gasLimit: plan.gasLimit,
@@ -27986,7 +28373,6 @@ function resolveConfigMutationCommandLabel(command, key) {
27986
28373
  }
27987
28374
 
27988
28375
  // src/lib/local-admin-access.ts
27989
- var import_node_readline3 = __toESM(require("readline"), 1);
27990
28376
  var MAX_SECRET_STDIN_BYTES4 = 16 * 1024;
27991
28377
  function renderError6(error) {
27992
28378
  return error instanceof Error ? error.message : String(error);
@@ -28004,35 +28390,13 @@ function currentProcessIsRoot2() {
28004
28390
  return typeof process.geteuid === "function" && process.geteuid() === 0;
28005
28391
  }
28006
28392
  async function promptHidden3(query, label) {
28007
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
28008
- throw new Error(`${label} is required; rerun on a local TTY`);
28009
- }
28010
- const rl = import_node_readline3.default.createInterface({
28011
- input: process.stdin,
28012
- output: process.stdout,
28013
- terminal: true
28014
- });
28015
- rl.stdoutMuted = true;
28016
- rl._writeToOutput = (value) => {
28017
- if (value.includes(query)) {
28018
- rl.output.write(value);
28019
- return;
28020
- }
28021
- if (!rl.stdoutMuted) {
28022
- rl.output.write(value);
28023
- }
28024
- };
28025
- const answer = await new Promise((resolve) => {
28026
- rl.question(query, resolve);
28027
- });
28028
- rl.close();
28029
- process.stdout.write("\n");
28393
+ const answer = await promptHiddenTty(query, `${label} is required; rerun on a local TTY`);
28030
28394
  return validateSecret5(answer, label);
28031
28395
  }
28032
28396
  var sudoSession3 = createSudoSession({
28033
28397
  promptPassword: async () => await promptHidden3(
28034
- "Root password (input hidden; required to change local admin chain and token configuration): ",
28035
- "root password"
28398
+ "macOS admin password for sudo (input hidden; required to change local admin chain and token configuration): ",
28399
+ "macOS admin password for sudo"
28036
28400
  )
28037
28401
  });
28038
28402
  async function requireLocalAdminMutationAccess(commandLabel, deps = {}) {
@@ -28915,10 +29279,9 @@ function isManualApprovalRequiredOutput(value) {
28915
29279
  const candidate = value;
28916
29280
  return typeof candidate.command === "string" && typeof candidate.approval_request_id === "string" && typeof candidate.cli_approval_command === "string";
28917
29281
  }
28918
- function printManualApprovalRequired(output, asJson) {
29282
+ function renderManualApprovalRequired(output, asJson) {
28919
29283
  if (asJson) {
28920
- print2(output, true);
28921
- return;
29284
+ return formatJson(output);
28922
29285
  }
28923
29286
  const lines = [
28924
29287
  `Command: ${output.command}`,
@@ -28931,7 +29294,27 @@ function printManualApprovalRequired(output, asJson) {
28931
29294
  lines.push(`Relay URL: ${output.relay_url}`);
28932
29295
  }
28933
29296
  lines.push(`CLI Approval Command: ${output.cli_approval_command}`);
28934
- print2(lines.join("\n"), false);
29297
+ return lines.join("\n");
29298
+ }
29299
+ function printManualApprovalRequired(output, asJson, useStderr = false) {
29300
+ const rendered = renderManualApprovalRequired(output, asJson);
29301
+ if (useStderr) {
29302
+ console.error(rendered);
29303
+ return;
29304
+ }
29305
+ console.log(rendered);
29306
+ }
29307
+ function printManualApprovalWaiting(output, asJson) {
29308
+ if (asJson) {
29309
+ console.error(
29310
+ formatJson({
29311
+ event: "manualApprovalPending",
29312
+ approvalRequestId: output.approval_request_id
29313
+ })
29314
+ );
29315
+ return;
29316
+ }
29317
+ console.error(`Waiting for manual approval decision: ${output.approval_request_id}`);
28935
29318
  }
28936
29319
  var MAX_SECRET_STDIN_BYTES5 = 16 * 1024;
28937
29320
  var AGENT_KEY_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
@@ -29052,6 +29435,15 @@ function print2(payload, asJson) {
29052
29435
  }
29053
29436
  var ONCHAIN_RECEIPT_TIMEOUT_MS = 3e4;
29054
29437
  var ONCHAIN_RECEIPT_POLL_INTERVAL_MS = 2e3;
29438
+ var MANUAL_APPROVAL_POLL_INTERVAL_MS = 2e3;
29439
+ var MANUAL_APPROVAL_WAIT_TIMEOUT_MS = (() => {
29440
+ const raw = process.env.WLFI_TEST_MANUAL_APPROVAL_TIMEOUT_MS;
29441
+ if (!raw) {
29442
+ return 5 * 6e4;
29443
+ }
29444
+ const parsed = Number(raw);
29445
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 5 * 6e4;
29446
+ })();
29055
29447
  async function reportOnchainReceiptStatus(input) {
29056
29448
  if (input.asJson) {
29057
29449
  console.error(
@@ -29193,39 +29585,40 @@ async function resolveAgentCommandContext(options, config) {
29193
29585
  warnForAgentAuthTokenSource(agentAuthTokenSource, agentKeyId);
29194
29586
  return { agentKeyId, agentAuthToken, daemonSocket };
29195
29587
  }
29196
- async function runAgentCommandJson(input) {
29197
- const { agentKeyId, agentAuthToken, daemonSocket } = await resolveAgentCommandContext(
29198
- input.auth,
29199
- input.config
29200
- );
29588
+ async function runAgentCommandJsonOnce(input) {
29201
29589
  try {
29202
- return await runRustBinaryJson(
29203
- "wlfi-agent-agent",
29204
- [
29205
- "--json",
29206
- "--agent-key-id",
29207
- agentKeyId,
29208
- "--agent-auth-token-stdin",
29209
- "--daemon-socket",
29210
- daemonSocket,
29211
- ...input.commandArgs
29212
- ],
29213
- input.config,
29214
- {
29215
- stdin: `${agentAuthToken}
29590
+ return {
29591
+ type: "success",
29592
+ value: await runRustBinaryJson(
29593
+ "wlfi-agent-agent",
29594
+ [
29595
+ "--json",
29596
+ "--agent-key-id",
29597
+ input.agentKeyId,
29598
+ "--agent-auth-token-stdin",
29599
+ "--daemon-socket",
29600
+ input.daemonSocket,
29601
+ ...input.commandArgs
29602
+ ],
29603
+ input.config,
29604
+ {
29605
+ stdin: `${input.agentAuthToken}
29216
29606
  `,
29217
- preSuppliedSecretStdin: "agentAuthToken",
29218
- scrubSensitiveEnv: true
29219
- }
29220
- );
29607
+ preSuppliedSecretStdin: "agentAuthToken",
29608
+ scrubSensitiveEnv: true
29609
+ }
29610
+ )
29611
+ };
29221
29612
  } catch (error) {
29222
29613
  if (error instanceof RustBinaryExitError && error.stdout.trim()) {
29223
29614
  try {
29224
29615
  const parsed = JSON.parse(error.stdout);
29225
29616
  if (isManualApprovalRequiredOutput(parsed)) {
29226
- printManualApprovalRequired(parsed, input.asJson);
29227
- process.exitCode = error.code;
29228
- return null;
29617
+ return {
29618
+ type: "manualApproval",
29619
+ output: parsed,
29620
+ exitCode: error.code
29621
+ };
29229
29622
  }
29230
29623
  } catch {
29231
29624
  }
@@ -29233,6 +29626,49 @@ async function runAgentCommandJson(input) {
29233
29626
  throw error;
29234
29627
  }
29235
29628
  }
29629
+ async function runAgentCommandJson(input) {
29630
+ const { agentKeyId, agentAuthToken, daemonSocket } = await resolveAgentCommandContext(
29631
+ input.auth,
29632
+ input.config
29633
+ );
29634
+ const runOnce = () => runAgentCommandJsonOnce({
29635
+ commandArgs: input.commandArgs,
29636
+ config: input.config,
29637
+ agentKeyId,
29638
+ agentAuthToken,
29639
+ daemonSocket
29640
+ });
29641
+ const firstAttempt = await runOnce();
29642
+ if (firstAttempt.type === "success") {
29643
+ return firstAttempt.value;
29644
+ }
29645
+ if (!input.waitForManualApproval) {
29646
+ printManualApprovalRequired(firstAttempt.output, input.asJson);
29647
+ process.exitCode = firstAttempt.exitCode;
29648
+ return null;
29649
+ }
29650
+ printManualApprovalRequired(firstAttempt.output, input.asJson, true);
29651
+ printManualApprovalWaiting(firstAttempt.output, input.asJson);
29652
+ const pendingApprovalRequestId = firstAttempt.output.approval_request_id;
29653
+ const startedWaitingAt = Date.now();
29654
+ for (; ; ) {
29655
+ await (0, import_promises2.setTimeout)(MANUAL_APPROVAL_POLL_INTERVAL_MS);
29656
+ const nextAttempt = await runOnce();
29657
+ if (nextAttempt.type === "success") {
29658
+ return nextAttempt.value;
29659
+ }
29660
+ if (nextAttempt.output.approval_request_id !== pendingApprovalRequestId) {
29661
+ throw new Error(
29662
+ `manual approval request changed while waiting for a decision (${pendingApprovalRequestId} -> ${nextAttempt.output.approval_request_id}); stop and rerun the command after checking the approval status.`
29663
+ );
29664
+ }
29665
+ if (Date.now() - startedWaitingAt >= MANUAL_APPROVAL_WAIT_TIMEOUT_MS) {
29666
+ throw new Error(
29667
+ `Timed out after ${Math.ceil(MANUAL_APPROVAL_WAIT_TIMEOUT_MS / 1e3)}s waiting for manual approval decision: ${pendingApprovalRequestId}`
29668
+ );
29669
+ }
29670
+ }
29671
+ }
29236
29672
  function legacyAmountToDecimalString(value) {
29237
29673
  if (value === void 0) {
29238
29674
  return void 0;
@@ -29870,7 +30306,8 @@ async function main() {
29870
30306
  ],
29871
30307
  auth: options,
29872
30308
  config,
29873
- asJson: options.json
30309
+ asJson: options.json,
30310
+ waitForManualApproval: true
29874
30311
  });
29875
30312
  if (!signed) {
29876
30313
  return;
@@ -29926,20 +30363,108 @@ async function main() {
29926
30363
  }
29927
30364
  });
29928
30365
  addAgentCommandAuthOptions(
29929
- program2.command("transfer-native").description("Submit a native ETH transfer request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient address").requiredOption("--amount <amount>", "Transfer amount in configured native token units").addOption(new Option("--amount-wei <wei>").hideHelp())
30366
+ program2.command("transfer-native").description("Submit a native ETH transfer request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient address").requiredOption("--amount <amount>", "Transfer amount in configured native token units").option("--broadcast", "Broadcast the signed transaction through RPC", false).option("--rpc-url <url>", "Ethereum RPC URL override used only for broadcast").option(
30367
+ "--from <address>",
30368
+ "Sender address override for broadcast; defaults to configured wallet address"
30369
+ ).option("--nonce <nonce>", "Explicit nonce override for broadcast").option("--gas-limit <gas>", "Gas limit override for broadcast").option("--max-fee-per-gas-wei <wei>", "Max fee per gas override for broadcast").option("--max-priority-fee-per-gas-wei <wei>", "Priority fee per gas override for broadcast").option("--tx-type <type>", "Typed tx value for broadcast", "0x02").option("--no-wait", "Do not wait up to 30s for an on-chain receipt after broadcast").option(
30370
+ "--reveal-raw-tx",
30371
+ "Include the signed raw transaction bytes in broadcast output",
30372
+ false
30373
+ ).option("--reveal-signature", "Include signer r/s/v fields in broadcast output", false).addOption(new Option("--amount-wei <wei>").hideHelp())
29930
30374
  ).action(async (options) => {
29931
30375
  const config = readConfig3();
29932
30376
  const network = resolveCliNetworkProfile(options.network, config).chainId;
29933
30377
  const asset = resolveConfiguredNativeAsset(config, network);
30378
+ const recipient = assertAddress(options.to, "to");
29934
30379
  const amountWei = options.amount ? parseConfiguredAmount(options.amount, asset.decimals, "amount") : parseBigIntString(options.amountWei, "amountWei");
29935
30380
  try {
30381
+ if (options.broadcast) {
30382
+ const plan = await resolveAssetBroadcastPlan(
30383
+ {
30384
+ rpcUrl: resolveCliRpcUrl(options.rpcUrl, options.network, config),
30385
+ chainId: network,
30386
+ from: options.from ? assertAddress(options.from, "from") : resolveWalletAddress(config),
30387
+ to: recipient,
30388
+ valueWei: amountWei,
30389
+ dataHex: "0x",
30390
+ nonce: options.nonce ? parseIntegerString(options.nonce, "nonce") : void 0,
30391
+ gasLimit: options.gasLimit ? parsePositiveBigIntString(options.gasLimit, "gasLimit") : void 0,
30392
+ maxFeePerGasWei: options.maxFeePerGasWei ? parsePositiveBigIntString(options.maxFeePerGasWei, "maxFeePerGasWei") : void 0,
30393
+ maxPriorityFeePerGasWei: options.maxPriorityFeePerGasWei ? parseBigIntString(options.maxPriorityFeePerGasWei, "maxPriorityFeePerGasWei") : void 0,
30394
+ txType: options.txType
30395
+ },
30396
+ {
30397
+ getChainInfo: getChainInfo2,
30398
+ assertRpcChainIdMatches,
30399
+ getNonce: getNonce2,
30400
+ estimateGas: estimateGas3,
30401
+ estimateFees: estimateFees2
30402
+ }
30403
+ );
30404
+ const signed = await runAgentCommandJson({
30405
+ commandArgs: [
30406
+ "broadcast",
30407
+ "--network",
30408
+ String(plan.chainId),
30409
+ "--nonce",
30410
+ String(plan.nonce),
30411
+ "--to",
30412
+ recipient,
30413
+ "--value-wei",
30414
+ amountWei.toString(),
30415
+ "--data-hex",
30416
+ "0x",
30417
+ "--gas-limit",
30418
+ plan.gasLimit.toString(),
30419
+ "--max-fee-per-gas-wei",
30420
+ plan.maxFeePerGasWei.toString(),
30421
+ "--max-priority-fee-per-gas-wei",
30422
+ plan.maxPriorityFeePerGasWei.toString(),
30423
+ "--tx-type",
30424
+ plan.txType
30425
+ ],
30426
+ auth: options,
30427
+ config,
30428
+ asJson: options.json,
30429
+ waitForManualApproval: true
30430
+ });
30431
+ if (!signed) {
30432
+ return;
30433
+ }
30434
+ const completed = await completeAssetBroadcast(plan, signed, {
30435
+ assertSignedBroadcastTransactionMatchesRequest,
30436
+ broadcastRawTransaction: broadcastRawTransaction2
30437
+ });
30438
+ print2(
30439
+ formatBroadcastedAssetOutput({
30440
+ command: "transfer-native",
30441
+ counterparty: recipient,
30442
+ asset,
30443
+ signed,
30444
+ plan,
30445
+ signedNonce: completed.signedNonce,
30446
+ networkTxHash: completed.networkTxHash,
30447
+ revealRawTx: options.revealRawTx,
30448
+ revealSignature: options.revealSignature
30449
+ }),
30450
+ options.json
30451
+ );
30452
+ if (options.wait) {
30453
+ await reportOnchainReceiptStatus({
30454
+ rpcUrl: plan.rpcUrl,
30455
+ txHash: completed.networkTxHash,
30456
+ asJson: options.json
30457
+ });
30458
+ }
30459
+ return;
30460
+ }
29936
30461
  const result = await runAgentCommandJson({
29937
30462
  commandArgs: [
29938
30463
  "transfer-native",
29939
30464
  "--network",
29940
30465
  String(network),
29941
30466
  "--to",
29942
- assertAddress(options.to, "to"),
30467
+ recipient,
29943
30468
  "--amount-wei",
29944
30469
  amountWei.toString()
29945
30470
  ],
@@ -30018,7 +30543,8 @@ async function main() {
30018
30543
  ],
30019
30544
  auth: options,
30020
30545
  config,
30021
- asJson: options.json
30546
+ asJson: options.json,
30547
+ waitForManualApproval: true
30022
30548
  });
30023
30549
  if (!signed) {
30024
30550
  return;
@@ -30074,29 +30600,46 @@ async function main() {
30074
30600
  }
30075
30601
  });
30076
30602
  addAgentCommandAuthOptions(
30077
- program2.command("broadcast").description("Submit a raw transaction broadcast request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient or target contract").requiredOption("--gas-limit <gas>", "Gas limit").requiredOption("--max-fee-per-gas-wei <wei>", "Max fee per gas in wei").option("--nonce <nonce>", "Explicit nonce override", "0").option("--value-wei <wei>", "Value in wei", "0").option("--data-hex <hex>", "Calldata hex", "0x").option("--max-priority-fee-per-gas-wei <wei>", "Priority fee per gas in wei", "0").option("--tx-type <type>", "Typed tx value", "2").option("--delegation-enabled", "Forward delegation flag to Rust signing request", false)
30603
+ program2.command("broadcast").description("Submit a raw transaction broadcast request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient or target contract").requiredOption("--gas-limit <gas>", "Gas limit").requiredOption("--max-fee-per-gas-wei <wei>", "Max fee per gas in wei").option("--nonce <nonce>", "Explicit nonce override").option("--value-wei <wei>", "Value in wei", "0").option("--data-hex <hex>", "Calldata hex", "0x").option("--max-priority-fee-per-gas-wei <wei>", "Priority fee per gas in wei").option("--tx-type <type>", "Typed tx value", "0x02").option("--delegation-enabled", "Forward delegation flag to Rust signing request", false)
30078
30604
  ).action(async (options) => {
30079
30605
  const config = readConfig3();
30080
30606
  const network = resolveCliNetworkProfile(options.network, config);
30081
- const result = await runAgentCommandJson({
30607
+ const to = assertAddress(options.to, "to");
30608
+ const valueWei = parseBigIntString(options.valueWei, "valueWei");
30609
+ const dataHex = assertHex(options.dataHex, "dataHex");
30610
+ const gasLimit = parsePositiveBigIntString(options.gasLimit, "gasLimit");
30611
+ const maxFeePerGasWei = parsePositiveBigIntString(options.maxFeePerGasWei, "maxFeePerGasWei");
30612
+ const explicitNonce = options.nonce ? parseIntegerString(options.nonce, "nonce") : void 0;
30613
+ const rpcUrl = resolveCliRpcUrl(void 0, options.network, config);
30614
+ const from14 = resolveWalletAddress(config);
30615
+ const chainInfo = await getChainInfo2(rpcUrl);
30616
+ assertRpcChainIdMatches(network.chainId, chainInfo.chainId);
30617
+ const nonce = explicitNonce ?? await getNonce2(rpcUrl, from14);
30618
+ const fees = options.maxPriorityFeePerGasWei ? null : await estimateFees2(rpcUrl);
30619
+ const maxPriorityFeePerGasWei = options.maxPriorityFeePerGasWei ? parseBigIntString(options.maxPriorityFeePerGasWei, "maxPriorityFeePerGasWei") : resolveEstimatedPriorityFeePerGasWei({
30620
+ gasPrice: fees?.gasPrice ?? null,
30621
+ maxFeePerGas: fees?.maxFeePerGas ?? null,
30622
+ maxPriorityFeePerGas: fees?.maxPriorityFeePerGas ?? null
30623
+ });
30624
+ const signed = await runAgentCommandJson({
30082
30625
  commandArgs: [
30083
30626
  "broadcast",
30084
30627
  "--network",
30085
30628
  String(network.chainId),
30086
30629
  "--nonce",
30087
- String(parseIntegerString(options.nonce, "nonce")),
30630
+ String(nonce),
30088
30631
  "--to",
30089
- assertAddress(options.to, "to"),
30632
+ to,
30090
30633
  "--value-wei",
30091
- parseBigIntString(options.valueWei, "valueWei").toString(),
30634
+ valueWei.toString(),
30092
30635
  "--data-hex",
30093
- assertHex(options.dataHex, "dataHex"),
30636
+ dataHex,
30094
30637
  "--gas-limit",
30095
- parsePositiveBigIntString(options.gasLimit, "gasLimit").toString(),
30638
+ gasLimit.toString(),
30096
30639
  "--max-fee-per-gas-wei",
30097
- parsePositiveBigIntString(options.maxFeePerGasWei, "maxFeePerGasWei").toString(),
30640
+ maxFeePerGasWei.toString(),
30098
30641
  "--max-priority-fee-per-gas-wei",
30099
- parseBigIntString(options.maxPriorityFeePerGasWei, "maxPriorityFeePerGasWei").toString(),
30642
+ maxPriorityFeePerGasWei.toString(),
30100
30643
  "--tx-type",
30101
30644
  options.txType,
30102
30645
  ...options.delegationEnabled ? ["--delegation-enabled"] : []
@@ -30105,9 +30648,28 @@ async function main() {
30105
30648
  config,
30106
30649
  asJson: options.json
30107
30650
  });
30108
- if (result) {
30109
- print2(result, options.json);
30651
+ if (!signed) {
30652
+ return;
30653
+ }
30654
+ if (!signed.raw_tx_hex) {
30655
+ throw new Error("Rust agent did not return raw_tx_hex for broadcast signing");
30110
30656
  }
30657
+ await assertSignedBroadcastTransactionMatchesRequest({
30658
+ rawTxHex: signed.raw_tx_hex,
30659
+ from: from14,
30660
+ to,
30661
+ chainId: network.chainId,
30662
+ nonce,
30663
+ allowHigherNonce: false,
30664
+ value: valueWei,
30665
+ data: dataHex,
30666
+ gasLimit,
30667
+ maxFeePerGas: maxFeePerGasWei,
30668
+ maxPriorityFeePerGas: maxPriorityFeePerGasWei,
30669
+ txType: options.txType
30670
+ });
30671
+ await broadcastRawTransaction2(rpcUrl, signed.raw_tx_hex);
30672
+ print2(signed, options.json);
30111
30673
  });
30112
30674
  const rpc = program2.command("rpc").description("RPC methods implemented in TypeScript");
30113
30675
  rpc.command("chain").option("--rpc-url <url>", "Ethereum RPC URL").option("--json", "Print JSON output", false).action(async (options) => {
@@ -30319,7 +30881,7 @@ async function main() {
30319
30881
  to,
30320
30882
  chainId,
30321
30883
  nonce,
30322
- allowHigherNonce: true,
30884
+ allowHigherNonce: false,
30323
30885
  value: valueWei,
30324
30886
  data: dataHex,
30325
30887
  gasLimit,