@wlfi-agent/cli 1.4.13 → 1.4.15

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 (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +45 -41
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1839
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1839
package/src/cli.ts ADDED
@@ -0,0 +1,2121 @@
1
+ import {
2
+ type ChainProfile,
3
+ defaultDaemonSocketPath,
4
+ deleteConfigKey,
5
+ ensureWlfiHome,
6
+ listBuiltinChains,
7
+ listBuiltinTokens,
8
+ listConfiguredChains,
9
+ listConfiguredTokens,
10
+ readConfig,
11
+ redactConfig,
12
+ removeChainProfile,
13
+ removeTokenChainProfile,
14
+ removeTokenProfile,
15
+ resolveChainProfile,
16
+ resolveConfigPath,
17
+ resolveTokenProfile,
18
+ saveChainProfile,
19
+ saveTokenChainProfile,
20
+ switchActiveChain,
21
+ type TokenChainProfile,
22
+ type WlfiConfig,
23
+ writeConfig,
24
+ } from '@wlfi-agent/config';
25
+ import {
26
+ broadcastRawTransaction,
27
+ estimateFees,
28
+ estimateGas,
29
+ getAccountSnapshot,
30
+ getChainInfo,
31
+ getCodeAtAddress,
32
+ getLatestBlockNumber,
33
+ getNativeBalance,
34
+ getNonce,
35
+ getTokenBalance,
36
+ getTransactionByHash,
37
+ getTransactionReceiptByHash,
38
+ } from '@wlfi-agent/rpc';
39
+ import { Command, Option } from 'commander';
40
+ import { type Address, type Hex, isAddress, isHex } from 'viem';
41
+ import { assertSafeRpcUrl } from '../packages/config/src/index.js';
42
+ import {
43
+ blockedRawAdminPassthroughMessage,
44
+ rewriteAdminHelpText,
45
+ } from './lib/admin-passthrough.js';
46
+ import { runAdminResetCli, runAdminUninstallCli } from './lib/admin-reset.js';
47
+ import { runAdminSetupCli, runAdminTuiCli } from './lib/admin-setup.js';
48
+ import { resolveAgentAuthToken } from './lib/agent-auth.js';
49
+ import { clearAgentAuthToken } from './lib/agent-auth-clear.js';
50
+ import { migrateLegacyAgentAuthToken } from './lib/agent-auth-migrate.js';
51
+ import {
52
+ buildRevokeAgentKeyAdminArgs,
53
+ completeAgentKeyRevocation,
54
+ type RevokeAgentKeyAdminOutput,
55
+ } from './lib/agent-auth-revoke.js';
56
+ import {
57
+ buildRotateAgentAuthTokenAdminArgs,
58
+ completeAgentAuthRotation,
59
+ type RotateAgentAuthTokenAdminOutput,
60
+ } from './lib/agent-auth-rotate.js';
61
+ import { assertValidAgentAuthToken } from './lib/agent-auth-token.js';
62
+ import {
63
+ completeAssetBroadcast,
64
+ encodeErc20ApproveData,
65
+ encodeErc20TransferData,
66
+ formatBroadcastedAssetOutput,
67
+ resolveAssetBroadcastPlan,
68
+ waitForOnchainReceipt,
69
+ } from './lib/asset-broadcast.js';
70
+ import {
71
+ assertBootstrapSetupSummaryLeaseIsActive,
72
+ deleteBootstrapAgentCredentialsFile,
73
+ readBootstrapAgentCredentialsFile,
74
+ readBootstrapSetupSummaryFile,
75
+ redactBootstrapAgentCredentialsFile,
76
+ } from './lib/bootstrap-credentials.js';
77
+ import {
78
+ normalizeAgentAmountOutput,
79
+ normalizePositiveDecimalInput,
80
+ parseConfiguredAmount,
81
+ resolveConfiguredErc20Asset,
82
+ resolveConfiguredNativeAsset,
83
+ rewriteAmountPolicyErrorMessage,
84
+ } from './lib/config-amounts.js';
85
+ import {
86
+ assertWritableConfigKey,
87
+ resolveConfigMutationCommandLabel,
88
+ type WritableConfigKey,
89
+ } from './lib/config-mutation.js';
90
+ import {
91
+ assertTrustedAdminDaemonSocketPath,
92
+ assertTrustedDaemonSocketPath,
93
+ } from './lib/fs-trust.js';
94
+ import {
95
+ AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE,
96
+ hasAgentAuthTokenInKeychain,
97
+ readAgentAuthTokenFromKeychain,
98
+ storeAgentAuthTokenInKeychain,
99
+ } from './lib/keychain.js';
100
+ import {
101
+ withDynamicLocalAdminMutationAccess,
102
+ withLocalAdminMutationAccess,
103
+ } from './lib/local-admin-access.js';
104
+ import { resolveCliNetworkProfile, resolveCliRpcUrl } from './lib/network-selection.js';
105
+ import { assertRpcChainIdMatches } from './lib/rpc-guard.js';
106
+ import {
107
+ passthroughRustBinary,
108
+ RustBinaryExitError,
109
+ runRustBinary,
110
+ runRustBinaryJson,
111
+ } from './lib/rust.js';
112
+ import { assertSignedBroadcastTransactionMatchesRequest } from './lib/signed-tx.js';
113
+ import { registerRepairCommand, registerStatusCommand } from './lib/status-repair-cli.js';
114
+ import {
115
+ formatWalletProfileText,
116
+ resolveWalletAddress,
117
+ resolveWalletProfileWithBalances,
118
+ walletProfileFromBootstrapSummary,
119
+ } from './lib/wallet-profile.js';
120
+
121
+ interface RustBroadcastOutput {
122
+ command: string;
123
+ network: string;
124
+ asset: string;
125
+ counterparty: string;
126
+ amount_wei: string;
127
+ estimated_max_gas_spend_wei?: string;
128
+ tx_type?: string;
129
+ delegation_enabled?: boolean;
130
+ signature_hex: string;
131
+ r_hex?: string;
132
+ s_hex?: string;
133
+ v?: number;
134
+ raw_tx_hex?: string;
135
+ tx_hash_hex?: string;
136
+ }
137
+
138
+ interface RustManualApprovalRequiredOutput {
139
+ command: string;
140
+ approval_request_id: string;
141
+ relay_url?: string;
142
+ frontend_url?: string;
143
+ cli_approval_command: string;
144
+ }
145
+
146
+ function rewriteAgentAmountError(
147
+ error: unknown,
148
+ symbolAwareAsset: { decimals: number; symbol: string; assetId: string },
149
+ ): Error {
150
+ if (!(error instanceof Error)) {
151
+ return new Error(String(error));
152
+ }
153
+
154
+ const rewritten = rewriteAmountPolicyErrorMessage(error.message, symbolAwareAsset);
155
+ if (rewritten === error.message) {
156
+ return error;
157
+ }
158
+
159
+ return new Error(rewritten);
160
+ }
161
+
162
+ function isManualApprovalRequiredOutput(value: unknown): value is RustManualApprovalRequiredOutput {
163
+ if (!value || typeof value !== 'object') {
164
+ return false;
165
+ }
166
+ const candidate = value as Partial<RustManualApprovalRequiredOutput>;
167
+ return (
168
+ typeof candidate.command === 'string' &&
169
+ typeof candidate.approval_request_id === 'string' &&
170
+ typeof candidate.cli_approval_command === 'string'
171
+ );
172
+ }
173
+
174
+ function printManualApprovalRequired(output: RustManualApprovalRequiredOutput, asJson: boolean) {
175
+ if (asJson) {
176
+ print(output, true);
177
+ return;
178
+ }
179
+
180
+ const lines = [
181
+ `Command: ${output.command}`,
182
+ `Approval Request ID: ${output.approval_request_id}`,
183
+ ];
184
+ if (output.frontend_url) {
185
+ lines.push(`Frontend Approval URL: ${output.frontend_url}`);
186
+ }
187
+ if (output.relay_url) {
188
+ lines.push(`Relay URL: ${output.relay_url}`);
189
+ }
190
+ lines.push(`CLI Approval Command: ${output.cli_approval_command}`);
191
+ print(lines.join('\n'), false);
192
+ }
193
+
194
+ const MAX_SECRET_STDIN_BYTES = 16 * 1024;
195
+ const AGENT_KEY_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
196
+
197
+ function requiredString(value: string | undefined, label: string): string {
198
+ if (!value?.trim()) {
199
+ throw new Error(`${label} is required`);
200
+ }
201
+ return value;
202
+ }
203
+
204
+ function assertAgentKeyId(value: string | undefined, label = 'agentKeyId'): string {
205
+ const normalized = requiredString(value, label).trim();
206
+ if (!AGENT_KEY_ID_PATTERN.test(normalized)) {
207
+ throw new Error(`${label} must be a valid UUID`);
208
+ }
209
+ return normalized;
210
+ }
211
+
212
+ function hasStoredAgentAuthToken(agentKeyId: string | undefined): boolean {
213
+ if (!agentKeyId) {
214
+ return false;
215
+ }
216
+
217
+ try {
218
+ return hasAgentAuthTokenInKeychain(agentKeyId);
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+
224
+ function resolveRpcUrl(value: string | undefined, config: WlfiConfig): string {
225
+ return assertSafeRpcUrl(requiredString(value ?? config.rpcUrl, 'rpcUrl'), 'rpcUrl');
226
+ }
227
+
228
+ function resolveDaemonSocket(value: string | undefined, config: WlfiConfig): string {
229
+ if (value !== undefined) {
230
+ return requiredString(value, 'daemonSocket').trim();
231
+ }
232
+ if (config.daemonSocket !== undefined) {
233
+ return requiredString(config.daemonSocket, 'configured daemonSocket').trim();
234
+ }
235
+ return defaultDaemonSocketPath();
236
+ }
237
+
238
+ function assertAddress(value: string, label: string): Address {
239
+ if (!isAddress(value)) {
240
+ throw new Error(`${label} must be a valid EVM address`);
241
+ }
242
+ return value as Address;
243
+ }
244
+
245
+ function assertHex(value: string, label: string): Hex {
246
+ if (!isHex(value)) {
247
+ throw new Error(`${label} must be valid hex`);
248
+ }
249
+ return value as Hex;
250
+ }
251
+
252
+ function parseBigIntString(value: string, label: string): bigint {
253
+ const normalized = value.trim();
254
+ if (!/^(0|[1-9][0-9]*)$/u.test(normalized)) {
255
+ throw new Error(`${label} must be a non-negative integer string`);
256
+ }
257
+ try {
258
+ return BigInt(normalized);
259
+ } catch {
260
+ throw new Error(`${label} must be a non-negative integer string`);
261
+ }
262
+ }
263
+
264
+ function parseIntegerString(value: string, label: string): number {
265
+ const normalized = value.trim();
266
+ if (!/^(0|[1-9][0-9]*)$/u.test(normalized)) {
267
+ throw new Error(`${label} must be a non-negative integer`);
268
+ }
269
+ const parsed = Number(normalized);
270
+ if (!Number.isSafeInteger(parsed)) {
271
+ throw new Error(`${label} must be a safe integer`);
272
+ }
273
+ return parsed;
274
+ }
275
+
276
+ function parsePositiveBigIntString(value: string, label: string): bigint {
277
+ const parsed = parseBigIntString(value, label);
278
+ if (parsed <= 0n) {
279
+ throw new Error(`${label} must be greater than zero`);
280
+ }
281
+ return parsed;
282
+ }
283
+
284
+ function parsePositiveIntegerString(value: string, label: string): number {
285
+ const parsed = parseIntegerString(value, label);
286
+ if (parsed <= 0) {
287
+ throw new Error(`${label} must be greater than zero`);
288
+ }
289
+ return parsed;
290
+ }
291
+
292
+ async function readTrimmedStdin(label: string): Promise<string> {
293
+ process.stdin.setEncoding('utf8');
294
+ let raw = '';
295
+ for await (const chunk of process.stdin) {
296
+ raw += chunk;
297
+ if (Buffer.byteLength(raw, 'utf8') > MAX_SECRET_STDIN_BYTES) {
298
+ throw new Error(`${label} must not exceed ${MAX_SECRET_STDIN_BYTES} bytes`);
299
+ }
300
+ }
301
+ return requiredString(raw.replace(/[\r\n]+$/u, ''), label);
302
+ }
303
+
304
+ function formatJson(payload: unknown) {
305
+ return JSON.stringify(payload, null, 2);
306
+ }
307
+
308
+ function stringifyOptionalValue(value: { toString(): string } | null | undefined): string | null {
309
+ return value === null || value === undefined ? null : value.toString();
310
+ }
311
+
312
+ function print(payload: unknown, asJson: boolean) {
313
+ if (asJson) {
314
+ console.log(formatJson(payload));
315
+ return;
316
+ }
317
+ if (typeof payload === 'string') {
318
+ console.log(payload);
319
+ return;
320
+ }
321
+ console.log(formatJson(payload));
322
+ }
323
+
324
+ const ONCHAIN_RECEIPT_TIMEOUT_MS = 30_000;
325
+ const ONCHAIN_RECEIPT_POLL_INTERVAL_MS = 2_000;
326
+
327
+ async function reportOnchainReceiptStatus(input: {
328
+ rpcUrl: string;
329
+ txHash: Hex;
330
+ asJson: boolean;
331
+ }): Promise<void> {
332
+ if (input.asJson) {
333
+ console.error(
334
+ formatJson({
335
+ event: 'onchainReceiptPending',
336
+ txHash: input.txHash,
337
+ timeoutMs: ONCHAIN_RECEIPT_TIMEOUT_MS,
338
+ }),
339
+ );
340
+ } else {
341
+ console.error(
342
+ `Waiting up to ${ONCHAIN_RECEIPT_TIMEOUT_MS / 1000}s for on-chain receipt: ${input.txHash}`,
343
+ );
344
+ }
345
+
346
+ try {
347
+ const result = await waitForOnchainReceipt(
348
+ {
349
+ rpcUrl: input.rpcUrl,
350
+ txHash: input.txHash,
351
+ timeoutMs: ONCHAIN_RECEIPT_TIMEOUT_MS,
352
+ intervalMs: ONCHAIN_RECEIPT_POLL_INTERVAL_MS,
353
+ },
354
+ {
355
+ getTransactionReceiptByHash,
356
+ },
357
+ );
358
+
359
+ if (result.receipt) {
360
+ const summary = {
361
+ event: 'onchainReceipt',
362
+ txHash: input.txHash,
363
+ blockNumber: stringifyOptionalValue(result.receipt.blockNumber),
364
+ transactionIndex: result.receipt.transactionIndex,
365
+ status: result.receipt.status,
366
+ };
367
+ if (input.asJson) {
368
+ console.error(formatJson(summary));
369
+ } else {
370
+ console.error(
371
+ `On-chain receipt: ${result.receipt.status} block ${result.receipt.blockNumber} txIndex ${result.receipt.transactionIndex}`,
372
+ );
373
+ }
374
+ return;
375
+ }
376
+
377
+ if (input.asJson) {
378
+ console.error(
379
+ formatJson({
380
+ event: 'onchainReceiptTimeout',
381
+ txHash: input.txHash,
382
+ timeoutMs: ONCHAIN_RECEIPT_TIMEOUT_MS,
383
+ }),
384
+ );
385
+ } else {
386
+ console.error(
387
+ `Timed out after ${ONCHAIN_RECEIPT_TIMEOUT_MS / 1000}s waiting for on-chain receipt`,
388
+ );
389
+ }
390
+ } catch (error) {
391
+ const message = error instanceof Error ? error.message : String(error);
392
+ if (input.asJson) {
393
+ console.error(
394
+ formatJson({
395
+ event: 'onchainReceiptPollingError',
396
+ txHash: input.txHash,
397
+ error: message,
398
+ }),
399
+ );
400
+ } else {
401
+ console.error(`On-chain receipt polling failed: ${message}`);
402
+ }
403
+ }
404
+ }
405
+
406
+ function parseConfigValue(key: WritableConfigKey, value: string): WlfiConfig {
407
+ if (key === 'chainId') {
408
+ return { [key]: parsePositiveIntegerString(value, key) };
409
+ }
410
+ if (key === 'agentKeyId') {
411
+ return { [key]: assertAgentKeyId(value, key) };
412
+ }
413
+ if (key === 'rpcUrl') {
414
+ return { [key]: assertSafeRpcUrl(value, key) };
415
+ }
416
+ return { [key]: value } as WlfiConfig;
417
+ }
418
+
419
+ function activeChainSummary(config: WlfiConfig) {
420
+ return {
421
+ chainId: config.chainId ?? null,
422
+ chainName: config.chainName ?? null,
423
+ rpcUrl: config.rpcUrl ?? null,
424
+ };
425
+ }
426
+
427
+ function normalizeChainProfile(
428
+ key: string,
429
+ options: { chainId: string; name?: string; rpcUrl?: string },
430
+ ): ChainProfile {
431
+ const rpcUrl = options.rpcUrl?.trim();
432
+ return {
433
+ chainId: parsePositiveIntegerString(options.chainId, 'chainId'),
434
+ name: options.name?.trim() || key.trim().toLowerCase(),
435
+ rpcUrl: rpcUrl ? assertSafeRpcUrl(rpcUrl, 'rpcUrl') : undefined,
436
+ };
437
+ }
438
+
439
+ interface AgentCommandAuthOptions {
440
+ agentKeyId?: string;
441
+ agentAuthToken?: string;
442
+ agentAuthTokenStdin?: boolean;
443
+ allowLegacyAgentAuthSource?: boolean;
444
+ daemonSocket?: string;
445
+ }
446
+
447
+ function warnForAgentAuthTokenSource(source: string, agentKeyId: string) {
448
+ if (source === 'argv') {
449
+ console.error(
450
+ 'warning: --agent-auth-token exposes secrets in shell history and process listings; prefer --agent-auth-token-stdin',
451
+ );
452
+ return;
453
+ }
454
+
455
+ if (source === 'config') {
456
+ console.error(
457
+ 'warning: agentAuthToken is being loaded from config.json; migrate it with `wlfi-agent config agent-auth set --agent-key-id ' +
458
+ agentKeyId +
459
+ ' --agent-auth-token-stdin`',
460
+ );
461
+ return;
462
+ }
463
+
464
+ if (source === 'env') {
465
+ console.error(
466
+ 'warning: WLFI_AGENT_AUTH_TOKEN exposes secrets to child processes and shell sessions; prefer macOS Keychain or --agent-auth-token-stdin',
467
+ );
468
+ }
469
+ }
470
+
471
+ async function resolveAgentCommandContext(
472
+ options: AgentCommandAuthOptions,
473
+ config: WlfiConfig,
474
+ ): Promise<{ agentKeyId: string; agentAuthToken: string; daemonSocket: string }> {
475
+ const agentKeyId = assertAgentKeyId(
476
+ options.agentKeyId ?? config.agentKeyId ?? process.env.WLFI_AGENT_KEY_ID,
477
+ 'agentKeyId',
478
+ );
479
+ const keychainAgentAuthToken = readAgentAuthTokenFromKeychain(agentKeyId);
480
+ const { token: agentAuthToken, source: agentAuthTokenSource } = await resolveAgentAuthToken({
481
+ agentKeyId,
482
+ cliToken: options.agentAuthToken,
483
+ cliTokenStdin: options.agentAuthTokenStdin,
484
+ keychainToken: keychainAgentAuthToken,
485
+ configToken: config.agentAuthToken,
486
+ envToken: process.env.WLFI_AGENT_AUTH_TOKEN,
487
+ allowLegacySource: options.allowLegacyAgentAuthSource,
488
+ readFromStdin: readTrimmedStdin,
489
+ });
490
+ const daemonSocket = resolveDaemonSocket(
491
+ options.daemonSocket ?? process.env.WLFI_DAEMON_SOCKET,
492
+ config,
493
+ );
494
+ assertTrustedDaemonSocketPath(daemonSocket);
495
+
496
+ warnForAgentAuthTokenSource(agentAuthTokenSource, agentKeyId);
497
+
498
+ return { agentKeyId, agentAuthToken, daemonSocket };
499
+ }
500
+
501
+ async function runAgentCommandJson<T>(input: {
502
+ commandArgs: string[];
503
+ auth: AgentCommandAuthOptions;
504
+ config: WlfiConfig;
505
+ asJson: boolean;
506
+ }): Promise<T | null> {
507
+ const { agentKeyId, agentAuthToken, daemonSocket } = await resolveAgentCommandContext(
508
+ input.auth,
509
+ input.config,
510
+ );
511
+
512
+ try {
513
+ return await runRustBinaryJson<T>(
514
+ 'wlfi-agent-agent',
515
+ [
516
+ '--json',
517
+ '--agent-key-id',
518
+ agentKeyId,
519
+ '--agent-auth-token-stdin',
520
+ '--daemon-socket',
521
+ daemonSocket,
522
+ ...input.commandArgs,
523
+ ],
524
+ input.config,
525
+ {
526
+ stdin: `${agentAuthToken}\n`,
527
+ preSuppliedSecretStdin: 'agentAuthToken',
528
+ scrubSensitiveEnv: true,
529
+ },
530
+ );
531
+ } catch (error) {
532
+ if (error instanceof RustBinaryExitError && error.stdout.trim()) {
533
+ try {
534
+ const parsed = JSON.parse(error.stdout) as unknown;
535
+ if (isManualApprovalRequiredOutput(parsed)) {
536
+ printManualApprovalRequired(parsed, input.asJson);
537
+ process.exitCode = error.code;
538
+ return null;
539
+ }
540
+ } catch {
541
+ // fall through to the original error
542
+ }
543
+ }
544
+ throw error;
545
+ }
546
+ }
547
+
548
+ function legacyAmountToDecimalString(value: number | undefined): string | undefined {
549
+ if (value === undefined) {
550
+ return undefined;
551
+ }
552
+ return value.toString();
553
+ }
554
+
555
+ function describeResolvedToken(token: NonNullable<ReturnType<typeof resolveTokenProfile>>) {
556
+ return {
557
+ key: token.key,
558
+ source: token.source,
559
+ symbol: token.symbol,
560
+ chains: Object.entries(token.chains ?? {})
561
+ .map(([key, value]) => ({ key, ...value }))
562
+ .sort((left, right) => left.chainId - right.chainId || left.key.localeCompare(right.key)),
563
+ };
564
+ }
565
+
566
+ function normalizeTokenChainConfig(
567
+ tokenKey: string,
568
+ chainKey: string,
569
+ options: {
570
+ symbol?: string;
571
+ chainId?: string;
572
+ native?: boolean;
573
+ address?: string;
574
+ decimals?: string;
575
+ perTx?: string;
576
+ daily?: string;
577
+ weekly?: string;
578
+ },
579
+ config: WlfiConfig,
580
+ ): { symbol: string; profile: TokenChainProfile } {
581
+ const normalizedTokenKey = tokenKey.trim().toLowerCase();
582
+ const normalizedChainKey = chainKey.trim().toLowerCase();
583
+ if (!normalizedTokenKey) {
584
+ throw new Error('token key is required');
585
+ }
586
+ if (!normalizedChainKey) {
587
+ throw new Error('chain key is required');
588
+ }
589
+ if (options.native && options.address) {
590
+ throw new Error('--native conflicts with --address');
591
+ }
592
+
593
+ const existingToken = resolveTokenProfile(normalizedTokenKey, config);
594
+ const existingChain = existingToken?.chains?.[normalizedChainKey];
595
+ const resolvedChain = resolveChainProfile(normalizedChainKey, config);
596
+ const symbol =
597
+ options.symbol?.trim() || existingToken?.symbol || normalizedTokenKey.toUpperCase();
598
+ const chainId = options.chainId
599
+ ? parsePositiveIntegerString(options.chainId, 'chainId')
600
+ : (existingChain?.chainId ?? resolvedChain?.chainId);
601
+ if (chainId === undefined) {
602
+ throw new Error(
603
+ `chainId is required; pass --chain-id or configure chain '${normalizedChainKey}' first`,
604
+ );
605
+ }
606
+
607
+ const isNative = options.native ? true : options.address ? false : existingChain?.isNative;
608
+ if (isNative === undefined) {
609
+ throw new Error('pass --native or --address for a new token chain');
610
+ }
611
+
612
+ const decimals =
613
+ options.decimals !== undefined
614
+ ? parseIntegerString(options.decimals, 'decimals')
615
+ : existingChain?.decimals;
616
+ if (decimals === undefined) {
617
+ throw new Error('decimals is required; pass --decimals or edit an existing token chain');
618
+ }
619
+
620
+ const address = isNative
621
+ ? undefined
622
+ : options.address
623
+ ? assertAddress(options.address, 'address')
624
+ : existingChain?.address;
625
+ if (!isNative && !address) {
626
+ throw new Error('address is required for a non-native token chain');
627
+ }
628
+
629
+ const hasPolicyOverride =
630
+ options.perTx !== undefined || options.daily !== undefined || options.weekly !== undefined;
631
+ const defaultPolicy =
632
+ hasPolicyOverride || existingChain?.defaultPolicy
633
+ ? {
634
+ perTxAmountDecimal:
635
+ options.perTx !== undefined
636
+ ? normalizePositiveDecimalInput(options.perTx, 'perTx')
637
+ : (existingChain?.defaultPolicy?.perTxAmountDecimal ??
638
+ legacyAmountToDecimalString(existingChain?.defaultPolicy?.perTxAmount)),
639
+ dailyAmountDecimal:
640
+ options.daily !== undefined
641
+ ? normalizePositiveDecimalInput(options.daily, 'daily')
642
+ : (existingChain?.defaultPolicy?.dailyAmountDecimal ??
643
+ legacyAmountToDecimalString(existingChain?.defaultPolicy?.dailyAmount)),
644
+ weeklyAmountDecimal:
645
+ options.weekly !== undefined
646
+ ? normalizePositiveDecimalInput(options.weekly, 'weekly')
647
+ : (existingChain?.defaultPolicy?.weeklyAmountDecimal ??
648
+ legacyAmountToDecimalString(existingChain?.defaultPolicy?.weeklyAmount)),
649
+ }
650
+ : undefined;
651
+
652
+ return {
653
+ symbol,
654
+ profile: {
655
+ chainId,
656
+ isNative,
657
+ decimals,
658
+ ...(address ? { address } : {}),
659
+ ...(defaultPolicy && Object.values(defaultPolicy).some((value) => value !== undefined)
660
+ ? { defaultPolicy }
661
+ : {}),
662
+ },
663
+ };
664
+ }
665
+
666
+ function buildAdminChainCommand(): Command {
667
+ const command = new Command('chain')
668
+ .description(
669
+ 'Manage active chain selection and chain profiles (mutations require verified root access)',
670
+ )
671
+ .showHelpAfterError();
672
+
673
+ command
674
+ .command('list')
675
+ .option('--json', 'Print JSON output', false)
676
+ .action((options) => {
677
+ const config = readConfig();
678
+ print(
679
+ {
680
+ active: activeChainSummary(config),
681
+ configured: listConfiguredChains(config),
682
+ builtin: listBuiltinChains(),
683
+ },
684
+ options.json,
685
+ );
686
+ });
687
+
688
+ command
689
+ .command('current')
690
+ .option('--json', 'Print JSON output', false)
691
+ .action(async (options) => {
692
+ const config = readConfig();
693
+ let rpc = null;
694
+ if (config.rpcUrl) {
695
+ try {
696
+ rpc = await getChainInfo(config.rpcUrl);
697
+ } catch {
698
+ rpc = null;
699
+ }
700
+ }
701
+ print(
702
+ {
703
+ active: activeChainSummary(config),
704
+ rpc,
705
+ },
706
+ options.json,
707
+ );
708
+ });
709
+
710
+ command
711
+ .command('add')
712
+ .argument('<key>')
713
+ .requiredOption('--chain-id <id>', 'Chain ID')
714
+ .option('--name <name>', 'Display name')
715
+ .option('--rpc-url <url>', 'Default RPC URL for this chain profile')
716
+ .option('--activate', 'Activate this profile immediately', false)
717
+ .option('--json', 'Print JSON output', false)
718
+ .action(
719
+ withLocalAdminMutationAccess('wlfi-agent admin chain add', (key: string, options) => {
720
+ const profile = normalizeChainProfile(key, options);
721
+ let updated = saveChainProfile(key, profile);
722
+ if (options.activate) {
723
+ updated = switchActiveChain(key, { rpcUrl: profile.rpcUrl });
724
+ }
725
+ print(
726
+ {
727
+ saved: { key: key.trim().toLowerCase(), ...profile },
728
+ active: activeChainSummary(updated),
729
+ },
730
+ options.json,
731
+ );
732
+ }),
733
+ );
734
+
735
+ command
736
+ .command('remove')
737
+ .argument('<key>')
738
+ .option('--json', 'Print JSON output', false)
739
+ .action(
740
+ withLocalAdminMutationAccess('wlfi-agent admin chain remove', (key: string, options) => {
741
+ const updated = removeChainProfile(key);
742
+ print(
743
+ {
744
+ removed: key.trim().toLowerCase(),
745
+ configured: listConfiguredChains(updated),
746
+ },
747
+ options.json,
748
+ );
749
+ }),
750
+ );
751
+
752
+ command
753
+ .command('switch')
754
+ .argument('<selector>')
755
+ .option('--rpc-url <url>', 'RPC URL to save as the active endpoint')
756
+ .option('--save', 'Persist the active selection as a named profile', false)
757
+ .option('--json', 'Print JSON output', false)
758
+ .action(
759
+ withLocalAdminMutationAccess('wlfi-agent admin chain switch', (selector: string, options) => {
760
+ const updated = switchActiveChain(selector, {
761
+ rpcUrl: options.rpcUrl,
762
+ persistProfile: options.save,
763
+ });
764
+ const profile = resolveChainProfile(selector, updated);
765
+ print(
766
+ {
767
+ selected: selector,
768
+ resolved: profile,
769
+ active: activeChainSummary(updated),
770
+ },
771
+ options.json,
772
+ );
773
+ }),
774
+ );
775
+
776
+ return command;
777
+ }
778
+
779
+ function buildAdminTokenCommand(): Command {
780
+ const command = new Command('token')
781
+ .description(
782
+ 'Manage shared token definitions and default policies (mutations require verified root access)',
783
+ )
784
+ .showHelpAfterError();
785
+
786
+ command
787
+ .command('list')
788
+ .option('--json', 'Print JSON output', false)
789
+ .action((options) => {
790
+ const config = readConfig();
791
+ print(
792
+ {
793
+ builtin: listBuiltinTokens(),
794
+ configured: listConfiguredTokens(config),
795
+ },
796
+ options.json,
797
+ );
798
+ });
799
+
800
+ command
801
+ .command('show')
802
+ .argument('<selector>')
803
+ .option('--json', 'Print JSON output', false)
804
+ .action((selector: string, options) => {
805
+ const config = readConfig();
806
+ const resolved = resolveTokenProfile(selector, config);
807
+ if (!resolved) {
808
+ throw new Error(`unknown token selector: ${selector}`);
809
+ }
810
+ print(describeResolvedToken(resolved), options.json);
811
+ });
812
+
813
+ command
814
+ .command('set-chain')
815
+ .argument('<tokenKey>')
816
+ .argument('<chainKey>')
817
+ .option('--symbol <symbol>', 'Token symbol')
818
+ .option('--chain-id <id>', 'Chain ID (defaults from the existing token or configured chain)')
819
+ .option('--native', 'Mark the token as the native asset on this chain', false)
820
+ .option('--address <address>', 'ERC-20 token contract address')
821
+ .option('--decimals <count>', 'Token decimals')
822
+ .option('--per-tx <amount>', 'Default per-transaction policy amount in token units')
823
+ .option('--daily <amount>', 'Default daily policy amount in token units')
824
+ .option('--weekly <amount>', 'Default weekly policy amount in token units')
825
+ .option('--json', 'Print JSON output', false)
826
+ .action(
827
+ withLocalAdminMutationAccess(
828
+ 'wlfi-agent admin token set-chain',
829
+ (tokenKey: string, chainKey: string, options) => {
830
+ const config = readConfig();
831
+ const { symbol, profile } = normalizeTokenChainConfig(
832
+ tokenKey,
833
+ chainKey,
834
+ options,
835
+ config,
836
+ );
837
+ const updated = saveTokenChainProfile(tokenKey, chainKey, profile, { symbol });
838
+ const resolved = resolveTokenProfile(tokenKey, updated);
839
+ if (!resolved) {
840
+ throw new Error(`failed to save token '${tokenKey}'`);
841
+ }
842
+ print(
843
+ {
844
+ saved: describeResolvedToken(resolved),
845
+ },
846
+ options.json,
847
+ );
848
+ },
849
+ ),
850
+ );
851
+
852
+ command
853
+ .command('remove')
854
+ .argument('<tokenKey>')
855
+ .option('--json', 'Print JSON output', false)
856
+ .action(
857
+ withLocalAdminMutationAccess('wlfi-agent admin token remove', (tokenKey: string, options) => {
858
+ const updated = removeTokenProfile(tokenKey);
859
+ print(
860
+ {
861
+ removed: tokenKey.trim().toLowerCase(),
862
+ configured: listConfiguredTokens(updated),
863
+ },
864
+ options.json,
865
+ );
866
+ }),
867
+ );
868
+
869
+ command
870
+ .command('remove-chain')
871
+ .argument('<tokenKey>')
872
+ .argument('<chainKey>')
873
+ .option('--json', 'Print JSON output', false)
874
+ .action(
875
+ withLocalAdminMutationAccess(
876
+ 'wlfi-agent admin token remove-chain',
877
+ (tokenKey: string, chainKey: string, options) => {
878
+ const updated = removeTokenChainProfile(tokenKey, chainKey);
879
+ const resolved = resolveTokenProfile(tokenKey, updated);
880
+ print(
881
+ {
882
+ removed: {
883
+ token: tokenKey.trim().toLowerCase(),
884
+ chain: chainKey.trim().toLowerCase(),
885
+ },
886
+ remaining: resolved ? describeResolvedToken(resolved) : null,
887
+ },
888
+ options.json,
889
+ );
890
+ },
891
+ ),
892
+ );
893
+
894
+ return command;
895
+ }
896
+
897
+ async function runLocalAdminCommand(forwarded: string[]): Promise<boolean> {
898
+ const target = forwarded[0] === 'help' ? forwarded[1] : forwarded[0];
899
+ if (target !== 'chain' && target !== 'token' && target !== 'daemon') {
900
+ return false;
901
+ }
902
+
903
+ const command =
904
+ target === 'chain'
905
+ ? buildAdminChainCommand()
906
+ : target === 'token'
907
+ ? buildAdminTokenCommand()
908
+ : buildAdminDaemonCommand();
909
+ const args = forwarded[0] === 'help' ? ['--help'] : forwarded.slice(1);
910
+ command.exitOverride();
911
+
912
+ try {
913
+ await command.parseAsync(args, { from: 'user' });
914
+ return true;
915
+ } catch (error) {
916
+ if (error && typeof error === 'object' && 'code' in error) {
917
+ const code = String((error as { code?: unknown }).code ?? '');
918
+ if (code === 'commander.helpDisplayed') {
919
+ return true;
920
+ }
921
+ }
922
+ throw error;
923
+ }
924
+ }
925
+
926
+ function buildAdminDaemonCommand(): Command {
927
+ return new Command()
928
+ .name('daemon')
929
+ .description('Daemon launch is managed by wlfi-agent admin setup')
930
+ .action(() => {
931
+ throw new Error(
932
+ 'Direct daemon execution is disabled. Use `wlfi-agent admin setup` to install and manage the daemon.',
933
+ );
934
+ });
935
+ }
936
+
937
+ async function main() {
938
+ ensureWlfiHome();
939
+ const program = new Command();
940
+ program
941
+ .name('wlfi-agent')
942
+ .description('Single entrypoint for WLFI agent admin and signing operations')
943
+ .showHelpAfterError();
944
+
945
+ const configCommand = program.command('config').description('Manage ~/.wlfi_agent configuration');
946
+ configCommand
947
+ .command('show')
948
+ .option('--json', 'Print JSON output', false)
949
+ .action((options) => {
950
+ const config = readConfig();
951
+ print(
952
+ {
953
+ ...redactConfig(config),
954
+ keychain: {
955
+ agentAuthTokenStored: hasStoredAgentAuthToken(config.agentKeyId),
956
+ service: process.platform === 'darwin' ? AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE : null,
957
+ },
958
+ },
959
+ options.json,
960
+ );
961
+ });
962
+ configCommand.command('path').action(() => {
963
+ console.log(resolveConfigPath());
964
+ });
965
+ configCommand
966
+ .command('set')
967
+ .argument('<key>')
968
+ .argument('<value>')
969
+ .action(
970
+ withDynamicLocalAdminMutationAccess(
971
+ (key: string, _value: string) => resolveConfigMutationCommandLabel('set', key),
972
+ (key: string, value: string) => {
973
+ const writableKey = assertWritableConfigKey(key);
974
+ const updated = writeConfig(parseConfigValue(writableKey, value));
975
+ print(redactConfig(updated), true);
976
+ },
977
+ ),
978
+ );
979
+ configCommand
980
+ .command('unset')
981
+ .argument('<key>')
982
+ .action(
983
+ withDynamicLocalAdminMutationAccess(
984
+ (key: string) => resolveConfigMutationCommandLabel('unset', key),
985
+ (key: string) => {
986
+ const writableKey = assertWritableConfigKey(key);
987
+ const updated = deleteConfigKey(writableKey);
988
+ print(redactConfig(updated), true);
989
+ },
990
+ ),
991
+ );
992
+
993
+ const agentAuthCommand = configCommand
994
+ .command('agent-auth')
995
+ .description('Manage the agent auth token in macOS Keychain');
996
+
997
+ agentAuthCommand
998
+ .command('set')
999
+ .requiredOption('--agent-key-id <uuid>', 'Agent key id')
1000
+ .option('--agent-auth-token <token>', 'Agent auth token')
1001
+ .option('--agent-auth-token-stdin', 'Read agent auth token from stdin', false)
1002
+ .option('--json', 'Print JSON output', false)
1003
+ .action(
1004
+ withLocalAdminMutationAccess('wlfi-agent config agent-auth set', async (options) => {
1005
+ if (options.agentAuthToken && options.agentAuthTokenStdin) {
1006
+ throw new Error('--agent-auth-token conflicts with --agent-auth-token-stdin');
1007
+ }
1008
+
1009
+ const agentKeyId = assertAgentKeyId(options.agentKeyId);
1010
+ const agentAuthToken = options.agentAuthTokenStdin
1011
+ ? assertValidAgentAuthToken(await readTrimmedStdin('agentAuthToken'), 'agentAuthToken')
1012
+ : assertValidAgentAuthToken(
1013
+ requiredString(options.agentAuthToken, 'agentAuthToken'),
1014
+ 'agentAuthToken',
1015
+ );
1016
+
1017
+ if (options.agentAuthToken) {
1018
+ console.error(
1019
+ 'warning: --agent-auth-token exposes secrets in shell history and process listings; prefer --agent-auth-token-stdin',
1020
+ );
1021
+ }
1022
+
1023
+ storeAgentAuthTokenInKeychain(agentKeyId, agentAuthToken);
1024
+
1025
+ let updated = writeConfig({ agentKeyId });
1026
+ if (updated.agentAuthToken !== undefined) {
1027
+ updated = deleteConfigKey('agentAuthToken');
1028
+ }
1029
+
1030
+ print(
1031
+ {
1032
+ agentKeyId: updated.agentKeyId ?? agentKeyId,
1033
+ keychain: {
1034
+ stored: true,
1035
+ service: AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE,
1036
+ },
1037
+ config: redactConfig(updated),
1038
+ },
1039
+ options.json,
1040
+ );
1041
+ }),
1042
+ );
1043
+
1044
+ agentAuthCommand
1045
+ .command('import')
1046
+ .description('Import agent credentials from a private admin bootstrap JSON file')
1047
+ .argument('<path>', 'Path to bootstrap JSON output with an unredacted agent auth token')
1048
+ .option('--keep-source', 'Keep the imported bootstrap file unchanged after import', false)
1049
+ .option('--delete-source', 'Delete the imported bootstrap file after import', false)
1050
+ .option('--json', 'Print JSON output', false)
1051
+ .action(
1052
+ withLocalAdminMutationAccess(
1053
+ 'wlfi-agent config agent-auth import',
1054
+ (inputPath: string, options) => {
1055
+ if (options.keepSource && options.deleteSource) {
1056
+ throw new Error('--keep-source conflicts with --delete-source');
1057
+ }
1058
+
1059
+ const summary = readBootstrapSetupSummaryFile(inputPath);
1060
+ assertBootstrapSetupSummaryLeaseIsActive(summary);
1061
+ const imported = readBootstrapAgentCredentialsFile(inputPath);
1062
+ const agentKeyId = assertAgentKeyId(imported.agentKeyId);
1063
+ if (summary.agentKeyId !== agentKeyId) {
1064
+ throw new Error(
1065
+ 'bootstrap credentials file agent_key_id does not match setup summary agent_key_id',
1066
+ );
1067
+ }
1068
+
1069
+ storeAgentAuthTokenInKeychain(agentKeyId, imported.agentAuthToken);
1070
+
1071
+ let sourceCleanup: 'redacted' | 'deleted' | 'kept' = 'kept';
1072
+ if (options.deleteSource) {
1073
+ deleteBootstrapAgentCredentialsFile(imported.sourcePath);
1074
+ sourceCleanup = 'deleted';
1075
+ } else if (!options.keepSource) {
1076
+ redactBootstrapAgentCredentialsFile(imported.sourcePath);
1077
+ sourceCleanup = 'redacted';
1078
+ } else {
1079
+ console.error(
1080
+ 'warning: imported bootstrap file still contains a plaintext agent auth token; prefer the default redaction or --delete-source',
1081
+ );
1082
+ }
1083
+
1084
+ let updated = writeConfig({
1085
+ agentKeyId,
1086
+ wallet: walletProfileFromBootstrapSummary(summary),
1087
+ });
1088
+ if (updated.agentAuthToken !== undefined) {
1089
+ updated = deleteConfigKey('agentAuthToken');
1090
+ }
1091
+
1092
+ print(
1093
+ {
1094
+ sourcePath: imported.sourcePath,
1095
+ sourceCleanup,
1096
+ agentKeyId,
1097
+ keychain: {
1098
+ stored: true,
1099
+ service: AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE,
1100
+ },
1101
+ config: redactConfig(updated),
1102
+ },
1103
+ options.json,
1104
+ );
1105
+ },
1106
+ ),
1107
+ );
1108
+
1109
+ agentAuthCommand
1110
+ .command('migrate')
1111
+ .description(
1112
+ 'Move a legacy config.json agentAuthToken into macOS Keychain and scrub plaintext storage',
1113
+ )
1114
+ .option('--agent-key-id <uuid>', 'Agent key id (defaults to configured agentKeyId)')
1115
+ .option(
1116
+ '--overwrite-keychain',
1117
+ 'Replace a different existing Keychain token for this agent',
1118
+ false,
1119
+ )
1120
+ .option('--json', 'Print JSON output', false)
1121
+ .action(
1122
+ withLocalAdminMutationAccess('wlfi-agent config agent-auth migrate', (options) => {
1123
+ print(
1124
+ migrateLegacyAgentAuthToken({
1125
+ agentKeyId: options.agentKeyId,
1126
+ overwriteKeychain: options.overwriteKeychain,
1127
+ }),
1128
+ options.json,
1129
+ );
1130
+ }),
1131
+ );
1132
+
1133
+ agentAuthCommand
1134
+ .command('rotate')
1135
+ .description('Rotate the agent auth token via Rust admin flow, then store it in macOS Keychain')
1136
+ .option('--agent-key-id <uuid>', 'Agent key id (defaults to configured agentKeyId)')
1137
+ .option('--vault-password-stdin', 'Read vault password from stdin', false)
1138
+ .option('--non-interactive', 'Disable password prompts', false)
1139
+ .option('--daemon-socket <path>', 'Daemon unix socket path')
1140
+ .option('--json', 'Print JSON output', false)
1141
+ .action(async (options) => {
1142
+ const config = readConfig();
1143
+ const agentKeyId = options.agentKeyId
1144
+ ? assertAgentKeyId(options.agentKeyId)
1145
+ : config.agentKeyId
1146
+ ? assertAgentKeyId(config.agentKeyId, 'configured agentKeyId')
1147
+ : undefined;
1148
+ if (!agentKeyId) {
1149
+ throw new Error(
1150
+ 'agentKeyId is required; pass --agent-key-id or configure agentKeyId first',
1151
+ );
1152
+ }
1153
+
1154
+ const daemonSocket = resolveDaemonSocket(
1155
+ options.daemonSocket ?? process.env.WLFI_DAEMON_SOCKET,
1156
+ config,
1157
+ );
1158
+ assertTrustedAdminDaemonSocketPath(daemonSocket);
1159
+
1160
+ const rotated = await runRustBinaryJson<RotateAgentAuthTokenAdminOutput>(
1161
+ 'wlfi-agent-admin',
1162
+ buildRotateAgentAuthTokenAdminArgs({
1163
+ agentKeyId,
1164
+ vaultPasswordStdin: options.vaultPasswordStdin,
1165
+ nonInteractive: options.nonInteractive,
1166
+ daemonSocket,
1167
+ }),
1168
+ config,
1169
+ );
1170
+
1171
+ print(completeAgentAuthRotation(rotated), options.json);
1172
+ });
1173
+
1174
+ agentAuthCommand
1175
+ .command('revoke')
1176
+ .description('Revoke the agent key via Rust admin flow, then remove local credentials')
1177
+ .option('--agent-key-id <uuid>', 'Agent key id (defaults to configured agentKeyId)')
1178
+ .option('--vault-password-stdin', 'Read vault password from stdin', false)
1179
+ .option('--non-interactive', 'Disable password prompts', false)
1180
+ .option('--daemon-socket <path>', 'Daemon unix socket path')
1181
+ .option('--json', 'Print JSON output', false)
1182
+ .action(async (options) => {
1183
+ const config = readConfig();
1184
+ const agentKeyId = options.agentKeyId
1185
+ ? assertAgentKeyId(options.agentKeyId)
1186
+ : config.agentKeyId
1187
+ ? assertAgentKeyId(config.agentKeyId, 'configured agentKeyId')
1188
+ : undefined;
1189
+ if (!agentKeyId) {
1190
+ throw new Error(
1191
+ 'agentKeyId is required; pass --agent-key-id or configure agentKeyId first',
1192
+ );
1193
+ }
1194
+
1195
+ const daemonSocket = resolveDaemonSocket(
1196
+ options.daemonSocket ?? process.env.WLFI_DAEMON_SOCKET,
1197
+ config,
1198
+ );
1199
+ assertTrustedAdminDaemonSocketPath(daemonSocket);
1200
+
1201
+ const revoked = await runRustBinaryJson<RevokeAgentKeyAdminOutput>(
1202
+ 'wlfi-agent-admin',
1203
+ buildRevokeAgentKeyAdminArgs({
1204
+ agentKeyId,
1205
+ vaultPasswordStdin: options.vaultPasswordStdin,
1206
+ nonInteractive: options.nonInteractive,
1207
+ daemonSocket,
1208
+ }),
1209
+ config,
1210
+ );
1211
+
1212
+ print(completeAgentKeyRevocation(revoked), options.json);
1213
+ });
1214
+
1215
+ agentAuthCommand
1216
+ .command('status')
1217
+ .option('--agent-key-id <uuid>', 'Agent key id (defaults to configured agentKeyId)')
1218
+ .option('--json', 'Print JSON output', false)
1219
+ .action((options) => {
1220
+ const config = readConfig();
1221
+ const agentKeyId = options.agentKeyId
1222
+ ? assertAgentKeyId(options.agentKeyId)
1223
+ : config.agentKeyId && AGENT_KEY_ID_PATTERN.test(config.agentKeyId.trim())
1224
+ ? config.agentKeyId.trim()
1225
+ : null;
1226
+ print(
1227
+ {
1228
+ agentKeyId,
1229
+ keychain: {
1230
+ supported: process.platform === 'darwin',
1231
+ service: process.platform === 'darwin' ? AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE : null,
1232
+ stored: hasStoredAgentAuthToken(agentKeyId ?? undefined),
1233
+ },
1234
+ },
1235
+ options.json,
1236
+ );
1237
+ });
1238
+
1239
+ agentAuthCommand
1240
+ .command('clear')
1241
+ .option('--agent-key-id <uuid>', 'Agent key id (defaults to configured agentKeyId)')
1242
+ .option('--json', 'Print JSON output', false)
1243
+ .action(
1244
+ withLocalAdminMutationAccess('wlfi-agent config agent-auth clear', (options) => {
1245
+ const config = readConfig();
1246
+ const agentKeyId = options.agentKeyId
1247
+ ? assertAgentKeyId(options.agentKeyId)
1248
+ : config.agentKeyId
1249
+ ? assertAgentKeyId(config.agentKeyId, 'configured agentKeyId')
1250
+ : undefined;
1251
+ if (!agentKeyId) {
1252
+ throw new Error(
1253
+ 'agentKeyId is required; pass --agent-key-id or configure agentKeyId first',
1254
+ );
1255
+ }
1256
+
1257
+ print(clearAgentAuthToken(agentKeyId), options.json);
1258
+ }),
1259
+ );
1260
+
1261
+ program
1262
+ .command('wallet')
1263
+ .description('Show the configured wallet public key and associated policy summary')
1264
+ .option('--json', 'Print JSON output', false)
1265
+ .action(async (options) => {
1266
+ const profile = await resolveWalletProfileWithBalances(readConfig(), {
1267
+ getNativeBalance,
1268
+ getTokenBalance,
1269
+ });
1270
+ print(options.json ? profile : formatWalletProfileText(profile), options.json);
1271
+ });
1272
+
1273
+ registerStatusCommand(program, {
1274
+ print: (payload, options) => {
1275
+ print(payload, options.asJson);
1276
+ },
1277
+ setExitCode: (code) => {
1278
+ process.exitCode = code;
1279
+ },
1280
+ });
1281
+
1282
+ registerRepairCommand(program, {
1283
+ print: (payload, options) => {
1284
+ print(payload, options.asJson);
1285
+ },
1286
+ setExitCode: (code) => {
1287
+ process.exitCode = code;
1288
+ },
1289
+ });
1290
+
1291
+ program
1292
+ .command('admin')
1293
+ .helpOption(false)
1294
+ .allowUnknownOption(true)
1295
+ .allowExcessArguments(true)
1296
+ .argument('[args...]')
1297
+ .description('Admin commands and setup passthrough')
1298
+ .action(async () => {
1299
+ const index = process.argv.indexOf('admin');
1300
+ const forwarded = index >= 0 ? process.argv.slice(index + 1) : [];
1301
+ const passthroughTarget = forwarded[0] === 'help' ? forwarded[1] : forwarded[0];
1302
+ const blockedPassthroughMessage = blockedRawAdminPassthroughMessage(passthroughTarget);
1303
+ if (forwarded[0] === 'setup') {
1304
+ await runAdminSetupCli(forwarded.slice(1));
1305
+ return;
1306
+ }
1307
+ if (forwarded[0] === 'tui') {
1308
+ await runAdminTuiCli(forwarded.slice(1));
1309
+ return;
1310
+ }
1311
+ if (forwarded[0] === 'reset') {
1312
+ await runAdminResetCli(forwarded.slice(1));
1313
+ return;
1314
+ }
1315
+ if (forwarded[0] === 'uninstall') {
1316
+ await runAdminUninstallCli(forwarded.slice(1));
1317
+ return;
1318
+ }
1319
+ if (forwarded[0] === 'help' && forwarded[1] === 'tui') {
1320
+ await runAdminTuiCli(['--help']);
1321
+ return;
1322
+ }
1323
+ if (forwarded[0] === 'help' && forwarded[1] === 'setup') {
1324
+ await runAdminSetupCli(['--help']);
1325
+ return;
1326
+ }
1327
+ if (forwarded[0] === 'help' && forwarded[1] === 'reset') {
1328
+ await runAdminResetCli(['--help']);
1329
+ return;
1330
+ }
1331
+ if (forwarded[0] === 'help' && forwarded[1] === 'uninstall') {
1332
+ await runAdminUninstallCli(['--help']);
1333
+ return;
1334
+ }
1335
+ if (blockedPassthroughMessage) {
1336
+ if (forwarded[0] === 'help') {
1337
+ print(blockedPassthroughMessage, false);
1338
+ return;
1339
+ }
1340
+ throw new Error(blockedPassthroughMessage);
1341
+ }
1342
+ if (forwarded[0] === 'bootstrap') {
1343
+ throw new Error(
1344
+ '`wlfi-agent admin bootstrap` has been removed; use `wlfi-agent admin setup`',
1345
+ );
1346
+ }
1347
+ if (await runLocalAdminCommand(forwarded)) {
1348
+ return;
1349
+ }
1350
+ const config = readConfig();
1351
+ const wantsHelp =
1352
+ forwarded.length === 0 ||
1353
+ forwarded[0] === 'help' ||
1354
+ forwarded.includes('--help') ||
1355
+ forwarded.includes('-h');
1356
+ if (wantsHelp) {
1357
+ const rendered = await runRustBinary(
1358
+ 'wlfi-agent-admin',
1359
+ forwarded.length === 0 ? ['--help'] : forwarded,
1360
+ config,
1361
+ );
1362
+ if (rendered.stdout) {
1363
+ process.stdout.write(rewriteAdminHelpText(rendered.stdout));
1364
+ }
1365
+ if (rendered.stderr) {
1366
+ process.stderr.write(rewriteAdminHelpText(rendered.stderr));
1367
+ }
1368
+ return;
1369
+ }
1370
+ const code = await passthroughRustBinary('wlfi-agent-admin', forwarded, config);
1371
+ process.exitCode = code;
1372
+ });
1373
+
1374
+ const addAgentCommandAuthOptions = (command: Command) =>
1375
+ command
1376
+ .option('--daemon-socket <path>', 'Daemon socket path')
1377
+ .option('--agent-key-id <uuid>', 'Agent key id')
1378
+ .option('--agent-auth-token <token>', 'Agent auth token')
1379
+ .option('--agent-auth-token-stdin', 'Read agent auth token from stdin', false)
1380
+ .option(
1381
+ '--allow-legacy-agent-auth-source',
1382
+ 'Allow deprecated argv/config/env fallback for agent auth token',
1383
+ false,
1384
+ )
1385
+ .option('--json', 'Print JSON output', false);
1386
+
1387
+ addAgentCommandAuthOptions(
1388
+ program
1389
+ .command('transfer')
1390
+ .description('Submit an ERC-20 transfer request through policy checks')
1391
+ .requiredOption('--network <name>', 'Network name')
1392
+ .requiredOption('--token <address>', 'ERC-20 token contract')
1393
+ .requiredOption('--to <address>', 'Recipient address')
1394
+ .requiredOption('--amount <amount>', 'Transfer amount in token units')
1395
+ .option('--broadcast', 'Broadcast the signed transaction through RPC', false)
1396
+ .option('--rpc-url <url>', 'Ethereum RPC URL override used only for broadcast')
1397
+ .option(
1398
+ '--from <address>',
1399
+ 'Sender address override for broadcast; defaults to configured wallet address',
1400
+ )
1401
+ .option('--nonce <nonce>', 'Explicit nonce override for broadcast')
1402
+ .option('--gas-limit <gas>', 'Gas limit override for broadcast')
1403
+ .option('--max-fee-per-gas-wei <wei>', 'Max fee per gas override for broadcast')
1404
+ .option('--max-priority-fee-per-gas-wei <wei>', 'Priority fee per gas override for broadcast')
1405
+ .option('--tx-type <type>', 'Typed tx value for broadcast', '0x02')
1406
+ .option('--no-wait', 'Do not wait up to 30s for an on-chain receipt after broadcast')
1407
+ .option(
1408
+ '--reveal-raw-tx',
1409
+ 'Include the signed raw transaction bytes in broadcast output',
1410
+ false,
1411
+ )
1412
+ .option('--reveal-signature', 'Include signer r/s/v fields in broadcast output', false)
1413
+ .addOption(new Option('--amount-wei <wei>').hideHelp()),
1414
+ ).action(async (options) => {
1415
+ const config = readConfig();
1416
+ const network = resolveCliNetworkProfile(options.network, config).chainId;
1417
+ const token = assertAddress(options.token, 'token');
1418
+ const recipient = assertAddress(options.to, 'to');
1419
+ const asset = resolveConfiguredErc20Asset(config, network, token);
1420
+ const amountWei = options.amount
1421
+ ? parseConfiguredAmount(options.amount, asset.decimals, 'amount')
1422
+ : parseBigIntString(options.amountWei, 'amountWei');
1423
+ try {
1424
+ if (options.broadcast) {
1425
+ const plan = await resolveAssetBroadcastPlan(
1426
+ {
1427
+ rpcUrl: resolveCliRpcUrl(options.rpcUrl, options.network, config),
1428
+ chainId: network,
1429
+ from: options.from ? assertAddress(options.from, 'from') : resolveWalletAddress(config),
1430
+ to: token,
1431
+ valueWei: 0n,
1432
+ dataHex: encodeErc20TransferData(recipient, amountWei),
1433
+ nonce: options.nonce ? parseIntegerString(options.nonce, 'nonce') : undefined,
1434
+ gasLimit: options.gasLimit
1435
+ ? parsePositiveBigIntString(options.gasLimit, 'gasLimit')
1436
+ : undefined,
1437
+ maxFeePerGasWei: options.maxFeePerGasWei
1438
+ ? parsePositiveBigIntString(options.maxFeePerGasWei, 'maxFeePerGasWei')
1439
+ : undefined,
1440
+ maxPriorityFeePerGasWei: options.maxPriorityFeePerGasWei
1441
+ ? parseBigIntString(options.maxPriorityFeePerGasWei, 'maxPriorityFeePerGasWei')
1442
+ : undefined,
1443
+ txType: options.txType,
1444
+ },
1445
+ {
1446
+ getChainInfo,
1447
+ assertRpcChainIdMatches,
1448
+ getNonce,
1449
+ estimateGas,
1450
+ estimateFees,
1451
+ },
1452
+ );
1453
+ const signed = await runAgentCommandJson<RustBroadcastOutput>({
1454
+ commandArgs: [
1455
+ 'broadcast',
1456
+ '--network',
1457
+ String(plan.chainId),
1458
+ '--nonce',
1459
+ String(plan.nonce),
1460
+ '--to',
1461
+ token,
1462
+ '--value-wei',
1463
+ '0',
1464
+ '--data-hex',
1465
+ plan.dataHex,
1466
+ '--gas-limit',
1467
+ plan.gasLimit.toString(),
1468
+ '--max-fee-per-gas-wei',
1469
+ plan.maxFeePerGasWei.toString(),
1470
+ '--max-priority-fee-per-gas-wei',
1471
+ plan.maxPriorityFeePerGasWei.toString(),
1472
+ '--tx-type',
1473
+ plan.txType,
1474
+ ],
1475
+ auth: options,
1476
+ config,
1477
+ asJson: options.json,
1478
+ });
1479
+ if (!signed) {
1480
+ return;
1481
+ }
1482
+ const completed = await completeAssetBroadcast(plan, signed, {
1483
+ assertSignedBroadcastTransactionMatchesRequest,
1484
+ broadcastRawTransaction,
1485
+ });
1486
+ print(
1487
+ formatBroadcastedAssetOutput({
1488
+ command: 'transfer',
1489
+ counterparty: recipient,
1490
+ asset,
1491
+ signed,
1492
+ plan,
1493
+ signedNonce: completed.signedNonce,
1494
+ networkTxHash: completed.networkTxHash,
1495
+ revealRawTx: options.revealRawTx,
1496
+ revealSignature: options.revealSignature,
1497
+ }),
1498
+ options.json,
1499
+ );
1500
+ if (options.wait) {
1501
+ await reportOnchainReceiptStatus({
1502
+ rpcUrl: plan.rpcUrl,
1503
+ txHash: completed.networkTxHash,
1504
+ asJson: options.json,
1505
+ });
1506
+ }
1507
+ return;
1508
+ }
1509
+
1510
+ const result = await runAgentCommandJson<RustBroadcastOutput>({
1511
+ commandArgs: [
1512
+ 'transfer',
1513
+ '--network',
1514
+ String(network),
1515
+ '--token',
1516
+ token,
1517
+ '--to',
1518
+ recipient,
1519
+ '--amount-wei',
1520
+ amountWei.toString(),
1521
+ ],
1522
+ auth: options,
1523
+ config,
1524
+ asJson: options.json,
1525
+ });
1526
+ if (result) {
1527
+ print(normalizeAgentAmountOutput(result, asset), options.json);
1528
+ }
1529
+ } catch (error) {
1530
+ throw rewriteAgentAmountError(error, asset);
1531
+ }
1532
+ });
1533
+
1534
+ addAgentCommandAuthOptions(
1535
+ program
1536
+ .command('transfer-native')
1537
+ .description('Submit a native ETH transfer request through policy checks')
1538
+ .requiredOption('--network <name>', 'Network name')
1539
+ .requiredOption('--to <address>', 'Recipient address')
1540
+ .requiredOption('--amount <amount>', 'Transfer amount in configured native token units')
1541
+ .addOption(new Option('--amount-wei <wei>').hideHelp()),
1542
+ ).action(async (options) => {
1543
+ const config = readConfig();
1544
+ const network = resolveCliNetworkProfile(options.network, config).chainId;
1545
+ const asset = resolveConfiguredNativeAsset(config, network);
1546
+ const amountWei = options.amount
1547
+ ? parseConfiguredAmount(options.amount, asset.decimals, 'amount')
1548
+ : parseBigIntString(options.amountWei, 'amountWei');
1549
+ try {
1550
+ const result = await runAgentCommandJson<RustBroadcastOutput>({
1551
+ commandArgs: [
1552
+ 'transfer-native',
1553
+ '--network',
1554
+ String(network),
1555
+ '--to',
1556
+ assertAddress(options.to, 'to'),
1557
+ '--amount-wei',
1558
+ amountWei.toString(),
1559
+ ],
1560
+ auth: options,
1561
+ config,
1562
+ asJson: options.json,
1563
+ });
1564
+ if (result) {
1565
+ print(normalizeAgentAmountOutput(result, asset), options.json);
1566
+ }
1567
+ } catch (error) {
1568
+ throw rewriteAgentAmountError(error, asset);
1569
+ }
1570
+ });
1571
+
1572
+ addAgentCommandAuthOptions(
1573
+ program
1574
+ .command('approve')
1575
+ .description('Submit an ERC-20 approve request through policy checks')
1576
+ .requiredOption('--network <name>', 'Network name')
1577
+ .requiredOption('--token <address>', 'ERC-20 token contract')
1578
+ .requiredOption('--spender <address>', 'Spender address')
1579
+ .requiredOption('--amount <amount>', 'Approval amount in token units')
1580
+ .option('--broadcast', 'Broadcast the signed transaction through RPC', false)
1581
+ .option('--rpc-url <url>', 'Ethereum RPC URL override used only for broadcast')
1582
+ .option(
1583
+ '--from <address>',
1584
+ 'Sender address override for broadcast; defaults to configured wallet address',
1585
+ )
1586
+ .option('--nonce <nonce>', 'Explicit nonce override for broadcast')
1587
+ .option('--gas-limit <gas>', 'Gas limit override for broadcast')
1588
+ .option('--max-fee-per-gas-wei <wei>', 'Max fee per gas override for broadcast')
1589
+ .option('--max-priority-fee-per-gas-wei <wei>', 'Priority fee per gas override for broadcast')
1590
+ .option('--tx-type <type>', 'Typed tx value for broadcast', '0x02')
1591
+ .option('--no-wait', 'Do not wait up to 30s for an on-chain receipt after broadcast')
1592
+ .option(
1593
+ '--reveal-raw-tx',
1594
+ 'Include the signed raw transaction bytes in broadcast output',
1595
+ false,
1596
+ )
1597
+ .option('--reveal-signature', 'Include signer r/s/v fields in broadcast output', false)
1598
+ .addOption(new Option('--amount-wei <wei>').hideHelp()),
1599
+ ).action(async (options) => {
1600
+ const config = readConfig();
1601
+ const network = resolveCliNetworkProfile(options.network, config).chainId;
1602
+ const token = assertAddress(options.token, 'token');
1603
+ const spender = assertAddress(options.spender, 'spender');
1604
+ const asset = resolveConfiguredErc20Asset(config, network, token);
1605
+ const amountWei = options.amount
1606
+ ? parseConfiguredAmount(options.amount, asset.decimals, 'amount')
1607
+ : parseBigIntString(options.amountWei, 'amountWei');
1608
+ try {
1609
+ if (options.broadcast) {
1610
+ const plan = await resolveAssetBroadcastPlan(
1611
+ {
1612
+ rpcUrl: resolveCliRpcUrl(options.rpcUrl, options.network, config),
1613
+ chainId: network,
1614
+ from: options.from ? assertAddress(options.from, 'from') : resolveWalletAddress(config),
1615
+ to: token,
1616
+ valueWei: 0n,
1617
+ dataHex: encodeErc20ApproveData(spender, amountWei),
1618
+ nonce: options.nonce ? parseIntegerString(options.nonce, 'nonce') : undefined,
1619
+ gasLimit: options.gasLimit
1620
+ ? parsePositiveBigIntString(options.gasLimit, 'gasLimit')
1621
+ : undefined,
1622
+ maxFeePerGasWei: options.maxFeePerGasWei
1623
+ ? parsePositiveBigIntString(options.maxFeePerGasWei, 'maxFeePerGasWei')
1624
+ : undefined,
1625
+ maxPriorityFeePerGasWei: options.maxPriorityFeePerGasWei
1626
+ ? parseBigIntString(options.maxPriorityFeePerGasWei, 'maxPriorityFeePerGasWei')
1627
+ : undefined,
1628
+ txType: options.txType,
1629
+ },
1630
+ {
1631
+ getChainInfo,
1632
+ assertRpcChainIdMatches,
1633
+ getNonce,
1634
+ estimateGas,
1635
+ estimateFees,
1636
+ },
1637
+ );
1638
+ const signed = await runAgentCommandJson<RustBroadcastOutput>({
1639
+ commandArgs: [
1640
+ 'broadcast',
1641
+ '--network',
1642
+ String(plan.chainId),
1643
+ '--nonce',
1644
+ String(plan.nonce),
1645
+ '--to',
1646
+ token,
1647
+ '--value-wei',
1648
+ '0',
1649
+ '--data-hex',
1650
+ plan.dataHex,
1651
+ '--gas-limit',
1652
+ plan.gasLimit.toString(),
1653
+ '--max-fee-per-gas-wei',
1654
+ plan.maxFeePerGasWei.toString(),
1655
+ '--max-priority-fee-per-gas-wei',
1656
+ plan.maxPriorityFeePerGasWei.toString(),
1657
+ '--tx-type',
1658
+ plan.txType,
1659
+ ],
1660
+ auth: options,
1661
+ config,
1662
+ asJson: options.json,
1663
+ });
1664
+ if (!signed) {
1665
+ return;
1666
+ }
1667
+ const completed = await completeAssetBroadcast(plan, signed, {
1668
+ assertSignedBroadcastTransactionMatchesRequest,
1669
+ broadcastRawTransaction,
1670
+ });
1671
+ print(
1672
+ formatBroadcastedAssetOutput({
1673
+ command: 'approve',
1674
+ counterparty: spender,
1675
+ asset,
1676
+ signed,
1677
+ plan,
1678
+ signedNonce: completed.signedNonce,
1679
+ networkTxHash: completed.networkTxHash,
1680
+ revealRawTx: options.revealRawTx,
1681
+ revealSignature: options.revealSignature,
1682
+ }),
1683
+ options.json,
1684
+ );
1685
+ if (options.wait) {
1686
+ await reportOnchainReceiptStatus({
1687
+ rpcUrl: plan.rpcUrl,
1688
+ txHash: completed.networkTxHash,
1689
+ asJson: options.json,
1690
+ });
1691
+ }
1692
+ return;
1693
+ }
1694
+
1695
+ const result = await runAgentCommandJson<RustBroadcastOutput>({
1696
+ commandArgs: [
1697
+ 'approve',
1698
+ '--network',
1699
+ String(network),
1700
+ '--token',
1701
+ token,
1702
+ '--spender',
1703
+ spender,
1704
+ '--amount-wei',
1705
+ amountWei.toString(),
1706
+ ],
1707
+ auth: options,
1708
+ config,
1709
+ asJson: options.json,
1710
+ });
1711
+ if (result) {
1712
+ print(normalizeAgentAmountOutput(result, asset), options.json);
1713
+ }
1714
+ } catch (error) {
1715
+ throw rewriteAgentAmountError(error, asset);
1716
+ }
1717
+ });
1718
+
1719
+ addAgentCommandAuthOptions(
1720
+ program
1721
+ .command('broadcast')
1722
+ .description('Submit a raw transaction broadcast request through policy checks')
1723
+ .requiredOption('--network <name>', 'Network name')
1724
+ .requiredOption('--to <address>', 'Recipient or target contract')
1725
+ .requiredOption('--gas-limit <gas>', 'Gas limit')
1726
+ .requiredOption('--max-fee-per-gas-wei <wei>', 'Max fee per gas in wei')
1727
+ .option('--nonce <nonce>', 'Explicit nonce override', '0')
1728
+ .option('--value-wei <wei>', 'Value in wei', '0')
1729
+ .option('--data-hex <hex>', 'Calldata hex', '0x')
1730
+ .option('--max-priority-fee-per-gas-wei <wei>', 'Priority fee per gas in wei', '0')
1731
+ .option('--tx-type <type>', 'Typed tx value', '2')
1732
+ .option('--delegation-enabled', 'Forward delegation flag to Rust signing request', false),
1733
+ ).action(async (options) => {
1734
+ const config = readConfig();
1735
+ const network = resolveCliNetworkProfile(options.network, config);
1736
+ const result = await runAgentCommandJson<RustBroadcastOutput>({
1737
+ commandArgs: [
1738
+ 'broadcast',
1739
+ '--network',
1740
+ String(network.chainId),
1741
+ '--nonce',
1742
+ String(parseIntegerString(options.nonce, 'nonce')),
1743
+ '--to',
1744
+ assertAddress(options.to, 'to'),
1745
+ '--value-wei',
1746
+ parseBigIntString(options.valueWei, 'valueWei').toString(),
1747
+ '--data-hex',
1748
+ assertHex(options.dataHex, 'dataHex'),
1749
+ '--gas-limit',
1750
+ parsePositiveBigIntString(options.gasLimit, 'gasLimit').toString(),
1751
+ '--max-fee-per-gas-wei',
1752
+ parsePositiveBigIntString(options.maxFeePerGasWei, 'maxFeePerGasWei').toString(),
1753
+ '--max-priority-fee-per-gas-wei',
1754
+ parseBigIntString(options.maxPriorityFeePerGasWei, 'maxPriorityFeePerGasWei').toString(),
1755
+ '--tx-type',
1756
+ options.txType,
1757
+ ...(options.delegationEnabled ? ['--delegation-enabled'] : []),
1758
+ ],
1759
+ auth: options,
1760
+ config,
1761
+ asJson: options.json,
1762
+ });
1763
+ if (result) {
1764
+ print(result, options.json);
1765
+ }
1766
+ });
1767
+
1768
+ const rpc = program.command('rpc').description('RPC methods implemented in TypeScript');
1769
+ rpc
1770
+ .command('chain')
1771
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1772
+ .option('--json', 'Print JSON output', false)
1773
+ .action(async (options) => {
1774
+ const config = readConfig();
1775
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1776
+ const result = await getChainInfo(rpcUrl);
1777
+ print(
1778
+ {
1779
+ rpcUrl,
1780
+ ...result,
1781
+ configured: activeChainSummary(config),
1782
+ },
1783
+ options.json,
1784
+ );
1785
+ });
1786
+ rpc
1787
+ .command('block-number')
1788
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1789
+ .option('--json', 'Print JSON output', false)
1790
+ .action(async (options) => {
1791
+ const config = readConfig();
1792
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1793
+ const blockNumber = await getLatestBlockNumber(rpcUrl);
1794
+ print({ rpcUrl, blockNumber: blockNumber.toString() }, options.json);
1795
+ });
1796
+ rpc
1797
+ .command('account')
1798
+ .requiredOption('--address <address>', 'Account address')
1799
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1800
+ .option('--json', 'Print JSON output', false)
1801
+ .action(async (options) => {
1802
+ const config = readConfig();
1803
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1804
+ const address = assertAddress(options.address, 'address');
1805
+ const snapshot = await getAccountSnapshot(rpcUrl, address);
1806
+ print(
1807
+ {
1808
+ ...snapshot,
1809
+ latestBlockNumber: snapshot.latestBlockNumber.toString(),
1810
+ balance: {
1811
+ raw: snapshot.balance.raw.toString(),
1812
+ formatted: snapshot.balance.formatted,
1813
+ },
1814
+ },
1815
+ options.json,
1816
+ );
1817
+ });
1818
+ rpc
1819
+ .command('balance')
1820
+ .requiredOption('--address <address>', 'Account address')
1821
+ .option('--token <address>', 'Optional ERC-20 token address')
1822
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1823
+ .option('--decimals <decimals>', 'ERC-20 decimals override')
1824
+ .option('--json', 'Print JSON output', false)
1825
+ .action(async (options) => {
1826
+ const config = readConfig();
1827
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1828
+ const owner = assertAddress(options.address, 'address');
1829
+ if (options.token) {
1830
+ const token = assertAddress(options.token, 'token');
1831
+ const result = await getTokenBalance(
1832
+ rpcUrl,
1833
+ token,
1834
+ owner,
1835
+ options.decimals ? parseIntegerString(options.decimals, 'decimals') : undefined,
1836
+ );
1837
+ print(
1838
+ {
1839
+ kind: 'erc20',
1840
+ token,
1841
+ owner,
1842
+ balanceWei: result.raw.toString(),
1843
+ decimals: result.decimals,
1844
+ name: result.name,
1845
+ symbol: result.symbol,
1846
+ formatted: result.formatted,
1847
+ },
1848
+ options.json,
1849
+ );
1850
+ return;
1851
+ }
1852
+
1853
+ const result = await getNativeBalance(rpcUrl, owner);
1854
+ print(
1855
+ {
1856
+ kind: 'native',
1857
+ owner,
1858
+ balanceWei: result.raw.toString(),
1859
+ formattedEth: result.formatted,
1860
+ },
1861
+ options.json,
1862
+ );
1863
+ });
1864
+ rpc
1865
+ .command('nonce')
1866
+ .requiredOption('--address <address>', 'Account address')
1867
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1868
+ .option('--json', 'Print JSON output', false)
1869
+ .action(async (options) => {
1870
+ const config = readConfig();
1871
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1872
+ const address = assertAddress(options.address, 'address');
1873
+ const nonce = await getNonce(rpcUrl, address);
1874
+ print({ address, nonce }, options.json);
1875
+ });
1876
+ rpc
1877
+ .command('fees')
1878
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1879
+ .option('--json', 'Print JSON output', false)
1880
+ .action(async (options) => {
1881
+ const config = readConfig();
1882
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1883
+ const fees = await estimateFees(rpcUrl);
1884
+ print(
1885
+ {
1886
+ rpcUrl,
1887
+ gasPrice: stringifyOptionalValue(fees.gasPrice),
1888
+ maxFeePerGas: stringifyOptionalValue(fees.maxFeePerGas),
1889
+ maxPriorityFeePerGas: stringifyOptionalValue(fees.maxPriorityFeePerGas),
1890
+ },
1891
+ options.json,
1892
+ );
1893
+ });
1894
+ rpc
1895
+ .command('gas-estimate')
1896
+ .requiredOption('--from <address>', 'Sender address')
1897
+ .requiredOption('--to <address>', 'Recipient or target contract')
1898
+ .option('--value-wei <wei>', 'Value in wei', '0')
1899
+ .option('--data-hex <hex>', 'Calldata hex', '0x')
1900
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1901
+ .option('--json', 'Print JSON output', false)
1902
+ .action(async (options) => {
1903
+ const config = readConfig();
1904
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1905
+ const gas = await estimateGas({
1906
+ rpcUrl,
1907
+ from: assertAddress(options.from, 'from'),
1908
+ to: assertAddress(options.to, 'to'),
1909
+ value: parseBigIntString(options.valueWei, 'valueWei'),
1910
+ data: assertHex(options.dataHex, 'dataHex'),
1911
+ });
1912
+ print({ rpcUrl, gas: gas.toString() }, options.json);
1913
+ });
1914
+ rpc
1915
+ .command('tx')
1916
+ .requiredOption('--hash <hash>', 'Transaction hash')
1917
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1918
+ .option('--json', 'Print JSON output', false)
1919
+ .action(async (options) => {
1920
+ const config = readConfig();
1921
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1922
+ const tx = await getTransactionByHash(rpcUrl, assertHex(options.hash, 'hash'));
1923
+ print(tx, options.json);
1924
+ });
1925
+ rpc
1926
+ .command('receipt')
1927
+ .requiredOption('--hash <hash>', 'Transaction hash')
1928
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1929
+ .option('--json', 'Print JSON output', false)
1930
+ .action(async (options) => {
1931
+ const config = readConfig();
1932
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1933
+ const receipt = await getTransactionReceiptByHash(rpcUrl, assertHex(options.hash, 'hash'));
1934
+ print(receipt, options.json);
1935
+ });
1936
+ rpc
1937
+ .command('code')
1938
+ .requiredOption('--address <address>', 'Contract address')
1939
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1940
+ .option('--json', 'Print JSON output', false)
1941
+ .action(async (options) => {
1942
+ const config = readConfig();
1943
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1944
+ const bytecode = await getCodeAtAddress(rpcUrl, assertAddress(options.address, 'address'));
1945
+ print(
1946
+ {
1947
+ address: options.address,
1948
+ rpcUrl,
1949
+ bytecode: bytecode ?? '0x',
1950
+ hasCode: bytecode !== undefined && bytecode !== '0x',
1951
+ },
1952
+ options.json,
1953
+ );
1954
+ });
1955
+ rpc
1956
+ .command('broadcast-raw')
1957
+ .requiredOption('--raw-tx-hex <hex>', 'Signed raw transaction hex')
1958
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1959
+ .option('--no-wait', 'Do not wait up to 30s for an on-chain receipt after broadcast')
1960
+ .option('--json', 'Print JSON output', false)
1961
+ .action(async (options) => {
1962
+ const config = readConfig();
1963
+ const rpcUrl = resolveRpcUrl(options.rpcUrl, config);
1964
+ const txHash = await broadcastRawTransaction(rpcUrl, assertHex(options.rawTxHex, 'rawTxHex'));
1965
+ print({ txHash }, options.json);
1966
+ if (options.wait) {
1967
+ await reportOnchainReceiptStatus({
1968
+ rpcUrl,
1969
+ txHash,
1970
+ asJson: options.json,
1971
+ });
1972
+ }
1973
+ });
1974
+
1975
+ const tx = program
1976
+ .command('tx')
1977
+ .description('Sign with Rust, then perform network RPC in TypeScript');
1978
+ tx.command('broadcast')
1979
+ .requiredOption('--from <address>', 'Sender address for nonce/gas estimation')
1980
+ .requiredOption('--to <address>', 'Recipient or target contract')
1981
+ .option('--network <name>', 'Network name')
1982
+ .option('--rpc-url <url>', 'Ethereum RPC URL')
1983
+ .option('--daemon-socket <path>', 'Daemon socket path')
1984
+ .option('--agent-key-id <uuid>', 'Agent key id')
1985
+ .option('--agent-auth-token <token>', 'Agent auth token')
1986
+ .option('--agent-auth-token-stdin', 'Read agent auth token from stdin', false)
1987
+ .option(
1988
+ '--allow-legacy-agent-auth-source',
1989
+ 'Allow deprecated argv/config/env fallback for agent auth token',
1990
+ false,
1991
+ )
1992
+ .option('--nonce <nonce>', 'Explicit nonce override')
1993
+ .option('--value-wei <wei>', 'Value in wei', '0')
1994
+ .option('--data-hex <hex>', 'Calldata hex', '0x')
1995
+ .option('--gas-limit <gas>', 'Gas limit override')
1996
+ .option('--max-fee-per-gas-wei <wei>', 'Max fee per gas override')
1997
+ .option('--max-priority-fee-per-gas-wei <wei>', 'Priority fee per gas override')
1998
+ .option('--tx-type <type>', 'Typed tx value', '0x02')
1999
+ .option('--delegation-enabled', 'Forward delegation flag to Rust signing request', false)
2000
+ .option('--no-wait', 'Do not wait up to 30s for an on-chain receipt after broadcast')
2001
+ .option('--reveal-raw-tx', 'Include the signed raw transaction bytes in output', false)
2002
+ .option('--reveal-signature', 'Include signer r/s/v fields in output', false)
2003
+ .option('--json', 'Print JSON output', false)
2004
+ .action(async (options) => {
2005
+ const config = readConfig();
2006
+ const network = resolveCliNetworkProfile(options.network, config);
2007
+ const rpcUrl = resolveCliRpcUrl(options.rpcUrl, options.network, config);
2008
+ const chainId = network.chainId;
2009
+ const chainInfo = await getChainInfo(rpcUrl);
2010
+ assertRpcChainIdMatches(chainId, chainInfo.chainId);
2011
+ const from = assertAddress(options.from, 'from');
2012
+ const to = assertAddress(options.to, 'to');
2013
+ const valueWei = parseBigIntString(options.valueWei, 'valueWei');
2014
+ const dataHex = assertHex(options.dataHex, 'dataHex');
2015
+ const nonce = options.nonce
2016
+ ? parseIntegerString(options.nonce, 'nonce')
2017
+ : await getNonce(rpcUrl, from);
2018
+ const gasLimit = options.gasLimit
2019
+ ? parsePositiveBigIntString(options.gasLimit, 'gasLimit')
2020
+ : await estimateGas({ rpcUrl, from, to, value: valueWei, data: dataHex });
2021
+ const fees = await estimateFees(rpcUrl);
2022
+ const maxFeePerGasWei = options.maxFeePerGasWei
2023
+ ? parsePositiveBigIntString(options.maxFeePerGasWei, 'maxFeePerGasWei')
2024
+ : (fees.maxFeePerGas ?? fees.gasPrice);
2025
+ const maxPriorityFeePerGasWei = options.maxPriorityFeePerGasWei
2026
+ ? parseBigIntString(options.maxPriorityFeePerGasWei, 'maxPriorityFeePerGasWei')
2027
+ : (fees.maxPriorityFeePerGas ?? fees.gasPrice ?? 0n);
2028
+
2029
+ if (maxFeePerGasWei === null || maxFeePerGasWei <= 0n) {
2030
+ throw new Error('Could not determine maxFeePerGas; pass --max-fee-per-gas-wei');
2031
+ }
2032
+
2033
+ const signed = await runAgentCommandJson<RustBroadcastOutput>({
2034
+ commandArgs: [
2035
+ 'broadcast',
2036
+ '--network',
2037
+ String(chainId),
2038
+ '--nonce',
2039
+ String(nonce),
2040
+ '--to',
2041
+ to,
2042
+ '--value-wei',
2043
+ valueWei.toString(),
2044
+ '--data-hex',
2045
+ dataHex,
2046
+ '--gas-limit',
2047
+ gasLimit.toString(),
2048
+ '--max-fee-per-gas-wei',
2049
+ maxFeePerGasWei.toString(),
2050
+ '--max-priority-fee-per-gas-wei',
2051
+ maxPriorityFeePerGasWei.toString(),
2052
+ '--tx-type',
2053
+ options.txType,
2054
+ ...(options.delegationEnabled ? ['--delegation-enabled'] : []),
2055
+ ],
2056
+ auth: options,
2057
+ config,
2058
+ asJson: options.json,
2059
+ });
2060
+ if (!signed) {
2061
+ return;
2062
+ }
2063
+
2064
+ if (!signed.raw_tx_hex) {
2065
+ throw new Error('Rust agent did not return raw_tx_hex for broadcast signing');
2066
+ }
2067
+
2068
+ const inspected = await assertSignedBroadcastTransactionMatchesRequest({
2069
+ rawTxHex: signed.raw_tx_hex as Hex,
2070
+ from,
2071
+ to,
2072
+ chainId,
2073
+ nonce,
2074
+ allowHigherNonce: true,
2075
+ value: valueWei,
2076
+ data: dataHex,
2077
+ gasLimit,
2078
+ maxFeePerGas: maxFeePerGasWei,
2079
+ maxPriorityFeePerGas: maxPriorityFeePerGasWei,
2080
+ txType: options.txType,
2081
+ });
2082
+
2083
+ const networkTxHash = await broadcastRawTransaction(rpcUrl, signed.raw_tx_hex as Hex);
2084
+ print(
2085
+ {
2086
+ command: 'broadcast',
2087
+ rpcUrl,
2088
+ chainId,
2089
+ nonce: inspected.nonce,
2090
+ gasLimit: gasLimit.toString(),
2091
+ maxFeePerGasWei: maxFeePerGasWei.toString(),
2092
+ maxPriorityFeePerGasWei: maxPriorityFeePerGasWei.toString(),
2093
+ signedTxHash: signed.tx_hash_hex,
2094
+ networkTxHash,
2095
+ rawTxHex: options.revealRawTx ? signed.raw_tx_hex : '<redacted>',
2096
+ signer: options.revealSignature
2097
+ ? {
2098
+ r: signed.r_hex,
2099
+ s: signed.s_hex,
2100
+ v: signed.v,
2101
+ }
2102
+ : '<redacted>',
2103
+ },
2104
+ options.json,
2105
+ );
2106
+ if (options.wait) {
2107
+ await reportOnchainReceiptStatus({
2108
+ rpcUrl,
2109
+ txHash: networkTxHash,
2110
+ asJson: options.json,
2111
+ });
2112
+ }
2113
+ });
2114
+
2115
+ await program.parseAsync(process.argv);
2116
+ }
2117
+
2118
+ main().catch((error) => {
2119
+ console.error(error instanceof Error ? error.message : String(error));
2120
+ process.exitCode = 1;
2121
+ });