@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
@@ -41,6 +41,7 @@ import {
41
41
  resolveWalletSetupCleanupAction,
42
42
  type WalletSetupPlan,
43
43
  } from './wallet-setup.js';
44
+ import { promptHiddenTty } from './hidden-tty-prompt.js';
44
45
 
45
46
  const DEFAULT_LAUNCH_DAEMON_LABEL = 'com.wlfi.agent.daemon';
46
47
  const DEFAULT_SIGNER_BACKEND = 'software';
@@ -69,6 +70,7 @@ interface AdminSetupOptions {
69
70
  nonInteractive?: boolean;
70
71
  plan?: boolean;
71
72
  yes?: boolean;
73
+ reuseExistingWallet?: boolean;
72
74
  printAgentAuthToken?: boolean;
73
75
  daemonSocket?: string;
74
76
  perTxMaxWei?: string;
@@ -170,6 +172,12 @@ export interface ExistingWalletSetupTarget {
170
172
  hasLegacyAgentAuthToken: boolean;
171
173
  }
172
174
 
175
+ interface ReusableWalletSetupTarget {
176
+ address?: string;
177
+ existingVaultKeyId: string;
178
+ existingVaultPublicKey: string;
179
+ }
180
+
173
181
  interface ConfirmAdminSetupOverwriteDeps {
174
182
  prompt?: (query: string) => Promise<string>;
175
183
  stderr?: Pick<NodeJS.WriteStream, 'write'>;
@@ -203,34 +211,13 @@ async function readTrimmedStdin(label: string): Promise<string> {
203
211
  return validateSecret(raw.replace(/[\r\n]+$/u, ''), label);
204
212
  }
205
213
 
206
- async function promptHidden(query: string): Promise<string> {
207
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
208
- throw new Error('vault password is required; use --vault-password-stdin or a local TTY prompt');
209
- }
210
-
211
- const rl = readline.createInterface({
212
- input: process.stdin,
213
- output: process.stdout,
214
- terminal: true,
215
- }) as readline.Interface & { stdoutMuted?: boolean; _writeToOutput?: (value: string) => void };
216
-
217
- rl.stdoutMuted = true;
218
- rl._writeToOutput = (value: string) => {
219
- if (value.includes(query)) {
220
- (rl as unknown as { output: NodeJS.WritableStream }).output.write(value);
221
- return;
222
- }
223
- if (!rl.stdoutMuted) {
224
- (rl as unknown as { output: NodeJS.WritableStream }).output.write(value);
225
- }
226
- };
227
-
228
- const answer = await new Promise<string>((resolve) => {
229
- rl.question(query, resolve);
230
- });
231
- rl.close();
232
- process.stdout.write('\n');
233
- return validateSecret(answer, 'vault password');
214
+ async function promptHidden(
215
+ query: string,
216
+ label = 'vault password',
217
+ nonInteractiveError = 'vault password is required; use --vault-password-stdin or a local TTY prompt',
218
+ ): Promise<string> {
219
+ const answer = await promptHiddenTty(query, nonInteractiveError);
220
+ return validateSecret(answer, label);
234
221
  }
235
222
 
236
223
  async function promptVisible(query: string, nonInteractiveError: string): Promise<string> {
@@ -260,6 +247,7 @@ function deriveWalletAddress(vaultPublicKey: string | undefined): string | undef
260
247
  return publicKeyToAddress(
261
248
  (normalized.startsWith('0x') ? normalized : `0x${normalized}`) as Hex,
262
249
  );
250
+ /* c8 ignore next 2 -- viem currently normalizes malformed public-key strings instead of throwing; catch is defensive */
263
251
  } catch {
264
252
  return undefined;
265
253
  }
@@ -284,8 +272,33 @@ export function resolveExistingWalletSetupTarget(
284
272
  };
285
273
  }
286
274
 
275
+ function resolveReusableWalletSetupTarget(config: WlfiConfig): ReusableWalletSetupTarget {
276
+ let walletProfile: ReturnType<typeof resolveWalletProfile>;
277
+ try {
278
+ walletProfile = resolveWalletProfile(config);
279
+ } catch (error) {
280
+ throw new Error(
281
+ `--reuse-existing-wallet requires a local wallet to reuse; ${renderError(error)}`,
282
+ );
283
+ }
284
+
285
+ const existingVaultKeyId = walletProfile.vaultKeyId?.trim();
286
+ const existingVaultPublicKey = walletProfile.vaultPublicKey?.trim();
287
+ if (!existingVaultKeyId || !existingVaultPublicKey) {
288
+ throw new Error(
289
+ '--reuse-existing-wallet requires wallet.vaultKeyId and wallet.vaultPublicKey in the local wallet profile',
290
+ );
291
+ }
292
+
293
+ return {
294
+ address: walletProfile.address?.trim() || deriveWalletAddress(existingVaultPublicKey),
295
+ existingVaultKeyId,
296
+ existingVaultPublicKey,
297
+ };
298
+ }
299
+
287
300
  export async function confirmAdminSetupOverwrite(
288
- options: Pick<AdminSetupOptions, 'yes' | 'nonInteractive'>,
301
+ options: Pick<AdminSetupOptions, 'yes' | 'nonInteractive' | 'reuseExistingWallet'>,
289
302
  config: WlfiConfig,
290
303
  deps: ConfirmAdminSetupOverwriteDeps = {},
291
304
  ): Promise<void> {
@@ -295,13 +308,17 @@ export async function confirmAdminSetupOverwrite(
295
308
  }
296
309
  if (options.nonInteractive) {
297
310
  throw new Error(
298
- '`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode',
311
+ options.reuseExistingWallet
312
+ ? '`wlfi-agent admin setup --reuse-existing-wallet` would refresh the existing local wallet metadata and agent credentials; rerun with --yes in non-interactive mode'
313
+ : '`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode',
299
314
  );
300
315
  }
301
316
 
302
317
  const stderr = deps.stderr ?? process.stderr;
303
318
  stderr.write(
304
- 'warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n',
319
+ options.reuseExistingWallet
320
+ ? 'warning: admin setup will reuse the current vault and refresh the local wallet metadata and agent credentials.\n'
321
+ : 'warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n',
305
322
  );
306
323
  if (existing.address) {
307
324
  stderr.write(`current address: ${existing.address}\n`);
@@ -313,15 +330,21 @@ export async function confirmAdminSetupOverwrite(
313
330
  stderr.write('legacy agent auth token is still present in config.json\n');
314
331
  }
315
332
 
333
+ const confirmationToken = options.reuseExistingWallet ? 'REUSE' : 'OVERWRITE';
334
+ const confirmationPrompt = options.reuseExistingWallet
335
+ ? 'Type REUSE to reattach the current local vault: '
336
+ : 'Type OVERWRITE to replace the current local wallet: ';
316
337
  const confirmation = await (
317
338
  deps.prompt ??
318
339
  ((query: string) =>
319
340
  promptVisible(
320
341
  query,
321
- '`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet',
342
+ options.reuseExistingWallet
343
+ ? '`wlfi-agent admin setup --reuse-existing-wallet` requires --yes in non-interactive environments when reusing an existing wallet'
344
+ : '`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet',
322
345
  ))
323
- )('Type OVERWRITE to replace the current local wallet: ');
324
- if (confirmation !== 'OVERWRITE') {
346
+ )(confirmationPrompt);
347
+ if (confirmation !== confirmationToken) {
325
348
  throw new Error('admin setup aborted');
326
349
  }
327
350
  }
@@ -357,7 +380,9 @@ export async function resolveAdminSetupVaultPassword(
357
380
  'vault password is required in non-interactive mode; use --vault-password-stdin',
358
381
  );
359
382
  }
360
- return promptForVaultPassword('Vault password (input hidden; nothing will be echoed): ');
383
+ return promptForVaultPassword(
384
+ 'Vault password (input hidden; this unlocks the wallet, not sudo): ',
385
+ );
361
386
  }
362
387
 
363
388
  function resolveDaemonSocket(optionValue: string | undefined): string {
@@ -368,6 +393,35 @@ function resolveStateFile(): string {
368
393
  return path.resolve(DEFAULT_MANAGED_STATE_FILE);
369
394
  }
370
395
 
396
+ function resolveDefaultActiveChainForFreshSetup(
397
+ config: WlfiConfig,
398
+ ): { chainId: number; chainName: string; rpcUrl?: string } | null {
399
+ try {
400
+ const profile = resolveCliNetworkProfile('bsc', config);
401
+ return {
402
+ chainId: profile.chainId,
403
+ chainName: profile.key?.trim() || profile.name.trim().toLowerCase() || 'bsc',
404
+ ...(profile.rpcUrl?.trim() ? { rpcUrl: profile.rpcUrl.trim() } : {}),
405
+ };
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+
411
+ function shouldSeedDefaultActiveChain(
412
+ options: Pick<AdminSetupOptions, 'network' | 'rpcUrl' | 'chainName'>,
413
+ config: WlfiConfig,
414
+ ): boolean {
415
+ return (
416
+ !options.network &&
417
+ !options.rpcUrl &&
418
+ !options.chainName &&
419
+ config.chainId === undefined &&
420
+ !config.chainName?.trim() &&
421
+ !config.rpcUrl?.trim()
422
+ );
423
+ }
424
+
371
425
  export function createAdminSetupPlan(
372
426
  options: AdminSetupOptions,
373
427
  deps: CreateAdminSetupPlanDeps = {},
@@ -398,6 +452,9 @@ export function createAdminSetupPlan(
398
452
  }
399
453
 
400
454
  const existingWallet = resolveExistingWalletSetupTarget(config);
455
+ const reusableWallet = options.reuseExistingWallet
456
+ ? resolveReusableWalletSetupTarget(config)
457
+ : null;
401
458
 
402
459
  return {
403
460
  command: 'setup',
@@ -442,6 +499,8 @@ export function createAdminSetupPlan(
442
499
  recipient: options.recipient,
443
500
  attachPolicyId: options.attachPolicyId,
444
501
  attachBootstrapPolicies: options.attachBootstrapPolicies,
502
+ existingVaultKeyId: reusableWallet?.existingVaultKeyId,
503
+ existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
445
504
  bootstrapOutputPath: options.bootstrapOutput,
446
505
  deleteBootstrapOutput: options.deleteBootstrapOutput,
447
506
  },
@@ -851,7 +910,9 @@ async function daemonAcceptsVaultPassword(
851
910
  const sudoSession = createSudoSession({
852
911
  promptPassword: async () =>
853
912
  await promptHidden(
854
- 'Root password (input hidden; required to install or recover the root daemon): ',
913
+ 'macOS admin password for sudo (input hidden; required to install or recover the root daemon): ',
914
+ 'macOS admin password for sudo',
915
+ 'macOS admin password for sudo is required; rerun on a local TTY',
855
916
  ),
856
917
  });
857
918
 
@@ -873,6 +934,158 @@ async function managedStateFileExists(stateFile: string): Promise<boolean> {
873
934
  );
874
935
  }
875
936
 
937
+ async function inspectManagedState(
938
+ stateFile: string,
939
+ showProgress: boolean,
940
+ message = 'Inspecting managed daemon state',
941
+ ): Promise<boolean> {
942
+ await sudoSession.prime();
943
+ const stateProbeProgress = createProgress(message, showProgress);
944
+ let stateExists: boolean;
945
+ try {
946
+ stateExists = await managedStateFileExists(stateFile);
947
+ stateProbeProgress.succeed(
948
+ stateExists ? 'Managed daemon state already exists' : 'No managed daemon state found',
949
+ );
950
+ } catch (error) {
951
+ stateProbeProgress.fail();
952
+ throw error;
953
+ }
954
+ return stateExists;
955
+ }
956
+
957
+ function createManagedStatePasswordMismatchError(stateFile: string): Error {
958
+ return new Error(
959
+ `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.`,
960
+ );
961
+ }
962
+
963
+ function isManagedStatePasswordMismatch(output: string): boolean {
964
+ return /failed to decrypt state|wrong password or tampered file|authentication failed/iu.test(
965
+ output,
966
+ );
967
+ }
968
+
969
+ async function managedStateAcceptsRequestedVaultPassword(
970
+ config: WlfiConfig,
971
+ daemonSocket: string,
972
+ stateFile: string,
973
+ vaultPassword: string,
974
+ ): Promise<boolean> {
975
+ const installPreconditions = assertManagedDaemonInstallPreconditions(
976
+ config,
977
+ daemonSocket,
978
+ stateFile,
979
+ );
980
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wlfi-managed-state-probe-'));
981
+ const tempSocket = path.join(tempRoot, 'daemon.sock');
982
+ const allowedUid = String(process.getuid?.() ?? process.geteuid?.() ?? 0);
983
+ const probeScript = [
984
+ 'set -euo pipefail',
985
+ 'vault_password="$(cat)"',
986
+ 'daemon_bin="$1"',
987
+ 'state_file="$2"',
988
+ 'daemon_socket="$3"',
989
+ 'admin_uid="$4"',
990
+ 'agent_uid="$5"',
991
+ 'signer_backend="$6"',
992
+ '"$daemon_bin" \\',
993
+ ' --non-interactive \\',
994
+ ' --vault-password-stdin \\',
995
+ ' --state-file "$state_file" \\',
996
+ ' --daemon-socket "$daemon_socket" \\',
997
+ ' --signer-backend "$signer_backend" \\',
998
+ ' --allow-admin-euid "$admin_uid" \\',
999
+ ' --allow-agent-euid "$agent_uid" <<<"$vault_password" &',
1000
+ 'child="$!"',
1001
+ 'for _ in $(seq 1 20); do',
1002
+ ' if ! kill -0 "$child" 2>/dev/null; then',
1003
+ ' wait "$child"',
1004
+ ' exit $?',
1005
+ ' fi',
1006
+ ' sleep 0.25',
1007
+ 'done',
1008
+ 'kill "$child" >/dev/null 2>&1 || true',
1009
+ 'wait "$child" >/dev/null 2>&1 || true',
1010
+ 'exit 0',
1011
+ ].join('\n');
1012
+
1013
+ try {
1014
+ const result = await sudoSession.run(
1015
+ [
1016
+ '/bin/bash',
1017
+ '-lc',
1018
+ probeScript,
1019
+ '--',
1020
+ installPreconditions.daemonBin,
1021
+ stateFile,
1022
+ tempSocket,
1023
+ allowedUid,
1024
+ allowedUid,
1025
+ DEFAULT_SIGNER_BACKEND,
1026
+ ],
1027
+ {
1028
+ stdin: `${vaultPassword}\n`,
1029
+ },
1030
+ );
1031
+ if (result.code === 0) {
1032
+ return true;
1033
+ }
1034
+
1035
+ const combinedOutput = `${result.stderr}\n${result.stdout}`.trim();
1036
+ if (isManagedStatePasswordMismatch(combinedOutput)) {
1037
+ return false;
1038
+ }
1039
+ throw new Error(
1040
+ combinedOutput ||
1041
+ `failed to validate the managed daemon state with the requested vault password (exit code ${result.code})`,
1042
+ );
1043
+ } finally {
1044
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1045
+ }
1046
+ }
1047
+
1048
+ async function assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(
1049
+ config: WlfiConfig,
1050
+ daemonSocket: string,
1051
+ stateFile: string,
1052
+ vaultPassword: string,
1053
+ showProgress: boolean,
1054
+ ): Promise<void> {
1055
+ const stateExists = await inspectManagedState(
1056
+ stateFile,
1057
+ showProgress,
1058
+ 'Inspecting managed daemon state before install',
1059
+ );
1060
+ if (!stateExists) {
1061
+ return;
1062
+ }
1063
+
1064
+ const verifyProgress = createProgress(
1065
+ 'Verifying the requested vault password against the managed daemon state',
1066
+ showProgress,
1067
+ );
1068
+ let accepted: boolean;
1069
+ try {
1070
+ accepted = await managedStateAcceptsRequestedVaultPassword(
1071
+ config,
1072
+ daemonSocket,
1073
+ stateFile,
1074
+ vaultPassword,
1075
+ );
1076
+ } catch (error) {
1077
+ verifyProgress.fail();
1078
+ throw error;
1079
+ }
1080
+
1081
+ if (!accepted) {
1082
+ verifyProgress.fail('Existing daemon password does not unlock the stored daemon state');
1083
+ throw createManagedStatePasswordMismatchError(stateFile);
1084
+ }
1085
+
1086
+ verifyProgress.succeed('Requested vault password unlocks the existing managed daemon state');
1087
+ }
1088
+
876
1089
  async function waitForTrustedDaemonSocket(targetPath: string, timeoutMs = 15_000): Promise<void> {
877
1090
  async function canConnect(socketPath: string): Promise<boolean> {
878
1091
  return new Promise<boolean>((resolve) => {
@@ -1109,6 +1322,12 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1109
1322
  const config = readConfig();
1110
1323
  const daemonSocket = resolveDaemonSocket(options.daemonSocket);
1111
1324
  const stateFile = resolveStateFile();
1325
+ const defaultActiveChain = shouldSeedDefaultActiveChain(options, config)
1326
+ ? resolveDefaultActiveChainForFreshSetup(config)
1327
+ : null;
1328
+ const reusableWallet = options.reuseExistingWallet
1329
+ ? resolveReusableWalletSetupTarget(config)
1330
+ : null;
1112
1331
  assertManagedDaemonInstallPreconditions(config, daemonSocket, stateFile);
1113
1332
  await confirmAdminSetupOverwrite(options, config);
1114
1333
  const vaultPassword = await resolveAdminSetupVaultPassword(options);
@@ -1176,10 +1395,17 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1176
1395
  }
1177
1396
  if (!options.json && typeof process.geteuid === 'function' && process.geteuid() !== 0) {
1178
1397
  process.stderr.write(
1179
- 'Root password required: setup must install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n',
1398
+ 'macOS admin password required: setup uses sudo to install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n',
1180
1399
  );
1181
1400
  }
1182
1401
  await sudoSession.prime();
1402
+ await assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(
1403
+ config,
1404
+ daemonSocket,
1405
+ stateFile,
1406
+ vaultPassword,
1407
+ showProgress,
1408
+ );
1183
1409
  const installProgress = createProgress('Installing and restarting daemon', showProgress);
1184
1410
  try {
1185
1411
  daemon = await installLaunchDaemon(config, daemonSocket, stateFile, vaultPassword);
@@ -1212,29 +1438,15 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1212
1438
  if (!daemonAcceptedPassword) {
1213
1439
  if (existingDaemonRejectedPassword) {
1214
1440
  authProgress.fail('Existing daemon password does not unlock the stored daemon state');
1215
- throw new Error(
1216
- `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.`,
1217
- );
1441
+ throw createManagedStatePasswordMismatchError(stateFile);
1218
1442
  }
1219
1443
 
1220
1444
  authProgress.info('Daemon rejected the requested vault password; inspecting managed state');
1221
- const stateProbeProgress = createProgress('Inspecting managed daemon state', showProgress);
1222
- let stateExists: boolean;
1223
- try {
1224
- stateExists = await managedStateFileExists(stateFile);
1225
- stateProbeProgress.succeed(
1226
- stateExists ? 'Managed daemon state already exists' : 'No managed daemon state found',
1227
- );
1228
- } catch (error) {
1229
- stateProbeProgress.fail();
1230
- throw error;
1231
- }
1445
+ const stateExists = await inspectManagedState(stateFile, showProgress);
1232
1446
 
1233
1447
  if (stateExists) {
1234
1448
  authProgress.fail('Existing daemon password does not unlock the stored daemon state');
1235
- throw new Error(
1236
- `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.`,
1237
- );
1449
+ throw createManagedStatePasswordMismatchError(stateFile);
1238
1450
  }
1239
1451
 
1240
1452
  authProgress.fail(
@@ -1243,7 +1455,7 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1243
1455
 
1244
1456
  if (!options.json && typeof process.geteuid === 'function' && process.geteuid() !== 0) {
1245
1457
  process.stderr.write(
1246
- 'Root password required: setup must reinstall the root LaunchDaemon to rotate the managed daemon password.\n',
1458
+ 'macOS admin password required: setup uses sudo to reinstall the root LaunchDaemon and rotate the managed daemon password.\n',
1247
1459
  );
1248
1460
  }
1249
1461
  await sudoSession.prime();
@@ -1333,6 +1545,8 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1333
1545
  recipient: options.recipient,
1334
1546
  attachPolicyId: options.attachPolicyId,
1335
1547
  attachBootstrapPolicies: options.attachBootstrapPolicies,
1548
+ existingVaultKeyId: reusableWallet?.existingVaultKeyId,
1549
+ existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
1336
1550
  bootstrapOutputPath: bootstrapOutput.path,
1337
1551
  });
1338
1552
 
@@ -1406,6 +1620,13 @@ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1406
1620
  const persistedConfig = writeConfig({
1407
1621
  daemonSocket,
1408
1622
  stateFile,
1623
+ ...(defaultActiveChain
1624
+ ? {
1625
+ chainId: defaultActiveChain.chainId,
1626
+ chainName: defaultActiveChain.chainName,
1627
+ ...(defaultActiveChain.rpcUrl ? { rpcUrl: defaultActiveChain.rpcUrl } : {}),
1628
+ }
1629
+ : {}),
1409
1630
  });
1410
1631
 
1411
1632
  printCliPayload(
@@ -1460,6 +1681,8 @@ export function buildAdminSetupBootstrapInvocation(input: {
1460
1681
  recipient?: string;
1461
1682
  attachPolicyId?: string[];
1462
1683
  attachBootstrapPolicies?: boolean;
1684
+ existingVaultKeyId?: string;
1685
+ existingVaultPublicKey?: string;
1463
1686
  bootstrapOutputPath: string;
1464
1687
  }): { args: string[]; stdin: string } {
1465
1688
  return {
@@ -1481,6 +1704,8 @@ export function buildAdminSetupBootstrapInvocation(input: {
1481
1704
  recipient: input.recipient,
1482
1705
  attachPolicyId: input.attachPolicyId,
1483
1706
  attachBootstrapPolicies: input.attachBootstrapPolicies,
1707
+ existingVaultKeyId: input.existingVaultKeyId,
1708
+ existingVaultPublicKey: input.existingVaultPublicKey,
1484
1709
  bootstrapOutputPath: input.bootstrapOutputPath,
1485
1710
  }),
1486
1711
  stdin: `${validateSecret(input.vaultPassword, 'vault password')}\n`,
@@ -1552,6 +1777,11 @@ export async function runAdminSetupCli(argv: string[]): Promise<void> {
1552
1777
  .option('--vault-password-stdin', 'Read vault password from stdin', false)
1553
1778
  .option('--non-interactive', 'Disable password prompts', false)
1554
1779
  .option('-y, --yes', 'Skip the overwrite confirmation prompt', false)
1780
+ .option(
1781
+ '--reuse-existing-wallet',
1782
+ 'Reuse the current local vault instead of generating a fresh wallet',
1783
+ false,
1784
+ )
1555
1785
  .option('--daemon-socket <path>', 'Daemon unix socket path')
1556
1786
  .option('--per-tx-max-wei <wei>', 'Per-transaction max spend in wei')
1557
1787
  .option('--daily-max-wei <wei>', 'Daily max spend in wei')
@@ -62,8 +62,12 @@ function resolveConfiguredAgentKeyId(
62
62
  if (explicitAgentKeyId) {
63
63
  return undefined;
64
64
  }
65
+ const renderedError =
66
+ error instanceof Error
67
+ ? error.message
68
+ : /* c8 ignore next -- assertValidAgentKeyId throws Error objects */ String(error);
65
69
  throw new Error(
66
- (error instanceof Error ? error.message : String(error)) +
70
+ renderedError +
67
71
  '; pass --agent-key-id to migrate the legacy config secret explicitly'
68
72
  );
69
73
  }
@@ -94,6 +94,16 @@ export interface WaitForOnchainReceiptDeps {
94
94
  sleep?: (ms: number) => Promise<void>;
95
95
  }
96
96
 
97
+ export function resolveEstimatedPriorityFeePerGasWei(fees: BroadcastFeeEstimate): bigint {
98
+ const resolved = fees.maxPriorityFeePerGas ?? fees.gasPrice;
99
+ if (resolved === null || resolved <= 0n) {
100
+ throw new Error(
101
+ 'Could not determine maxPriorityFeePerGas; pass --max-priority-fee-per-gas-wei',
102
+ );
103
+ }
104
+ return resolved;
105
+ }
106
+
97
107
  export function encodeErc20TransferData(recipient: Address, amountWei: bigint): Hex {
98
108
  return encodeFunctionData({
99
109
  abi: erc20Abi,
@@ -128,14 +138,15 @@ export async function resolveAssetBroadcastPlan(
128
138
  const fees = await deps.estimateFees(input.rpcUrl);
129
139
  const resolvedFees = fees as BroadcastFeeEstimate;
130
140
  const maxFeePerGasWei = input.maxFeePerGasWei ?? (resolvedFees.maxFeePerGas ?? resolvedFees.gasPrice);
131
- const maxPriorityFeePerGasWei =
132
- input.maxPriorityFeePerGasWei
133
- ?? (resolvedFees.maxPriorityFeePerGas ?? resolvedFees.gasPrice ?? 0n);
134
141
 
135
142
  if (maxFeePerGasWei === null || maxFeePerGasWei <= 0n) {
136
143
  throw new Error('Could not determine maxFeePerGas; pass --max-fee-per-gas-wei');
137
144
  }
138
145
 
146
+ const maxPriorityFeePerGasWei =
147
+ input.maxPriorityFeePerGasWei
148
+ ?? resolveEstimatedPriorityFeePerGasWei(resolvedFees);
149
+
139
150
  return {
140
151
  rpcUrl: input.rpcUrl,
141
152
  chainId: input.chainId,
@@ -166,7 +177,7 @@ export async function completeAssetBroadcast(
166
177
  to: plan.to,
167
178
  chainId: plan.chainId,
168
179
  nonce: plan.nonce,
169
- allowHigherNonce: true,
180
+ allowHigherNonce: false,
170
181
  value: plan.valueWei,
171
182
  data: plan.dataHex,
172
183
  gasLimit: plan.gasLimit,
@@ -181,9 +181,11 @@ export function rewriteAmountPolicyErrorMessage(
181
181
  minAmountLiteral: string,
182
182
  minAmountWei: string | undefined,
183
183
  maxAmountWei: string,
184
- ) =>
185
- `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${
186
- minAmountLiteral === 'None' ? 'None' : `Some(${formatAmount(minAmountWei ?? '0')})`
187
- }..=${formatAmount(maxAmountWei)}`,
184
+ ) => {
185
+ const minAmountDisplay =
186
+ /* c8 ignore next -- both None and Some(...) paths are exercised, but c8 misattributes this ternary under --experimental-strip-types */
187
+ minAmountLiteral === 'None' ? 'None' : `Some(${formatAmount(minAmountWei ?? '0')})`;
188
+ return `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${minAmountDisplay}..=${formatAmount(maxAmountWei)}`;
189
+ },
188
190
  );
189
191
  }
@@ -0,0 +1 @@
1
+ export * from './hidden-tty-prompt.ts';