@wlfi-agent/cli 1.4.12 → 1.4.14

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 +49 -43
  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 -1894
  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 -1894
@@ -0,0 +1,180 @@
1
+ use anyhow::{anyhow, bail, Result};
2
+
3
+ const MAX_U128_DECIMALS: u8 = 38;
4
+
5
+ pub(super) fn format_token_amount(raw: u128, decimals: u8) -> Result<String> {
6
+ if decimals > MAX_U128_DECIMALS {
7
+ bail!("token decimals must be <= {MAX_U128_DECIMALS} for u128 formatting");
8
+ }
9
+ if decimals == 0 {
10
+ return Ok(raw.to_string());
11
+ }
12
+
13
+ let scale = pow10(decimals)?;
14
+ let whole = raw / scale;
15
+ let fractional = raw % scale;
16
+ if fractional == 0 {
17
+ return Ok(whole.to_string());
18
+ }
19
+
20
+ let mut fractional_text = format!("{fractional:0width$}", width = decimals as usize);
21
+ while fractional_text.ends_with('0') {
22
+ fractional_text.pop();
23
+ }
24
+ Ok(format!("{whole}.{fractional_text}"))
25
+ }
26
+
27
+ pub(super) fn format_gwei_amount(raw_wei: u128) -> Result<String> {
28
+ format_token_amount(raw_wei, 9)
29
+ }
30
+
31
+ pub(super) fn parse_required_token_amount(label: &str, value: &str, decimals: u8) -> Result<u128> {
32
+ parse_token_amount(label, value, decimals, false)
33
+ }
34
+
35
+ pub(super) fn parse_optional_token_amount(
36
+ label: &str,
37
+ value: Option<&str>,
38
+ decimals: u8,
39
+ ) -> Result<u128> {
40
+ match value {
41
+ Some(value) if !value.trim().is_empty() => parse_token_amount(label, value, decimals, true),
42
+ _ => Ok(0),
43
+ }
44
+ }
45
+
46
+ pub(super) fn parse_optional_gwei_amount(label: &str, value: Option<&str>) -> Result<u128> {
47
+ parse_optional_token_amount(label, value, 9)
48
+ }
49
+
50
+ pub(super) fn parse_legacy_amount(label: &str, value: f64, decimals: u8) -> Result<u128> {
51
+ if !value.is_finite() || value <= 0.0 {
52
+ bail!("{label} must be a positive finite number");
53
+ }
54
+ parse_required_token_amount(label, &value.to_string(), decimals)
55
+ }
56
+
57
+ fn parse_token_amount(label: &str, value: &str, decimals: u8, allow_zero: bool) -> Result<u128> {
58
+ if decimals > MAX_U128_DECIMALS {
59
+ bail!("{label} token decimals must be <= {MAX_U128_DECIMALS}");
60
+ }
61
+
62
+ let trimmed = value.trim();
63
+ if trimmed.is_empty() {
64
+ bail!("{label} is required");
65
+ }
66
+ if trimmed.starts_with('-') {
67
+ bail!("{label} must not be negative");
68
+ }
69
+
70
+ let mut parts = trimmed.split('.');
71
+ let whole_part = parts.next().unwrap_or_default();
72
+ let fractional_part = parts.next();
73
+ if parts.next().is_some() {
74
+ bail!("{label} must be a decimal number");
75
+ }
76
+
77
+ let whole_digits = if whole_part.is_empty() {
78
+ "0"
79
+ } else {
80
+ whole_part
81
+ };
82
+ if !whole_digits.chars().all(|ch| ch.is_ascii_digit()) {
83
+ bail!("{label} must be a decimal number");
84
+ }
85
+
86
+ let fractional_digits = fractional_part.unwrap_or_default();
87
+ if !fractional_digits.chars().all(|ch| ch.is_ascii_digit()) {
88
+ bail!("{label} must be a decimal number");
89
+ }
90
+ if fractional_digits.len() > decimals as usize {
91
+ bail!("{label} must use at most {} decimal places", decimals);
92
+ }
93
+
94
+ let scale = pow10(decimals)?;
95
+ let whole = whole_digits
96
+ .parse::<u128>()
97
+ .map_err(|_| anyhow!("{label} is too large"))?;
98
+ let whole_scaled = whole
99
+ .checked_mul(scale)
100
+ .ok_or_else(|| anyhow!("{label} is too large"))?;
101
+
102
+ let mut fractional_text = fractional_digits.to_string();
103
+ while fractional_text.len() < decimals as usize {
104
+ fractional_text.push('0');
105
+ }
106
+ let fractional = if fractional_text.is_empty() {
107
+ 0
108
+ } else {
109
+ fractional_text
110
+ .parse::<u128>()
111
+ .map_err(|_| anyhow!("{label} is too large"))?
112
+ };
113
+
114
+ let value = whole_scaled
115
+ .checked_add(fractional)
116
+ .ok_or_else(|| anyhow!("{label} is too large"))?;
117
+ if !allow_zero && value == 0 {
118
+ bail!("{label} must be greater than zero");
119
+ }
120
+ Ok(value)
121
+ }
122
+
123
+ fn pow10(decimals: u8) -> Result<u128> {
124
+ let mut value = 1_u128;
125
+ for _ in 0..decimals {
126
+ value = value
127
+ .checked_mul(10)
128
+ .ok_or_else(|| anyhow!("10^{} overflows u128", decimals))?;
129
+ }
130
+ Ok(value)
131
+ }
132
+
133
+ #[cfg(test)]
134
+ mod tests {
135
+ use super::{
136
+ format_gwei_amount, format_token_amount, parse_optional_gwei_amount,
137
+ parse_optional_token_amount, parse_required_token_amount,
138
+ };
139
+
140
+ #[test]
141
+ fn parse_required_token_amount_accepts_decimal_strings() {
142
+ assert_eq!(
143
+ parse_required_token_amount("amount", "1.25", 6).expect("amount"),
144
+ 1_250_000
145
+ );
146
+ }
147
+
148
+ #[test]
149
+ fn parse_required_token_amount_rejects_excess_precision() {
150
+ let err = parse_required_token_amount("amount", "1.0000001", 6).expect_err("must reject");
151
+ assert!(err
152
+ .to_string()
153
+ .contains("must use at most 6 decimal places"));
154
+ }
155
+
156
+ #[test]
157
+ fn parse_optional_token_amount_defaults_blank_to_zero() {
158
+ assert_eq!(
159
+ parse_optional_token_amount("amount", Some(""), 18).expect("blank"),
160
+ 0
161
+ );
162
+ assert_eq!(
163
+ parse_optional_token_amount("amount", None, 18).expect("none"),
164
+ 0
165
+ );
166
+ }
167
+
168
+ #[test]
169
+ fn format_token_amount_trims_trailing_zeroes() {
170
+ assert_eq!(format_token_amount(1_250_000, 6).expect("format"), "1.25");
171
+ assert_eq!(format_token_amount(1_000_000, 6).expect("format"), "1");
172
+ }
173
+
174
+ #[test]
175
+ fn gwei_helpers_round_trip() {
176
+ let raw = parse_optional_gwei_amount("fee", Some("2.5")).expect("parse");
177
+ assert_eq!(raw, 2_500_000_000);
178
+ assert_eq!(format_gwei_amount(raw).expect("format"), "2.5");
179
+ }
180
+ }
@@ -0,0 +1,250 @@
1
+ use anyhow::{anyhow, bail, Context, Result};
2
+ use hex::FromHex;
3
+ use reqwest::Client;
4
+ use serde::{Deserialize, Serialize};
5
+ use serde_json::{json, Value};
6
+ use vault_domain::EvmAddress;
7
+
8
+ #[derive(Debug, Clone, PartialEq, Eq)]
9
+ pub(super) struct FetchedTokenMetadata {
10
+ pub(super) chain_id: u64,
11
+ pub(super) name: String,
12
+ pub(super) symbol: String,
13
+ pub(super) decimals: u8,
14
+ }
15
+
16
+ #[derive(Debug, Serialize)]
17
+ struct RpcRequest<'a> {
18
+ jsonrpc: &'static str,
19
+ method: &'a str,
20
+ params: Value,
21
+ id: u64,
22
+ }
23
+
24
+ #[derive(Debug, Deserialize)]
25
+ struct RpcResponse {
26
+ result: Option<Value>,
27
+ error: Option<RpcError>,
28
+ }
29
+
30
+ #[derive(Debug, Deserialize)]
31
+ struct RpcError {
32
+ code: i64,
33
+ message: String,
34
+ }
35
+
36
+ pub(super) async fn fetch_token_metadata(
37
+ chain_key: &str,
38
+ rpc_url: &str,
39
+ expected_chain_id: u64,
40
+ is_native: bool,
41
+ address: Option<&EvmAddress>,
42
+ ) -> Result<FetchedTokenMetadata> {
43
+ let client = Client::new();
44
+ let chain_id = fetch_chain_id(&client, rpc_url).await?;
45
+ if chain_id != expected_chain_id {
46
+ bail!(
47
+ "rpc url returned chain id {} but the saved network expects {}",
48
+ chain_id,
49
+ expected_chain_id
50
+ );
51
+ }
52
+
53
+ if is_native {
54
+ let (name, symbol) = native_metadata_for_chain(chain_id, chain_key);
55
+ return Ok(FetchedTokenMetadata {
56
+ chain_id,
57
+ name,
58
+ symbol,
59
+ decimals: 18,
60
+ });
61
+ }
62
+
63
+ let address = address.context("token address is required to fetch metadata")?;
64
+ let decimals = read_erc20_decimals(&client, rpc_url, address).await?;
65
+ let symbol = read_erc20_string(&client, rpc_url, address, "0x95d89b41")
66
+ .await
67
+ .context("failed to read token symbol")?;
68
+ let name = read_erc20_string(&client, rpc_url, address, "0x06fdde03")
69
+ .await
70
+ .context("failed to read token name")?;
71
+
72
+ Ok(FetchedTokenMetadata {
73
+ chain_id,
74
+ name,
75
+ symbol,
76
+ decimals,
77
+ })
78
+ }
79
+
80
+ async fn fetch_chain_id(client: &Client, rpc_url: &str) -> Result<u64> {
81
+ let response = call_rpc(client, rpc_url, "eth_chainId", json!([])).await?;
82
+ let result = response
83
+ .as_str()
84
+ .context("eth_chainId returned a non-string result")?;
85
+ parse_hex_u64(result, "eth_chainId")
86
+ }
87
+
88
+ async fn read_erc20_decimals(client: &Client, rpc_url: &str, address: &EvmAddress) -> Result<u8> {
89
+ let result = eth_call(client, rpc_url, address, "0x313ce567").await?;
90
+ let decoded = decode_hex_bytes(&result)?;
91
+ if decoded.len() != 32 {
92
+ bail!("decimals() returned {} bytes; expected 32", decoded.len());
93
+ }
94
+ let decimals = *decoded
95
+ .last()
96
+ .context("decimals() returned an empty payload")?;
97
+ Ok(decimals)
98
+ }
99
+
100
+ async fn read_erc20_string(
101
+ client: &Client,
102
+ rpc_url: &str,
103
+ address: &EvmAddress,
104
+ selector: &str,
105
+ ) -> Result<String> {
106
+ let result = eth_call(client, rpc_url, address, selector).await?;
107
+ decode_abi_string(&result)
108
+ }
109
+
110
+ async fn eth_call(
111
+ client: &Client,
112
+ rpc_url: &str,
113
+ address: &EvmAddress,
114
+ data: &str,
115
+ ) -> Result<String> {
116
+ let result = call_rpc(
117
+ client,
118
+ rpc_url,
119
+ "eth_call",
120
+ json!([
121
+ {
122
+ "to": address.to_string(),
123
+ "data": data,
124
+ },
125
+ "latest"
126
+ ]),
127
+ )
128
+ .await?;
129
+ result
130
+ .as_str()
131
+ .map(ToString::to_string)
132
+ .context("eth_call returned a non-string result")
133
+ }
134
+
135
+ async fn call_rpc(client: &Client, rpc_url: &str, method: &str, params: Value) -> Result<Value> {
136
+ let request = RpcRequest {
137
+ jsonrpc: "2.0",
138
+ method,
139
+ params,
140
+ id: 1,
141
+ };
142
+ let response = client
143
+ .post(rpc_url.trim())
144
+ .json(&request)
145
+ .send()
146
+ .await
147
+ .with_context(|| format!("rpc call {method} failed"))?;
148
+ let response = response
149
+ .error_for_status()
150
+ .with_context(|| format!("rpc call {method} failed"))?;
151
+ let payload: RpcResponse = response
152
+ .json()
153
+ .await
154
+ .with_context(|| format!("rpc call {method} returned invalid json"))?;
155
+ if let Some(error) = payload.error {
156
+ bail!(
157
+ "rpc call {method} failed: {} ({})",
158
+ error.message,
159
+ error.code
160
+ );
161
+ }
162
+ payload
163
+ .result
164
+ .ok_or_else(|| anyhow!("rpc call {method} returned no result"))
165
+ }
166
+
167
+ fn native_metadata_for_chain(chain_id: u64, chain_key: &str) -> (String, String) {
168
+ match chain_id {
169
+ 1 | 10 | 8453 | 84532 | 42161 | 11155111 => ("Ether".to_string(), "ETH".to_string()),
170
+ 56 => ("BNB".to_string(), "BNB".to_string()),
171
+ 137 => ("MATIC".to_string(), "MATIC".to_string()),
172
+ _ => (format!("Native Asset ({chain_key})"), "NATIVE".to_string()),
173
+ }
174
+ }
175
+
176
+ fn parse_hex_u64(value: &str, label: &str) -> Result<u64> {
177
+ let trimmed = value.trim();
178
+ let digits = trimmed
179
+ .strip_prefix("0x")
180
+ .or_else(|| trimmed.strip_prefix("0X"))
181
+ .context(format!("{label} must be a 0x-prefixed hex string"))?;
182
+ u64::from_str_radix(digits, 16).with_context(|| format!("{label} must be valid hex"))
183
+ }
184
+
185
+ fn decode_hex_bytes(value: &str) -> Result<Vec<u8>> {
186
+ let trimmed = value.trim();
187
+ let digits = trimmed
188
+ .strip_prefix("0x")
189
+ .or_else(|| trimmed.strip_prefix("0X"))
190
+ .context("hex payload must be 0x-prefixed")?;
191
+ Vec::from_hex(digits).context("hex payload must contain valid hex bytes")
192
+ }
193
+
194
+ fn decode_abi_string(value: &str) -> Result<String> {
195
+ let bytes = decode_hex_bytes(value)?;
196
+ if bytes.len() < 64 {
197
+ bail!("abi string payload is too short");
198
+ }
199
+
200
+ let offset = decode_abi_usize(&bytes[0..32]).context("invalid abi string offset")?;
201
+ if offset + 32 > bytes.len() {
202
+ bail!("abi string offset points past the payload");
203
+ }
204
+ let length =
205
+ decode_abi_usize(&bytes[offset..offset + 32]).context("invalid abi string length")?;
206
+ let start = offset + 32;
207
+ let end = start + length;
208
+ if end > bytes.len() {
209
+ bail!("abi string length points past the payload");
210
+ }
211
+
212
+ String::from_utf8(bytes[start..end].to_vec()).context("abi string is not valid utf-8")
213
+ }
214
+
215
+ fn decode_abi_usize(bytes: &[u8]) -> Result<usize> {
216
+ if bytes.len() != 32 {
217
+ bail!("abi integer must be 32 bytes");
218
+ }
219
+ if bytes[..24].iter().any(|byte| *byte != 0) {
220
+ bail!("abi integer does not fit into usize");
221
+ }
222
+ let value = u64::from_be_bytes(
223
+ bytes[24..32]
224
+ .try_into()
225
+ .map_err(|_| anyhow!("abi integer tail is malformed"))?,
226
+ );
227
+ usize::try_from(value).context("abi integer does not fit into usize")
228
+ }
229
+
230
+ #[cfg(test)]
231
+ mod tests {
232
+ use super::{decode_abi_string, parse_hex_u64};
233
+
234
+ #[test]
235
+ fn parse_hex_u64_reads_eth_chain_id_payloads() {
236
+ assert_eq!(parse_hex_u64("0x1", "chain").expect("chain"), 1);
237
+ assert_eq!(parse_hex_u64("0xa4b1", "chain").expect("chain"), 42161);
238
+ }
239
+
240
+ #[test]
241
+ fn decode_abi_string_reads_dynamic_strings() {
242
+ let encoded = concat!(
243
+ "0x",
244
+ "0000000000000000000000000000000000000000000000000000000000000020",
245
+ "0000000000000000000000000000000000000000000000000000000000000004",
246
+ "5553444300000000000000000000000000000000000000000000000000000000"
247
+ );
248
+ assert_eq!(decode_abi_string(encoded).expect("string"), "USDC");
249
+ }
250
+ }
@@ -0,0 +1,82 @@
1
+ use anyhow::{anyhow, bail, Result};
2
+ use vault_domain::EvmAddress;
3
+
4
+ use super::Field;
5
+
6
+ pub(super) fn parse_positive_u128(label: &str, value: &str) -> Result<u128> {
7
+ let trimmed = value.trim();
8
+ let parsed = trimmed
9
+ .parse::<u128>()
10
+ .map_err(|_| anyhow!("{label} must be a valid unsigned integer"))?;
11
+ if parsed == 0 {
12
+ bail!("{label} must be greater than zero");
13
+ }
14
+ Ok(parsed)
15
+ }
16
+
17
+ pub(super) fn parse_non_negative_u128(label: &str, value: &str) -> Result<u128> {
18
+ value
19
+ .trim()
20
+ .parse::<u128>()
21
+ .map_err(|_| anyhow!("{label} must be a valid unsigned integer"))
22
+ }
23
+
24
+ pub(super) fn parse_positive_u64(label: &str, value: &str) -> Result<u64> {
25
+ let trimmed = value.trim();
26
+ let parsed = trimmed
27
+ .parse::<u64>()
28
+ .map_err(|_| anyhow!("{label} must be a valid unsigned integer"))?;
29
+ if parsed == 0 {
30
+ bail!("{label} must be greater than zero");
31
+ }
32
+ Ok(parsed)
33
+ }
34
+
35
+ pub(super) fn parse_address(label: &str, value: &str) -> Result<EvmAddress> {
36
+ value
37
+ .trim()
38
+ .parse::<EvmAddress>()
39
+ .map_err(|err| anyhow!("invalid {label} address: {err}"))
40
+ }
41
+
42
+ pub(super) fn bool_label(value: bool) -> &'static str {
43
+ if value {
44
+ "yes"
45
+ } else {
46
+ "no"
47
+ }
48
+ }
49
+
50
+ pub(super) fn is_allowed_input_char(field: Field, ch: char) -> bool {
51
+ match field {
52
+ Field::TokenKey | Field::ChainConfigKey => {
53
+ ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.')
54
+ }
55
+ Field::PerTxLimit
56
+ | Field::DailyLimit
57
+ | Field::WeeklyLimit
58
+ | Field::PerTxMaxFeePerGasGwei
59
+ | Field::OverridePerTxLimit
60
+ | Field::OverrideDailyLimit
61
+ | Field::OverrideWeeklyLimit
62
+ | Field::OverridePerTxMaxFeePerGasGwei
63
+ | Field::ManualApprovalMinAmount
64
+ | Field::ManualApprovalMaxAmount => ch.is_ascii_digit() || ch == '.',
65
+ Field::MaxGasPerChainWei
66
+ | Field::DailyMaxTxCount
67
+ | Field::PerTxMaxPriorityFeePerGasWei
68
+ | Field::PerTxMaxCalldataBytes
69
+ | Field::OverrideMaxGasPerChainWei
70
+ | Field::OverrideDailyMaxTxCount
71
+ | Field::OverridePerTxMaxPriorityFeePerGasWei
72
+ | Field::OverridePerTxMaxCalldataBytes
73
+ | Field::ManualApprovalPriority
74
+ | Field::ChainConfigId => ch.is_ascii_digit(),
75
+ Field::NetworkAddress
76
+ | Field::OverrideRecipientAddress
77
+ | Field::ManualApprovalRecipientAddress => {
78
+ ch.is_ascii_hexdigit() || matches!(ch, 'x' | 'X')
79
+ }
80
+ _ => true,
81
+ }
82
+ }