@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
@@ -0,0 +1,651 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ use alloy_primitives::U256;
4
+ use alloy_sol_types::{sol, SolCall};
5
+ use time::OffsetDateTime;
6
+ use uuid::Uuid;
7
+
8
+ use super::*;
9
+
10
+ sol! {
11
+ function approve(address spender, uint256 value);
12
+ function transfer(address to, uint256 value);
13
+ }
14
+
15
+ #[test]
16
+ fn address_parser_validates_prefix_and_length() {
17
+ let valid = "0x1111111111111111111111111111111111111111"
18
+ .parse::<EvmAddress>()
19
+ .expect("must parse");
20
+ assert_eq!(valid.as_str(), "0x1111111111111111111111111111111111111111");
21
+
22
+ let missing_prefix = "1111111111111111111111111111111111111111".parse::<EvmAddress>();
23
+ assert!(missing_prefix.is_err());
24
+
25
+ let too_short = "0x1234".parse::<EvmAddress>();
26
+ assert!(too_short.is_err());
27
+ }
28
+
29
+ #[test]
30
+ fn manual_approval_capability_token_is_deterministic_per_request() {
31
+ let approval_request_id =
32
+ Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").expect("uuid");
33
+ let relay_private_key_hex = "11".repeat(32);
34
+
35
+ let first = manual_approval_capability_token(&relay_private_key_hex, approval_request_id)
36
+ .expect("must derive token");
37
+ let second = manual_approval_capability_token(&relay_private_key_hex, approval_request_id)
38
+ .expect("must derive token");
39
+
40
+ assert_eq!(first, second);
41
+ assert_eq!(first.len(), 64);
42
+
43
+ let capability_hash = manual_approval_capability_hash(&first).expect("must hash token");
44
+ assert_eq!(capability_hash.len(), 64);
45
+ assert_ne!(capability_hash, first);
46
+ }
47
+
48
+ #[test]
49
+ fn manual_approval_capability_token_rejects_invalid_secret() {
50
+ let approval_request_id = Uuid::new_v4();
51
+ let err = manual_approval_capability_token("not-hex", approval_request_id)
52
+ .expect_err("must reject invalid secret");
53
+ assert!(matches!(err, DomainError::InvalidRelayCapabilitySecret));
54
+ }
55
+
56
+ #[test]
57
+ fn address_deserialize_rejects_invalid_values() {
58
+ let invalid =
59
+ serde_json::from_str::<EvmAddress>(r#""not-an-address""#).expect_err("must reject");
60
+ assert!(invalid.to_string().contains("address"));
61
+ }
62
+
63
+ #[test]
64
+ fn address_deserialize_normalizes_case() {
65
+ let parsed =
66
+ serde_json::from_str::<EvmAddress>(r#""0xABCD000000000000000000000000000000000000""#)
67
+ .expect("must deserialize");
68
+ assert_eq!(
69
+ parsed.as_str(),
70
+ "0xabcd000000000000000000000000000000000000"
71
+ );
72
+ }
73
+
74
+ #[test]
75
+ fn all_scope_allows_any_value() {
76
+ let addr = "0x2222222222222222222222222222222222222222"
77
+ .parse::<EvmAddress>()
78
+ .expect("must parse");
79
+ let scope: EntityScope<EvmAddress> = EntityScope::All;
80
+
81
+ assert!(scope.allows(&addr));
82
+ }
83
+
84
+ #[test]
85
+ fn policy_set_cannot_be_empty() {
86
+ let empty = BTreeSet::new();
87
+ let result = PolicyAttachment::policy_set(empty);
88
+ assert!(matches!(result, Err(DomainError::EmptyPolicySet)));
89
+ }
90
+
91
+ #[test]
92
+ fn parse_erc20_transfer_call_succeeds() {
93
+ let to = alloy_primitives::Address::from([0x11; 20]);
94
+ let amount = U256::from(42_u64);
95
+ let calldata = transferCall { to, value: amount }.abi_encode();
96
+
97
+ let parsed = parse_erc20_call(&calldata).expect("decode");
98
+ assert_eq!(
99
+ parsed,
100
+ Erc20Call::Transfer {
101
+ to: "0x1111111111111111111111111111111111111111"
102
+ .parse()
103
+ .expect("address"),
104
+ amount_wei: 42,
105
+ }
106
+ );
107
+ }
108
+
109
+ #[test]
110
+ fn parse_erc20_approve_call_succeeds() {
111
+ let spender = alloy_primitives::Address::from([0x22; 20]);
112
+ let amount = U256::from(1337_u64);
113
+ let calldata = approveCall {
114
+ spender,
115
+ value: amount,
116
+ }
117
+ .abi_encode();
118
+
119
+ let action = action_from_erc20_calldata(
120
+ 1,
121
+ "0x3333333333333333333333333333333333333333"
122
+ .parse()
123
+ .expect("token"),
124
+ &calldata,
125
+ )
126
+ .expect("action");
127
+
128
+ assert_eq!(
129
+ action,
130
+ AgentAction::Approve {
131
+ chain_id: 1,
132
+ token: "0x3333333333333333333333333333333333333333"
133
+ .parse()
134
+ .expect("token"),
135
+ spender: "0x2222222222222222222222222222222222222222"
136
+ .parse()
137
+ .expect("spender"),
138
+ amount_wei: 1337,
139
+ }
140
+ );
141
+ }
142
+
143
+ #[test]
144
+ fn parse_erc20_call_rejects_unknown_selector() {
145
+ let err = parse_erc20_call(&[0xde, 0xad, 0xbe, 0xef, 0x00]).expect_err("must fail");
146
+ assert!(matches!(err, DomainError::InvalidErc20Calldata(_)));
147
+ }
148
+
149
+ #[test]
150
+ fn broadcast_tx_rejects_delegation() {
151
+ let tx = BroadcastTx {
152
+ chain_id: 1,
153
+ nonce: 0,
154
+ to: "0x1234000000000000000000000000000000000000"
155
+ .parse()
156
+ .expect("to"),
157
+ value_wei: 0,
158
+ data_hex: "0x".to_string(),
159
+ gas_limit: 21_000,
160
+ max_fee_per_gas_wei: 1_000_000_000,
161
+ max_priority_fee_per_gas_wei: 1_000_000_000,
162
+ tx_type: 0x02,
163
+ delegation_enabled: true,
164
+ };
165
+
166
+ let err = tx.validate().expect_err("must reject delegation");
167
+ assert!(matches!(err, DomainError::DelegationNotAllowed));
168
+ }
169
+
170
+ #[test]
171
+ fn broadcast_tx_rejects_delegation_for_eip7702() {
172
+ let tx = BroadcastTx {
173
+ chain_id: 1,
174
+ nonce: 0,
175
+ to: "0x1234000000000000000000000000000000000000"
176
+ .parse()
177
+ .expect("to"),
178
+ value_wei: 0,
179
+ data_hex: "0x".to_string(),
180
+ gas_limit: 21_000,
181
+ max_fee_per_gas_wei: 1_000_000_000,
182
+ max_priority_fee_per_gas_wei: 1_000_000_000,
183
+ tx_type: EIP7702_TX_TYPE,
184
+ delegation_enabled: true,
185
+ };
186
+
187
+ let err = tx.validate().expect_err("must reject delegation");
188
+ assert!(matches!(err, DomainError::DelegationNotAllowed));
189
+ }
190
+
191
+ #[test]
192
+ fn broadcast_tx_rejects_erc20_call_with_native_value() {
193
+ let to = alloy_primitives::Address::from([0x11; 20]);
194
+ let amount = U256::from(7_u64);
195
+ let calldata = transferCall { to, value: amount }.abi_encode();
196
+
197
+ let tx = BroadcastTx {
198
+ chain_id: 1,
199
+ nonce: 0,
200
+ to: "0x1000000000000000000000000000000000000000"
201
+ .parse()
202
+ .expect("token"),
203
+ value_wei: 1,
204
+ data_hex: format!("0x{}", hex::encode(calldata)),
205
+ gas_limit: 60_000,
206
+ max_fee_per_gas_wei: 1_000_000_000,
207
+ max_priority_fee_per_gas_wei: 1_000_000_000,
208
+ tx_type: 0x02,
209
+ delegation_enabled: false,
210
+ };
211
+
212
+ let err = tx.validate().expect_err("must reject mixed erc20/native");
213
+ assert!(matches!(err, DomainError::Erc20CallWithNativeValue));
214
+ }
215
+
216
+ #[test]
217
+ fn broadcast_action_derives_erc20_scope_from_calldata() {
218
+ let to = alloy_primitives::Address::from([0x33; 20]);
219
+ let calldata = transferCall {
220
+ to,
221
+ value: U256::from(77_u64),
222
+ }
223
+ .abi_encode();
224
+ let token: EvmAddress = "0x4444000000000000000000000000000000000000"
225
+ .parse()
226
+ .expect("token");
227
+ let tx = BroadcastTx {
228
+ chain_id: 1,
229
+ nonce: 0,
230
+ to: token.clone(),
231
+ value_wei: 0,
232
+ data_hex: format!("0x{}", hex::encode(calldata)),
233
+ gas_limit: 60_000,
234
+ max_fee_per_gas_wei: 1_000_000_000,
235
+ max_priority_fee_per_gas_wei: 1_000_000_000,
236
+ tx_type: 0x02,
237
+ delegation_enabled: false,
238
+ };
239
+ let action = AgentAction::BroadcastTx { tx };
240
+
241
+ assert_eq!(action.asset(), AssetId::Erc20(token));
242
+ assert_eq!(action.amount_wei(), 77);
243
+ assert_eq!(
244
+ action.recipient(),
245
+ "0x3333333333333333333333333333333333333333"
246
+ .parse()
247
+ .expect("recipient")
248
+ );
249
+ }
250
+
251
+ #[test]
252
+ fn broadcast_action_unknown_selector_uses_native_fields() {
253
+ let to: EvmAddress = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
254
+ .parse()
255
+ .expect("to");
256
+ let tx = BroadcastTx {
257
+ chain_id: 1,
258
+ nonce: 0,
259
+ to: to.clone(),
260
+ value_wei: 123,
261
+ data_hex: "0xdeadbeef".to_string(),
262
+ gas_limit: 21_000,
263
+ max_fee_per_gas_wei: 1_000_000_000,
264
+ max_priority_fee_per_gas_wei: 1_000_000_000,
265
+ tx_type: 0x02,
266
+ delegation_enabled: false,
267
+ };
268
+ let action = AgentAction::BroadcastTx { tx };
269
+
270
+ assert_eq!(action.asset(), AssetId::NativeEth);
271
+ assert_eq!(action.amount_wei(), 123);
272
+ assert_eq!(action.recipient(), to);
273
+ }
274
+
275
+ #[test]
276
+ fn debug_output_redacts_secret_fields() {
277
+ let lease = Lease {
278
+ lease_id: Uuid::nil(),
279
+ issued_at: OffsetDateTime::UNIX_EPOCH,
280
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(5),
281
+ };
282
+ let session = AdminSession {
283
+ vault_password: "vault-password".to_string(),
284
+ lease: lease.clone(),
285
+ };
286
+ let credentials = AgentCredentials {
287
+ agent_key: AgentKey {
288
+ id: Uuid::nil(),
289
+ vault_key_id: Uuid::nil(),
290
+ policies: PolicyAttachment::AllPolicies,
291
+ created_at: OffsetDateTime::UNIX_EPOCH,
292
+ },
293
+ auth_token: "agent-secret".to_string(),
294
+ };
295
+ let request = SignRequest {
296
+ request_id: Uuid::nil(),
297
+ agent_key_id: Uuid::nil(),
298
+ agent_auth_token: "agent-secret".to_string(),
299
+ payload: vec![1, 2, 3],
300
+ action: AgentAction::TransferNative {
301
+ chain_id: 1,
302
+ to: "0x2222222222222222222222222222222222222222"
303
+ .parse()
304
+ .expect("to"),
305
+ amount_wei: 1,
306
+ },
307
+ requested_at: OffsetDateTime::UNIX_EPOCH,
308
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1),
309
+ };
310
+
311
+ let session_debug = format!("{session:?}");
312
+ let credentials_debug = format!("{credentials:?}");
313
+ let request_debug = format!("{request:?}");
314
+
315
+ assert!(!session_debug.contains("vault-password"));
316
+ assert!(session_debug.contains("<redacted>"));
317
+ assert!(!credentials_debug.contains("agent-secret"));
318
+ assert!(credentials_debug.contains("<redacted>"));
319
+ assert!(!request_debug.contains("agent-secret"));
320
+ assert!(request_debug.contains("<redacted>"));
321
+ }
322
+
323
+ #[test]
324
+ fn broadcast_action_exposes_gas_and_calldata_metadata() {
325
+ let action = AgentAction::BroadcastTx {
326
+ tx: BroadcastTx {
327
+ chain_id: 1,
328
+ nonce: 0,
329
+ to: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
330
+ .parse()
331
+ .expect("to"),
332
+ value_wei: 0,
333
+ data_hex: "0xdeadbeef".to_string(),
334
+ gas_limit: 21_000,
335
+ max_fee_per_gas_wei: 2_000_000_000,
336
+ max_priority_fee_per_gas_wei: 1_000_000_000,
337
+ tx_type: 0x02,
338
+ delegation_enabled: false,
339
+ },
340
+ };
341
+
342
+ assert_eq!(action.max_fee_per_gas_wei(), Some(2_000_000_000));
343
+ assert_eq!(action.max_priority_fee_per_gas_wei(), Some(1_000_000_000));
344
+ assert_eq!(action.calldata_len_bytes(), Some(4));
345
+ }
346
+
347
+ #[test]
348
+ fn eip1559_signing_message_is_typed_and_non_empty() {
349
+ let tx = BroadcastTx {
350
+ chain_id: 1,
351
+ nonce: 0,
352
+ to: "0xf0109fc8df283027b6285cc889f5aa624eac1f55"
353
+ .parse()
354
+ .expect("to"),
355
+ value_wei: 1_000_000_000,
356
+ data_hex: "0x".to_string(),
357
+ gas_limit: 2_000_000,
358
+ max_fee_per_gas_wei: 21_000_000_000,
359
+ max_priority_fee_per_gas_wei: 1_000_000_000,
360
+ tx_type: 0x02,
361
+ delegation_enabled: false,
362
+ };
363
+
364
+ let signing_message = tx.eip1559_signing_message().expect("signing message");
365
+ assert_eq!(signing_message.first().copied(), Some(0x02));
366
+ assert!(!signing_message.is_empty());
367
+ }
368
+
369
+ #[test]
370
+ fn eip1559_signed_raw_transaction_includes_signature_components() {
371
+ let tx = BroadcastTx {
372
+ chain_id: 1,
373
+ nonce: 0,
374
+ to: "0xf0109fc8df283027b6285cc889f5aa624eac1f55"
375
+ .parse()
376
+ .expect("to"),
377
+ value_wei: 1_000_000_000,
378
+ data_hex: "0x".to_string(),
379
+ gas_limit: 2_000_000,
380
+ max_fee_per_gas_wei: 21_000_000_000,
381
+ max_priority_fee_per_gas_wei: 1_000_000_000,
382
+ tx_type: 0x02,
383
+ delegation_enabled: false,
384
+ };
385
+
386
+ let r = [0x11u8; 32];
387
+ let s = [0x22u8; 32];
388
+ let raw = tx
389
+ .eip1559_signed_raw_transaction(1, r, s)
390
+ .expect("signed raw tx");
391
+ assert_eq!(raw.first().copied(), Some(0x02));
392
+ assert!(!raw.is_empty());
393
+ }
394
+
395
+ #[test]
396
+ fn eip1559_helpers_reject_unsupported_tx_type() {
397
+ let tx = BroadcastTx {
398
+ chain_id: 1,
399
+ nonce: 0,
400
+ to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
401
+ .parse()
402
+ .expect("to"),
403
+ value_wei: 0,
404
+ data_hex: "0x".to_string(),
405
+ gas_limit: 21_000,
406
+ max_fee_per_gas_wei: 1_000_000_000,
407
+ max_priority_fee_per_gas_wei: 1_000_000_000,
408
+ tx_type: EIP7702_TX_TYPE,
409
+ delegation_enabled: false,
410
+ };
411
+ let err = tx
412
+ .eip1559_signing_message()
413
+ .expect_err("must reject non-eip-1559 tx type");
414
+ assert!(matches!(err, DomainError::UnsupportedTransactionType(_)));
415
+ }
416
+
417
+ sol! {
418
+ struct PermitDetails {
419
+ address token;
420
+ uint160 amount;
421
+ uint48 expiration;
422
+ uint48 nonce;
423
+ }
424
+
425
+ struct PermitSingle {
426
+ PermitDetails details;
427
+ address spender;
428
+ uint256 sigDeadline;
429
+ }
430
+
431
+ function permit(address owner, PermitSingle permitSingle, bytes signature);
432
+ function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature);
433
+ }
434
+
435
+ #[test]
436
+ fn broadcast_tx_rejects_permit2_call_with_native_value() {
437
+ let permit_single = PermitSingle {
438
+ details: PermitDetails {
439
+ token: alloy_primitives::Address::from([0x44; 20]),
440
+ amount: alloy_primitives::U160::from_be_slice(&42u128.to_be_bytes()),
441
+ expiration: alloy_primitives::aliases::U48::from_be_slice(&100u64.to_be_bytes()[2..]),
442
+ nonce: alloy_primitives::aliases::U48::from_be_slice(&7u64.to_be_bytes()[2..]),
443
+ },
444
+ spender: alloy_primitives::Address::from([0x55; 20]),
445
+ sigDeadline: U256::from(1234u64),
446
+ };
447
+ let calldata = permitCall {
448
+ owner: alloy_primitives::Address::from([0x66; 20]),
449
+ permitSingle: permit_single,
450
+ signature: vec![0x12, 0x34].into(),
451
+ }
452
+ .abi_encode();
453
+
454
+ let tx = BroadcastTx {
455
+ chain_id: 1,
456
+ nonce: 0,
457
+ to: "0x9999000000000000000000000000000000000000"
458
+ .parse()
459
+ .expect("permit2 contract"),
460
+ value_wei: 1,
461
+ data_hex: format!("0x{}", hex::encode(calldata)),
462
+ gas_limit: 60_000,
463
+ max_fee_per_gas_wei: 1_000_000_000,
464
+ max_priority_fee_per_gas_wei: 1_000_000_000,
465
+ tx_type: 0x02,
466
+ delegation_enabled: false,
467
+ };
468
+
469
+ let err = tx.validate().expect_err("must reject mixed permit2/native");
470
+ assert!(matches!(err, DomainError::Erc20CallWithNativeValue));
471
+ }
472
+
473
+ #[test]
474
+ fn broadcast_action_derives_permit2_scope_from_calldata() {
475
+ let token: EvmAddress = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
476
+ .parse()
477
+ .expect("token");
478
+ let spender: EvmAddress = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
479
+ .parse()
480
+ .expect("spender");
481
+ let permit2_contract: EvmAddress = "0xcccccccccccccccccccccccccccccccccccccccc"
482
+ .parse()
483
+ .expect("permit2 contract");
484
+ let permit_single = PermitSingle {
485
+ details: PermitDetails {
486
+ token: alloy_primitives::Address::from([0xaa; 20]),
487
+ amount: alloy_primitives::U160::from_be_slice(&77u128.to_be_bytes()),
488
+ expiration: alloy_primitives::aliases::U48::from_be_slice(&100u64.to_be_bytes()[2..]),
489
+ nonce: alloy_primitives::aliases::U48::from_be_slice(&5u64.to_be_bytes()[2..]),
490
+ },
491
+ spender: alloy_primitives::Address::from([0xbb; 20]),
492
+ sigDeadline: U256::from(200u64),
493
+ };
494
+ let calldata = permitCall {
495
+ owner: alloy_primitives::Address::from([0xdd; 20]),
496
+ permitSingle: permit_single,
497
+ signature: vec![0xab, 0xcd].into(),
498
+ }
499
+ .abi_encode();
500
+ let action = AgentAction::BroadcastTx {
501
+ tx: BroadcastTx {
502
+ chain_id: 1,
503
+ nonce: 0,
504
+ to: permit2_contract,
505
+ value_wei: 0,
506
+ data_hex: format!("0x{}", hex::encode(calldata)),
507
+ gas_limit: 120_000,
508
+ max_fee_per_gas_wei: 1_000_000_000,
509
+ max_priority_fee_per_gas_wei: 1_000_000_000,
510
+ tx_type: 0x02,
511
+ delegation_enabled: false,
512
+ },
513
+ };
514
+
515
+ assert_eq!(action.asset(), AssetId::Erc20(token));
516
+ assert_eq!(action.recipient(), spender);
517
+ assert_eq!(action.amount_wei(), 77);
518
+ }
519
+
520
+ #[test]
521
+ fn broadcast_action_derives_eip3009_scope_from_calldata() {
522
+ let token: EvmAddress = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
523
+ .parse()
524
+ .expect("token");
525
+ let recipient: EvmAddress = "0xffffffffffffffffffffffffffffffffffffffff"
526
+ .parse()
527
+ .expect("recipient");
528
+ let calldata = transferWithAuthorizationCall {
529
+ from: alloy_primitives::Address::from([0x11; 20]),
530
+ to: alloy_primitives::Address::from([0xff; 20]),
531
+ value: U256::from(91u64),
532
+ validAfter: U256::from(1u64),
533
+ validBefore: U256::from(2u64),
534
+ nonce: [0x55; 32].into(),
535
+ signature: vec![0xde, 0xad].into(),
536
+ }
537
+ .abi_encode();
538
+ let action = AgentAction::BroadcastTx {
539
+ tx: BroadcastTx {
540
+ chain_id: 1,
541
+ nonce: 0,
542
+ to: token.clone(),
543
+ value_wei: 0,
544
+ data_hex: format!("0x{}", hex::encode(calldata)),
545
+ gas_limit: 120_000,
546
+ max_fee_per_gas_wei: 1_000_000_000,
547
+ max_priority_fee_per_gas_wei: 1_000_000_000,
548
+ tx_type: 0x02,
549
+ delegation_enabled: false,
550
+ },
551
+ };
552
+
553
+ assert_eq!(action.asset(), AssetId::Erc20(token));
554
+ assert_eq!(action.recipient(), recipient);
555
+ assert_eq!(action.amount_wei(), 91);
556
+ }
557
+
558
+ #[test]
559
+ fn permit2_and_eip3009_actions_produce_signing_hashes() {
560
+ let permit = Permit2Permit {
561
+ chain_id: 1,
562
+ permit2_contract: "0x000000000022d473030f116ddee9f6b43ac78ba3"
563
+ .parse()
564
+ .expect("permit2"),
565
+ token: "0x1111111111111111111111111111111111111111"
566
+ .parse()
567
+ .expect("token"),
568
+ spender: "0x2222222222222222222222222222222222222222"
569
+ .parse()
570
+ .expect("spender"),
571
+ amount_wei: 123,
572
+ expiration: 1_700_000_000,
573
+ nonce: 7,
574
+ sig_deadline: 1_700_000_100,
575
+ };
576
+ let eip3009 = Eip3009Transfer {
577
+ chain_id: 1,
578
+ token: "0x3333333333333333333333333333333333333333"
579
+ .parse()
580
+ .expect("token"),
581
+ token_name: "USD Coin".to_string(),
582
+ token_version: Some("2".to_string()),
583
+ from: "0x4444444444444444444444444444444444444444"
584
+ .parse()
585
+ .expect("from"),
586
+ to: "0x5555555555555555555555555555555555555555"
587
+ .parse()
588
+ .expect("to"),
589
+ amount_wei: 456,
590
+ valid_after: 10,
591
+ valid_before: 20,
592
+ nonce_hex: format!("0x{}", hex::encode([0xabu8; 32])),
593
+ };
594
+
595
+ let permit_hash = AgentAction::Permit2Permit {
596
+ permit: permit.clone(),
597
+ }
598
+ .signing_hash()
599
+ .expect("permit2 hash")
600
+ .expect("permit2 typed hash");
601
+ let transfer_hash = AgentAction::Eip3009TransferWithAuthorization {
602
+ authorization: eip3009.clone(),
603
+ }
604
+ .signing_hash()
605
+ .expect("eip3009 transfer hash")
606
+ .expect("transfer typed hash");
607
+ let receive_hash = AgentAction::Eip3009ReceiveWithAuthorization {
608
+ authorization: eip3009,
609
+ }
610
+ .signing_hash()
611
+ .expect("eip3009 receive hash")
612
+ .expect("receive typed hash");
613
+
614
+ assert_ne!(permit_hash, [0u8; 32]);
615
+ assert_ne!(transfer_hash, [0u8; 32]);
616
+ assert_ne!(receive_hash, [0u8; 32]);
617
+ assert_ne!(transfer_hash, receive_hash);
618
+ }
619
+
620
+ #[test]
621
+ fn nonce_reservation_request_debug_redacts_agent_auth_token() {
622
+ let request = NonceReservationRequest {
623
+ request_id: Uuid::nil(),
624
+ agent_key_id: Uuid::nil(),
625
+ agent_auth_token: "super-secret-token".to_string(),
626
+ chain_id: 1,
627
+ min_nonce: 7,
628
+ requested_at: OffsetDateTime::UNIX_EPOCH,
629
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
630
+ };
631
+
632
+ let rendered = format!("{request:?}");
633
+ assert!(rendered.contains("<redacted>"));
634
+ assert!(!rendered.contains("super-secret-token"));
635
+ }
636
+
637
+ #[test]
638
+ fn nonce_release_request_debug_redacts_agent_auth_token() {
639
+ let request = NonceReleaseRequest {
640
+ request_id: Uuid::nil(),
641
+ agent_key_id: Uuid::nil(),
642
+ agent_auth_token: "super-secret-token".to_string(),
643
+ reservation_id: Uuid::nil(),
644
+ requested_at: OffsetDateTime::UNIX_EPOCH,
645
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
646
+ };
647
+
648
+ let rendered = format!("{request:?}");
649
+ assert!(rendered.contains("<redacted>"));
650
+ assert!(!rendered.contains("super-secret-token"));
651
+ }
@@ -0,0 +1,44 @@
1
+ use serde::{Deserialize, Deserializer, Serializer};
2
+
3
+ pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
4
+ where
5
+ S: Serializer,
6
+ {
7
+ serializer.serialize_str(&value.to_string())
8
+ }
9
+
10
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
11
+ where
12
+ D: Deserializer<'de>,
13
+ {
14
+ let value = String::deserialize(deserializer)?;
15
+ value.parse::<u128>().map_err(serde::de::Error::custom)
16
+ }
17
+
18
+ pub mod option {
19
+ use serde::{Deserialize, Deserializer, Serializer};
20
+
21
+ pub fn serialize<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
22
+ where
23
+ S: Serializer,
24
+ {
25
+ match value {
26
+ Some(value) => serializer.serialize_some(&value.to_string()),
27
+ None => serializer.serialize_none(),
28
+ }
29
+ }
30
+
31
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
32
+ where
33
+ D: Deserializer<'de>,
34
+ {
35
+ let value = Option::<String>::deserialize(deserializer)?;
36
+ match value {
37
+ Some(value) => value
38
+ .parse::<u128>()
39
+ .map(Some)
40
+ .map_err(serde::de::Error::custom),
41
+ None => Ok(None),
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "vault-policy"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ authors.workspace = true
7
+
8
+ [dependencies]
9
+ serde.workspace = true
10
+ thiserror.workspace = true
11
+ time.workspace = true
12
+ uuid.workspace = true
13
+ vault-domain = { path = "../vault-domain" }
14
+
15
+ [dev-dependencies]
16
+ alloy-primitives.workspace = true
17
+ alloy-sol-types.workspace = true